web: admin surfaces so the PhotoPrism UI is never needed
- Account tab in General settings — self-service password change. - UsersDialog (admin-only footer entry) — full /api/v1/users CRUD with admin-issued password reset. - People as a fifth tag category alongside Labels/Keywords/Colors/Ratings, backed by /api/v1/subjects and the `person:` DSL clause. - About tab in Library settings — version, library counts, feature chips, and a collapsible env-config help panel for the bits PP has no runtime API for (OIDC, TF, WebDAV). - Library tab expanded with Indexer-advanced, extra Downloads checksums, and a Features grid that only renders keys PhotoPrism actually returns. - Fix the SettingsDialog null-draft race the same way GeneralSettingsDialog already had: normalize on open, never null on close. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,7 @@
|
||||
import HeapConvertDialog from './HeapConvertDialog.svelte';
|
||||
import KebabMenu, { Item, Separator } from './KebabMenu.svelte';
|
||||
import SettingsDialog from './SettingsDialog.svelte';
|
||||
import UsersDialog from './UsersDialog.svelte';
|
||||
import {
|
||||
Copy,
|
||||
Download,
|
||||
@@ -63,7 +64,8 @@
|
||||
Pencil,
|
||||
Settings,
|
||||
Sun,
|
||||
Trash2
|
||||
Trash2,
|
||||
Users
|
||||
} from 'lucide-svelte';
|
||||
import { EmptyState, InlineLoader } from '$lib/components/feedback';
|
||||
|
||||
@@ -351,6 +353,10 @@
|
||||
// admin dialog above — opened from the bottom-of-sidebar footer.
|
||||
let generalSettingsOpen = $state(false);
|
||||
|
||||
// Admin-only user management dialog. Footer icon is gated on
|
||||
// `isAdminUser` so non-admins never see the entry point.
|
||||
let usersOpen = $state(false);
|
||||
|
||||
// Root-folder collapse state. Persisted to its own localStorage key so
|
||||
// it doesn't collide with FolderTree's per-subfolder openSet. Defaults
|
||||
// to open so first-time users see the full tree.
|
||||
@@ -385,6 +391,7 @@
|
||||
const TAG_CATEGORY_LABELS: Record<TagCategory, string> = {
|
||||
labels: 'Labels',
|
||||
keywords: 'Keywords',
|
||||
people: 'People',
|
||||
colors: 'Colors',
|
||||
ratings: 'Ratings'
|
||||
};
|
||||
@@ -399,6 +406,10 @@
|
||||
// Keywords sub-row's distinct-count semantics.
|
||||
if (cat === 'labels') return configQuery.data?.count?.labels;
|
||||
if (cat === 'keywords') return keywordsQuery.data?.length;
|
||||
// People follows the same distinct-count semantics as Labels —
|
||||
// `/api/v1/config.count.people` is the number of named subjects PP
|
||||
// has clustered, surfaced eagerly without a separate /subjects fetch.
|
||||
if (cat === 'people') return configQuery.data?.count?.people;
|
||||
if (cat === 'ratings') return ratingsCount;
|
||||
return colorsCount;
|
||||
}
|
||||
@@ -576,14 +587,15 @@
|
||||
];
|
||||
|
||||
// Total badge for the "Tags" header row. Rolls up labels + keywords +
|
||||
// ratings + colors. Labels flows through countPhotos (scoped); keywords/
|
||||
// ratings/colors are library-wide marks tables and only contribute when
|
||||
// we're in admin-without-BasePath mode (their sources don't scope).
|
||||
// people + ratings + colors. Labels flows through countPhotos (scoped);
|
||||
// keywords/people/ratings/colors are library-wide and only contribute
|
||||
// when we're in admin-without-BasePath mode (their sources don't scope).
|
||||
const tagsTotal = $derived.by<number | undefined>(() => {
|
||||
if (labelsBadge === undefined) return undefined;
|
||||
if (wantScoped) return labelsBadge;
|
||||
const keywords = keywordsQuery.data?.length ?? 0;
|
||||
return labelsBadge + keywords + ratingsCount + colorsCount;
|
||||
const people = configQuery.data?.count?.people ?? 0;
|
||||
return labelsBadge + keywords + people + ratingsCount + colorsCount;
|
||||
});
|
||||
|
||||
const manageViews: ViewItem[] = [
|
||||
@@ -1002,6 +1014,17 @@
|
||||
>
|
||||
<Settings class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{#if isAdminUser}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
onclick={() => (usersOpen = true)}
|
||||
title="Users"
|
||||
aria-label="Manage users"
|
||||
>
|
||||
<Users class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
@@ -1020,3 +1043,6 @@
|
||||
open={generalSettingsOpen}
|
||||
onClose={() => (generalSettingsOpen = false)}
|
||||
/>
|
||||
{#if isAdminUser}
|
||||
<UsersDialog open={usersOpen} onClose={() => (usersOpen = false)} />
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user