feat(grid): add data grid quick entry
This commit is contained in:
1 parent
074cef8358
commit
3e7cf30ce5
23 files changed
+1679
-186
No files matched your search
@@ -17,11 +17,30 @@ interface RowItem {
|
||||
newIndex?: number;
|
||||
data: CellValue[];
|
||||
isNew: boolean;
|
||||
isDraft?: boolean;
|
||||
isDeleted: boolean;
|
||||
isDirtyCol: boolean[];
|
||||
status: string;
|
||||
}
|
||||
|
||||
export const DATA_GRID_QUICK_ENTRY_DRAFT_ROW_ID = Number.MIN_SAFE_INTEGER;
|
||||
|
||||
type RowKind = "none" | "existing" | "new" | "draft";
|
||||
|
||||
type CommitEditResult =
|
||||
| {
|
||||
changed: false;
|
||||
rowKind: RowKind;
|
||||
}
|
||||
| {
|
||||
changed: true;
|
||||
rowKind: Exclude<RowKind, "none">;
|
||||
};
|
||||
|
||||
interface CommitEditOptions {
|
||||
promoteDraft?: boolean;
|
||||
}
|
||||
|
||||
type GridScrollerRef =
|
||||
| HTMLElement
|
||||
| {
|
||||
@@ -37,6 +56,7 @@ export interface CustomSaveHandler {
|
||||
canInsert?: boolean;
|
||||
canDelete?: boolean;
|
||||
readonlyColumns?: string[];
|
||||
supportsInsert?: boolean;
|
||||
targetLabel?: string;
|
||||
}
|
||||
|
||||
@@ -65,6 +85,7 @@ export interface UseDataGridEditorOptions {
|
||||
currentWhereInput: ComputedRef<string | undefined>;
|
||||
orderByInput: Ref<string>;
|
||||
rowStatusFilter: Ref<RowStatusFilter>;
|
||||
dataGridQuickEntryEnabled?: ComputedRef<boolean>;
|
||||
initialEditColumn?: ComputedRef<number>;
|
||||
getRowItem: (rowId: number) => RowItem | undefined;
|
||||
pageSize: Ref<number>;
|
||||
@@ -77,6 +98,7 @@ export interface UseDataGridEditorOptions {
|
||||
|
||||
interface PendingChangesSnapshot {
|
||||
newRows: CellValue[][];
|
||||
quickEntryDraftRow?: CellValue[];
|
||||
dirtyRows: Map<number, Map<number, CellValue>>;
|
||||
deletedRows: Set<number>;
|
||||
editingCell?: { rowId: number; col: number } | null;
|
||||
@@ -87,7 +109,24 @@ interface PendingChangesSnapshot {
|
||||
rowCount: number;
|
||||
}
|
||||
|
||||
type PendingChangesHistorySnapshot = Pick<PendingChangesSnapshot, "newRows" | "dirtyRows" | "deletedRows" | "transactionActive">;
|
||||
interface PendingSaveSnapshot {
|
||||
newRows: CellValue[][];
|
||||
newRowRefs: CellValue[][];
|
||||
dirtyRows: Map<number, Map<number, CellValue>>;
|
||||
deletedRows: Set<number>;
|
||||
}
|
||||
|
||||
interface SaveChangesOptions {
|
||||
autoSave?: boolean;
|
||||
}
|
||||
|
||||
interface QueuedAutoSaveChange {
|
||||
sourceIndex: number;
|
||||
col: number;
|
||||
value: CellValue;
|
||||
}
|
||||
|
||||
type PendingChangesHistorySnapshot = Pick<PendingChangesSnapshot, "newRows" | "quickEntryDraftRow" | "dirtyRows" | "deletedRows" | "transactionActive">;
|
||||
|
||||
const pendingChangesCache = new Map<string, PendingChangesSnapshot>();
|
||||
const closingPendingSnapshotTabs = new Set<string>();
|
||||
@@ -143,6 +182,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
searchText,
|
||||
orderByInput,
|
||||
rowStatusFilter,
|
||||
dataGridQuickEntryEnabled = computed(() => false),
|
||||
initialEditColumn,
|
||||
getRowItem,
|
||||
pageSize,
|
||||
@@ -156,12 +196,17 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
const dirtyRows = ref<Map<number, Map<number, CellValue>>>(new Map());
|
||||
const newRows = ref<CellValue[][]>([]);
|
||||
const deletedRows = ref<Set<number>>(new Set());
|
||||
const quickEntryDraftRow = ref<CellValue[]>([]);
|
||||
const undoStack = ref<PendingChangesHistorySnapshot[]>([]);
|
||||
const redoStack = ref<PendingChangesHistorySnapshot[]>([]);
|
||||
const pendingChangesVersion = ref(0);
|
||||
let restoredEditingCell = false;
|
||||
let restoredTransactionActive = false;
|
||||
let suppressNextBlurCommit = false;
|
||||
let pendingAutoSaveRequested = false;
|
||||
const queuedAutoSaveChanges = new Map<string, QueuedAutoSaveChange>();
|
||||
let draftPromotionScheduled = false;
|
||||
const savingNewRows = new WeakSet<CellValue[]>();
|
||||
let pendingScrollRestore: PendingChangesSnapshot["scroll"] | undefined;
|
||||
let saveScrollSnapshotTimer = 0;
|
||||
let componentActive = true;
|
||||
@@ -172,6 +217,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
const cached = pendingChangesCache.get(key);
|
||||
if (cached && cached.columnCount === result.value.columns.length && cached.rowCount === result.value.rows.length) {
|
||||
newRows.value = cached.newRows;
|
||||
quickEntryDraftRow.value = cached.quickEntryDraftRow ? [...cached.quickEntryDraftRow] : [];
|
||||
dirtyRows.value = cached.dirtyRows;
|
||||
deletedRows.value = cached.deletedRows;
|
||||
editingCell.value = cached.editingCell ?? null;
|
||||
@@ -243,6 +289,10 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
transactionActive.value = true;
|
||||
}
|
||||
|
||||
function exitTransaction() {
|
||||
transactionActive.value = false;
|
||||
}
|
||||
|
||||
function touchPendingChanges() {
|
||||
pendingChangesVersion.value++;
|
||||
}
|
||||
@@ -250,6 +300,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
function pendingChangesSnapshot(): PendingChangesHistorySnapshot {
|
||||
return {
|
||||
newRows: newRows.value.map((row) => [...row]),
|
||||
quickEntryDraftRow: quickEntryDraftRow.value.length > 0 ? [...quickEntryDraftRow.value] : undefined,
|
||||
dirtyRows: new Map([...dirtyRows.value].map(([rowIndex, changes]) => [rowIndex, new Map(changes)])),
|
||||
deletedRows: new Set(deletedRows.value),
|
||||
transactionActive: transactionActive.value,
|
||||
@@ -258,9 +309,11 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
|
||||
function restorePendingChangesSnapshot(snapshot: PendingChangesHistorySnapshot) {
|
||||
newRows.value = snapshot.newRows.map((row) => [...row]);
|
||||
quickEntryDraftRow.value = snapshot.quickEntryDraftRow ? [...snapshot.quickEntryDraftRow] : emptyDraftRow();
|
||||
dirtyRows.value = new Map([...snapshot.dirtyRows].map(([rowIndex, changes]) => [rowIndex, new Map(changes)]));
|
||||
deletedRows.value = new Set(snapshot.deletedRows);
|
||||
transactionActive.value = snapshot.transactionActive === true && useTransaction.value === true;
|
||||
queuedAutoSaveChanges.clear();
|
||||
editingCell.value = null;
|
||||
touchPendingChanges();
|
||||
}
|
||||
@@ -291,10 +344,6 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
restorePendingChangesSnapshot(snapshot);
|
||||
}
|
||||
|
||||
function exitTransaction() {
|
||||
transactionActive.value = false;
|
||||
}
|
||||
|
||||
// --- Scroll helpers ---
|
||||
let isCancelling = false;
|
||||
let isCommitting = false;
|
||||
@@ -451,13 +500,103 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
return row.map((v, colIdx) => (dirty.has(colIdx) ? dirty.get(colIdx)! : v));
|
||||
}
|
||||
|
||||
function editingSourceRowItem(rowId: number): RowItem | undefined {
|
||||
if (!dataGridQuickEntryEnabled.value || rowId < 0) return undefined;
|
||||
const row = result.value.rows[rowId];
|
||||
if (!row || deletedRows.value.has(rowId)) return undefined;
|
||||
const dirty = dirtyRows.value.get(rowId);
|
||||
return {
|
||||
id: rowId,
|
||||
sourceIndex: rowId,
|
||||
data: rowDataWithChanges(row, rowId),
|
||||
isNew: false,
|
||||
isDeleted: false,
|
||||
isDirtyCol: result.value.columns.map((_, colIdx) => !!dirty?.has(colIdx)),
|
||||
status: dirty?.size ? "edited" : "clean",
|
||||
};
|
||||
}
|
||||
|
||||
function emptyDraftRow(): CellValue[] {
|
||||
return result.value.columns.map(() => null);
|
||||
}
|
||||
|
||||
function ensureQuickEntryDraftRow() {
|
||||
if (quickEntryDraftRow.value.length !== result.value.columns.length) {
|
||||
quickEntryDraftRow.value = emptyDraftRow();
|
||||
}
|
||||
}
|
||||
|
||||
function draftRowHasValue(row = quickEntryDraftRow.value): boolean {
|
||||
return row.some((value) => value !== null && String(value).trim() !== "");
|
||||
}
|
||||
|
||||
function isSavingNewRow(item: Pick<RowItem, "isNew" | "data"> | undefined): boolean {
|
||||
return !!item?.isNew && savingNewRows.has(item.data);
|
||||
}
|
||||
|
||||
function queuedAutoSaveKey(sourceIndex: number, col: number): string {
|
||||
return `${sourceIndex}:${col}`;
|
||||
}
|
||||
|
||||
function rememberQueuedAutoSaveChange(sourceIndex: number, col: number, value: CellValue) {
|
||||
queuedAutoSaveChanges.set(queuedAutoSaveKey(sourceIndex, col), { sourceIndex, col, value });
|
||||
}
|
||||
|
||||
function applyQueuedAutoSaveChanges(savedSnapshot?: PendingSaveSnapshot) {
|
||||
if (queuedAutoSaveChanges.size === 0) return false;
|
||||
let applied = false;
|
||||
for (const change of queuedAutoSaveChanges.values()) {
|
||||
if (deletedRows.value.has(change.sourceIndex) || !canEditExistingRows.value) continue;
|
||||
const oldVal = result.value.rows[change.sourceIndex]?.[change.col];
|
||||
const savedChanges = savedSnapshot?.dirtyRows.get(change.sourceIndex);
|
||||
const baseline = savedChanges?.has(change.col) ? savedChanges.get(change.col) : oldVal;
|
||||
if (change.value !== baseline) {
|
||||
if (!dirtyRows.value.has(change.sourceIndex)) dirtyRows.value.set(change.sourceIndex, new Map());
|
||||
dirtyRows.value.get(change.sourceIndex)!.set(change.col, change.value);
|
||||
applied = true;
|
||||
} else {
|
||||
const rowChanges = dirtyRows.value.get(change.sourceIndex);
|
||||
rowChanges?.delete(change.col);
|
||||
if (rowChanges?.size === 0) dirtyRows.value.delete(change.sourceIndex);
|
||||
}
|
||||
}
|
||||
queuedAutoSaveChanges.clear();
|
||||
dirtyRows.value = new Map(dirtyRows.value);
|
||||
return applied;
|
||||
}
|
||||
|
||||
async function promoteQuickEntryDraftRow() {
|
||||
draftPromotionScheduled = false;
|
||||
ensureQuickEntryDraftRow();
|
||||
if (!draftRowHasValue()) {
|
||||
quickEntryDraftRow.value = emptyDraftRow();
|
||||
return;
|
||||
}
|
||||
rowStatusFilter.value = rowStatusFilterAfterAddingRow(rowStatusFilter.value);
|
||||
newRows.value = [...newRows.value, [...quickEntryDraftRow.value]];
|
||||
quickEntryDraftRow.value = emptyDraftRow();
|
||||
if (useTransaction.value && !transactionActive.value) {
|
||||
enterTransaction();
|
||||
}
|
||||
if (dataGridQuickEntryEnabled.value) {
|
||||
await saveChanges({ autoSave: true });
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleQuickEntryDraftPromotion() {
|
||||
if (draftPromotionScheduled) return;
|
||||
draftPromotionScheduled = true;
|
||||
void Promise.resolve().then(promoteQuickEntryDraftRow);
|
||||
}
|
||||
|
||||
// --- Inline editing ---
|
||||
function startEdit(rowId: number, colIdx: number) {
|
||||
if (!editable.value) return;
|
||||
if (!canEditColumn(colIdx)) return;
|
||||
const item = getRowItem(rowId);
|
||||
if (!item || item.isDeleted) return;
|
||||
if (!item.isNew && !canEditExistingRows.value) return;
|
||||
if (!item.isNew && !item.isDraft && !canEditExistingRows.value) return;
|
||||
if (isSavingNewRow(item)) return;
|
||||
isCancelling = false;
|
||||
suppressNextBlurCommit = false;
|
||||
editingCell.value = { rowId, col: colIdx };
|
||||
@@ -470,47 +609,76 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
focusEditInput();
|
||||
}
|
||||
|
||||
function commitEdit() {
|
||||
if (isCancelling || isCommitting) return;
|
||||
if (!editingCell.value) return;
|
||||
function commitEdit(options: CommitEditOptions = {}): CommitEditResult {
|
||||
if (isCancelling || isCommitting) return { changed: false, rowKind: "none" };
|
||||
if (!editingCell.value) return { changed: false, rowKind: "none" };
|
||||
isCommitting = true;
|
||||
const { rowId, col } = editingCell.value;
|
||||
const item = getRowItem(rowId);
|
||||
const item = getRowItem(rowId) ?? editingSourceRowItem(rowId);
|
||||
if (!item || item.isDeleted) {
|
||||
editingCell.value = null;
|
||||
isCommitting = false;
|
||||
return;
|
||||
return { changed: false, rowKind: "none" };
|
||||
}
|
||||
|
||||
if (item.isDraft) {
|
||||
ensureQuickEntryDraftRow();
|
||||
const oldVal = quickEntryDraftRow.value[col] ?? null;
|
||||
const newVal = coerceCellValue(editValue.value, oldVal, col);
|
||||
const nextDraftRow = [...quickEntryDraftRow.value];
|
||||
nextDraftRow[col] = newVal;
|
||||
if (newVal !== oldVal) pushUndoSnapshot();
|
||||
quickEntryDraftRow.value = nextDraftRow;
|
||||
editingCell.value = null;
|
||||
isCommitting = false;
|
||||
if (!draftRowHasValue(nextDraftRow)) {
|
||||
quickEntryDraftRow.value = emptyDraftRow();
|
||||
return { changed: false, rowKind: "draft" };
|
||||
}
|
||||
if (options.promoteDraft === false) {
|
||||
return { changed: false, rowKind: "draft" };
|
||||
}
|
||||
rowStatusFilter.value = rowStatusFilterAfterAddingRow(rowStatusFilter.value);
|
||||
newRows.value = [...newRows.value, nextDraftRow];
|
||||
quickEntryDraftRow.value = emptyDraftRow();
|
||||
touchPendingChanges();
|
||||
if (useTransaction.value && !transactionActive.value) {
|
||||
enterTransaction();
|
||||
}
|
||||
return { changed: true, rowKind: "draft" };
|
||||
}
|
||||
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
const oldVal = newRows.value[item.newIndex]?.[col];
|
||||
const newVal = coerceCellValue(editValue.value, oldVal, col);
|
||||
const changed = newVal !== oldVal;
|
||||
if (changed) pushUndoSnapshot();
|
||||
if (newRows.value[item.newIndex]) {
|
||||
if (newVal !== oldVal) pushUndoSnapshot();
|
||||
newRows.value[item.newIndex][col] = newVal;
|
||||
}
|
||||
newRows.value = [...newRows.value];
|
||||
touchPendingChanges();
|
||||
if (changed) touchPendingChanges();
|
||||
editingCell.value = null;
|
||||
isCommitting = false;
|
||||
return;
|
||||
return changed ? { changed: true, rowKind: "new" } : { changed: false, rowKind: "new" };
|
||||
}
|
||||
|
||||
if (item.sourceIndex === undefined) {
|
||||
editingCell.value = null;
|
||||
isCommitting = false;
|
||||
return;
|
||||
return { changed: false, rowKind: "none" };
|
||||
}
|
||||
if (!canEditExistingRows.value) {
|
||||
editingCell.value = null;
|
||||
isCommitting = false;
|
||||
return;
|
||||
return { changed: false, rowKind: "existing" };
|
||||
}
|
||||
|
||||
const oldVal = result.value.rows[item.sourceIndex]?.[col];
|
||||
const newVal = coerceCellValue(editValue.value, oldVal, col);
|
||||
const changed = newVal !== item.data[col];
|
||||
if (newVal !== oldVal) {
|
||||
pushUndoSnapshot();
|
||||
if (changed) pushUndoSnapshot();
|
||||
if (!dirtyRows.value.has(item.sourceIndex)) dirtyRows.value.set(item.sourceIndex, new Map());
|
||||
dirtyRows.value.get(item.sourceIndex)!.set(col, newVal);
|
||||
if (useTransaction.value && !transactionActive.value) {
|
||||
@@ -523,17 +691,28 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
if (rowChanges?.size === 0) dirtyRows.value.delete(item.sourceIndex);
|
||||
}
|
||||
dirtyRows.value = new Map(dirtyRows.value);
|
||||
touchPendingChanges();
|
||||
if (changed) touchPendingChanges();
|
||||
editingCell.value = null;
|
||||
isCommitting = false;
|
||||
if (dataGridQuickEntryEnabled.value && isSaving.value && changed) {
|
||||
rememberQueuedAutoSaveChange(item.sourceIndex, col, newVal);
|
||||
}
|
||||
return changed ? { changed: true, rowKind: "existing" } : { changed: false, rowKind: "existing" };
|
||||
}
|
||||
|
||||
function commitEditFromBlur() {
|
||||
async function commitEditAndMaybeAutoSave(options: CommitEditOptions = {}) {
|
||||
const result = commitEdit(options);
|
||||
if (dataGridQuickEntryEnabled.value && options.promoteDraft !== false && result.changed) {
|
||||
await saveChanges({ autoSave: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function commitEditFromBlur(options: CommitEditOptions = {}) {
|
||||
if (suppressNextBlurCommit) {
|
||||
suppressNextBlurCommit = false;
|
||||
return;
|
||||
}
|
||||
commitEdit();
|
||||
await commitEditAndMaybeAutoSave(options);
|
||||
}
|
||||
|
||||
function applyCellValue(rowId: number, col: number, value: string | null) {
|
||||
@@ -541,7 +720,21 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
const item = getRowItem(rowId);
|
||||
if (!item || item.isDeleted) return;
|
||||
|
||||
if (item.isDraft) {
|
||||
ensureQuickEntryDraftRow();
|
||||
const oldVal = quickEntryDraftRow.value[col] ?? null;
|
||||
const nextDraftRow = [...quickEntryDraftRow.value];
|
||||
nextDraftRow[col] = value === null ? null : coerceCellValue(value, oldVal, col);
|
||||
if (nextDraftRow[col] === oldVal) return;
|
||||
pushUndoSnapshot();
|
||||
quickEntryDraftRow.value = draftRowHasValue(nextDraftRow) ? nextDraftRow : emptyDraftRow();
|
||||
touchPendingChanges();
|
||||
scheduleQuickEntryDraftPromotion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
if (isSavingNewRow(item)) return;
|
||||
const row = newRows.value[item.newIndex];
|
||||
if (!row) return;
|
||||
const oldVal = row[col];
|
||||
@@ -584,7 +777,19 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
const item = getRowItem(rowId);
|
||||
if (!item || item.isDeleted) return;
|
||||
|
||||
if (item.isDraft) {
|
||||
ensureQuickEntryDraftRow();
|
||||
if (quickEntryDraftRow.value[col] === null) return;
|
||||
pushUndoSnapshot();
|
||||
const nextDraftRow = [...quickEntryDraftRow.value];
|
||||
nextDraftRow[col] = null;
|
||||
quickEntryDraftRow.value = draftRowHasValue(nextDraftRow) ? nextDraftRow : emptyDraftRow();
|
||||
touchPendingChanges();
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
if (isSavingNewRow(item)) return;
|
||||
const row = newRows.value[item.newIndex];
|
||||
if (!row || row[col] === null) return;
|
||||
pushUndoSnapshot();
|
||||
@@ -614,11 +819,10 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
}
|
||||
|
||||
function onEditKeydown(e: KeyboardEvent) {
|
||||
const isExpandedTextarea = e.target instanceof HTMLTextAreaElement && e.target.dataset.expandedCellEditor === "true";
|
||||
const isExpandedTextarea = typeof HTMLTextAreaElement !== "undefined" && e.target instanceof HTMLTextAreaElement && e.target.dataset.expandedCellEditor === "true";
|
||||
if (e.key === "Enter" && (!isExpandedTextarea || e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
commitEdit();
|
||||
nextTick(focusScrollerWithoutScrolling);
|
||||
void commitEditAndMaybeAutoSave().finally(() => nextTick(focusScrollerWithoutScrolling));
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -700,6 +904,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
const item = getRowItem(rowId);
|
||||
if (!item) return;
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
if (isSavingNewRow(item)) return;
|
||||
pushUndoSnapshot();
|
||||
newRows.value.splice(item.newIndex, 1);
|
||||
newRows.value = [...newRows.value];
|
||||
@@ -772,7 +977,81 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
}
|
||||
|
||||
// --- Save/Discard ---
|
||||
function saveStatementOptions() {
|
||||
function snapshotPendingSaveChanges(): PendingSaveSnapshot {
|
||||
const currentNewRows = [...newRows.value];
|
||||
return {
|
||||
dirtyRows: new Map([...dirtyRows.value.entries()].map(([rowIndex, changes]) => [rowIndex, new Map(changes)])),
|
||||
newRows: currentNewRows.map((row) => [...row]),
|
||||
newRowRefs: currentNewRows,
|
||||
deletedRows: new Set(deletedRows.value),
|
||||
};
|
||||
}
|
||||
|
||||
function hasPendingSaveChanges(snapshot: PendingSaveSnapshot) {
|
||||
return snapshot.newRows.length > 0 || snapshot.dirtyRows.size > 0 || snapshot.deletedRows.size > 0;
|
||||
}
|
||||
|
||||
function applyDirtyRowsToResult(snapshot: PendingSaveSnapshot) {
|
||||
for (const [sourceIndex, changes] of snapshot.dirtyRows) {
|
||||
const row = result.value.rows[sourceIndex];
|
||||
if (row) {
|
||||
for (const [colIdx, value] of changes) {
|
||||
row[colIdx] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearSavedPendingChanges(snapshot: PendingSaveSnapshot) {
|
||||
for (const [sourceIndex, changes] of snapshot.dirtyRows) {
|
||||
const liveChanges = dirtyRows.value.get(sourceIndex);
|
||||
if (!liveChanges) continue;
|
||||
for (const [colIdx, savedValue] of changes) {
|
||||
if (liveChanges.get(colIdx) === savedValue) {
|
||||
liveChanges.delete(colIdx);
|
||||
}
|
||||
}
|
||||
if (liveChanges.size === 0) {
|
||||
dirtyRows.value.delete(sourceIndex);
|
||||
}
|
||||
}
|
||||
dirtyRows.value = new Map(dirtyRows.value);
|
||||
|
||||
if (snapshot.newRows.length > 0) {
|
||||
const savedNewRows = new Set(snapshot.newRowRefs);
|
||||
newRows.value = newRows.value.filter((row) => !savedNewRows.has(row));
|
||||
}
|
||||
|
||||
for (const sourceIndex of snapshot.deletedRows) {
|
||||
deletedRows.value.delete(sourceIndex);
|
||||
}
|
||||
deletedRows.value = new Set(deletedRows.value);
|
||||
touchPendingChanges();
|
||||
}
|
||||
|
||||
async function finishSaveChanges(savedSnapshot?: PendingSaveSnapshot) {
|
||||
isSaving.value = false;
|
||||
if (pendingAutoSaveRequested && dataGridQuickEntryEnabled.value) {
|
||||
applyQueuedAutoSaveChanges(savedSnapshot);
|
||||
} else {
|
||||
queuedAutoSaveChanges.clear();
|
||||
}
|
||||
if (!hasPendingChanges.value) {
|
||||
pendingAutoSaveRequested = false;
|
||||
return;
|
||||
}
|
||||
if (pendingAutoSaveRequested && dataGridQuickEntryEnabled.value) {
|
||||
pendingAutoSaveRequested = false;
|
||||
await saveChanges({ autoSave: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function finishInterruptedSaveChanges(snapshot: PendingSaveSnapshot) {
|
||||
snapshot.newRowRefs.forEach((row) => savingNewRows.delete(row));
|
||||
await finishSaveChanges();
|
||||
}
|
||||
|
||||
function saveStatementOptions(snapshot = snapshotPendingSaveChanges()) {
|
||||
if (!tableMeta.value) return null;
|
||||
return {
|
||||
databaseType: resolvedDatabaseType.value,
|
||||
@@ -780,9 +1059,9 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
columns: result.value.columns,
|
||||
sourceColumns: sourceColumns.value,
|
||||
rows: result.value.rows,
|
||||
dirtyRows: [...dirtyRows.value.entries()].map(([rowIndex, changes]) => [rowIndex, [...changes.entries()]] as [number, Array<[number, CellValue]>]),
|
||||
deletedRows: [...deletedRows.value],
|
||||
newRows: newRows.value,
|
||||
dirtyRows: [...snapshot.dirtyRows.entries()].map(([rowIndex, changes]) => [rowIndex, [...changes.entries()]] as [number, Array<[number, CellValue]>]),
|
||||
deletedRows: [...snapshot.deletedRows],
|
||||
newRows: snapshot.newRows,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -791,21 +1070,21 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
return [tableMeta.value.schema, tableMeta.value.tableName].filter(Boolean).join(".");
|
||||
}
|
||||
|
||||
function dataChangeOperation() {
|
||||
const operations = [newRows.value.length > 0 ? "INSERT" : "", dirtyRows.value.size > 0 ? "UPDATE" : "", deletedRows.value.size > 0 ? "DELETE" : ""].filter(Boolean);
|
||||
function dataChangeOperation(snapshot: PendingSaveSnapshot) {
|
||||
const operations = [snapshot.newRows.length > 0 ? "INSERT" : "", snapshot.dirtyRows.size > 0 ? "UPDATE" : "", snapshot.deletedRows.size > 0 ? "DELETE" : ""].filter(Boolean);
|
||||
return operations.length === 1 ? operations[0] : "DATA CHANGE";
|
||||
}
|
||||
|
||||
async function recordDataGridHistory(statements: string[], rollbackStatements: string[], elapsed: number, historyResult?: { affected_rows?: number; success?: boolean; error?: string }) {
|
||||
async function recordDataGridHistory(statements: string[], rollbackStatements: string[], elapsed: number, snapshot: PendingSaveSnapshot, historyResult?: { affected_rows?: number; success?: boolean; error?: string }) {
|
||||
if (!connectionId.value || !tableMeta.value) return;
|
||||
const connName = connectionStore.getConfig(connectionId.value)?.name || "";
|
||||
const success = historyResult?.success ?? true;
|
||||
const details = {
|
||||
schema: tableMeta.value.schema,
|
||||
table: tableMeta.value.tableName,
|
||||
inserted_rows: newRows.value.length,
|
||||
updated_rows: dirtyRows.value.size,
|
||||
deleted_rows: deletedRows.value.size,
|
||||
inserted_rows: snapshot.newRows.length,
|
||||
updated_rows: snapshot.dirtyRows.size,
|
||||
deleted_rows: snapshot.deletedRows.size,
|
||||
statements,
|
||||
rollback_statements: success ? rollbackStatements : [],
|
||||
error: success ? undefined : historyResult?.error,
|
||||
@@ -819,7 +1098,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
success,
|
||||
error: success ? undefined : historyResult?.error,
|
||||
activity_kind: "data_change",
|
||||
operation: dataChangeOperation(),
|
||||
operation: dataChangeOperation(snapshot),
|
||||
target: tableHistoryTarget(),
|
||||
affected_rows: success ? (historyResult?.affected_rows ?? statements.length) : undefined,
|
||||
rollback_sql: success && rollbackStatements.length ? rollbackStatements.join("\n") : undefined,
|
||||
@@ -827,10 +1106,10 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
async function recordFailedDataGridHistory(statements: string[], rollbackStatements: string[], start: number, error: unknown) {
|
||||
const message = normalizeDataGridSaveError(resolvedDatabaseType.value, error);
|
||||
async function recordFailedDataGridHistory(statements: string[], rollbackStatements: string[], start: number, snapshot: PendingSaveSnapshot, error: unknown) {
|
||||
const message = normalizeDataGridSaveError(databaseType.value, error);
|
||||
try {
|
||||
await recordDataGridHistory(statements, rollbackStatements, Date.now() - start, {
|
||||
await recordDataGridHistory(statements, rollbackStatements, Date.now() - start, snapshot, {
|
||||
success: false,
|
||||
error: message,
|
||||
});
|
||||
@@ -844,70 +1123,71 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
options.emit("reload", sql.value, searchText.value, options.currentWhereInput.value, orderByInput.value.trim() || undefined, pageSize.value, (currentPage.value - 1) * pageSize.value);
|
||||
}
|
||||
|
||||
function applyDirtyRowsToResult() {
|
||||
for (const [sourceIndex, changes] of dirtyRows.value) {
|
||||
const row = result.value.rows[sourceIndex];
|
||||
if (row) {
|
||||
for (const [colIdx, value] of changes) {
|
||||
row[colIdx] = value;
|
||||
}
|
||||
}
|
||||
async function saveChanges(saveOptions: SaveChangesOptions = {}) {
|
||||
if (isSaving.value) {
|
||||
if (saveOptions.autoSave) pendingAutoSaveRequested = true;
|
||||
return;
|
||||
}
|
||||
const snapshot = snapshotPendingSaveChanges();
|
||||
if (!hasPendingSaveChanges(snapshot)) {
|
||||
return;
|
||||
}
|
||||
const customHandler = customSaveHandler?.value;
|
||||
if (customHandler && snapshot.newRows.length > 0 && customHandler.supportsInsert !== true && customHandler.canInsert !== true) {
|
||||
saveError.value = "当前保存目标不支持新增行。";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
saveError.value = "";
|
||||
isSaving.value = true;
|
||||
const shouldReloadAfterSave = newRows.value.length > 0 || deletedRows.value.size > 0;
|
||||
snapshot.newRowRefs.forEach((row) => savingNewRows.add(row));
|
||||
const shouldReloadAfterSave = snapshot.newRows.length > 0 || snapshot.deletedRows.size > 0;
|
||||
|
||||
if (customSaveHandler?.value) {
|
||||
if (customHandler) {
|
||||
try {
|
||||
await customSaveHandler.value.save({
|
||||
dirtyRows: dirtyRows.value,
|
||||
newRows: newRows.value,
|
||||
deletedRows: deletedRows.value,
|
||||
await customHandler.save({
|
||||
dirtyRows: snapshot.dirtyRows,
|
||||
newRows: snapshot.newRows,
|
||||
deletedRows: snapshot.deletedRows,
|
||||
columns: result.value.columns,
|
||||
rows: result.value.rows,
|
||||
});
|
||||
} catch (e: any) {
|
||||
saveError.value = normalizeDataGridSaveError(resolvedDatabaseType.value, e);
|
||||
isSaving.value = false;
|
||||
saveError.value = normalizeDataGridSaveError(databaseType.value, e);
|
||||
await finishInterruptedSaveChanges(snapshot);
|
||||
return;
|
||||
}
|
||||
applyDirtyRowsToResult();
|
||||
dirtyRows.value.clear();
|
||||
newRows.value = [];
|
||||
deletedRows.value.clear();
|
||||
snapshot.newRowRefs.forEach((row) => savingNewRows.delete(row));
|
||||
applyDirtyRowsToResult(snapshot);
|
||||
clearSavedPendingChanges(snapshot);
|
||||
if (!hasPendingChanges.value) exitTransaction();
|
||||
clearPendingChangeHistory();
|
||||
touchPendingChanges();
|
||||
exitTransaction();
|
||||
isSaving.value = false;
|
||||
if (shouldReloadAfterSave) {
|
||||
reloadCurrentData();
|
||||
}
|
||||
await finishSaveChanges(snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
const stmtOptions = saveStatementOptions();
|
||||
const stmtOptions = saveStatementOptions(snapshot);
|
||||
let preparedSave: Awaited<ReturnType<typeof api.prepareDataGridSave>> | undefined;
|
||||
if (stmtOptions) {
|
||||
try {
|
||||
preparedSave = await api.prepareDataGridSave(stmtOptions);
|
||||
} catch (e: any) {
|
||||
saveError.value = normalizeDataGridSaveError(resolvedDatabaseType.value, e);
|
||||
isSaving.value = false;
|
||||
saveError.value = normalizeDataGridSaveError(databaseType.value, e);
|
||||
await finishInterruptedSaveChanges(snapshot);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (preparedSave?.validationError) {
|
||||
saveError.value = preparedSave.validationError;
|
||||
isSaving.value = false;
|
||||
await finishInterruptedSaveChanges(snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
const stmts = preparedSave?.statements ?? [];
|
||||
if (stmts.length === 0) {
|
||||
isSaving.value = false;
|
||||
await finishInterruptedSaveChanges(snapshot);
|
||||
return;
|
||||
}
|
||||
const rollbackStmts = preparedSave?.rollbackStatements ?? [];
|
||||
@@ -915,7 +1195,6 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
let apiResult: { affected_rows?: number } | undefined;
|
||||
console.info("[DBX][dataGrid:save-statements]", {
|
||||
databaseType: databaseType.value,
|
||||
resolvedDatabaseType: resolvedDatabaseType.value,
|
||||
table: tableMeta.value ? [tableMeta.value.schema, tableMeta.value.tableName].filter(Boolean).join(".") : undefined,
|
||||
statements: stmts,
|
||||
rollbackStatements: rollbackStmts,
|
||||
@@ -925,16 +1204,16 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
try {
|
||||
apiResult = await api.executeInTransaction(connectionId.value!, database.value ?? "", stmts, preparedSave?.executionSchema);
|
||||
} catch (e: any) {
|
||||
saveError.value = await recordFailedDataGridHistory(stmts, rollbackStmts, start, e);
|
||||
isSaving.value = false;
|
||||
saveError.value = await recordFailedDataGridHistory(stmts, rollbackStmts, start, snapshot, e);
|
||||
await finishInterruptedSaveChanges(snapshot);
|
||||
return;
|
||||
}
|
||||
} else if (hasBackendSaveTarget.value) {
|
||||
try {
|
||||
apiResult = await api.executeBatch(connectionId.value!, database.value ?? "", stmts, preparedSave?.executionSchema);
|
||||
} catch (e: any) {
|
||||
saveError.value = await recordFailedDataGridHistory(stmts, rollbackStmts, start, e);
|
||||
isSaving.value = false;
|
||||
saveError.value = await recordFailedDataGridHistory(stmts, rollbackStmts, start, snapshot, e);
|
||||
await finishInterruptedSaveChanges(snapshot);
|
||||
return;
|
||||
}
|
||||
} else if (onExecuteSql.value) {
|
||||
@@ -943,36 +1222,36 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
await onExecuteSql.value(sqlStmt);
|
||||
}
|
||||
} catch (e: any) {
|
||||
saveError.value = await recordFailedDataGridHistory(stmts, rollbackStmts, start, e);
|
||||
isSaving.value = false;
|
||||
saveError.value = await recordFailedDataGridHistory(stmts, rollbackStmts, start, snapshot, e);
|
||||
await finishInterruptedSaveChanges(snapshot);
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
await recordDataGridHistory(stmts, rollbackStmts, Date.now() - start, apiResult);
|
||||
await recordDataGridHistory(stmts, rollbackStmts, Date.now() - start, snapshot, apiResult);
|
||||
} catch (e) {
|
||||
console.warn("[DBX] failed to record data grid history", e);
|
||||
}
|
||||
applyDirtyRowsToResult();
|
||||
dirtyRows.value.clear();
|
||||
newRows.value = [];
|
||||
deletedRows.value.clear();
|
||||
applyDirtyRowsToResult(snapshot);
|
||||
snapshot.newRowRefs.forEach((row) => savingNewRows.delete(row));
|
||||
clearSavedPendingChanges(snapshot);
|
||||
if (!hasPendingChanges.value) exitTransaction();
|
||||
clearPendingChangeHistory();
|
||||
touchPendingChanges();
|
||||
exitTransaction();
|
||||
isSaving.value = false;
|
||||
if (shouldReloadAfterSave) {
|
||||
reloadCurrentData();
|
||||
}
|
||||
await finishSaveChanges(snapshot);
|
||||
}
|
||||
|
||||
function discardChanges() {
|
||||
dirtyRows.value.clear();
|
||||
newRows.value = [];
|
||||
deletedRows.value.clear();
|
||||
quickEntryDraftRow.value = emptyDraftRow();
|
||||
queuedAutoSaveChanges.clear();
|
||||
editingCell.value = null;
|
||||
clearPendingChangeHistory();
|
||||
touchPendingChanges();
|
||||
editingCell.value = null;
|
||||
exitTransaction();
|
||||
}
|
||||
|
||||
@@ -1001,12 +1280,14 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
}
|
||||
const scroll = includeScroll ? (readScrollPosition() ?? pendingScrollRestore) : undefined;
|
||||
if (includeScroll) pendingScrollRestore = scroll;
|
||||
if (!hasPendingChanges.value && !(includeEditing && editingCell.value) && !scroll) {
|
||||
const quickEntryDraftRowSnapshot = draftRowHasValue() ? [...quickEntryDraftRow.value] : undefined;
|
||||
if (!hasPendingChanges.value && !quickEntryDraftRowSnapshot && !(includeEditing && editingCell.value) && !scroll) {
|
||||
pendingChangesCache.delete(k);
|
||||
return;
|
||||
}
|
||||
pendingChangesCache.set(k, {
|
||||
newRows: newRows.value.map((r) => [...r]),
|
||||
quickEntryDraftRow: quickEntryDraftRowSnapshot,
|
||||
dirtyRows: new Map([...dirtyRows.value].map(([i, m]) => [i, new Map(m)])),
|
||||
deletedRows: new Set(deletedRows.value),
|
||||
editingCell: includeEditing && editingCell.value ? { ...editingCell.value } : null,
|
||||
@@ -1084,7 +1365,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
previewStatements.value = stmts;
|
||||
return stmts;
|
||||
} catch (e: any) {
|
||||
saveError.value = normalizeDataGridSaveError(resolvedDatabaseType.value, e);
|
||||
saveError.value = normalizeDataGridSaveError(databaseType.value, e);
|
||||
return [];
|
||||
} finally {
|
||||
isPreviewLoading.value = false;
|
||||
@@ -1098,14 +1379,14 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
dirtyRows,
|
||||
newRows,
|
||||
deletedRows,
|
||||
pendingChangesVersion,
|
||||
quickEntryDraftRow,
|
||||
quickEntryDraftRowId: DATA_GRID_QUICK_ENTRY_DRAFT_ROW_ID,
|
||||
dirtyRowCount,
|
||||
newRowCount,
|
||||
deletedRowCount,
|
||||
pendingChangesVersion,
|
||||
pendingChangeCount,
|
||||
hasPendingChanges,
|
||||
canUndoPendingChange,
|
||||
canRedoPendingChange,
|
||||
transactionActive,
|
||||
isSaving,
|
||||
saveError,
|
||||
@@ -1114,11 +1395,10 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
exitTransaction,
|
||||
startEdit,
|
||||
commitEdit,
|
||||
commitEditAndMaybeAutoSave,
|
||||
commitEditFromBlur,
|
||||
applyCellValue,
|
||||
restoreCellValue,
|
||||
undoPendingChange,
|
||||
redoPendingChange,
|
||||
cancelEdit,
|
||||
onEditKeydown,
|
||||
addRow,
|
||||
@@ -1136,7 +1416,14 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
deleteSelectedRow,
|
||||
saveChanges,
|
||||
discardChanges,
|
||||
canUndoPendingChange,
|
||||
canRedoPendingChange,
|
||||
undoPendingChange,
|
||||
redoPendingChange,
|
||||
rowDataWithChanges,
|
||||
ensureQuickEntryDraftRow,
|
||||
draftRowHasValue,
|
||||
isSavingNewRow,
|
||||
coerceCellValue,
|
||||
canEditColumn,
|
||||
resetGridVerticalScroll,
|
||||
@@ -1147,6 +1434,8 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
previewStatements,
|
||||
isPreviewLoading,
|
||||
previewChanges,
|
||||
savePendingSnapshot,
|
||||
restorePendingSnapshotFocus,
|
||||
syncHeaderScroll: (headerRef: Ref<HTMLDivElement | undefined>) => (e: Event) => {
|
||||
if (headerRef.value) {
|
||||
headerRef.value.scrollLeft = (e.target as HTMLElement).scrollLeft;
|
||||
|
||||
@@ -22,6 +22,7 @@ interface RowItem {
|
||||
newIndex?: number;
|
||||
data: CellValue[];
|
||||
isNew: boolean;
|
||||
isDraft?: boolean;
|
||||
isDeleted: boolean;
|
||||
isDirtyCol: boolean[];
|
||||
status: string;
|
||||
@@ -139,13 +140,13 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
}
|
||||
|
||||
function rowsToExport(rowIds?: number[]): RowItem[] {
|
||||
if (!rowIds?.length) return displayItems.value;
|
||||
if (rowIds === undefined) return displayItems.value.filter((item) => !item.isDraft);
|
||||
const rowIdSet = new Set(rowIds);
|
||||
return displayItems.value.filter((item) => rowIdSet.has(item.id));
|
||||
return displayItems.value.filter((item) => rowIdSet.has(item.id) && !item.isDraft);
|
||||
}
|
||||
|
||||
async function resultToExport(rowIds?: number[], onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void, useFullExport = true): Promise<{ columns: string[]; rows: CellValue[][] }> {
|
||||
if (useFullExport && !rowIds?.length && fullExportResult) {
|
||||
if (useFullExport && rowIds === undefined && fullExportResult) {
|
||||
const result = await fullExportResult(onProgress);
|
||||
if (result) return { columns: result.columns, rows: result.rows };
|
||||
}
|
||||
@@ -161,19 +162,19 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
|
||||
function targetedRows(): RowItem[] {
|
||||
if (hasRowSelection.value && selectedRowIds.value.size > 0) {
|
||||
return displayItems.value.filter((item) => selectedRowIds.value.has(item.id));
|
||||
return displayItems.value.filter((item) => selectedRowIds.value.has(item.id) && !item.isDraft);
|
||||
}
|
||||
const range = selectedRange.value;
|
||||
if (range && range.startRow !== range.endRow) {
|
||||
return displayItems.value.slice(range.startRow, range.endRow + 1);
|
||||
return displayItems.value.slice(range.startRow, range.endRow + 1).filter((item) => !item.isDraft);
|
||||
}
|
||||
if (!contextCell.value) return [];
|
||||
const item = getRowItem(contextCell.value.rowId);
|
||||
return item ? [item] : [];
|
||||
return item && !item.isDraft ? [item] : [];
|
||||
}
|
||||
|
||||
function updateEligibleRows(): RowItem[] {
|
||||
return targetedRows().filter((item) => !item.isNew && !item.isDeleted);
|
||||
return targetedRows().filter((item) => !item.isNew && !item.isDraft && !item.isDeleted);
|
||||
}
|
||||
|
||||
function updateCopyKey(): string {
|
||||
@@ -389,7 +390,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
|
||||
async function copySelectedRowsTsv() {
|
||||
if (!hasRowSelection.value || selectedRowIds.value.size === 0) return;
|
||||
const rows = displayItems.value.filter((item) => selectedRowIds.value.has(item.id)).map((item) => item.data);
|
||||
const rows = displayItems.value.filter((item) => selectedRowIds.value.has(item.id) && !item.isDraft).map((item) => item.data);
|
||||
if (rows.length === 0) return;
|
||||
await copyText(formatSelectionAsTsv({ columns: columns.value, rows }));
|
||||
}
|
||||
|
||||
@@ -417,30 +419,31 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
async function copyCell() {
|
||||
if (!contextCell.value || contextCell.value.col < 0) return;
|
||||
const item = getRowItem(contextCell.value.rowId);
|
||||
if (!item || item.isDraft) return;
|
||||
const val = item?.data[contextCell.value.col] ?? null;
|
||||
await copyText(displayCellValue(val));
|
||||
}
|
||||
|
||||
async function copyRow() {
|
||||
if (hasRowSelection.value && selectedRowIds.value.size > 0) {
|
||||
const items = displayItems.value.filter((item) => selectedRowIds.value.has(item.id));
|
||||
const items = displayItems.value.filter((item) => selectedRowIds.value.has(item.id) && !item.isDraft);
|
||||
await copyRowsAsJson(items);
|
||||
return;
|
||||
}
|
||||
const range = selectedRange.value;
|
||||
if (range && range.startRow !== range.endRow) {
|
||||
const items = displayItems.value.slice(range.startRow, range.endRow + 1);
|
||||
const items = displayItems.value.slice(range.startRow, range.endRow + 1).filter((item) => !item.isDraft);
|
||||
await copyRowsAsJson(items);
|
||||
return;
|
||||
}
|
||||
if (!contextCell.value) return;
|
||||
const item = getRowItem(contextCell.value.rowId);
|
||||
if (!item) return;
|
||||
if (!item || item.isDraft) return;
|
||||
await copyRowsAsJson([item]);
|
||||
}
|
||||
|
||||
function insertEligibleRows(): RowItem[] {
|
||||
return targetedRows();
|
||||
return targetedRows().filter((item) => !item.isDraft);
|
||||
}
|
||||
|
||||
async function copyRowAsInsert() {
|
||||
@@ -480,7 +483,10 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
|
||||
async function copyAll() {
|
||||
const header = columns.value.join("\t");
|
||||
const body = displayItems.value.map((item) => item.data.map((c) => displayCellValue(c)).join("\t")).join("\n");
|
||||
const body = displayItems.value
|
||||
.filter((item) => !item.isDraft)
|
||||
.map((item) => item.data.map((c) => displayCellValue(c)).join("\t"))
|
||||
.join("\n");
|
||||
await copyText(`${header}\n${body}`);
|
||||
}
|
||||
|
||||
@@ -501,7 +507,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
if (await exportQueryResultViaBackend("csv", rowIds)) return;
|
||||
if (await exportFullTableDataViaBackend("csv", rowIds)) return;
|
||||
|
||||
const needsFullExport = !rowIds?.length && !!fullExportResult;
|
||||
const needsFullExport = rowIds === undefined && !!fullExportResult;
|
||||
if (needsFullExport && exportProgressDialog && exportProgressState) {
|
||||
exportProgressState.value = {
|
||||
title: t("exportProgress.title"),
|
||||
@@ -705,7 +711,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
if (!path) return;
|
||||
outputPath = path as string;
|
||||
}
|
||||
const needsFullExport = !rowIds?.length && !!fullExportResult;
|
||||
const needsFullExport = rowIds === undefined && !!fullExportResult;
|
||||
if (needsFullExport && exportProgressDialog && exportProgressState) {
|
||||
exportProgressState.value = {
|
||||
title: t("exportProgress.title"),
|
||||
@@ -815,7 +821,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
|
||||
async function exportFullTableDataViaBackend(format: "csv" | "xlsx" | "json" | "markdown" | "sql", rowIds?: number[]): Promise<boolean> {
|
||||
const meta = tableMeta.value;
|
||||
if (rowIds?.length || context.value !== "table-data" || !meta || !connectionId.value || !database.value) {
|
||||
if (rowIds !== undefined || context.value !== "table-data" || !meta || !connectionId.value || !database.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -894,7 +900,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
}
|
||||
|
||||
async function exportQueryResultViaBackend(format: "csv" | "xlsx", rowIds?: number[]): Promise<boolean> {
|
||||
if (rowIds?.length || context.value !== "results" || !queryResultExportRequest) {
|
||||
if (rowIds !== undefined || context.value !== "results" || !queryResultExportRequest) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ interface RowItem {
|
||||
newIndex?: number;
|
||||
data: CellValue[];
|
||||
isNew: boolean;
|
||||
isDraft?: boolean;
|
||||
isDeleted: boolean;
|
||||
isDirtyCol: boolean[];
|
||||
status: string;
|
||||
@@ -45,21 +46,37 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
return normalizeSelectionRange(selectionAnchor.value, selectionFocus.value);
|
||||
});
|
||||
|
||||
const visibleSelectionRows = computed(() => displayItems.value.map((item) => item.data));
|
||||
|
||||
const selectedCells = computed<SelectionData>(() => {
|
||||
if (hasColumnSelection.value) {
|
||||
return extractColumnsSelection(columns.value, visibleSelectionRows.value, selectedColumnIndexes.value);
|
||||
const rows = displayItems.value.filter((item) => !item.isDraft).map((item) => item.data);
|
||||
return extractColumnsSelection(columns.value, rows, selectedColumnIndexes.value);
|
||||
}
|
||||
if (selectedCellKeys.value.size > 0) {
|
||||
return extractSelectedCellKeys(columns.value, visibleSelectionRows.value, selectedCellKeys.value);
|
||||
const selectableKeys = [...selectedCellKeys.value].filter((key) => {
|
||||
const position = parseCellKey(key);
|
||||
return !!position && !displayItems.value[position.rowIndex]?.isDraft;
|
||||
});
|
||||
return extractSelectedCellKeys(
|
||||
columns.value,
|
||||
displayItems.value.map((item) => item.data),
|
||||
selectableKeys,
|
||||
);
|
||||
}
|
||||
const range = selectedRange.value;
|
||||
if (!range) return { columns: [], rows: [] };
|
||||
return extractSelection(columns.value, visibleSelectionRows.value, range);
|
||||
const rows = displayItems.value
|
||||
.slice(range.startRow, range.endRow + 1)
|
||||
.filter((item) => !item.isDraft)
|
||||
.map((item) => item.data);
|
||||
return extractSelection(columns.value, rows, {
|
||||
startRow: 0,
|
||||
endRow: rows.length - 1,
|
||||
startCol: range.startCol,
|
||||
endCol: range.endCol,
|
||||
});
|
||||
});
|
||||
|
||||
const selectedCellCount = computed(() => (selectedCellKeys.value.size > 0 ? selectedCellKeys.value.size : selectedCells.value.columns.length * selectedCells.value.rows.length));
|
||||
const selectedCellCount = computed(() => selectedCells.value.rows.reduce((count, row) => count + row.length, 0));
|
||||
const hasCellSelection = computed(() => selectedCellCount.value > 0);
|
||||
|
||||
function clearCellSelection() {
|
||||
|
||||
Reference in new issue
Block a user