feat(settings): add searchable navigation

This commit is contained in:
Carlos Valente
2026-08-01 17:09:34 +02:00
committed by Carlos Valente
parent bdd815678b
commit 9e42f18299
11 changed files with 341 additions and 162 deletions
@@ -3,64 +3,107 @@ import { useMemo } from 'react';
import useAppVersion from '../../common/hooks-query/useAppVersion';
import { isDocker } from '../../externals';
export type SettingsOption = {
type SettingsOptionBase = {
id: string;
label: string;
secondary?: Readonly<SettingsOption[]>;
highlight?: string;
};
export type SettingsOption =
| (SettingsOptionBase & { secondary: Readonly<SettingsOption[]>; keywords?: never })
| (SettingsOptionBase & { secondary?: never; keywords: Readonly<string[]> });
const staticOptions = [
{
id: 'settings',
label: 'Settings',
secondary: [
{ id: 'settings__data', label: 'Project data' },
{ id: 'settings__general', label: 'General settings' },
{ id: 'settings__view', label: 'View settings' },
{ id: 'settings__custom-views', label: 'Custom views' },
{ id: 'settings__mcp', label: 'MCP Server' },
...(isDocker ? [] : [{ id: 'settings__port', label: 'Server port' }]),
{ id: 'settings__data', label: 'Project data', keywords: ['title', 'description', 'logo', 'url', 'info'] },
{
id: 'settings__general',
label: 'General settings',
keywords: ['pin', 'password', 'lock', 'language', 'time format', 'timezone'],
},
{
id: 'settings__view',
label: 'View settings',
keywords: ['css', 'style', 'theme', 'translation', 'freeze', 'overtime'],
},
{ id: 'settings__custom-views', label: 'Custom views', keywords: ['html', 'upload', 'external', 'embed'] },
{ id: 'settings__mcp', label: 'MCP Server', keywords: ['ai', 'agent', 'model'] },
...(isDocker ? [] : [{ id: 'settings__port', label: 'Server port', keywords: ['http', 'network', 'address'] }]),
],
},
{
id: 'project',
label: 'Project',
secondary: [
{ id: 'project__create', label: 'Create...' },
{ id: 'project__list', label: 'Manage projects' },
{ id: 'project__create', label: 'Create...', keywords: ['new project', 'quick start', 'wizard'] },
{
id: 'project__list',
label: 'Manage projects',
keywords: ['load', 'open', 'rename', 'duplicate', 'delete', 'download', 'import', 'backup', 'merge'],
},
],
},
{
id: 'manage',
label: 'Project settings',
secondary: [
{ id: 'manage__defaults', label: 'Rundown defaults' },
{ id: 'manage__custom', label: 'Custom fields' },
{ id: 'manage__rundowns', label: 'Manage rundowns' },
{ id: 'manage__sheets', label: 'Import spreadsheet' },
{ id: 'manage__sheets', label: 'Sync with Google Sheet' },
{
id: 'manage__defaults',
label: 'Rundown defaults',
keywords: ['duration', 'warning', 'danger', 'defaults'],
},
{
id: 'manage__custom',
label: 'Custom fields',
keywords: ['metadata', 'columns', 'extra data', 'image field'],
},
{
id: 'manage__rundowns',
label: 'Manage rundowns',
keywords: ['rundown', 'xlsx', 'excel', 'load', 'export'],
},
{
id: 'manage__sheets',
label: 'Import spreadsheet',
keywords: ['google sheet', 'sync', 'spreadsheet', 'xlsx', 'excel', 'csv', 'export'],
},
],
},
{
id: 'automation',
label: 'Automation',
secondary: [
{ id: 'automation__settings', label: 'Automation settings' },
{ id: 'automation__automations', label: 'Manage automations' },
{ id: 'automation__triggers', label: 'Manage triggers' },
{
id: 'automation__settings',
label: 'Automation settings',
keywords: ['osc input', 'port', 'enable', 'remote control'],
},
{
id: 'automation__automations',
label: 'Manage automations',
keywords: ['osc', 'http', 'webhook', 'integration', 'api', 'output', 'action'],
},
{
id: 'automation__triggers',
label: 'Manage triggers',
keywords: ['lifecycle', 'on load', 'on start', 'on finish', 'on update'],
},
],
},
{
id: 'sharing',
label: 'Sharing and reporting',
secondary: [
{ id: 'sharing__presets', label: 'URL Presets' },
{ id: 'sharing__presets', label: 'URL Presets', keywords: ['alias', 'link', 'url', 'shortcut'] },
{
id: 'sharing__link',
label: 'Share link',
keywords: ['qr code', 'guest', 'cuesheet link', 'permissions', 'read only'],
},
{ id: 'sharing__report', label: 'Runtime report' },
{ id: 'sharing__report', label: 'Runtime report', keywords: ['actual times', 'csv', 'export', 'history'] },
],
},
{
@@ -70,20 +113,24 @@ const staticOptions = [
{
id: 'network__log',
label: 'Event log',
keywords: ['debug', 'errors', 'console', 'export log'],
},
{
id: 'network__clients',
label: 'Manage clients',
keywords: ['redirect', 'identify', 'rename client', 'connected'],
},
],
},
{
id: 'about',
label: 'About',
keywords: ['version', 'update', 'licence', 'license', 'credits'],
},
{
id: 'shutdown',
label: 'Shutdown',
keywords: ['quit', 'exit', 'close ontime'],
},
] as const;
@@ -92,6 +139,51 @@ export type SettingsOptionId =
| (typeof staticOptions)[number]['id']
| Extract<(typeof staticOptions)[number], { secondary: object }>['secondary'][number]['id'];
function sanitiseSearchInput(query: string): string {
return query.trim().toLowerCase();
}
export function matchesSettingsOptionQuery(option: SettingsOption, query: string): boolean {
const sanitisedQuery = sanitiseSearchInput(query);
if (!sanitisedQuery) {
return false;
}
const sanitisedLabel = sanitiseSearchInput(option.label);
if (sanitisedLabel.includes(sanitisedQuery)) {
return true;
}
// check keywords
return Boolean(option.keywords?.some((keyword) => keyword.includes(sanitisedQuery)));
}
/**
* Filters the settings menu against a user query.
* A group is kept if it matches itself (with all its children) or if any of its children match.
*/
export function filterSettingsOptions(options: Readonly<SettingsOption[]>, query: string): SettingsOption[] {
const sanitisedQuery = sanitiseSearchInput(query);
if (!sanitisedQuery) {
return [...options];
}
return options.reduce<SettingsOption[]>((accumulator, option) => {
if (matchesSettingsOptionQuery(option, sanitisedQuery)) {
accumulator.push(option);
return accumulator;
}
if (option.secondary) {
const secondary = option.secondary.filter((child) => matchesSettingsOptionQuery(child, sanitisedQuery));
if (secondary.length) {
accumulator.push({ ...option, secondary });
}
}
return accumulator;
}, []);
}
export function useAppSettingsMenu() {
const { data } = useAppVersion();