feat(web): assign metadata curation permission
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user