feat(files): multipart upload module + attach files to comments - #413
feat(files): multipart upload module + attach files to comments#413nossila wants to merge 20 commits into
Conversation
…ntract
- regenerate packages/components/schema.graphql against the deployed ported backend
(image/avatar/bannerImage fields are now String scalars, not { url } objects)
- update fragments to select image fields as scalars; drop { url } subselections
- update components/utils to consume image fields as strings (coalesce null -> undefined
where passed to string|undefined props)
- regenerate Relay __generated__ artifacts
- rebased onto origin/master
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XyuGcRJryi9hQiWRVXU7Js
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd # Conflicts: # packages/components/modules/profiles/web/ProfileMembers/MemberItem/index.tsx # packages/components/schema.graphql
Master's InviteMemberDialog queried image { url }; on this branch Profile.image
is a String scalar, so select it directly and use it as avatarUrl.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
README claimed a FileUploadProvider JSX wrapper; the store is a module-level zustand singleton, so no provider exists. Documented the shared-state implication for multiple targets and fixed the edges.map any cast. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
- Record ETags per chunk index as chunks complete (new recordChunkEtag store action); completedChunks was chunkIndex+1, which under 3-way parallel uploads mis-counted on out-of-order completion, and etags were only stored at the very end — a paused upload resumed with wrong slices and empty parts. - Resume now uploads exactly the chunks with missing ETags (non-contiguous safe) against their original presigned URLs. - Track initiatedAt/expiresIn from the initiate response; resuming past expiry aborts the stale upload (best effort) and transparently re-initiates. - Extract shared startUpload used by upload/retry/expired-resume. - Drop parentContentType/parentObjectId from initiate payloads — the API accepts parent_id only, so the fields were silently ignored; attaching is done via fileAttachToTarget. - Select id on FilesList file nodes (was masked by the removed any cast); remove leftover merge-conflict marker in ProfileMembers MemberItem. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
…paths chunkFile slicing, uploadChunks (out-of-order ETag indexing, retry/backoff, abort short-circuit), the upload store (recordChunkEtag sparse counting, pause/resume/retry transitions), and useChunkedUpload (initiate/complete payloads, sparse resume, expired-URL re-initiation, failed-state handling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
…ume works Two bugs made in-session pause/resume unreachable from the UI (the jest tests exercised resumeUpload directly on a hand-crafted PAUSED state, masking both): - The catch in upload/resume/retry set status=FAILED on any thrown error, including the AbortController rejection a pause triggers — clobbering the PAUSED the store just set and firing onUploadError for a deliberate pause. A shared handleUploadError now leaves PAUSED/ABORTED untouched. - UploadingFileItem.handleResume called the store's resumeFile (PAUSED→PENDING) before resumeUpload, whose guard requires PAUSED — so resume no-op'd and the file stranded in PENDING. Removed the redundant call; the hook flips it. - resumeUpload now computes pending chunks before treating URL expiry as fatal: if every chunk already uploaded, it only re-issues the (URL-free) complete call instead of discarding the whole upload and re-uploading from byte zero. Added regression tests for both fixed behaviors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
…App design) Decouple the three concerns so the pieces can be mounted to match the Figma comment design while keeping today's behaviour as defaults: - useFileSelect: headless hidden-input select (accept/size/count validation), so the trigger is pluggable. - FileUploadTrigger: owns the upload logic and renders as a dropzone (default) or a compact paperclip button (as="button"). FileUpload stays as a back-compat dropzone wrapper. - FileThumbnail + FileChip: presentational 40x40 thumbnail (image or type-icon tile) and the compact bordered chip from the design. - UploadingFileItem / AttachedFileItem gain variant='chip'; AttachedFileItem gains editable (× remove on files the user can change). - FileUploadList gains variant (cards|chips), layout (stack|horizontal), and editable — cards/stack remain the defaults so nothing changes for existing callers. Wire comments to the design: CommentItem renders the chip list (editable while editing); CommentUpdate renders the paperclip trigger button. Tests: useFileSelect unit coverage; full components suite green (37). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
…ion bar
- AttachedFileItem and FileUploadTrigger now use the design-system IconButton
(which renders a spinner via isLoading) instead of raw MUI: the remove/delete
action shows isLoading={isDeletingFile}, the attach button isLoading while
attaching.
- Wire the comment editor's attach icon: SocialInput forwards a new optional
SocialUpsertActionsProps to its SocialUpsertActions slot, and CommentUpdate
passes a module-level CommentFilesUpsertActions (target-bound) whose
AttachmentIcon is the real FileUploadTrigger. Removes the standalone attach
button that used to sit below the input.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
- The attached-file preview used file.thumbnail, which the backend leaves null when easy_thumbnails can't generate one, and downloads used the raw `file` field (a host-less /media path the frontend origin can't serve). Query the absolute `url` field and use it: preview falls back to thumbnail || url so images always render, and download opens `url`. - FileChip max-width 264 -> 232px to match the design. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
Previously the uploading item decoded the source File (via createObjectURL) into an image preview. Even for a 40px tile the browser decodes the full-res bitmap, which is wasteful for large images. Show a generic type icon during upload; the real thumbnail arrives when FileAttachToTarget prependEdges the committed File node. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
Adds file attachment to CommentCreate, mirroring CommentUpdate. Because the comment does not exist at selection time, files upload immediately and their relay ids are held by a new useDeferredFileAttachments hook; on successful create they are attached to the new comment in one FileAttachToTarget mutation (the comment's FilesList_files connection already exists via the CommentItem response, so the prependEdge lands). The attach icon lives in a create-specific SocialUpsertActions bar and shows a loading state while uploads are in flight. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
The upload store is a global singleton, so every FileUploadList (one per comment) was rendering all in-flight uploads — a file attached during comment creation appeared on existing comments. Tag each upload with a scope: the auto-attach path scopes to its target id; the deferred (create) path scopes to a per-composer id. FileUploadList now shows only uploads matching its target, and CommentCreate renders a new target-less UploadingFilesList for its own scope. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
In a composer (e.g. new comment) an uploaded-but-not-yet-attached file kept showing its progress bar and a pause control, with no way to undo it. Once COMPLETED the chip now shows the file size instead of the bar (card hides FileProgress) and offers a remove control. Removing works correctly because the deferred hook now reads the completed uploads for its scope straight from the store at attach time (fileRelayId is persisted on completion), so a removed file is excluded from the attach; cleanup is scoped to the composer instead of clearing all completed uploads globally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
| files: new Map(), | ||
|
|
||
| addFile: (file: File, scope?: string) => { | ||
| const id = `${Date.now()}-${file.name}-${Math.random()}` |
| const inFlightRef = useRef(0) | ||
| // Stable per-instance scope so this composer's uploads are shown here and not | ||
| // on unrelated file lists (they share one global upload store). | ||
| const scopeRef = useRef(`deferred-${Math.random().toString(36).slice(2)}`) |




Summary
Adds a files module (
@baseapp-frontend/components/files) — multipart chunked upload plus attaching files to anyFilesInterface-enabled object — and wires it into comments. Consumes the backendbaseapp_filespackage (PR onsilverlogic/baseapp-backend).What's included
Files module (
packages/components/modules/files)useChunkedUpload): 5 MB chunks, concurrent parts, XHR progress, exponential-backoff retry, and in-session pause/resume/retry viaAbortController. Backed by a zustand upload store.FileUploadTrigger(dropzone or compact icon button),FileUploadList/UploadingFilesList,AttachedFileItem,FileChip,FileThumbnail— split so consumers can mount just the pieces they need and reproduce the BaseApp Figma design.useFileUploadLogic(upload → auto-attach to an existing target) anduseDeferredFileAttachments(upload now, attach after the target is created).FileAttachToTarget/FileDeletemutations,FilesList_targetonFilesInterface,FileItem_file.files/native) for a later pass.Comments integration
CommentUpdate: attach icon in theSocialInputaction bar; files auto-attach to the existing comment.CommentCreate: attach icon uploads immediately, then files attach to the new comment on successful create (the comment'sFilesList_filesconnection already exists via theCommentItem_commentresponse, so the@prependEdgelands). Composer shows its own upload progress; completed items hide the progress bar and expose a remove control.Schema / other
schema.graphqlsynced with backend (ContentPost implements CommentsInterface, files types) and image fields adapted to theStringcontract across profiles/messages/notifications.Tests
chunkFile,uploadChunk/uploadChunks(retry/backoff/ETag), the upload store,useFileSelect, and pause/resume paths.pnpm relaycompiles per package against the committed schema;tsc --noEmitclean; eslint clean.Status
Draft — depends on the backend
baseapp_filesPR. Opening for early review of the module API and comments wiring.🤖 Generated with Claude Code