Repository navigation
refactor(web): one person-scope module for personal vs space people - #1160
Merged
Merged
Conversation
global-person-route.ts now decides once which (spaceId, id) a person resolves to: spacePersonId inside a known space, else a space-person primaryProfile; a shared-space person row already is that pair. Every hand-built /shared-spaces/.../people/.../thumbnail URL goes through getSpacePersonThumbnailUrl / getGlobalPersonThumbnailUrl, and person.service.ts reuses the same getSpaceProfile for its writes. Replaces the per-site URL tests with one table-driven spec covering personal and space rows for id, thumbnail, hide, favorite and rename.
The page had its own hide and favorite handlers calling the owner-only updatePerson, safe only because the action menu is gated to personal people. Use the HidePerson / Favorite / Unfavorite actions from person.service.ts instead, so the page has one hide implementation. The canShowActions gating is unchanged.
- Action table checks Unfavorite too, adds favorited personal and space rows, and asserts the other endpoint is never called. - The write-routing comment moves from person.service.ts into the global-person-route.ts header, with a note on the profile id guard. - Drop a redundant updatedAt argument and wrap the merge selector's thumbnail callback so a future map() cannot pass an index as updatedAt.
Drop people-utils' positional getSpacePersonThumbnailUrl, added on main alongside this branch, and point SpacePersonSidePanel and SpaceFaceEditor at the person-scope module's helper. Same URL, one definition.
Deeds67
force-pushed
the
refactor/web-person-scope
branch
from
October 6, 2026 06:49
391b717 to
d367962
Compare
Deeds67
added a commit
that referenced
this pull request
Oct 6, 2026
…1160) * refactor(web): one person-scope module for space vs personal people global-person-route.ts now decides once which (spaceId, id) a person resolves to: spacePersonId inside a known space, else a space-person primaryProfile; a shared-space person row already is that pair. Every hand-built /shared-spaces/.../people/.../thumbnail URL goes through getSpacePersonThumbnailUrl / getGlobalPersonThumbnailUrl, and person.service.ts reuses the same getSpaceProfile for its writes. Replaces the per-site URL tests with one table-driven spec covering personal and space rows for id, thumbnail, hide, favorite and rename. * refactor(web): use the space-aware person actions on the People page The page had its own hide and favorite handlers calling the owner-only updatePerson, safe only because the action menu is gated to personal people. Use the HidePerson / Favorite / Unfavorite actions from person.service.ts instead, so the page has one hide implementation. The canShowActions gating is unchanged. * refactor(web): fold review into the person-scope module - Action table checks Unfavorite too, adds favorited personal and space rows, and asserts the other endpoint is never called. - The write-routing comment moves from person.service.ts into the global-person-route.ts header, with a note on the profile id guard. - Drop a redundant updatedAt argument and wrap the merge selector's thumbnail callback so a future map() cannot pass an index as updatedAt. * refactor(web): one space-person thumbnail helper Drop people-utils' positional getSpacePersonThumbnailUrl, added on main alongside this branch, and point SpacePersonSidePanel and SpaceFaceEditor at the person-scope module's helper. Same URL, one definition. --------- Co-authored-by: Pierre Marais <pierremarais67@gmail.com>
Deeds67
added a commit
that referenced
this pull request
Oct 6, 2026
…026-10-06 sync report Claude-Session: https://claude.ai/code/session_01XGvBrUkF5gBoiamBysaRbe
Deeds67
added a commit
that referenced
this pull request
Oct 6, 2026
…1160) * refactor(web): one person-scope module for space vs personal people global-person-route.ts now decides once which (spaceId, id) a person resolves to: spacePersonId inside a known space, else a space-person primaryProfile; a shared-space person row already is that pair. Every hand-built /shared-spaces/.../people/.../thumbnail URL goes through getSpacePersonThumbnailUrl / getGlobalPersonThumbnailUrl, and person.service.ts reuses the same getSpaceProfile for its writes. Replaces the per-site URL tests with one table-driven spec covering personal and space rows for id, thumbnail, hide, favorite and rename. * refactor(web): use the space-aware person actions on the People page The page had its own hide and favorite handlers calling the owner-only updatePerson, safe only because the action menu is gated to personal people. Use the HidePerson / Favorite / Unfavorite actions from person.service.ts instead, so the page has one hide implementation. The canShowActions gating is unchanged. * refactor(web): fold review into the person-scope module - Action table checks Unfavorite too, adds favorited personal and space rows, and asserts the other endpoint is never called. - The write-routing comment moves from person.service.ts into the global-person-route.ts header, with a note on the profile id guard. - Drop a redundant updatedAt argument and wrap the merge selector's thumbnail callback so a future map() cannot pass an index as updatedAt. * refactor(web): one space-person thumbnail helper Drop people-utils' positional getSpacePersonThumbnailUrl, added on main alongside this branch, and point SpacePersonSidePanel and SpaceFaceEditor at the person-scope module's helper. Same URL, one definition. --------- Co-authored-by: Pierre Marais <pierremarais67@gmail.com>
Deeds67
added a commit
that referenced
this pull request
Oct 6, 2026
…026-10-06 sync report Claude-Session: https://claude.ai/code/session_01XGvBrUkF5gBoiamBysaRbe
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.
Summary
The web app built
/shared-spaces/{spaceId}/people/{id}/thumbnailURLs by hand in 16 places. Those sites disagreed on which field holds the space person id:primaryProfile.id,spacePersonId, the raw rowid, orspaceId ?? space.id. This PR moves that decision intoweb/src/lib/utils/global-person-route.ts, and every thumbnail site and every write inperson.service.tsnow goes through it. It also removes the People page's second hide implementation. The page had its own hide and favorite handlers that called the owner-onlyupdatePersondirectly, and they were safe only because the action menu is shown for personal people alone. The page now uses the space-aware actions thatperson.service.tsalready had.The id rule
(spaceId, id)pair, returned bygetSpaceProfile(person, spaceId?).spacePersonId, but only when the caller passes the space it is viewing through. Today that is only the asset detail Info panel.space-personprimaryProfilethat has bothidandspaceId. This covers people lists and search, plus filter suggestions outside a space.SharedSpacePersonResponseDtorow already is the pair. ItsspaceIdis required by the API, so thespace.idfallbacks are gone.id.getPhotosPersonFilterThumbnailUrl(p, spaceId)owns this rule.primaryProfile?.id ?? id.Changes
global-person-route.ts(fork-only) hasgetSpaceProfile,getSpacePersonThumbnailUrl(row, updatedAt?)andgetGlobalPersonThumbnailUrl(person, { spaceId?, updatedAt? }), next to the existinggetGlobalPersonHref.person.service.ts(upstream) drops its privategetSpaceProfileand imports the shared one. Write routing is unchanged.photos-filter-options,map-filter-config,space-search,person-avatar. Components and routes:manage-space-people-visibility, the space timeline page, the space people page, the space person detail page (4 sites), and the person detail page (upstream).handleHidePersonandhandleToggleFavoriteare deleted. The menu rendersHidePerson,FavoriteandUnfavoritefromgetPersonActions. The list refreshes through the existingPersonUpdateevent.canShowActions={isPersonalPrimary}is unchanged, because the menu's merge option is personal-only.global-person-route.spec.tsis now one table-driven spec. It runs personal and space rows (each also withisFavorite: true) through the id and thumbnail URL, which menu actions are offered, and which endpoint hide and rename call. It also checks that the other endpoint is never called. Overlapping tests were removed fromperson.service.spec.tsandphotos-filter-options.spec.ts.Behaviour changes
favorite: false. A failed add therefore reads as a failed remove. The deleted page handler had the inverse mistake for removes. See follow-ups.getSpaceProfilerequiresprimaryProfile.id. Aspace-personprofile with no id, which is only possible through the loosely typed typed-search preview, now gets a personal/people/{id}/thumbnailURL instead of/shared-spaces/{spaceId}/people/undefined/thumbnail. Both 404; the new one avoids building a broken URL. This is intended.spaceIdfrom the row instead of the route, and the server guarantees the two match.Testing
Run from
web/after buildingpackages/sdk(pnpm build), which@immich/sdkimports need.pnpm test --run src/lib/utils/global-person-route.spec.ts: 16 passed.person.service,photos-filter-options,person-avatar,map-filter-config,space-search, People page, live typed filter section,src/lib/components/spaces, space people routes): 34 files, 664 tests passed.pnpm check:svelte: 640 files, 0 errors, 0 warnings.pnpm check:typescript: exit 0.npx eslint . --max-warnings 0 --concurrency 2: exit 0.pnpm lint(concurrency 6) crashed locally withAtomics.wait() failed: timed-outinside the tailwind ESLint plugin, on an untouched file. That is a load issue on this machine, not a lint finding, and lower concurrency passes.Follow-ups
isPersonalPrimary,isSpacePrimaryand the space-editability$effect. The same applies to the person detail page, and the same check appears inglobal-search-manager.svelte.ts. These were left alone to avoid colliding with the space-role PR, which rewrites the$effect. Once both land, they can callgetSpaceProfile.web/src/lib/utils/scoped-person-ref.ts(toScopedPersonRef,isSpaceScopedPerson) is a second module that answers "is this a space person", and it keeps aspaceId ?? fallbackSpaceIdfallback. It could be rebuilt ongetSpaceProfile.handleFavoritePersonandhandleUnfavoritePersoninperson.service.tsboth pass{ favorite: false }toerrors.unable_to_add_remove_favorites, so a failed add reads as a failed remove. This is identical in upstream Immich, so it is not patched here.Person.spaceIdrouting,image_url_builder.dart). Out of scope for this web PR.