Files
dbx/apps/desktop/src/lib/settings/editorSettingsDraft.ts
T

158 lines
5.5 KiB
TypeScript

import type { EditorSettings } from "@/stores/settingsStore";
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
import { normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit";
import { normalizeCompletionTriggerMode } from "@/lib/sql/sqlCompletionTriggerPolicy";
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
export const EDITOR_SETTINGS_DRAFT_KEYS = [
"fontFamily",
"fontSize",
"tableFontFamily",
"uiFontFamily",
"uiScale",
"theme",
"customThemes",
"activeCustomThemeId",
"executeMode",
"executeAllOnBlankLine",
"showExecutionTargetPicker",
"showStatementRunButtons",
"showLineNumbers",
"showCurrentStatementFrame",
"showInsertValueHints",
"autoAliasTables",
"insertSpaceAfterCompletion",
"sortCompletionColumnsAlphabetically",
"selectFirstCompletionOnOpen",
"wordWrap",
"vimModeEnabled",
"autoCloseBrackets",
"sqlSemanticDiagnosticsMode",
"confirmDangerousSqlExecution",
"confirmUnsavedSqlClose",
"appCloseUnsavedTabsMode",
"savedSqlOpenTargetMode",
"appLayout",
"tabLayout",
"showColumnCommentsInHeader",
"showColumnTypesInHeader",
"dataGridShowTransposeFieldMetadata",
"colorizeDataGridCellTypes",
"dataGridTypeColorSchemes",
"activeDataGridTypeColorSchemeId",
"showIndexIndicatorsInHeader",
"compactColumnHeaderActions",
"dataGridQuickEntry",
"dataGridFilterEditorView",
"dataGridTextFilterPanelHeight",
"dataGridAutoTransposeSingleRow",
"dataGridCellDetailButtonVisible",
"dataGridCrosshairHighlight",
"pageSize",
"tableOpenPageSize",
"queryResultMaxRowsEnabled",
"queryResultMaxRows",
"infiniteScroll",
"regexMaxMatchCount",
"autoCalculateTotalRows",
"flatteningMultiLineText",
"tableColumnTemplateFields",
"shortcuts",
"sqlFormatter",
"sidebarActivation",
"sidebarObjectDisplay",
"routineSourceOpenMode",
"sidebarTableSearchEnabled",
"autoSelectActiveSidebarNode",
"sidebarOpenDatabaseOnSingleClick",
"openTabsRestoreMode",
"disconnectTabHandlingMode",
"dataTabReuseMode",
"openDataTabsNextToActive",
"prefillNewQueryWithSelect",
"generateSqlIncludeDatabaseName",
"formatSqlOnSqlFileSave",
"updateNotificationsEnabled",
"sidebarObjectInfoMode",
"sidebarAllowHorizontalScroll",
"sidebarIndent",
"sidebarFontSize",
"sidebarHiddenTablePrefixes",
"redisKeyTemplates",
"exportBatchSize",
"exportRowLimitEnabled",
"exportRowLimit",
"queryExportKeysetOptimizationEnabled",
"globalDateTimeDisplayFormat",
"globalDateTimeExportFormat",
"globalDateTimeImportFormat",
"updateDownloadSource",
"toolbarItems",
"snippets",
"sqlVariableSubstitutionEnabled",
"sqlVariableSyntaxOverrides",
"continueOnErrorOnBatch",
"clickTableNavigationTarget",
"completionTriggerMode",
] as const satisfies readonly (keyof EditorSettings)[];
export type EditorSettingsDraftKey = (typeof EDITOR_SETTINGS_DRAFT_KEYS)[number];
export type EditorSettingsDraft = Pick<EditorSettings, EditorSettingsDraftKey>;
function cloneDraftValue<T>(value: T): T {
if (value === null || typeof value !== "object") return value;
return JSON.parse(JSON.stringify(value)) as T;
}
export function normalizeTableOpenPageSizeDraft(value: unknown): number {
// Match persistence so legacy, invalid, and fractional values cannot leave the dialog dirty after apply.
return normalizeResultPageSize(value);
}
export function normalizeQueryResultMaxRowsDraft(value: unknown): number {
return normalizeQueryResultMaxRows(value);
}
function normalizedDraftValue(key: EditorSettingsDraftKey, value: unknown): unknown {
if (key === "pageSize" || key === "tableOpenPageSize") return normalizeTableOpenPageSizeDraft(value);
if (key === "queryResultMaxRows") return normalizeQueryResultMaxRowsDraft(value);
if (key === "completionTriggerMode") return normalizeCompletionTriggerMode(value);
if (key === "redisKeyTemplates") return normalizeRedisKeyTemplates(value);
return value;
}
function draftValueChanged(key: EditorSettingsDraftKey, a: unknown, b: unknown): boolean {
return JSON.stringify(normalizedDraftValue(key, a)) !== JSON.stringify(normalizedDraftValue(key, b));
}
export function editorSettingsDraftFromSettings(settings: EditorSettings): EditorSettingsDraft {
const draft = {} as EditorSettingsDraft;
for (const key of EDITOR_SETTINGS_DRAFT_KEYS) {
draft[key] = cloneDraftValue(normalizedDraftValue(key, settings[key])) as never;
}
return draft;
}
export function editorSettingsPatchFromDraft(draft: EditorSettingsDraft, base: EditorSettingsDraft): Partial<EditorSettings> {
const patch: Partial<EditorSettings> = {};
for (const key of EDITOR_SETTINGS_DRAFT_KEYS) {
if (draftValueChanged(key, draft[key], base[key])) {
patch[key] = cloneDraftValue(normalizedDraftValue(key, draft[key])) as never;
}
}
return patch;
}
export function editorSettingsDraftChanged(draft: EditorSettingsDraft, base: EditorSettingsDraft): boolean {
return EDITOR_SETTINGS_DRAFT_KEYS.some((key) => draftValueChanged(key, draft[key], base[key]));
}
// Closing the settings dialog (Escape, clicking outside, the X button, or the
// "Close" footer button) must never silently drop an unapplied draft — the
// dialog only persists shortcuts/sidebarActivation/etc. to the store on an
// explicit Apply. Route every close attempt through this check so an unsaved
// draft always surfaces a confirmation instead of vanishing.
export function shouldConfirmEditorSettingsDialogClose(nextOpen: boolean, hasUnsavedChanges: boolean): boolean {
return nextOpen === false && hasUnsavedChanges;
}