Add generic plugin architecture and watch-directory feature
Introduces a manifest contract so feature containers self-describe their settings (JSON Schema + access rules). Backend and frontend gain generic plugin proxy and dynamic Extensions UI with zero feature-specific code. Doc-service is the first plugin consumer: exposes /plugin/manifest and /plugin/settings, adds a watchdog-based file watcher that auto-ingests PDFs from a mounted directory, maps subfolders to categories, supports AI-suggested folder/filename (user-confirmed), and enforces a no-remove policy. Access is gated by is_superuser or doc-service-admin group. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { getPluginManifest, getPluginSettings, updatePluginSettings } from "@/api/client";
|
||||
import PluginSchemaForm from "@/components/PluginSchemaForm";
|
||||
|
||||
export default function PluginSettingsPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: manifest, isLoading: manifestLoading, isError: manifestError } = useQuery({
|
||||
queryKey: ["plugin-manifest", id],
|
||||
queryFn: () => getPluginManifest(id!),
|
||||
enabled: !!id,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const { data: settings, isLoading: settingsLoading } = useQuery({
|
||||
queryKey: ["plugin-settings", id],
|
||||
queryFn: () => getPluginSettings(id!),
|
||||
enabled: !!id && !!manifest,
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (values: Record<string, unknown>) => updatePluginSettings(id!, values),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["plugin-settings", id] });
|
||||
},
|
||||
});
|
||||
|
||||
if (manifestLoading || settingsLoading) {
|
||||
return <p className="text-sm text-muted p-6">Loading…</p>;
|
||||
}
|
||||
|
||||
if (manifestError || !manifest) {
|
||||
return (
|
||||
<p className="text-sm text-destructive p-6">
|
||||
Plugin not found or you do not have access to its settings.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl p-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-foreground">
|
||||
{manifest.settings_schema.title ?? manifest.name}
|
||||
</h1>
|
||||
<p className="text-sm text-muted mt-1">
|
||||
{manifest.name} · v{manifest.version}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<PluginSchemaForm
|
||||
schema={manifest.settings_schema}
|
||||
values={settings ?? {}}
|
||||
onSave={(values) => mutation.mutate(values)}
|
||||
isPending={mutation.isPending}
|
||||
isError={mutation.isError}
|
||||
isSuccess={mutation.isSuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user