diff --git a/apps/desktop/src/components/document/DocumentBrowser.vue b/apps/desktop/src/components/document/DocumentBrowser.vue index 162964e86..87b6c6386 100644 --- a/apps/desktop/src/components/document/DocumentBrowser.vue +++ b/apps/desktop/src/components/document/DocumentBrowser.vue @@ -9,6 +9,7 @@ import { Input } from "@/components/ui/input"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue"; +import ErrorBanner from "@/components/ui/ErrorBanner.vue"; import DataGrid from "@/components/grid/DataGrid.vue"; import * as api from "@/lib/api"; import { clampSearchSplitWidth } from "@/lib/dataGridSearchSplit"; @@ -18,6 +19,7 @@ import { useSettingsStore } from "@/stores/settingsStore"; import JsonEditNode from "./JsonEditNode.vue"; import type { EditNode } from "@/types/editor"; import type { DatabaseType, QueryResult } from "@/types/database"; +import type { CustomSaveHandler } from "@/composables/useDataGridEditor"; import { Splitpanes, Pane } from "splitpanes"; import "splitpanes/dist/splitpanes.css"; @@ -256,6 +258,110 @@ async function gridSave(changes: { dirtyRows: Map, columns: string[]): Record { + const setFields: Record = {}; + const unsetFields: Record = {}; + for (const [colIdx, newVal] of changes) { + const col = columns[colIdx]; + if (!col || col === "_id") continue; + if (newVal === null) { + unsetFields[col] = ""; + } else if (typeof newVal === "string") { + try { + setFields[col] = JSON.parse(newVal); + } catch { + setFields[col] = newVal; + } + } else { + setFields[col] = newVal; + } + } + const doc: Record = {}; + if (Object.keys(setFields).length > 0) doc.$set = setFields; + if (Object.keys(unsetFields).length > 0) doc.$unset = unsetFields; + return doc; +} + +function buildNewDoc(newRow: (string | number | boolean | null)[], columns: string[]): Record { + const doc: Record = {}; + for (let ci = 0; ci < columns.length; ci++) { + const col = columns[ci]; + if (!col || col === "_id") continue; + const val = newRow[ci]; + if (val === null) continue; + if (typeof val === "string") { + try { + doc[col] = JSON.parse(val); + } catch { + doc[col] = val; + } + } else { + doc[col] = val; + } + } + return doc; +} + +async function previewDocumentChanges(changes: { dirtyRows: Map>; deletedRows: Set; newRows: (string | number | boolean | null)[][]; columns: string[]; rows: (string | number | boolean | null)[][] }): Promise { + const { dirtyRows, deletedRows, newRows, columns, rows } = changes; + const idColIdx = columns.indexOf("_id"); + const stmts: string[] = []; + const coll = props.collection; + const isEs = documentStoreProvider.value.kind === "elasticsearch"; + + for (const [rowIdx, dirtyCols] of dirtyRows) { + const row = rows[rowIdx]; + const id = row?.[idColIdx]; + if (id == null) continue; + const updateDoc = buildUpdateDoc(dirtyCols, columns); + if (isEs) { + stmts.push(`POST /${coll}/_update/${JSON.stringify(String(id))}\n${JSON.stringify({ doc: updateDoc.$set ?? updateDoc }, null, 2)}`); + } else { + stmts.push(`db.${coll}.updateOne({_id: ${mongoIdPreview(id)}}, ${JSON.stringify(updateDoc)})`); + } + } + + for (const rowIdx of deletedRows) { + const row = rows[rowIdx]; + const id = row?.[idColIdx]; + if (id == null) continue; + if (isEs) { + stmts.push(`DELETE /${coll}/_doc/${JSON.stringify(String(id))}`); + } else { + stmts.push(`db.${coll}.deleteOne({_id: ${mongoIdPreview(id)}})`); + } + } + + for (const newRow of newRows) { + const doc = buildNewDoc(newRow, columns); + if (isEs) { + stmts.push(`POST /${coll}/_doc\n${JSON.stringify(doc, null, 2)}`); + } else { + stmts.push(`db.${coll}.insertOne(${JSON.stringify(doc)})`); + } + } + + return stmts; +} + +const customSaveHandler = computed(() => ({ + save: gridSave, + preview: previewDocumentChanges, +})); + async function load() { loading.value = true; error.value = ""; @@ -688,7 +794,7 @@ function resetTableSearchSplitWidth() { :result="gridResult" context="results" editable - :custom-save="gridSave" + :custom-save-handler="customSaveHandler" :loading="loading" :sql="documentStoreLabels.queryPreview" :page-offset="page * pageSize" @@ -932,9 +1038,7 @@ function resetTableSearchSplitWidth() { {{ t("mongo.selectDocument") }} -
- {{ error }} -
+ diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 253c1ff5e..5e2b6a5de 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -45,7 +45,6 @@ import { WrapText, Info, Rows3, - TriangleAlert, RefreshCcw, RotateCcw, Pencil, @@ -72,6 +71,7 @@ import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "@/compon import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import ErrorBanner from "@/components/ui/ErrorBanner.vue"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue"; import ImagePreviewDialog from "@/components/grid/ImagePreviewDialog.vue"; @@ -181,7 +181,7 @@ const props = defineProps<{ cacheKey?: string; onExecuteSql?: (sql: string) => Promise; fullExportResult?: () => Promise; - customSave?: (changes: { dirtyRows: Map>; newRows: CellValue[][]; deletedRows: Set; columns: string[]; rows: CellValue[][] }) => Promise; + customSaveHandler?: import("@/composables/useDataGridEditor").CustomSaveHandler; }>(); const dataGridTraceId = uuid().slice(0, 8); @@ -1902,7 +1902,7 @@ const canShowWhereSearch = computed(() => !!props.onExecuteSql && !isResultsCont const canUseWhereSearch = computed(() => !!props.tableMeta && !!props.onExecuteSql && !isResultsContext.value); type DataGridTableMeta = NonNullable; const hiveTableTransactional = ref(undefined); -const canEditExistingRows = computed(() => !!props.customSave || canEditExistingTableRows(props.databaseType, hiveTableTransactional.value, props.tableMeta?.primaryKeys ?? [])); +const canEditExistingRows = computed(() => !!props.customSaveHandler || canEditExistingTableRows(props.databaseType, hiveTableTransactional.value, props.tableMeta?.primaryKeys ?? [])); watch( () => [props.databaseType, props.connectionId, props.database, props.tableMeta?.schema, props.tableMeta?.tableName], async () => { @@ -2118,7 +2118,7 @@ const editor = useDataGridEditor({ sourceColumns: computed(() => props.sourceColumns), canEditExistingRows, onExecuteSql: computed(() => props.onExecuteSql), - customSave: computed(() => props.customSave), + customSaveHandler: computed(() => props.customSaveHandler), sql: computed(() => props.sql), searchText, whereFilterInput, @@ -2234,7 +2234,7 @@ const saveActionMode = computed(() => const saveToolbarState = computed(() => dataGridSaveToolbarState({ editable: props.editable, - hasSaveTarget: !!props.tableMeta || !!props.customSave, + hasSaveTarget: !!props.tableMeta || !!props.customSaveHandler, hasPendingChanges: hasPendingChanges.value, isSaving: isSaving.value, }), @@ -2242,13 +2242,13 @@ const saveToolbarState = computed(() => const hasSearchBarSlot = computed(() => !!slots["search-bar"]); const showDataGridTopbar = computed( () => - (useTransaction.value && !!props.editable && (!!props.tableMeta || !!props.customSave)) || + (useTransaction.value && !!props.editable && (!!props.tableMeta || !!props.customSaveHandler)) || hasLocalColumnFilters.value || canShowWhereSearch.value || hasSearchBarSlot.value || showQueryEditReadyBadge.value || props.context !== "results" || - (!!props.editable && (!!props.tableMeta || !!props.customSave)) || + (!!props.editable && (!!props.tableMeta || !!props.customSaveHandler)) || transactionActive.value || saveToolbarState.value.showActions, ); @@ -2616,11 +2616,6 @@ const { isRowSelected, } = selection; -const selectionSummary = computed(() => { - if (hasRowSelection.value) return t("grid.selectedRows", { count: selectedRowCount.value }); - return t("grid.selectedCells", { count: selectedCellCount.value }); -}); - const multiRowCount = computed(() => { if (hasRowSelection.value) return selectedRowCount.value; const range = selectedRange.value; @@ -5858,7 +5853,7 @@ const gridContextMenuItems = computed(() => { " >
-
+