feat(grid): add custom column formatters
This commit is contained in:
parent
75893eee27
commit
d95225d641
|
|
@ -95,6 +95,7 @@ import {
|
|||
applyColumnFormatter,
|
||||
buildColumnFormatterKey,
|
||||
normalizeColumnFormatter,
|
||||
resolveColumnFormatter,
|
||||
type ColumnFormatterConfig,
|
||||
type DateTimeFormatterUnit,
|
||||
} from "@/lib/columnFormatter";
|
||||
|
|
@ -294,11 +295,22 @@ const localFilterOpenColumn = ref<number | null>(null);
|
|||
const localFilterSearch = ref("");
|
||||
const localFilterDraft = ref<LocalColumnFilterDraft | null>(null);
|
||||
const formatterOpenColumn = ref<number | null>(null);
|
||||
const formatterKind = ref<ColumnFormatterConfig["kind"]>("datetime");
|
||||
type FormatterDraftKind = Exclude<ColumnFormatterConfig["kind"], "custom-ref">;
|
||||
const CUSTOM_FORMATTER_NEW = "__new";
|
||||
const formatterKind = ref<FormatterDraftKind>("datetime");
|
||||
const formatterDateUnit = ref<DateTimeFormatterUnit>("auto");
|
||||
const formatterJsonPath = ref("$.user.name");
|
||||
const formatterMaskPrefix = ref(4);
|
||||
const formatterMaskSuffix = ref(4);
|
||||
const formatterCustomId = ref(CUSTOM_FORMATTER_NEW);
|
||||
const formatterCustomName = ref("");
|
||||
const formatterCustomTemplate = ref("${value}");
|
||||
|
||||
const savedCustomFormatters = computed(() => {
|
||||
return Object.values(settingsStore.editorSettings.customColumnFormatters).sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
});
|
||||
|
||||
function localFilterKey(value: CellValue): string {
|
||||
if (value === null) return "__dbx_null__";
|
||||
|
|
@ -414,6 +426,18 @@ function formatterKeyForColumn(column: string): string | null {
|
|||
}
|
||||
|
||||
function columnFormatter(columnIndex: number): ColumnFormatterConfig | undefined {
|
||||
const column = props.result.columns[columnIndex];
|
||||
if (!column) return undefined;
|
||||
const key = formatterKeyForColumn(column);
|
||||
return key
|
||||
? resolveColumnFormatter(
|
||||
settingsStore.editorSettings.columnFormatters[key],
|
||||
settingsStore.editorSettings.customColumnFormatters,
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function savedColumnFormatter(columnIndex: number): ColumnFormatterConfig | undefined {
|
||||
const column = props.result.columns[columnIndex];
|
||||
if (!column) return undefined;
|
||||
const key = formatterKeyForColumn(column);
|
||||
|
|
@ -435,12 +459,15 @@ function currentFormatterDraft(): ColumnFormatterConfig {
|
|||
suffix: Math.max(0, Math.floor(Number(formatterMaskSuffix.value) || 0)),
|
||||
};
|
||||
}
|
||||
if (formatterKind.value === "custom-template") {
|
||||
return { kind: "custom-template", template: formatterCustomTemplate.value.trim() || "${value}" };
|
||||
}
|
||||
return { kind: "datetime", unit: formatterDateUnit.value };
|
||||
}
|
||||
|
||||
function loadFormatterDraft(formatter: ColumnFormatterConfig | undefined) {
|
||||
const draft = formatter ?? { kind: "datetime", unit: "auto" as const };
|
||||
formatterKind.value = draft.kind;
|
||||
formatterKind.value = draft.kind === "custom-ref" ? "custom-template" : draft.kind;
|
||||
if (draft.kind === "datetime") {
|
||||
formatterDateUnit.value = draft.unit;
|
||||
} else if (draft.kind === "json-path") {
|
||||
|
|
@ -448,11 +475,20 @@ function loadFormatterDraft(formatter: ColumnFormatterConfig | undefined) {
|
|||
} else if (draft.kind === "mask") {
|
||||
formatterMaskPrefix.value = draft.prefix;
|
||||
formatterMaskSuffix.value = draft.suffix;
|
||||
} else if (draft.kind === "custom-ref") {
|
||||
const saved = settingsStore.editorSettings.customColumnFormatters[draft.formatterId];
|
||||
formatterCustomId.value = saved ? saved.id : CUSTOM_FORMATTER_NEW;
|
||||
formatterCustomName.value = saved?.name ?? "";
|
||||
formatterCustomTemplate.value = saved?.template ?? "${value}";
|
||||
} else if (draft.kind === "custom-template") {
|
||||
formatterCustomId.value = CUSTOM_FORMATTER_NEW;
|
||||
formatterCustomName.value = "";
|
||||
formatterCustomTemplate.value = draft.template;
|
||||
}
|
||||
}
|
||||
|
||||
function openColumnFormatter(columnIndex: number) {
|
||||
loadFormatterDraft(columnFormatter(columnIndex));
|
||||
loadFormatterDraft(savedColumnFormatter(columnIndex));
|
||||
formatterOpenColumn.value = columnIndex;
|
||||
}
|
||||
|
||||
|
|
@ -464,7 +500,17 @@ function saveColumnFormatter(columnIndex: number) {
|
|||
const column = props.result.columns[columnIndex];
|
||||
const key = column ? formatterKeyForColumn(column) : null;
|
||||
if (!key) return;
|
||||
settingsStore.updateColumnFormatter(key, currentFormatterDraft());
|
||||
let formatter = currentFormatterDraft();
|
||||
if (formatterKind.value === "custom-template" && formatterCustomName.value.trim()) {
|
||||
const id = formatterCustomId.value === CUSTOM_FORMATTER_NEW ? createCustomFormatterId() : formatterCustomId.value;
|
||||
const saved = settingsStore.upsertCustomColumnFormatter({
|
||||
id,
|
||||
name: formatterCustomName.value,
|
||||
template: formatterCustomTemplate.value,
|
||||
});
|
||||
if (saved) formatter = { kind: "custom-ref", formatterId: saved.id };
|
||||
}
|
||||
settingsStore.updateColumnFormatter(key, formatter);
|
||||
closeColumnFormatter();
|
||||
}
|
||||
|
||||
|
|
@ -480,8 +526,29 @@ function formatterDraftIsSavable(): boolean {
|
|||
return !!normalizeColumnFormatter(currentFormatterDraft());
|
||||
}
|
||||
|
||||
function selectCustomFormatter(value: string) {
|
||||
formatterCustomId.value = value;
|
||||
if (value === CUSTOM_FORMATTER_NEW) {
|
||||
formatterCustomName.value = "";
|
||||
formatterCustomTemplate.value = "${value}";
|
||||
return;
|
||||
}
|
||||
const saved = settingsStore.editorSettings.customColumnFormatters[value];
|
||||
if (!saved) return;
|
||||
formatterCustomName.value = saved.name;
|
||||
formatterCustomTemplate.value = saved.template;
|
||||
}
|
||||
|
||||
function createCustomFormatterId(): string {
|
||||
if (typeof crypto !== "undefined" && "randomUUID" in crypto) return `fmt_${crypto.randomUUID()}`;
|
||||
return `fmt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
function formatterPreviewRows(columnIndex: number) {
|
||||
const formatter = currentFormatterDraft();
|
||||
const formatter = resolveColumnFormatter(
|
||||
currentFormatterDraft(),
|
||||
settingsStore.editorSettings.customColumnFormatters,
|
||||
);
|
||||
return displayItems.value.slice(0, 5).map((item, index) => {
|
||||
const value = item.data[columnIndex] ?? null;
|
||||
return {
|
||||
|
|
@ -2810,6 +2877,9 @@ defineExpose({
|
|||
<SelectItem value="datetime">{{ t("grid.formatterDatetime") }}</SelectItem>
|
||||
<SelectItem value="json-path">{{ t("grid.formatterJsonPath") }}</SelectItem>
|
||||
<SelectItem value="mask">{{ t("grid.formatterMask") }}</SelectItem>
|
||||
<SelectItem value="custom-template">{{
|
||||
t("grid.formatterCustomTemplate")
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
@ -2849,7 +2919,7 @@ defineExpose({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-2 gap-2">
|
||||
<div v-else-if="formatterKind === 'mask'" class="grid grid-cols-2 gap-2">
|
||||
<label class="space-y-1.5">
|
||||
<span class="text-xs font-medium text-muted-foreground">
|
||||
{{ t("grid.formatterMaskPrefix") }}
|
||||
|
|
@ -2874,6 +2944,60 @@ defineExpose({
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<div v-if="savedCustomFormatters.length" class="space-y-1.5">
|
||||
<div class="text-xs font-medium text-muted-foreground">
|
||||
{{ t("grid.formatterSavedCustom") }}
|
||||
</div>
|
||||
<Select
|
||||
:model-value="formatterCustomId"
|
||||
@update:model-value="(value: any) => selectCustomFormatter(String(value))"
|
||||
>
|
||||
<SelectTrigger class="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="CUSTOM_FORMATTER_NEW">{{
|
||||
t("grid.formatterNewCustom")
|
||||
}}</SelectItem>
|
||||
<SelectItem
|
||||
v-for="formatter in savedCustomFormatters"
|
||||
:key="formatter.id"
|
||||
:value="formatter.id"
|
||||
>
|
||||
{{ formatter.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<label class="block space-y-1.5">
|
||||
<span class="text-xs font-medium text-muted-foreground">
|
||||
{{ t("grid.formatterCustomName") }}
|
||||
</span>
|
||||
<input
|
||||
v-model="formatterCustomName"
|
||||
class="h-8 w-full rounded border bg-background px-2 text-xs outline-none focus:border-primary"
|
||||
:placeholder="t('grid.formatterCustomNamePlaceholder')"
|
||||
/>
|
||||
</label>
|
||||
<label class="block space-y-1.5">
|
||||
<span class="text-xs font-medium text-muted-foreground">
|
||||
{{ t("grid.formatterCustomTemplateInput") }}
|
||||
</span>
|
||||
<input
|
||||
v-model="formatterCustomTemplate"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
class="h-8 w-full rounded border bg-background px-2 font-mono text-xs outline-none focus:border-primary"
|
||||
placeholder="ID-${value}"
|
||||
/>
|
||||
</label>
|
||||
<div class="text-[11px] leading-4 text-muted-foreground">
|
||||
{{ t("grid.formatterCustomTemplateHint") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<div class="text-xs font-medium text-muted-foreground">
|
||||
{{ t("grid.formatterPreview") }}
|
||||
|
|
|
|||
|
|
@ -270,6 +270,7 @@ export default {
|
|||
formatterDatetime: "Unix timestamp",
|
||||
formatterJsonPath: "JSON path",
|
||||
formatterMask: "Mask text",
|
||||
formatterCustomTemplate: "Custom template",
|
||||
formatterTimestampUnit: "Timestamp unit",
|
||||
formatterUnitAuto: "Auto",
|
||||
formatterUnitSeconds: "Seconds",
|
||||
|
|
@ -277,6 +278,13 @@ export default {
|
|||
formatterJsonPathInput: "JSON path",
|
||||
formatterMaskPrefix: "Visible prefix",
|
||||
formatterMaskSuffix: "Visible suffix",
|
||||
formatterSavedCustom: "Saved templates",
|
||||
formatterNewCustom: "New template",
|
||||
formatterCustomName: "Template name",
|
||||
formatterCustomNamePlaceholder: "Example: User label",
|
||||
formatterCustomTemplateInput: "Template",
|
||||
formatterCustomTemplateHint:
|
||||
"Available variables: ${value}, ${upper}, ${lower}, ${length}. Give it a name to save it to the list.",
|
||||
formatterPreview: "Preview",
|
||||
saveFormatter: "Save",
|
||||
clearFormatter: "Clear",
|
||||
|
|
|
|||
|
|
@ -267,6 +267,7 @@ export default {
|
|||
formatterDatetime: "Unix 时间戳",
|
||||
formatterJsonPath: "JSON 路径",
|
||||
formatterMask: "文本脱敏",
|
||||
formatterCustomTemplate: "自定义模板",
|
||||
formatterTimestampUnit: "时间戳单位",
|
||||
formatterUnitAuto: "自动",
|
||||
formatterUnitSeconds: "秒",
|
||||
|
|
@ -274,6 +275,12 @@ export default {
|
|||
formatterJsonPathInput: "JSON 路径",
|
||||
formatterMaskPrefix: "前缀保留",
|
||||
formatterMaskSuffix: "后缀保留",
|
||||
formatterSavedCustom: "已保存模板",
|
||||
formatterNewCustom: "新建模板",
|
||||
formatterCustomName: "模板名称",
|
||||
formatterCustomNamePlaceholder: "例如:用户标签",
|
||||
formatterCustomTemplateInput: "模板",
|
||||
formatterCustomTemplateHint: "可用变量:${value}、${upper}、${lower}、${length}。填写名称后会保存到列表。",
|
||||
formatterPreview: "预览",
|
||||
saveFormatter: "保存",
|
||||
clearFormatter: "清除",
|
||||
|
|
|
|||
|
|
@ -2,10 +2,18 @@ import { displayCellValue, type CellValue } from "@/lib/cellValue";
|
|||
|
||||
export type DateTimeFormatterUnit = "seconds" | "milliseconds" | "auto";
|
||||
|
||||
export interface CustomColumnFormatterConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
template: string;
|
||||
}
|
||||
|
||||
export type ColumnFormatterConfig =
|
||||
| { kind: "datetime"; unit: DateTimeFormatterUnit }
|
||||
| { kind: "json-path"; path: string }
|
||||
| { kind: "mask"; prefix: number; suffix: number };
|
||||
| { kind: "mask"; prefix: number; suffix: number }
|
||||
| { kind: "custom-template"; template: string }
|
||||
| { kind: "custom-ref"; formatterId: string };
|
||||
|
||||
export interface ColumnFormatterKeyParts {
|
||||
connectionId: string;
|
||||
|
|
@ -41,24 +49,60 @@ export function normalizeColumnFormatter(value: unknown): ColumnFormatterConfig
|
|||
return { kind: "mask", prefix: config.prefix as number, suffix: config.suffix as number };
|
||||
}
|
||||
|
||||
if (config.kind === "custom-template") {
|
||||
return typeof config.template === "string" && config.template.trim()
|
||||
? { kind: "custom-template", template: config.template.slice(0, 500) }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (config.kind === "custom-ref") {
|
||||
return typeof config.formatterId === "string" && config.formatterId.trim()
|
||||
? { kind: "custom-ref", formatterId: config.formatterId }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function normalizeCustomColumnFormatter(value: unknown): CustomColumnFormatterConfig | undefined {
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
const config = value as Record<string, unknown>;
|
||||
if (typeof config.id !== "string" || !config.id.trim()) return undefined;
|
||||
if (typeof config.name !== "string" || !config.name.trim()) return undefined;
|
||||
if (typeof config.template !== "string" || !config.template.trim()) return undefined;
|
||||
return {
|
||||
id: config.id.trim(),
|
||||
name: config.name.trim().slice(0, 80),
|
||||
template: config.template.slice(0, 500),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveColumnFormatter(
|
||||
formatter: ColumnFormatterConfig | undefined,
|
||||
customFormatters: Record<string, CustomColumnFormatterConfig>,
|
||||
): ColumnFormatterConfig | undefined {
|
||||
if (!formatter) return undefined;
|
||||
if (formatter.kind !== "custom-ref") return formatter;
|
||||
const customFormatter = customFormatters[formatter.formatterId];
|
||||
return customFormatter ? { kind: "custom-template", template: customFormatter.template } : undefined;
|
||||
}
|
||||
|
||||
export function applyColumnFormatter(value: CellValue, formatter: ColumnFormatterConfig | undefined): string {
|
||||
if (!formatter) return displayCellValue(value);
|
||||
if (value === null) return displayCellValue(value);
|
||||
|
||||
try {
|
||||
if (formatter.kind === "datetime") return formatDateTime(value, formatter.unit);
|
||||
if (formatter.kind === "json-path") return formatJsonPath(value, formatter.path);
|
||||
if (formatter.kind === "mask") return formatMask(value, formatter);
|
||||
if (formatter.kind === "custom-template") return formatCustomTemplate(value, formatter.template);
|
||||
return displayCellValue(value);
|
||||
} catch {
|
||||
return displayCellValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateTime(value: Exclude<CellValue, null>, unit: DateTimeFormatterUnit): string {
|
||||
function formatDateTime(value: CellValue, unit: DateTimeFormatterUnit): string {
|
||||
if (value === null) return displayCellValue(value);
|
||||
const numeric = typeof value === "number" ? value : Number(String(value).trim());
|
||||
if (!Number.isFinite(numeric)) return displayCellValue(value);
|
||||
const timestamp =
|
||||
|
|
@ -67,7 +111,8 @@ function formatDateTime(value: Exclude<CellValue, null>, unit: DateTimeFormatter
|
|||
return Number.isNaN(date.getTime()) ? displayCellValue(value) : date.toLocaleString();
|
||||
}
|
||||
|
||||
function formatJsonPath(value: Exclude<CellValue, null>, path: string): string {
|
||||
function formatJsonPath(value: CellValue, path: string): string {
|
||||
if (value === null) return displayCellValue(value);
|
||||
if (typeof value !== "string") return displayCellValue(value);
|
||||
const parsed = JSON.parse(value);
|
||||
const tokens = parseJsonPath(path);
|
||||
|
|
@ -90,10 +135,8 @@ function formatJsonPath(value: Exclude<CellValue, null>, path: string): string {
|
|||
return String(current);
|
||||
}
|
||||
|
||||
function formatMask(
|
||||
value: Exclude<CellValue, null>,
|
||||
formatter: Extract<ColumnFormatterConfig, { kind: "mask" }>,
|
||||
): string {
|
||||
function formatMask(value: CellValue, formatter: Extract<ColumnFormatterConfig, { kind: "mask" }>): string {
|
||||
if (value === null) return displayCellValue(value);
|
||||
const text = displayCellValue(value);
|
||||
const visibleCount = formatter.prefix + formatter.suffix;
|
||||
if (text.length <= visibleCount) return "*".repeat(text.length);
|
||||
|
|
@ -102,6 +145,15 @@ function formatMask(
|
|||
)}`;
|
||||
}
|
||||
|
||||
function formatCustomTemplate(value: CellValue, template: string): string {
|
||||
const text = displayCellValue(value);
|
||||
return template
|
||||
.replaceAll("${value}", text)
|
||||
.replaceAll("${upper}", text.toUpperCase())
|
||||
.replaceAll("${lower}", text.toLowerCase())
|
||||
.replaceAll("${length}", String(text.length));
|
||||
}
|
||||
|
||||
function isSupportedJsonPath(path: string): boolean {
|
||||
if (!path.startsWith("$")) return false;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import * as api from "@/lib/api";
|
||||
import { normalizeColumnFormatter, type ColumnFormatterConfig } from "@/lib/columnFormatter";
|
||||
import {
|
||||
normalizeColumnFormatter,
|
||||
normalizeCustomColumnFormatter,
|
||||
type ColumnFormatterConfig,
|
||||
type CustomColumnFormatterConfig,
|
||||
} from "@/lib/columnFormatter";
|
||||
import { normalizeShortcutSettings, type ShortcutSettings } from "@/lib/shortcutRegistry";
|
||||
import type { SidebarActivation } from "@/lib/treeNodeClick";
|
||||
|
||||
|
|
@ -151,6 +156,7 @@ export interface EditorSettings {
|
|||
shortcuts: ShortcutSettings;
|
||||
sidebarActivation: SidebarActivation;
|
||||
columnFormatters: Record<string, ColumnFormatterConfig>;
|
||||
customColumnFormatters: Record<string, CustomColumnFormatterConfig>;
|
||||
}
|
||||
|
||||
export const EDITOR_THEMES: { value: EditorTheme; label: string; dark: boolean }[] = [
|
||||
|
|
@ -187,6 +193,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
|||
shortcuts: normalizeShortcutSettings(),
|
||||
sidebarActivation: "single",
|
||||
columnFormatters: {},
|
||||
customColumnFormatters: {},
|
||||
};
|
||||
|
||||
export const STORAGE_KEY = "dbx-editor-settings";
|
||||
|
|
@ -202,6 +209,16 @@ function normalizeColumnFormatters(value: unknown): Record<string, ColumnFormatt
|
|||
return formatters;
|
||||
}
|
||||
|
||||
function normalizeCustomColumnFormatters(value: unknown): Record<string, CustomColumnFormatterConfig> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
||||
const formatters: Record<string, CustomColumnFormatterConfig> = {};
|
||||
for (const formatter of Object.values(value as Record<string, unknown>)) {
|
||||
const normalized = normalizeCustomColumnFormatter(formatter);
|
||||
if (normalized) formatters[normalized.id] = normalized;
|
||||
}
|
||||
return formatters;
|
||||
}
|
||||
|
||||
export function normalizeEditorSettings(settings: Partial<EditorSettings>): EditorSettings {
|
||||
return {
|
||||
fontFamily: settings.fontFamily ?? DEFAULT_EDITOR_SETTINGS.fontFamily,
|
||||
|
|
@ -218,6 +235,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>): Edit
|
|||
? settings.sidebarActivation
|
||||
: DEFAULT_EDITOR_SETTINGS.sidebarActivation,
|
||||
columnFormatters: normalizeColumnFormatters(settings.columnFormatters),
|
||||
customColumnFormatters: normalizeCustomColumnFormatters(settings.customColumnFormatters),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -309,6 +327,31 @@ export const useSettingsStore = defineStore("settings", () => {
|
|||
updateEditorSettings({ columnFormatters });
|
||||
}
|
||||
|
||||
function upsertCustomColumnFormatter(
|
||||
formatter: CustomColumnFormatterConfig,
|
||||
): CustomColumnFormatterConfig | undefined {
|
||||
const normalized = normalizeCustomColumnFormatter(formatter);
|
||||
if (!normalized) return undefined;
|
||||
updateEditorSettings({
|
||||
customColumnFormatters: {
|
||||
...editorSettings.value.customColumnFormatters,
|
||||
[normalized.id]: normalized,
|
||||
},
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function deleteCustomColumnFormatter(id: string) {
|
||||
const customColumnFormatters = { ...editorSettings.value.customColumnFormatters };
|
||||
delete customColumnFormatters[id];
|
||||
const columnFormatters = Object.fromEntries(
|
||||
Object.entries(editorSettings.value.columnFormatters).filter(([, formatter]) => {
|
||||
return formatter.kind !== "custom-ref" || formatter.formatterId !== id;
|
||||
}),
|
||||
);
|
||||
updateEditorSettings({ customColumnFormatters, columnFormatters });
|
||||
}
|
||||
|
||||
return {
|
||||
aiConfig,
|
||||
isAiConfigLoaded,
|
||||
|
|
@ -318,5 +361,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
|||
editorSettings,
|
||||
updateEditorSettings,
|
||||
updateColumnFormatter,
|
||||
upsertCustomColumnFormatter,
|
||||
deleteCustomColumnFormatter,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import test from "node:test";
|
|||
import {
|
||||
applyColumnFormatter,
|
||||
buildColumnFormatterKey,
|
||||
resolveColumnFormatter,
|
||||
normalizeColumnFormatter,
|
||||
type ColumnFormatterConfig,
|
||||
} from "../src/lib/columnFormatter.ts";
|
||||
|
|
@ -50,6 +51,14 @@ test("normalizes only supported formatter configs", () => {
|
|||
path: "$.a[0]",
|
||||
});
|
||||
assert.equal(normalizeColumnFormatter({ kind: "json-path", path: "a.b" }), undefined);
|
||||
assert.deepEqual(normalizeColumnFormatter({ kind: "custom-template", template: "ID-${value}" }), {
|
||||
kind: "custom-template",
|
||||
template: "ID-${value}",
|
||||
});
|
||||
assert.deepEqual(normalizeColumnFormatter({ kind: "custom-ref", formatterId: "fmt_1" }), {
|
||||
kind: "custom-ref",
|
||||
formatterId: "fmt_1",
|
||||
});
|
||||
});
|
||||
|
||||
test("builds stable formatter keys for table columns", () => {
|
||||
|
|
@ -64,3 +73,22 @@ test("builds stable formatter keys for table columns", () => {
|
|||
"conn::db::public::users::created_at",
|
||||
);
|
||||
});
|
||||
|
||||
test("applies safe custom formatter templates", () => {
|
||||
assert.equal(applyColumnFormatter("ada", { kind: "custom-template", template: "user:${value}" }), "user:ada");
|
||||
assert.equal(applyColumnFormatter("Ada", { kind: "custom-template", template: "${upper}" }), "ADA");
|
||||
assert.equal(applyColumnFormatter("Ada", { kind: "custom-template", template: "${lower}" }), "ada");
|
||||
assert.equal(applyColumnFormatter("Ada", { kind: "custom-template", template: "${length}" }), "3");
|
||||
assert.equal(applyColumnFormatter(null, { kind: "custom-template", template: "value=${value}" }), "value=NULL");
|
||||
});
|
||||
|
||||
test("resolves saved custom formatter references", () => {
|
||||
assert.deepEqual(
|
||||
resolveColumnFormatter(
|
||||
{ kind: "custom-ref", formatterId: "fmt_1" },
|
||||
{ fmt_1: { id: "fmt_1", name: "User label", template: "user:${value}" } },
|
||||
),
|
||||
{ kind: "custom-template", template: "user:${value}" },
|
||||
);
|
||||
assert.equal(resolveColumnFormatter({ kind: "custom-ref", formatterId: "missing" }, {}), undefined);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -54,6 +54,12 @@ test("keeps only valid saved column formatter configs", () => {
|
|||
"conn::db::public::users::name": { kind: "mask", prefix: 2, suffix: 2 },
|
||||
"conn::db::public::users::payload": { kind: "json-path", path: "$.user.name" },
|
||||
"conn::db::public::users::invalid_json": { kind: "json-path", path: "user.name" },
|
||||
"conn::db::public::users::status": { kind: "custom-ref", formatterId: "fmt_1" },
|
||||
},
|
||||
customColumnFormatters: {
|
||||
fmt_1: { id: "fmt_1", name: "Status label", template: "status:${value}" },
|
||||
fmt_empty_name: { id: "fmt_empty_name", name: "", template: "x:${value}" },
|
||||
fmt_empty_template: { id: "fmt_empty_template", name: "Broken", template: "" },
|
||||
},
|
||||
} as any);
|
||||
|
||||
|
|
@ -61,6 +67,10 @@ test("keeps only valid saved column formatter configs", () => {
|
|||
"conn::db::public::users::created_at": { kind: "datetime", unit: "auto" },
|
||||
"conn::db::public::users::name": { kind: "mask", prefix: 2, suffix: 2 },
|
||||
"conn::db::public::users::payload": { kind: "json-path", path: "$.user.name" },
|
||||
"conn::db::public::users::status": { kind: "custom-ref", formatterId: "fmt_1" },
|
||||
});
|
||||
assert.deepEqual(settings.customColumnFormatters, {
|
||||
fmt_1: { id: "fmt_1", name: "Status label", template: "status:${value}" },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue