feat(grid): format JSON values in cell editor

This commit is contained in:
t8y2 committed 2026-05-21 14:39:47 +08:00
1 parent cb7682e932
commit f7f39f124c
6 files changed
+72 -9

No files matched your search

+25 -4
View File
@@ -103,6 +103,8 @@ import { cellImagePreviewUrl } from "@/lib/cellImageUrl";
import { import {
cellDetailEditorText, cellDetailEditorText,
defaultCellDetailTab, defaultCellDetailTab,
formatJsonText,
isJsonColumnType,
valueEditorActions, valueEditorActions,
visibleCellDetailTabs, visibleCellDetailTabs,
type CellDetailTab, type CellDetailTab,
@@ -1774,7 +1776,10 @@ watch(activeCellDetailTab, (tab) => {
const activeValueEditorActions = computed(() => { const activeValueEditorActions = computed(() => {
const detail = activeCellDetail.value; const detail = activeCellDetail.value;
return valueEditorActions({ canSetNull: !!detail?.isEditable && detail.value !== null }); return valueEditorActions({
canSetNull: !!detail?.isEditable && detail.value !== null,
canFormatJson: !!detail?.isEditable && isJsonColumnType(detail?.type),
});
}); });
const detailEditValue = ref(""); const detailEditValue = ref("");
@@ -1798,7 +1803,7 @@ function closeCellDetails() {
function startDetailEdit() { function startDetailEdit() {
const detail = activeCellDetail.value; const detail = activeCellDetail.value;
if (!detail || !detail.isEditable) return; if (!detail || !detail.isEditable) return;
detailEditValue.value = cellDetailEditorText(detail.value); detailEditValue.value = cellDetailEditorText(detail.value, detail.type);
isEditingDetail.value = true; isEditingDetail.value = true;
} }
@@ -1842,7 +1847,7 @@ function cancelDetailEdit() {
function cancelValueEditorEdit() { function cancelValueEditorEdit() {
const detail = activeCellDetail.value; const detail = activeCellDetail.value;
if (!detail || !detail.isEditable) return; if (!detail || !detail.isEditable) return;
detailEditValue.value = cellDetailEditorText(detail.value); detailEditValue.value = cellDetailEditorText(detail.value, detail.type);
isEditingDetail.value = true; isEditingDetail.value = true;
} }
@@ -1873,7 +1878,7 @@ function restoreDetailOriginalValue() {
dirtyRows.value = new Map(dirtyRows.value); dirtyRows.value = new Map(dirtyRows.value);
} }
detailEditValue.value = cellDetailEditorText(restoredValue); detailEditValue.value = cellDetailEditorText(restoredValue, detail.type);
isEditingDetail.value = activeCellDetailTab.value === "valueEditor"; isEditingDetail.value = activeCellDetailTab.value === "valueEditor";
detailCell.value = { ...detailCell.value! }; detailCell.value = { ...detailCell.value! };
} }
@@ -1884,6 +1889,12 @@ function setValueEditorNull() {
isEditingDetail.value = activeCellDetailTab.value === "valueEditor"; isEditingDetail.value = activeCellDetailTab.value === "valueEditor";
} }
function formatValueEditorJson() {
const detail = activeCellDetail.value;
if (!detail || !isJsonColumnType(detail.type)) return;
detailEditValue.value = formatJsonText(detailEditValue.value) ?? detailEditValue.value;
}
function setDetailNull() { function setDetailNull() {
const detail = activeCellDetail.value; const detail = activeCellDetail.value;
if (!detail || !detail.isEditable) return; if (!detail || !detail.isEditable) return;
@@ -4267,6 +4278,16 @@ defineExpose({
/> />
</div> </div>
<div class="flex gap-1 mt-2 shrink-0"> <div class="flex gap-1 mt-2 shrink-0">
<Button
v-if="activeValueEditorActions.includes('formatJson')"
variant="outline"
size="sm"
class="h-6 text-xs"
@mousedown.prevent
@click="formatValueEditorJson"
>
{{ t("grid.formatJson") }}
</Button>
<Button <Button
v-if="activeValueEditorActions.includes('setNull')" v-if="activeValueEditorActions.includes('setNull')"
variant="outline" variant="outline"
+1
View File
@@ -374,6 +374,7 @@ export default {
rawValue: "Raw Value", rawValue: "Raw Value",
copyValue: "Copy Value", copyValue: "Copy Value",
editValue: "Edit Value", editValue: "Edit Value",
formatJson: "Format JSON",
setNull: "Set NULL", setNull: "Set NULL",
restoreOriginalValue: "Restore Original", restoreOriginalValue: "Restore Original",
copyColumnName: "Copy Column Name", copyColumnName: "Copy Column Name",
+1
View File
@@ -342,6 +342,7 @@ export default {
rawValue: "Valor sin procesar", rawValue: "Valor sin procesar",
copyValue: "Copiar valor", copyValue: "Copiar valor",
editValue: "Editar valor", editValue: "Editar valor",
formatJson: "Formatear JSON",
setNull: "Establecer NULL", setNull: "Establecer NULL",
restoreOriginalValue: "Restaurar original", restoreOriginalValue: "Restaurar original",
copyColumnName: "Copiar nombre de columna", copyColumnName: "Copiar nombre de columna",
+1
View File
@@ -371,6 +371,7 @@ export default {
rawValue: "原始值", rawValue: "原始值",
copyValue: "复制值", copyValue: "复制值",
editValue: "编辑值", editValue: "编辑值",
formatJson: "格式化 JSON",
setNull: "设为 NULL", setNull: "设为 NULL",
restoreOriginalValue: "恢复原值", restoreOriginalValue: "恢复原值",
copyColumnName: "复制列名", copyColumnName: "复制列名",
+30 -4
View File
@@ -1,5 +1,5 @@
export type CellDetailTab = "details" | "valueEditor"; export type CellDetailTab = "details" | "valueEditor";
export type ValueEditorAction = "setNull" | "restoreOriginal"; export type ValueEditorAction = "formatJson" | "setNull" | "restoreOriginal";
export interface CellDetailPresentationOptions { export interface CellDetailPresentationOptions {
isEditable: boolean; isEditable: boolean;
@@ -17,12 +17,38 @@ export function visibleCellDetailTabs(options: CellDetailPresentationOptions): C
return tabs; return tabs;
} }
export function cellDetailEditorText(value: unknown): string { export function cellDetailEditorText(value: unknown, columnType?: string): string {
if (value === null) return ""; if (value === null) return "";
if (isJsonColumnType(columnType)) {
const text = typeof value === "object" ? JSON.stringify(value) : String(value);
return formatJsonText(text) ?? text;
}
if (typeof value === "object") return JSON.stringify(value); if (typeof value === "object") return JSON.stringify(value);
return String(value); return String(value);
} }
export function valueEditorActions(options: { canSetNull: boolean }): ValueEditorAction[] { export function valueEditorActions(options: { canSetNull: boolean; canFormatJson?: boolean }): ValueEditorAction[] {
return options.canSetNull ? ["setNull", "restoreOriginal"] : ["restoreOriginal"]; const actions: ValueEditorAction[] = [];
if (options.canFormatJson) actions.push("formatJson");
if (options.canSetNull) actions.push("setNull");
actions.push("restoreOriginal");
return actions;
}
export function isJsonColumnType(columnType: string | undefined): boolean {
const base = (columnType ?? "")
.trim()
.toLowerCase()
.split(/[(:\s]/)[0];
return base === "json" || base === "jsonb";
}
export function formatJsonText(text: string): string | undefined {
const trimmed = text.trim();
if (!trimmed) return undefined;
try {
return JSON.stringify(JSON.parse(trimmed), null, 2);
} catch {
return undefined;
}
} }
@@ -33,7 +33,20 @@ test("cell detail value editor restores the original value text on cancel", () =
assert.equal(cellDetailEditorText("already text"), "already text"); assert.equal(cellDetailEditorText("already text"), "already text");
}); });
test("cell detail value editor formats json typed string values", () => {
assert.equal(
cellDetailEditorText('{"nested":true,"items":[1,2]}', "jsonb"),
'{\n "nested": true,\n "items": [\n 1,\n 2\n ]\n}',
);
assert.equal(cellDetailEditorText('{"nested":true}', "varchar"), '{"nested":true}');
assert.equal(cellDetailEditorText("{invalid", "json"), "{invalid");
});
test("cell detail value editor uses cell actions instead of confirm and cancel", () => { test("cell detail value editor uses cell actions instead of confirm and cancel", () => {
assert.deepEqual(valueEditorActions({ canSetNull: true }), ["setNull", "restoreOriginal"]); assert.deepEqual(valueEditorActions({ canSetNull: true, canFormatJson: true }), [
"formatJson",
"setNull",
"restoreOriginal",
]);
assert.deepEqual(valueEditorActions({ canSetNull: false }), ["restoreOriginal"]); assert.deepEqual(valueEditorActions({ canSetNull: false }), ["restoreOriginal"]);
}); });