fix(ui): propagate photo tag edits to grid and viewer
Add a shared photo-updates signal the panel publishes on rename or tag add/remove; the album grid and viewer subscribe and patch the changed photo into their lists, so the 360 badge and panorama mode update without a reload.
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
import PhotoPanel from './PhotoPanel.svelte';
|
||||
import { albums } from '$lib/stores/albums.svelte';
|
||||
import { selection } from '$lib/stores/selection.svelte';
|
||||
import { photoUpdates } from '$lib/stores/photo-updates.svelte';
|
||||
import { transfer } from '$lib/stores/transfer.svelte';
|
||||
import type { Album, Photo } from '$lib/types';
|
||||
|
||||
@@ -52,6 +53,7 @@
|
||||
function onRenamed(updated: Photo) {
|
||||
photo = updated;
|
||||
if (selectedItems.some((p) => p.id === updated.id)) selection.replace(updated);
|
||||
photoUpdates.publish(updated);
|
||||
}
|
||||
|
||||
function onDeleted() {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Photo } from '$lib/types';
|
||||
|
||||
class PhotoUpdatesStore {
|
||||
latest = $state<Photo | null>(null);
|
||||
|
||||
publish(photo: Photo) {
|
||||
this.latest = photo;
|
||||
}
|
||||
}
|
||||
|
||||
export const photoUpdates = new PhotoUpdatesStore();
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { tick } from 'svelte';
|
||||
import { tick, untrack } from 'svelte';
|
||||
import { api, photosApi } from '$lib/api';
|
||||
import PhotoGrid from '$lib/components/PhotoGrid.svelte';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
@@ -12,6 +12,7 @@
|
||||
import { albums } from '$lib/stores/albums.svelte';
|
||||
import { density } from '$lib/stores/density.svelte';
|
||||
import { selection } from '$lib/stores/selection.svelte';
|
||||
import { photoUpdates } from '$lib/stores/photo-updates.svelte';
|
||||
import { viewerReturn } from '$lib/stores/viewer-return';
|
||||
import { transfer } from '$lib/stores/transfer.svelte';
|
||||
import { contextMenu, type ContextMenuItem } from '$lib/stores/context-menu.svelte';
|
||||
@@ -80,6 +81,18 @@
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
// Patch a photo edited in the panel (e.g. tags) into the grid list so
|
||||
// derived UI like the 360 badge updates without a reload. Read the list
|
||||
// untracked so writing it cannot re-trigger this effect.
|
||||
const updated = photoUpdates.latest;
|
||||
if (!updated) return;
|
||||
const list = untrack(() => photos);
|
||||
if (list.some((p) => p.id === updated.id)) {
|
||||
photos = list.map((p) => (p.id === updated.id ? updated : p));
|
||||
}
|
||||
});
|
||||
|
||||
let client: SSEClient | null = null;
|
||||
|
||||
$effect(() => {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { untrack } from 'svelte';
|
||||
import { api } from '$lib/api';
|
||||
import Viewer from '$lib/components/Viewer.svelte';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import { viewerActions } from '$lib/stores/viewer-actions.svelte';
|
||||
import { photoUpdates } from '$lib/stores/photo-updates.svelte';
|
||||
import { viewerReturn } from '$lib/stores/viewer-return';
|
||||
import type { Album, Photo } from '$lib/types';
|
||||
|
||||
@@ -40,6 +42,18 @@
|
||||
.finally(() => (loading = false));
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
// Patch a photo edited in the panel (e.g. tags) so the viewer reflects
|
||||
// changes such as panorama mode without reopening. Read the list
|
||||
// untracked so writing it cannot re-trigger this effect.
|
||||
const updated = photoUpdates.latest;
|
||||
if (!updated) return;
|
||||
const list = untrack(() => photos);
|
||||
if (list.some((p) => p.id === updated.id)) {
|
||||
photos = list.map((p) => (p.id === updated.id ? updated : p));
|
||||
}
|
||||
});
|
||||
|
||||
function close() {
|
||||
goto(`/albums/${albumId}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-11
|
||||
@@ -0,0 +1,73 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
See `proposal.md` - Why. Relevant current state:
|
||||
|
||||
- The photo panel lives in the app layout (`AlbumPanel.svelte`) and keeps its own copy of
|
||||
the photo; `PhotoPanel` reports edits through `onRenamed(updated)`.
|
||||
- Each route keeps its own list: the grid page and the viewer page both fetch
|
||||
`photos`, and `Viewer` additionally fetches a single `fresh` photo.
|
||||
- Renames reach the grid through the album SSE `photo.updated` event; `AddTag`/`RemoveTag`
|
||||
publish nothing and the panel's callback only touches panel-local state, so tags never
|
||||
reach the route lists.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- A tag change in the panel is reflected in the grid (360 badge) and the viewer
|
||||
(panorama mode) with no reload.
|
||||
- Keep the change local to the acting client so user-scoped tags are not mixed between
|
||||
users.
|
||||
|
||||
**Non-Goals:**
|
||||
- Replacing the SSE `photo.updated` path used for renames and variant readiness.
|
||||
- Reworking the photo list into a single shared per-album store.
|
||||
- Propagating tag changes across devices/users (tags remain user-scoped).
|
||||
|
||||
## Decisions
|
||||
|
||||
### Frontend-local signal instead of an SSE broadcast
|
||||
Add a small `photoUpdates` store that exposes the latest refreshed `Photo`. The panel
|
||||
publishes; the grid and viewer pages subscribe and patch their own lists by id.
|
||||
|
||||
- Alternative considered: publish `photo.updated` from `AddTag`/`RemoveTag` like
|
||||
`Rename`. Rejected - the SSE payload is built for the acting viewer and would carry
|
||||
that user's tags to every album member, overwriting their own tags in their grid.
|
||||
Local propagation keeps each client on its own tag set.
|
||||
|
||||
### One shared store, consumed by id patch
|
||||
`photoUpdates.latest` holds the most recent published photo; an `$effect` in each
|
||||
consumer maps over its `photos` and replaces the matching id. Only photos already loaded
|
||||
are affected (the panel only edits photos the user has open/selected), so no
|
||||
insert/remove logic is needed.
|
||||
|
||||
- Alternative considered: promote the album photo list into a shared store. Rejected -
|
||||
it touches loading, pagination and SSE handling for no behavior gain here.
|
||||
|
||||
### Publish on the existing `onRenamed` path
|
||||
`AlbumPanel.onRenamed` already receives the server-refreshed photo for tags and rename.
|
||||
It publishes that photo in addition to its local state and `selection.replace`. Renames
|
||||
also flow through SSE, so the extra patch is idempotent.
|
||||
|
||||
### Cover the viewer by patching its page list
|
||||
The viewer page passes its `photos` to `Viewer`, and `Viewer` derives `current` from it,
|
||||
so patching the page list makes the panorama mode switch live. `Viewer`'s internal
|
||||
`fresh` fetch is left as-is.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Two photos updated within one tick could drop one] -> User edits are sequential; keep
|
||||
the signal as "latest" and patching by id. Revisit with a map if batch edits appear.
|
||||
- [Grid list reassignment on patch] -> Only the changed id is replaced; Svelte keyed each
|
||||
keeps DOM stable.
|
||||
- [Panel state and route list diverge again] -> Single publish point in `onRenamed`
|
||||
covers tags and rename.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
No data or API change. Deploy frontend only. Rollback is reverting the deploy.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None blocking.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
After adding or removing a tag in the photo panel, the grid and viewer keep showing
|
||||
the stale photo: the `pano360` badge and panorama mode only appear after reloading the
|
||||
page. Tag edits are the one panel change that updates nothing outside the panel —
|
||||
`AddTag`/`RemoveTag` publish no event, and the panel's `onRenamed` only refreshes the
|
||||
panel's own copy, not the page-level photo lists.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Introduce a shared photo-updates signal that the panel publishes to and the grid and
|
||||
viewer pages subscribe to, so a changed photo (tags included) is patched into their
|
||||
local lists immediately.
|
||||
- Wire the photo panel to publish the refreshed photo on tag add/remove (and rename is
|
||||
covered by the same path, harmlessly).
|
||||
- Add a `ui-workspace` scenario: changing a photo's tags in the panel updates the grid
|
||||
badge and the panorama mode without a page reload.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
<!-- none -->
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `ui-workspace`: the «Метки в панели фото» requirement gains a no-reload propagation
|
||||
scenario.
|
||||
|
||||
## Impact
|
||||
|
||||
- Frontend: new `frontend/src/lib/stores/photo-updates.svelte.ts`;
|
||||
`frontend/src/lib/components/AlbumPanel.svelte` (publish);
|
||||
`frontend/src/routes/(app)/albums/[id]/+page.svelte` and
|
||||
`frontend/src/routes/(app)/albums/[id]/photos/[photoId]/+page.svelte` (subscribe+patch).
|
||||
- Spec: `openspec/specs/ui-workspace/spec.md` («Метки в панели фото»).
|
||||
@@ -0,0 +1,16 @@
|
||||
# Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Метки в панели фото
|
||||
Панель фото SHALL содержать раздел «Метки» с чипами меток фото и полем добавления, дающим
|
||||
поиск по меткам пользователя и создание новых. Изменение меток в панели SHALL сразу отражаться
|
||||
в сетке и в режиме просмотра без перезагрузки страницы.
|
||||
|
||||
#### Scenario: Раздел «Метки»
|
||||
- **WHEN** открыт просмотр или выбран один фото в сетке
|
||||
- **THEN** в панели есть раздел «Метки» с текущими метками и полем добавления с поиском
|
||||
|
||||
#### Scenario: Обновление без перезагрузки
|
||||
- **WHEN** пользователь добавляет или снимает метку в панели фото
|
||||
- **THEN** сетка и режим просмотра сразу отражают изменение (например, бейдж и режим 360-панорамы), без перезагрузки страницы
|
||||
@@ -0,0 +1,20 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Shared photo-updates store
|
||||
|
||||
- [x] 1.1 Add `frontend/src/lib/stores/photo-updates.svelte.ts` exposing `latest: Photo | null` and `publish(photo: Photo)`; verify `npm run check` passes.
|
||||
- [x] 1.2 In `frontend/src/lib/components/AlbumPanel.svelte`, call `photoUpdates.publish(updated)` from `onRenamed` (keeping the local `photo` assignment and `selection.replace`); verify the tag chips in the panel still update as before.
|
||||
|
||||
## 2. Route lists reflect the update
|
||||
|
||||
- [x] 2.1 In `frontend/src/routes/(app)/albums/[id]/+page.svelte`, add an effect that patches `photos` by id from `photoUpdates.latest`; verify that adding a `pano360` tag shows the 360 badge in the grid without reloading.
|
||||
- [x] 2.2 In `frontend/src/routes/(app)/albums/[id]/photos/[photoId]/+page.svelte`, add the same patch to its `photos`; verify the panorama mode switches on/off live after tagging while in the viewer.
|
||||
|
||||
## 3. Integration verification
|
||||
|
||||
- [x] 3.1 Run `npm run check` and `npm run build` (frontend); verify both pass with no regressions.
|
||||
|
||||
## Workflow follow-up
|
||||
|
||||
- Run `/opsx-apply` to implement this change.
|
||||
- Archive the change after review and verification (sync the delta spec into `ui-workspace`).
|
||||
Reference in New Issue
Block a user