feat(grid): add generated cell values

This commit is contained in:
zipg authored and GitHub committed 2026-07-16 18:43:49 +08:00
1 parent 9efdf79ad9
commit 97d15b3e2d
16 files changed
+376 -13

No files matched your search

+140 -5
View File
@@ -37,6 +37,7 @@ import {
Database,
Columns3,
PencilRuler,
WandSparkles,
} from "@lucide/vue";
import { Button } from "@/components/ui/button";
import QueryLoadingState from "@/components/common/QueryLoadingState.vue";
@@ -65,6 +66,7 @@ import { createColumnDrafts } from "@/lib/table/tableStructureEditorState";
import type { BuildSingleColumnAlterSqlOptions } from "@/lib/table/tableStructureEditorSql";
import { buildTableSelectSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
import { uuid } from "@/lib/common/utils";
import { generateCellValues, type CellValueGenerationKind } from "@/lib/dataGrid/cellValueGeneration";
import { compactHeaderColumnType, resolveHeaderColumnType } from "@/lib/dataGrid/dataGridColumnType";
import {
canDeleteExistingTdengineRows,
@@ -508,6 +510,9 @@ const contextHeaderColumn = ref<string | null>(null);
const contextHeaderColumnIndex = ref<number | null>(null);
const bulkEditDialogOpen = ref(false);
const bulkEditValue = ref("");
const generateIncrementDialogOpen = ref(false);
const generateIncrementStartValue = ref("1");
const generateIncrementTarget = ref<"selection" | "detail">("selection");
const detailCell = ref<{ rowIndex: number; col: number } | null>(null);
const hoveredDetailCell = ref<{ rowIndex: number; col: number } | null>(null);
const quickDownloadMenuCell = ref<{ rowIndex: number; col: number } | null>(null);
@@ -5290,16 +5295,16 @@ function selectedVisibleColumnIndexes(): number[] {
return [...selectedColumnIndexes.value].filter((index) => index >= 0 && index < visibleColumns.value.length).sort((a, b) => a - b);
}
function applyVisibleCellValue(item: RowItem, visibleCol: number, value: string | null): boolean {
function applyVisibleCellValue(item: RowItem, visibleCol: number, value: string | null, options: { preserveEmptyString?: boolean } = {}): boolean {
const actualCol = actualColumnIndex(visibleCol);
if (!canEditCellItem(item, actualCol)) return false;
applyCellValue(item.id, actualCol, value);
applyCellValue(item.id, actualCol, value, options);
return true;
}
function applyVisibleSelectedCellValue(item: RowItem, visibleCol: number, value: string | null, allowDraft = selectedRangeTargetsOnlyDraftRow()): boolean {
function applyVisibleSelectedCellValue(item: RowItem, visibleCol: number, value: string | null, allowDraft = selectedRangeTargetsOnlyDraftRow(), options: { preserveEmptyString?: boolean } = {}): boolean {
if (!canApplyGridSelectionValue({ isDraft: !!item.isDraft, allowDraft })) return false;
return applyVisibleCellValue(item, visibleCol, value);
return applyVisibleCellValue(item, visibleCol, value, options);
}
function selectedRangeTargetsOnlyDraftRow(): boolean {
@@ -5380,6 +5385,94 @@ function applyBulkEditValue() {
bulkEditDialogOpen.value = false;
}
interface EditableSelectionCell {
item: RowItem;
visibleCol: number;
}
function editableSelectionCells(): EditableSelectionCell[] {
const cells: EditableSelectionCell[] = [];
const range = selectedRange.value;
if (range) {
for (let rowIndex = range.startRow; rowIndex <= range.endRow; rowIndex++) {
const item = displayItemAt(rowIndex);
if (!item) continue;
for (let visibleCol = range.startCol; visibleCol <= range.endCol; visibleCol++) {
if (canEditCellItem(item, actualColumnIndex(visibleCol))) cells.push({ item, visibleCol });
}
}
return cells;
}
const visibleColumnIndexes = selectedVisibleColumnIndexes();
for (let rowIndex = 0; rowIndex < displayRowCount.value; rowIndex++) {
const item = displayItemAt(rowIndex);
if (!item) continue;
for (const visibleCol of visibleColumnIndexes) {
if (canEditCellItem(item, actualColumnIndex(visibleCol))) cells.push({ item, visibleCol });
}
}
return cells;
}
function applyGeneratedSelectionValue(kind: CellValueGenerationKind, startValue = 1n): boolean {
if (!props.editable) return false;
const cells = editableSelectionCells();
if (!cells.length) return false;
const values = generateCellValues(kind, cells.length, { startValue });
const allowDraftSelectionValue = selectedRangeTargetsOnlyDraftRow();
let applied = false;
cells.forEach((cell, index) => {
applied = applyVisibleSelectedCellValue(cell.item, cell.visibleCol, values[index] ?? null, allowDraftSelectionValue, { preserveEmptyString: kind === "empty" }) || applied;
});
if (applied) toast(t("grid.generatedValuesApplied", { count: cells.length }));
return applied;
}
function applyGeneratedDetailValue(kind: CellValueGenerationKind, startValue = 1n): boolean {
const detail = activeCellDetail.value;
if (!detail?.isEditable) return false;
const value = generateCellValues(kind, 1, { startValue })[0] ?? null;
applyCellValue(detail.rowId, detail.colIndex, value, { preserveEmptyString: kind === "empty" });
detailEditValue.value = cellDetailEditorText(value);
syncEditorFromDetailEdit();
isEditingDetail.value = activeCellDetailTab.value === "valueEditor";
detailCell.value = { ...detailCell.value! };
return true;
}
function openGenerateIncrementDialog(target: "selection" | "detail") {
if (target === "selection" && (!props.editable || !selectionHasEditableCells())) return;
if (target === "detail" && !activeCellDetail.value?.isEditable) return;
generateIncrementTarget.value = target;
generateIncrementStartValue.value = "1";
generateIncrementDialogOpen.value = true;
}
function applyGenerateIncrementValue() {
let startValue: bigint;
try {
startValue = BigInt(generateIncrementStartValue.value.trim() || "1");
} catch {
toast(t("grid.generateStartInvalid"));
return;
}
const applied = generateIncrementTarget.value === "detail" ? applyGeneratedDetailValue("increment", startValue) : applyGeneratedSelectionValue("increment", startValue);
if (applied) generateIncrementDialogOpen.value = false;
}
function generateSelectionMenuItems(disabled: boolean): ContextMenuItem[] {
return [
{ label: t("grid.generateEmptyString"), action: () => applyGeneratedSelectionValue("empty"), disabled },
{ label: t("grid.generateNull"), action: () => applyGeneratedSelectionValue("null"), disabled },
{ label: t("grid.generateCurrentDatetime"), action: () => applyGeneratedSelectionValue("datetime"), disabled },
{ label: t("grid.generateCurrentDate"), action: () => applyGeneratedSelectionValue("date"), disabled },
{ label: t("grid.generateUuid"), action: () => applyGeneratedSelectionValue("uuid"), disabled },
{ label: t("grid.generateSnowflakeId"), action: () => applyGeneratedSelectionValue("snowflake"), disabled },
{ label: t("grid.generateIncrementId"), action: () => openGenerateIncrementDialog("selection"), disabled },
];
}
function cutSelection() {
if (!props.editable || !selectedRange.value) return;
copySelectionTsv();
@@ -7178,6 +7271,7 @@ function exportSubmenu(): ContextMenuItem {
const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
const row = contextRowItem.value;
const rowLabels = rowActionLabels();
const hasEditableSelection = selectionHasEditableCells();
const previewItems: ContextMenuItem[] = [];
if (!contextHeaderColumn.value && contextCell.value) {
const colType = props.result.column_types?.[contextCell.value.col];
@@ -7225,7 +7319,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
headerColumn: !!contextHeaderColumn.value,
editable: props.editable,
hasCellSelection: hasCellSelection.value,
hasEditableSelection: selectionHasEditableCells(),
hasEditableSelection,
hasSelection: hasCellSelection.value,
labels: {
cellDetails: t("grid.openCellDetailsDialog"),
@@ -7240,6 +7334,12 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
downloadItem: binaryDownloadSubmenu(contextCellDetail.value),
copySubmenu: copySubmenu(),
selectionSubmenu: selectionSubmenu(),
generateSubmenu: {
label: t("grid.generateValue"),
icon: WandSparkles,
disabled: !hasEditableSelection,
children: generateSelectionMenuItems(!hasEditableSelection),
},
}),
createDataGridRowContextMenuItems({
editable: props.editable,
@@ -8608,6 +8708,23 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<div v-else ref="valueEditorContainer" data-cell-detail-editor-root class="min-h-0 flex-1 w-full rounded border overflow-auto" />
</div>
<div class="flex gap-1 mt-2 shrink-0">
<DropdownMenu v-if="activeCellDetail?.isEditable">
<DropdownMenuTrigger as-child>
<Button variant="outline" size="sm" class="h-6 gap-1 text-xs" @mousedown.prevent>
<WandSparkles class="h-3 w-3" />
{{ t("grid.generateValue") }}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" class="w-44">
<DropdownMenuItem @click="applyGeneratedDetailValue('empty')">{{ t("grid.generateEmptyString") }}</DropdownMenuItem>
<DropdownMenuItem @click="applyGeneratedDetailValue('null')">{{ t("grid.generateNull") }}</DropdownMenuItem>
<DropdownMenuItem @click="applyGeneratedDetailValue('datetime')">{{ t("grid.generateCurrentDatetime") }}</DropdownMenuItem>
<DropdownMenuItem @click="applyGeneratedDetailValue('date')">{{ t("grid.generateCurrentDate") }}</DropdownMenuItem>
<DropdownMenuItem @click="applyGeneratedDetailValue('uuid')">{{ t("grid.generateUuid") }}</DropdownMenuItem>
<DropdownMenuItem @click="applyGeneratedDetailValue('snowflake')">{{ t("grid.generateSnowflakeId") }}</DropdownMenuItem>
<DropdownMenuItem @click="openGenerateIncrementDialog('detail')">{{ t("grid.generateIncrementId") }}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button v-if="activeValueEditorActions.includes('formatJson')" variant="outline" size="sm" class="h-6 text-xs" @mousedown.prevent @click="formatValueEditorJson">
{{ t("grid.formatJson") }}
</Button>
@@ -8734,6 +8851,24 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<DataGridBulkEditDialog v-if="bulkEditDialogMounted" v-model:open="bulkEditDialogOpen" v-model:value="bulkEditValue" :selected-cell-count="selectedCellCount" @apply="applyBulkEditValue" />
<Dialog v-model:open="generateIncrementDialogOpen">
<DialogContent class="sm:max-w-[380px]">
<DialogHeader>
<DialogTitle>{{ t("grid.generateIncrementId") }}</DialogTitle>
</DialogHeader>
<div class="space-y-2">
<p class="text-sm text-muted-foreground">
{{ t("grid.generateSequenceDescription", { count: generateIncrementTarget === "detail" ? 1 : editableSelectionCells().length }) }}
</p>
<Input v-model="generateIncrementStartValue" inputmode="numeric" autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false" placeholder="1" @keydown.enter.prevent="applyGenerateIncrementValue" />
</div>
<DialogFooter>
<Button variant="outline" @click="generateIncrementDialogOpen = false">{{ t("dangerDialog.cancel") }}</Button>
<Button @click="applyGenerateIncrementValue">{{ t("grid.applyBulkEdit") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- SQL Preview panel for pending data changes -->
<div v-if="showSqlPreview" class="h-52 shrink-0 border-t">
<SqlPreviewPanel :sql="previewSqlText" :loading="isPreviewLoading" :can-undo="canUndoPendingChange" :can-redo="canRedoPendingChange" @undo="undoGridChange" @redo="redoGridChange" @close="closeSqlPreview" />