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 @@