refactor(admin): remove unused EditableRow and EditIcon components

- Delete EditableRow from formPrimitives and its usage in AdminFeaturesPanel
- Remove EditIcon from icon set as it is no longer referenced
- Simplify AdminFeaturesPanel state and draft handling
- Update AdminProvidersPanel to use distinct query key for default provider
- Ensure query invalidation covers new default provider key

This streamlines the admin codebase by eliminating obsolete editing UI and related icon, and clarifies provider query logic.
This commit is contained in:
Richard R 2026-05-30 10:33:11 -06:00
parent 267e2ce411
commit b0a4bccf4c
4 changed files with 87 additions and 178 deletions

View file

@ -7,7 +7,6 @@ import toast from 'react-hot-toast';
import { ChevronUpDownIcon, CheckIcon } from '@/components/icons/Icons'; import { ChevronUpDownIcon, CheckIcon } from '@/components/icons/Icons';
import { import {
Badge, Badge,
EditableRow,
Section, Section,
ToggleRow, ToggleRow,
buttonClass, buttonClass,
@ -57,7 +56,6 @@ export function AdminFeaturesPanel() {
}); });
const [draft, setDraft] = useState<Record<string, unknown>>({}); const [draft, setDraft] = useState<Record<string, unknown>>({});
const [dirty, setDirty] = useState<Set<string>>(new Set()); const [dirty, setDirty] = useState<Set<string>>(new Set());
const [editingChangelogUrl, setEditingChangelogUrl] = useState(false);
const { providers: sharedProviders } = useSharedProviders(); const { providers: sharedProviders } = useSharedProviders();
useEffect(() => { useEffect(() => {
@ -144,14 +142,10 @@ export function AdminFeaturesPanel() {
providerType: entry.providerType, providerType: entry.providerType,
})); }));
}, [sharedProviders]); }, [sharedProviders]);
const valueFor = (key: string) =>
Object.prototype.hasOwnProperty.call(draft, key)
? draft[key]
: data?.values?.[key];
const currentProviderId = const currentProviderId =
typeof valueFor('defaultTtsProvider') === 'string' typeof draft.defaultTtsProvider === 'string'
? (valueFor('defaultTtsProvider') as string) ? draft.defaultTtsProvider
: ''; : '';
const currentSharedEntry: SharedProviderEntry | undefined = sharedProviders.find( const currentSharedEntry: SharedProviderEntry | undefined = sharedProviders.find(
(p) => p.slug === currentProviderId, (p) => p.slug === currentProviderId,
@ -165,7 +159,7 @@ export function AdminFeaturesPanel() {
} as ProviderOption } as ProviderOption
: fallbackShared; : fallbackShared;
const selectedProviderOption = effectiveSelectedProvider; const selectedProviderOption = effectiveSelectedProvider;
const showTtsDailyLimitInputs = !Boolean(valueFor('disableTtsRateLimit')); const shouldRenderRateLimitInputs = draft.disableTtsRateLimit === false;
const handleProviderChange = (opt: ProviderOption) => { const handleProviderChange = (opt: ProviderOption) => {
updateDraft('defaultTtsProvider', opt.id); updateDraft('defaultTtsProvider', opt.id);
@ -256,7 +250,7 @@ export function AdminFeaturesPanel() {
<ToggleRow <ToggleRow
label="Restrict user API keys (recommended)" label="Restrict user API keys (recommended)"
description="Only allow admin shared providers." description="Only allow admin shared providers."
checked={Boolean(valueFor('restrictUserApiKeys'))} checked={Boolean(draft.restrictUserApiKeys)}
onChange={(checked) => { onChange={(checked) => {
if (!checked) { if (!checked) {
const ok = confirm( const ok = confirm(
@ -272,7 +266,7 @@ export function AdminFeaturesPanel() {
<ToggleRow <ToggleRow
label="Show TTS provider settings tab" label="Show TTS provider settings tab"
description="Allow per-user provider overrides." description="Allow per-user provider overrides."
checked={Boolean(valueFor('enableTtsProvidersTab'))} checked={Boolean(draft.enableTtsProvidersTab)}
onChange={(checked) => updateDraft('enableTtsProvidersTab', checked)} onChange={(checked) => updateDraft('enableTtsProvidersTab', checked)}
right={renderSource('enableTtsProvidersTab')} right={renderSource('enableTtsProvidersTab')}
variant="flat" variant="flat"
@ -280,7 +274,7 @@ export function AdminFeaturesPanel() {
<ToggleRow <ToggleRow
label="Show all provider models" label="Show all provider models"
description="Allow model selection beyond defaults." description="Allow model selection beyond defaults."
checked={Boolean(valueFor('showAllProviderModels'))} checked={Boolean(draft.showAllProviderModels)}
onChange={(checked) => updateDraft('showAllProviderModels', checked)} onChange={(checked) => updateDraft('showAllProviderModels', checked)}
right={renderSource('showAllProviderModels')} right={renderSource('showAllProviderModels')}
variant="flat" variant="flat"
@ -288,82 +282,71 @@ export function AdminFeaturesPanel() {
<ToggleRow <ToggleRow
label="Disable TTS daily rate limiting" label="Disable TTS daily rate limiting"
description="When on, per-user/IP daily character quotas are not enforced." description="When on, per-user/IP daily character quotas are not enforced."
checked={Boolean(valueFor('disableTtsRateLimit'))} checked={Boolean(draft.disableTtsRateLimit)}
onChange={(checked) => updateDraft('disableTtsRateLimit', checked)} onChange={(checked) => updateDraft('disableTtsRateLimit', checked)}
right={renderSource('disableTtsRateLimit')} right={renderSource('disableTtsRateLimit')}
variant="flat" variant="flat"
/> />
<Transition {shouldRenderRateLimitInputs ? (
as={Fragment} <div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 px-0.5 py-1.5">
show={showTtsDailyLimitInputs} <div className="space-y-1">
enter="transition-all duration-200 ease-out" <div className="flex items-center justify-between gap-2">
enterFrom="opacity-0 -translate-y-1 max-h-0" <label className="text-xs font-medium text-foreground">Anonymous per-user daily limit</label>
enterTo="opacity-100 translate-y-0 max-h-[420px]" {renderSource('ttsDailyLimitAnonymous')}
leave="transition-all duration-150 ease-in"
leaveFrom="opacity-100 translate-y-0 max-h-[420px]"
leaveTo="opacity-0 -translate-y-1 max-h-0"
>
<div className="overflow-hidden">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 px-0.5 py-1.5">
<div className="space-y-1">
<div className="flex items-center justify-between gap-2">
<label className="text-xs font-medium text-foreground">Anonymous per-user daily limit</label>
{renderSource('ttsDailyLimitAnonymous')}
</div>
<input
type="number"
min={1}
step={1}
className={inputClass}
value={String(valueFor('ttsDailyLimitAnonymous') ?? '')}
onChange={(event) => updatePositiveIntDraft('ttsDailyLimitAnonymous', event.target.value)}
/>
</div> </div>
<div className="space-y-1"> <input
<div className="flex items-center justify-between gap-2"> type="number"
<label className="text-xs font-medium text-foreground">Authenticated per-user daily limit</label> min={1}
{renderSource('ttsDailyLimitAuthenticated')} step={1}
</div> className={inputClass}
<input value={String(draft.ttsDailyLimitAnonymous ?? '')}
type="number" onChange={(event) => updatePositiveIntDraft('ttsDailyLimitAnonymous', event.target.value)}
min={1} />
step={1} </div>
className={inputClass} <div className="space-y-1">
value={String(valueFor('ttsDailyLimitAuthenticated') ?? '')} <div className="flex items-center justify-between gap-2">
onChange={(event) => updatePositiveIntDraft('ttsDailyLimitAuthenticated', event.target.value)} <label className="text-xs font-medium text-foreground">Authenticated per-user daily limit</label>
/> {renderSource('ttsDailyLimitAuthenticated')}
</div> </div>
<div className="space-y-1"> <input
<div className="flex items-center justify-between gap-2"> type="number"
<label className="text-xs font-medium text-foreground">Anonymous IP daily backstop</label> min={1}
{renderSource('ttsIpDailyLimitAnonymous')} step={1}
</div> className={inputClass}
<input value={String(draft.ttsDailyLimitAuthenticated ?? '')}
type="number" onChange={(event) => updatePositiveIntDraft('ttsDailyLimitAuthenticated', event.target.value)}
min={1} />
step={1} </div>
className={inputClass} <div className="space-y-1">
value={String(valueFor('ttsIpDailyLimitAnonymous') ?? '')} <div className="flex items-center justify-between gap-2">
onChange={(event) => updatePositiveIntDraft('ttsIpDailyLimitAnonymous', event.target.value)} <label className="text-xs font-medium text-foreground">Anonymous IP daily backstop</label>
/> {renderSource('ttsIpDailyLimitAnonymous')}
</div> </div>
<div className="space-y-1"> <input
<div className="flex items-center justify-between gap-2"> type="number"
<label className="text-xs font-medium text-foreground">Authenticated IP daily backstop</label> min={1}
{renderSource('ttsIpDailyLimitAuthenticated')} step={1}
</div> className={inputClass}
<input value={String(draft.ttsIpDailyLimitAnonymous ?? '')}
type="number" onChange={(event) => updatePositiveIntDraft('ttsIpDailyLimitAnonymous', event.target.value)}
min={1} />
step={1} </div>
className={inputClass} <div className="space-y-1">
value={String(valueFor('ttsIpDailyLimitAuthenticated') ?? '')} <div className="flex items-center justify-between gap-2">
onChange={(event) => updatePositiveIntDraft('ttsIpDailyLimitAuthenticated', event.target.value)} <label className="text-xs font-medium text-foreground">Authenticated IP daily backstop</label>
/> {renderSource('ttsIpDailyLimitAuthenticated')}
</div> </div>
<input
type="number"
min={1}
step={1}
className={inputClass}
value={String(draft.ttsIpDailyLimitAuthenticated ?? '')}
onChange={(event) => updatePositiveIntDraft('ttsIpDailyLimitAuthenticated', event.target.value)}
/>
</div> </div>
</div> </div>
</Transition> ) : null}
</Section> </Section>
<Section <Section
@ -371,27 +354,28 @@ export function AdminFeaturesPanel() {
subtitle="Feature flags for all users." subtitle="Feature flags for all users."
action={<Badge tone="foreground">Feature Flags</Badge>} action={<Badge tone="foreground">Feature Flags</Badge>}
> >
<EditableRow <div className="space-y-1.5 pb-2 border-b border-offbase">
label="Changelog feed URL" <div className="flex items-start justify-between gap-3">
description="Public URL to the changelog manifest JSON used by Settings." <div className="min-w-0">
value={String(valueFor('changelogFeedUrl') ?? '') || '—'} <p className="text-sm font-medium text-foreground">Changelog feed URL</p>
expanded={editingChangelogUrl} <p className="text-xs text-muted mt-0.5">
onExpandedChange={setEditingChangelogUrl} Public URL to the changelog manifest JSON used by Settings.
right={renderSource('changelogFeedUrl')} </p>
editLabel={editingChangelogUrl ? 'Close editor' : 'Edit URL'} </div>
> <div className="shrink-0">{renderSource('changelogFeedUrl')}</div>
</div>
<input <input
type="text" type="text"
className={inputClass} className={inputClass}
value={String(valueFor('changelogFeedUrl') ?? '')} value={String(draft.changelogFeedUrl ?? '')}
onChange={(event) => updateDraft('changelogFeedUrl', event.target.value)} onChange={(event) => updateDraft('changelogFeedUrl', event.target.value)}
placeholder="https://docs.openreader.richardr.dev/changelog/manifest.json" placeholder="https://docs.openreader.richardr.dev/changelog/manifest.json"
/> />
</EditableRow> </div>
<ToggleRow <ToggleRow
label="Allow new account sign-ups" label="Allow new account sign-ups"
description="When off, new accounts cannot be created. Existing accounts can still sign in." description="When off, new accounts cannot be created. Existing accounts can still sign in."
checked={Boolean(valueFor('enableUserSignups'))} checked={Boolean(draft.enableUserSignups)}
onChange={(checked) => updateDraft('enableUserSignups', checked)} onChange={(checked) => updateDraft('enableUserSignups', checked)}
right={renderSource('enableUserSignups')} right={renderSource('enableUserSignups')}
variant="flat" variant="flat"
@ -399,7 +383,7 @@ export function AdminFeaturesPanel() {
<ToggleRow <ToggleRow
label="Audiobook export" label="Audiobook export"
description='Show "Export audiobook" on PDF/EPUB pages.' description='Show "Export audiobook" on PDF/EPUB pages.'
checked={Boolean(valueFor('enableAudiobookExport'))} checked={Boolean(draft.enableAudiobookExport)}
onChange={(checked) => updateDraft('enableAudiobookExport', checked)} onChange={(checked) => updateDraft('enableAudiobookExport', checked)}
right={renderSource('enableAudiobookExport')} right={renderSource('enableAudiobookExport')}
variant="flat" variant="flat"
@ -407,7 +391,7 @@ export function AdminFeaturesPanel() {
<ToggleRow <ToggleRow
label="DOCX upload conversion" label="DOCX upload conversion"
description="Allow DOCX uploads (converted to PDF)." description="Allow DOCX uploads (converted to PDF)."
checked={Boolean(valueFor('enableDocxConversion'))} checked={Boolean(draft.enableDocxConversion)}
onChange={(checked) => updateDraft('enableDocxConversion', checked)} onChange={(checked) => updateDraft('enableDocxConversion', checked)}
right={renderSource('enableDocxConversion')} right={renderSource('enableDocxConversion')}
variant="flat" variant="flat"
@ -415,7 +399,7 @@ export function AdminFeaturesPanel() {
<ToggleRow <ToggleRow
label="Destructive delete buttons" label="Destructive delete buttons"
description='Show "Delete all data" actions (auth-off mode).' description='Show "Delete all data" actions (auth-off mode).'
checked={Boolean(valueFor('enableDestructiveDeleteActions'))} checked={Boolean(draft.enableDestructiveDeleteActions)}
onChange={(checked) => updateDraft('enableDestructiveDeleteActions', checked)} onChange={(checked) => updateDraft('enableDestructiveDeleteActions', checked)}
right={renderSource('enableDestructiveDeleteActions')} right={renderSource('enableDestructiveDeleteActions')}
variant="flat" variant="flat"

View file

@ -57,6 +57,7 @@ interface FormState {
const providerDefaultModel = defaultModelForProviderType; const providerDefaultModel = defaultModelForProviderType;
const ADMIN_PROVIDERS_QUERY_KEY = ['admin-providers'] as const; const ADMIN_PROVIDERS_QUERY_KEY = ['admin-providers'] as const;
const ADMIN_SETTINGS_QUERY_KEY = ['admin-settings'] as const; const ADMIN_SETTINGS_QUERY_KEY = ['admin-settings'] as const;
const ADMIN_DEFAULT_PROVIDER_QUERY_KEY = ['admin-settings', 'default-provider-slug'] as const;
async function fetchDefaultProviderSlug(): Promise<string> { async function fetchDefaultProviderSlug(): Promise<string> {
const res = await fetch('/api/admin/settings'); const res = await fetch('/api/admin/settings');
@ -160,7 +161,7 @@ export function AdminProvidersPanel() {
data: defaultProviderSlug = '', data: defaultProviderSlug = '',
error: defaultProviderError, error: defaultProviderError,
} = useQuery({ } = useQuery({
queryKey: ADMIN_SETTINGS_QUERY_KEY, queryKey: ADMIN_DEFAULT_PROVIDER_QUERY_KEY,
queryFn: fetchDefaultProviderSlug, queryFn: fetchDefaultProviderSlug,
}); });
@ -183,6 +184,7 @@ export function AdminProvidersPanel() {
await Promise.all([ await Promise.all([
queryClient.invalidateQueries({ queryKey: ADMIN_PROVIDERS_QUERY_KEY }), queryClient.invalidateQueries({ queryKey: ADMIN_PROVIDERS_QUERY_KEY }),
queryClient.invalidateQueries({ queryKey: ADMIN_SETTINGS_QUERY_KEY }), queryClient.invalidateQueries({ queryKey: ADMIN_SETTINGS_QUERY_KEY }),
queryClient.invalidateQueries({ queryKey: ADMIN_DEFAULT_PROVIDER_QUERY_KEY }),
]); ]);
}, },
onError: (mutationError) => { onError: (mutationError) => {
@ -198,6 +200,7 @@ export function AdminProvidersPanel() {
await Promise.all([ await Promise.all([
queryClient.invalidateQueries({ queryKey: ADMIN_PROVIDERS_QUERY_KEY }), queryClient.invalidateQueries({ queryKey: ADMIN_PROVIDERS_QUERY_KEY }),
queryClient.invalidateQueries({ queryKey: ADMIN_SETTINGS_QUERY_KEY }), queryClient.invalidateQueries({ queryKey: ADMIN_SETTINGS_QUERY_KEY }),
queryClient.invalidateQueries({ queryKey: ADMIN_DEFAULT_PROVIDER_QUERY_KEY }),
]); ]);
}, },
onError: (mutationError) => { onError: (mutationError) => {
@ -212,6 +215,7 @@ export function AdminProvidersPanel() {
await Promise.all([ await Promise.all([
queryClient.invalidateQueries({ queryKey: ADMIN_PROVIDERS_QUERY_KEY }), queryClient.invalidateQueries({ queryKey: ADMIN_PROVIDERS_QUERY_KEY }),
queryClient.invalidateQueries({ queryKey: ADMIN_SETTINGS_QUERY_KEY }), queryClient.invalidateQueries({ queryKey: ADMIN_SETTINGS_QUERY_KEY }),
queryClient.invalidateQueries({ queryKey: ADMIN_DEFAULT_PROVIDER_QUERY_KEY }),
]); ]);
}, },
onError: (mutationError) => { onError: (mutationError) => {
@ -223,7 +227,10 @@ export function AdminProvidersPanel() {
mutationFn: patchDefaultProviderSlug, mutationFn: patchDefaultProviderSlug,
onSuccess: async () => { onSuccess: async () => {
toast.success('Default provider updated'); toast.success('Default provider updated');
await queryClient.invalidateQueries({ queryKey: ADMIN_SETTINGS_QUERY_KEY }); await Promise.all([
queryClient.invalidateQueries({ queryKey: ADMIN_SETTINGS_QUERY_KEY }),
queryClient.invalidateQueries({ queryKey: ADMIN_DEFAULT_PROVIDER_QUERY_KEY }),
]);
}, },
onError: (mutationError) => { onError: (mutationError) => {
console.error('[AdminProvidersPanel] set default failed:', mutationError); console.error('[AdminProvidersPanel] set default failed:', mutationError);

View file

@ -1,8 +1,6 @@
'use client'; 'use client';
import { Fragment, useId, type ReactNode } from 'react'; import { useId, type ReactNode } from 'react';
import { Transition } from '@headlessui/react';
import { EditIcon } from '@/components/icons/Icons';
/** /**
* Shared compact form primitives used by settings-like surfaces across * Shared compact form primitives used by settings-like surfaces across
@ -241,65 +239,6 @@ export function ToggleRow({
); );
} }
export function EditableRow({
label,
description,
value,
expanded,
onExpandedChange,
right,
children,
editLabel = 'Edit',
}: {
label: string;
description: string;
value: ReactNode;
expanded: boolean;
onExpandedChange: (expanded: boolean) => void;
right?: ReactNode;
children: ReactNode;
editLabel?: string;
}) {
const rowClass =
'px-0.5 pt-1 pb-2 border-b border-offbase last:border-b-0 transition-transform duration-200 ease-out hover:scale-[1.003]';
return (
<div className={rowClass}>
<div className="flex items-start gap-2.5">
<div className="flex-1 min-w-0 space-y-0.5">
<span className="block text-sm font-medium leading-5 text-foreground">{label}</span>
<span className="block text-xs leading-4 text-muted">{description}</span>
<span className="block text-xs leading-5 text-foreground truncate">{value}</span>
</div>
{right ? <div className="shrink-0 self-start pl-1.5">{right}</div> : null}
<button
type="button"
onClick={() => onExpandedChange(!expanded)}
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-offbase bg-base text-muted transition-colors hover:bg-offbase hover:text-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-accent"
title={editLabel}
aria-label={editLabel}
aria-expanded={expanded}
>
<EditIcon className="h-3.5 w-3.5" />
</button>
</div>
<Transition
as={Fragment}
show={expanded}
enter="transition-all duration-200 ease-out"
enterFrom="opacity-0 -translate-y-1 max-h-0"
enterTo="opacity-100 translate-y-0 max-h-48"
leave="transition-all duration-150 ease-in"
leaveFrom="opacity-100 translate-y-0 max-h-48"
leaveTo="opacity-0 -translate-y-1 max-h-0"
>
<div className="overflow-hidden pt-2">
{children}
</div>
</Transition>
</div>
);
}
export function CheckItem({ export function CheckItem({
label, label,
checked, checked,

View file

@ -193,27 +193,6 @@ export function PlusIcon(props: React.SVGProps<SVGSVGElement>) {
); );
} }
export function EditIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={props.className}
width={props.width || '1.5em'}
height={props.height || '1.5em'}
{...props}
>
<path d="M12 20h9" />
<path d="M16.5 3.5a2.1 2.1 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5z" />
</svg>
);
}
export function UploadIcon(props: React.SVGProps<SVGSVGElement>) { export function UploadIcon(props: React.SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg