feat(grid): add data grid quick entry

This commit is contained in:
funnytime authored and GitHub committed 2026-06-30 09:14:44 +08:00
1 parent 074cef8358
commit 3e7cf30ce5
23 files changed
+1679 -186

No files matched your search

@@ -369,6 +369,7 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise<str
const customSaveHandler = computed<CustomSaveHandler>(() => ({
save: gridSave,
preview: previewDocumentChanges,
supportsInsert: true,
}));
function stopDocumentLoadingTimer() {
@@ -203,6 +203,7 @@ const debugLogDownloaded = ref(false);
const editShowColumnCommentsInHeader = ref(settingsStore.editorSettings.showColumnCommentsInHeader);
const editShowColumnTypesInHeader = ref(settingsStore.editorSettings.showColumnTypesInHeader);
const editCompactColumnHeaderActions = ref(settingsStore.editorSettings.compactColumnHeaderActions);
const editDataGridQuickEntry = ref(settingsStore.editorSettings.dataGridQuickEntry);
const editInfiniteScroll = ref(settingsStore.editorSettings.infiniteScroll);
const editInfiniteScrollMaxRows = ref(settingsStore.editorSettings.infiniteScrollMaxRows);
const editTableColumnTemplateRows = ref<TableColumnTemplateGridRow[]>(tableColumnTemplateRowsFromSettings(settingsStore.editorSettings.tableColumnTemplateFields));
@@ -477,6 +478,7 @@ watch(
editShowColumnCommentsInHeader.value = settingsStore.editorSettings.showColumnCommentsInHeader;
editShowColumnTypesInHeader.value = settingsStore.editorSettings.showColumnTypesInHeader;
editCompactColumnHeaderActions.value = settingsStore.editorSettings.compactColumnHeaderActions;
editDataGridQuickEntry.value = settingsStore.editorSettings.dataGridQuickEntry;
editInfiniteScroll.value = settingsStore.editorSettings.infiniteScroll;
editInfiniteScrollMaxRows.value = settingsStore.editorSettings.infiniteScrollMaxRows;
editTableColumnTemplateRows.value = tableColumnTemplateRowsFromSettings(settingsStore.editorSettings.tableColumnTemplateFields);
@@ -540,6 +542,7 @@ function hasChanges(): boolean {
editShowColumnCommentsInHeader.value !== settingsStore.editorSettings.showColumnCommentsInHeader ||
editShowColumnTypesInHeader.value !== settingsStore.editorSettings.showColumnTypesInHeader ||
editCompactColumnHeaderActions.value !== settingsStore.editorSettings.compactColumnHeaderActions ||
editDataGridQuickEntry.value !== settingsStore.editorSettings.dataGridQuickEntry ||
editInfiniteScroll.value !== settingsStore.editorSettings.infiniteScroll ||
editInfiniteScrollMaxRows.value !== settingsStore.editorSettings.infiniteScrollMaxRows ||
JSON.stringify(normalizedEditTableColumnTemplateFields.value) !== JSON.stringify(settingsStore.editorSettings.tableColumnTemplateFields) ||
@@ -584,6 +587,7 @@ async function persistSettings() {
showColumnCommentsInHeader: editShowColumnCommentsInHeader.value,
showColumnTypesInHeader: editShowColumnTypesInHeader.value,
compactColumnHeaderActions: editCompactColumnHeaderActions.value,
dataGridQuickEntry: editDataGridQuickEntry.value,
infiniteScroll: editInfiniteScroll.value,
infiniteScrollMaxRows: editInfiniteScrollMaxRows.value,
tableColumnTemplateFields: normalizedEditTableColumnTemplateFields.value,
@@ -670,6 +674,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader;
editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader;
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry;
editInfiniteScroll.value = DEFAULT_EDITOR_SETTINGS.infiniteScroll;
editInfiniteScrollMaxRows.value = DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows;
editTableColumnTemplateRows.value = tableColumnTemplateRowsFromSettings(DEFAULT_EDITOR_SETTINGS.tableColumnTemplateFields);
@@ -708,6 +713,7 @@ function resetAllDefaults() {
editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader;
editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader;
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry;
editInfiniteScroll.value = DEFAULT_EDITOR_SETTINGS.infiniteScroll;
editInfiniteScrollMaxRows.value = DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows;
editTableColumnTemplateRows.value = tableColumnTemplateRowsFromSettings(DEFAULT_EDITOR_SETTINGS.tableColumnTemplateFields);
@@ -2323,6 +2329,17 @@ watch(
</div>
<Switch id="compact-column-header-actions" v-model="editCompactColumnHeaderActions" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="data-grid-quick-entry">
{{ t("settings.dataGridQuickEntry") }}
</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.dataGridQuickEntryDescription") }}
</p>
</div>
<Switch id="data-grid-quick-entry" v-model="editDataGridQuickEntry" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="infinite-scroll">
+213 -60
View File
@@ -92,7 +92,7 @@ import type { BuildSingleColumnAlterSqlOptions } from "@/lib/tableStructureEdito
import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql";
import { uuid } from "@/lib/utils";
import { resolveHeaderColumnType } from "@/lib/dataGridColumnType";
import { canEditExistingTableRows, canUseKeylessRowPredicate, hiveTablePropertiesIndicateTransactional, isClickHouseExistingRowReadonlyColumn, isHiddenGridColumn, isTdengineExistingRowReadonlyColumn, usesSyntheticRowIdKey } from "@/lib/tableEditing";
import { canEditExistingTableRows, canInsertTableRows, canUseKeylessRowPredicate, hiveTablePropertiesIndicateTransactional, isClickHouseExistingRowReadonlyColumn, isHiddenGridColumn, isTdengineExistingRowReadonlyColumn, usesSyntheticRowIdKey } from "@/lib/tableEditing";
import { buildDataGridColumnDistinctValuesSql, buildDataGridContextFilterCondition, buildDataGridCountSql, buildHiveTablePropertiesSql, type DataGridContextFilterMode } from "@/lib/dataGridSql";
import {
buildVisibleTransposeRows,
@@ -106,7 +106,7 @@ import {
transposeScrollLeftForRecord,
visibleTransposeRecordWindow,
} from "@/lib/dataGridTranspose";
import { matchesRowStatusFilter, type RowStatus, type RowStatusFilter } from "@/lib/gridRowStatus";
import { canApplyGridSelectionValue, canDeleteGridRowItem, canEditGridCellDetail, matchesRowStatusFilter, shouldShowQuickEntryDraftRow, type RowStatus, type RowStatusFilter } from "@/lib/gridRowStatus";
import { displayCellValue, type CellValue } from "@/lib/cellValue";
import { getApplicablePreviewActions } from "@/lib/resultPreviewRegistry";
import "@/lib/previewHandlers/geometryMapPreview";
@@ -2710,7 +2710,13 @@ type DataGridTableMeta = NonNullable<typeof props.tableMeta>;
const hiveTableTransactional = ref<boolean | undefined>(undefined);
const canEditExistingRows = computed(() => !!props.customSaveHandler || canEditExistingTableRows(props.databaseType, hiveTableTransactional.value, props.tableMeta?.primaryKeys ?? []));
const customReadonlyColumns = computed(() => new Set((props.customSaveHandler?.readonlyColumns ?? []).map((column) => column.toLowerCase())));
const canInsertRows = computed(() => !props.customSaveHandler || props.customSaveHandler.canInsert !== false);
const hasDataGridSaveTarget = computed(() => !!props.tableMeta || !!props.customSaveHandler);
const hasDataGridInsertTarget = computed(() => {
const handler = props.customSaveHandler;
if (handler) return handler.supportsInsert === true || handler.canInsert === true;
return !!props.tableMeta && canInsertTableRows(props.databaseType);
});
const canInsertRows = computed(() => !!props.editable && hasDataGridInsertTarget.value);
const canDeleteRows = computed(() => !props.customSaveHandler || props.customSaveHandler.canDelete !== false);
watch(
() => [props.databaseType, props.connectionId, props.database, props.tableMeta?.schema, props.tableMeta?.tableName],
@@ -2936,6 +2942,7 @@ interface RowItem {
newIndex?: number;
data: CellValue[];
isNew: boolean;
isDraft?: boolean;
isDeleted: boolean;
isDirtyCol: boolean[];
status: RowStatus;
@@ -2957,6 +2964,14 @@ type DisplayRowRef =
isNew: true;
isDeleted: false;
status: RowStatus;
}
| {
id: number;
displayIndex: number;
isNew: false;
isDraft: true;
isDeleted: false;
status: RowStatus;
};
const editor = useDataGridEditor({
@@ -2976,6 +2991,7 @@ const editor = useDataGridEditor({
currentWhereInput: computed(() => currentWhereInput()),
orderByInput,
rowStatusFilter,
dataGridQuickEntryEnabled: computed(() => settingsStore.editorSettings.dataGridQuickEntry),
initialEditColumn: firstVisibleColumnIndex,
getRowItem,
pageSize,
@@ -2991,6 +3007,8 @@ const {
dirtyRows,
newRows,
deletedRows,
quickEntryDraftRow,
quickEntryDraftRowId,
pendingChangesVersion,
pendingChangeCount,
hasPendingChanges,
@@ -3001,6 +3019,7 @@ const {
exitTransaction,
startEdit,
commitEdit,
commitEditAndMaybeAutoSave,
commitEditFromBlur,
applyCellValue,
restoreCellValue,
@@ -3023,6 +3042,8 @@ const {
undoPendingChange,
redoPendingChange,
rowDataWithChanges,
ensureQuickEntryDraftRow,
isSavingNewRow,
canEditColumn,
resetGridVerticalScroll,
getResetScrollAfterResult,
@@ -3032,6 +3053,7 @@ const {
isPreviewLoading,
previewChanges,
} = editor;
const pendingQuickEntryDraftCellFocus = ref<{ rowId: number; col: number } | null>(null);
const showSqlPreview = ref(false);
const previewSqlText = ref("");
@@ -3090,34 +3112,45 @@ const saveActionMode = computed(() =>
const saveToolbarState = computed(() =>
dataGridSaveToolbarState({
editable: props.editable,
hasSaveTarget: !!props.tableMeta || !!props.customSaveHandler,
hasSaveTarget: hasDataGridSaveTarget.value,
hasPendingChanges: hasPendingChanges.value,
isSaving: isSaving.value,
}),
);
const hasSearchBarSlot = computed(() => !!slots["search-bar"]);
const quickEntryEnabled = computed(() => settingsStore.editorSettings.dataGridQuickEntry);
const showQuickEntryDraftRow = computed(() =>
shouldShowQuickEntryDraftRow({
editable: !!props.editable,
hasInsertTarget: hasDataGridInsertTarget.value,
quickEntryEnabled: quickEntryEnabled.value,
rowStatusFilter: rowStatusFilter.value,
hasPendingChanges: hasPendingChanges.value,
}),
);
const showDataGridTopbar = computed(
() =>
(useTransaction.value && !!props.editable && (!!props.tableMeta || !!props.customSaveHandler)) ||
(useTransaction.value && !!props.editable && hasDataGridSaveTarget.value) ||
hasLocalColumnFilters.value ||
canShowWhereSearch.value ||
hasSearchBarSlot.value ||
showQueryEditReadyBadge.value ||
props.context !== "results" ||
(!!props.editable && (!!props.tableMeta || !!props.customSaveHandler)) ||
(!!props.editable && hasDataGridSaveTarget.value) ||
transactionActive.value ||
saveToolbarState.value.showActions,
);
function canEditRowItem(item: RowItem | undefined): boolean {
return !!props.editable && !!item && !item.isDeleted && (item.isNew || canEditExistingRows.value);
return !!props.editable && !!item && !item.isDeleted && (item.isNew || item.isDraft || canEditExistingRows.value);
}
function canEditCellItem(item: RowItem | undefined, columnIndex: number): boolean {
if (!canEditRowItem(item) || !canEditColumn(columnIndex)) return false;
if (isSavingNewRow(item)) return false;
const column = props.result.columns[columnIndex] ?? "";
if (customReadonlyColumns.value.has(column.toLowerCase())) return false;
if (!item?.isNew) {
if (!item?.isNew && !item?.isDraft) {
const sourceColumn = props.sourceColumns?.[columnIndex] ?? column;
if (isClickHouseExistingRowReadonlyColumn(props.databaseType, sourceColumn, props.tableMeta?.primaryKeys ?? [], props.tableMeta?.columns ?? [])) return false;
if (isTdengineExistingRowReadonlyColumn(props.databaseType, column, props.tableMeta?.columns ?? [])) return false;
@@ -3217,7 +3250,15 @@ function isDecimalColumnType(dataType: string): boolean {
}
function canDeleteRowItem(item: RowItem | undefined): boolean {
return !!props.editable && canDeleteRows.value && !!item && !item.isDeleted && (item.isNew || canEditExistingRows.value);
if (!item) return false;
return canDeleteGridRowItem({
editable: !!props.editable && canDeleteRows.value,
isDraft: !!item.isDraft,
isDeleted: item.isDeleted,
isNew: item.isNew,
canEditExistingRows: canEditExistingRows.value,
isSavingNewRow: isSavingNewRow(item),
});
}
function resetInfiniteScrollState() {
@@ -3292,7 +3333,8 @@ const displayRowRefs = computed<DisplayRowRef[]>(() => {
const dirty = dirtyRows.value.get(sourceIndex);
const isDeleted = deletedRows.value.has(sourceIndex);
const status: RowStatus = isDeleted ? "deleted" : dirty?.size ? "edited" : "clean";
if (matchesRowStatusFilter(status, rowStatusFilter.value)) {
const isActiveEditingRow = quickEntryEnabled.value && editingCell.value?.rowId === sourceIndex;
if (matchesRowStatusFilter(status, rowStatusFilter.value) || isActiveEditingRow) {
refs.push({ id: sourceIndex, displayIndex: refs.length, sourceIndex, isNew: false, isDeleted, status });
}
}
@@ -3309,6 +3351,17 @@ const displayRowRefs = computed<DisplayRowRef[]>(() => {
status,
});
});
if (showQuickEntryDraftRow.value) {
ensureQuickEntryDraftRow();
refs.push({
id: quickEntryDraftRowId,
displayIndex: refs.length,
isNew: false,
isDraft: true,
isDeleted: false,
status: "draft",
});
}
return refs;
});
@@ -3322,6 +3375,14 @@ function rowItemFromDisplayRef(ref: DisplayRowRef): RowItem {
isDirtyCol: cleanDirtyColumns.value,
};
}
if (!("sourceIndex" in ref)) {
ensureQuickEntryDraftRow();
return {
...ref,
data: quickEntryDraftRow.value,
isDirtyCol: cleanDirtyColumns.value,
};
}
const row = props.result.rows[ref.sourceIndex] ?? [];
const dirty = dirtyRows.value.get(ref.sourceIndex);
return {
@@ -3606,40 +3667,57 @@ function cellDetailButtonVisible(rowIndex: number, actualColIdx: number) {
function affectedRowIds(): number[] {
if (hasRowSelection.value && selectedRowCount.value > 0) {
return [...selectedRowIds.value];
return [...selectedRowIds.value].filter((rowId) => !getRowItem(rowId)?.isDraft);
}
const range = selectedRange.value;
if (range && range.startRow !== range.endRow) {
return displayRowRefs.value.slice(range.startRow, range.endRow + 1).map((ref) => ref.id);
return displayRowRefs.value
.slice(range.startRow, range.endRow + 1)
.filter((ref) => !("isDraft" in ref && ref.isDraft))
.map((ref) => ref.id);
}
return [];
}
function deletableRowIds(rowIds: number[]): number[] {
return rowIds.filter((rowId) => canDeleteRowItem(getRowItem(rowId)));
}
function exportSelectedRowsCsv() {
return exportCsv(affectedRowIds());
const rowIds = affectedRowIds();
if (rowIds.length === 0) return;
return exportCsv(rowIds);
}
function exportSelectedRowsXlsx() {
return exportXlsx(affectedRowIds());
const rowIds = affectedRowIds();
if (rowIds.length === 0) return;
return exportXlsx(rowIds);
}
function exportSelectedRowsJson() {
return exportJson(affectedRowIds());
const rowIds = affectedRowIds();
if (rowIds.length === 0) return;
return exportJson(rowIds);
}
function exportSelectedRowsMarkdown() {
return exportMarkdown(affectedRowIds());
const rowIds = affectedRowIds();
if (rowIds.length === 0) return;
return exportMarkdown(rowIds);
}
function exportSelectedRowsSql() {
return exportSql(affectedRowIds());
const rowIds = affectedRowIds();
if (rowIds.length === 0) return;
return exportSql(rowIds);
}
function executePreviewAction(action: { execute: (ctx: any) => any }) {
const config = action.execute({
result: props.result,
selectedRowIds: affectedRowIds(),
displayRowRefs: displayRowRefs.value,
displayRowRefs: displayRowRefs.value.filter((ref) => "sourceIndex" in ref),
});
if (config) {
previewDialogConfig.value = config;
@@ -3685,7 +3763,7 @@ function cellDetailFor(rowIndex: number, columnIndex: number): DataGridCellDetai
resultColumnTypes: props.result.column_types,
commentByColumn: columnCommentMap.value,
displayValue: (value, index) => formatCellCached(value, index),
isEditable: canEditCellItem(item, columnIndex),
isEditable: canEditGridCellDetail({ canEditCell: canEditCellItem(item, columnIndex), isDraft: !!item.isDraft }),
});
}
@@ -3713,7 +3791,7 @@ const rowDetail = computed(() => {
resultColumnTypes: props.result.column_types,
commentByColumn: columnCommentMap.value,
displayValue: (value, index) => formatCellCached(value, index),
isEditableColumn: (columnIndex) => canEditCellItem(item, columnIndex),
isEditableColumn: (columnIndex) => canEditGridCellDetail({ canEditCell: canEditCellItem(item, columnIndex), isDraft: !!item.isDraft }),
});
});
@@ -3721,12 +3799,14 @@ const columnDetail = computed(() => {
if (columnDetailDialogColumnIndex.value === null) return null;
const columnIndex = columnDetailDialogColumnIndex.value;
return buildDataGridColumnDetail({
rows: displayItems.value.map((item) => ({
rowIndex: item.displayIndex,
rowId: item.id,
row: item.data,
isEditable: canEditCellItem(item, columnIndex),
})),
rows: displayItems.value
.filter((item) => !item.isDraft)
.map((item) => ({
rowIndex: item.displayIndex,
rowId: item.id,
row: item.data,
isEditable: canEditGridCellDetail({ canEditCell: canEditCellItem(item, columnIndex), isDraft: !!item.isDraft }),
})),
columns: props.result.columns,
columnIndex,
typeByColumn: columnTypeMap.value,
@@ -4386,6 +4466,16 @@ function formatCellCached(value: CellValue, columnIndex?: number): string {
return rememberPrimitiveCellFormat(key, formatCell(value, columnIndex));
}
function rowNumberText(item: RowItem | undefined): string {
if (!item) return "";
if (item.isDraft) return "*";
return String(infiniteScrollEnabled.value ? item.displayIndex + 1 : item.displayIndex + 1 + (currentPage.value - 1) * pageSize.value);
}
function draftCellPlaceholder(item: RowItem | undefined, columnIndex: number): string | null {
return item?.isDraft && item.data[columnIndex] === null ? t("grid.quickEntryDraftPlaceholder") : null;
}
watch(() => [props.result.columns.join("\u0000"), columnFormatterSignatures.value.join("\u0000")], clearCellFormatCache);
function quoteIdent(name: string): string {
@@ -4402,6 +4492,9 @@ function isNull(value: unknown): boolean {
}
function rowNumberStatusClass(item: RowItem): string {
if (item.status === "draft") {
return "border-muted-foreground/20 bg-muted/20 font-semibold text-muted-foreground";
}
if (item.status === "new") {
return "border-emerald-500/40 bg-emerald-500/15 font-semibold text-emerald-700 dark:text-emerald-300";
}
@@ -4692,10 +4785,15 @@ function clearCanvasDetailHover(event?: MouseEvent) {
function onCanvasMouseDown(event: MouseEvent) {
if (event.button !== 0) return;
commitHiddenCanvasEditBeforeCellInteraction();
const hit = canvasHitTest(event);
if (!hit) return;
if (!hit) {
commitHiddenCanvasEditBeforeCellInteraction();
return;
}
const item = displayItemAt(hit.rowIndex);
const actualColIdx = hit.rowNumber ? undefined : visibleColumnIndexes.value[hit.visibleColIdx];
if (item && actualColIdx !== undefined) prepareDataCellMouseDown(item, actualColIdx);
commitHiddenCanvasEditBeforeCellInteraction();
if (!item) return;
if (hit.rowNumber) {
handleRowClick(item.displayIndex, item.id, event);
@@ -4780,7 +4878,7 @@ function canvasEditingCellIsVisible() {
function commitHiddenCanvasEditBeforeCellInteraction() {
if (!editingCell.value || !useCanvasGridRows.value) return;
if (canvasEditingCellIsVisible()) return;
commitEdit();
void commitEditFromCellBlur();
}
const canvasEditingCell = computed(() => {
@@ -4881,6 +4979,7 @@ function drawCanvasGrid() {
searchMatchKeys: searchMatchSet.value,
currentSearchMatch: currentSearchMatch.value,
formatCell: formatCellCached,
draftCellPlaceholder: t("grid.quickEntryDraftPlaceholder"),
isRowActive,
rowCellsUseSelectionVisual,
cellIsSelected,
@@ -5299,6 +5398,7 @@ async function pasteClipboardIntoSelection() {
function pasteTextIntoSelection(text: string): boolean {
const rows = parseClipboardTable(text);
const allowDraftSelectionValue = selectedRangeTargetsOnlyDraftRow();
if (rows.length === 1 && rows[0]?.length === 1 && fillSelectionWithValue(rows[0][0])) {
toast(t("grid.pasted"));
@@ -5314,7 +5414,7 @@ function pasteTextIntoSelection(text: string): boolean {
row.forEach((value, colOffset) => {
const visibleCol = start.colIndex + colOffset;
if (visibleCol >= visibleColumns.value.length) return;
applied = applyVisibleCellValue(item, visibleCol, value) || applied;
applied = applyVisibleSelectedCellValue(item, visibleCol, value, allowDraftSelectionValue) || applied;
});
});
if (applied) toast(t("grid.pasted"));
@@ -5351,15 +5451,28 @@ function applyVisibleCellValue(item: RowItem, visibleCol: number, value: string
return true;
}
function applyVisibleSelectedCellValue(item: RowItem, visibleCol: number, value: string | null, allowDraft = selectedRangeTargetsOnlyDraftRow()): boolean {
if (!canApplyGridSelectionValue({ isDraft: !!item.isDraft, allowDraft })) return false;
return applyVisibleCellValue(item, visibleCol, value);
}
function selectedRangeTargetsOnlyDraftRow(): boolean {
const range = selectedRange.value;
if (!range) return false;
if (range.startRow !== range.endRow) return false;
return displayItemAt(range.startRow)?.isDraft === true;
}
function fillSelectionWithValue(value: string | null): boolean {
const range = selectedRange.value;
let applied = false;
const allowDraftSelectionValue = selectedRangeTargetsOnlyDraftRow();
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++) {
applied = applyVisibleCellValue(item, visibleCol, value) || applied;
applied = applyVisibleSelectedCellValue(item, visibleCol, value, allowDraftSelectionValue) || applied;
}
}
return applied;
@@ -5372,7 +5485,7 @@ function fillSelectionWithValue(value: string | null): boolean {
const item = displayItemAt(rowIndex);
if (!item) continue;
for (const visibleCol of visibleColumnIndexes) {
applied = applyVisibleCellValue(item, visibleCol, value) || applied;
applied = applyVisibleSelectedCellValue(item, visibleCol, value) || applied;
}
}
return applied;
@@ -5420,11 +5533,12 @@ function cutSelection() {
if (!props.editable || !selectedRange.value) return;
copySelectionTsv();
const range = selectedRange.value;
const allowDraftSelectionValue = selectedRangeTargetsOnlyDraftRow();
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++) {
applyCellValue(item.id, actualColumnIndex(visibleCol), null);
applyVisibleSelectedCellValue(item, visibleCol, null, allowDraftSelectionValue);
}
}
}
@@ -5566,7 +5680,8 @@ function selectedOrCurrentRowIds(): number[] {
}
function copyCurrentRow(): boolean {
const rowIds = selectedOrCurrentRowIds();
if (!canInsertRows.value) return false;
const rowIds = selectedOrCurrentRowIds().filter((rowId) => !getRowItem(rowId)?.isDraft);
if (rowIds.length === 0) return false;
if (rowIds.length === 1) {
cloneRow(rowIds[0]);
@@ -5580,19 +5695,46 @@ function deleteCurrentRow(): boolean {
const rowIds = selectedOrCurrentRowIds();
if (rowIds.length === 0) return false;
const deletableRowIds = rowIds.filter((rowId) => canDeleteRowItem(getRowItem(rowId)));
if (deletableRowIds.length === 0) return false;
if (deletableRowIds.length === 1) {
requestDeleteRow(deletableRowIds[0]);
const targetRowIds = deletableRowIds(rowIds);
if (targetRowIds.length === 0) return false;
if (targetRowIds.length === 1) {
requestDeleteRow(targetRowIds[0]);
return true;
}
requestDeleteRows(deletableRowIds);
requestDeleteRows(targetRowIds);
return true;
}
function commitGridEdit() {
commitEdit();
nextTick(() => gridRef.value?.focus({ preventScroll: true }));
void commitEditAndMaybeAutoSave().finally(() => nextTick(() => gridRef.value?.focus({ preventScroll: true })));
}
async function commitEditFromCellBlur() {
const target = pendingQuickEntryDraftCellFocus.value;
pendingQuickEntryDraftCellFocus.value = null;
if (target && editingCell.value?.rowId === quickEntryDraftRowId && target.rowId === quickEntryDraftRowId) {
await commitEditFromBlur({ promoteDraft: false });
nextTick(() => {
const item = getRowItem(target.rowId);
if (item && canEditCellItem(item, target.col)) startEdit(target.rowId, target.col);
});
return;
}
await commitEditFromBlur();
}
function prepareDataCellMouseDown(item: RowItem, actualColIdx: number) {
const editing = editingCell.value;
if (editing?.rowId === quickEntryDraftRowId && item.isDraft && item.id === quickEntryDraftRowId && editing.col !== actualColIdx) {
pendingQuickEntryDraftCellFocus.value = { rowId: item.id, col: actualColIdx };
} else {
pendingQuickEntryDraftCellFocus.value = null;
}
}
function prepareTransposeCellMouseDown(rowIndex: number, actualColIdx: number) {
const item = displayItemAt(rowIndex);
if (item) prepareDataCellMouseDown(item, actualColIdx);
}
async function saveGridChangesFromShortcut() {
@@ -5662,7 +5804,7 @@ async function onGridKeydown(event: KeyboardEvent) {
return;
}
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key.toLowerCase() === "n") {
if (props.editable && (props.tableMeta || props.customSaveHandler) && canInsertRows.value) {
if (props.editable && hasDataGridInsertTarget.value && canInsertRows.value) {
event.preventDefault();
event.stopPropagation();
addRow();
@@ -7130,7 +7272,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
if (props.editable && contextRowItem.value) {
const labels = rowActionLabels();
items.push({ label: "", separator: true });
if (canInsertRows.value) {
if (canInsertRows.value && !contextRowItem.value.isDraft) {
items.push({
label: labels.clone,
action: () => (isMultiRow.value ? cloneRows(affectedRowIds()) : cloneRow(contextRowItem.value!.id)),
@@ -7146,7 +7288,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
} else if (canDeleteRowItem(contextRowItem.value)) {
items.push({
label: labels.delete,
action: () => (isMultiRow.value ? requestDeleteRows(affectedRowIds()) : requestDeleteRow(contextRowItem.value!.id)),
action: () => (isMultiRow.value ? requestDeleteRows(deletableRowIds(affectedRowIds())) : requestDeleteRow(contextRowItem.value!.id)),
icon: Trash2,
variant: "destructive" as const,
});
@@ -7194,7 +7336,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
"
>
<div class="data-grid-topbar flex items-stretch relative" :class="{ 'data-grid-topbar--compact': compactDataGridToolbar }">
<div v-if="useTransaction && editable && (tableMeta || customSaveHandler)" class="flex items-center px-2 py-0.5 border-r shrink-0">
<div v-if="useTransaction && editable && hasDataGridSaveTarget" class="flex items-center px-2 py-0.5 border-r shrink-0">
<Select :model-value="rowStatusFilter" @update:model-value="(value: any) => setRowStatusFilter(String(value))">
<SelectTrigger class="h-5 max-w-28 border-0 bg-transparent px-0 py-0 text-xs font-medium text-foreground/70 shadow-none focus-visible:ring-0 data-[state=open]:text-foreground [&_svg]:size-3">
<SelectValue :placeholder="t('grid.filterRows')" />
@@ -7219,7 +7361,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</template>
<template v-if="canShowWhereSearch">
<div ref="searchSplitContainerRef" class="flex flex-1 min-w-0">
<div class="flex flex-1 items-center gap-1 px-2 py-0.5 min-w-0 relative" :class="{ 'border-l': useTransaction && editable && (tableMeta || customSaveHandler) }" :style="whereSearchPaneStyle">
<div class="flex flex-1 items-center gap-1 px-2 py-0.5 min-w-0 relative" :class="{ 'border-l': useTransaction && editable && hasDataGridSaveTarget }" :style="whereSearchPaneStyle">
<Popover v-model:open="filterBuilderOpen">
<PopoverTrigger as-child>
<button
@@ -7542,7 +7684,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</TooltipTrigger>
<TooltipContent side="bottom">{{ t("grid.goToColumn") }}</TooltipContent>
</Tooltip>
<Tooltip v-if="editable && (tableMeta || customSaveHandler) && canInsertRows">
<Tooltip v-if="editable && hasDataGridInsertTarget && canInsertRows">
<TooltipTrigger as-child>
<Button variant="ghost" size="sm" :class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5', compactDataGridToolbar ? 'data-grid-topbar-action-button--compact' : '']" @click="addRow">
<Plus class="data-grid-topbar-action-icon w-3 h-3" />
@@ -7719,7 +7861,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
@click="selectTransposeRecord(recordIndex, $event)"
@contextmenu="selectTransposeRecord(recordIndex, $event)"
>
{{ recordIndex + 1 }}
{{ rowNumberText(displayItems[recordIndex]) }}
<div class="absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-primary/30" @mousedown.stop="onTransposeRecordResizeStart(recordIndex, $event)" @dblclick.stop="autoFitTransposeRecord(recordIndex)" />
</div>
<div class="shrink-0" :style="{ width: `${transposeAfterSpacerWidth}px` }" />
@@ -7749,6 +7891,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
}"
:style="{ width: `${getTransposeRecordWidth(cell.recordIndex)}px` }"
:title="cell.display"
@mousedown="prepareTransposeCellMouseDown(cell.recordIndex, cell.valueIndex)"
@click="selectTransposeCell(cell.recordIndex, cell.valueIndex, $event)"
@mouseenter="onTransposeCellMouseenter(cell.recordIndex, cell.valueIndex)"
@mouseleave="onCellMouseleave(cell.recordIndex, cell.valueIndex)"
@@ -7768,7 +7911,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
autocorrect="off"
spellcheck="false"
class="cell-edit-input cell-edit-input--expanded absolute left-0 top-0 min-h-full bg-background px-1.5 py-1 leading-[18px] outline-none z-10"
@blur="commitEditFromBlur"
@blur="commitEditFromCellBlur"
@click.stop
@focus="onCellEditTextareaInput"
@input="onCellEditTextareaInput"
@@ -7783,7 +7926,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
autocorrect="off"
spellcheck="false"
class="cell-edit-input absolute inset-0 bg-background border-2 border-primary px-1.5 py-0 leading-[26px] outline-none z-10"
@blur="commitEditFromBlur"
@blur="commitEditFromCellBlur"
@click.stop
@input="onCellEditTextareaInput"
@keydown.stop="onCellEditKeydown"
@@ -7791,7 +7934,10 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
/>
</template>
<template v-else>
{{ cell.display }}
<template v-if="draftCellPlaceholder(displayItems[cell.recordIndex], cell.valueIndex)">
<span class="text-muted-foreground/70 italic">{{ draftCellPlaceholder(displayItems[cell.recordIndex], cell.valueIndex) }}</span>
</template>
<template v-else>{{ cell.display }}</template>
<div v-if="cellDetailButtonVisible(cell.recordIndex, cell.valueIndex)" class="absolute right-2 top-0.5 flex items-center gap-1">
<LightDropdownMenu
v-if="canQuickDownloadCellValue(cell.recordIndex, cell.valueIndex)"
@@ -8226,7 +8372,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
autocorrect="off"
spellcheck="false"
class="cell-edit-input cell-edit-input--expanded absolute left-0 top-0 min-h-full bg-background px-2.5 py-1 leading-[18px] outline-none z-10"
@blur="commitEditFromBlur"
@blur="commitEditFromCellBlur"
@click.stop
@focus="onCellEditTextareaInput"
@input="onCellEditTextareaInput"
@@ -8241,7 +8387,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
autocorrect="off"
spellcheck="false"
class="cell-edit-input absolute inset-0 bg-background border-2 border-primary px-2.5 py-0 leading-[22px] outline-none z-10"
@blur="commitEditFromBlur"
@blur="commitEditFromCellBlur"
@click.stop
@input="onCellEditTextareaInput"
@keydown.stop="onCellEditKeydown"
@@ -8302,7 +8448,8 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
:class="{
'bg-destructive/5 opacity-70': item.isDeleted,
'bg-primary/5': item.isNew && !isRowActive(item.displayIndex),
'bg-muted/30': !item.isNew && !item.isDeleted && !isRowActive(item.displayIndex) && item.displayIndex % 2 === 1,
'bg-muted/20': item.isDraft && !isRowActive(item.displayIndex),
'bg-muted/30': !item.isNew && !item.isDraft && !item.isDeleted && !isRowActive(item.displayIndex) && item.displayIndex % 2 === 1,
'active-row': isRowActive(item.displayIndex) && !item.isDeleted,
'relative z-20 overflow-visible': editingCell?.rowId === item.id,
}"
@@ -8315,7 +8462,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
@dblclick.stop="toggleTranspose(item.displayIndex)"
@contextmenu="onRowContext(item.id, item.displayIndex)"
>
{{ infiniteScrollEnabled ? item.displayIndex + 1 : item.displayIndex + 1 + (currentPage - 1) * pageSize }}
{{ rowNumberText(item) }}
</div>
<div class="shrink-0" :style="{ width: `${horizontalColumnWindow.beforeWidth}px` }" />
<div
@@ -8338,7 +8485,10 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
'overflow-visible z-20 border-r-transparent': editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx,
'overflow-hidden': !(editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx),
}"
@mousedown="handleDataCellMousedown(item.displayIndex, col.visibleColIdx, item.id, $event)"
@mousedown="
prepareDataCellMouseDown(item, col.actualColIdx);
handleDataCellMousedown(item.displayIndex, col.visibleColIdx, item.id, $event);
"
@mouseenter="onCellMouseenter(item.displayIndex, col.visibleColIdx, col.actualColIdx)"
@mouseleave="onCellMouseleave(item.displayIndex, col.actualColIdx)"
@dblclick="canEditCellItem(item, col.actualColIdx) && startDomCellEdit(item.id, col.actualColIdx, formatCellCached(item.data[col.actualColIdx], col.actualColIdx), $event)"
@@ -8358,7 +8508,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
autocorrect="off"
spellcheck="false"
class="cell-edit-input cell-edit-input--expanded absolute left-0 top-0 min-h-full bg-background px-2.5 py-1 leading-[18px] outline-none z-10"
@blur="commitEditFromBlur"
@blur="commitEditFromCellBlur"
@click.stop
@focus="onCellEditTextareaInput"
@input="onCellEditTextareaInput"
@@ -8373,7 +8523,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
autocorrect="off"
spellcheck="false"
class="cell-edit-input absolute inset-0 bg-background border-2 border-primary px-2.5 py-0 leading-[22px] outline-none z-10"
@blur="commitEditFromBlur"
@blur="commitEditFromCellBlur"
@click.stop
@input="onCellEditTextareaInput"
@keydown.stop="onCellEditKeydown"
@@ -8381,7 +8531,10 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
/>
</template>
<template v-else>
{{ formatCellCached(item.data[col.actualColIdx], col.actualColIdx) }}
<template v-if="draftCellPlaceholder(item, col.actualColIdx)">
<span class="text-muted-foreground/70 italic">{{ draftCellPlaceholder(item, col.actualColIdx) }}</span>
</template>
<template v-else>{{ formatCellCached(item.data[col.actualColIdx], col.actualColIdx) }}</template>
<div v-if="cellDetailButtonVisible(item.displayIndex, col.actualColIdx)" class="absolute right-2 top-0.5 flex items-center gap-1">
<LightDropdownMenu
v-if="canQuickDownloadCellValue(item.displayIndex, col.actualColIdx)"
@@ -8871,7 +9024,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<span v-if="!hasData" class="shrink-0">{{ t("grid.rowsAffected", { count: result.affected_rows }) }}</span>
<span class="shrink-0">{{ result.execution_time_ms }}ms</span>
<template v-if="editable && (tableMeta || customSaveHandler)">
<template v-if="editable && hasDataGridSaveTarget">
<span v-if="hasPendingChanges" class="shrink-0 text-foreground">
{{ t("grid.pendingChanges", { count: pendingChangeCount }) }}
</span>
@@ -348,7 +348,7 @@ const mongoQueryResultSaveHandler = computed<CustomSaveHandler | undefined>(() =
return stmts;
};
return { save, preview, canInsert: false, canDelete: false, readonlyColumns: [target.idColumn], targetLabel: target.collection };
return { save, preview, canInsert: false, canDelete: false, supportsInsert: false, readonlyColumns: [target.idColumn], targetLabel: target.collection };
});
const resultsPaneOpen = ref(false);
const resultsPaneSize = ref(Number(safeLocalStorageGet("dbx-results-pane-size")) || DEFAULT_QUERY_RESULTS_PANE_SIZE);