From f004ead6d0a6edf094188445cca571872c0cbea2 Mon Sep 17 00:00:00 2001 From: ajie Date: Mon, 12 May 2025 08:02:23 +0800 Subject: [PATCH] fix: exporting empty values in nested associations (#6845) --- packages/core/database/src/repository.ts | 1 + .../server/__tests__/export-to-xlsx.test.ts | 99 +++++++++++++++++++ .../src/server/services/base-exporter.ts | 20 ++-- .../plugin-file-manager/src/server/server.ts | 23 ++++- 4 files changed, 133 insertions(+), 10 deletions(-) diff --git a/packages/core/database/src/repository.ts b/packages/core/database/src/repository.ts index fd05dc7db14..28cddfffdab 100644 --- a/packages/core/database/src/repository.ts +++ b/packages/core/database/src/repository.ts @@ -125,6 +125,7 @@ export interface CommonFindOptions extends Transactionable { sort?: Sort; context?: any; tree?: boolean; + hooks?: boolean; } export type FindOneOptions = Omit & { diff --git a/packages/plugins/@nocobase/plugin-action-export/src/server/__tests__/export-to-xlsx.test.ts b/packages/plugins/@nocobase/plugin-action-export/src/server/__tests__/export-to-xlsx.test.ts index be6d05fea3d..256b0128529 100644 --- a/packages/plugins/@nocobase/plugin-action-export/src/server/__tests__/export-to-xlsx.test.ts +++ b/packages/plugins/@nocobase/plugin-action-export/src/server/__tests__/export-to-xlsx.test.ts @@ -440,6 +440,105 @@ describe('export to xlsx with preset', () => { } }); + it('should export with attachment url field and associations', async () => { + const Post = app.db.collection({ + name: 'posts', + fields: [ + { type: 'string', name: 'title' }, + { + name: 'attachment1', + type: 'belongsTo', + interface: 'm2o', + onDelete: 'SET NULL', + targetKey: 'id', + target: 'attachments', + uiSchema: { + 'x-component-props': { + accept: 'image/*', + multiple: false, + }, + 'x-component': 'AssociationField', + title: 'attachment1', + }, + storage: 'local', + foreignKey: 'f_63ni7i8z7cx', + }, + ], + }); + + const Tag = app.db.collection({ + name: 'tags', + fields: [ + { type: 'string', name: 'name' }, + { + name: 'post', + type: 'belongsTo', + target: 'posts', + targetKey: 'id', + foreignKey: 'aaa', + }, + ], + }); + + await app.db.sync(); + await Post.repository.create({ + values: { + title: 'p1', + attachment1: { + title: 'nocobase-logo1', + filename: '682e5ad037dd02a0fe4800a3e91c283b.png', + extname: '.png', + mimetype: 'image/png', + url: '', + storageId: 1, + }, + }, + }); + + await Tag.repository.create({ + values: [ + { + name: 'tag1', + post: { + id: 1, + }, + }, + ], + }); + + const exporter = new XlsxExporter({ + collectionManager: app.mainDataSource.collectionManager, + collection: Tag, + chunkSize: 10, + columns: [ + { dataIndex: ['name'], defaultTitle: 'Name' }, + { + dataIndex: ['post', 'attachment1', 'url'], + defaultTitle: 'URL', + }, + ], + }); + + const wb = await exporter.run(); + const xlsxFilePath = path.resolve(__dirname, `t_${uid()}.xlsx`); + try { + XLSX.writeFile(wb, xlsxFilePath); + + // read xlsx file + const workbook = XLSX.readFile(xlsxFilePath); + const firstSheet = workbook.Sheets[workbook.SheetNames[0]]; + const sheetData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 }); + + const header = sheetData[0]; + expect(header).toEqual(['Name', 'URL']); + + const result = sheetData[1]; + expect(result[1]).toEqual('/storage/uploads/682e5ad037dd02a0fe4800a3e91c283b.png'); + } finally { + fs.unlinkSync(xlsxFilePath); + } + }); + it('should export with china region field', async () => { const Post = app.db.collection({ name: 'posts', diff --git a/packages/plugins/@nocobase/plugin-action-export/src/server/services/base-exporter.ts b/packages/plugins/@nocobase/plugin-action-export/src/server/services/base-exporter.ts index d699a96b5d4..6e9c155bbf9 100644 --- a/packages/plugins/@nocobase/plugin-action-export/src/server/services/base-exporter.ts +++ b/packages/plugins/@nocobase/plugin-action-export/src/server/services/base-exporter.ts @@ -82,19 +82,21 @@ abstract class BaseExporter extends Eve } protected getAppendOptionsFromFields(ctx?) { - const fields = this.options.fields.map((x) => x[0]); - const hasPermissionFields = _.isEmpty(ctx?.permission?.can?.params) - ? fields - : _.intersection(ctx?.permission?.can?.params?.appends || [], fields); - return hasPermissionFields - .map((field) => { - const fieldInstance = this.options.collection.getField(field); + return this.options.fields + .filter((fieldPath) => { + const field = fieldPath[0]; + const hasPermission = + _.isEmpty(ctx?.permission?.can?.params) || (ctx?.permission?.can?.params?.appends || []).includes(field); + return hasPermission; + }) + .map((fieldPath) => { + const fieldInstance = this.options.collection.getField(fieldPath[0]); if (!fieldInstance) { - throw new Error(`Field "${field}" not found: , please check the fields configuration.`); + throw new Error(`Field "${fieldPath[0]}" not found: , please check the fields configuration.`); } if (fieldInstance.isRelationField()) { - return field; + return fieldPath.join('.'); } return null; diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts index a5dc7f2a300..1706af4c8c3 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts @@ -24,6 +24,7 @@ import StorageTypeLocal from './storages/local'; import StorageTypeS3 from './storages/s3'; import StorageTypeTxCos from './storages/tx-cos'; import { encodeURL } from './utils'; +import _ from 'lodash'; export type * from './storages'; @@ -312,7 +313,7 @@ export class PluginFileManagerServer extends Plugin { this.app.db.interfaceManager.registerInterfaceType('attachment', AttachmentInterface); - this.db.on('afterFind', async (instances) => { + this.db.on('afterFind', async (instances, options) => { if (!instances) { return; } @@ -321,7 +322,27 @@ export class PluginFileManagerServer extends Plugin { if (name) { const collection = this.db.getCollection(name); if (collection?.name === 'attachments' || collection?.options?.template === 'file') { + let recordMap = null; + if (!options?.attributes?.includes('storageId')) { + // 导出时,不会查出来 storageId,这里兼容重新取一下 + const ids = records.map((x) => x.id).filter(Boolean); + const fullRecords = await collection.repository.find({ + where: { id: ids }, + transaction: options.transaction, + hooks: false, + }); + recordMap = _.keyBy(fullRecords, (x) => x.id); + } for (const record of records) { + if (recordMap) { + const fullRecord = recordMap[record.id]; + if (fullRecord) { + record.set('storageId', fullRecord.storageId); + record.set('title', fullRecord.title); + record.set('filename', fullRecord.filename); + record.set('extname', fullRecord.extname); + } + } const url = await this.getFileURL(record); const previewUrl = await this.getFileURL(record, true); record.set('url', url);