mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-02 18:13:27 +02:00
Text/code attachments (markdown, JSON, .ts, .log, …) need an attachment id
to render through `/api/attachments/{id}/content`. The composer pipeline
was dropping that id at the upload-hook boundary, so the Eye preview gate
only fired for media (PDF / video / audio via filename fallback).
- `useFileUpload` now returns the full `Attachment` (with `link` kept as a
`url` alias) so editor providers can resolve content-type and id.
- New-comment and reply composers hold a `pendingAttachments` state and
feed it to `ContentEditor`; the active subset (those still referenced in
the markdown) is sent on submit as before.
- Comment edit modes (CommentRow + CommentCardImpl) merge pending uploads
with `entry.attachments` for the editor and pipe `attachment_ids` into
`onEdit` so newly uploaded files actually bind to the comment.
- Issue description editor pushes pending `attachment_ids` on every
debounced save and invalidates `issueKeys.attachments` so the preview
Eye survives a refresh.
- `UpdateComment` and `UpdateIssue` handlers accept `attachment_ids` and
call the existing `linkAttachmentsByIDs` / `linkAttachmentsByIssueIDs`
helpers; the bind is idempotent so re-sending an existing id is safe.
Closes MUL-2153.
Co-authored-by: multica-agent <github@multica.ai>
61 lines
1.7 KiB
TypeScript
61 lines
1.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useCallback } from "react";
|
|
import type { ApiClient } from "../api/client";
|
|
import type { Attachment } from "../types";
|
|
import { MAX_FILE_SIZE } from "../constants/upload";
|
|
|
|
// Carries the full Attachment so editors that need preview metadata
|
|
// (`content_type`, `download_url`) get it directly; `link` is kept as an
|
|
// alias for `url` because many callers persist it into Markdown / avatar
|
|
// fields by that name.
|
|
export type UploadResult = Attachment & { link: string };
|
|
|
|
export interface UploadContext {
|
|
issueId?: string;
|
|
commentId?: string;
|
|
chatSessionId?: string;
|
|
}
|
|
|
|
export function useFileUpload(
|
|
api: ApiClient,
|
|
onError?: (error: Error) => void,
|
|
) {
|
|
const [uploading, setUploading] = useState(false);
|
|
|
|
const upload = useCallback(
|
|
async (file: File, ctx?: UploadContext): Promise<UploadResult | null> => {
|
|
if (file.size > MAX_FILE_SIZE) {
|
|
throw new Error("File exceeds 100 MB limit");
|
|
}
|
|
|
|
setUploading(true);
|
|
try {
|
|
const att: Attachment = await api.uploadFile(file, {
|
|
issueId: ctx?.issueId,
|
|
commentId: ctx?.commentId,
|
|
chatSessionId: ctx?.chatSessionId,
|
|
});
|
|
return { ...att, link: att.url };
|
|
} finally {
|
|
setUploading(false);
|
|
}
|
|
},
|
|
[api],
|
|
);
|
|
|
|
const uploadWithToast = useCallback(
|
|
async (file: File, ctx?: UploadContext): Promise<UploadResult | null> => {
|
|
try {
|
|
return await upload(file, ctx);
|
|
} catch (err) {
|
|
onError?.(err instanceof Error ? err : new Error("Upload failed"));
|
|
return null;
|
|
}
|
|
},
|
|
[upload, onError],
|
|
);
|
|
|
|
return { upload, uploadWithToast, uploading };
|
|
}
|