diff --git a/apps/web/src/components/plugins/PluginSettingsSection.tsx b/apps/web/src/components/plugins/PluginSettingsSection.tsx index ada90f85..9d4c346e 100644 --- a/apps/web/src/components/plugins/PluginSettingsSection.tsx +++ b/apps/web/src/components/plugins/PluginSettingsSection.tsx @@ -6,9 +6,11 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { groupPluginSettingFields } from "./plugin-settings-layout"; const PluginSettingFieldRow = ({ configuredSecret, + compact = false, disabled, field, inputId, @@ -16,6 +18,7 @@ const PluginSettingFieldRow = ({ value, }: { configuredSecret: boolean; + compact?: boolean; disabled: boolean; field: PluginSettingField; inputId: string; @@ -32,7 +35,9 @@ const PluginSettingFieldRow = ({ ); return ( -
+
{label} {field.description ?

{field.description}

: null} @@ -92,6 +97,7 @@ export const PluginSettingsSection = ({ host, manifest }: { host: EdgeEverPlugin const { t } = useTranslation(); const formId = useId(); const fields = manifest.settings?.fields ?? []; + const fieldGroups = groupPluginSettingFields(fields); const [values, setValues] = useState>({}); const [configuredSecrets, setConfiguredSecrets] = useState>({}); const [loading, setLoading] = useState(fields.length > 0); @@ -179,25 +185,35 @@ export const PluginSettingsSection = ({ host, manifest }: { host: EdgeEverPlugin ) : (
{ event.preventDefault(); void save(); }}>
-
- {fields.map((field) => { - const value = values[field.key] ?? ""; - const inputId = `${formId}-${field.key}`; - return ( - { - clearFeedback(); - setValues((current) => ({ ...current, [field.key]: nextValue })); - }} - /> - ); - })} +
+ {fieldGroups.map((group, groupIndex) => ( +
0 ? " mt-5 border-t border-slate-100 pt-5" : ""}` + : groupIndex > 0 ? "border-t border-slate-100" : ""} + > + {group.fields.map((field) => { + const value = values[field.key] ?? ""; + const inputId = `${formId}-${field.key}`; + return ( + { + clearFeedback(); + setValues((current) => ({ ...current, [field.key]: nextValue })); + }} + /> + ); + })} +
+ ))}
{message ? {message} : null} diff --git a/apps/web/src/components/plugins/plugin-settings-layout.test.mjs b/apps/web/src/components/plugins/plugin-settings-layout.test.mjs new file mode 100644 index 00000000..3e1430f0 --- /dev/null +++ b/apps/web/src/components/plugins/plugin-settings-layout.test.mjs @@ -0,0 +1,32 @@ +import { describe, expect, test } from "bun:test"; +import { groupPluginSettingFields } from "./plugin-settings-layout.ts"; + +describe("plugin settings layout", () => { + test("groups consecutive topic switches without absorbing unrelated switches", () => { + const fields = [ + { key: "topics.ai", type: "boolean", label: "AI" }, + { key: "topics.engineering", type: "boolean", label: "Engineering" }, + { key: "topics.science", type: "boolean", label: "Science" }, + { key: "translation.auto-enabled", type: "boolean", label: "Translate" }, + { key: "translation.target-language", type: "select", label: "Language", options: [{ value: "en", label: "English" }] }, + { key: "digest.auto-enabled", type: "boolean", label: "Digest" }, + ]; + + const groups = groupPluginSettingFields(fields); + expect(groups.map((group) => ({ compact: group.compact, keys: group.fields.map((field) => field.key) }))).toEqual([ + { compact: true, keys: ["topics.ai", "topics.engineering", "topics.science"] }, + { compact: false, keys: ["translation.auto-enabled"] }, + { compact: false, keys: ["translation.target-language"] }, + { compact: false, keys: ["digest.auto-enabled"] }, + ]); + }); + + test("keeps short boolean runs in the standard full-width layout", () => { + const fields = [ + { key: "alerts.email", type: "boolean", label: "Email" }, + { key: "alerts.push", type: "boolean", label: "Push" }, + ]; + + expect(groupPluginSettingFields(fields).every((group) => !group.compact)).toBe(true); + }); +}); diff --git a/apps/web/src/components/plugins/plugin-settings-layout.ts b/apps/web/src/components/plugins/plugin-settings-layout.ts new file mode 100644 index 00000000..99db918c --- /dev/null +++ b/apps/web/src/components/plugins/plugin-settings-layout.ts @@ -0,0 +1,30 @@ +import type { PluginSettingField } from "@edgeever/plugin-api"; + +export interface PluginSettingFieldGroup { + id: string; + compact: boolean; + fields: PluginSettingField[]; +} + +const fieldNamespace = (field: PluginSettingField) => field.key.split(".", 1)[0] ?? field.key; + +export const groupPluginSettingFields = (fields: PluginSettingField[]): PluginSettingFieldGroup[] => { + const groups: PluginSettingFieldGroup[] = []; + for (let index = 0; index < fields.length;) { + const field = fields[index]!; + if (field.type !== "boolean") { + groups.push({ id: field.key, compact: false, fields: [field] }); + index += 1; + continue; + } + + const namespace = fieldNamespace(field); + let end = index + 1; + while (end < fields.length && fields[end]!.type === "boolean" && fieldNamespace(fields[end]!) === namespace) end += 1; + const run = fields.slice(index, end); + if (run.length >= 3) groups.push({ id: `${namespace}:${field.key}`, compact: true, fields: run }); + else for (const item of run) groups.push({ id: item.key, compact: false, fields: [item] }); + index = end; + } + return groups; +};