feat(mongodb): support editing find query results
This commit is contained in:
8 files changed
+152
-23
No files matched your search
@@ -2499,13 +2499,17 @@ const canGoNextPage = computed(() => {
|
||||
const canJumpLastPage = computed(() => canGoNextPage.value && (hasKnownTotalRowCount.value || allRowsLoaded.value || !!props.tableMeta || !!props.countSql));
|
||||
const totalRowCountBusy = computed(() => props.totalRowCountLoading === true || manualTotalRowCountLoading.value);
|
||||
const canCalculateTotalRowCount = computed(() => !isResultsContext.value && !!props.connectionId && (!!props.tableMeta || !!props.countSql));
|
||||
const showQueryEditReadyBadge = computed(() => isResultsContext.value && hasData.value && !!props.editable && !!props.tableMeta);
|
||||
const showQueryEditReadyBadge = computed(() => isResultsContext.value && hasData.value && !!props.editable && (!!props.tableMeta || !!props.customSaveHandler));
|
||||
const queryEditReadyTargetLabel = computed(() => props.tableMeta?.tableName ?? props.customSaveHandler?.targetLabel ?? "");
|
||||
const showKeylessEditWarning = computed(() => !!props.editable && !!props.tableMeta && canUseKeylessRowPredicate(props.databaseType, props.tableMeta.primaryKeys ?? []));
|
||||
const canShowWhereSearch = computed(() => !!props.onExecuteSql && !isResultsContext.value);
|
||||
const canUseWhereSearch = computed(() => !!props.tableMeta && !!props.onExecuteSql && !isResultsContext.value);
|
||||
type DataGridTableMeta = NonNullable<typeof props.tableMeta>;
|
||||
const hiveTableTransactional = ref<boolean | undefined>(undefined);
|
||||
const canEditExistingRows = computed(() => !!props.customSaveHandler || canEditExistingTableRows(props.databaseType, hiveTableTransactional.value, props.tableMeta?.primaryKeys ?? []));
|
||||
const customReadonlyColumns = computed(() => new Set((props.customSaveHandler?.readonlyColumns ?? []).map((column) => column.toLowerCase())));
|
||||
const canInsertRows = computed(() => !props.customSaveHandler || props.customSaveHandler.canInsert !== false);
|
||||
const canDeleteRows = computed(() => !props.customSaveHandler || props.customSaveHandler.canDelete !== false);
|
||||
watch(
|
||||
() => [props.databaseType, props.connectionId, props.database, props.tableMeta?.schema, props.tableMeta?.tableName],
|
||||
async () => {
|
||||
@@ -2909,8 +2913,9 @@ function canEditRowItem(item: RowItem | undefined): boolean {
|
||||
|
||||
function canEditCellItem(item: RowItem | undefined, columnIndex: number): boolean {
|
||||
if (!canEditRowItem(item) || !canEditColumn(columnIndex)) return false;
|
||||
const column = props.result.columns[columnIndex] ?? "";
|
||||
if (customReadonlyColumns.value.has(column.toLowerCase())) return false;
|
||||
if (!item?.isNew) {
|
||||
const column = props.result.columns[columnIndex] ?? "";
|
||||
const sourceColumn = props.sourceColumns?.[columnIndex] ?? column;
|
||||
if (isClickHouseExistingRowReadonlyColumn(props.databaseType, sourceColumn, props.tableMeta?.primaryKeys ?? [], props.tableMeta?.columns ?? [])) return false;
|
||||
if (isTdengineExistingRowReadonlyColumn(props.databaseType, column, props.tableMeta?.columns ?? [])) return false;
|
||||
@@ -3010,7 +3015,7 @@ function isDecimalColumnType(dataType: string): boolean {
|
||||
}
|
||||
|
||||
function canDeleteRowItem(item: RowItem | undefined): boolean {
|
||||
return !!props.editable && !!item && !item.isDeleted && (item.isNew || canEditExistingRows.value);
|
||||
return !!props.editable && canDeleteRows.value && !!item && !item.isDeleted && (item.isNew || canEditExistingRows.value);
|
||||
}
|
||||
|
||||
function resetInfiniteScrollState() {
|
||||
@@ -3050,6 +3055,7 @@ function onToolbarRollback() {
|
||||
}
|
||||
|
||||
function addRow() {
|
||||
if (!canInsertRows.value) return;
|
||||
addEditorRow();
|
||||
focusAppendedTransposeRecord();
|
||||
}
|
||||
@@ -4441,7 +4447,7 @@ function onCanvasContext(event: MouseEvent) {
|
||||
}
|
||||
const actualColIdx = visibleColumnIndexes.value[hit.visibleColIdx];
|
||||
if (actualColIdx === undefined) return;
|
||||
onCellContext(item.id, item.displayIndex, actualColIdx, hit.visibleColIdx);
|
||||
onCellContext(item.id, item.displayIndex, actualColIdx, hit.visibleColIdx, event);
|
||||
}
|
||||
|
||||
function onCanvasDblClick(event: MouseEvent) {
|
||||
@@ -5355,7 +5361,7 @@ async function onGridKeydown(event: KeyboardEvent) {
|
||||
return;
|
||||
}
|
||||
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key.toLowerCase() === "n") {
|
||||
if (props.editable && (props.tableMeta || props.customSaveHandler)) {
|
||||
if (props.editable && (props.tableMeta || props.customSaveHandler) && canInsertRows.value) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
addRow();
|
||||
@@ -5981,7 +5987,13 @@ async function copyAlterColumnSql() {
|
||||
toast(t("grid.copyAlterSqlFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
function onCellContext(rowId: number, rowIndex: number, colIdx: number, visibleColIdx: number) {
|
||||
function clearNativeTextSelection() {
|
||||
window.getSelection()?.removeAllRanges();
|
||||
}
|
||||
|
||||
function onCellContext(rowId: number, rowIndex: number, colIdx: number, visibleColIdx: number, event?: MouseEvent) {
|
||||
event?.preventDefault();
|
||||
clearNativeTextSelection();
|
||||
contextHeaderColumn.value = null;
|
||||
contextHeaderColumnIndex.value = null;
|
||||
contextCell.value = { rowId, rowIndex, col: colIdx };
|
||||
@@ -6806,11 +6818,13 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
if (props.editable && contextRowItem.value) {
|
||||
const labels = rowActionLabels();
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({
|
||||
label: labels.clone,
|
||||
action: () => (isMultiRow.value ? cloneRows(affectedRowIds()) : cloneRow(contextRowItem.value!.id)),
|
||||
icon: CopyPlus,
|
||||
});
|
||||
if (canInsertRows.value) {
|
||||
items.push({
|
||||
label: labels.clone,
|
||||
action: () => (isMultiRow.value ? cloneRows(affectedRowIds()) : cloneRow(contextRowItem.value!.id)),
|
||||
icon: CopyPlus,
|
||||
});
|
||||
}
|
||||
if (contextRowItem.value.isDeleted) {
|
||||
items.push({
|
||||
label: labels.restore,
|
||||
@@ -7163,7 +7177,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" class="max-w-sm">
|
||||
{{ t("grid.queryEditReadyHint", { table: tableMeta?.tableName }) }}
|
||||
{{ t("grid.queryEditReadyHint", { table: queryEditReadyTargetLabel }) }}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="showKeylessEditWarning">
|
||||
@@ -7216,7 +7230,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{{ t("grid.goToColumn") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="editable && (tableMeta || customSaveHandler)">
|
||||
<Tooltip v-if="editable && (tableMeta || customSaveHandler) && canInsertRows">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" :class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5', compactDataGridToolbar ? 'data-grid-topbar-action-button--compact' : '']" @click="addRow">
|
||||
<Plus class="data-grid-topbar-action-icon w-3 h-3" />
|
||||
@@ -7999,7 +8013,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
@mouseleave="onCellMouseleave(item.displayIndex, col.actualColIdx)"
|
||||
@dblclick="canEditCellItem(item, col.actualColIdx) && startDomCellEdit(item.id, col.actualColIdx, formatCellCached(item.data[col.actualColIdx], col.actualColIdx), $event)"
|
||||
:data-visible-col-index="col.visibleColIdx"
|
||||
@contextmenu="onCellContext(item.id, item.displayIndex, col.actualColIdx, col.visibleColIdx)"
|
||||
@contextmenu="onCellContext(item.id, item.displayIndex, col.actualColIdx, col.visibleColIdx, $event)"
|
||||
>
|
||||
<template v-if="editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx">
|
||||
<TemporalCellEditor v-if="temporalEditorKindForColumn(col.actualColIdx)" v-model="editValue" :kind="temporalEditorKindForColumn(col.actualColIdx)!" @cancel="cancelEdit" @commit="commitGridEdit" />
|
||||
|
||||
@@ -60,9 +60,12 @@ import { tableMetaForDataTab } from "@/lib/tableDataTabMeta";
|
||||
import { formatShortcut } from "@/lib/shortcutRegistry";
|
||||
import { effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect";
|
||||
import { chartableColumnIndexes } from "@/lib/chartData";
|
||||
import * as api from "@/lib/api";
|
||||
import { buildMongoUpdateDocument, formatMongoShellLiteral, type MongoInputValue } from "@/lib/mongoDocumentValues";
|
||||
import type { SqlExecutionOverride } from "@/lib/sqlExecutionTarget";
|
||||
import type { DataGridSortMode } from "@/lib/dataGridSort";
|
||||
import { useTabScroll } from "@/composables/useTabScroll";
|
||||
import type { CustomSaveHandler } from "@/composables/useDataGridEditor";
|
||||
import type { QueryTab, ConnectionConfig, TableInfoTab, TreeNode, VectorCollectionMeta } from "@/types/database";
|
||||
import { sqlFormatDialectForDbType, type SqlFormatDialect } from "@/lib/sqlFormatter";
|
||||
|
||||
@@ -279,6 +282,60 @@ const hasTabularResult = computed(() => {
|
||||
return visibleResultItems.value.length > 0;
|
||||
});
|
||||
const canShowResultOutput = computed(() => hasTabularResult.value || props.activeTab.isExecuting);
|
||||
type MongoQueryGridChanges = {
|
||||
dirtyRows: Map<number, Map<number, MongoInputValue>>;
|
||||
deletedRows: Set<number>;
|
||||
newRows: MongoInputValue[][];
|
||||
columns: string[];
|
||||
rows: MongoInputValue[][];
|
||||
};
|
||||
function mongoIdPreview(val: unknown): string {
|
||||
if (val === null || val === undefined) return "null";
|
||||
if (typeof val === "string" && /^[a-fA-F0-9]{24}$/.test(val)) return `ObjectId("${val}")`;
|
||||
return formatMongoShellLiteral(val);
|
||||
}
|
||||
function mongoCollectionExpression(collection: string): string {
|
||||
return `db.getCollection(${JSON.stringify(collection)})`;
|
||||
}
|
||||
const mongoQueryResultSaveHandler = computed<CustomSaveHandler | undefined>(() => {
|
||||
const tab = props.activeTab;
|
||||
const target = tab.mongoEditTarget;
|
||||
if (tab.mode !== "query" || activeEffectiveDatabaseType.value !== "mongodb" || !target || !tab.connectionId || !tab.database || !tab.result) return undefined;
|
||||
if (!tab.result.columns.includes(target.idColumn)) return undefined;
|
||||
|
||||
const save: CustomSaveHandler["save"] = async (changes: MongoQueryGridChanges) => {
|
||||
if (changes.newRows.length > 0 || changes.deletedRows.size > 0) {
|
||||
throw new Error("MongoDB query result editing only supports updating existing rows.");
|
||||
}
|
||||
const idColIdx = changes.columns.indexOf(target.idColumn);
|
||||
if (idColIdx < 0) throw new Error("No _id column");
|
||||
for (const [rowIdx, dirtyCols] of changes.dirtyRows) {
|
||||
const row = changes.rows[rowIdx];
|
||||
const id = row?.[idColIdx];
|
||||
if (id === null || id === undefined || String(id).trim() === "") continue;
|
||||
const updateDoc = buildMongoUpdateDocument(dirtyCols, changes.columns);
|
||||
if (Object.keys(updateDoc).length === 0) continue;
|
||||
await api.mongoUpdateDocument(tab.connectionId, tab.database, target.collection, String(id), JSON.stringify(updateDoc));
|
||||
}
|
||||
};
|
||||
|
||||
const preview: CustomSaveHandler["preview"] = async (changes: MongoQueryGridChanges) => {
|
||||
const idColIdx = changes.columns.indexOf(target.idColumn);
|
||||
if (idColIdx < 0) return [];
|
||||
const stmts: string[] = [];
|
||||
for (const [rowIdx, dirtyCols] of changes.dirtyRows) {
|
||||
const row = changes.rows[rowIdx];
|
||||
const id = row?.[idColIdx];
|
||||
if (id === null || id === undefined || String(id).trim() === "") continue;
|
||||
const updateDoc = buildMongoUpdateDocument(dirtyCols, changes.columns);
|
||||
if (Object.keys(updateDoc).length === 0) continue;
|
||||
stmts.push(`${mongoCollectionExpression(target.collection)}.updateOne({_id: ${mongoIdPreview(id)}}, ${formatMongoShellLiteral(updateDoc)})`);
|
||||
}
|
||||
return stmts;
|
||||
};
|
||||
|
||||
return { save, preview, canInsert: false, canDelete: false, readonlyColumns: [target.idColumn], targetLabel: target.collection };
|
||||
});
|
||||
const resultsPaneOpen = ref(false);
|
||||
const resultsPaneSize = ref(Number(safeLocalStorageGet("dbx-results-pane-size")) || DEFAULT_QUERY_RESULTS_PANE_SIZE);
|
||||
const editorPaneSize = computed(() => (resultsPaneOpen.value ? 100 - resultsPaneSize.value : 100));
|
||||
@@ -821,8 +878,9 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
|
||||
:initial-order-by-input="activeTab.orderByInput"
|
||||
:sql="activeTab.lastExecutedSql || activeTab.sql"
|
||||
:loading="activeTab.isExecuting"
|
||||
:editable="!!activeTab.queryAnalysis"
|
||||
:editable="!!activeTab.queryAnalysis || !!mongoQueryResultSaveHandler"
|
||||
:source-columns="activeTab.querySourceColumns"
|
||||
:custom-save-handler="mongoQueryResultSaveHandler"
|
||||
context="results"
|
||||
:database-type="activeEffectiveDatabaseType"
|
||||
:connection-id="activeTab.connectionId"
|
||||
|
||||
Reference in new issue
Block a user