Improve: 记住单元格详情 JSON 格式化状态 (#2252)

Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
zipg 2026-06-30 22:38:45 +08:00 committed by GitHub
parent 427d0c548b
commit c4de3738fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 32 additions and 15 deletions

View File

@ -158,7 +158,8 @@ import { formatShortcut } from "@/lib/shortcutRegistry";
import { SearchableSelect } from "@/components/ui/searchable-select";
const SqlPreviewPanel = defineAsyncComponent(() => import("@/components/editor/SqlPreviewPanel.vue"));
const FORMATTED_JSON_EDIT_WARNING_STORAGE_KEY = "dbx-cell-detail-formatted-json-edit-warning-shown";
const FORMATTED_JSON_EDIT_WARNING_COUNT_STORAGE_KEY = "dbx-cell-detail-formatted-json-edit-warning-count";
const FORMATTED_JSON_EDIT_WARNING_MAX_COUNT = 10;
const { t } = useI18n();
const slots = useSlots();
@ -448,8 +449,6 @@ const rowDetailDialogOpen = ref(false);
const rowDetailDialogRowId = ref<number | null>(null);
const columnDetailDialogOpen = ref(false);
const columnDetailDialogColumnIndex = ref<number | null>(null);
const cellDetailJsonView = ref(false);
const sideDetailJsonView = ref(false);
const rowDetailSearch = ref("");
const columnDetailSearch = ref("");
const isResizingDetail = ref(false);
@ -3810,6 +3809,10 @@ const dialogCellDetail = computed(() => {
return target ? cellDetailFor(target.rowIndex, target.col) : null;
});
const cellDetailJsonFormatted = computed(() => settingsStore.editorSettings.cellDetailJsonFormatted);
const sideDetailJsonView = computed(() => cellDetailJsonFormatted.value && !!activeCellDetail.value?.formattedJson);
const cellDetailJsonView = computed(() => cellDetailJsonFormatted.value && !!dialogCellDetail.value?.formattedJson);
const rowDetail = computed(() => {
if (rowDetailDialogRowId.value === null) return null;
const item = getRowItem(rowDetailDialogRowId.value);
@ -3854,8 +3857,7 @@ const filteredRowDetailFields = computed(() => (rowDetail.value ? filterDataGrid
const filteredColumnDetailFields = computed(() => (columnDetail.value ? filterDataGridDetailFields(columnDetail.value.fields, columnDetailSearch.value) : []));
watch(cellDetailDialogOpen, (open) => {
if (open) cellDetailJsonView.value = false;
else cellDetailDialogTarget.value = null;
if (!open) cellDetailDialogTarget.value = null;
});
watch(rowDetailDialogOpen, (open) => {
@ -4014,7 +4016,6 @@ async function prefetchDetailSqlCondition() {
watch(activeCellDetail, (detail) => {
void prefetchDetailSqlCondition();
sideDetailJsonView.value = false;
if (activeCellDetailTab.value !== "valueEditor") return;
if (!detail?.isEditable) {
resetDetailEdit();
@ -4174,9 +4175,14 @@ function cellDetailEditText(detail: DataGridCellDetail): string {
function warnFormattedJsonEditIfNeeded(detail: DataGridCellDetail, force = false) {
if (!force && (!sideDetailJsonView.value || !detail.formattedJson)) return;
if (safeLocalStorageGet(FORMATTED_JSON_EDIT_WARNING_STORAGE_KEY) === "1") return;
const count = Number(safeLocalStorageGet(FORMATTED_JSON_EDIT_WARNING_COUNT_STORAGE_KEY)) || 0;
if (count >= FORMATTED_JSON_EDIT_WARNING_MAX_COUNT) return;
toast(t("grid.formattedJsonEditWarning"), 10000);
safeLocalStorageSet(FORMATTED_JSON_EDIT_WARNING_STORAGE_KEY, "1");
safeLocalStorageSet(FORMATTED_JSON_EDIT_WARNING_COUNT_STORAGE_KEY, String(count + 1));
}
function toggleCellDetailJsonFormatted() {
settingsStore.updateEditorSettings({ cellDetailJsonFormatted: !cellDetailJsonFormatted.value });
}
function startDetailEdit() {
@ -6038,7 +6044,6 @@ async function openDialogCellInSidePanel() {
showCellDetails(detail.rowNumber - 1, detail.colIndex);
cellDetailDialogOpen.value = false;
await nextTick();
sideDetailJsonView.value = cellDetailJsonView.value && !!detail.formattedJson;
if (detail.isEditable) startDetailEdit();
}
@ -8917,7 +8922,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<div class="flex min-h-5 items-center justify-between gap-2">
<div class="text-muted-foreground">{{ t("grid.cellValue") }}</div>
<div v-if="!isEditingDetail" class="flex items-center gap-1">
<Button v-if="activeCellDetail.formattedJson" :variant="sideDetailJsonView ? 'secondary' : 'ghost'" size="sm" class="h-5 gap-1 px-1.5 text-xs" :title="t('grid.formattedJson')" @click="sideDetailJsonView = !sideDetailJsonView">
<Button v-if="activeCellDetail.formattedJson" :variant="sideDetailJsonView ? 'secondary' : 'ghost'" size="sm" class="h-5 gap-1 px-1.5 text-xs" :title="t('grid.formattedJson')" @click="toggleCellDetailJsonFormatted">
<Code2 class="h-3 w-3" />
{{ t("grid.formattedJson") }}
</Button>
@ -8959,7 +8964,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</a>
</div>
<template v-if="isEditingDetail">
<div class="min-h-32 shrink-0" :class="cellDetailPanelIsBottom ? 'h-32' : ''" :style="sideDetailEditorStyle">
<div :class="cellDetailPanelIsBottom ? 'min-h-0 flex-1' : 'min-h-32 shrink-0'" :style="sideDetailEditorStyle">
<TemporalCellEditor v-if="detailTemporalEditorKind" v-model="detailEditValue" :kind="detailTemporalEditorKind" variant="inline" :commit-on-close="false" @cancel="cancelDetailEdit" @commit="commitDetailEdit" />
<div v-else ref="detailsEditorContainer" data-cell-detail-editor-root class="min-h-0 h-full w-full rounded border overflow-hidden" />
</div>
@ -9224,7 +9229,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<div class="flex items-center justify-between gap-2">
<div class="text-muted-foreground">{{ t("grid.cellValue") }}</div>
<div class="flex items-center gap-1">
<Button v-if="dialogCellDetail.formattedJson" :variant="cellDetailJsonView ? 'secondary' : 'ghost'" size="sm" class="h-6 gap-1 px-2 text-xs" :title="t('grid.formattedJson')" @click="cellDetailJsonView = !cellDetailJsonView">
<Button v-if="dialogCellDetail.formattedJson" :variant="cellDetailJsonView ? 'secondary' : 'ghost'" size="sm" class="h-6 gap-1 px-2 text-xs" :title="t('grid.formattedJson')" @click="toggleCellDetailJsonFormatted">
<Code2 class="h-3 w-3" />
{{ t("grid.formattedJson") }}
</Button>

View File

@ -798,7 +798,7 @@ export default {
copyColumnTsv: "Copy Column (TSV)",
editValue: "Edit Value",
formatJson: "Format JSON",
formattedJsonEditWarning: "You are editing formatted JSON. Saving writes back formatted text and may change the original whitespace and layout",
formattedJsonEditWarning: "You are editing formatted JSON. Saving will write it as JSON, so the original formatting may change.",
setNull: "Set NULL",
restoreOriginalValue: "Restore Original",
copyColumnName: "Copy Column Name",

View File

@ -788,6 +788,7 @@ export default withEnglishFallback({
copyColumnTsv: "Copiar columna (TSV)",
editValue: "Editar valor",
formatJson: "Formatear JSON",
formattedJsonEditWarning: "Estás editando JSON formateado. Al guardar se escribirá como JSON, por lo que el formato original puede cambiar.",
setNull: "Establecer NULL",
restoreOriginalValue: "Restaurar original",
copyColumnName: "Copiar nombre de columna",

View File

@ -795,6 +795,7 @@ export default withEnglishFallback({
copyColumnTsv: "Copia Colonna (TSV)",
editValue: "Modifica Valore",
formatJson: "Formatta JSON",
formattedJsonEditWarning: "Stai modificando JSON formattato. Salvando verrà scritto come JSON, quindi la formattazione originale potrebbe cambiare.",
setNull: "Imposta NULL",
restoreOriginalValue: "Ripristina Originale",
copyColumnName: "Copia Nome Colonna",

View File

@ -786,6 +786,7 @@ export default withEnglishFallback({
copyColumnTsv: "列をコピー (TSV)",
editValue: "値を編集",
formatJson: "JSONをフォーマット",
formattedJsonEditWarning: "フォーマット済みJSONを編集中です。保存するとJSONとして書き込まれるため、元の書式が変わる場合があります。",
setNull: "NULLに設定",
restoreOriginalValue: "元の値に戻す",
copyColumnName: "列名をコピー",

View File

@ -800,6 +800,7 @@ export default withEnglishFallback({
copyColumnTsv: "Copiar Coluna (TSV)",
editValue: "Editar Valor",
formatJson: "Formatar JSON",
formattedJsonEditWarning: "Você está editando JSON formatado. Ao salvar, ele será gravado como JSON, então a formatação original pode mudar.",
setNull: "Definir NULL",
restoreOriginalValue: "Restaurar Original",
copyColumnName: "Copiar Nome da Coluna",

View File

@ -801,7 +801,7 @@ export default withEnglishFallback({
copyColumnTsv: "复制列 (TSV)",
editValue: "编辑值",
formatJson: "格式化 JSON",
formattedJsonEditWarning: "正在编辑格式化后的 JSON。保存后会写回格式化文本,原始空白和排版可能变化",
formattedJsonEditWarning: "正在编辑格式化后的 JSON。保存后会以JSON格式写入原始排版可能变化。",
setNull: "设为 NULL",
restoreOriginalValue: "恢复原值",
copyColumnName: "复制列名",

View File

@ -789,7 +789,7 @@ export default withEnglishFallback({
copyColumnTsv: "複製欄 (TSV)",
editValue: "編輯值",
formatJson: "格式化 JSON",
formattedJsonEditWarning: "正在編輯格式化後的 JSON。儲存後會寫回格式化文字,原始空白和排版可能改變",
formattedJsonEditWarning: "正在編輯格式化後的 JSON。儲存後會以 JSON 格式寫入,原始排版可能改變。",
setNull: "設為 NULL",
restoreOriginalValue: "復原資料",
copyColumnName: "複製欄位名稱",

View File

@ -351,6 +351,7 @@ export interface EditorSettings {
tableInfoDrawerWidth: number;
cellDetailDrawerWidth: number;
cellDetailPanelLayout: CellDetailPanelLayout;
cellDetailJsonFormatted: boolean;
shortcuts: ShortcutSettings;
sqlFormatter: SqlFormatterSettings;
sidebarActivation: SidebarActivation;
@ -459,6 +460,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
tableInfoDrawerWidth: 320,
cellDetailDrawerWidth: 380,
cellDetailPanelLayout: "bottom",
cellDetailJsonFormatted: false,
shortcuts: normalizeShortcutSettings(),
sqlFormatter: normalizeSqlFormatterSettings(DEFAULT_SQL_FORMATTER_SETTINGS),
sidebarActivation: "single",
@ -656,6 +658,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
tableInfoDrawerWidth: normalizeDrawerWidth(settings.tableInfoDrawerWidth, 240, DEFAULT_EDITOR_SETTINGS.tableInfoDrawerWidth),
cellDetailDrawerWidth: normalizeDrawerWidth(settings.cellDetailDrawerWidth, 260, DEFAULT_EDITOR_SETTINGS.cellDetailDrawerWidth),
cellDetailPanelLayout: normalizeCellDetailPanelLayout(settings.cellDetailPanelLayout),
cellDetailJsonFormatted: typeof settings.cellDetailJsonFormatted === "boolean" ? settings.cellDetailJsonFormatted : DEFAULT_EDITOR_SETTINGS.cellDetailJsonFormatted,
shortcuts: normalizeShortcutSettings(settings.shortcuts),
sqlFormatter: normalizeSqlFormatterSettings(settings.sqlFormatter),
sidebarActivation: settings.sidebarActivation === "single" || settings.sidebarActivation === "double" ? settings.sidebarActivation : DEFAULT_EDITOR_SETTINGS.sidebarActivation,
@ -835,6 +838,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.tableInfoDrawerWidth !== undefined) editorSettings.value.tableInfoDrawerWidth = normalizeDrawerWidth(partial.tableInfoDrawerWidth, 240, DEFAULT_EDITOR_SETTINGS.tableInfoDrawerWidth);
if (partial.cellDetailDrawerWidth !== undefined) editorSettings.value.cellDetailDrawerWidth = normalizeDrawerWidth(partial.cellDetailDrawerWidth, 260, DEFAULT_EDITOR_SETTINGS.cellDetailDrawerWidth);
if (partial.cellDetailPanelLayout !== undefined) editorSettings.value.cellDetailPanelLayout = normalizeCellDetailPanelLayout(partial.cellDetailPanelLayout);
if (partial.cellDetailJsonFormatted !== undefined) editorSettings.value.cellDetailJsonFormatted = partial.cellDetailJsonFormatted === true;
if (partial.shortcuts !== undefined) editorSettings.value.shortcuts = normalizeShortcutSettings(partial.shortcuts);
if (partial.sqlFormatter !== undefined) editorSettings.value.sqlFormatter = normalizeSqlFormatterSettings(partial.sqlFormatter);
if (partial.sidebarActivation !== undefined) editorSettings.value.sidebarActivation = partial.sidebarActivation;

View File

@ -228,15 +228,19 @@ test("normalizes grid drawer widths", () => {
assert.equal(DEFAULT_EDITOR_SETTINGS.tableInfoDrawerWidth, 320);
assert.equal(DEFAULT_EDITOR_SETTINGS.cellDetailDrawerWidth, 380);
assert.equal(DEFAULT_EDITOR_SETTINGS.cellDetailPanelLayout, "bottom");
assert.equal(DEFAULT_EDITOR_SETTINGS.cellDetailJsonFormatted, false);
assert.equal(normalizeEditorSettings({}).tableInfoDrawerWidth, 320);
assert.equal(normalizeEditorSettings({}).cellDetailDrawerWidth, 380);
assert.equal(normalizeEditorSettings({}).cellDetailPanelLayout, "bottom");
assert.equal(normalizeEditorSettings({}).cellDetailJsonFormatted, false);
assert.equal(normalizeEditorSettings({ tableInfoDrawerWidth: 200 } as any).tableInfoDrawerWidth, 240);
assert.equal(normalizeEditorSettings({ cellDetailDrawerWidth: 200 } as any).cellDetailDrawerWidth, 260);
assert.equal(normalizeEditorSettings({ tableInfoDrawerWidth: 1000 } as any).tableInfoDrawerWidth, 900);
assert.equal(normalizeEditorSettings({ cellDetailDrawerWidth: 456.7 } as any).cellDetailDrawerWidth, 457);
assert.equal(normalizeEditorSettings({ cellDetailPanelLayout: "right" } as any).cellDetailPanelLayout, "right");
assert.equal(normalizeEditorSettings({ cellDetailPanelLayout: "invalid" } as any).cellDetailPanelLayout, "bottom");
assert.equal(normalizeEditorSettings({ cellDetailJsonFormatted: true } as any).cellDetailJsonFormatted, true);
assert.equal(normalizeEditorSettings({ cellDetailJsonFormatted: "true" } as any).cellDetailJsonFormatted, false);
});
test("keeps saved active tab sidebar selection", () => {