Skip to content

fix: show same-path re-uploads without a manual refresh#409

Merged
alukach merged 5 commits into
mainfrom
worktree-fix-reupload-refresh
Jul 1, 2026
Merged

fix: show same-path re-uploads without a manual refresh#409
alukach merged 5 commits into
mainfrom
worktree-fix-reupload-refresh

Conversation

@alukach

@alukach alukach commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Problem

Re-uploading a file to the exact same path in the object browser, without navigating away, stayed hidden until you navigated or refreshed the page.

Root cause

  • The object listing is fetched server-side once at page render and passed to DirectoryList as a static objects prop — nothing re-fetches it after an upload.
  • mergeUploadsWithFiles keys rows by filename. A re-upload to an existing path finds the existing server row and just spreads uploadProgress onto it, leaving the old size/updated_at/object in place. When it flips to completed the row looks identical to before — no new row, no changed metadata — so the re-upload is invisible.
  • Unlike the profile-image flow (a server action that calls revalidatePath), product uploads go client→S3 directly, so nothing invalidates the listing.

Fix

Edge-trigger router.refresh() in DirectoryList when this product scope's uploads drain (last uploading/queued → none). That re-runs the server component and re-lists S3 — the programmatic equivalent of the manual navigate/refresh that already worked. A same-path re-upload now updates its size/date/etag in place.

router.refresh() rather than revalidatePath because product uploads have no server action to hang revalidation on.

Not covered (separate layer)

File content URLs (fileSourceUrl, the external image-viewer iframe) are keyed by path only, so a re-uploaded image's bytes can still render from browser/CDN cache even after the listing updates. Fixing that means adding a ?v={etag|updated_at} cache-buster across file URLs + previews — left out of this PR.

Testing

  • tsc --noEmit passes clean.

🤖 Generated with Claude Code

Re-uploading a file to an existing path stayed hidden until you
navigated/refreshed: the object listing is fetched server-side once and
passed as a static prop, and mergeUploadsWithFiles keys rows by filename,
so a re-upload just overlays upload progress on the existing row without
updating its size/date. Edge-trigger router.refresh() when a product
scope's uploads drain to re-run the server component and re-list S3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
source-cooperative Ready Ready Preview, Comment Jul 1, 2026 6:09am

Request Review

@claude

claude Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Claude finished @alukach's task in 2m 31s —— View job


Reviewing PR #409

  • Gather context (diff, changed files)
  • Read DirectoryList.tsx and Dropzone.tsx changes in full
  • Analyze correctness, security, best-practice issues
  • Post final review

✅ No blocking issues — safe to merge.

Traced both fixes end-to-end against UploadProvider/upload-queue-manager.ts (to confirm upload.key semantics) and DirectoryRow.tsx (to confirm onDeleted fires with item.path only after the delete call resolves). Both mechanisms check out:

  • src/components/features/uploader/Dropzone.tsx:37-44 — the wasActive ref edge-triggers correctly: on mount or remount it just resyncs to current state (no spurious refresh), and only fires router.refresh() on the true → false transition of hasActiveUploads. Since Dropzone is mounted once in the persistent product layout (layout.tsx:92), the ref survives prefix navigation as intended.
  • src/components/features/products/object-browser/DirectoryList.tsx:58-70 — clearing deletedPaths keyed off scopedUploads keys is consistent with how mergeUploadsWithFiles sets path = upload.key for files uploaded directly into the current directory (utils.ts:60-73), so the un-hide correctly targets the same key space used for hiding.

Two non-blocking observations, already effectively acknowledged by the PR's scoping:

  • DirectoryList.tsx:63-70: the un-hide only matches on exact upload.key. If a directory was deleted (DirectoryRow.tsx:93,98 calls onDeleted(item.path) with the directory's placeholder path, e.g. "foo/") and the user then re-uploads files nested under that directory, the nested upload keys (e.g. "foo/bar.txt") never match the stored directory path, so the directory stays hidden until navigate/refresh. Same class of edge case as the file-content-caching gap the PR already calls out as "not covered" — worth a follow-up if it comes up in practice, not blocking here since the PR is explicitly scoped to same-path file re-uploads.
  • Dropzone.tsx:37-39: getUploadsForScope(scope) is called unmemoized on every render to derive hasActiveUploads; harmless given the cheap array scan and existing codebase patterns, just noting it's slightly more work than necessary.

alukach and others added 3 commits June 30, 2026 23:04
The refresh-on-drain effect was in DirectoryList, which the page renders
only for directory views — when the path resolves to a single file, page.tsx
returns the ObjectSummary/ObjectPreview branch and never mounts DirectoryList.
So re-uploading while viewing that exact file never triggered a refresh.

Move the effect into Dropzone, which lives in the product layout and is
mounted for both directory and file views. router.refresh() now re-runs the
server render (listing, file metadata, and server-fetched internal previews)
after either kind of re-upload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DirectoryList optimistically hides deleted paths in client state
(deletedPaths). That state survives router.refresh(), so re-uploading to a
just-deleted path stayed hidden until you navigated away (which resets the
set) — the exact case the prior ponytail comment flagged as unhandled.
Reconcile: when a scoped upload targets a hidden path, drop it from
deletedPaths so the re-created object shows again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
deletedPaths never holds "", so "".split("\n") -> [""] already no-ops
through the .some() guard. Remove the ternary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alukach alukach merged commit b878b68 into main Jul 1, 2026
6 checks passed
@alukach alukach deleted the worktree-fix-reupload-refresh branch July 1, 2026 06:09
alukach pushed a commit that referenced this pull request Jul 2, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.4.0](v1.3.0...v1.4.0)
(2026-07-01)


### ⚠ BREAKING CHANGES

* require auth for restricted products
([#284](#284))

### Features

* add INI-style credentials format tab
([2bcc3f1](2bcc3f1)),
closes
[#388](#388)
* add INI-style credentials format tab
([#389](#389))
([ebd4a1e](ebd4a1e))
* add X-Robots-Tag header for non-production deployments
([72249b9](72249b9)),
closes
[#302](#302)
* add X-Robots-Tag header for non-production deployments
([#323](#323))
([4fa9f9d](4fa9f9d))
* **admin-ui:** trust-policy sub-pattern preview on the federated
connection edit page
([#377](#377))
([ba82dc4](ba82dc4))
* **admin:** admin email-to-profile lookup view
([#350](#350))
([a056a28](a056a28))
* **admin:** data connection management (CRUD, S3/Azure/GCP/R2, product
mirrors)
([#364](#364))
([9ca1f53](9ca1f53))
* **authz:** let product owners view their own deactivated products
([#399](#399))
([09fd4d0](09fd4d0))
* **data-connection:** expose secret-less federated config; never return
stored secrets
([#376](#376))
([395b6c2](395b6c2))
* **data-connection:** federated backend authentication (AWS web
identity + GCP/Azure scaffold)
([#332](#332))
([999a076](999a076))
* **data-connections:** account-owned data connections
([#383](#383))
([b04755b](b04755b))
* **data-connections:** expose secret-less federated config by
connection visibility
([#327](#327))
([#339](#339))
([43f1f63](43f1f63))
* **data-connections:** link product-owned connections to admin form
([#392](#392))
([c6e8f10](c6e8f10))
* **data-connections:** require unsigned connections to be read-only
([#394](#394))
([c2af48b](c2af48b))
* delete product feature with confirmation modal
([#361](#361))
([0f68e47](0f68e47))
* **globe:** render live activity per datacenter
([#395](#395))
([d119935](d119935))
* **globe:** tidy live-activity popup, per-product count on hover
([#397](#397))
([1d92b1f](1d92b1f))
* **object-browser:** delete files and prefixes in edit mode
([#403](#403))
([2b1426b](2b1426b))
* OIDC auth
([#283](#283))
([c40ec89](c40ec89))
* **products:** choose a data connection and enforce its allowed
visibilities
([#338](#338))
([c40818f](c40818f))
* **products:** deactivate products — form toggle, admin-only viewing,
API 404s others
([#372](#372))
([77dc24b](77dc24b))
* **products:** mark deactivated products in account product list
([#385](#385))
([b8e6806](b8e6806))
* **profile:** link to Ory settings from read-only email field
([#349](#349))
([34a2244](34a2244))
* **scripts:** bulk-apply web-identity auth to opendata S3 connections
([#398](#398))
([45e4133](45e4133))
* **scripts:** migrate restricted open-data products to unlisted
([#343](#343))
([#371](#371))
([2977ee4](2977ee4))
* **uploads:** route in-browser uploads through the data proxy
([#391](#391))
([92f61ed](92f61ed))
* **viewer:** Add PDF viewer, PNG bindings
([#315](#315))
([7927431](7927431))
* **viewer:** support ndjson/jsonl
([3790a24](3790a24)),
closes
[#390](#390)


### Bug Fixes

* **api:** return 200 on successful member invite
([#381](#381))
([3be8e49](3be8e49))
* **auth:** log out sessions stuck at AAL1 when whoami requires AAL2
([#396](#396))
([b9af694](b9af694))
* **data-connection:** enforce provider↔auth pairing, tighten ARN + GCP
SA validation
([#368](#368))
([4780089](4780089))
* **data-connections:** admin form-reset, redirect, and prefix-template
handling
([#379](#379))
([963e7c8](963e7c8))
* **data-connections:** correct trust-policy aud to sts.amazonaws.com
([#382](#382))
([d53b894](d53b894))
* **email-verification:** reconcile Ory verification on every page load
([#347](#347))
([e0a85c3](e0a85c3))
* **forms:** opt out of React's auto form-reset so controlled fields
don't flash
([#373](#373))
([0eb033f](0eb033f))
* **globe:** fall back to static image when WebGL is disabled
([#387](#387))
([eb5c9a6](eb5c9a6))
* make full menu item area clickable
([#295](#295))
([e27a3c2](e27a3c2))
* **product:** hide edit buttons when data connection is read-only
([#353](#353))
([b57cfab](b57cfab))
* **products:** degrade gracefully when a data-proxy read fails
([#400](#400))
([3b5adb5](3b5adb5))
* re-render auth UI after form-submission redirects
([#344](#344))
([a378ebc](a378ebc))
* require auth for restricted products
([#284](#284))
([112265a](112265a))
* return user to current view after login
([#346](#346))
([9dd6d5f](9dd6d5f))
* show same-path re-uploads without a manual refresh
([#409](#409))
([b878b68](b878b68))
* **uploads:** refresh STS credentials mid-upload
([#401](#401))
([#402](#402))
([8da202d](8da202d))
* use [source-coop] as INI profile name
([c06a5ac](c06a5ac))
* validate product DOI format
([#319](#319))
([4727dec](4727dec))


### Performance Improvements

* **db:** request-scoped memoization of DynamoDB reads
([#320](#320))
([1f295cf](1f295cf))


### Miscellaneous Chores

* release 1.4.0
([810ff3c](810ff3c))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: source-release-bot[bot] <265100246+source-release-bot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant