From 35362c8bf4693f2bbbcb61ad6bf560746afc4889 Mon Sep 17 00:00:00 2001 From: zipg Date: Sun, 9 Aug 2026 20:13:18 +0800 Subject: [PATCH] feat(grid): import BYTEA/BLOB cell from file with size gate --- apps/desktop/src/components/grid/DataGrid.vue | 67 ++++++++++++-- .../grid/DataGridCellDetailDialog.vue | 9 +- .../grid/DataGridCellDetailPanel.vue | 7 +- .../components/grid/DataGridExportMenu.vue | 4 +- .../src/components/grid/DataGridToolbar.vue | 4 +- .../grid/__tests__/DataGridSurfaces.spec.ts | 24 ++++- apps/desktop/src/i18n/locales/en.ts | 4 + apps/desktop/src/i18n/locales/es.ts | 4 + apps/desktop/src/i18n/locales/it.ts | 4 + apps/desktop/src/i18n/locales/ja.ts | 4 + apps/desktop/src/i18n/locales/ko.ts | 1 + apps/desktop/src/i18n/locales/pt-BR.ts | 4 + apps/desktop/src/i18n/locales/zh-CN.ts | 4 + apps/desktop/src/i18n/locales/zh-TW.ts | 4 + .../dataGrid/dataGridContextMenu.spec.ts | 3 + .../__tests__/binaryCellFileImport.spec.ts | 18 ++++ .../src/lib/dataGrid/binaryCellDownload.ts | 91 ++++++++++++++++++- .../src/lib/dataGrid/dataGridContextMenu.ts | 2 + crates/dbx-core/src/data_grid_sql.rs | 40 ++++++++ packages/app-tests/binaryCellDownload.test.ts | 45 ++++++++- 20 files changed, 326 insertions(+), 17 deletions(-) create mode 100644 apps/desktop/src/lib/dataGrid/__tests__/binaryCellFileImport.spec.ts diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 635a8b2b5..e55bd2658 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -6,7 +6,8 @@ import { ArrowDown, ArrowUpDown, ArrowUpRight, - Upload, + Download, + FileUp, Trash2, ChevronDown, ChevronUp, @@ -122,12 +123,17 @@ import { getApplicablePreviewActions } from "@/lib/dataGrid/resultPreviewRegistr import "@/lib/dataGrid/geometryMapPreview"; import { BINARY_CELL_DOWNLOAD_MODES, + BinaryCellImportTooLargeError, + binaryCellBytesToHexValue, binaryCellDisplayText, binaryCellDownloadFileName, binaryCellDownloadPayload, + canImportBinaryCellFile, canDownloadBinaryCellValue, downloadBinaryCellPayload, + formatBinaryCellByteSize, isBinaryCellColumnType, + openBinaryCellFile, parseBinaryCellBytes, retainBinaryCellDownloadMenuForHover, type BinaryCellDownloadMode, @@ -7063,6 +7069,39 @@ function canDownloadDetailBinaryValue(detail: DataGridCellDetail | null): boolea return !!detail && canDownloadBinaryCellValue(detail.value, detail.type); } +function canImportDetailBinaryValue(detail: DataGridCellDetail | null): boolean { + return !!detail?.isEditable && canImportBinaryCellFile(resolvedDatabaseType.value, detail.type); +} + +async function importDetailBinaryValue(detail: DataGridCellDetail | null) { + if (!detail || !canImportDetailBinaryValue(detail)) return; + try { + const bytes = await openBinaryCellFile(); + if (!bytes) return; + const value = binaryCellBytesToHexValue(bytes); + applyCellValue(detail.rowId, detail.colIndex, value); + if (activeCellDetail.value?.rowId === detail.rowId && activeCellDetail.value.colIndex === detail.colIndex) { + detailEditValue.value = value; + detailEditOriginalValue.value = value; + syncEditorFromDetailEdit(); + detailCell.value = detailCell.value ? { ...detailCell.value } : null; + } + toast(t("grid.binaryImportApplied", { count: bytes.length })); + } catch (e: any) { + if (e instanceof BinaryCellImportTooLargeError) { + toast( + t("grid.binaryImportTooLarge", { + size: formatBinaryCellByteSize(e.bytes), + limit: formatBinaryCellByteSize(e.limit), + }), + 5000, + ); + return; + } + toast(t("grid.binaryImportFailed", { message: e?.message || String(e) }), 5000); + } +} + function canQuickDownloadCellValue(rowIndex: number, columnIndex: number): boolean { return canDownloadDetailBinaryValue(cellDetailFor(rowIndex, columnIndex)); } @@ -7096,7 +7135,7 @@ function binaryDownloadSubmenu(detail: DataGridCellDetail | null): ContextMenuIt if (!canDownloadDetailBinaryValue(detail)) return null; return { label: t("grid.downloadBinaryValue"), - icon: Upload, + icon: Download, children: BINARY_CELL_DOWNLOAD_MODES.map((mode) => ({ label: t(`grid.binaryDownload.${mode}`), action: () => { @@ -7106,6 +7145,17 @@ function binaryDownloadSubmenu(detail: DataGridCellDetail | null): ContextMenuIt }; } +function binaryImportItem(detail: DataGridCellDetail | null): ContextMenuItem | null { + if (!canImportDetailBinaryValue(detail)) return null; + return { + label: t("grid.importBinaryValue"), + icon: FileUp, + action: () => { + void importDetailBinaryValue(detail); + }, + }; +} + async function copyDetailSqlCondition() { if (!canCopyPreparedDetailSqlCondition()) return; copyText(detailSqlConditionCopy.value.text); @@ -8846,7 +8896,7 @@ function exportSubmenu(): ContextMenuItem { { label: t("grid.exportSelectedRowsTxt"), action: exportSelectedRowsTxt }, ); } - return { label: t("grid.export"), icon: Upload, children: items }; + return { label: t("grid.export"), icon: Download, children: items }; } const gridContextMenuItems = computed(() => { @@ -8939,6 +8989,7 @@ const gridContextMenuItems = computed(() => { }, icons: { cellDetails: Maximize2, columnDetails: TableProperties, rowDetails: ListTree, setNull: X, bulkEdit: Pencil, transpose: Rows3 }, actions: { cellDetails: openContextCellDetailDialog, columnDetails: openContextColumnDetailDialog, rowDetails: openContextRowDetailDialog, setNull: setSelectionNull, bulkEdit: openBulkEditDialog, transpose: openContextTranspose }, + importItem: binaryImportItem(contextCellDetail.value), downloadItem: binaryDownloadSubmenu(contextCellDetail.value), foreignKeyItem: contextForeignKeyMenuItem(), copySubmenu: copySubmenu(), @@ -9376,7 +9427,7 @@ const gridContextMenuItems = computed(() => { > @@ -9890,7 +9941,7 @@ const gridContextMenuItems = computed(() => { > @@ -10093,7 +10144,7 @@ const gridContextMenuItems = computed(() => { > @@ -10408,6 +10459,8 @@ const gridContextMenuItems = computed(() => { :type-color-class="typeColorClass" :can-download-binary-value="canDownloadDetailBinaryValue" :download-binary-value="downloadDetailBinaryValue" + :can-import-binary-value="canImportDetailBinaryValue" + :import-binary-value="importDetailBinaryValue" :open-image-preview="openImagePreview" :can-copy-sql-condition="canCopyPreparedDetailSqlCondition" @start-edit="startDetailEdit" @@ -10582,6 +10635,8 @@ const gridContextMenuItems = computed(() => { :copy-text="copyText" :can-download-binary-value="canDownloadDetailBinaryValue" :download-binary-value="downloadDetailBinaryValue" + :can-import-binary-value="canImportDetailBinaryValue" + :import-binary-value="importDetailBinaryValue" @edit="openDialogCellInSidePanel" /> diff --git a/apps/desktop/src/components/grid/DataGridCellDetailDialog.vue b/apps/desktop/src/components/grid/DataGridCellDetailDialog.vue index 623e7caa8..c8bdc3882 100644 --- a/apps/desktop/src/components/grid/DataGridCellDetailDialog.vue +++ b/apps/desktop/src/components/grid/DataGridCellDetailDialog.vue @@ -1,6 +1,6 @@