feat(settings): add global settings search
This commit is contained in:
parent
3af3dda9b5
commit
86c53479a1
|
|
@ -121,6 +121,7 @@ import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
|||
import { usePromptTemplateStore } from "@/stores/promptTemplateStore";
|
||||
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
|
||||
import { currentLocale, setLocale, type Locale } from "@/i18n";
|
||||
import { SETTINGS_SEARCH_DEFINITIONS, TOOLBAR_VISIBILITY_ITEMS, createShortcutSettingsSearchDefinitions, resolveSettingsSearchEntries, searchSettings, toolbarVisibilityItemLabel, type SettingsCategory, type SettingsSearchEntry, type ToolbarVisibilityItem } from "@/lib/settings/settingsSearch";
|
||||
import { LOCALE_OPTIONS } from "@/lib/app/localeOptions";
|
||||
import { DEFAULT_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES, DEFAULT_WEB_DAV_REMOTE_PATH, normalizedWebDavAutoUploadInterval, writeWebDavAutoUploadFields } from "@/lib/webdav/webdavAutoUploadConfig";
|
||||
import { apiUrl } from "@/lib/common/webPath";
|
||||
|
|
@ -410,6 +411,10 @@ const editExportRowLimit = ref(settingsStore.editorSettings.exportRowLimit);
|
|||
const editQueryExportKeysetOptimizationEnabled = ref(settingsStore.editorSettings.queryExportKeysetOptimizationEnabled);
|
||||
const editUpdateDownloadSource = ref<UpdateDownloadSource>(settingsStore.editorSettings.updateDownloadSource);
|
||||
const editToolbarItems = ref({ ...settingsStore.editorSettings.toolbarItems });
|
||||
const toolbarVisibilityItems = TOOLBAR_VISIBILITY_ITEMS;
|
||||
function getToolbarVisibilityItemLabel(item: ToolbarVisibilityItem): string {
|
||||
return toolbarVisibilityItemLabel(item, t);
|
||||
}
|
||||
const systemFonts = ref<string[]>([]);
|
||||
const systemFontsLoading = ref(false);
|
||||
const systemFontsLoaded = ref(false);
|
||||
|
|
@ -1369,7 +1374,6 @@ const appSupportInfoLabels = computed<AppSupportInfoLabels>(() => ({
|
|||
unknown: t("settings.supportInfoUnknown"),
|
||||
}));
|
||||
const appSupportInfoRows = computed(() => (appSupportInfo.value ? buildAppSupportInfoRows(appSupportInfo.value, appSupportInfoLabels.value) : []));
|
||||
type SettingsCategory = "editor" | "formatter" | "appearance" | "navigation" | "data" | "backups" | "tunnels" | "shortcuts" | "snippets" | "sync" | "ai" | "mcp" | "security" | "about";
|
||||
const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[]>(() => [
|
||||
{ value: "appearance", label: t("settings.appearanceTab") },
|
||||
{ value: "editor", label: t("settings.editorTab") },
|
||||
|
|
@ -1399,6 +1403,164 @@ function settingsCategoryButton(value: SettingsCategory): string {
|
|||
].join(" ");
|
||||
}
|
||||
|
||||
const settingsSearchQuery = ref("");
|
||||
const settingsSearchOpen = ref(false);
|
||||
const settingsSearchActiveIndex = ref(0);
|
||||
const settingsSearchInputContainerRef = ref<HTMLElement | null>(null);
|
||||
const highlightedSettingsSearchTargetId = ref("");
|
||||
let highlightedSettingsSearchElement: HTMLElement | null = null;
|
||||
let pendingSettingsSearchResult: SettingsSearchEntry | null = null;
|
||||
let settingsSearchHighlightTimer: ReturnType<typeof window.setTimeout> | null = null;
|
||||
let settingsSearchHighlightAnimationHandler: ((event: AnimationEvent) => void) | null = null;
|
||||
const settingsSearchHighlightClasses = ["rounded-md", "bg-primary/5", "transition-[box-shadow,background-color]", "duration-200", "settings-search-highlight-breathe"];
|
||||
|
||||
const settingsSearchCategoryLabels = computed(() => Object.fromEntries(settingsCategoryNav.value.map((category) => [category.value, category.label])) as Record<SettingsCategory, string>);
|
||||
const settingsSearchEntries = computed(() =>
|
||||
resolveSettingsSearchEntries(
|
||||
[...SETTINGS_SEARCH_DEFINITIONS, ...createShortcutSettingsSearchDefinitions(SHORTCUT_DEFINITIONS)],
|
||||
{
|
||||
isWeb,
|
||||
visibleCategories: new Set(settingsCategoryNav.value.map((category) => category.value)),
|
||||
},
|
||||
t,
|
||||
settingsSearchCategoryLabels.value,
|
||||
),
|
||||
);
|
||||
const settingsSearchResults = computed(() => searchSettings(settingsSearchEntries.value, settingsSearchQuery.value, currentLocale()));
|
||||
const settingsSearchActive = computed(() => Boolean(settingsSearchQuery.value.trim()));
|
||||
const settingsSearchVisible = computed(() => settingsSearchOpen.value && settingsSearchActive.value);
|
||||
const settingsSearchResultGroups = computed(() => {
|
||||
const groups = new Map<SettingsCategory, { categoryLabel: string; results: (typeof settingsSearchResults.value)[number][] }>();
|
||||
for (const result of settingsSearchResults.value) {
|
||||
const group = groups.get(result.category) ?? { categoryLabel: result.categoryLabel, results: [] };
|
||||
group.results.push(result);
|
||||
groups.set(result.category, group);
|
||||
}
|
||||
return Array.from(groups, ([category, group]) => ({ category, ...group }));
|
||||
});
|
||||
|
||||
function clearSettingsSearchHighlight() {
|
||||
if (settingsSearchHighlightTimer) {
|
||||
window.clearTimeout(settingsSearchHighlightTimer);
|
||||
settingsSearchHighlightTimer = null;
|
||||
}
|
||||
if (highlightedSettingsSearchElement && settingsSearchHighlightAnimationHandler) {
|
||||
highlightedSettingsSearchElement.removeEventListener("animationend", settingsSearchHighlightAnimationHandler);
|
||||
}
|
||||
settingsSearchHighlightAnimationHandler = null;
|
||||
highlightedSettingsSearchElement?.classList.remove(...settingsSearchHighlightClasses);
|
||||
highlightedSettingsSearchElement = null;
|
||||
highlightedSettingsSearchTargetId.value = "";
|
||||
}
|
||||
|
||||
function resetSettingsSearchState() {
|
||||
settingsSearchQuery.value = "";
|
||||
settingsSearchOpen.value = false;
|
||||
settingsSearchActiveIndex.value = 0;
|
||||
pendingSettingsSearchResult = null;
|
||||
shortcutSearchQuery.value = "";
|
||||
clearSettingsSearchHighlight();
|
||||
}
|
||||
|
||||
function exitSettingsSearch() {
|
||||
settingsSearchQuery.value = "";
|
||||
settingsSearchOpen.value = false;
|
||||
}
|
||||
|
||||
async function focusSettingsSearchInput() {
|
||||
await nextTick();
|
||||
settingsSearchInputContainerRef.value?.querySelector<HTMLInputElement>("input")?.focus();
|
||||
}
|
||||
|
||||
function settingsSearchTargetClass(targetId: string): string {
|
||||
return highlightedSettingsSearchTargetId.value === targetId ? "ring-2 ring-primary ring-offset-2 ring-offset-background transition-shadow" : "";
|
||||
}
|
||||
|
||||
function onSettingsCategoryClick(category: SettingsCategory) {
|
||||
settingsSearchOpen.value = false;
|
||||
activeSettingsTab.value = category;
|
||||
}
|
||||
|
||||
function applySettingsSearchRoute(result: SettingsSearchEntry) {
|
||||
if (result.route?.syncMethodTab) syncMethodTab.value = result.route.syncMethodTab;
|
||||
}
|
||||
|
||||
function normalizeSettingsSearchText(value: string | null | undefined): string {
|
||||
return value?.replace(/\s+/g, " ").trim() ?? "";
|
||||
}
|
||||
|
||||
function findSettingsSearchHighlightTarget(searchRoot: HTMLElement, title: string): HTMLElement {
|
||||
const titleElement = Array.from(searchRoot.querySelectorAll<HTMLElement>("label, h3, h4")).find((element) => normalizeSettingsSearchText(element.textContent) === title);
|
||||
if (!titleElement) return searchRoot;
|
||||
|
||||
let candidate = titleElement.parentElement;
|
||||
while (candidate && candidate !== searchRoot) {
|
||||
if (candidate.classList.contains("rounded-md") && candidate.classList.contains("border")) return candidate;
|
||||
if (candidate.querySelector("input, button, [role='combobox'], textarea")) return candidate;
|
||||
candidate = candidate.parentElement;
|
||||
}
|
||||
return titleElement;
|
||||
}
|
||||
|
||||
async function revealSettingsSearchTarget(result: SettingsSearchEntry) {
|
||||
await nextTick();
|
||||
const searchRoot = settingsContentScrollRef.value?.querySelector<HTMLElement>(`[data-settings-search-id="${result.targetId}"]`);
|
||||
if (!searchRoot) return;
|
||||
const target = findSettingsSearchHighlightTarget(searchRoot, result.title);
|
||||
target.scrollIntoView({ block: "center", behavior: "smooth" });
|
||||
clearSettingsSearchHighlight();
|
||||
if (target === searchRoot) {
|
||||
highlightedSettingsSearchTargetId.value = result.targetId;
|
||||
}
|
||||
target.classList.add(...settingsSearchHighlightClasses);
|
||||
highlightedSettingsSearchElement = target;
|
||||
|
||||
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
|
||||
settingsSearchHighlightTimer = window.setTimeout(clearSettingsSearchHighlight, 1500);
|
||||
return;
|
||||
}
|
||||
settingsSearchHighlightAnimationHandler = (event) => {
|
||||
if (event.animationName === "settings-search-highlight-breathe") clearSettingsSearchHighlight();
|
||||
};
|
||||
target.addEventListener("animationend", settingsSearchHighlightAnimationHandler);
|
||||
}
|
||||
|
||||
async function selectSettingsSearchResult(result: SettingsSearchEntry) {
|
||||
pendingSettingsSearchResult = result;
|
||||
if (result.shortcutId) shortcutSearchQuery.value = result.title;
|
||||
applySettingsSearchRoute(result);
|
||||
settingsSearchQuery.value = "";
|
||||
settingsSearchOpen.value = false;
|
||||
settingsSearchActiveIndex.value = 0;
|
||||
if (activeSettingsTab.value === result.category) {
|
||||
pendingSettingsSearchResult = null;
|
||||
await revealSettingsSearchTarget(result);
|
||||
return;
|
||||
}
|
||||
activeSettingsTab.value = result.category;
|
||||
}
|
||||
|
||||
function onSettingsSearchKeydown(event: KeyboardEvent) {
|
||||
const results = settingsSearchResults.value;
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
exitSettingsSearch();
|
||||
return;
|
||||
}
|
||||
if (!results.length) return;
|
||||
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
settingsSearchOpen.value = true;
|
||||
const direction = event.key === "ArrowDown" ? 1 : -1;
|
||||
settingsSearchActiveIndex.value = (settingsSearchActiveIndex.value + direction + results.length) % results.length;
|
||||
return;
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
void selectSettingsSearchResult(results[settingsSearchActiveIndex.value] ?? results[0]);
|
||||
}
|
||||
}
|
||||
|
||||
async function resetSettingsContentScroll() {
|
||||
await nextTick();
|
||||
const scroller = settingsContentScrollRef.value;
|
||||
|
|
@ -2041,6 +2203,8 @@ watch(
|
|||
() => settingsVisible.value,
|
||||
async (open) => {
|
||||
if (open) {
|
||||
resetSettingsSearchState();
|
||||
void focusSettingsSearchInput();
|
||||
snippetSyncSettingsLoading.value = true;
|
||||
mcpPolicyLoading.value = true;
|
||||
mcpPolicyLoadError.value = "";
|
||||
|
|
@ -2095,6 +2259,8 @@ watch(
|
|||
if (!isWeb && activeSettingsTab.value === "ai" && aiIsCliProvider.value) void ensureCliMcpStatus();
|
||||
if (activeSettingsTab.value === "about") void refreshAppSupportInfo();
|
||||
await scrollToInitialSettingsSection();
|
||||
} else {
|
||||
resetSettingsSearchState();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
|
|
@ -2155,6 +2321,16 @@ watch(activeSettingsTab, async (tab) => {
|
|||
checkLayoutDescTruncation();
|
||||
checkIconThemeDescTruncation();
|
||||
}
|
||||
const result = pendingSettingsSearchResult;
|
||||
if (result) {
|
||||
pendingSettingsSearchResult = null;
|
||||
await revealSettingsSearchTarget(result);
|
||||
}
|
||||
});
|
||||
|
||||
watch(settingsSearchQuery, (query) => {
|
||||
settingsSearchActiveIndex.value = 0;
|
||||
settingsSearchOpen.value = Boolean(query.trim());
|
||||
});
|
||||
|
||||
// If the store finishes loading while the AI tab is already open (e.g. a retry
|
||||
|
|
@ -3225,7 +3401,10 @@ watch(
|
|||
},
|
||||
);
|
||||
|
||||
onUnmounted(cleanupPreviewEditor);
|
||||
onUnmounted(() => {
|
||||
cleanupPreviewEditor();
|
||||
resetSettingsSearchState();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -3239,15 +3418,72 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</DialogHeader>
|
||||
|
||||
<div class="settings-layout flex min-h-0 flex-1 flex-col gap-3 overflow-hidden lg:flex-row">
|
||||
<nav class="settingsCategoryNav settings-category-nav flex min-h-0 shrink-0 gap-1 overflow-x-auto border-b pb-3 lg:w-40 lg:flex-col lg:overflow-x-hidden lg:overflow-y-auto lg:border-b-0 lg:border-r lg:pb-0 lg:pr-3">
|
||||
<button v-for="category in settingsCategoryNav" :key="category.value" type="button" :class="settingsCategoryButton(category.value)" @click="activeSettingsTab = category.value">
|
||||
<nav class="settingsCategoryNav settings-category-nav flex min-h-0 shrink-0 gap-1 overflow-x-auto border-b pb-3 lg:w-52 lg:flex-col lg:overflow-x-hidden lg:overflow-y-auto lg:border-b-0 lg:border-r lg:pb-0 lg:pr-3">
|
||||
<button v-for="category in settingsCategoryNav" :key="category.value" type="button" :class="settingsCategoryButton(category.value)" @click="onSettingsCategoryClick(category.value)">
|
||||
{{ category.label }}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="min-w-0 flex-1 overflow-hidden px-1 flex flex-col">
|
||||
<div ref="settingsContentScrollRef" class="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-1 pr-2">
|
||||
<section v-if="activeSettingsTab === 'editor'" class="flex flex-col gap-5 py-2">
|
||||
<div class="shrink-0 px-2 pt-1 pb-3">
|
||||
<div ref="settingsSearchInputContainerRef" class="relative">
|
||||
<Search class="pointer-events-none absolute top-1/2 left-4 z-10 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
v-model="settingsSearchQuery"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
role="combobox"
|
||||
:aria-label="t('settings.searchSettings')"
|
||||
:aria-expanded="settingsSearchVisible ? 'true' : 'false'"
|
||||
aria-controls="settings-search-results"
|
||||
:aria-activedescendant="settingsSearchVisible && settingsSearchResults.length ? `settings-search-result-${settingsSearchResults[settingsSearchActiveIndex]?.id}` : undefined"
|
||||
:placeholder="t('settings.searchSettings')"
|
||||
class="h-11 w-full rounded-xl border-border bg-muted/30 pr-10 pl-11 text-sm shadow-none hover:bg-muted/40 focus-visible:ring-2 focus-visible:ring-inset focus-visible:border-primary focus-visible:bg-background"
|
||||
@focus="settingsSearchOpen = Boolean(settingsSearchQuery.trim())"
|
||||
@keydown="onSettingsSearchKeydown"
|
||||
/>
|
||||
<button v-if="settingsSearchQuery" type="button" class="absolute top-1/2 right-2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground" :aria-label="t('settings.clearSettingsSearch')" @click="exitSettingsSearch">
|
||||
<X class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="settingsSearchVisible" id="settings-search-results" role="listbox" :aria-label="t('settings.searchSettingsResults')" class="min-h-0 flex-1 overflow-y-auto px-1 pr-2">
|
||||
<div class="mx-auto w-full max-w-3xl pb-4">
|
||||
<button type="button" class="mb-4 inline-flex items-center gap-1.5 rounded-md px-1 py-1 text-sm text-muted-foreground transition-colors hover:text-foreground" @click="exitSettingsSearch">
|
||||
<ArrowLeft class="h-4 w-4" />
|
||||
{{ t("settings.exitSettingsSearch") }}
|
||||
</button>
|
||||
<div v-if="settingsSearchResults.length === 0" class="rounded-xl border border-dashed px-4 py-12 text-center text-sm text-muted-foreground">
|
||||
{{ t("settings.searchSettingsNoResults") }}
|
||||
</div>
|
||||
<div v-for="group in settingsSearchResultGroups" :key="group.category" class="mb-6 last:mb-0">
|
||||
<div class="mb-2 flex items-center gap-2 px-1 text-sm font-medium text-muted-foreground">
|
||||
<span class="flex h-7 w-7 items-center justify-center rounded-md border bg-muted/40">
|
||||
<Settings class="h-4 w-4" />
|
||||
</span>
|
||||
{{ group.categoryLabel }}
|
||||
</div>
|
||||
<div class="overflow-hidden rounded-xl border bg-card p-1 shadow-sm">
|
||||
<button
|
||||
v-for="result in group.results"
|
||||
:id="`settings-search-result-${result.id}`"
|
||||
:key="result.id"
|
||||
type="button"
|
||||
role="option"
|
||||
:aria-selected="result.id === settingsSearchResults[settingsSearchActiveIndex]?.id"
|
||||
:class="['flex w-full flex-col gap-1 rounded-lg px-3 py-3 text-left outline-none transition-colors sm:px-4', result.id === settingsSearchResults[settingsSearchActiveIndex]?.id ? 'bg-accent text-accent-foreground' : 'hover:bg-muted/70']"
|
||||
@mousedown.prevent
|
||||
@click="void selectSettingsSearchResult(result)"
|
||||
>
|
||||
<span class="text-sm font-medium">{{ result.title }}</span>
|
||||
<span v-if="result.description" class="line-clamp-2 text-xs text-muted-foreground">{{ result.description }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else ref="settingsContentScrollRef" class="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-1 pr-2">
|
||||
<section v-if="activeSettingsTab === 'editor'" data-settings-search-id="editor" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('editor')]">
|
||||
<div class="grid gap-4 md:grid-cols-[1fr_auto]">
|
||||
<!-- Font Family -->
|
||||
<div class="space-y-2 min-w-0">
|
||||
|
|
@ -3564,7 +3800,7 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'formatter'" class="flex flex-col gap-5 py-2">
|
||||
<section v-else-if="activeSettingsTab === 'formatter'" data-settings-search-id="formatter" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('formatter')]">
|
||||
<div class="space-y-3 rounded-md border border-border/70 bg-muted/10 p-3">
|
||||
<div class="text-sm font-medium">
|
||||
{{ t("settings.sqlFormatterEditorShortcuts") }}
|
||||
|
|
@ -3637,7 +3873,7 @@ onUnmounted(cleanupPreviewEditor);
|
|||
<SqlFormatterSettingsPanel v-model="editSqlFormatter" @validity-change="(value: boolean) => (sqlFormatterConfigValid = value)" />
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'appearance'" class="settings-appearance-section flex flex-col gap-4 py-2">
|
||||
<section v-else-if="activeSettingsTab === 'appearance'" class="settings-appearance-section flex flex-col gap-4 py-2" data-settings-search-id="appearance" :class="settingsSearchTargetClass('appearance')">
|
||||
<div class="settings-appearance-top-grid">
|
||||
<div class="settings-appearance-field min-w-0">
|
||||
<div class="flex h-9 items-end">
|
||||
|
|
@ -4142,38 +4378,15 @@ onUnmounted(cleanupPreviewEditor);
|
|||
<Switch id="exclusive-right-sidebar-panels" v-model="editToolbarItems.exclusiveRightSidebarPanels" />
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2 mt-2">
|
||||
<div
|
||||
v-for="item in [
|
||||
{
|
||||
key: 'dataTransfer',
|
||||
label: t('transfer.dataTransfer'),
|
||||
},
|
||||
{
|
||||
key: 'driverManager',
|
||||
label: t('toolbar.driverManager'),
|
||||
},
|
||||
{ key: 'sqlFile', label: t('sqlFile.title') },
|
||||
{ key: 'schemaDiff', label: t('diff.title') },
|
||||
{ key: 'dataCompare', label: t('dataCompare.title') },
|
||||
{ key: 'checkUpdates', label: t('updates.check') },
|
||||
{ key: 'sqlLibrary', label: t('sqlLibrary.title') },
|
||||
{ key: 'sqlFileTree', label: t('sqlFileTree.title') },
|
||||
{ key: 'history', label: t('history.title') },
|
||||
{ key: 'ai', label: 'AI' },
|
||||
{ key: 'theme', label: t('toolbar.theme') },
|
||||
{ key: 'github', label: 'GitHub' },
|
||||
]"
|
||||
:key="item.key"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<div v-for="item in toolbarVisibilityItems" :key="item.key" class="flex items-center gap-2">
|
||||
<Switch :id="`toolbar-${item.key}`" :model-value="(editToolbarItems as any)[item.key]" @update:model-value="(v: boolean) => ((editToolbarItems as any)[item.key] = v)" />
|
||||
<Label :for="`toolbar-${item.key}`" class="text-sm cursor-pointer">{{ item.label }}</Label>
|
||||
<Label :for="`toolbar-${item.key}`" class="text-sm cursor-pointer">{{ getToolbarVisibilityItemLabel(item) }}</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'navigation'" class="flex flex-col gap-5 py-2">
|
||||
<section v-else-if="activeSettingsTab === 'navigation'" data-settings-search-id="navigation" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('navigation')]">
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("settings.sidebarActivation") }}</Label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
|
|
@ -4410,7 +4623,7 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</section>
|
||||
|
||||
<!-- Data Tab -->
|
||||
<section v-else-if="activeSettingsTab === 'data'" class="flex flex-col gap-5 py-2">
|
||||
<section v-else-if="activeSettingsTab === 'data'" data-settings-search-id="data" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('data')]">
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm font-medium text-muted-foreground">
|
||||
{{ t("settings.dataGridDisplay") }}
|
||||
|
|
@ -4599,7 +4812,7 @@ onUnmounted(cleanupPreviewEditor);
|
|||
<div class="text-sm font-medium text-muted-foreground">
|
||||
{{ t("settings.tableStructureSection") }}
|
||||
</div>
|
||||
<div ref="tableColumnTemplateSectionRef" class="space-y-2 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div ref="tableColumnTemplateSectionRef" data-settings-search-id="table-column-templates" :class="['space-y-2 rounded-md border bg-muted/20 px-3 py-2', settingsSearchTargetClass('table-column-templates')]">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="space-y-1">
|
||||
<Label>{{ t("settings.tableColumnTemplateFields") }}</Label>
|
||||
|
|
@ -4702,7 +4915,7 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'shortcuts'" class="flex flex-col gap-2 py-2">
|
||||
<section v-else-if="activeSettingsTab === 'shortcuts'" data-settings-search-id="shortcuts" :class="['flex flex-col gap-2 py-2', settingsSearchTargetClass('shortcuts')]">
|
||||
<div class="relative">
|
||||
<Search class="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input v-model="shortcutSearchQuery" autocomplete="off" :placeholder="t('settings.shortcutSearchPlaceholder')" class="h-9 pl-9 text-sm" />
|
||||
|
|
@ -4782,7 +4995,7 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</section>
|
||||
|
||||
<!-- Snippets Tab -->
|
||||
<section v-else-if="activeSettingsTab === 'snippets'" class="flex flex-col gap-4 py-2">
|
||||
<section v-else-if="activeSettingsTab === 'snippets'" data-settings-search-id="snippets" :class="['flex flex-col gap-4 py-2', settingsSearchTargetClass('snippets')]">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t("settings.snippetsDescription") }}
|
||||
|
|
@ -4847,18 +5060,18 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'backups' && !isWeb" class="py-2">
|
||||
<section v-else-if="activeSettingsTab === 'backups' && !isWeb" data-settings-search-id="backups" :class="['py-2', settingsSearchTargetClass('backups')]">
|
||||
<ScheduledDatabaseBackupSettings />
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'sync'" class="py-2">
|
||||
<section v-else-if="activeSettingsTab === 'sync'" data-settings-search-id="sync" :class="['py-2', settingsSearchTargetClass('sync')]">
|
||||
<Tabs v-model="syncMethodTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="webdav">WebDAV</TabsTrigger>
|
||||
<TabsTrigger value="snippet">GitHub / Gitee</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="webdav" class="mt-5 space-y-5">
|
||||
<TabsContent value="webdav" data-settings-search-id="sync-webdav" :class="['mt-5 space-y-5', settingsSearchTargetClass('sync-webdav')]">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-2 text-sm font-medium">
|
||||
<Cloud class="h-4 w-4 text-muted-foreground" />
|
||||
|
|
@ -4954,7 +5167,7 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="snippet" class="mt-5 space-y-5">
|
||||
<TabsContent value="snippet" data-settings-search-id="sync-snippet" :class="['mt-5 space-y-5', settingsSearchTargetClass('sync-snippet')]">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2 text-sm font-medium">
|
||||
|
|
@ -5112,7 +5325,7 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</section>
|
||||
|
||||
<!-- AI Settings Tab -->
|
||||
<section v-else-if="activeSettingsTab === 'ai'" class="flex flex-col gap-5 py-2">
|
||||
<section v-else-if="activeSettingsTab === 'ai'" data-settings-search-id="ai" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('ai')]">
|
||||
<!-- Config List View -->
|
||||
<div v-if="aiConfigListMode === 'list'" class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
|
|
@ -5574,7 +5787,7 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'mcp'" class="flex flex-col gap-5 py-2">
|
||||
<section v-else-if="activeSettingsTab === 'mcp'" data-settings-search-id="mcp" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('mcp')]">
|
||||
<div class="rounded-md border bg-muted/20 p-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0 space-y-2">
|
||||
|
|
@ -5922,7 +6135,7 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'security' && isWeb" class="flex flex-col gap-5 py-2">
|
||||
<section v-else-if="activeSettingsTab === 'security' && isWeb" data-settings-search-id="security" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('security')]">
|
||||
<div class="space-y-3">
|
||||
<Label class="text-base">{{ t("auth.changePassword") }}</Label>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
|
|
@ -5937,11 +6150,11 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'tunnels'" class="flex flex-col gap-5 py-2">
|
||||
<section v-else-if="activeSettingsTab === 'tunnels'" data-settings-search-id="tunnels" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('tunnels')]">
|
||||
<TunnelProfileManager />
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'about'" class="flex flex-col gap-5 py-2">
|
||||
<section v-else-if="activeSettingsTab === 'about'" data-settings-search-id="about" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('about')]">
|
||||
<div class="rounded-lg border p-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
|
|
|
|||
|
|
@ -4460,6 +4460,11 @@ export default {
|
|||
},
|
||||
settings: {
|
||||
title: "Settings",
|
||||
searchSettings: "Search settings",
|
||||
searchSettingsNoResults: "No matching settings",
|
||||
searchSettingsResults: "Settings search results",
|
||||
clearSettingsSearch: "Clear settings search",
|
||||
exitSettingsSearch: "Back to settings",
|
||||
editorTab: "Editor",
|
||||
sqlFormatterTab: "SQL Formatter",
|
||||
sqlFormatterImport: "Import config",
|
||||
|
|
|
|||
|
|
@ -4240,6 +4240,11 @@ export default withEnglishFallback({
|
|||
},
|
||||
settings: {
|
||||
title: "Configuración",
|
||||
searchSettings: "Buscar configuración",
|
||||
searchSettingsNoResults: "No hay ajustes coincidentes",
|
||||
searchSettingsResults: "Resultados de búsqueda de configuración",
|
||||
clearSettingsSearch: "Borrar búsqueda de configuración",
|
||||
exitSettingsSearch: "Volver a configuración",
|
||||
editorTab: "Editor",
|
||||
sqlFormatterTab: "Formateador SQL",
|
||||
sqlFormatterImport: "Importar configuración",
|
||||
|
|
|
|||
|
|
@ -4240,6 +4240,11 @@ export default withEnglishFallback({
|
|||
},
|
||||
settings: {
|
||||
title: "Impostazioni",
|
||||
searchSettings: "Cerca impostazioni",
|
||||
searchSettingsNoResults: "Nessuna impostazione corrispondente",
|
||||
searchSettingsResults: "Risultati della ricerca nelle impostazioni",
|
||||
clearSettingsSearch: "Cancella ricerca impostazioni",
|
||||
exitSettingsSearch: "Torna alle impostazioni",
|
||||
editorTab: "Editor",
|
||||
sqlFormatterTab: "Formattatore SQL",
|
||||
sqlFormatterImport: "Importa configurazione",
|
||||
|
|
|
|||
|
|
@ -4281,6 +4281,11 @@ export default withEnglishFallback({
|
|||
},
|
||||
settings: {
|
||||
title: "設定",
|
||||
searchSettings: "設定を検索",
|
||||
searchSettingsNoResults: "一致する設定がありません",
|
||||
searchSettingsResults: "設定の検索結果",
|
||||
clearSettingsSearch: "設定検索をクリア",
|
||||
exitSettingsSearch: "設定に戻る",
|
||||
editorTab: "エディタ",
|
||||
sqlFormatterTab: "SQLフォーマッター",
|
||||
sqlFormatterImport: "設定をインポート",
|
||||
|
|
|
|||
|
|
@ -4202,6 +4202,11 @@ export default withEnglishFallback({
|
|||
},
|
||||
settings: {
|
||||
title: "설정",
|
||||
searchSettings: "설정 검색",
|
||||
searchSettingsNoResults: "일치하는 설정이 없습니다",
|
||||
searchSettingsResults: "설정 검색 결과",
|
||||
clearSettingsSearch: "설정 검색 지우기",
|
||||
exitSettingsSearch: "설정으로 돌아가기",
|
||||
editorTab: "편집기",
|
||||
sqlFormatterTab: "SQL 포매터",
|
||||
sqlFormatterImport: "설정 가져오기",
|
||||
|
|
|
|||
|
|
@ -4242,6 +4242,11 @@ export default withEnglishFallback({
|
|||
},
|
||||
settings: {
|
||||
title: "Configurações",
|
||||
searchSettings: "Pesquisar configurações",
|
||||
searchSettingsNoResults: "Nenhuma configuração encontrada",
|
||||
searchSettingsResults: "Resultados da pesquisa de configurações",
|
||||
clearSettingsSearch: "Limpar pesquisa de configurações",
|
||||
exitSettingsSearch: "Voltar às configurações",
|
||||
editorTab: "Editor",
|
||||
sqlFormatterTab: "Formatador SQL",
|
||||
sqlFormatterImport: "Importar configuração",
|
||||
|
|
|
|||
|
|
@ -4460,6 +4460,11 @@ export default withEnglishFallback({
|
|||
},
|
||||
settings: {
|
||||
title: "设置",
|
||||
searchSettings: "搜索设置",
|
||||
searchSettingsNoResults: "未找到匹配的设置项",
|
||||
searchSettingsResults: "设置搜索结果",
|
||||
clearSettingsSearch: "清除设置搜索",
|
||||
exitSettingsSearch: "返回设置",
|
||||
editorTab: "编辑器",
|
||||
sqlFormatterTab: "SQL 格式化",
|
||||
sqlFormatterImport: "导入配置",
|
||||
|
|
|
|||
|
|
@ -3708,6 +3708,11 @@ export default withEnglishFallback({
|
|||
},
|
||||
settings: {
|
||||
title: "設定",
|
||||
searchSettings: "搜尋設定",
|
||||
searchSettingsNoResults: "找不到相符的設定項目",
|
||||
searchSettingsResults: "設定搜尋結果",
|
||||
clearSettingsSearch: "清除設定搜尋",
|
||||
exitSettingsSearch: "返回設定",
|
||||
editorTab: "編輯器",
|
||||
sqlFormatterTab: "SQL 格式化",
|
||||
sqlFormatterImport: "匯入設定",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { SETTINGS_SEARCH_DEFINITIONS, TOOLBAR_VISIBILITY_ITEMS, createShortcutSettingsSearchDefinitions, createToolbarVisibilitySettingsSearchDefinitions, resolveSettingsSearchEntries, searchSettings, type SettingsCategory, type SettingsSearchDefinition } from "@/lib/settings/settingsSearch";
|
||||
|
||||
const settingsDialogSource = readFileSync(new URL("../../../components/editor/EditorSettingsDialog.vue", import.meta.url), "utf8");
|
||||
|
||||
const categoryLabels = {
|
||||
editor: "Editor",
|
||||
formatter: "SQL Formatter",
|
||||
appearance: "Appearance",
|
||||
navigation: "Navigation",
|
||||
data: "Data",
|
||||
backups: "Backups",
|
||||
tunnels: "Tunnels",
|
||||
shortcuts: "Shortcuts",
|
||||
snippets: "Snippets",
|
||||
sync: "Sync",
|
||||
ai: "AI",
|
||||
mcp: "MCP",
|
||||
security: "Security",
|
||||
about: "About",
|
||||
} satisfies Record<SettingsCategory, string>;
|
||||
|
||||
const translations: Record<string, string> = {
|
||||
font: "Editor font",
|
||||
fontDescription: "Choose the typeface used by the editor",
|
||||
export: "Export options",
|
||||
hidden: "Desktop only",
|
||||
};
|
||||
const translate = (key: string) => translations[key] ?? key;
|
||||
const allCategories = new Set(Object.keys(categoryLabels) as SettingsCategory[]);
|
||||
|
||||
describe("settings search", () => {
|
||||
const definitions: readonly SettingsSearchDefinition[] = [
|
||||
{ id: "font", category: "editor", titleKey: "font", descriptionKey: "fontDescription" },
|
||||
{ id: "export", category: "data", titleKey: "export" },
|
||||
{ id: "desktop", category: "about", titleKey: "hidden", visible: ({ isWeb }) => !isWeb },
|
||||
];
|
||||
|
||||
it("matches translated title, description, and category without changing declared order", () => {
|
||||
const entries = resolveSettingsSearchEntries(definitions, { isWeb: false, visibleCategories: allCategories }, translate, categoryLabels);
|
||||
expect(searchSettings(entries, "TYPEFACE", "en").map((entry) => entry.id)).toEqual(["font"]);
|
||||
expect(searchSettings(entries, "data", "en").map((entry) => entry.id)).toEqual(["export"]);
|
||||
expect(searchSettings(entries, "font", "en").map((entry) => entry.id)).toEqual(["font"]);
|
||||
});
|
||||
|
||||
it("returns no result for empty queries and honours visibility conditions", () => {
|
||||
const webEntries = resolveSettingsSearchEntries(definitions, { isWeb: true, visibleCategories: allCategories }, translate, categoryLabels);
|
||||
expect(searchSettings(webEntries, " ", "en")).toEqual([]);
|
||||
expect(searchSettings(webEntries, "desktop", "en")).toEqual([]);
|
||||
});
|
||||
|
||||
it("matches Chinese text as a Unicode substring", () => {
|
||||
expect(searchSettings([{ id: "font", category: "editor", title: "界面字体", description: "选择应用字体", categoryLabel: "编辑器", targetId: "editor" }], "字体", "zh-CN").map((entry) => entry.id)).toEqual(["font"]);
|
||||
});
|
||||
|
||||
it("filters out unavailable categories and caps results", () => {
|
||||
const entries = resolveSettingsSearchEntries(definitions, { isWeb: false, visibleCategories: new Set<SettingsCategory>(["editor", "data"]) }, translate, categoryLabels);
|
||||
expect(entries.map((entry) => entry.id)).toEqual(["font", "export"]);
|
||||
expect(
|
||||
searchSettings(
|
||||
Array.from({ length: 10 }, (_, index) => ({ ...entries[0], id: String(index) })),
|
||||
"font",
|
||||
"en",
|
||||
),
|
||||
).toHaveLength(8);
|
||||
});
|
||||
|
||||
it("returns matching categories in the navigation order", () => {
|
||||
const entries = resolveSettingsSearchEntries(definitions, { isWeb: false, visibleCategories: new Set<SettingsCategory>(["data", "editor", "about"]) }, translate, categoryLabels);
|
||||
|
||||
expect(entries.map((entry) => entry.id)).toEqual(["export", "font", "desktop"]);
|
||||
});
|
||||
|
||||
it("preserves nested settings routes on resolved entries", () => {
|
||||
const [entry] = resolveSettingsSearchEntries([{ id: "snippet", category: "sync", title: "GitHub", targetId: "sync-snippet", route: { syncMethodTab: "snippet" } }], { isWeb: false, visibleCategories: new Set<SettingsCategory>(["sync"]) }, translate, categoryLabels);
|
||||
|
||||
expect(entry).toMatchObject({ targetId: "sync-snippet", route: { syncMethodTab: "snippet" } });
|
||||
});
|
||||
|
||||
it("uses the open state for result visibility and applies nested routes before revealing targets", () => {
|
||||
expect(settingsDialogSource).toContain("const settingsSearchVisible = computed(() => settingsSearchOpen.value && settingsSearchActive.value)");
|
||||
expect(settingsDialogSource).toContain('v-if="settingsSearchVisible" id="settings-search-results"');
|
||||
expect(settingsDialogSource).toMatch(/function applySettingsSearchRoute[\s\S]*?syncMethodTab\.value = result\.route\.syncMethodTab/);
|
||||
expect(settingsDialogSource).toMatch(/async function selectSettingsSearchResult[\s\S]*?applySettingsSearchRoute\(result\)[\s\S]*?revealSettingsSearchTarget/);
|
||||
});
|
||||
|
||||
it("derives one search result for every built-in shortcut", () => {
|
||||
expect(
|
||||
createShortcutSettingsSearchDefinitions([
|
||||
{ id: "formatSql", labelKey: "settings.shortcutFormatSql" },
|
||||
{ id: "toggleLineComment", labelKey: "settings.shortcutToggleLineComment" },
|
||||
]),
|
||||
).toEqual([
|
||||
{ id: "shortcut-formatSql", category: "shortcuts", titleKey: "settings.shortcutFormatSql", targetId: "shortcuts", shortcutId: "formatSql" },
|
||||
{ id: "shortcut-toggleLineComment", category: "shortcuts", titleKey: "settings.shortcutToggleLineComment", targetId: "shortcuts", shortcutId: "toggleLineComment" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("derives a search result for every toolbar visibility control", () => {
|
||||
const definitions = createToolbarVisibilitySettingsSearchDefinitions();
|
||||
|
||||
expect(definitions).toHaveLength(TOOLBAR_VISIBILITY_ITEMS.length);
|
||||
expect(definitions.map((definition) => definition.id)).toEqual(TOOLBAR_VISIBILITY_ITEMS.map((item) => `appearance-toolbar-${item.key}`));
|
||||
expect(definitions).toContainEqual({ id: "appearance-toolbar-dataTransfer", category: "appearance", titleKey: "transfer.dataTransfer", targetId: "appearance" });
|
||||
expect(definitions).toContainEqual({ id: "appearance-toolbar-ai", category: "appearance", title: "AI", targetId: "appearance" });
|
||||
});
|
||||
|
||||
it("indexes the existing descriptions for fixed appearance controls", () => {
|
||||
const descriptionTranslations: Record<string, string> = {
|
||||
"settings.uiScale": "Interface scale",
|
||||
"settings.uiScaleDescription": "Scale the interface for high-DPI displays",
|
||||
"settings.uiFontFamily": "Interface font",
|
||||
"settings.uiFontFamilyDescription": "Applies to the toolbar and dialogs",
|
||||
"settings.showTrayIcon": "Show tray icon",
|
||||
"settings.showTrayIconDescription": "Keep DBX hidden in the background",
|
||||
};
|
||||
const entries = resolveSettingsSearchEntries(SETTINGS_SEARCH_DEFINITIONS, { isWeb: false, visibleCategories: new Set<SettingsCategory>(["appearance"]) }, (key) => descriptionTranslations[key] ?? key, categoryLabels);
|
||||
|
||||
expect(searchSettings(entries, "high-DPI", "en").map((entry) => entry.id)).toEqual(["appearance-ui-scale"]);
|
||||
expect(searchSettings(entries, "toolbar and dialogs", "en").map((entry) => entry.id)).toEqual(["appearance-ui-font"]);
|
||||
expect(searchSettings(entries, "hidden in the background", "en").map((entry) => entry.id)).toEqual(["appearance-tray"]);
|
||||
});
|
||||
|
||||
it("activates result buttons through click for keyboard and assistive technology", () => {
|
||||
expect(settingsDialogSource).toMatch(/role="option"[\s\S]*?@mousedown\.prevent[\s\S]*?@click="void selectSettingsSearchResult\(result\)"/);
|
||||
});
|
||||
|
||||
it("registers every fixed settings control that needs a dedicated search result", () => {
|
||||
const expectedControls: ReadonlyArray<Pick<SettingsSearchDefinition, "titleKey" | "category" | "targetId">> = [
|
||||
{ titleKey: "settings.savedSqlOpenTarget", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.confirmDangerousSqlExecution", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.continueOnErrorOnBatch", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.dataGridQuickEntry", category: "appearance", targetId: "appearance" },
|
||||
{ titleKey: "transfer.dataTransfer", category: "appearance", targetId: "appearance" },
|
||||
{ titleKey: "toolbar.driverManager", category: "appearance", targetId: "appearance" },
|
||||
{ titleKey: "toolbar.theme", category: "appearance", targetId: "appearance" },
|
||||
{ titleKey: "settings.sidebarObjectInfoMode", category: "navigation", targetId: "navigation" },
|
||||
{ titleKey: "settings.insertSpaceAfterCompletion", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.autoAliasTables", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.clickTableNavigationTarget", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.sqlFormatterKeywordCase", category: "formatter", targetId: "formatter" },
|
||||
{ titleKey: "settings.sqlFormatterFunctionCase", category: "formatter", targetId: "formatter" },
|
||||
{ titleKey: "settings.sqlFormatterDataTypeCase", category: "formatter", targetId: "formatter" },
|
||||
{ titleKey: "settings.sqlFormatterIdentifierCase", category: "formatter", targetId: "formatter" },
|
||||
{ titleKey: "settings.sqlFormatterIndent", category: "formatter", targetId: "formatter" },
|
||||
{ titleKey: "settings.sqlFormatterTabWidth", category: "formatter", targetId: "formatter" },
|
||||
{ titleKey: "settings.sqlFormatterIndentStyle", category: "formatter", targetId: "formatter" },
|
||||
{ titleKey: "settings.sqlFormatterLogicalOperatorNewline", category: "formatter", targetId: "formatter" },
|
||||
{ titleKey: "settings.sqlFormatterExpressionWidth", category: "formatter", targetId: "formatter" },
|
||||
{ titleKey: "settings.sqlFormatterLinesBetweenQueries", category: "formatter", targetId: "formatter" },
|
||||
{ titleKey: "settings.sqlFormatterDenseOperators", category: "formatter", targetId: "formatter" },
|
||||
{ titleKey: "settings.sqlFormatterNewlineBeforeSemicolon", category: "formatter", targetId: "formatter" },
|
||||
{ titleKey: "settings.sqlFormatterParamTypes", category: "formatter", targetId: "formatter" },
|
||||
{ titleKey: "settings.routineSourceOpenMode", category: "navigation", targetId: "navigation" },
|
||||
{ titleKey: "settings.disconnectTabHandlingMode", category: "navigation", targetId: "navigation" },
|
||||
{ titleKey: "settings.compactColumnHeaderActions", category: "appearance", targetId: "appearance" },
|
||||
{ titleKey: "settings.infiniteScrollMaxRows", category: "appearance", targetId: "appearance" },
|
||||
{ titleKey: "settings.globalDateTimeDisplayFormat", category: "data", targetId: "data" },
|
||||
{ titleKey: "settings.globalDateTimeExportFormat", category: "data", targetId: "data" },
|
||||
{ titleKey: "settings.globalDateTimeImportFormat", category: "data", targetId: "data" },
|
||||
{ titleKey: "settings.exportRowLimitEnabled", category: "data", targetId: "data" },
|
||||
{ titleKey: "settings.exportRowLimit", category: "data", targetId: "data" },
|
||||
{ titleKey: "settings.queryExportKeysetOptimizationEnabled", category: "data", targetId: "data" },
|
||||
{ titleKey: "ai.maxAgentTurns", category: "ai", targetId: "ai" },
|
||||
{ titleKey: "ai.maxRetriesGlobal", category: "ai", targetId: "ai" },
|
||||
{ titleKey: "ai.globalInstructions", category: "ai", targetId: "ai" },
|
||||
];
|
||||
|
||||
for (const expectedControl of expectedControls) {
|
||||
expect(SETTINGS_SEARCH_DEFINITIONS).toContainEqual(expect.objectContaining(expectedControl));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
export type SettingsCategory = "editor" | "formatter" | "appearance" | "navigation" | "data" | "backups" | "tunnels" | "shortcuts" | "snippets" | "sync" | "ai" | "mcp" | "security" | "about";
|
||||
|
||||
export interface SettingsSearchContext {
|
||||
isWeb: boolean;
|
||||
visibleCategories: ReadonlySet<SettingsCategory>;
|
||||
}
|
||||
|
||||
export interface SettingsSearchDefinition {
|
||||
id: string;
|
||||
category: SettingsCategory;
|
||||
/** A localized title key, or a literal title for product names such as AI. */
|
||||
titleKey?: string;
|
||||
title?: string;
|
||||
descriptionKey?: string;
|
||||
/** Identifies a built-in shortcut row so its local filter can be restored on navigation. */
|
||||
shortcutId?: string;
|
||||
route?: SettingsSearchRoute;
|
||||
/** The fixed settings-group anchor; omitted values use the owning tab. */
|
||||
targetId?: string;
|
||||
visible?: (context: SettingsSearchContext) => boolean;
|
||||
}
|
||||
|
||||
export interface SettingsSearchEntry {
|
||||
id: string;
|
||||
category: SettingsCategory;
|
||||
title: string;
|
||||
description: string;
|
||||
categoryLabel: string;
|
||||
targetId: string;
|
||||
shortcutId?: string;
|
||||
route?: SettingsSearchRoute;
|
||||
}
|
||||
|
||||
export interface SettingsSearchRoute {
|
||||
syncMethodTab?: "webdav" | "snippet";
|
||||
}
|
||||
|
||||
export type Translate = (key: string) => string;
|
||||
|
||||
type ToolbarVisibilityItemKey = "dataTransfer" | "driverManager" | "sqlFile" | "schemaDiff" | "dataCompare" | "checkUpdates" | "sqlLibrary" | "sqlFileTree" | "history" | "ai" | "theme" | "github";
|
||||
|
||||
export type ToolbarVisibilityItem = { key: ToolbarVisibilityItemKey; titleKey: string; title?: never } | { key: ToolbarVisibilityItemKey; title: string; titleKey?: never };
|
||||
|
||||
/**
|
||||
* The toolbar visibility controls and their search entries use this same list.
|
||||
* Keeping the labels here prevents a newly added toggle from being absent from
|
||||
* settings search.
|
||||
*/
|
||||
export const TOOLBAR_VISIBILITY_ITEMS: readonly ToolbarVisibilityItem[] = [
|
||||
{ key: "dataTransfer", titleKey: "transfer.dataTransfer" },
|
||||
{ key: "driverManager", titleKey: "toolbar.driverManager" },
|
||||
{ key: "sqlFile", titleKey: "sqlFile.title" },
|
||||
{ key: "schemaDiff", titleKey: "diff.title" },
|
||||
{ key: "dataCompare", titleKey: "dataCompare.title" },
|
||||
{ key: "checkUpdates", titleKey: "updates.check" },
|
||||
{ key: "sqlLibrary", titleKey: "sqlLibrary.title" },
|
||||
{ key: "sqlFileTree", titleKey: "sqlFileTree.title" },
|
||||
{ key: "history", titleKey: "history.title" },
|
||||
{ key: "ai", title: "AI" },
|
||||
{ key: "theme", titleKey: "toolbar.theme" },
|
||||
{ key: "github", title: "GitHub" },
|
||||
];
|
||||
|
||||
export function toolbarVisibilityItemLabel(item: ToolbarVisibilityItem, translate: Translate): string {
|
||||
return item.titleKey ? translate(item.titleKey) : (item.title ?? "");
|
||||
}
|
||||
|
||||
export function createToolbarVisibilitySettingsSearchDefinitions(items: readonly ToolbarVisibilityItem[] = TOOLBAR_VISIBILITY_ITEMS): SettingsSearchDefinition[] {
|
||||
return items.map((item) => ({
|
||||
id: `appearance-toolbar-${item.key}`,
|
||||
category: "appearance",
|
||||
...(item.titleKey ? { titleKey: item.titleKey } : { title: item.title }),
|
||||
targetId: "appearance",
|
||||
}));
|
||||
}
|
||||
|
||||
const desktopOnly = (context: SettingsSearchContext) => !context.isWeb;
|
||||
const webOnly = (context: SettingsSearchContext) => context.isWeb;
|
||||
|
||||
export interface ShortcutSearchDefinitionSource {
|
||||
id: string;
|
||||
labelKey: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Built-in shortcut definitions are already the canonical list of fixed
|
||||
* shortcut settings. Keep their search entries derived from that list so new
|
||||
* shortcuts cannot be omitted from global settings search.
|
||||
*/
|
||||
export function createShortcutSettingsSearchDefinitions(shortcuts: readonly ShortcutSearchDefinitionSource[]): SettingsSearchDefinition[] {
|
||||
return shortcuts.map((shortcut) => ({
|
||||
id: `shortcut-${shortcut.id}`,
|
||||
category: "shortcuts",
|
||||
titleKey: shortcut.labelKey,
|
||||
targetId: "shortcuts",
|
||||
shortcutId: shortcut.id,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed settings groups. Dynamic rows (snippets, prompt templates and provider
|
||||
* configurations) deliberately resolve to their management entry instead.
|
||||
*/
|
||||
export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] = [
|
||||
{ id: "editor-font", category: "editor", titleKey: "settings.fontFamily", targetId: "editor" },
|
||||
{ id: "editor-theme", category: "editor", titleKey: "settings.theme", targetId: "editor" },
|
||||
{ id: "editor-font-size", category: "editor", titleKey: "settings.fontSize", targetId: "editor" },
|
||||
{ id: "editor-execute-mode", category: "editor", titleKey: "settings.executeMode", targetId: "editor" },
|
||||
{ id: "editor-execution-target", category: "editor", titleKey: "settings.showExecutionTargetPicker", descriptionKey: "settings.showExecutionTargetPickerDescription", targetId: "editor" },
|
||||
{ id: "editor-run-buttons", category: "editor", titleKey: "settings.showStatementRunButtons", descriptionKey: "settings.showStatementRunButtonsDescription", targetId: "editor" },
|
||||
{ id: "editor-statement-frame", category: "editor", titleKey: "settings.showCurrentStatementFrame", descriptionKey: "settings.showCurrentStatementFrameDescription", targetId: "editor" },
|
||||
{ id: "editor-value-hints", category: "editor", titleKey: "settings.showInsertValueHints", descriptionKey: "settings.showInsertValueHintsDescription", targetId: "editor" },
|
||||
{ id: "editor-word-wrap", category: "editor", titleKey: "settings.wordWrap", descriptionKey: "settings.wordWrapDescription", targetId: "editor" },
|
||||
{ id: "editor-vim", category: "editor", titleKey: "settings.vimMode", descriptionKey: "settings.vimModeDescription", targetId: "editor" },
|
||||
{ id: "editor-brackets", category: "editor", titleKey: "settings.autoCloseBrackets", descriptionKey: "settings.autoCloseBracketsDescription", targetId: "editor" },
|
||||
{ id: "editor-completion-spacing", category: "editor", titleKey: "settings.insertSpaceAfterCompletion", descriptionKey: "settings.insertSpaceAfterCompletionDescription", targetId: "editor" },
|
||||
{ id: "editor-auto-alias", category: "editor", titleKey: "settings.autoAliasTables", descriptionKey: "settings.autoAliasTablesDescription", targetId: "editor" },
|
||||
{ id: "editor-unsaved-close", category: "editor", titleKey: "settings.confirmUnsavedSqlClose", descriptionKey: "settings.confirmUnsavedSqlCloseDescription", targetId: "editor" },
|
||||
{ id: "editor-prefill-query", category: "editor", titleKey: "settings.prefillNewQueryWithSelect", descriptionKey: "settings.prefillNewQueryWithSelectDescription", targetId: "editor" },
|
||||
{ id: "editor-diagnostics", category: "editor", titleKey: "settings.sqlSemanticDiagnosticsEnabled", descriptionKey: "settings.sqlSemanticDiagnosticsEnabledDescription", targetId: "editor" },
|
||||
{ id: "editor-sql-variables", category: "editor", titleKey: "settings.sqlVariableSyntax", descriptionKey: "settings.sqlVariableSyntaxDescription", targetId: "editor" },
|
||||
{ id: "editor-saved-sql-target", category: "editor", titleKey: "settings.savedSqlOpenTarget", targetId: "editor" },
|
||||
{ id: "editor-confirm-dangerous-sql", category: "editor", titleKey: "settings.confirmDangerousSqlExecution", descriptionKey: "settings.confirmDangerousSqlExecutionDescription", targetId: "editor" },
|
||||
{ id: "editor-continue-batch-on-error", category: "editor", titleKey: "settings.continueOnErrorOnBatch", descriptionKey: "settings.continueOnErrorOnBatchDescription", targetId: "editor" },
|
||||
{ id: "editor-table-click-navigation", category: "editor", titleKey: "settings.clickTableNavigationTarget", descriptionKey: "settings.clickTableNavigationTargetDescription", targetId: "editor" },
|
||||
{ id: "formatter", category: "formatter", titleKey: "settings.sqlFormatterTab", targetId: "formatter" },
|
||||
{ id: "formatter-shortcuts", category: "formatter", titleKey: "settings.sqlFormatterEditorShortcuts", targetId: "formatter" },
|
||||
{ id: "formatter-keyword-case", category: "formatter", titleKey: "settings.sqlFormatterKeywordCase", targetId: "formatter" },
|
||||
{ id: "formatter-function-case", category: "formatter", titleKey: "settings.sqlFormatterFunctionCase", targetId: "formatter" },
|
||||
{ id: "formatter-data-type-case", category: "formatter", titleKey: "settings.sqlFormatterDataTypeCase", targetId: "formatter" },
|
||||
{ id: "formatter-identifier-case", category: "formatter", titleKey: "settings.sqlFormatterIdentifierCase", targetId: "formatter" },
|
||||
{ id: "formatter-indent", category: "formatter", titleKey: "settings.sqlFormatterIndent", targetId: "formatter" },
|
||||
{ id: "formatter-tab-width", category: "formatter", titleKey: "settings.sqlFormatterTabWidth", targetId: "formatter" },
|
||||
{ id: "formatter-indent-style", category: "formatter", titleKey: "settings.sqlFormatterIndentStyle", targetId: "formatter" },
|
||||
{ id: "formatter-logical-operator-newline", category: "formatter", titleKey: "settings.sqlFormatterLogicalOperatorNewline", targetId: "formatter" },
|
||||
{ id: "formatter-expression-width", category: "formatter", titleKey: "settings.sqlFormatterExpressionWidth", targetId: "formatter" },
|
||||
{ id: "formatter-lines-between-queries", category: "formatter", titleKey: "settings.sqlFormatterLinesBetweenQueries", targetId: "formatter" },
|
||||
{ id: "formatter-dense-operators", category: "formatter", titleKey: "settings.sqlFormatterDenseOperators", targetId: "formatter" },
|
||||
{ id: "formatter-newline-before-semicolon", category: "formatter", titleKey: "settings.sqlFormatterNewlineBeforeSemicolon", targetId: "formatter" },
|
||||
{ id: "formatter-param-types", category: "formatter", titleKey: "settings.sqlFormatterParamTypes", targetId: "formatter" },
|
||||
{ id: "appearance-language", category: "appearance", titleKey: "settings.languageTitle", targetId: "appearance" },
|
||||
{ id: "appearance-theme", category: "appearance", titleKey: "settings.theme", targetId: "appearance" },
|
||||
{ id: "appearance-color-theme", category: "appearance", titleKey: "settings.colorTheme", targetId: "appearance" },
|
||||
{ id: "appearance-ui-scale", category: "appearance", titleKey: "settings.uiScale", descriptionKey: "settings.uiScaleDescription", targetId: "appearance" },
|
||||
{ id: "appearance-ui-font", category: "appearance", titleKey: "settings.uiFontFamily", descriptionKey: "settings.uiFontFamilyDescription", targetId: "appearance" },
|
||||
{ id: "appearance-grid-font", category: "appearance", titleKey: "settings.dataGridFontFamily", descriptionKey: "settings.dataGridFontFamilyDescription", targetId: "appearance" },
|
||||
{ id: "appearance-corners", category: "appearance", titleKey: "settings.cornerStyle", targetId: "appearance" },
|
||||
{ id: "appearance-layout", category: "appearance", titleKey: "settings.appLayout", targetId: "appearance" },
|
||||
{ id: "appearance-tab-layout", category: "appearance", titleKey: "settings.tabLayout", targetId: "appearance" },
|
||||
{ id: "appearance-icons", category: "appearance", titleKey: "settings.iconTheme", targetId: "appearance", visible: desktopOnly },
|
||||
{ id: "appearance-tray", category: "appearance", titleKey: "settings.showTrayIcon", descriptionKey: "settings.showTrayIconDescription", targetId: "appearance", visible: desktopOnly },
|
||||
{ id: "appearance-quit", category: "appearance", titleKey: "settings.quitOnClose", descriptionKey: "settings.quitOnCloseDescription", targetId: "appearance", visible: desktopOnly },
|
||||
{ id: "appearance-updates", category: "appearance", titleKey: "settings.updateNotificationsEnabled", descriptionKey: "settings.updateNotificationsEnabledDescription", targetId: "appearance" },
|
||||
{ id: "appearance-debug-logs", category: "appearance", titleKey: "settings.debugLoggingEnabled", descriptionKey: "settings.debugLoggingEnabledDescription", targetId: "appearance", visible: desktopOnly },
|
||||
{ id: "navigation", category: "navigation", titleKey: "settings.navigationTab", targetId: "navigation" },
|
||||
{ id: "navigation-sidebar", category: "navigation", titleKey: "settings.sidebarActivation", targetId: "navigation" },
|
||||
{ id: "navigation-routine-source", category: "navigation", titleKey: "settings.routineSourceOpenMode", descriptionKey: "settings.routineSourceOpenModeDescription", targetId: "navigation" },
|
||||
{ id: "navigation-reuse-data-tab", category: "navigation", titleKey: "settings.reuseDataTab", descriptionKey: "settings.reuseDataTabDescription", targetId: "navigation" },
|
||||
{ id: "navigation-object-display", category: "navigation", titleKey: "settings.sidebarObjectDisplay", targetId: "navigation" },
|
||||
{ id: "navigation-object-info", category: "navigation", titleKey: "settings.sidebarObjectInfoMode", descriptionKey: "settings.sidebarObjectInfoModeDescription", targetId: "navigation" },
|
||||
{ id: "navigation-table-search", category: "navigation", titleKey: "settings.sidebarTableSearchEnabled", descriptionKey: "settings.sidebarTableSearchEnabledDescription", targetId: "navigation" },
|
||||
{ id: "navigation-active-node", category: "navigation", titleKey: "settings.autoSelectActiveSidebarNode", descriptionKey: "settings.autoSelectActiveSidebarNodeDescription", targetId: "navigation" },
|
||||
{ id: "navigation-tabs-restore", category: "navigation", titleKey: "settings.openTabsRestoreMode", descriptionKey: "settings.openTabsRestoreModeDescription", targetId: "navigation" },
|
||||
{ id: "navigation-sidebar-scroll", category: "navigation", titleKey: "settings.sidebarAllowHorizontalScroll", descriptionKey: "settings.sidebarAllowHorizontalScrollDescription", targetId: "navigation" },
|
||||
{ id: "navigation-hidden-tables", category: "navigation", titleKey: "settings.sidebarHiddenTablePrefixes", descriptionKey: "settings.sidebarHiddenTablePrefixesDescription", targetId: "navigation" },
|
||||
{ id: "navigation-table-page-size", category: "navigation", titleKey: "settings.sidebarTablePageSize", descriptionKey: "settings.sidebarTablePageSizeDescription", targetId: "navigation" },
|
||||
{ id: "navigation-disconnect-tabs", category: "navigation", titleKey: "settings.disconnectTabHandlingMode", descriptionKey: "settings.disconnectTabHandlingModeDescription", targetId: "navigation" },
|
||||
{ id: "data-page-size", category: "data", titleKey: "settings.tableOpenPageSize", descriptionKey: "settings.tableOpenPageSizeDescription", targetId: "data" },
|
||||
{ id: "appearance-header-comments", category: "appearance", titleKey: "settings.showColumnCommentsInHeader", descriptionKey: "settings.showColumnCommentsInHeaderDescription", targetId: "appearance" },
|
||||
{ id: "appearance-header-types", category: "appearance", titleKey: "settings.showColumnTypesInHeader", descriptionKey: "settings.showColumnTypesInHeaderDescription", targetId: "appearance" },
|
||||
{ id: "appearance-compact-header-actions", category: "appearance", titleKey: "settings.compactColumnHeaderActions", descriptionKey: "settings.compactColumnHeaderActionsDescription", targetId: "appearance" },
|
||||
{ id: "appearance-auto-total", category: "appearance", titleKey: "settings.autoCalculateTotalRows", descriptionKey: "settings.autoCalculateTotalRowsDescription", targetId: "appearance" },
|
||||
{ id: "appearance-infinite-scroll", category: "appearance", titleKey: "settings.infiniteScroll", descriptionKey: "settings.infiniteScrollDescription", targetId: "appearance" },
|
||||
{ id: "appearance-infinite-scroll-limit", category: "appearance", titleKey: "settings.infiniteScrollMaxRows", descriptionKey: "settings.infiniteScrollMaxRowsDescription", targetId: "appearance" },
|
||||
{ id: "appearance-auto-transpose", category: "appearance", titleKey: "settings.dataGridAutoTransposeSingleRow", descriptionKey: "settings.dataGridAutoTransposeSingleRowDescription", targetId: "appearance" },
|
||||
{ id: "appearance-quick-entry", category: "appearance", titleKey: "settings.dataGridQuickEntry", descriptionKey: "settings.dataGridQuickEntryDescription", targetId: "appearance" },
|
||||
{ id: "appearance-toolbar", category: "appearance", titleKey: "settings.toolbarTitle", descriptionKey: "settings.toolbarHiddenHint", targetId: "appearance" },
|
||||
{ id: "appearance-exclusive-sidebar-panels", category: "appearance", titleKey: "settings.exclusiveRightSidebarPanels", descriptionKey: "settings.exclusiveRightSidebarPanelsDescription", targetId: "appearance" },
|
||||
...createToolbarVisibilitySettingsSearchDefinitions(),
|
||||
{ id: "data-datetime", category: "data", titleKey: "settings.dateTimeSection", targetId: "data" },
|
||||
{ id: "data-datetime-display-format", category: "data", titleKey: "settings.globalDateTimeDisplayFormat", descriptionKey: "settings.globalDateTimeDisplayFormatDescription", targetId: "data" },
|
||||
{ id: "data-datetime-export-format", category: "data", titleKey: "settings.globalDateTimeExportFormat", descriptionKey: "settings.globalDateTimeExportFormatDescription", targetId: "data" },
|
||||
{ id: "data-datetime-import-format", category: "data", titleKey: "settings.globalDateTimeImportFormat", descriptionKey: "settings.globalDateTimeImportFormatDescription", targetId: "data" },
|
||||
{ id: "data-export", category: "data", titleKey: "settings.exportSection", targetId: "data" },
|
||||
{ id: "data-export-batch", category: "data", titleKey: "settings.exportBatchSize", descriptionKey: "settings.exportBatchSizeDescription", targetId: "data" },
|
||||
{ id: "data-export-row-limit-enabled", category: "data", titleKey: "settings.exportRowLimitEnabled", descriptionKey: "settings.exportRowLimitEnabledDescription", targetId: "data" },
|
||||
{ id: "data-export-row-limit", category: "data", titleKey: "settings.exportRowLimit", descriptionKey: "settings.exportRowLimitDescription", targetId: "data" },
|
||||
{ id: "data-export-keyset", category: "data", titleKey: "settings.queryExportKeysetOptimizationEnabled", descriptionKey: "settings.queryExportKeysetOptimizationEnabledDescription", targetId: "data" },
|
||||
{ id: "data-table-template", category: "data", titleKey: "settings.tableColumnTemplateFields", descriptionKey: "settings.tableColumnTemplateFieldsDescription", targetId: "table-column-templates" },
|
||||
{ id: "data-duckdb", category: "data", titleKey: "settings.duckDbWorkerProcessIsolation", descriptionKey: "settings.duckDbWorkerProcessIsolationDescription", targetId: "data", visible: desktopOnly },
|
||||
{ id: "data-duckdb-process-limit", category: "data", titleKey: "settings.duckDbWorkerMaxProcesses", descriptionKey: "settings.duckDbWorkerMaxProcessesDescription", targetId: "data", visible: desktopOnly },
|
||||
{ id: "backups", category: "backups", titleKey: "databaseBackup.title", targetId: "backups", visible: desktopOnly },
|
||||
{ id: "tunnels", category: "tunnels", titleKey: "settings.tunnelsTab", targetId: "tunnels" },
|
||||
{ id: "shortcuts", category: "shortcuts", titleKey: "settings.shortcutsTab", targetId: "shortcuts" },
|
||||
{ id: "snippets", category: "snippets", titleKey: "settings.snippetsTab", descriptionKey: "settings.snippetsDescription", targetId: "snippets" },
|
||||
{ id: "sync-webdav", category: "sync", titleKey: "settings.syncWebDavTitle", descriptionKey: "settings.syncWebDavDescription", targetId: "sync-webdav", route: { syncMethodTab: "webdav" }, visible: desktopOnly },
|
||||
{ id: "sync-webdav-endpoint", category: "sync", titleKey: "settings.syncEndpoint", targetId: "sync-webdav", route: { syncMethodTab: "webdav" }, visible: desktopOnly },
|
||||
{ id: "sync-webdav-username", category: "sync", titleKey: "settings.syncUsername", targetId: "sync-webdav", route: { syncMethodTab: "webdav" }, visible: desktopOnly },
|
||||
{ id: "sync-webdav-password", category: "sync", titleKey: "settings.syncPassword", targetId: "sync-webdav", route: { syncMethodTab: "webdav" }, visible: desktopOnly },
|
||||
{ id: "sync-webdav-remote-path", category: "sync", titleKey: "settings.syncRemotePath", targetId: "sync-webdav", route: { syncMethodTab: "webdav" }, visible: desktopOnly },
|
||||
{ id: "sync-webdav-auto-upload", category: "sync", titleKey: "settings.syncAutoUploadInterval", targetId: "sync-webdav", route: { syncMethodTab: "webdav" }, visible: desktopOnly },
|
||||
{ id: "sync-snippet", category: "sync", titleKey: "settings.syncSnippetTitle", descriptionKey: "settings.syncSnippetDescription", targetId: "sync-snippet", route: { syncMethodTab: "snippet" }, visible: desktopOnly },
|
||||
{ id: "sync-snippet-provider", category: "sync", titleKey: "settings.syncSnippetProvider", targetId: "sync-snippet", route: { syncMethodTab: "snippet" }, visible: desktopOnly },
|
||||
{ id: "sync-snippet-id", category: "sync", titleKey: "settings.syncSnippetId", targetId: "sync-snippet", route: { syncMethodTab: "snippet" }, visible: desktopOnly },
|
||||
{ id: "sync-snippet-token", category: "sync", titleKey: "settings.syncSnippetToken", targetId: "sync-snippet", route: { syncMethodTab: "snippet" }, visible: desktopOnly },
|
||||
{ id: "sync-secrets", category: "sync", titleKey: "settings.syncSecrets", targetId: "sync", visible: desktopOnly },
|
||||
{ id: "sync-secrets-passphrase", category: "sync", titleKey: "settings.syncSecretsPassphrase", targetId: "sync", visible: desktopOnly },
|
||||
{ id: "ai-config", category: "ai", titleKey: "ai.configList", targetId: "ai" },
|
||||
{ id: "ai-prompts", category: "ai", titleKey: "ai.promptTemplates", descriptionKey: "ai.promptTemplatesDescription", targetId: "ai" },
|
||||
{ id: "ai-agent-turn-limit", category: "ai", titleKey: "ai.maxAgentTurns", descriptionKey: "ai.maxAgentTurnsDescription", targetId: "ai" },
|
||||
{ id: "ai-global-retries", category: "ai", titleKey: "ai.maxRetriesGlobal", descriptionKey: "ai.maxRetriesGlobalDescription", targetId: "ai" },
|
||||
{ id: "ai-global-instructions", category: "ai", titleKey: "ai.globalInstructions", descriptionKey: "ai.globalInstructionsDescription", targetId: "ai" },
|
||||
{ id: "mcp", category: "mcp", titleKey: "settings.mcpTitle", descriptionKey: "settings.mcpDescription", targetId: "mcp" },
|
||||
{ id: "mcp-bin-path", category: "mcp", titleKey: "settings.mcpBinPath", targetId: "mcp" },
|
||||
{ id: "mcp-permissions", category: "mcp", titleKey: "settings.mcpExecutionMode", descriptionKey: "settings.mcpExecutionModeDescription", targetId: "mcp" },
|
||||
{ id: "mcp-config", category: "mcp", titleKey: "settings.mcpConfig", targetId: "mcp" },
|
||||
{ id: "security", category: "security", titleKey: "settings.securityTab", targetId: "security", visible: webOnly },
|
||||
{ id: "security-password", category: "security", titleKey: "auth.changePassword", targetId: "security", visible: webOnly },
|
||||
{ id: "about-support", category: "about", titleKey: "settings.supportInfoTitle", descriptionKey: "settings.supportInfoDescription", targetId: "about" },
|
||||
{ id: "about-update", category: "about", titleKey: "settings.updateDownloadSource", descriptionKey: "settings.updateDownloadSourceDescription", targetId: "about" },
|
||||
];
|
||||
|
||||
export function resolveSettingsSearchEntries(definitions: readonly SettingsSearchDefinition[], context: SettingsSearchContext, translate: Translate, categoryLabels: Readonly<Record<SettingsCategory, string>>): SettingsSearchEntry[] {
|
||||
const categoryOrder = new Map(Array.from(context.visibleCategories, (category, index) => [category, index]));
|
||||
|
||||
return definitions
|
||||
.filter((definition) => context.visibleCategories.has(definition.category) && (definition.visible?.(context) ?? true))
|
||||
.map((definition) => ({
|
||||
id: definition.id,
|
||||
category: definition.category,
|
||||
title: definition.titleKey ? translate(definition.titleKey) : (definition.title ?? ""),
|
||||
description: definition.descriptionKey ? translate(definition.descriptionKey) : "",
|
||||
categoryLabel: categoryLabels[definition.category],
|
||||
targetId: definition.targetId ?? definition.category,
|
||||
shortcutId: definition.shortcutId,
|
||||
route: definition.route,
|
||||
}))
|
||||
.sort((left, right) => (categoryOrder.get(left.category) ?? Number.MAX_SAFE_INTEGER) - (categoryOrder.get(right.category) ?? Number.MAX_SAFE_INTEGER));
|
||||
}
|
||||
|
||||
export function searchSettings(entries: readonly SettingsSearchEntry[], query: string, locale: string, limit = 8): SettingsSearchEntry[] {
|
||||
const normalizedQuery = query.trim().toLocaleLowerCase(locale);
|
||||
if (!normalizedQuery) return [];
|
||||
return entries.filter((entry) => `${entry.title}\n${entry.description}\n${entry.categoryLabel}`.toLocaleLowerCase(locale).includes(normalizedQuery)).slice(0, limit);
|
||||
}
|
||||
|
|
@ -43,6 +43,29 @@
|
|||
|
||||
@import "shadcn-vue/tailwind.css";
|
||||
|
||||
@keyframes settings-search-highlight-breathe {
|
||||
0%,
|
||||
100% {
|
||||
background-color: color-mix(in srgb, var(--primary) 4%, transparent);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 34%, transparent);
|
||||
}
|
||||
50% {
|
||||
background-color: color-mix(in srgb, var(--primary) 9%, transparent);
|
||||
box-shadow: inset 0 0 0 3px color-mix(in srgb, var(--primary) 54%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-search-highlight-breathe {
|
||||
animation: settings-search-highlight-breathe 900ms cubic-bezier(0.4, 0, 0.2, 1) 3;
|
||||
will-change: background-color, box-shadow;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.settings-search-highlight-breathe {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
|
|
|
|||
Loading…
Reference in New Issue