Consolidate doc-service settings to a single Save changes button
Lift state to page level, fire both upload-limits and watch-directory mutations from one button. Add noSaveButton and onChange props to PluginSchemaForm to support this pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,10 @@ interface PluginSchemaFormProps {
|
||||
isPending?: boolean;
|
||||
isError?: boolean;
|
||||
isSuccess?: boolean;
|
||||
/** When true, the built-in save button row is hidden (caller renders its own). */
|
||||
noSaveButton?: boolean;
|
||||
/** Expose current form state to the parent via callback on every change. */
|
||||
onChange?: (values: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
function Toggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
||||
@@ -48,6 +52,8 @@ export default function PluginSchemaForm({
|
||||
isPending,
|
||||
isError,
|
||||
isSuccess,
|
||||
noSaveButton,
|
||||
onChange,
|
||||
}: PluginSchemaFormProps) {
|
||||
const [form, setForm] = useState<Record<string, unknown>>(values);
|
||||
|
||||
@@ -56,7 +62,9 @@ export default function PluginSchemaForm({
|
||||
}, [values]);
|
||||
|
||||
const setField = (key: string, value: unknown) => {
|
||||
setForm((prev) => ({ ...prev, [key]: value }));
|
||||
const next = { ...form, [key]: value };
|
||||
setForm(next);
|
||||
onChange?.(next);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -103,17 +111,19 @@ export default function PluginSchemaForm({
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<Button onClick={() => onSave(form)} disabled={isPending} size="sm">
|
||||
{isPending ? "Saving…" : "Save changes"}
|
||||
</Button>
|
||||
{isError && (
|
||||
<span className="text-sm text-destructive">Failed to save. Please try again.</span>
|
||||
)}
|
||||
{isSuccess && !isPending && (
|
||||
<span className="text-sm text-green-600 dark:text-green-400">Saved successfully.</span>
|
||||
)}
|
||||
</div>
|
||||
{!noSaveButton && (
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<Button onClick={() => onSave(form)} disabled={isPending} size="sm">
|
||||
{isPending ? "Saving…" : "Save changes"}
|
||||
</Button>
|
||||
{isError && (
|
||||
<span className="text-sm text-destructive">Failed to save. Please try again.</span>
|
||||
)}
|
||||
{isSuccess && !isPending && (
|
||||
<span className="text-sm text-green-600 dark:text-green-400">Saved successfully.</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getDocumentLimits,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getPluginManifest,
|
||||
} from "../api/client";
|
||||
import PluginSchemaForm from "../components/PluginSchemaForm";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: 120,
|
||||
@@ -20,133 +21,130 @@ const inputStyle: React.CSSProperties = {
|
||||
color: "rgb(var(--color-text-primary))",
|
||||
};
|
||||
|
||||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
function Section({ title, description, children }: { title: string; description?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<section style={{ marginBottom: 36 }}>
|
||||
<h2 style={{ fontSize: 18, marginBottom: 16 }}>{title}</h2>
|
||||
<h2 style={{ fontSize: 18, marginBottom: description ? 8 : 16 }}>{title}</h2>
|
||||
{description && (
|
||||
<p style={{ fontSize: 13, color: "rgb(var(--color-text-muted))", marginBottom: 16 }}>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function UploadLimitsSection() {
|
||||
const { data: rawSettings, isLoading } = useQuery({
|
||||
export default function DocServiceSettingsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// ── Upload limits ────────────────────────────────────────────────────────────
|
||||
const { data: limitsData, isLoading: limitsLoading } = useQuery({
|
||||
queryKey: ["docLimits"],
|
||||
queryFn: getDocumentLimits,
|
||||
});
|
||||
|
||||
const [maxPdfMb, setMaxPdfMb] = useState(20);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rawSettings) return;
|
||||
const s = rawSettings as Record<string, unknown>;
|
||||
if (!limitsData) return;
|
||||
const s = limitsData as Record<string, unknown>;
|
||||
const docs = s.documents as Record<string, unknown> | undefined;
|
||||
if (typeof docs?.max_pdf_bytes === "number") {
|
||||
setMaxPdfMb(Math.round((docs.max_pdf_bytes as number) / (1024 * 1024)));
|
||||
}
|
||||
}, [rawSettings]);
|
||||
|
||||
const limitsMut = useMutation({
|
||||
mutationFn: (mb: number) => updateDocumentLimits(mb),
|
||||
});
|
||||
|
||||
if (isLoading) return <div>Loading…</div>;
|
||||
|
||||
return (
|
||||
<Section title="Upload Limits">
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "rgb(var(--color-text-muted))" }}>
|
||||
Max file size (MB)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={200}
|
||||
value={maxPdfMb}
|
||||
onChange={(e) => setMaxPdfMb(Number(e.target.value))}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => limitsMut.mutate(maxPdfMb)}
|
||||
disabled={limitsMut.isPending}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
background: "#222",
|
||||
color: "#fff",
|
||||
borderRadius: 4,
|
||||
border: "none",
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
{limitsMut.isPending ? "Saving…" : "Save"}
|
||||
</button>
|
||||
{limitsMut.isSuccess && (
|
||||
<p style={{ marginTop: 8, fontSize: 13, color: "#2a9d8f" }}>Limits saved.</p>
|
||||
)}
|
||||
{limitsMut.isError && (
|
||||
<p style={{ marginTop: 8, fontSize: 13, color: "#e76f51" }}>Failed to save.</p>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
function WatchDirectorySection() {
|
||||
const queryClient = useQueryClient();
|
||||
}, [limitsData]);
|
||||
|
||||
// ── Watch directory ──────────────────────────────────────────────────────────
|
||||
const { data: manifest, isLoading: manifestLoading } = useQuery({
|
||||
queryKey: ["plugin-manifest", "doc-service"],
|
||||
queryFn: () => getPluginManifest("doc-service"),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const { data: settingsValues, isLoading: settingsLoading } = useQuery({
|
||||
const { data: watchData, isLoading: watchLoading } = useQuery({
|
||||
queryKey: ["plugin-settings", "doc-service"],
|
||||
queryFn: () => getPluginSettings("doc-service"),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const updateMut = useMutation({
|
||||
// Ref so PluginSchemaForm can push its current values to us on change
|
||||
const watchValuesRef = useRef<Record<string, unknown>>({});
|
||||
useEffect(() => {
|
||||
if (watchData) watchValuesRef.current = watchData as Record<string, unknown>;
|
||||
}, [watchData]);
|
||||
|
||||
// ── Mutations ────────────────────────────────────────────────────────────────
|
||||
const limitsMut = useMutation({
|
||||
mutationFn: (mb: number) => updateDocumentLimits(mb),
|
||||
});
|
||||
const watchMut = useMutation({
|
||||
mutationFn: (values: Record<string, unknown>) => updatePluginSettings("doc-service", values),
|
||||
onSuccess: (data) => {
|
||||
queryClient.setQueryData(["plugin-settings", "doc-service"], data);
|
||||
},
|
||||
});
|
||||
|
||||
if (manifestLoading || settingsLoading) return <div>Loading…</div>;
|
||||
const isPending = limitsMut.isPending || watchMut.isPending;
|
||||
const isError = limitsMut.isError || watchMut.isError;
|
||||
const isSuccess = limitsMut.isSuccess && watchMut.isSuccess;
|
||||
|
||||
if (!manifest?.settings_schema || !settingsValues) {
|
||||
return (
|
||||
<div style={{ fontSize: 13, color: "rgb(var(--color-text-muted))" }}>
|
||||
Watch directory settings unavailable. Ensure the doc-service is running.
|
||||
</div>
|
||||
);
|
||||
const handleSave = () => {
|
||||
limitsMut.mutate(maxPdfMb);
|
||||
if (manifest?.settings_schema) {
|
||||
watchMut.mutate(watchValuesRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = limitsLoading || manifestLoading || watchLoading;
|
||||
|
||||
if (isLoading) {
|
||||
return <div style={{ padding: 32 }}>Loading…</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Section title="Watch Directory">
|
||||
<p style={{ fontSize: 13, color: "rgb(var(--color-text-muted))", marginBottom: 20 }}>
|
||||
Automatically ingest PDF files dropped into the watched directory. Subfolders are mapped to document categories.
|
||||
</p>
|
||||
<PluginSchemaForm
|
||||
schema={manifest.settings_schema}
|
||||
values={settingsValues as Record<string, unknown>}
|
||||
onSave={(values) => updateMut.mutate(values)}
|
||||
isPending={updateMut.isPending}
|
||||
isError={updateMut.isError}
|
||||
isSuccess={updateMut.isSuccess}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DocServiceSettingsPage() {
|
||||
return (
|
||||
<div style={{ padding: 32, maxWidth: 700, margin: "0 auto" }}>
|
||||
<h1 style={{ fontSize: 24, marginBottom: 32 }}>Documents — Settings</h1>
|
||||
<UploadLimitsSection />
|
||||
<WatchDirectorySection />
|
||||
|
||||
<Section title="Upload Limits">
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "rgb(var(--color-text-muted))" }}>
|
||||
Max file size (MB)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={200}
|
||||
value={maxPdfMb}
|
||||
onChange={(e) => setMaxPdfMb(Number(e.target.value))}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{manifest?.settings_schema && watchData ? (
|
||||
<Section
|
||||
title="Watch Directory"
|
||||
description="Automatically ingest PDF files dropped into the watched directory. Subfolders are mapped to document categories."
|
||||
>
|
||||
<PluginSchemaForm
|
||||
schema={manifest.settings_schema}
|
||||
values={watchData as Record<string, unknown>}
|
||||
onSave={() => {}}
|
||||
onChange={(values) => { watchValuesRef.current = values; }}
|
||||
noSaveButton
|
||||
/>
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<Button onClick={handleSave} disabled={isPending} size="sm">
|
||||
{isPending ? "Saving…" : "Save changes"}
|
||||
</Button>
|
||||
{isError && (
|
||||
<span className="text-sm text-destructive">Failed to save. Please try again.</span>
|
||||
)}
|
||||
{isSuccess && !isPending && (
|
||||
<span className="text-sm text-green-600 dark:text-green-400">Saved successfully.</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user