diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 044ce1204..744eb0d09 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -119,6 +119,7 @@ import { } from "@/lib/paginationPageSize"; import { filterColumnVisibilityOptions, + invertedHiddenColumnIndexes, nextHiddenColumnIndexes, visibleColumnIndexesForFilter, } from "@/lib/dataGridColumnVisibility"; @@ -1076,6 +1077,9 @@ function toggleColumnVisibility(columnIndex: number) { function showAllColumns() { hiddenColumnIndexes.value = new Set(); } +function invertColumnVisibility() { + hiddenColumnIndexes.value = invertedHiddenColumnIndexes(displayableColumnIndexes.value, hiddenColumnIndexes.value); +} const firstVisibleColumnIndex = computed(() => visibleColumnIndexes.value[0] ?? 0); function actualColumnIndex(visibleColumnIndex: number): number { return visibleColumnIndexes.value[visibleColumnIndex] ?? visibleColumnIndex; @@ -1716,6 +1720,21 @@ const activeCellDetail = computed(() => { const detailEditValue = ref(""); const isEditingDetail = ref(false); +const detailTemporalEditorKind = computed(() => { + const detail = activeCellDetail.value; + return detail ? temporalEditorKindForColumn(detail.colIndex) : undefined; +}); + +function resetDetailEdit() { + isEditingDetail.value = false; + detailEditValue.value = ""; +} + +function closeCellDetails() { + resetDetailEdit(); + showCellDetail.value = false; + detailCell.value = null; +} function startDetailEdit() { const detail = activeCellDetail.value; @@ -1759,7 +1778,7 @@ function commitDetailEdit() { } function cancelDetailEdit() { - isEditingDetail.value = false; + resetDetailEdit(); } function setDetailNull() { @@ -1772,7 +1791,7 @@ function setDetailNull() { if (item.isNew && item.newIndex !== undefined) { newRows.value[item.newIndex][detail.colIndex] = null; newRows.value = [...newRows.value]; - isEditingDetail.value = false; + resetDetailEdit(); detailCell.value = { ...detailCell.value! }; return; } @@ -1785,7 +1804,7 @@ function setDetailNull() { if (useTransaction.value && !transactionActive.value) { enterTransaction(); } - isEditingDetail.value = false; + resetDetailEdit(); detailCell.value = { ...detailCell.value! }; } @@ -2002,6 +2021,7 @@ const { // --- Cell selection and detail --- function showCellDetails(rowIndex: number, colIndex: number) { + resetDetailEdit(); detailCell.value = { rowIndex, col: colIndex }; showCellDetail.value = true; } @@ -2059,6 +2079,48 @@ function cutSelection() { } } +function currentSelectedCellPosition() { + const range = selectedRange.value; + if (!range) return null; + return { rowIndex: range.startRow, colIndex: range.startCol }; +} + +function scrollCellIntoView(rowIndex: number, colIndex: number) { + nextTick(() => { + const rowEl = gridRef.value?.querySelector(`[data-row-index="${rowIndex}"]`); + const cellEl = rowEl?.querySelector(`[data-visible-col-index="${colIndex}"]`); + (cellEl ?? rowEl)?.scrollIntoView({ block: "nearest", inline: "nearest" }); + }); +} + +function moveSelectedCell(rowDelta: number, colDelta: number): boolean { + const position = currentSelectedCellPosition(); + if (!position || editingCell.value || displayItems.value.length === 0 || visibleColumnIndexes.value.length === 0) + return false; + const rowIndex = Math.max(0, Math.min(displayItems.value.length - 1, position.rowIndex + rowDelta)); + const colIndex = Math.max(0, Math.min(visibleColumnIndexes.value.length - 1, position.colIndex + colDelta)); + selectSingleCell(rowIndex, colIndex); + clearRowSelection(); + if (showTranspose.value) transposeRowIndex.value = rowIndex; + scrollCellIntoView(rowIndex, colIndex); + return true; +} + +function editSelectedCell(): boolean { + const position = currentSelectedCellPosition(); + if (!position || editingCell.value) return false; + const item = displayItems.value[position.rowIndex]; + const actualColIndex = actualColumnIndex(position.colIndex); + if (!item || !canEditCellItem(item, actualColIndex)) return false; + startEdit(item.id, actualColIndex); + return true; +} + +function commitGridEdit() { + commitEdit(); + nextTick(() => gridRef.value?.focus({ preventScroll: true })); +} + async function onGridKeydown(event: KeyboardEvent) { if (isFocusSearchShortcut(event)) { event.preventDefault(); @@ -2066,6 +2128,34 @@ async function onGridKeydown(event: KeyboardEvent) { return; } if (eventTargetAllowsNativeClipboard(event)) return; + if (event.key === "ArrowLeft" && moveTransposeRecordSelection(-1)) { + event.preventDefault(); + return; + } + if (event.key === "ArrowRight" && moveTransposeRecordSelection(1)) { + event.preventDefault(); + return; + } + if (event.key === "ArrowUp" && moveSelectedCell(-1, 0)) { + event.preventDefault(); + return; + } + if (event.key === "ArrowDown" && moveSelectedCell(1, 0)) { + event.preventDefault(); + return; + } + if (event.key === "ArrowLeft" && moveSelectedCell(0, -1)) { + event.preventDefault(); + return; + } + if (event.key === "ArrowRight" && moveSelectedCell(0, 1)) { + event.preventDefault(); + return; + } + if (event.key === "Enter" && editSelectedCell()) { + event.preventDefault(); + return; + } if (clipboardShortcut(event, "c")) { if (!hasCellSelection.value) return; event.preventDefault(); @@ -2092,6 +2182,12 @@ function copyDetailValue() { copyText(text); } +function copyDetailFormattedJson() { + const detail = activeCellDetail.value; + if (!detail?.formattedJson) return; + copyText(detail.formattedJson); +} + function copyDetailColumnName() { if (!activeCellDetail.value) return; copyText(activeCellDetail.value.column); @@ -2105,8 +2201,14 @@ function copyDetailSqlCondition() { copyText(condition); } -const TRANSPOSE_RECORD_WIDTH = 168; -const transposePinnedWidth = computed(() => transposeFieldWidth(visibleColumns.value)); +const TRANSPOSE_RECORD_DEFAULT_WIDTH = 168; +const TRANSPOSE_RECORD_MIN_WIDTH = 96; +const TRANSPOSE_PINNED_MIN_WIDTH = 104; +const transposeRecordWidth = ref(TRANSPOSE_RECORD_DEFAULT_WIDTH); +const transposePinnedWidthOverride = ref(null); +const transposePinnedWidth = computed( + () => transposePinnedWidthOverride.value ?? transposeFieldWidth(visibleColumns.value), +); const transposeRows = computed(() => { return buildTransposeRows({ @@ -2123,7 +2225,7 @@ const transposeRecordWindow = computed(() => scrollLeft: transposeScrollLeft.value, viewportWidth: transposeViewportWidth.value, pinnedWidth: transposePinnedWidth.value, - recordWidth: TRANSPOSE_RECORD_WIDTH, + recordWidth: transposeRecordWidth.value, overscan: 2, }), ); @@ -2132,7 +2234,7 @@ const visibleTransposeRecordIndexes = computed(() => { return Array.from({ length: window.end - window.start }, (_, offset) => window.start + offset); }); const transposeTotalWidth = computed( - () => transposePinnedWidth.value + displayItems.value.length * TRANSPOSE_RECORD_WIDTH, + () => transposePinnedWidth.value + displayItems.value.length * transposeRecordWidth.value, ); function transposeScrollElement(): HTMLElement | undefined { @@ -2161,13 +2263,54 @@ function scrollTransposeRecordIntoView(rowIndex: number) { totalRecords: displayItems.value.length, viewportWidth: el.clientWidth, pinnedWidth: transposePinnedWidth.value, - recordWidth: TRANSPOSE_RECORD_WIDTH, + recordWidth: transposeRecordWidth.value, }); updateTransposeViewport(); }); } +function onTransposePinnedResizeStart(event: MouseEvent) { + event.preventDefault(); + const startX = event.clientX; + const startWidth = transposePinnedWidth.value; + const onMove = (e: MouseEvent) => { + transposePinnedWidthOverride.value = Math.max(TRANSPOSE_PINNED_MIN_WIDTH, startWidth + e.clientX - startX); + updateTransposeViewport(); + }; + const onUp = () => { + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + }; + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); +} + +function onTransposeRecordResizeStart(event: MouseEvent) { + event.preventDefault(); + const startX = event.clientX; + const startWidth = transposeRecordWidth.value; + const onMove = (e: MouseEvent) => { + transposeRecordWidth.value = Math.max(TRANSPOSE_RECORD_MIN_WIDTH, startWidth + e.clientX - startX); + updateTransposeViewport(); + }; + const onUp = () => { + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + }; + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); +} + +function closeTranspose() { + showTranspose.value = false; + transposeRowIndex.value = null; +} + function openContextTranspose() { + if (showTranspose.value) { + closeTranspose(); + return; + } if (!contextCell.value) return; const next = nextContextTransposeState({ showTranspose: showTranspose.value, @@ -2180,7 +2323,7 @@ function openContextTranspose() { transposeRowIndex.value = next.transposeRowIndex; showTranspose.value = next.showTranspose; if (next.showTranspose) { - showCellDetail.value = false; + closeCellDetails(); nextTick(updateTransposeViewport); if (next.transposeRowIndex !== null) scrollTransposeRecordIntoView(next.transposeRowIndex); } @@ -2191,19 +2334,29 @@ function toggleTranspose(rowIndex: number) { transposeRowIndex.value = next.transposeRowIndex; showTranspose.value = next.showTranspose; if (next.showTranspose) { - showCellDetail.value = false; + closeCellDetails(); nextTick(updateTransposeViewport); if (next.transposeRowIndex !== null) scrollTransposeRecordIntoView(next.transposeRowIndex); } } +function selectTransposeRecord(rowIndex: number) { + if (rowIndex < 0 || rowIndex >= displayItems.value.length) return; + transposeRowIndex.value = rowIndex; + gridRef.value?.focus({ preventScroll: true }); +} + +function moveTransposeRecordSelection(delta: number): boolean { + if (!isTransposeMode.value || displayItems.value.length === 0) return false; + const current = transposeRowIndex.value ?? 0; + const next = Math.max(0, Math.min(displayItems.value.length - 1, current + delta)); + transposeRowIndex.value = next; + scrollTransposeRecordIntoView(next); + return true; +} + function transposeNav(delta: number) { - if (transposeRowIndex.value === null) return; - const next = transposeRowIndex.value + delta; - if (next >= 0 && next < displayItems.value.length) { - transposeRowIndex.value = next; - scrollTransposeRecordIntoView(next); - } + moveTransposeRecordSelection(delta); } watch(isTransposeMode, (active) => { @@ -2219,10 +2372,8 @@ watch( } clearCellSelection(); clearRowSelection(); - showCellDetail.value = false; - detailCell.value = null; - showTranspose.value = false; - transposeRowIndex.value = null; + closeCellDetails(); + closeTranspose(); exitTransaction(); }, ); @@ -2546,6 +2697,7 @@ defineExpose({ isColumnVisible, toggleColumnVisibility, showAllColumns, + invertColumnVisibility, }); @@ -2885,7 +3037,7 @@ defineExpose({ > - @@ -2895,7 +3047,7 @@ defineExpose({ :style="{ '--transpose-total-w': `${transposeTotalWidth}px`, '--transpose-field-w': `${transposePinnedWidth}px`, - '--transpose-record-w': `${TRANSPOSE_RECORD_WIDTH}px`, + '--transpose-record-w': `${transposeRecordWidth}px`, }" :items="transposeRows" :item-size="30" @@ -2909,24 +3061,33 @@ defineExpose({ :style="{ width: `${transposeTotalWidth}px` }" >
{{ t("grid.columnName") }} +
{{ recordIndex + 1 }} +
@@ -2952,8 +3113,9 @@ defineExpose({ 'text-muted-foreground italic': item.values[recordIndex]?.isNull, 'bg-primary/10': recordIndex === transposeRowIndex, }" - :style="{ width: `${TRANSPOSE_RECORD_WIDTH}px` }" + :style="{ width: `${transposeRecordWidth}px` }" :title="item.values[recordIndex]?.display" + @click="selectTransposeRecord(recordIndex)" > {{ item.values[recordIndex]?.display }}
@@ -3397,16 +3559,27 @@ defineExpose({
+ {{ t("grid.columnName") }} + + {{ col }} + + @@ -3509,6 +3682,7 @@ defineExpose({ @mousedown="handleDataCellMousedown(index, visibleColIdx, item.id, $event)" @mouseenter="extendCellSelection(index, visibleColIdx)" @dblclick="canEditCellItem(item, actualColIdx) && startEdit(item.id, actualColIdx)" + :data-visible-col-index="visibleColIdx" @contextmenu="onCellContext(item.id, index, actualColIdx, visibleColIdx)" >