Skip to content

Commit 42f2789

Browse files
CopilotTechQuery
andcommitted
refactor: PasteDropBox component and MUI label-based file upload button
- Extract paste/drop handling into standalone PasteDropBox component that reads text/html, text/plain, and files from DataTransfer and fires separate onHTML, onText, onFiles callbacks - Replace hidden input + ref + onClick with MUI component="label" pattern on IconButton (sr-only input inside the button label) - Wrap TextField with PasteDropBox in project evaluation page Co-authored-by: TechQuery <[email protected]>
1 parent 25a4d57 commit 42f2789

2 files changed

Lines changed: 83 additions & 43 deletions

File tree

components/PasteDropBox.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { ClipboardEvent, DragEvent, FC, PropsWithChildren } from 'react';
2+
3+
export interface PasteDropBoxProps {
4+
className?: string;
5+
onText?: (text: string) => void;
6+
onHTML?: (html: string) => void;
7+
onFiles?: (files: File[]) => void;
8+
}
9+
10+
export const PasteDropBox: FC<PropsWithChildren<PasteDropBoxProps>> = ({
11+
children,
12+
className,
13+
onFiles,
14+
onHTML,
15+
onText,
16+
}) => {
17+
const handlePasteDrop = async (event: ClipboardEvent | DragEvent) => {
18+
const items =
19+
event.type === 'paste'
20+
? [...(event as ClipboardEvent).clipboardData.items]
21+
: [...(event as DragEvent).dataTransfer.items];
22+
23+
const files = items
24+
.filter(item => item.kind === 'file')
25+
.map(item => item.getAsFile())
26+
.filter((file): file is File => file !== null);
27+
28+
if (files.length > 0) {
29+
event.preventDefault();
30+
onFiles?.(files);
31+
32+
return;
33+
}
34+
35+
const htmlItem = items.find(({ type }) => type === 'text/html');
36+
const plainItem = items.find(({ type }) => type === 'text/plain');
37+
38+
if (htmlItem && onHTML) {
39+
const html = await new Promise<string>(resolve => htmlItem.getAsString(resolve));
40+
41+
event.preventDefault();
42+
onHTML(html);
43+
} else if (plainItem && onText) {
44+
const text = await new Promise<string>(resolve => plainItem.getAsString(resolve));
45+
46+
event.preventDefault();
47+
onText(text);
48+
}
49+
};
50+
51+
return (
52+
<div
53+
className={className}
54+
onDragOver={e => e.preventDefault()}
55+
onDrop={handlePasteDrop}
56+
onPaste={handlePasteDrop}
57+
>
58+
{children}
59+
</div>
60+
);
61+
};

pages/dashboard/project/[id].tsx

Lines changed: 22 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import { marked } from 'marked';
44
import { observer } from 'mobx-react';
55
import { ObservedComponent, reaction } from 'mobx-react-helper';
66
import { compose, JWTProps, jwtVerifier, RouteProps, router } from 'next-ssr-middleware';
7-
import { ChangeEvent, ClipboardEvent, createRef, DragEvent, FormEvent, KeyboardEventHandler } from 'react';
7+
import { ChangeEvent, FormEvent, KeyboardEventHandler } from 'react';
88
import { formToJSON, scrollTo, sleep } from 'web-utility';
99

1010
import { SymbolIcon } from '../../../components/Icon';
1111
import { PageHead } from '../../../components/PageHead';
12+
import { PasteDropBox } from '../../../components/PasteDropBox';
1213
import { EvaluationDisplay } from '../../../components/Project/EvaluationDisplay';
1314
import { ScrollList } from '../../../components/ScrollList';
1415
import { SessionBox } from '../../../components/User/SessionBox';
@@ -33,8 +34,6 @@ export default class ProjectEvaluationPage extends ObservedComponent<
3334

3435
messageStore = new ConsultMessageModel(this.projectId);
3536

36-
fileInputRef = createRef<HTMLInputElement>();
37-
3837
get menu() {
3938
const { t } = this.observedContext;
4039

@@ -99,24 +98,6 @@ export default class ProjectEvaluationPage extends ObservedComponent<
9998
if (files.length > 0) await this.handleFiles(files);
10099
};
101100

102-
handlePasteDrop = async (event: ClipboardEvent | DragEvent) => {
103-
const items =
104-
event.type === 'paste'
105-
? [...(event as ClipboardEvent).clipboardData.items]
106-
: [...(event as DragEvent).dataTransfer.items];
107-
108-
const files = items
109-
.filter(item => item.kind === 'file')
110-
.map(item => item.getAsFile())
111-
.filter((file): file is File => file !== null);
112-
113-
if (files.length > 0) {
114-
event.preventDefault();
115-
116-
await this.handleFiles(files);
117-
}
118-
};
119-
120101
renderChatMessage = (
121102
{ id, content, evaluation, prototypes, createdAt, createdBy }: ConsultMessage,
122103
index = 0,
@@ -216,36 +197,34 @@ export default class ProjectEvaluationPage extends ObservedComponent<
216197
className="sticky bottom-0 mx-1 mt-auto mb-1 flex items-end gap-2 p-1.5 sm:mx-0 sm:mb-0 sm:p-2"
217198
onSubmit={this.handleMessageSubmit}
218199
>
219-
<input
220-
ref={this.fileInputRef}
221-
type="file"
222-
multiple
223-
className="hidden"
224-
onChange={this.handleFileInputChange}
225-
/>
226200
<Tooltip title={t('attach_files')}>
227201
<IconButton
202+
component="label"
228203
size="small"
229204
disabled={fileStore.uploading > 0 || messageStore.uploading > 0}
230-
onClick={() => this.fileInputRef.current?.click()}
231205
>
232206
<SymbolIcon name="attach_file" />
207+
<input
208+
type="file"
209+
multiple
210+
className="sr-only"
211+
onChange={this.handleFileInputChange}
212+
/>
233213
</IconButton>
234214
</Tooltip>
235-
<TextField
236-
name="content"
237-
placeholder={t('type_your_message')}
238-
multiline
239-
maxRows={4}
240-
fullWidth
241-
variant="outlined"
242-
size="small"
243-
required
244-
onKeyUp={this.handleQuickSubmit}
245-
onPaste={this.handlePasteDrop}
246-
onDragOver={e => e.preventDefault()}
247-
onDrop={this.handlePasteDrop}
248-
/>
215+
<PasteDropBox className="flex-1 min-w-0" onFiles={this.handleFiles}>
216+
<TextField
217+
name="content"
218+
placeholder={t('type_your_message')}
219+
multiline
220+
maxRows={4}
221+
fullWidth
222+
variant="outlined"
223+
size="small"
224+
required
225+
onKeyUp={this.handleQuickSubmit}
226+
/>
227+
</PasteDropBox>
249228
<Button
250229
type="submit"
251230
variant="contained"

0 commit comments

Comments
 (0)