feat(plugins): compact grouped boolean settings
This commit is contained in:
1 parent
9498902640
commit
131248b530
3 files changed
+98
-20
No files matched your search
@@ -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 (
|
||||
<div className="grid min-w-0 gap-3 py-5 first:pt-0 last:pb-0 md:grid-cols-[minmax(0,14rem)_minmax(0,1fr)] md:gap-8">
|
||||
<div className={compact
|
||||
? "flex min-w-0 items-start justify-between gap-4 rounded-lg border border-slate-200 bg-white p-4"
|
||||
: "grid min-w-0 gap-3 py-5 first:pt-0 last:pb-0 md:grid-cols-[minmax(0,14rem)_minmax(0,1fr)] md:gap-8"}>
|
||||
<div className="min-w-0">
|
||||
{label}
|
||||
{field.description ? <p id={descriptionId} className="mt-1 text-xs leading-5 text-slate-500">{field.description}</p> : 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<Record<string, PluginSettingValue | "">>({});
|
||||
const [configuredSecrets, setConfiguredSecrets] = useState<Record<string, boolean>>({});
|
||||
const [loading, setLoading] = useState(fields.length > 0);
|
||||
@@ -179,25 +185,35 @@ export const PluginSettingsSection = ({ host, manifest }: { host: EdgeEverPlugin
|
||||
) : (
|
||||
<form onChange={clearFeedback} onSubmit={(event) => { event.preventDefault(); void save(); }}>
|
||||
<fieldset disabled={saving} className="min-w-0">
|
||||
<div className="divide-y divide-slate-100 py-5">
|
||||
{fields.map((field) => {
|
||||
const value = values[field.key] ?? "";
|
||||
const inputId = `${formId}-${field.key}`;
|
||||
return (
|
||||
<PluginSettingFieldRow
|
||||
key={field.key}
|
||||
configuredSecret={Boolean(configuredSecrets[field.key])}
|
||||
disabled={saving}
|
||||
field={field}
|
||||
inputId={inputId}
|
||||
value={value}
|
||||
onChange={(nextValue) => {
|
||||
clearFeedback();
|
||||
setValues((current) => ({ ...current, [field.key]: nextValue }));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="py-5">
|
||||
{fieldGroups.map((group, groupIndex) => (
|
||||
<div
|
||||
key={group.id}
|
||||
className={group.compact
|
||||
? `grid gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4${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 (
|
||||
<PluginSettingFieldRow
|
||||
key={field.key}
|
||||
compact={group.compact}
|
||||
configuredSecret={Boolean(configuredSecrets[field.key])}
|
||||
disabled={saving}
|
||||
field={field}
|
||||
inputId={inputId}
|
||||
value={value}
|
||||
onChange={(nextValue) => {
|
||||
clearFeedback();
|
||||
setValues((current) => ({ ...current, [field.key]: nextValue }));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex min-h-14 flex-wrap items-center justify-end gap-3 border-t border-slate-200 pt-4">
|
||||
{message ? <span className="mr-auto text-sm text-emerald-700" role="status">{message}</span> : null}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
Reference in new issue
Block a user