feat(grid): add cell detail value editor
This commit is contained in:
parent
75f92ce725
commit
1ed26ff4d7
|
|
@ -66,6 +66,7 @@ import {
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import ImagePreviewDialog from "@/components/grid/ImagePreviewDialog.vue";
|
||||
|
|
@ -98,6 +99,13 @@ import {
|
|||
import { matchesRowStatusFilter, type RowStatus, type RowStatusFilter } from "@/lib/gridRowStatus";
|
||||
import { displayCellValue, type CellValue } from "@/lib/cellValue";
|
||||
import { cellImagePreviewUrl } from "@/lib/cellImageUrl";
|
||||
import {
|
||||
cellDetailEditorText,
|
||||
defaultCellDetailTab,
|
||||
valueEditorActions,
|
||||
visibleCellDetailTabs,
|
||||
type CellDetailTab,
|
||||
} from "@/lib/cellDetailPresentation";
|
||||
import {
|
||||
applyColumnFormatter,
|
||||
buildColumnFormatterKey,
|
||||
|
|
@ -272,6 +280,7 @@ function typeColorClass(t: string): string {
|
|||
const contextCell = ref<{ rowId: number; rowIndex: number; col: number } | null>(null);
|
||||
const detailCell = ref<{ rowIndex: number; col: number } | null>(null);
|
||||
const showCellDetail = ref(false);
|
||||
const activeCellDetailTab = ref<CellDetailTab>(defaultCellDetailTab());
|
||||
const detailWidth = ref(320);
|
||||
const isResizingDetail = ref(false);
|
||||
const imagePreviewOpen = ref(false);
|
||||
|
|
@ -1731,6 +1740,30 @@ const activeCellDetail = computed(() => {
|
|||
};
|
||||
});
|
||||
|
||||
const activeCellDetailTabs = computed(() => {
|
||||
const detail = activeCellDetail.value;
|
||||
return visibleCellDetailTabs({ isEditable: !!detail?.isEditable });
|
||||
});
|
||||
|
||||
watch(activeCellDetailTabs, (tabs) => {
|
||||
if (!tabs.includes(activeCellDetailTab.value)) {
|
||||
activeCellDetailTab.value = defaultCellDetailTab();
|
||||
}
|
||||
});
|
||||
|
||||
watch(activeCellDetailTab, (tab) => {
|
||||
if (tab === "valueEditor") {
|
||||
startDetailEdit();
|
||||
} else {
|
||||
resetDetailEdit();
|
||||
}
|
||||
});
|
||||
|
||||
const activeValueEditorActions = computed(() => {
|
||||
const detail = activeCellDetail.value;
|
||||
return valueEditorActions({ canSetNull: !!detail?.isEditable && detail.value !== null });
|
||||
});
|
||||
|
||||
const detailEditValue = ref("");
|
||||
const isEditingDetail = ref(false);
|
||||
const detailTemporalEditorKind = computed(() => {
|
||||
|
|
@ -1752,8 +1785,7 @@ function closeCellDetails() {
|
|||
function startDetailEdit() {
|
||||
const detail = activeCellDetail.value;
|
||||
if (!detail || !detail.isEditable) return;
|
||||
detailEditValue.value =
|
||||
detail.value === null ? "" : typeof detail.value === "object" ? JSON.stringify(detail.value) : String(detail.value);
|
||||
detailEditValue.value = cellDetailEditorText(detail.value);
|
||||
isEditingDetail.value = true;
|
||||
}
|
||||
|
||||
|
|
@ -1794,6 +1826,51 @@ function cancelDetailEdit() {
|
|||
resetDetailEdit();
|
||||
}
|
||||
|
||||
function cancelValueEditorEdit() {
|
||||
const detail = activeCellDetail.value;
|
||||
if (!detail || !detail.isEditable) return;
|
||||
detailEditValue.value = cellDetailEditorText(detail.value);
|
||||
isEditingDetail.value = true;
|
||||
}
|
||||
|
||||
function commitValueEditorEdit() {
|
||||
commitDetailEdit();
|
||||
if (activeCellDetailTab.value === "valueEditor") {
|
||||
isEditingDetail.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function restoreDetailOriginalValue() {
|
||||
const detail = activeCellDetail.value;
|
||||
if (!detail || !detail.isEditable) return;
|
||||
|
||||
const item = getRowItem(detail.rowId);
|
||||
if (!item || item.isDeleted) return;
|
||||
|
||||
let restoredValue: CellValue = null;
|
||||
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
newRows.value[item.newIndex][detail.colIndex] = null;
|
||||
newRows.value = [...newRows.value];
|
||||
} else if (item.sourceIndex !== undefined) {
|
||||
restoredValue = props.result.rows[item.sourceIndex]?.[detail.colIndex] ?? null;
|
||||
const rowChanges = dirtyRows.value.get(item.sourceIndex);
|
||||
rowChanges?.delete(detail.colIndex);
|
||||
if (rowChanges?.size === 0) dirtyRows.value.delete(item.sourceIndex);
|
||||
dirtyRows.value = new Map(dirtyRows.value);
|
||||
}
|
||||
|
||||
detailEditValue.value = cellDetailEditorText(restoredValue);
|
||||
isEditingDetail.value = activeCellDetailTab.value === "valueEditor";
|
||||
detailCell.value = { ...detailCell.value! };
|
||||
}
|
||||
|
||||
function setValueEditorNull() {
|
||||
setDetailNull();
|
||||
detailEditValue.value = cellDetailEditorText(null);
|
||||
isEditingDetail.value = activeCellDetailTab.value === "valueEditor";
|
||||
}
|
||||
|
||||
function setDetailNull() {
|
||||
const detail = activeCellDetail.value;
|
||||
if (!detail || !detail.isEditable) return;
|
||||
|
|
@ -2036,6 +2113,7 @@ const {
|
|||
function showCellDetails(rowIndex: number, colIndex: number) {
|
||||
resetDetailEdit();
|
||||
detailCell.value = { rowIndex, col: colIndex };
|
||||
activeCellDetailTab.value = defaultCellDetailTab();
|
||||
showCellDetail.value = true;
|
||||
}
|
||||
|
||||
|
|
@ -3972,149 +4050,219 @@ defineExpose({
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-auto p-3 text-xs space-y-3">
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.columnName") }}</div>
|
||||
<div class="font-medium break-all">{{ activeCellDetail.column }}</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.rowNumber") }}</div>
|
||||
<div>{{ activeCellDetail.rowNumber }}</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.columnType") }}</div>
|
||||
<div
|
||||
:class="activeCellDetail.type ? typeColorClass(activeCellDetail.type) : 'text-muted-foreground'"
|
||||
<Tabs v-model="activeCellDetailTab" class="flex-1 min-h-0 gap-0">
|
||||
<div class="shrink-0 border-b px-3 py-2">
|
||||
<TabsList
|
||||
class="grid h-7 w-full p-0.5"
|
||||
:class="activeCellDetailTabs.length > 1 ? 'grid-cols-2' : 'grid-cols-1'"
|
||||
>
|
||||
<TabsTrigger value="details" class="h-6 text-xs">{{ t("grid.cellDetails") }}</TabsTrigger>
|
||||
<TabsTrigger
|
||||
v-if="activeCellDetailTabs.includes('valueEditor')"
|
||||
value="valueEditor"
|
||||
class="h-6 text-xs"
|
||||
>
|
||||
{{ activeCellDetail.type || "-" }}
|
||||
{{ t("grid.valueEditor") }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="details" class="m-0 min-h-0 flex-1 flex flex-col">
|
||||
<div class="flex-1 min-h-0 overflow-auto p-3 text-xs space-y-3">
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.columnName") }}</div>
|
||||
<div class="font-medium break-all">{{ activeCellDetail.column }}</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.rowNumber") }}</div>
|
||||
<div>{{ activeCellDetail.rowNumber }}</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.columnType") }}</div>
|
||||
<div
|
||||
:class="
|
||||
activeCellDetail.type ? typeColorClass(activeCellDetail.type) : 'text-muted-foreground'
|
||||
"
|
||||
>
|
||||
{{ activeCellDetail.type || "-" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.nullValue") }}</div>
|
||||
<div>{{ activeCellDetail.value === null ? "true" : "false" }}</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.valueLength") }}</div>
|
||||
<div>{{ activeCellDetail.length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.columnComment") }}</div>
|
||||
<div class="whitespace-pre-wrap break-words">
|
||||
{{ activeCellDetail.comment || t("grid.noComment") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.cellValue") }}</div>
|
||||
<div v-if="activeCellDetail.imagePreviewUrl && !isEditingDetail" class="space-y-1.5">
|
||||
<div class="text-muted-foreground">{{ t("grid.imagePreview") }}</div>
|
||||
<a
|
||||
:href="activeCellDetail.imagePreviewUrl"
|
||||
role="button"
|
||||
class="block overflow-hidden rounded border bg-muted/20"
|
||||
@click.prevent="openImagePreview(activeCellDetail.imagePreviewUrl, activeCellDetail.column)"
|
||||
>
|
||||
<img
|
||||
:src="activeCellDetail.imagePreviewUrl"
|
||||
:alt="activeCellDetail.column"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
referrerpolicy="no-referrer"
|
||||
class="max-h-72 w-full object-contain"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<template v-if="isEditingDetail">
|
||||
<TemporalCellEditor
|
||||
v-if="detailTemporalEditorKind"
|
||||
v-model="detailEditValue"
|
||||
:kind="detailTemporalEditorKind"
|
||||
variant="inline"
|
||||
:commit-on-close="false"
|
||||
@cancel="cancelDetailEdit"
|
||||
@commit="commitDetailEdit"
|
||||
/>
|
||||
<textarea
|
||||
v-else
|
||||
v-model="detailEditValue"
|
||||
wrap="off"
|
||||
class="w-full h-40 overflow-auto rounded border bg-background p-2 font-mono text-xs outline-none resize-y focus:border-primary"
|
||||
@keydown.escape.stop="cancelDetailEdit"
|
||||
/>
|
||||
<div class="flex gap-1 mt-1">
|
||||
<Button size="sm" class="h-6 text-xs" @click="commitDetailEdit">
|
||||
{{ t("dangerDialog.confirm") }}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" class="h-6 text-xs" @click="cancelDetailEdit">
|
||||
{{ t("dangerDialog.cancel") }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<pre
|
||||
v-else
|
||||
class="max-h-56 overflow-auto rounded border bg-muted/20 p-2 font-mono text-xs whitespace-pre cursor-pointer hover:border-primary/50"
|
||||
:class="{ 'cursor-text': activeCellDetail.isEditable }"
|
||||
@dblclick="startDetailEdit"
|
||||
>{{ activeCellDetail.displayValue }}</pre
|
||||
>
|
||||
</div>
|
||||
<div v-if="activeCellDetail.displayValue !== activeCellDetail.rawValue" class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.rawValue") }}</div>
|
||||
<pre
|
||||
class="max-h-40 overflow-auto rounded border bg-muted/20 p-2 font-mono text-xs whitespace-pre-wrap break-words"
|
||||
>{{ activeCellDetail.rawValue }}</pre
|
||||
>
|
||||
</div>
|
||||
<div v-if="activeCellDetail.formattedJson" class="mt-2 space-y-1">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="text-muted-foreground">{{ t("grid.formattedJson") }}</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-6 px-2 text-xs"
|
||||
:title="t('grid.copyValue')"
|
||||
@click="copyDetailFormattedJson"
|
||||
>
|
||||
<Copy class="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<pre
|
||||
class="max-h-72 overflow-auto rounded border bg-muted/20 p-2 font-mono text-xs whitespace-pre-wrap break-words"
|
||||
>{{ activeCellDetail.formattedJson }}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.nullValue") }}</div>
|
||||
<div>{{ activeCellDetail.value === null ? "true" : "false" }}</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.valueLength") }}</div>
|
||||
<div>{{ activeCellDetail.length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.columnComment") }}</div>
|
||||
<div class="whitespace-pre-wrap break-words">
|
||||
{{ activeCellDetail.comment || t("grid.noComment") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.cellValue") }}</div>
|
||||
<div v-if="activeCellDetail.imagePreviewUrl && !isEditingDetail" class="space-y-1.5">
|
||||
<div class="text-muted-foreground">{{ t("grid.imagePreview") }}</div>
|
||||
<a
|
||||
:href="activeCellDetail.imagePreviewUrl"
|
||||
role="button"
|
||||
class="block overflow-hidden rounded border bg-muted/20"
|
||||
@click.prevent="openImagePreview(activeCellDetail.imagePreviewUrl, activeCellDetail.column)"
|
||||
|
||||
<div class="border-t p-2 grid grid-cols-1 gap-1">
|
||||
<Button
|
||||
v-if="activeCellDetail.isEditable && !isEditingDetail"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 justify-start text-xs"
|
||||
@click="startDetailEdit"
|
||||
>
|
||||
<img
|
||||
:src="activeCellDetail.imagePreviewUrl"
|
||||
:alt="activeCellDetail.column"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
referrerpolicy="no-referrer"
|
||||
class="max-h-72 w-full object-contain"
|
||||
/>
|
||||
</a>
|
||||
<Pencil class="w-3 h-3 mr-2" /> {{ t("grid.editValue") }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="activeCellDetail.isEditable && activeCellDetail.value !== null"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 justify-start text-xs"
|
||||
@click="setDetailNull"
|
||||
>
|
||||
<X class="w-3 h-3 mr-2" /> {{ t("grid.setNull") }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-7 justify-start text-xs" @click="copyDetailValue">
|
||||
<Copy class="w-3 h-3 mr-2" /> {{ t("grid.copyValue") }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-7 justify-start text-xs" @click="copyDetailColumnName">
|
||||
<Copy class="w-3 h-3 mr-2" /> {{ t("grid.copyColumnName") }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-7 justify-start text-xs" @click="copyDetailSqlCondition">
|
||||
<Code2 class="w-3 h-3 mr-2" /> {{ t("grid.copySqlCondition") }}
|
||||
</Button>
|
||||
</div>
|
||||
<template v-if="isEditingDetail">
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
v-if="activeCellDetailTabs.includes('valueEditor')"
|
||||
value="valueEditor"
|
||||
class="m-0 min-h-0 flex-1 flex flex-col p-3 text-xs"
|
||||
>
|
||||
<div class="flex min-h-0 flex-1 flex-col">
|
||||
<TemporalCellEditor
|
||||
v-if="detailTemporalEditorKind"
|
||||
v-model="detailEditValue"
|
||||
:kind="detailTemporalEditorKind"
|
||||
variant="inline"
|
||||
:commit-on-close="false"
|
||||
@cancel="cancelDetailEdit"
|
||||
@commit="commitDetailEdit"
|
||||
@cancel="cancelValueEditorEdit"
|
||||
@commit="commitValueEditorEdit"
|
||||
/>
|
||||
<textarea
|
||||
v-else
|
||||
v-model="detailEditValue"
|
||||
wrap="off"
|
||||
class="w-full h-40 overflow-auto rounded border bg-background p-2 font-mono text-xs outline-none resize-y focus:border-primary"
|
||||
@keydown.escape.stop="cancelDetailEdit"
|
||||
class="min-h-0 flex-1 w-full overflow-auto rounded border bg-background p-2 font-mono text-xs outline-none resize-none focus:border-primary"
|
||||
@blur="commitValueEditorEdit"
|
||||
@keydown.escape.stop="restoreDetailOriginalValue"
|
||||
/>
|
||||
<div class="flex gap-1 mt-1">
|
||||
<Button size="sm" class="h-6 text-xs" @click="commitDetailEdit">
|
||||
{{ t("dangerDialog.confirm") }}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" class="h-6 text-xs" @click="cancelDetailEdit">
|
||||
{{ t("dangerDialog.cancel") }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<pre
|
||||
v-else
|
||||
class="max-h-56 overflow-auto rounded border bg-muted/20 p-2 font-mono text-xs whitespace-pre cursor-pointer hover:border-primary/50"
|
||||
:class="{ 'cursor-text': activeCellDetail.isEditable }"
|
||||
@dblclick="startDetailEdit"
|
||||
>{{ activeCellDetail.displayValue }}</pre
|
||||
>
|
||||
</div>
|
||||
<div v-if="activeCellDetail.displayValue !== activeCellDetail.rawValue" class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.rawValue") }}</div>
|
||||
<pre
|
||||
class="max-h-40 overflow-auto rounded border bg-muted/20 p-2 font-mono text-xs whitespace-pre-wrap break-words"
|
||||
>{{ activeCellDetail.rawValue }}</pre
|
||||
>
|
||||
</div>
|
||||
<div v-if="activeCellDetail.formattedJson" class="mt-2 space-y-1">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="text-muted-foreground">{{ t("grid.formattedJson") }}</div>
|
||||
</div>
|
||||
<div class="flex gap-1 mt-2 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
v-if="activeValueEditorActions.includes('setNull')"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-6 px-2 text-xs"
|
||||
:title="t('grid.copyValue')"
|
||||
@click="copyDetailFormattedJson"
|
||||
class="h-6 text-xs"
|
||||
@mousedown.prevent
|
||||
@click="setValueEditorNull"
|
||||
>
|
||||
<Copy class="h-3 w-3" />
|
||||
{{ t("grid.setNull") }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="activeValueEditorActions.includes('restoreOriginal')"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-6 text-xs"
|
||||
@mousedown.prevent
|
||||
@click="restoreDetailOriginalValue"
|
||||
>
|
||||
{{ t("grid.restoreOriginalValue") }}
|
||||
</Button>
|
||||
</div>
|
||||
<pre
|
||||
class="max-h-72 overflow-auto rounded border bg-muted/20 p-2 font-mono text-xs whitespace-pre-wrap break-words"
|
||||
>{{ activeCellDetail.formattedJson }}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t p-2 grid grid-cols-1 gap-1">
|
||||
<Button
|
||||
v-if="activeCellDetail.isEditable && !isEditingDetail"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 justify-start text-xs"
|
||||
@click="startDetailEdit"
|
||||
>
|
||||
<Pencil class="w-3 h-3 mr-2" /> {{ t("grid.editValue") }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="activeCellDetail.isEditable && activeCellDetail.value !== null"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 justify-start text-xs"
|
||||
@click="setDetailNull"
|
||||
>
|
||||
<X class="w-3 h-3 mr-2" /> {{ t("grid.setNull") }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-7 justify-start text-xs" @click="copyDetailValue">
|
||||
<Copy class="w-3 h-3 mr-2" /> {{ t("grid.copyValue") }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-7 justify-start text-xs" @click="copyDetailColumnName">
|
||||
<Copy class="w-3 h-3 mr-2" /> {{ t("grid.copyColumnName") }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-7 justify-start text-xs" @click="copyDetailSqlCondition">
|
||||
<Code2 class="w-3 h-3 mr-2" /> {{ t("grid.copySqlCondition") }}
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -354,6 +354,7 @@ export default {
|
|||
selectedCells: "{count} selected",
|
||||
cellDetails: "Cell Details",
|
||||
cellValue: "Value",
|
||||
valueEditor: "Value Editor",
|
||||
imagePreview: "Image Preview",
|
||||
imageLoadFailed: "Image failed to load",
|
||||
zoomIn: "Zoom In",
|
||||
|
|
@ -372,6 +373,7 @@ export default {
|
|||
copyValue: "Copy Value",
|
||||
editValue: "Edit Value",
|
||||
setNull: "Set NULL",
|
||||
restoreOriginalValue: "Restore Original",
|
||||
copyColumnName: "Copy Column Name",
|
||||
copySqlCondition: "Copy SQL Condition",
|
||||
transpose: "Transpose Row",
|
||||
|
|
|
|||
|
|
@ -322,6 +322,7 @@ export default {
|
|||
selectedCells: "{count} seleccionadas",
|
||||
cellDetails: "Detalles de celda",
|
||||
cellValue: "Valor",
|
||||
valueEditor: "Editor de valor",
|
||||
imagePreview: "Vista previa de imagen",
|
||||
imageLoadFailed: "No se pudo cargar la imagen",
|
||||
zoomIn: "Acercar",
|
||||
|
|
@ -336,9 +337,11 @@ export default {
|
|||
nullValue: "NULL",
|
||||
noComment: "Sin comentario",
|
||||
formattedJson: "JSON formateado",
|
||||
rawValue: "Valor sin procesar",
|
||||
copyValue: "Copiar valor",
|
||||
editValue: "Editar valor",
|
||||
setNull: "Establecer NULL",
|
||||
restoreOriginalValue: "Restaurar original",
|
||||
copyColumnName: "Copiar nombre de columna",
|
||||
copySqlCondition: "Copiar condición SQL",
|
||||
transpose: "Transponer fila",
|
||||
|
|
|
|||
|
|
@ -351,6 +351,7 @@ export default {
|
|||
selectedCells: "已选 {count} 个单元格",
|
||||
cellDetails: "单元格详情",
|
||||
cellValue: "值",
|
||||
valueEditor: "值编辑器",
|
||||
imagePreview: "图片预览",
|
||||
imageLoadFailed: "图片加载失败",
|
||||
zoomIn: "放大",
|
||||
|
|
@ -369,6 +370,7 @@ export default {
|
|||
copyValue: "复制值",
|
||||
editValue: "编辑值",
|
||||
setNull: "设为 NULL",
|
||||
restoreOriginalValue: "恢复原值",
|
||||
copyColumnName: "复制列名",
|
||||
copySqlCondition: "复制 SQL 条件",
|
||||
transpose: "转置查看",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
export type CellDetailTab = "details" | "valueEditor";
|
||||
export type ValueEditorAction = "setNull" | "restoreOriginal";
|
||||
|
||||
export interface CellDetailPresentationOptions {
|
||||
isEditable: boolean;
|
||||
}
|
||||
|
||||
export function defaultCellDetailTab(): CellDetailTab {
|
||||
return "details";
|
||||
}
|
||||
|
||||
export function visibleCellDetailTabs(options: CellDetailPresentationOptions): CellDetailTab[] {
|
||||
const tabs: CellDetailTab[] = ["details"];
|
||||
if (options.isEditable) {
|
||||
tabs.push("valueEditor");
|
||||
}
|
||||
return tabs;
|
||||
}
|
||||
|
||||
export function cellDetailEditorText(value: unknown): string {
|
||||
if (value === null) return "";
|
||||
if (typeof value === "object") return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export function valueEditorActions(options: { canSetNull: boolean }): ValueEditorAction[] {
|
||||
return options.canSetNull ? ["setNull", "restoreOriginal"] : ["restoreOriginal"];
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
import {
|
||||
cellDetailEditorText,
|
||||
defaultCellDetailTab,
|
||||
visibleCellDetailTabs,
|
||||
valueEditorActions,
|
||||
type CellDetailPresentationOptions,
|
||||
} from "../../apps/desktop/src/lib/cellDetailPresentation.ts";
|
||||
|
||||
function options(overrides: Partial<CellDetailPresentationOptions> = {}): CellDetailPresentationOptions {
|
||||
return {
|
||||
isEditable: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("cell detail drawer keeps the original details tab as the default", () => {
|
||||
assert.equal(defaultCellDetailTab(), "details");
|
||||
});
|
||||
|
||||
test("cell detail drawer only shows the original details tab for readonly cells", () => {
|
||||
assert.deepEqual(visibleCellDetailTabs(options()), ["details"]);
|
||||
});
|
||||
|
||||
test("cell detail drawer adds a long value editor tab for editable cells", () => {
|
||||
assert.deepEqual(visibleCellDetailTabs(options({ isEditable: true })), ["details", "valueEditor"]);
|
||||
});
|
||||
|
||||
test("cell detail value editor restores the original value text on cancel", () => {
|
||||
assert.equal(cellDetailEditorText({ nested: true }), '{"nested":true}');
|
||||
assert.equal(cellDetailEditorText(null), "");
|
||||
assert.equal(cellDetailEditorText("already text"), "already text");
|
||||
});
|
||||
|
||||
test("cell detail value editor uses cell actions instead of confirm and cancel", () => {
|
||||
assert.deepEqual(valueEditorActions({ canSetNull: true }), ["setNull", "restoreOriginal"]);
|
||||
assert.deepEqual(valueEditorActions({ canSetNull: false }), ["restoreOriginal"]);
|
||||
});
|
||||
Loading…
Reference in New Issue