feat(web): scope library views to per-user BasePath
PhotoPrism's user entity carries a per-user BasePath; the web app now mirrors that scope client-side so each user sees only their own subtree in the sidebar, timeline, folder counts, and heap-convert target picker. Admin without a BasePath is unchanged. Also removes the redundant "✕ <folder>" pill below the folder tree. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
type PpFolder
|
||||
} from '$lib/services/photoprism';
|
||||
import { filters, setSection } from '$lib/stores/filters.svelte';
|
||||
import { isAuthenticated } from '$lib/stores/session.svelte';
|
||||
import { isAuthenticated, toOriginalsPath } from '$lib/stores/session.svelte';
|
||||
import FolderTree, { buildTree } from './FolderTree.svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -95,10 +95,13 @@
|
||||
// pickedPath === '' is the root selection; falsy check would
|
||||
// wrongly block it. Distinguish `null` (nothing picked) from `''`.
|
||||
if (!heap || pickedPath === null) return;
|
||||
// pickedPath is user-relative (listFolders strips BasePath). The
|
||||
// sidecar moves files on disk so it needs a server-absolute path —
|
||||
// translate before submitting.
|
||||
convertMut.mutate({
|
||||
uid: heap.UID,
|
||||
body: {
|
||||
targetFolder: pickedPath,
|
||||
targetFolder: toOriginalsPath(pickedPath),
|
||||
mode,
|
||||
subfolder: subfolder.trim() || null,
|
||||
deleteHeap: mode === 'move' && deleteHeap
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
setSection,
|
||||
type Section
|
||||
} from '$lib/stores/filters.svelte';
|
||||
import { isAuthenticated, session } from '$lib/stores/session.svelte';
|
||||
import { isAuthenticated, session, userBasePath } from '$lib/stores/session.svelte';
|
||||
import FolderTree, { buildTree } from './FolderTree.svelte';
|
||||
import GeneralSettingsDialog from './GeneralSettingsDialog.svelte';
|
||||
import HeapConvertDialog from './HeapConvertDialog.svelte';
|
||||
@@ -158,15 +158,24 @@
|
||||
}));
|
||||
const folderCounts = $derived(folderCountsQuery.data ?? {});
|
||||
|
||||
// Root entry shows the whole library — see applyFolderScope() on /
|
||||
// timeline. PhotoPrism's `count.all` from /api/v1/config is the
|
||||
// authoritative library total (kept in sync server-side), so use it
|
||||
// directly. Earlier this subtracted Σ(folderCounts) from total, which
|
||||
// worked when folderCounts were direct-child only; now that the
|
||||
// sidecar fan-out recurses (see handlers_folders.go), every photo
|
||||
// gets summed once per ancestor — the subtraction would double-count
|
||||
// and drive rootCount to 0.
|
||||
const rootCount = $derived(configQuery.data?.count?.all ?? 0);
|
||||
// Root entry shows "the user's library" — for admins without a
|
||||
// BasePath that's still the whole library, served cheaply from
|
||||
// /api/v1/config's `count.all`. For any user with a non-empty
|
||||
// BasePath the precomputed total is wrong (it's library-wide), so we
|
||||
// ask the sidecar for a recursive count rooted at the user's
|
||||
// BasePath — listFolderCounts maps `""` through toOriginalsPath, which
|
||||
// resolves to the BasePath itself, and the sidecar fan-out recurses.
|
||||
const scopedRootCountQuery = createQuery<Record<string, number>>(() => ({
|
||||
queryKey: ['photos', 'root-count', userBasePath()],
|
||||
queryFn: () => listFolderCounts(['']),
|
||||
enabled: isAuthenticated() && userBasePath() !== '',
|
||||
staleTime: 60_000
|
||||
}));
|
||||
const rootCount = $derived(
|
||||
userBasePath() === ''
|
||||
? (configQuery.data?.count?.all ?? 0)
|
||||
: (scopedRootCountQuery.data?.[''] ?? 0)
|
||||
);
|
||||
|
||||
const createMut = createMutation(() => ({
|
||||
mutationFn: (title: string) => createHeap(title),
|
||||
@@ -565,18 +574,6 @@
|
||||
counts={folderCounts}
|
||||
/>
|
||||
{/if}
|
||||
{#if filters.folderPath && filters.folderPath !== '/'}
|
||||
<button
|
||||
class="mt-1 flex h-[22px] w-full items-center rounded px-2 text-[11px] leading-tight text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
onclick={() => {
|
||||
setFolderPath(null);
|
||||
void goto('/', { keepFocus: true, noScroll: true });
|
||||
}}
|
||||
title="Clear folder filter"
|
||||
>
|
||||
<span class="truncate">✕ {filters.folderPath}</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Views — everyday browse entries (section + route mixed) under
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
type PpLogEntry,
|
||||
type PpSettings
|
||||
} from '$lib/services/photoprism';
|
||||
import { userBasePath } from '$lib/stores/session.svelte';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -71,7 +72,16 @@
|
||||
}
|
||||
|
||||
// ── Index tab ─────────────────────────────────────────────────────────
|
||||
let indexForm = $state<IndexBody>({ path: '/', rescan: false, cleanup: false });
|
||||
// Default the reindex path to the user's BasePath when scoping is on,
|
||||
// so non-admins (and admins-with-BasePath) only rescan their own
|
||||
// subtree. PhotoPrism's /index expects originals-relative paths with
|
||||
// a leading slash; `'/'` means the whole library.
|
||||
const _bp = userBasePath();
|
||||
let indexForm = $state<IndexBody>({
|
||||
path: _bp === '' ? '/' : `/${_bp}`,
|
||||
rescan: false,
|
||||
cleanup: false
|
||||
});
|
||||
const startIndexMut = createMutation(() => ({
|
||||
mutationFn: (b: IndexBody) => startIndex(b),
|
||||
onSuccess: (r) => toast.success(r.message || 'Indexing complete'),
|
||||
|
||||
Reference in New Issue
Block a user