|
|
|
@ -52,6 +52,8 @@ interface State {
|
|
|
|
|
relationList: MemoRelation[];
|
|
|
|
|
location: Location | undefined;
|
|
|
|
|
isUploadingAttachment: boolean;
|
|
|
|
|
// Track in-flight uploads for UI progress bars
|
|
|
|
|
uploadTasks: { id: string; name: string; progress: number }[];
|
|
|
|
|
isRequesting: boolean;
|
|
|
|
|
isComposing: boolean;
|
|
|
|
|
isDraggingFile: boolean;
|
|
|
|
@ -68,6 +70,7 @@ const MemoEditor = observer((props: Props) => {
|
|
|
|
|
relationList: [],
|
|
|
|
|
location: undefined,
|
|
|
|
|
isUploadingAttachment: false,
|
|
|
|
|
uploadTasks: [],
|
|
|
|
|
isRequesting: false,
|
|
|
|
|
isComposing: false,
|
|
|
|
|
isDraggingFile: false,
|
|
|
|
@ -199,16 +202,52 @@ const MemoEditor = observer((props: Props) => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleUploadResource = async (file: File) => {
|
|
|
|
|
setState((state) => {
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
isUploadingAttachment: true,
|
|
|
|
|
};
|
|
|
|
|
const taskId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
|
|
|
// Add task entry
|
|
|
|
|
setState((prev) => ({ ...prev, uploadTasks: [...prev.uploadTasks, { id: taskId, name: file.name, progress: 0 }] }));
|
|
|
|
|
|
|
|
|
|
const setTaskProgress = (progress: number) => {
|
|
|
|
|
setState((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
uploadTasks: prev.uploadTasks.map((t) => (t.id === taskId ? { ...t, progress } : t)),
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
const removeTask = () => {
|
|
|
|
|
setState((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
uploadTasks: prev.uploadTasks.filter((t) => t.id !== taskId),
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Read file with progress (up to 30%)
|
|
|
|
|
const buffer = await new Promise<Uint8Array>((resolve, reject) => {
|
|
|
|
|
try {
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.onprogress = (e) => {
|
|
|
|
|
if (e.lengthComputable) {
|
|
|
|
|
const frac = e.loaded / e.total;
|
|
|
|
|
setTaskProgress(Math.min(0.3, frac * 0.3));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
reader.onerror = () => reject(reader.error ?? new Error("Failed to read file"));
|
|
|
|
|
reader.onload = () => {
|
|
|
|
|
setTaskProgress(0.3);
|
|
|
|
|
resolve(new Uint8Array(reader.result as ArrayBuffer));
|
|
|
|
|
};
|
|
|
|
|
reader.readAsArrayBuffer(file);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
reject(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { name: filename, size, type } = file;
|
|
|
|
|
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
|
|
|
// Simulate upload progress while the request is in-flight (30% -> 95%)
|
|
|
|
|
let current = 0.3;
|
|
|
|
|
const interval = window.setInterval(() => {
|
|
|
|
|
current = Math.min(0.95, current + 0.02);
|
|
|
|
|
setTaskProgress(current);
|
|
|
|
|
}, 200);
|
|
|
|
|
|
|
|
|
|
const { name: filename, size, type } = file;
|
|
|
|
|
try {
|
|
|
|
|
const attachment = await attachmentStore.createAttachment({
|
|
|
|
|
attachment: Attachment.fromPartial({
|
|
|
|
@ -219,26 +258,22 @@ const MemoEditor = observer((props: Props) => {
|
|
|
|
|
}),
|
|
|
|
|
attachmentId: "",
|
|
|
|
|
});
|
|
|
|
|
setState((state) => {
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
isUploadingAttachment: false,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
window.clearInterval(interval);
|
|
|
|
|
setTaskProgress(1);
|
|
|
|
|
// Remove task shortly after completion
|
|
|
|
|
window.setTimeout(removeTask, 500);
|
|
|
|
|
return attachment;
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
window.clearInterval(interval);
|
|
|
|
|
console.error(error);
|
|
|
|
|
toast.error(error.details);
|
|
|
|
|
setState((state) => {
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
isUploadingAttachment: false,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
toast.error(error.details ?? "Upload failed");
|
|
|
|
|
// Remove task on error as well
|
|
|
|
|
removeTask();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const uploadMultiFiles = async (files: FileList) => {
|
|
|
|
|
setState((prev) => ({ ...prev, isUploadingAttachment: true }));
|
|
|
|
|
const uploadedAttachmentList: Attachment[] = [];
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
const attachment = await handleUploadResource(file);
|
|
|
|
@ -261,6 +296,8 @@ const MemoEditor = observer((props: Props) => {
|
|
|
|
|
attachmentList: [...prevState.attachmentList, ...uploadedAttachmentList],
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
// If no more tasks in-flight, clear uploading flag
|
|
|
|
|
setState((prev) => ({ ...prev, isUploadingAttachment: false }));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDropEvent = async (event: React.DragEvent) => {
|
|
|
|
@ -478,6 +515,9 @@ const MemoEditor = observer((props: Props) => {
|
|
|
|
|
relationList,
|
|
|
|
|
}));
|
|
|
|
|
},
|
|
|
|
|
uploadFiles: async (files: FileList) => {
|
|
|
|
|
await uploadMultiFiles(files);
|
|
|
|
|
},
|
|
|
|
|
memoName,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
@ -497,6 +537,24 @@ const MemoEditor = observer((props: Props) => {
|
|
|
|
|
onCompositionEnd={handleCompositionEnd}
|
|
|
|
|
>
|
|
|
|
|
<Editor ref={editorRef} {...editorConfig} />
|
|
|
|
|
{state.uploadTasks.length > 0 && (
|
|
|
|
|
<div className="w-full mt-2 space-y-2">
|
|
|
|
|
{state.uploadTasks.map((task) => (
|
|
|
|
|
<div key={task.id} className="w-full">
|
|
|
|
|
<div className="flex items-center justify-between text-xs opacity-70 mb-1">
|
|
|
|
|
<span className="truncate max-w-[70%]">{task.name}</span>
|
|
|
|
|
<span className="tabular-nums">{Math.round(task.progress * 100)}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="w-full h-1.5 rounded bg-muted">
|
|
|
|
|
<div
|
|
|
|
|
className="h-1.5 rounded bg-primary transition-[width] duration-200"
|
|
|
|
|
style={{ width: `${Math.max(2, Math.round(task.progress * 100))}%` }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<AttachmentListView attachmentList={state.attachmentList} setAttachmentList={handleSetAttachmentList} />
|
|
|
|
|
<RelationListView relationList={referenceRelations} setRelationList={handleSetRelationList} />
|
|
|
|
|
<div className="relative w-full flex flex-row justify-between items-center py-1 gap-2" onFocus={(e) => e.stopPropagation()}>
|
|
|
|
|