diff --git a/Cargo.lock b/Cargo.lock index f8ee5c9a30..995dc30258 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1930,6 +1930,8 @@ dependencies = [ "tokio-util", "tracing", "tracing-subscriber", + "unicode-width", + "utoipa", "uuid", "webpki-roots 0.26.11", "winnow 1.0.3", @@ -1978,6 +1980,7 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", + "utoipa", "uuid", ] @@ -9347,6 +9350,30 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utoipa" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.117", +] + [[package]] name = "uuid" version = "1.23.1" diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 34c79677a2..407f5fc6af 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -58,6 +58,7 @@ import DataGridCellDetailPanel from "@/components/grid/DataGridCellDetailPanel.v import DataGridPagination from "@/components/grid/DataGridPagination.vue"; import DataGridSearchBar from "@/components/grid/DataGridSearchBar.vue"; import DataGridToolbar from "@/components/grid/DataGridToolbar.vue"; +import DataGridExtractorDialog from "@/components/grid/DataGridExtractorDialog.vue"; import DataGridColumnHeader from "@/components/grid/DataGridColumnHeader.vue"; import DataGridQueryControls from "@/components/grid/DataGridQueryControls.vue"; import TemporalCellEditor from "@/components/grid/TemporalCellEditor.vue"; @@ -70,6 +71,7 @@ import { tableInfoTabForDrawerToggle } from "@/lib/table/tableInfoTabPreference" import * as api from "@/lib/backend/api"; import { formatElapsedSeconds } from "@/lib/common/elapsedTime"; import { dataGridCellDisplayText, dataGridCellEditorText } from "@/lib/dataGrid/dataGridCellCoercion"; +import { dataGridActiveRowBackground } from "@/lib/dataGrid/dataGridPaintTheme"; import { createColumnDrafts } from "@/lib/table/tableStructureEditorState"; import type { BuildSingleColumnAlterSqlOptions } from "@/lib/table/tableStructureEditorSql"; import { buildTableSelectSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql"; @@ -180,6 +182,7 @@ import { useToast } from "@/composables/useToast"; import { useDataGridExport, type MongoCopyUpdateTarget } from "@/composables/useDataGridExport"; import { eventTargetAllowsNativeClipboard, isPlainClipboardShortcut, readTextFromClipboard } from "@/lib/common/clipboard"; import { claimDataGridPaste, clearDataGridClipboardCopy, parseDataGridClipboard, planDataGridPaste } from "@/lib/dataGrid/dataGridClipboard"; +import { DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS, DATA_GRID_COPY_EXTRACTOR_IDS, extractorUnavailableForDatabase, type DataGridCopyExtractorId } from "@/lib/dataGrid/dataGridCopyExtractor"; import { columnNamesForCopy } from "@/lib/dataGrid/dataGridColumnNameCopy"; import { DATA_GRID_ROW_NUM_WIDTH, useDataGridColumnResize } from "@/composables/useDataGridColumnResize"; import { createDataGridColumnStructureSignature } from "@/lib/dataGrid/dataGridColumnWidthState"; @@ -204,7 +207,7 @@ import { useConnectionStore } from "@/stores/connectionStore"; import { useQueryStore } from "@/stores/queryStore"; import { useSettingsStore } from "@/stores/settingsStore"; import type { DataGridSortDirection, DataGridSortMode } from "@/lib/dataGrid/dataGridSort"; -import { DATA_GRID_COMPACT_TOPBAR_WIDTH, type DataGridReloadIntent, type DataGridToolbarActionCapability, type DataGridToolbarAutoRefreshCapability, type DataGridToolbarSaveCapability } from "@/lib/dataGrid/dataGridToolbar"; +import { DATA_GRID_COMPACT_TOPBAR_WIDTH, type DataGridReloadIntent, type DataGridToolbarActionCapability, type DataGridToolbarAutoRefreshCapability, type DataGridToolbarCopyCapability, type DataGridToolbarSaveCapability } from "@/lib/dataGrid/dataGridToolbar"; import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities"; import { getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities"; import { filterObjectBrowserTableColumns } from "@/lib/table/objectBrowserTableInfo"; @@ -550,9 +553,19 @@ function typeColorClass(t: string): string { return "text-muted-foreground"; } const contextCell = ref<{ rowId: number; rowIndex: number; col: number } | null>(null); +// True when onCellContext created a synthetic single-cell selection (the cell +// was not previously selected). Lets buildRequest distinguish a genuine 1×1 +// selection (user Ctrl+click) from a synthetic one (right-click on unselected). +const contextSelectionIsSynthetic = ref(false); const contextHeaderColumn = ref(null); const contextHeaderColumnIndex = ref(null); const contextHeaderVisibleColIdx = ref(null); + +function invalidateSyntheticContextSelection() { + contextSelectionIsSynthetic.value = false; + contextCell.value = null; +} + const bulkEditDialogOpen = ref(false); const bulkEditValue = ref(""); const copyColumnNamesDialogOpen = ref(false); @@ -3433,6 +3446,8 @@ const selection = useDataGridSelection({ gridRef, getScrollElement: dataGridSelectionScroller, cellFromClientPoint: dataGridCellFromClientPoint, + rowFromClientPoint: dataGridRowFromClientPoint, + onUserCellSelection: invalidateSyntheticContextSelection, runtimeScope: dataGridRuntimeScope, }); @@ -3461,6 +3476,7 @@ const { hasColumnSelection, clearRowSelection, handleRowClick, + beginRowSelection, handleDataCellMousedown, isRowSelected, } = selection; @@ -4359,28 +4375,31 @@ function rowNumberStatusClass(item: RowItem): string { } function rowCellsUseSelectionVisual(rowId: number): boolean { - return hasRowSelection.value && isRowSelected(rowId) && !hasCellSelection.value; + return hasRowSelection.value && isRowSelected(rowId); } function dataGridRowStyle(item: RowItem): CSSProperties { const dark = isDark.value || (typeof document !== "undefined" && document.documentElement.classList.contains("dark")); + const activeRowBg = dataGridActiveRowBackground(dark); const rowBg = item.isDeleted ? dark ? "rgb(55, 31, 32)" : "rgb(255, 244, 244)" - : item.isNew - ? dark - ? "rgb(51, 51, 55)" - : "rgb(243, 243, 243)" - : item.isDraft + : isRowActive(item.displayIndex) + ? activeRowBg + : item.isNew ? dark ? "rgb(51, 51, 55)" : "rgb(243, 243, 243)" - : item.displayIndex % 2 === 1 - ? `var(--data-grid-row-muted-bg, ${dark ? DATA_GRID_DARK_STRIPED_ROW_BG : DATA_GRID_LIGHT_STRIPED_ROW_BG})` - : dark - ? "rgb(19, 20, 22)" - : "rgb(255, 255, 255)"; + : item.isDraft + ? dark + ? "rgb(51, 51, 55)" + : "rgb(243, 243, 243)" + : item.displayIndex % 2 === 1 + ? `var(--data-grid-row-muted-bg, ${dark ? DATA_GRID_DARK_STRIPED_ROW_BG : DATA_GRID_LIGHT_STRIPED_ROW_BG})` + : dark + ? "rgb(19, 20, 22)" + : "rgb(255, 255, 255)"; const rowNumberBg = item.status === "new" ? dark @@ -4395,20 +4414,19 @@ function dataGridRowStyle(item: RowItem): CSSProperties { ? "rgb(55, 31, 32)" : "rgb(255, 244, 244)" : isRowActive(item.displayIndex) && !item.isDeleted - ? dark - ? "rgb(66, 67, 70)" - : "rgb(226, 226, 226)" + ? activeRowBg : dark ? "rgb(35, 37, 42)" : "rgb(255, 255, 255)"; return { "--data-grid-cell-bg": rowBg, "--data-grid-row-number-bg": rowNumberBg, - "--data-grid-cell-selected-bg": dark ? "rgb(66, 67, 70)" : "rgb(226, 226, 226)", - "--data-grid-cell-selected-dirty-bg": dark ? "rgb(94, 75, 26)" : "rgb(244, 229, 186)", - "--data-grid-cell-selected-border": dark ? "rgb(170, 170, 175)" : "rgb(90, 90, 90)", - "--data-grid-row-number-active-bg": dark ? "rgb(66, 67, 70)" : "rgb(232, 232, 232)", - "--data-grid-row-number-selected-bg": dark ? "rgb(66, 67, 70)" : "rgb(226, 226, 226)", + "--data-grid-cell-selected-bg": dark ? "rgb(20, 40, 60)" : "rgb(239, 246, 255)", + "--data-grid-cell-selected-single-bg": dark ? "rgb(30, 64, 96)" : "rgb(191, 219, 254)", + "--data-grid-cell-selected-dirty-bg": dark ? "rgb(76, 66, 38)" : "rgb(235, 224, 184)", + "--data-grid-cell-selected-border": dark ? "rgb(96, 165, 250)" : "rgb(59, 130, 246)", + "--data-grid-row-number-active-bg": activeRowBg, + "--data-grid-row-number-selected-bg": dark ? "rgb(30, 64, 96)" : "rgb(191, 219, 254)", } as CSSProperties; } @@ -4490,6 +4508,24 @@ function dataGridCellFromClientPoint(clientX: number, clientY: number): { rowInd return { rowIndex, colIndex }; } +function dataGridRowFromClientPoint(_clientX: number, clientY: number): number | null { + const scroller = dataGridSelectionScroller(); + if (!scroller) return null; + const rect = scroller.getBoundingClientRect(); + const clampedY = Math.min(rect.bottom - 1, Math.max(rect.top + 1, clientY)); + + if (useCanvasGridRows.value) { + const rowIndex = Math.floor((scroller.scrollTop + clampedY - rect.top) / CANVAS_DATA_GRID_ROW_HEIGHT); + const item = displayItemAt(rowIndex); + return item?.displayIndex ?? null; + } + + const target = document.elementFromPoint(rect.left + Math.min(DATA_GRID_ROW_NUM_WIDTH / 2, rect.width / 2), clampedY); + const row = target instanceof Element ? target.closest("[data-row-index]") : null; + const rowIndex = Number(row?.dataset.rowIndex); + return Number.isInteger(rowIndex) ? rowIndex : null; +} + function syncCanvasViewport() { if (!dataGridIsActive) return; const scroller = canvasScrollerElement(); @@ -4755,7 +4791,7 @@ function onCanvasMouseDown(event: MouseEvent) { commitHiddenCanvasEditBeforeCellInteraction(); if (!item) return; if (hit.rowNumber) { - handleRowClick(item.displayIndex, item.id, event); + beginRowSelection(item.displayIndex, item.id, event); } else { handleDataCellMousedown(item.displayIndex, hit.visibleColIdx, item.id, event); } @@ -5080,22 +5116,10 @@ const { copyText, copyCell, copyRow, - copyRowAsInsert, - copyRowAsInsertWithoutPrimaryKeys, - canCopyRowAsInsert, - copyRowAsUpdate, - canCopyRowAsInsertWithoutPrimaryKeys, - canCopyRowAsUpdate, copyAll, - copySelectionTsv, - copySelectionTsvWithHeaders, - copySelectionCsv, - copySelectionJson, - copySelectionSqlInList, - copySelectionAsInsert, - canCopySelectionAsInsert, - copySelectedRowsTsv, - copySelectedRowsTsvWithHeaders, + copyWithExtractor, + previewWithExtractor, + canCopyWithExtractor, exportCsv, exportCurrentPageCsv, exportJson, @@ -5116,6 +5140,11 @@ const { } = useDataGridExport({ columns: visibleColumns, displayItems: visibleDisplayItems, + allColumns: computed(() => props.result.columns), + allDisplayItems: displayItems, + allSourceColumns: computed(() => props.sourceColumns), + visibleColumnIndexes, + extractorOptions: computed(() => settingsStore.editorSettings.dataGridExtractorOptions), sql: computed(() => props.sql), exportSql: computed(() => props.exportSql), tableMeta: computed(() => (props.tableMeta ? { ...props.tableMeta } : undefined)), @@ -5132,10 +5161,12 @@ const { orderBy: computed(() => currentOrderBy()), exportBatchSize: computed(() => settingsStore.editorSettings.exportBatchSize), hasCellSelection, + hasColumnSelection, selectedCells, selectedCellMatrix, selectedRange, contextCell: exportContextCell, + contextSelectionIsSynthetic, getRowItem: (rowId: number) => visibleDisplayItems.value.find((item) => item.id === rowId), selectedRowIds, hasRowSelection, @@ -5151,6 +5182,61 @@ const { exportCancelHandler, }); +function copyExtractorLabel(extractor: DataGridCopyExtractorId): string { + const labels: Record = { + tsv: "TSV", + "tsv-with-headers": t("grid.copyExtractorWithHeaders", { format: "TSV" }), + csv: "CSV", + "csv-with-headers": t("grid.copyExtractorWithHeaders", { format: "CSV" }), + "pipe-separated": t("grid.copyExtractorPipeSeparated"), + dsv: "DSV", + json: "JSON", + "json-lines": "JSON Lines", + "one-row": t("grid.copyExtractorOneRow"), + "sql-in-list": t("grid.copyExtractorSqlInList"), + "sql-inserts": t("grid.copyExtractorSqlInserts"), + "sql-updates": t("grid.copyExtractorSqlUpdates"), + "where-clause": t("grid.copyExtractorWhereClause"), + markdown: "Markdown", + html: "HTML", + xml: "XML", + pretty: t("grid.copyExtractorPretty"), + }; + return labels[extractor]; +} + +const selectedCopyExtractor = computed(() => settingsStore.editorSettings.dataGridCopyExtractor); +const extractorConfigOpen = ref(false); +const extractorMenuItems = computed(() => + DATA_GRID_COPY_EXTRACTOR_IDS.map((extractor) => ({ + value: extractor, + label: copyExtractorLabel(extractor), + disabled: extractorUnavailableForDatabase(extractor, props.databaseType) || (extractor === "sql-updates" && !props.tableMeta?.primaryKeys.length), + separatorBefore: DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS[extractor].separatorBefore, + })), +); +const copyToolbarCapability = computed(() => ({ + label: copyExtractorLabel(selectedCopyExtractor.value), + tooltip: `${t("grid.copy")} (${shortcutMod}+C)`, + currentValue: selectedCopyExtractor.value, + disabled: !canCopyWithExtractor(selectedCopyExtractor.value), + items: [...extractorMenuItems.value, { value: "__configure__", label: t("grid.copyExtractorConfigure"), separatorBefore: true }], + onCopy: async () => { + await copyWithExtractor(selectedCopyExtractor.value); + }, + onSelect: (value) => { + if (value === "__configure__") { + extractorConfigOpen.value = true; + return; + } + settingsStore.updateEditorSettings({ dataGridCopyExtractor: value as DataGridCopyExtractorId }); + }, +})); + +function saveExtractorConfiguration(value: { extractor: DataGridCopyExtractorId; options: typeof settingsStore.editorSettings.dataGridExtractorOptions }) { + settingsStore.updateEditorSettings({ dataGridCopyExtractor: value.extractor, dataGridExtractorOptions: value.options }); +} + const pageSizeMenuItems = computed(() => pageSizeOptions.value.map((size) => ({ value: String(size), @@ -5252,6 +5338,7 @@ function showCellDetails(rowIndex: number, colIndex: number) { function showCellDetailsForVisibleCell(rowIndex: number, visibleColIdx: number, actualColIdx: number) { clearRowSelection(); + invalidateSyntheticContextSelection(); selectSingleCell(rowIndex, visibleColIdx); showCellDetails(rowIndex, actualColIdx); } @@ -5339,6 +5426,7 @@ function selectTransposeCell(rowIndex: number, actualColIdx: number, event: Mous contextHeaderColumnIndex.value = null; contextHeaderVisibleColIdx.value = null; clearRowSelection(); + invalidateSyntheticContextSelection(); if (event.shiftKey || event.metaKey || event.ctrlKey) { extendCellSelectionTo(rowIndex, visibleColIdx); } else { @@ -5355,6 +5443,7 @@ function showTransposeCellDetails(rowIndex: number, actualColIdx: number) { contextHeaderColumnIndex.value = null; contextHeaderVisibleColIdx.value = null; clearRowSelection(); + invalidateSyntheticContextSelection(); selectSingleCell(rowIndex, visibleColIdx); transposeRowIndex.value = rowIndex; showCellDetails(rowIndex, actualColIdx); @@ -5647,7 +5736,7 @@ function generateSelectionMenuItems(disabled: boolean): ContextMenuItem[] { function cutSelection() { if (!props.editable || !selectedRange.value) return; - copySelectionTsv(); + void copyWithExtractor("tsv"); const range = selectedRange.value; const allowDraftSelectionValue = selectedRangeTargetsOnlyDraftRow(); beginBatch(); @@ -5780,6 +5869,7 @@ function moveSelectedCell(rowDelta: number, colDelta: number): boolean { visibleColumnCount: visibleColumnIndexes.value.length, }); if (!nextPosition) return false; + invalidateSyntheticContextSelection(); selectSingleCell(nextPosition.rowIndex, nextPosition.colIndex); clearRowSelection(); if (showTranspose.value) transposeRowIndex.value = nextPosition.rowIndex; @@ -5996,15 +6086,7 @@ async function onGridKeydown(event: KeyboardEvent) { if (clipboardShortcut(event, "c")) { if (!hasCellSelection.value && !hasRowSelection.value) return; event.preventDefault(); - if (isTransposeMode.value && hasRowSelection.value) { - copyRow(); - return; - } - if (hasCellSelection.value) { - copySelectionTsv(); - } else { - copySelectedRowsTsv(); - } + await copyWithExtractor(selectedCopyExtractor.value); return; } if (clipboardShortcut(event, "a")) { @@ -6597,11 +6679,15 @@ function onCellContext(rowId: number, rowIndex: number, colIdx: number, visibleC contextHeaderVisibleColIdx.value = null; contextCell.value = { rowId, rowIndex, col: colIdx }; if (hasRowSelection.value && isRowSelected(rowId)) { + contextSelectionIsSynthetic.value = false; return; } clearRowSelection(); if (!cellIsSelected(rowIndex, visibleColIdx)) { selectSingleCell(rowIndex, visibleColIdx); + contextSelectionIsSynthetic.value = true; + } else { + contextSelectionIsSynthetic.value = false; } } @@ -7409,20 +7495,6 @@ function rowActionLabels() { }; } -function copyRowLabels() { - const count = multiRowCount.value; - return { - row: isMultiRow.value ? t("grid.copyRows", { count }) : t("grid.copyRow"), - insert: isMultiRow.value ? t("grid.copyRowsInsert", { count }) : t("grid.copyRowInsert"), - insertMerged: t("grid.copyRowsInsertMerged", { count }), - insertRowByRow: t("grid.copyRowsInsertRowByRow", { count }), - insertNoPk: isMultiRow.value ? t("grid.copyRowsInsertWithoutPrimaryKeys", { count }) : t("grid.copyRowInsertWithoutPrimaryKeys"), - insertNoPkMerged: t("grid.copyRowsInsertWithoutPrimaryKeysMerged", { count }), - insertNoPkRowByRow: t("grid.copyRowsInsertWithoutPrimaryKeysRowByRow", { count }), - update: isMultiRow.value ? t("grid.copyRowsUpdate", { count }) : t("grid.copyRowUpdate"), - }; -} - function filterSubmenu(): ContextMenuItem { return createDataGridFilterSubmenu({ label: t("grid.filter"), @@ -7443,73 +7515,48 @@ function filterSubmenu(): ContextMenuItem { }); } +function buildExtractorContextItems(): ContextMenuItem[] { + const items: ContextMenuItem[] = []; + for (const extractor of DATA_GRID_COPY_EXTRACTOR_IDS) { + const descriptor = DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS[extractor]; + if (descriptor.separatorBefore) { + items.push({ label: "", separator: true }); + } + const selected = extractor === selectedCopyExtractor.value; + items.push({ + label: `${selected ? "✓ " : ""}${copyExtractorLabel(extractor)}`, + action: () => { + // One-off copy as the chosen format; do NOT persist it as the default — + // the saved default stays controlled by the toolbar/settings dialog. + void copyWithExtractor(extractor); + }, + disabled: !canCopyWithExtractor(extractor), + }); + } + items.push({ label: "", separator: true }); + items.push({ + label: t("grid.copyExtractorConfigure"), + action: () => { + extractorConfigOpen.value = true; + }, + }); + return items; +} + function copySubmenu(): ContextMenuItem { - const labels = copyRowLabels(); const items: ContextMenuItem[] = []; if (contextColumn.value) { items.push({ label: t("grid.copyCell"), action: copyCell }); } - items.push({ label: labels.row, action: copyRow }); - if (hasRowSelection.value && selectedRowCount.value > 0) { - const singleRowSelected = selectedRowCount.value === 1; - items.push({ label: singleRowSelected ? t("grid.copySelectedRowTsv") : t("grid.copySelectedRowsTsv", { count: selectedRowCount.value }), action: copySelectedRowsTsv }); - items.push({ label: singleRowSelected ? t("grid.copySelectedRowTsvWithHeaders") : t("grid.copySelectedRowsTsvWithHeaders", { count: selectedRowCount.value }), action: copySelectedRowsTsvWithHeaders }); - } - if (isMultiRow.value) { - items.push({ label: labels.insertMerged, action: () => copyRowAsInsert("merged"), disabled: !canCopyRowAsInsert.value }); - items.push({ label: labels.insertRowByRow, action: () => copyRowAsInsert("row-by-row"), disabled: !canCopyRowAsInsert.value }); - } else { - items.push({ label: labels.insert, action: () => copyRowAsInsert(), disabled: !canCopyRowAsInsert.value }); - } - if (canCopyRowAsInsertWithoutPrimaryKeys.value) { - if (isMultiRow.value) { - items.push({ - label: labels.insertNoPkMerged, - action: () => copyRowAsInsertWithoutPrimaryKeys("merged"), - }); - items.push({ - label: labels.insertNoPkRowByRow, - action: () => copyRowAsInsertWithoutPrimaryKeys("row-by-row"), - }); - } else { - items.push({ - label: labels.insertNoPk, - action: () => copyRowAsInsertWithoutPrimaryKeys(), - }); - } - } - if (canCopyRowAsUpdate.value) { - items.push({ label: labels.update, action: copyRowAsUpdate }); - } + items.push({ label: isMultiRow.value ? t("grid.copyRows", { count: multiRowCount.value }) : t("grid.copyRow"), action: copyRow }); + items.push({ label: "", separator: true }); + items.push(...buildExtractorContextItems()); + items.push({ label: "", separator: true }); items.push({ label: t("grid.copyAll"), action: copyAll }); items.push({ label: t("grid.copyColumnNames"), action: openCopyAllColumnNamesDialog }); return { label: t("grid.copy"), icon: Copy, children: items }; } -function selectionSubmenu(): ContextMenuItem { - const insertItems: ContextMenuItem[] = - selectedCells.value.rows.length > 1 - ? [ - { label: t("grid.copySelectionInsertMerged"), action: () => void copySelectionAsInsert("merged"), disabled: () => !canCopySelectionAsInsert.value }, - { label: t("grid.copySelectionInsertRowByRow"), action: () => void copySelectionAsInsert("row-by-row"), disabled: () => !canCopySelectionAsInsert.value }, - ] - : [{ label: t("grid.copySelectionInsert"), action: () => void copySelectionAsInsert(), disabled: () => !canCopySelectionAsInsert.value }]; - return { - label: t("grid.selection"), - icon: SquareDashed, - children: [ - { label: t("grid.copySelectionTsv"), action: copySelectionTsv }, - { label: t("grid.copySelectionTsvWithHeaders"), action: copySelectionTsvWithHeaders }, - { label: t("grid.copySelectionCsv"), action: copySelectionCsv }, - { label: t("grid.copySelectionJson"), action: copySelectionJson }, - { label: t("grid.copySelectionSql"), action: copySelectionSqlInList }, - ...insertItems, - { label: "", separator: true }, - { label: t("grid.clearSelection"), action: clearCellSelection }, - ], - }; -} - function exportSubmenu(): ContextMenuItem { const items: ContextMenuItem[] = [ { label: t("grid.exportCsv"), action: exportCsv }, @@ -7629,7 +7676,7 @@ const gridContextMenuItems = computed(() => { actions: { cellDetails: openContextCellDetailDialog, columnDetails: openContextColumnDetailDialog, rowDetails: openContextRowDetailDialog, setNull: setSelectionNull, bulkEdit: openBulkEditDialog, transpose: openContextTranspose }, downloadItem: binaryDownloadSubmenu(contextCellDetail.value), copySubmenu: copySubmenu(), - selectionSubmenu: selectionSubmenu(), + clearSelectionItem: { label: t("grid.clearSelection"), action: clearCellSelection, icon: SquareDashed }, generateSubmenu: { label: t("grid.generateValue"), icon: WandSparkles, @@ -7737,6 +7784,7 @@ const gridContextMenuItems = computed(() => { :compact="compactDataGridToolbar" :refresh="refreshToolbarCapability" :auto-refresh="autoRefreshToolbarCapability" + :copy-data="copyToolbarCapability" :add-row="addRowToolbarCapability" :preview="previewToolbarCapability" :save="saveToolbarCapability" @@ -8566,8 +8614,8 @@ const gridContextMenuItems = computed(() => { >
@@ -9181,6 +9229,7 @@ const gridContextMenuItems = computed(() => { + @@ -9247,11 +9296,12 @@ const gridContextMenuItems = computed(() => { --data-grid-row-muted-bg: rgb(240, 240, 240); --data-grid-row-new-bg: rgb(243, 243, 243); --data-grid-row-deleted-bg: rgb(255, 244, 244); - --data-grid-cell-active-bg: rgb(232, 232, 232); + --data-grid-cell-active-bg: rgb(244, 248, 255); --data-grid-cell-dirty-bg: rgb(255, 248, 230); - --data-grid-cell-selected-bg: rgb(226, 226, 226); - --data-grid-cell-selected-dirty-bg: rgb(244, 229, 186); - --data-grid-cell-selected-border: rgb(90, 90, 90); + --data-grid-cell-selected-bg: rgb(239, 246, 255); + --data-grid-cell-selected-single-bg: rgb(191, 219, 254); + --data-grid-cell-selected-dirty-bg: rgb(235, 224, 184); + --data-grid-cell-selected-border: rgb(59, 130, 246); --data-grid-cell-hover-bg: rgb(245, 245, 245); --data-grid-cell-search-bg: rgb(253, 245, 184); --data-grid-cell-current-search-bg: rgba(253, 224, 71, 0.52); @@ -9260,8 +9310,8 @@ const gridContextMenuItems = computed(() => { --data-grid-row-number-new-bg: rgb(219, 244, 233); --data-grid-row-number-edited-bg: rgb(253, 241, 219); --data-grid-row-number-deleted-bg: rgb(255, 244, 244); - --data-grid-row-number-active-bg: rgb(232, 232, 232); - --data-grid-row-number-selected-bg: rgb(226, 226, 226); + --data-grid-row-number-active-bg: rgb(244, 248, 255); + --data-grid-row-number-selected-bg: rgb(191, 219, 254); --data-grid-scrollbar-thumb: color-mix(in oklch, var(--foreground) 30%, transparent); --data-grid-scrollbar-thumb-hover: color-mix(in oklch, var(--foreground) 48%, transparent); --data-grid-scrollbar-track: transparent; @@ -9273,11 +9323,12 @@ const gridContextMenuItems = computed(() => { --data-grid-row-muted-bg: rgb(40, 40, 43); --data-grid-row-new-bg: rgb(51, 51, 55); --data-grid-row-deleted-bg: rgb(55, 31, 32); - --data-grid-cell-active-bg: rgb(64, 64, 64); + --data-grid-cell-active-bg: rgb(25, 34, 46); --data-grid-cell-dirty-bg: rgb(94, 75, 26); - --data-grid-cell-selected-bg: rgb(66, 67, 70); - --data-grid-cell-selected-dirty-bg: rgb(94, 75, 26); - --data-grid-cell-selected-border: rgb(170, 170, 175); + --data-grid-cell-selected-bg: rgb(20, 40, 60); + --data-grid-cell-selected-single-bg: rgb(30, 64, 96); + --data-grid-cell-selected-dirty-bg: rgb(76, 66, 38); + --data-grid-cell-selected-border: rgb(96, 165, 250); --data-grid-cell-hover-bg: rgb(46, 47, 51); --data-grid-cell-search-bg: rgb(72, 57, 8); --data-grid-cell-current-search-bg: rgb(116, 87, 0); @@ -9286,8 +9337,8 @@ const gridContextMenuItems = computed(() => { --data-grid-row-number-new-bg: rgb(33, 45, 40); --data-grid-row-number-edited-bg: rgb(48, 41, 28); --data-grid-row-number-deleted-bg: rgb(55, 31, 32); - --data-grid-row-number-active-bg: rgb(64, 64, 64); - --data-grid-row-number-selected-bg: rgb(66, 67, 70); + --data-grid-row-number-active-bg: rgb(25, 34, 46); + --data-grid-row-number-selected-bg: rgb(30, 64, 96); --data-grid-scrollbar-thumb: rgb(82, 82, 91); --data-grid-scrollbar-thumb-hover: rgb(113, 113, 122); --data-grid-scrollbar-track: rgb(24, 24, 27); @@ -9299,17 +9350,16 @@ const gridContextMenuItems = computed(() => { --data-grid-row-muted-bg: color-mix(in oklab, var(--muted) 99%, var(--foreground)); --data-grid-row-new-bg: color-mix(in oklab, var(--primary) 5%, transparent); --data-grid-row-deleted-bg: color-mix(in oklab, var(--destructive) 5%, transparent); - --data-grid-cell-active-bg: color-mix(in oklab, var(--primary) 15%, transparent); --data-grid-cell-dirty-bg: color-mix(in oklab, rgb(240 177 0) 10%, transparent); - --data-grid-cell-selected-bg: color-mix(in oklab, var(--primary) 25%, transparent); - --data-grid-cell-selected-dirty-bg: color-mix(in oklab, rgb(234 181 50) 30%, color-mix(in oklab, var(--primary) 18%, transparent)); - --data-grid-cell-selected-border: color-mix(in oklab, var(--primary) 70%, transparent); + --data-grid-cell-selected-bg: color-mix(in oklab, rgb(59 130 246) 12%, var(--background)); + --data-grid-cell-selected-single-bg: color-mix(in oklab, rgb(59 130 246) 30%, var(--background)); + --data-grid-cell-selected-dirty-bg: color-mix(in oklab, rgb(234 181 50) 30%, color-mix(in oklab, rgb(59 130 246) 18%, var(--background))); + --data-grid-cell-selected-border: color-mix(in oklab, rgb(59 130 246) 75%, transparent); --data-grid-cell-hover-bg: color-mix(in oklab, var(--accent) 50%, transparent); --data-grid-row-number-new-bg: color-mix(in oklab, rgb(16 185 129) 15%, var(--background)); --data-grid-row-number-edited-bg: color-mix(in oklab, rgb(245 158 11) 15%, var(--background)); --data-grid-row-number-deleted-bg: color-mix(in oklab, var(--destructive) 15%, var(--background)); - --data-grid-row-number-active-bg: color-mix(in oklab, var(--primary) 15%, var(--background)); - --data-grid-row-number-selected-bg: color-mix(in oklab, var(--primary) 25%, var(--background)); + --data-grid-row-number-selected-bg: color-mix(in oklab, rgb(59 130 246) 30%, var(--background)); } } @@ -9339,14 +9389,15 @@ const gridContextMenuItems = computed(() => { } .data-grid-header-cell--selected { - background-color: rgb(209, 213, 219) !important; + background-color: var(--data-grid-cell-selected-single-bg) !important; } :global(.dark) [data-grid-root] { - --data-grid-cell-selected-bg: rgb(66, 67, 70); - --data-grid-cell-selected-dirty-bg: rgb(94, 75, 26); - --data-grid-cell-selected-border: rgb(170, 170, 175); - --data-grid-row-number-selected-bg: rgb(66, 67, 70); + --data-grid-cell-selected-bg: rgb(20, 40, 60); + --data-grid-cell-selected-single-bg: rgb(30, 64, 96); + --data-grid-cell-selected-dirty-bg: rgb(76, 66, 38); + --data-grid-cell-selected-border: rgb(96, 165, 250); + --data-grid-row-number-selected-bg: rgb(30, 64, 96); } [data-grid-root].data-grid--dark .data-grid-header-cell--selected, @@ -9355,7 +9406,7 @@ const gridContextMenuItems = computed(() => { :global(.dark) [data-grid-root] .data-grid-header-cell--selected, :global(.dark) [data-grid-root] .transpose-record-header-selected, :global(.dark) [data-grid-root] .transpose-record-header-active { - background-color: rgb(66, 67, 70) !important; + background-color: var(--data-grid-cell-selected-single-bg) !important; color: rgb(244, 244, 245) !important; } @@ -9792,8 +9843,6 @@ const gridContextMenuItems = computed(() => { .row-cell-selected { background-color: var(--data-grid-cell-selected-bg) !important; - outline: 1px solid var(--data-grid-cell-selected-border); - outline-offset: -1px; } .cell-dirty { @@ -9827,8 +9876,6 @@ const gridContextMenuItems = computed(() => { .row-cell-selected-dirty { background-color: var(--data-grid-cell-selected-dirty-bg) !important; - outline: 1px solid var(--data-grid-cell-selected-border); - outline-offset: -1px; } .data-grid-row-number.bg-emerald-500\/15 { @@ -9847,9 +9894,15 @@ const gridContextMenuItems = computed(() => { background-color: var(--data-grid-row-number-active-bg) !important; } +.data-grid-row-number--selected { + background-color: var(--data-grid-row-number-selected-bg) !important; + color: var(--foreground) !important; + box-shadow: inset 3px 0 0 var(--data-grid-cell-selected-border); +} + .cell-selected { color: hsl(var(--foreground)); - background-color: var(--data-grid-cell-selected-bg) !important; + background-color: var(--data-grid-cell-selected-single-bg) !important; } .cell-selected { @@ -9859,19 +9912,24 @@ const gridContextMenuItems = computed(() => { :global(.dark) [data-grid-root] .cell-selected, :global(.dark) [data-grid-root] .row-cell-selected { color: rgb(244, 244, 245) !important; - background-color: rgb(66, 67, 70) !important; +} + +:global(.dark) [data-grid-root] .cell-selected { + background-color: var(--data-grid-cell-selected-single-bg) !important; +} + +:global(.dark) [data-grid-root] .row-cell-selected { + background-color: var(--data-grid-cell-selected-bg) !important; } :global(.dark) [data-grid-root] .cell-selected-dirty, :global(.dark) [data-grid-root] .row-cell-selected-dirty { - background-color: rgb(94, 75, 26) !important; + background-color: var(--data-grid-cell-selected-dirty-bg) !important; } :global(.dark) [data-grid-root] .cell-selected, -:global(.dark) [data-grid-root] .row-cell-selected, -:global(.dark) [data-grid-root] .cell-selected-dirty, -:global(.dark) [data-grid-root] .row-cell-selected-dirty { - outline-color: rgb(170, 170, 175) !important; +:global(.dark) [data-grid-root] .cell-selected-dirty { + outline-color: var(--data-grid-cell-selected-border) !important; } .ddl-code :deep(.ddl-kw) { diff --git a/apps/desktop/src/components/grid/DataGridColumnHeader.vue b/apps/desktop/src/components/grid/DataGridColumnHeader.vue index 71687fca93..a87f919a3b 100644 --- a/apps/desktop/src/components/grid/DataGridColumnHeader.vue +++ b/apps/desktop/src/components/grid/DataGridColumnHeader.vue @@ -107,11 +107,11 @@ const emit = defineEmits<{ } .data-grid-header-cell--selected { - background-color: rgb(209, 213, 219) !important; + background-color: var(--data-grid-cell-selected-single-bg, rgb(191, 219, 254)) !important; } .data-grid-header-cell--dark.data-grid-header-cell--selected { - background-color: rgb(66, 67, 70) !important; + background-color: var(--data-grid-cell-selected-single-bg, rgb(30, 64, 96)) !important; color: rgb(244, 244, 245) !important; } diff --git a/apps/desktop/src/components/grid/DataGridExtractorDialog.vue b/apps/desktop/src/components/grid/DataGridExtractorDialog.vue new file mode 100644 index 0000000000..bb35b6d18a --- /dev/null +++ b/apps/desktop/src/components/grid/DataGridExtractorDialog.vue @@ -0,0 +1,229 @@ + + + diff --git a/apps/desktop/src/components/grid/DataGridToolbar.vue b/apps/desktop/src/components/grid/DataGridToolbar.vue index 74daf737c7..e476c9f715 100644 --- a/apps/desktop/src/components/grid/DataGridToolbar.vue +++ b/apps/desktop/src/components/grid/DataGridToolbar.vue @@ -1,6 +1,6 @@