feat: surface plugin settings as search sub-results

This commit is contained in:
Linus Rath
2026-05-06 19:14:47 +02:00
parent 1cd6cde77c
commit b1573aada1
2 changed files with 51 additions and 14 deletions
+35 -10
View File
@@ -264,6 +264,9 @@ function flattenStrings(node: unknown, sink: string[]): void {
interface SubResult { interface SubResult {
label: string; label: string;
description?: string; description?: string;
// For plugin setting fields: the id of the plugin whose card needs to be
// expanded before the field becomes visible in the DOM.
pluginId?: string;
} }
// Walk a translation subtree and emit sub-results for renderable settings. // Walk a translation subtree and emit sub-results for renderable settings.
@@ -377,12 +380,29 @@ export default function SettingsPage() {
haystacks[tabId] = strings.join(' ').toLowerCase(); haystacks[tabId] = strings.join(' ').toLowerCase();
} }
if (installedPlugins.length) { if (installedPlugins.length) {
const text = installedPlugins.map((p) => `${p.name} ${p.description} ${p.author}`).join(' '); const haystackText = installedPlugins.map((p) => {
haystacks.plugins = `${haystacks.plugins ?? ''} ${text}`.toLowerCase(); const fieldText = p.settingsSchema
subs.plugins = [ ? Object.values(p.settingsSchema)
...(subs.plugins ?? []), .map((s) => `${s.label} ${s.description ?? ''}`)
...installedPlugins.map((p) => ({ label: p.name, description: p.description })), .join(' ')
]; : '';
return `${p.name} ${p.description} ${p.author} ${fieldText}`;
}).join(' ');
haystacks.plugins = `${haystacks.plugins ?? ''} ${haystackText}`.toLowerCase();
const pluginSubs: SubResult[] = installedPlugins.flatMap((p) => {
const items: SubResult[] = [{ label: p.name, description: p.description }];
if (p.settingsSchema) {
for (const schema of Object.values(p.settingsSchema)) {
items.push({
label: schema.label,
description: schema.description,
pluginId: p.id,
});
}
}
return items;
});
subs.plugins = [...(subs.plugins ?? []), ...pluginSubs];
} }
if (installedThemes.length) { if (installedThemes.length) {
const text = installedThemes.map((th) => `${th.name} ${th.description} ${th.author}`).join(' '); const text = installedThemes.map((th) => `${th.name} ${th.description} ${th.author}`).join(' ');
@@ -606,9 +626,14 @@ export default function SettingsPage() {
} }
}; };
const handleSubResultSelect = (tabId: Tab, label: string) => { const handleSubResultSelect = (tabId: Tab, sub: SubResult) => {
handleTabSelect(tabId); handleTabSelect(tabId);
setPendingHighlight({ tab: tabId, label }); if (sub.pluginId && typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent('settings-plugin-expand', { detail: { pluginId: sub.pluginId } })
);
}
setPendingHighlight({ tab: tabId, label: sub.label });
}; };
const activeTabLabel = tabs.find((tab) => tab.id === effectiveActiveTab)?.label ?? ''; const activeTabLabel = tabs.find((tab) => tab.id === effectiveActiveTab)?.label ?? '';
@@ -753,7 +778,7 @@ export default function SettingsPage() {
{subs.map((sub) => ( {subs.map((sub) => (
<button <button
key={`${tab.id}:${sub.label}`} key={`${tab.id}:${sub.label}`}
onClick={() => handleSubResultSelect(tab.id, sub.label)} onClick={() => handleSubResultSelect(tab.id, sub)}
className="w-full flex items-center pl-12 pr-5 py-2 text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors duration-150 text-left" className="w-full flex items-center pl-12 pr-5 py-2 text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors duration-150 text-left"
> >
<span className="truncate">{sub.label}</span> <span className="truncate">{sub.label}</span>
@@ -895,7 +920,7 @@ export default function SettingsPage() {
{subs.map((sub) => ( {subs.map((sub) => (
<button <button
key={`${tab.id}:${sub.label}`} key={`${tab.id}:${sub.label}`}
onClick={() => handleSubResultSelect(tab.id, sub.label)} onClick={() => handleSubResultSelect(tab.id, sub)}
className="w-full text-left pl-9 pr-3 py-1.5 rounded-md text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors duration-150" className="w-full text-left pl-9 pr-3 py-1.5 rounded-md text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors duration-150"
> >
<span className="truncate block">{sub.label}</span> <span className="truncate block">{sub.label}</span>
+16 -4
View File
@@ -32,6 +32,18 @@ export function PluginsSettings() {
initializePlugins(); initializePlugins();
}, [fetchPolicy, initializePlugins, loaded]); }, [fetchPolicy, initializePlugins, loaded]);
// Listen for "expand this plugin" events fired by the settings search when
// the user clicks a plugin-setting sub-result. Expanding the card mounts
// the per-field rows so the highlight effect can find them.
useEffect(() => {
const handler = (e: Event) => {
const id = (e as CustomEvent<{ pluginId: string }>).detail?.pluginId;
if (id) setExpandedPlugin(id);
};
window.addEventListener('settings-plugin-expand', handler);
return () => window.removeEventListener('settings-plugin-expand', handler);
}, []);
if (!isFeatureEnabled('pluginsEnabled')) { if (!isFeatureEnabled('pluginsEnabled')) {
return null; return null;
} }
@@ -312,7 +324,7 @@ function PluginSettingField({ schema, value, onChange }: PluginSettingFieldProps
switch (schema.type) { switch (schema.type) {
case 'boolean': case 'boolean':
return ( return (
<div className="flex items-center justify-between"> <div data-search-label={schema.label} className="flex items-center justify-between">
<div> <div>
<span className="text-xs text-foreground">{schema.label}</span> <span className="text-xs text-foreground">{schema.label}</span>
{schema.description && <p className="text-[10px] text-muted-foreground">{schema.description}</p>} {schema.description && <p className="text-[10px] text-muted-foreground">{schema.description}</p>}
@@ -323,7 +335,7 @@ function PluginSettingField({ schema, value, onChange }: PluginSettingFieldProps
case 'select': case 'select':
return ( return (
<div className="flex items-center justify-between"> <div data-search-label={schema.label} className="flex items-center justify-between">
<div> <div>
<span className="text-xs text-foreground">{schema.label}</span> <span className="text-xs text-foreground">{schema.label}</span>
{schema.description && <p className="text-[10px] text-muted-foreground">{schema.description}</p>} {schema.description && <p className="text-[10px] text-muted-foreground">{schema.description}</p>}
@@ -342,7 +354,7 @@ function PluginSettingField({ schema, value, onChange }: PluginSettingFieldProps
case 'string': case 'string':
return ( return (
<div> <div data-search-label={schema.label}>
<span className="text-xs text-foreground">{schema.label}</span> <span className="text-xs text-foreground">{schema.label}</span>
{schema.description && <p className="text-[10px] text-muted-foreground">{schema.description}</p>} {schema.description && <p className="text-[10px] text-muted-foreground">{schema.description}</p>}
<input <input
@@ -356,7 +368,7 @@ function PluginSettingField({ schema, value, onChange }: PluginSettingFieldProps
case 'number': case 'number':
return ( return (
<div className="flex items-center justify-between"> <div data-search-label={schema.label} className="flex items-center justify-between">
<div> <div>
<span className="text-xs text-foreground">{schema.label}</span> <span className="text-xs text-foreground">{schema.label}</span>
{schema.description && <p className="text-[10px] text-muted-foreground">{schema.description}</p>} {schema.description && <p className="text-[10px] text-muted-foreground">{schema.description}</p>}