Skip to content

Merge silverlogic/baseapp-frontend#413: files module - #1

Open
nossila wants to merge 25 commits into
masterfrom
merge/baseapp-files
Open

nossila wants to merge 25 commits into
masterfrom
merge/baseapp-files

Conversation

@nossila

@nossila nossila commented Oct 3, 2026

Copy link
Copy Markdown
Member

Merges silverlogic#413 (files module: chunked multipart upload and attaching files to FilesInterface objects) into the fork. It pairs with queplanta/baseapp-backend#1, which merges the backend side (silverlogic/baseapp-backend#442). Both upstream PRs are still open. whatplant needs them now to store and show iNaturalist observation photos.

Commits

  1. --no-ff merge of upstream/feature/baseapp_files onto fork master (d650d09d). One conflict: master replaced the Cypress component tests with Playwright (Feature/playwright tests silverlogic/baseapp-frontend#427) and deleted ProfileMembers/__tests__/Members.cy.tsx, which feat(files): multipart upload module + attach files to comments silverlogic/baseapp-frontend#413 had edited. The deletion is kept; Members.pw.ts replaces that file.
  2. test: mock GraphQL image fields as URL strings. feat(files): multipart upload module + attach files to comments silverlogic/baseapp-frontend#413 changes image, bannerImage and avatar to plain String URLs. Mocks it didn't convert, or that master added after it branched (GroupChatEdit, Members.story), still returned { url } objects. REST user mocks (avatar: { small }) and React Native styles are unchanged.
  3. test(files): port the files module tests from Jest to Vitest. feat(files): multipart upload module + attach files to comments silverlogic/baseapp-frontend#413 predates the Vitest switch. Vitest only hoists vi.mock(), so its jest.mock() calls never took effect and 14 tests failed.

Test plan

🤖 Generated with Claude Code

https://claude.ai/code/session_019P1oyApi4BLS8iLfMEGm8b

nossila and others added 25 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
Master refactored the comments module into a shared hook layer (silverlogic#392) and
converted tailwind to MUI styled components (silverlogic#408), touching the same
components this branch extends with file attachments. Resolved by taking
master's structure and re-applying the attachment feature on top:

- CommentCreate/CommentUpdate/CommentItem rebuilt on master's
  useCommentCreateForm / useCommentUpdateForm / useCommentItem hooks and
  styled components, keeping the file upsert actions, UploadingFilesList
  and FileUploadList.
- useCommentCreateForm now surfaces the created comment id to onSuccess,
  which the deferred-attachment flow needs to attach uploads once the
  comment exists.

This branch makes thumbnails resolve to a plain String, so master's
`image/avatar { url }` selections and readers had to be adapted:
AddContactToGroupItem's fragment plus three consumers new in master.
Relay compiles and tsc is clean; the remaining lint output is
pre-existing warnings only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LkamMd6XMPeYNHbAV1DZ9
Conflict: master replaced the Cypress component tests with Playwright (silverlogic#427),
deleting ProfileMembers/__tests__/Members.cy.tsx, which silverlogic#413 had edited for the
string image contract. Kept the deletion; the mock updates follow separately.
silverlogic#413 moved image, bannerImage and avatar to plain String URLs, but mocks it
didn't touch, or that master added after it branched (GroupChatEdit,
Members.story), still returned { url } objects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P1oyApi4BLS8iLfMEGm8b
silverlogic#413 was written before master moved to Vitest. The jest = vi shim covers
jest.fn(), but Vitest only hoists vi.mock(), so the jest.mock() calls never
replaced axios or uploadChunks and 14 tests failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P1oyApi4BLS8iLfMEGm8b
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.

1 participant