feat(web): assign metadata curation permission

This commit is contained in:
Quick
2026-05-26 18:21:16 -04:00
parent 1a72e59254
commit d637ae7dbf
2 changed files with 80 additions and 1 deletions
+44 -1
View File
@@ -1,4 +1,4 @@
import { useMemo, useState } from "react";
import { useId, useMemo, useState } from "react";
import type { FormEvent } from "react";
import { useParams, Link } from "react-router";
import {
@@ -66,6 +66,7 @@ import {
playbackQualityValueFromPreset,
type PlaybackQualityPreset,
} from "@/lib/playback-quality";
import { PERMISSION_METADATA_CURATION } from "@/lib/permissions";
import { RegistrySettingControl } from "@/components/settings/RegistrySettingControl";
import { formatSettingValue, getSettingDefinition } from "@/lib/settingsManifest";
import {
@@ -275,6 +276,14 @@ function OverviewTab({ user }: { user: AdminUser }) {
</div>
<div className="divide-border divide-y">
<DetailRow label="Library Access" value={libraryNames} />
<DetailRow
label="Metadata Curation"
value={
hasAssignedPermission(user.permissions, PERMISSION_METADATA_CURATION)
? "Allowed"
: "Not allowed"
}
/>
<DetailRow
label="Max Playback Quality"
value={formatPlaybackQualityPreset(user.max_playback_quality)}
@@ -308,6 +317,20 @@ function DetailRow({ label, value }: { label: string; value: string }) {
);
}
function hasAssignedPermission(permissions: string[] | undefined, permission: string) {
return Array.isArray(permissions) && permissions.includes(permission);
}
function setAssignedPermission(permissions: string[], permission: string, enabled: boolean) {
const next = new Set(permissions);
if (enabled) {
next.add(permission);
} else {
next.delete(permission);
}
return Array.from(next).sort();
}
function ProfilesTab({ userId }: { userId: number }) {
const { data: profiles, isLoading } = useAdminUserProfiles(userId);
@@ -878,6 +901,7 @@ function EditUserForm({ user, onClose }: { user: AdminUser; onClose: () => void
const [password, setPassword] = useState("");
const [role, setRole] = useState(user.role);
const [enabled, setEnabled] = useState(user.enabled);
const [permissions, setPermissions] = useState<string[]>(user.permissions ?? []);
const [libraryIDs, setLibraryIDs] = useState<number[] | null>(user.library_ids);
const [maxStreams, setMaxStreams] = useState(user.max_streams);
const [maxTranscodes, setMaxTranscodes] = useState(user.max_transcodes);
@@ -889,6 +913,7 @@ function EditUserForm({ user, onClose }: { user: AdminUser; onClose: () => void
const [downloadTranscodeAllowed, setDownloadTranscodeAllowed] = useState(
user.download_transcode_allowed,
);
const metadataCurationId = useId();
const updateMutation = useUpdateUser();
function handleSubmit(e: FormEvent) {
@@ -897,6 +922,7 @@ function EditUserForm({ user, onClose }: { user: AdminUser; onClose: () => void
username,
email,
role,
permissions,
enabled,
library_ids: libraryIDs,
max_streams: maxStreams,
@@ -982,6 +1008,23 @@ function EditUserForm({ user, onClose }: { user: AdminUser; onClose: () => void
value={libraryIDs}
onChange={setLibraryIDs}
/>
<div className="border-border flex items-center justify-between rounded-md border px-3 py-2">
<div>
<Label htmlFor={metadataCurationId}>Metadata Curation</Label>
<p className="text-muted-foreground text-xs">
Edit, refresh, and rematch metadata within assigned libraries.
</p>
</div>
<Switch
id={metadataCurationId}
checked={hasAssignedPermission(permissions, PERMISSION_METADATA_CURATION)}
onCheckedChange={(checked) =>
setPermissions((current) =>
setAssignedPermission(current, PERMISSION_METADATA_CURATION, checked),
)
}
/>
</div>
<div className="grid gap-2 sm:grid-cols-2">
<div className="border-border flex items-center justify-between rounded-md border px-3 py-2">
<Label>Downloads Allowed</Label>
+36
View File
@@ -59,6 +59,7 @@ import {
playbackQualityValueFromPreset,
type PlaybackQualityPreset,
} from "@/lib/playback-quality";
import { PERMISSION_METADATA_CURATION } from "@/lib/permissions";
const PAGE_SIZE_OPTIONS = ["25", "50", "100"] as const;
type UserSortField = "username" | "email" | "role" | "enabled" | "created_at" | "last_active_at";
@@ -514,6 +515,20 @@ function formatRelativeTime(value?: string | null, fallback = "-") {
return fallback;
}
function hasAssignedPermission(permissions: string[] | undefined, permission: string) {
return Array.isArray(permissions) && permissions.includes(permission);
}
function setAssignedPermission(permissions: string[], permission: string, enabled: boolean) {
const next = new Set(permissions);
if (enabled) {
next.add(permission);
} else {
next.delete(permission);
}
return Array.from(next).sort();
}
function UserForm({ user, onClose }: { user: AdminUser | null; onClose: () => void }) {
const { data: settings } = useAdminServerSettings();
const { data: libraries = [] } = useAdminLibraries();
@@ -522,6 +537,7 @@ function UserForm({ user, onClose }: { user: AdminUser | null; onClose: () => vo
const [password, setPassword] = useState("");
const [role, setRole] = useState(user?.role ?? "user");
const [enabled, setEnabled] = useState(user?.enabled ?? true);
const [permissions, setPermissions] = useState<string[]>(user?.permissions ?? []);
const [libraryIDs, setLibraryIDs] = useState<number[] | null>(user?.library_ids ?? null);
const [maxStreams, setMaxStreams] = useState<number>(
user?.max_streams ?? Number(settings?.["defaults.max_streams"] ?? "6"),
@@ -549,6 +565,7 @@ function UserForm({ user, onClose }: { user: AdminUser | null; onClose: () => vo
const passwordId = useId();
const roleId = useId();
const enabledId = useId();
const metadataCurationId = useId();
const downloadAllowedId = useId();
const downloadTranscodeAllowedId = useId();
const maxStreamsId = useId();
@@ -566,6 +583,7 @@ function UserForm({ user, onClose }: { user: AdminUser | null; onClose: () => vo
username,
email,
role,
permissions,
enabled,
library_ids: libraryIDs,
max_streams: maxStreams,
@@ -583,6 +601,7 @@ function UserForm({ user, onClose }: { user: AdminUser | null; onClose: () => vo
email,
password,
role,
permissions,
create_default_profile: true,
max_streams: maxStreams,
max_transcodes: maxTranscodes,
@@ -682,6 +701,23 @@ function UserForm({ user, onClose }: { user: AdminUser | null; onClose: () => vo
value={libraryIDs}
onChange={setLibraryIDs}
/>
<div className="border-border flex items-center justify-between rounded-md border px-3 py-2">
<div>
<Label htmlFor={metadataCurationId}>Metadata Curation</Label>
<p className="text-muted-foreground text-xs">
Edit, refresh, and rematch metadata within assigned libraries.
</p>
</div>
<Switch
id={metadataCurationId}
checked={hasAssignedPermission(permissions, PERMISSION_METADATA_CURATION)}
onCheckedChange={(checked) =>
setPermissions((current) =>
setAssignedPermission(current, PERMISSION_METADATA_CURATION, checked),
)
}
/>
</div>
<div className="grid gap-2 sm:grid-cols-2">
<div className="border-border flex items-center justify-between rounded-md border px-3 py-2">
<Label htmlFor={downloadAllowedId}>Downloads Allowed</Label>