mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-29 16:18:55 +00:00
feat: add value editor and Set NULL in cell detail panel
Double-click value area or click Edit button to open textarea editor for long text/JSON. Add Set NULL button for quick null assignment. Fix: null values via ?? operator were ignored in dirty row display.
This commit is contained in:
@@ -31,6 +31,7 @@ import {
|
||||
TriangleAlert,
|
||||
RefreshCcw,
|
||||
RotateCcw,
|
||||
Pencil,
|
||||
} from "lucide-vue-next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -556,7 +557,7 @@ const sortedRows = computed(() => {
|
||||
|
||||
function rowDataWithChanges(row: CellValue[], sourceIndex: number): CellValue[] {
|
||||
const dirty = dirtyRows.value.get(sourceIndex);
|
||||
return row.map((v, colIdx) => dirty?.get(colIdx) ?? v);
|
||||
return row.map((v, colIdx) => (dirty?.has(colIdx) ? dirty.get(colIdx)! : v));
|
||||
}
|
||||
|
||||
interface RowItem {
|
||||
@@ -637,6 +638,7 @@ const activeCellDetail = computed(() => {
|
||||
}
|
||||
return {
|
||||
rowNumber: cell.rowIndex + 1,
|
||||
rowId: item.id,
|
||||
colIndex: cell.col,
|
||||
column,
|
||||
type: columnTypeMap.value.get(column) || "",
|
||||
@@ -645,9 +647,83 @@ const activeCellDetail = computed(() => {
|
||||
rawValue,
|
||||
length: value === null ? 0 : String(value).length,
|
||||
formattedJson,
|
||||
isEditable: props.editable && !item.isDeleted,
|
||||
};
|
||||
});
|
||||
|
||||
const detailEditValue = ref("");
|
||||
const isEditingDetail = ref(false);
|
||||
|
||||
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);
|
||||
isEditingDetail.value = true;
|
||||
}
|
||||
|
||||
function commitDetailEdit() {
|
||||
const detail = activeCellDetail.value;
|
||||
if (!detail || !isEditingDetail.value) return;
|
||||
isEditingDetail.value = false;
|
||||
|
||||
const item = getRowItem(detail.rowId);
|
||||
if (!item || item.isDeleted) return;
|
||||
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
const oldVal = newRows.value[item.newIndex]?.[detail.colIndex];
|
||||
newRows.value[item.newIndex][detail.colIndex] = coerceCellValue(detailEditValue.value, oldVal);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.sourceIndex === undefined) return;
|
||||
|
||||
const oldVal = props.result.rows[item.sourceIndex]?.[detail.colIndex];
|
||||
const newVal = coerceCellValue(detailEditValue.value, oldVal);
|
||||
if (newVal !== oldVal) {
|
||||
if (!dirtyRows.value.has(item.sourceIndex)) dirtyRows.value.set(item.sourceIndex, new Map());
|
||||
dirtyRows.value.get(item.sourceIndex)!.set(detail.colIndex, newVal);
|
||||
if (useTransaction.value && !transactionActive.value) {
|
||||
enterTransaction();
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
function cancelDetailEdit() {
|
||||
isEditingDetail.value = false;
|
||||
}
|
||||
|
||||
function setDetailNull() {
|
||||
const detail = activeCellDetail.value;
|
||||
if (!detail || !detail.isEditable) return;
|
||||
|
||||
const item = getRowItem(detail.rowId);
|
||||
if (!item || item.isDeleted) return;
|
||||
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
newRows.value[item.newIndex][detail.colIndex] = null;
|
||||
newRows.value = [...newRows.value];
|
||||
isEditingDetail.value = false;
|
||||
detailCell.value = { ...detailCell.value! };
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.sourceIndex === undefined) return;
|
||||
if (!dirtyRows.value.has(item.sourceIndex)) dirtyRows.value.set(item.sourceIndex, new Map());
|
||||
dirtyRows.value.get(item.sourceIndex)!.set(detail.colIndex, null);
|
||||
dirtyRows.value = new Map(dirtyRows.value);
|
||||
if (useTransaction.value && !transactionActive.value) {
|
||||
enterTransaction();
|
||||
}
|
||||
isEditingDetail.value = false;
|
||||
detailCell.value = { ...detailCell.value! };
|
||||
}
|
||||
|
||||
function toggleSort(colName: string, colIdx: number) {
|
||||
if (isResizing) return;
|
||||
if (sortCol.value === colName && sortColIndex.value === colIdx) {
|
||||
@@ -1802,8 +1878,26 @@ defineExpose({
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.cellValue") }}</div>
|
||||
<template v-if="isEditingDetail">
|
||||
<textarea
|
||||
v-model="detailEditValue"
|
||||
class="w-full h-40 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
|
||||
class="max-h-56 overflow-auto rounded border bg-muted/20 p-2 font-mono text-xs whitespace-pre-wrap break-words"
|
||||
v-else
|
||||
class="max-h-56 overflow-auto rounded border bg-muted/20 p-2 font-mono text-xs whitespace-pre-wrap break-words cursor-pointer hover:border-primary/50"
|
||||
:class="{ 'cursor-text': activeCellDetail.isEditable }"
|
||||
@dblclick="startDetailEdit"
|
||||
>{{ activeCellDetail.rawValue }}</pre
|
||||
>
|
||||
</div>
|
||||
@@ -1817,6 +1911,24 @@ defineExpose({
|
||||
</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>
|
||||
|
||||
@@ -214,6 +214,8 @@ export default {
|
||||
noComment: "No comment",
|
||||
formattedJson: "Formatted JSON",
|
||||
copyValue: "Copy Value",
|
||||
editValue: "Edit Value",
|
||||
setNull: "Set NULL",
|
||||
copyColumnName: "Copy Column Name",
|
||||
copySqlCondition: "Copy SQL Condition",
|
||||
transpose: "Transpose Row",
|
||||
|
||||
@@ -213,6 +213,8 @@ export default {
|
||||
noComment: "暂无注释",
|
||||
formattedJson: "格式化 JSON",
|
||||
copyValue: "复制值",
|
||||
editValue: "编辑值",
|
||||
setNull: "设为 NULL",
|
||||
copyColumnName: "复制列名",
|
||||
copySqlCondition: "复制 SQL 条件",
|
||||
transpose: "转置查看",
|
||||
|
||||
Reference in New Issue
Block a user