mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 22:49:18 +00:00
feat(settings): add searchable navigation
This commit is contained in:
committed by
Carlos Valente
parent
bdd815678b
commit
9e42f18299
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user