Skip to content

feat(files): multipart upload module + attach files to comments - #413

Draft
nossila wants to merge 20 commits into
masterfrom
feature/baseapp_files
Draft

feat(files): multipart upload module + attach files to comments#413
nossila wants to merge 20 commits into
masterfrom
feature/baseapp_files

Conversation

@nossila

@nossila nossila commented Jul 23, 2026

Copy link
Copy Markdown
Member

Summary

Adds a files module (@baseapp-frontend/components/files) — multipart chunked upload plus attaching files to any FilesInterface-enabled object — and wires it into comments. Consumes the backend baseapp_files package (PR on silverlogic/baseapp-backend).

What's included

Files module (packages/components/modules/files)

  • Chunked multipart upload (useChunkedUpload): 5 MB chunks, concurrent parts, XHR progress, exponential-backoff retry, and in-session pause/resume/retry via AbortController. Backed by a zustand upload store.
  • Composable web components: 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.
  • Attach flows: useFileUploadLogic (upload → auto-attach to an existing target) and useDeferredFileAttachments (upload now, attach after the target is created).
  • Uploads are scoped to their owning composer so one composer's progress doesn't leak onto unrelated file lists (they share one global store).
  • Relay artifacts: FileAttachToTarget / FileDelete mutations, FilesList_target on FilesInterface, FileItem_file.
  • Native is a stub (files/native) for a later pass.

Comments integration

  • CommentUpdate: attach icon in the SocialInput action 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's FilesList_files connection already exists via the CommentItem_comment response, so the @prependEdge lands). Composer shows its own upload progress; completed items hide the progress bar and expose a remove control.

Schema / other

  • schema.graphql synced with backend (ContentPost implements CommentsInterface, files types) and image fields adapted to the String contract across profiles/messages/notifications.

Tests

  • Jest: chunkFile, uploadChunk/uploadChunks (retry/backoff/ETag), the upload store, useFileSelect, and pause/resume paths.
  • pnpm relay compiles per package against the committed schema; tsc --noEmit clean; eslint clean.

Status

Draft — depends on the backend baseapp_files PR. Opening for early review of the module API and comments wiring.

🤖 Generated with Claude Code

nossila and others added 20 commits June 27, 2026 01:19
…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
…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
@changeset-bot

changeset-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: dc95538

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ca99e9b-1b32-4a9b-b347-71e996626456

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/baseapp_files

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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)}`)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants