fix(spaces): offer a specific space album from every add-to-collection entry point (#965) - #970
Open
Deeds67 wants to merge 3 commits into
Open
fix(spaces): offer a specific space album from every add-to-collection entry point (#965)#970Deeds67 wants to merge 3 commits into
Deeds67 wants to merge 3 commits into
Conversation
…n entry point (#965) Adding a photo to an album inside a Shared Space depended on which screen you started from, and web and mobile disagreed about which screens worked. Web never listed space-linked albums at all: `CollectionPickerModal` loaded personal albums plus writable spaces and stopped there, so no web surface could target an album inside a space. A space row with linked albums now expands, on click, into an "Add to space" child plus one row per linked album — fetched lazily and cached for the life of the modal, so opening the picker still costs two requests however many spaces the user is in. The rows stay one flat keyboard-navigable sequence, and a space with no linked albums behaves exactly as before. Mobile had the right picker already but mounted it on only two surfaces; the rest still used upstream's album-only `AlbumSelector`. `CollectionPicker` is now wired into the album, favorites, archive and on-device-album sheets and into the asset viewer's "+". To make the viewer work it gained `source` (it dispatches as `ActionSource.viewer`) and `assets` — the viewer has no multiselect, and falling back to the empty one would read as "nothing non-owned" and offer space targets for someone else's photo. No server change: `Permission.AlbumAssetCreate` already grants a space Owner/Editor add-permission on every album linked to that space, so a space-linked album dispatches exactly like a personal one. The partner-detail sheet is deliberately left on the album-only selector — a partner's asset can never reach any space target, and web hides its "+" there for the same reason.
…usable (#965) Review of the first commit turned up a bug in the exact flow the issue is about, plus several smaller ones. Multi-selecting a space-linked album silently added nothing. `findByKey` resolved keys against `allCollections` only — personal albums plus spaces — but a space album owned by another member has no `album_user` row for the caller, so `getAllAlbums` never returns it and it lives only in the expand-time cache. The row ticked, the footer counted it, then `submitMulti` dropped it and called `onClose(undefined)`, which both wrappers read as "cancelled": modal closed, no request, no error. Keys now resolve against the cache too. The keyboard caret was cleared whenever a space opened or closed, so a keyboard user could reveal a space's albums and then had no way to arrow into them. It is now re-anchored on the space row itself — but only when the caret was already in use, so a mouse click does not acquire a selection highlight. The pool child's multi-select state was computed and asserted but never rendered. It is now a component alongside the other row types, with its own checkbox, the shared scroll-into-view action, and an indent that lines up with the album children. Also: collapsing and re-expanding while the first request was in flight fired a second identical one; the chevron was painted over by the hover checkbox; and on mobile `ref.invalidate` ran before the `context.mounted` guard, and the space albums' loading state was conflated with "this space has no albums", flashing that message on every expand — web already distinguished the two. Tests: each fix has a regression test, and each was confirmed to go red against the unfixed code. Mobile gained the two dispatch tests that were missing — the `source` pass-through on the album path, and `addToSpaceAlbum`, which is the literal subject of the issue and had no coverage at all. The design note's justification for skipping the mobile partner sheet was wrong: `Permission.AssetShare` is owner UNION partner, and web's partner route renders add-to-album unconditionally rather than via `getSelectionCapabilities`. The sheet is still skipped, but for the real reason — mobile's own `selectionHasNonOwned` rule is stricter than the server, and relaxing it is a change to every surface that rule governs. Recorded, with the other remaining web/mobile divergences, in the design note.
Three errors CI's Lint Web caught: two `unicorn/no-await-expression-member` from indexing straight into `await findAllByTestId(...)` in the new specs, and `svelte/prefer-svelte-reactivity` on the plain `Set` guarding in-flight space-album fetches. The Set is only a dedupe guard and nothing renders from it, but the rule forbids mutable built-in Sets in components, so it is now a `SvelteSet`. Worth noting for next time: `pnpm exec eslint` on the touched directories reported success here while these errors existed, so a local eslint pass on this tree cannot be trusted on its own.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #965.
The bug
Whether "add this photo to that Shared Space album" was possible depended on which screen you started from — and web and mobile disagreed about which screens worked.
+→ AlbumTwo independent causes:
CollectionPickerModalloads personal albums plus writable spaces and stops. The only place it listed a space's linked albums wasrestrictToSpaceIdmode, reached solely from a space surface with a non-owned selection ([bug] Contradictory notification when adding photo to album as non admin Editor ("Successful" but "File cannot be added") #764). So the target simply did not exist on any web surface.CollectionPickerwas wired intogeneral_bottom_sheetandspace_bottom_sheet; every other add-to-collection surface still mounted upstream's bareAlbumSelector.The fix
No server change.
Permission.AlbumAssetCreatealready has acheckSpaceLinkedAlbumAccessarm, so a space Owner/Editor holds add-permission on every album linked to that space. A space-linked album dispatches throughPOST /albums/:id/assetsexactly like a personal one.Web — a space row with
albumCount > 0becomes expandable and opens into an "Add to space" child plus one row per linked album, mirroring mobile'sSpaceCollectionSection. Albums are fetched lazily on expand and cached for the life of the modal, so opening the picker still costs two requests however many spaces you're in, and one space is open at a time. Children join the same flat keyboard-navigable row sequence. A space with no linked albums clicks straight through to the pool, exactly as before, andrestrictToSpaceIdmode is untouched.Mobile —
CollectionPickeris now mounted on the album, favorites, archive and on-device-album sheets, and on the asset viewer's+. It gainedsource(the viewer dispatches asActionSource.viewer),assets(the viewer has no multiselect, and falling back to the empty one would read as "nothing non-owned" and offer space targets for someone else's photo), andonCompleted(so the viewer keeps refreshing "Appears in" and popping its sheet now that the picker owns the dispatch). Each rewired surface also loses its bespokeaddToAlbumcopy.Second commit: fixes from review
A multi-agent review of the first commit found a bug in the exact flow this issue is about, plus several smaller ones. All are fixed in
af110f4, each with a regression test that was confirmed to go red against the unfixed code.findByKeyresolved keys against personal albums + spaces only — but a space album owned by another member has noalbum_userrow for the caller, sogetAllAlbumsnever returns it and it lives only in the expand-time cache. The row ticked, the footer counted it, then submit dropped it and calledonClose(undefined), which both wrappers read as cancelled. Keys now resolve against the cache too.ref.invalidateran before thecontext.mountedguard; and the space-albums loading state was conflated with "this space has no albums", flashing that message on every expand — web already distinguished the two.addToSpaceAlbum(the literal subject of this issue) had no coverage, and neither did thesourcepass-through on the album path — whose regression would make the asset viewer a silent no-op with a lying toast.Correction: why the mobile partner sheet is still skipped
The first version of this PR claimed a partner's asset "can never reach any space target" and that "web hides the
+entirely there". Both were wrong, and the code settles it:Permission.AssetShareis owner ∪ partner —access.ts:127-131unionscheckPartnerAccess. The space pool accepts a partner's assets.getSelectionCapabilities; it renders<ActionButton action={Actions.AddToAlbum} />unconditionally (partners/[userId]/…/+page.svelte:99).The sheet is still skipped, but for the real reason: mobile's own
selectionHasNonOwnedrule treats any non-owned asset as unreachable, so the Spaces section there would always be collapsed behind a notice — and that notice is itself stricter than the server. Relaxing the rule needs mobile to know which owners are partners, which changes every surface the rule already governs. Out of scope here; recorded as follow-up.One side effect worth flagging: viewing a partner's photo in the asset viewer now shows that notice where previously there was no Spaces section at all. Consistent with the timeline's existing behaviour, but it's the clearest surviving parity gap.
Other behaviour changes worth calling out
add_to_album_bottom_sheet_some_local_assetsis now an orphaned i18n key; the branch was already dead, since a favorites selection is remote-only.)Out of scope
MAX_SPACE_ASSETS_PER_REQUEST(50 000) still hide the whole spaces section, with the existing notice.Verification
Local: web 4363 unit tests,
check:typescript/check:svelte(586 files) /eslint/ prettier clean; mobile 3030 tests,dart analyze --fatal-infos lib testclean,dart formatclean on every touched file.Design notes:
docs/superpowers/specs/2026-08-10-965-space-album-add-parity-design.md.