Skip to content

Commit 25a4d57

Browse files
CopilotTechQuery
andcommitted
feat: add file upload support to project evaluation page
- Add models/File.ts with FileModel class for uploading files via signed links - Add file attach button (left of input) triggering multi-file selector - Add paste/drop handlers on TextField to extract and upload files - Each uploaded file is sent as a separate ConsultMessage (markdown link/image) - Send button disabled during file or message upload - Add attach_files translation key to all 3 locale files Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
1 parent 21434c0 commit 25a4d57

File tree

5 files changed

+98
-3
lines changed

5 files changed

+98
-3
lines changed

models/File.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Base } from '@idea2app/data-server';
2+
import { toggle } from 'mobx-restful';
3+
4+
import { TableModel } from './Base';
5+
import userStore from './User';
6+
7+
interface SignedLink {
8+
putLink: string;
9+
getLink: string;
10+
}
11+
12+
export class FileModel extends TableModel<Base> {
13+
baseURI = 'file';
14+
client = userStore.client;
15+
16+
@toggle('uploading')
17+
async upload(file: File | Blob) {
18+
const name = file instanceof File ? file.name : crypto.randomUUID();
19+
20+
const { body } = await this.client.post<SignedLink>(`file/signed-link/${name}`);
21+
22+
await fetch(body!.putLink, {
23+
method: 'PUT',
24+
body: file,
25+
headers: { 'Content-Type': file.type },
26+
});
27+
28+
return body!.getLink;
29+
}
30+
}
31+
32+
export default new FileModel();

pages/dashboard/project/[id].tsx

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { ConsultMessage, User, UserRole } from '@idea2app/data-server';
2-
import { Avatar, Button, Container, Paper, TextField, Typography } from '@mui/material';
2+
import { Avatar, Button, Container, IconButton, Paper, TextField, Tooltip, Typography } from '@mui/material';
33
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 { FormEvent, KeyboardEventHandler } from 'react';
7+
import { ChangeEvent, ClipboardEvent, createRef, DragEvent, FormEvent, KeyboardEventHandler } from 'react';
88
import { formToJSON, scrollTo, sleep } from 'web-utility';
99

10+
import { SymbolIcon } from '../../../components/Icon';
1011
import { PageHead } from '../../../components/PageHead';
1112
import { EvaluationDisplay } from '../../../components/Project/EvaluationDisplay';
1213
import { ScrollList } from '../../../components/ScrollList';
1314
import { SessionBox } from '../../../components/User/SessionBox';
15+
import fileStore from '../../../models/File';
1416
import { ConsultMessageModel, ProjectModel } from '../../../models/ProjectEvaluation';
1517
import { i18n, I18nContext } from '../../../models/Translation';
1618

@@ -31,6 +33,8 @@ export default class ProjectEvaluationPage extends ObservedComponent<
3133

3234
messageStore = new ConsultMessageModel(this.projectId);
3335

36+
fileInputRef = createRef<HTMLInputElement>();
37+
3438
get menu() {
3539
const { t } = this.observedContext;
3640

@@ -76,6 +80,43 @@ export default class ProjectEvaluationPage extends ObservedComponent<
7680
);
7781
};
7882

83+
handleFiles = async (files: File[]) => {
84+
for (const file of files) {
85+
const url = await fileStore.upload(file);
86+
const content = file.type.startsWith('image/')
87+
? `![${file.name}](${url})`
88+
: `[${file.name}](${url})`;
89+
90+
await this.messageStore.updateOne({ content });
91+
}
92+
};
93+
94+
handleFileInputChange = async (event: ChangeEvent<HTMLInputElement>) => {
95+
const files = [...(event.target.files || [])];
96+
97+
event.target.value = '';
98+
99+
if (files.length > 0) await this.handleFiles(files);
100+
};
101+
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+
79120
renderChatMessage = (
80121
{ id, content, evaluation, prototypes, createdAt, createdBy }: ConsultMessage,
81122
index = 0,
@@ -175,6 +216,22 @@ export default class ProjectEvaluationPage extends ObservedComponent<
175216
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"
176217
onSubmit={this.handleMessageSubmit}
177218
>
219+
<input
220+
ref={this.fileInputRef}
221+
type="file"
222+
multiple
223+
className="hidden"
224+
onChange={this.handleFileInputChange}
225+
/>
226+
<Tooltip title={t('attach_files')}>
227+
<IconButton
228+
size="small"
229+
disabled={fileStore.uploading > 0 || messageStore.uploading > 0}
230+
onClick={() => this.fileInputRef.current?.click()}
231+
>
232+
<SymbolIcon name="attach_file" />
233+
</IconButton>
234+
</Tooltip>
178235
<TextField
179236
name="content"
180237
placeholder={t('type_your_message')}
@@ -185,12 +242,15 @@ export default class ProjectEvaluationPage extends ObservedComponent<
185242
size="small"
186243
required
187244
onKeyUp={this.handleQuickSubmit}
245+
onPaste={this.handlePasteDrop}
246+
onDragOver={e => e.preventDefault()}
247+
onDrop={this.handlePasteDrop}
188248
/>
189249
<Button
190250
type="submit"
191251
variant="contained"
192252
className="min-w-full px-2 whitespace-nowrap sm:min-w-0"
193-
disabled={messageStore.uploading > 0}
253+
disabled={fileStore.uploading > 0 || messageStore.uploading > 0}
194254
>
195255
{t('send')}
196256
</Button>

translation/en-US.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export default {
150150
type_your_message: 'Type your message...',
151151
send_message: 'Send Message',
152152
send: 'Send',
153+
attach_files: 'Attach Files',
153154

154155
// Prototype Generator
155156
generate_prototype: 'Generate Prototype',

translation/zh-CN.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export default {
146146
type_your_message: '输入您的消息……',
147147
send_message: '发送消息',
148148
send: '发送',
149+
attach_files: '上传文件',
149150

150151
// Prototype Generator
151152
generate_prototype: '生成原型',

translation/zh-TW.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export default {
145145
type_your_message: '輸入您的訊息……',
146146
send_message: '發送訊息',
147147
send: '發送',
148+
attach_files: '上傳檔案',
148149

149150
// Prototype Generator
150151
generate_prototype: '生成原型',

0 commit comments

Comments
 (0)