feat(data-grid): add multi-format copy extractors
This commit is contained in:
1 parent
166027600f
commit
0b7a886cfd
60 files changed
+4615
-1766
No files matched your search
@@ -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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user