feat(data-grid): add multi-format copy extractors

This commit is contained in:
soddygo authored and GitHub committed 2026-07-27 16:11:53 +08:00
1 parent 166027600f
commit 0b7a886cfd
60 files changed
+4615 -1766

No files matched your search

+213 -155
View File
@@ -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<string | null>(null);
const contextHeaderColumnIndex = ref<number | null>(null);
const contextHeaderVisibleColIdx = ref<number | null>(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<HTMLElement>("[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<DataGridCopyExtractorId, string> = {
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<DataGridToolbarCopyCapability>(() => ({
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<ContextMenuItem[]>(() => {
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<ContextMenuItem[]>(() => {
:compact="compactDataGridToolbar"
:refresh="refreshToolbarCapability"
:auto-refresh="autoRefreshToolbarCapability"
:copy-data="copyToolbarCapability"
:add-row="addRowToolbarCapability"
:preview="previewToolbarCapability"
:save="saveToolbarCapability"
@@ -8566,8 +8614,8 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
>
<div
class="data-grid-row-number w-(--row-num-w) shrink-0 px-2 py-1 border-r text-center select-none cursor-default sticky left-0 z-10"
:class="rowNumberStatusClass(item)"
@click="handleRowClick(item.displayIndex, item.id, $event)"
:class="[rowNumberStatusClass(item), { 'data-grid-row-number--selected': isRowSelected(item.id) }]"
@mousedown="beginRowSelection(item.displayIndex, item.id, $event)"
@dblclick.stop="toggleTranspose(item.displayIndex)"
@contextmenu="onRowContext(item.id, item.displayIndex)"
>
@@ -9181,6 +9229,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<DataGridBulkEditDialog v-if="bulkEditDialogMounted" v-model:open="bulkEditDialogOpen" v-model:value="bulkEditValue" :selected-cell-count="selectedCellCount" @apply="applyBulkEditValue" />
<DataGridExtractorDialog v-model:open="extractorConfigOpen" :extractor="selectedCopyExtractor" :options="settingsStore.editorSettings.dataGridExtractorOptions" :items="extractorMenuItems" :preview="previewWithExtractor" @save="saveExtractorConfiguration" />
<DataGridCopyColumnNamesDialog v-if="copyColumnNamesDialogMounted" v-model:open="copyColumnNamesDialogOpen" :column-names="copyColumnNamesDialogColumns" :database-type="resolvedDatabaseType" @copy="copyText" />
<Dialog v-model:open="generateIncrementDialogOpen">
@@ -9247,11 +9296,12 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
--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<ContextMenuItem[]>(() => {
--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<ContextMenuItem[]>(() => {
--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<ContextMenuItem[]>(() => {
--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<ContextMenuItem[]>(() => {
--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<ContextMenuItem[]>(() => {
}
.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<ContextMenuItem[]>(() => {
: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<ContextMenuItem[]>(() => {
.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<ContextMenuItem[]>(() => {
.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<ContextMenuItem[]>(() => {
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<ContextMenuItem[]>(() => {
: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) {
@@ -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;
}
@@ -0,0 +1,229 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS, normalizeDataGridExtractorOptions, validateDataGridExtractorOptions, type DataGridCopyExtractorId, type DataGridExtractorOptions, type DataGridExtractPreview, type DataGridExtractWarningCode } from "@/lib/dataGrid/dataGridCopyExtractor";
const props = defineProps<{
open: boolean;
extractor: DataGridCopyExtractorId;
options: DataGridExtractorOptions;
items: Array<{ value: DataGridCopyExtractorId; label: string; disabled?: boolean }>;
preview: (extractor: DataGridCopyExtractorId, options: DataGridExtractorOptions) => Promise<DataGridExtractPreview>;
}>();
const emit = defineEmits<{
"update:open": [value: boolean];
save: [value: { extractor: DataGridCopyExtractorId; options: DataGridExtractorOptions }];
}>();
const { t } = useI18n();
const draftExtractor = ref<DataGridCopyExtractorId>(props.extractor);
const draftOptions = ref<DataGridExtractorOptions>(normalizeDataGridExtractorOptions(props.options));
const previewText = ref("");
const previewError = ref("");
const previewWarnings = ref<string[]>([]);
const previewLoading = ref(false);
const previewTruncatedRows = ref(0);
let previewSequence = 0;
let previewTimer: ReturnType<typeof setTimeout> | undefined;
const extractorCategory = computed(() => DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS[draftExtractor.value].category);
const isDsv = computed(() => extractorCategory.value === "delimited");
const isSql = computed(() => draftExtractor.value === "sql-inserts" || draftExtractor.value === "sql-updates");
const isJson = computed(() => extractorCategory.value === "json");
const optionsError = computed(() => {
const code = validateDataGridExtractorOptions(draftExtractor.value, draftOptions.value);
if (!code) return "";
return t(`grid.copyExtractorValidation.${code}`);
});
function visibleSeparator(value: string): string {
return value.replace(/\t/g, "\\t").replace(/\r/g, "\\r").replace(/\n/g, "\\n");
}
function separatorValue(value: string): string {
return value.replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\n/g, "\n");
}
function updateDsv<K extends keyof DataGridExtractorOptions["dsv"]>(key: K, value: DataGridExtractorOptions["dsv"][K]) {
draftOptions.value = { ...draftOptions.value, dsv: { ...draftOptions.value.dsv, [key]: value } };
}
function updateSql<K extends keyof DataGridExtractorOptions["sql"]>(key: K, value: DataGridExtractorOptions["sql"][K]) {
draftOptions.value = { ...draftOptions.value, sql: { ...draftOptions.value.sql, [key]: value } };
}
async function refreshPreview() {
const sequence = ++previewSequence;
previewLoading.value = true;
previewError.value = "";
previewWarnings.value = [];
previewTruncatedRows.value = 0;
if (optionsError.value) {
previewError.value = optionsError.value;
previewLoading.value = false;
return;
}
try {
const result = await props.preview(draftExtractor.value, normalizeDataGridExtractorOptions(draftOptions.value));
if (sequence === previewSequence) {
previewText.value = result.text;
previewWarnings.value = (result.warnings ?? []).map((warning) => warningText(warning.code, result.omittedColumns));
previewTruncatedRows.value = result.truncated ? result.sourceRowCount : 0;
}
} catch (error: unknown) {
if (sequence === previewSequence) {
previewText.value = "";
previewError.value = error instanceof Error ? error.message : String(error);
}
} finally {
if (sequence === previewSequence) previewLoading.value = false;
}
}
function warningText(code: DataGridExtractWarningCode, omittedColumns: string[] | undefined): string {
return code === "omitted-columns" ? t("grid.copyExtractorWarningOmittedColumns", { columns: omittedColumns?.join(", ") || "-" }) : t("grid.copyExtractorWarningDuplicateJsonColumns");
}
function schedulePreview() {
if (previewTimer) clearTimeout(previewTimer);
previewTimer = setTimeout(() => void refreshPreview(), 180);
}
watch(
() => props.open,
(open) => {
if (!open) {
if (previewTimer) clearTimeout(previewTimer);
previewTimer = undefined;
previewSequence += 1;
previewLoading.value = false;
return;
}
draftExtractor.value = props.extractor;
draftOptions.value = normalizeDataGridExtractorOptions(props.options);
schedulePreview();
},
);
watch(
[draftExtractor, draftOptions],
() => {
if (props.open) schedulePreview();
},
{ deep: true },
);
onBeforeUnmount(() => {
if (previewTimer) clearTimeout(previewTimer);
previewSequence += 1;
});
function save() {
if (optionsError.value) return;
emit("save", { extractor: draftExtractor.value, options: normalizeDataGridExtractorOptions(draftOptions.value) });
emit("update:open", false);
}
</script>
<template>
<Dialog :open="open" @update:open="emit('update:open', $event)">
<DialogContent class="max-h-[85vh] overflow-y-auto sm:max-w-[760px]">
<DialogHeader
><DialogTitle>{{ t("grid.copyExtractorConfigure") }}</DialogTitle></DialogHeader
>
<div class="grid gap-4 md:grid-cols-[260px_minmax(0,1fr)]">
<div class="space-y-3">
<div class="space-y-1.5">
<Label>{{ t("grid.copyExtractorFormat") }}</Label>
<Select v-model="draftExtractor">
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem v-for="item in items" :key="item.value" :value="item.value" :disabled="item.disabled">{{ item.label }}</SelectItem>
</SelectContent>
</Select>
</div>
<template v-if="isDsv">
<div v-if="draftExtractor === 'dsv'" class="space-y-1.5">
<Label>{{ t("grid.copyExtractorColumnSeparator") }}</Label>
<Input :model-value="visibleSeparator(draftOptions.dsv.columnSeparator)" @update:model-value="updateDsv('columnSeparator', separatorValue(String($event)))" />
</div>
<div v-if="draftExtractor !== 'one-row'" class="space-y-1.5">
<Label>{{ t("grid.copyExtractorRowSeparator") }}</Label>
<Input :model-value="visibleSeparator(draftOptions.dsv.rowSeparator)" @update:model-value="updateDsv('rowSeparator', separatorValue(String($event)))" />
</div>
<div class="space-y-1.5">
<Label>{{ t("grid.copyExtractorNullText") }}</Label>
<Input :model-value="draftOptions.dsv.nullText" @update:model-value="updateDsv('nullText', String($event))" />
</div>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1.5">
<Label>{{ t("grid.copyExtractorQuote") }}</Label>
<Input maxlength="1" :model-value="draftOptions.dsv.quote" @update:model-value="updateDsv('quote', String($event))" />
</div>
<div class="space-y-1.5">
<Label>{{ t("grid.copyExtractorQuotePolicy") }}</Label>
<Select :model-value="draftOptions.dsv.quotePolicy" @update:model-value="updateDsv('quotePolicy', $event as DataGridExtractorOptions['dsv']['quotePolicy'])">
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="minimal">{{ t("grid.copyExtractorQuoteMinimal") }}</SelectItem>
<SelectItem value="always">{{ t("grid.copyExtractorQuoteAlways") }}</SelectItem>
<SelectItem value="never">{{ t("grid.copyExtractorQuoteNever") }}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<label v-if="draftExtractor === 'dsv'" class="flex items-center gap-2 text-sm"
><input type="checkbox" :checked="draftOptions.dsv.includeColumnHeader" @change="updateDsv('includeColumnHeader', ($event.target as HTMLInputElement).checked)" />{{ t("grid.copyExtractorIncludeHeader") }}</label
>
<label v-if="draftExtractor !== 'one-row'" class="flex items-center gap-2 text-sm"
><input type="checkbox" :checked="draftOptions.dsv.includeRowHeader" @change="updateDsv('includeRowHeader', ($event.target as HTMLInputElement).checked)" />{{ t("grid.copyExtractorIncludeRowHeader") }}</label
>
</template>
<template v-if="isSql">
<label class="flex items-center gap-2 text-sm"><input type="checkbox" :checked="draftOptions.sql.skipComputedColumns" @change="updateSql('skipComputedColumns', ($event.target as HTMLInputElement).checked)" />{{ t("grid.copyExtractorSkipComputed") }}</label>
<label class="flex items-center gap-2 text-sm"><input type="checkbox" :checked="draftOptions.sql.skipGeneratedColumns" @change="updateSql('skipGeneratedColumns', ($event.target as HTMLInputElement).checked)" />{{ t("grid.copyExtractorSkipGenerated") }}</label>
<label v-if="draftExtractor === 'sql-inserts'" class="flex items-center gap-2 text-sm"
><input type="checkbox" :checked="draftOptions.sql.excludePrimaryKeysFromInsert" @change="updateSql('excludePrimaryKeysFromInsert', ($event.target as HTMLInputElement).checked)" />{{ t("grid.copyExtractorExcludePrimaryKeys") }}</label
>
<div v-if="draftExtractor === 'sql-inserts'" class="space-y-1.5">
<Label>{{ t("grid.copyExtractorInsertMode") }}</Label>
<Select :model-value="draftOptions.sql.insertMode" @update:model-value="updateSql('insertMode', $event as DataGridExtractorOptions['sql']['insertMode'])">
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="merged">{{ t("grid.copyExtractorInsertMerged") }}</SelectItem>
<SelectItem value="row-by-row">{{ t("grid.copyExtractorInsertRowByRow") }}</SelectItem>
</SelectContent>
</Select>
</div>
</template>
<label v-if="isJson" class="flex items-center gap-2 text-sm"><input type="checkbox" :checked="draftOptions.json.pretty" @change="draftOptions = { ...draftOptions, json: { pretty: ($event.target as HTMLInputElement).checked } }" />{{ t("grid.copyExtractorPrettyJson") }}</label>
</div>
<div class="min-w-0 space-y-1.5">
<Label>{{ t("grid.copyExtractorPreview") }}</Label>
<div class="relative min-h-72 rounded-md border bg-muted/30">
<pre class="max-h-[55vh] overflow-auto whitespace-pre p-3 font-mono text-xs">{{ previewError || previewText }}</pre>
<span v-if="previewLoading" class="absolute right-2 top-2 text-xs text-muted-foreground">{{ t("common.loading") }}</span>
</div>
<p v-if="previewTruncatedRows" class="text-xs text-muted-foreground">{{ t("grid.copyExtractorPreviewTruncated", { count: previewTruncatedRows }) }}</p>
<p v-for="warning in previewWarnings" :key="warning" class="text-xs text-amber-700 dark:text-amber-300">{{ warning }}</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="emit('update:open', false)">{{ t("common.cancel") }}</Button>
<Button :disabled="!!optionsError" @click="save">{{ t("common.save") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from "vue";
import { Check, Eye, Loader2, Plus, RefreshCcw, RotateCcw, Rows3, Save, TableProperties, Timer, Upload } from "@lucide/vue";
import { Check, ChevronDown, Copy, Eye, Loader2, Plus, RefreshCcw, RotateCcw, Rows3, Save, TableProperties, Timer, Upload } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
@@ -9,11 +9,14 @@ import {
isDataGridToolbarCapabilityDisabled,
isDataGridToolbarCapabilityVisible,
selectDataGridToolbarAutoRefreshInterval,
selectDataGridToolbarCopyItem,
selectDataGridToolbarExportItem,
toggleDataGridToolbarAutoRefresh,
triggerDataGridToolbarCopy,
triggerDataGridToolbarAction,
type DataGridToolbarActionCapability,
type DataGridToolbarAutoRefreshCapability,
type DataGridToolbarCopyCapability,
type DataGridToolbarExportCapability,
type DataGridToolbarSaveCapability,
} from "@/lib/dataGrid/dataGridToolbar";
@@ -23,6 +26,7 @@ const props = defineProps<{
refresh: DataGridToolbarActionCapability;
autoRefresh?: DataGridToolbarAutoRefreshCapability;
addRow?: DataGridToolbarActionCapability;
copyData?: DataGridToolbarCopyCapability;
exportData?: DataGridToolbarExportCapability;
transpose?: DataGridToolbarActionCapability;
tableInfo?: DataGridToolbarActionCapability;
@@ -85,6 +89,35 @@ function actionLabelClass() {
<slot name="navigation" />
<div v-if="isDataGridToolbarCapabilityVisible(copyData)" class="flex h-5 shrink-0 items-stretch overflow-hidden rounded-md border border-border">
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="sm" class="h-5 rounded-none border-0 px-1.5 text-xs" :disabled="copyData?.disabled" @click="void triggerDataGridToolbarCopy(copyData)">
<Copy class="h-3 w-3" :class="compact ? '' : 'mr-1'" />
<span v-if="!compact">{{ copyData?.label }}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{{ copyData?.tooltip ?? copyData?.label }}</TooltipContent>
</Tooltip>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="icon" class="h-5 w-5 rounded-none border-0 border-l border-border" :aria-label="copyData?.label">
<ChevronDown class="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="min-w-52">
<template v-for="item in copyData?.items ?? []" :key="item.value">
<DropdownMenuSeparator v-if="item.separatorBefore" />
<DropdownMenuItem class="gap-2" :disabled="item.disabled" @select="void selectDataGridToolbarCopyItem(copyData, item.value)">
<Check v-if="copyData?.currentValue === item.value" class="h-3.5 w-3.5" />
<span v-else class="h-3.5 w-3.5" />
{{ item.label }}
</DropdownMenuItem>
</template>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Tooltip v-if="isDataGridToolbarCapabilityVisible(addRow)">
<TooltipTrigger as-child>
<Button variant="ghost" size="sm" :class="actionButtonClass" :disabled="isDataGridToolbarCapabilityDisabled(addRow)" @click="void triggerDataGridToolbarAction(addRow)">
@@ -268,7 +268,7 @@ async function resendMessage() {
resendSuccess.value = undefined;
try {
const message = selectedMessage.value;
const headers: Record<string, string> = { ...(message?.headers ?? {}) };
const headers: Record<string, string> = { ...message?.headers };
if (isRocketMqCluster.value && message?.tag?.trim()) {
headers.TAGS = message.tag.trim();
}