diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/client-v2/__tests__/attachment-interface.test.ts b/packages/plugins/@nocobase/plugin-file-manager/src/client-v2/__tests__/attachment-interface.test.ts new file mode 100644 index 00000000000..c31ed54c894 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-file-manager/src/client-v2/__tests__/attachment-interface.test.ts @@ -0,0 +1,59 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Application } from '@nocobase/client-v2'; +import { describe, expect, it } from 'vitest'; +import { AttachmentFieldInterface } from '../interfaces/attachment'; +import PluginFileManagerClientV2 from '../plugin'; + +describe('AttachmentFieldInterface', () => { + it('provides v2 data source manager metadata for attachment fields', async () => { + const app = new Application({ + plugins: [PluginFileManagerClientV2], + }); + + await app.load(); + + const fieldInterface = + app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface('attachment'); + + expect(fieldInterface).toBeInstanceOf(AttachmentFieldInterface); + expect(fieldInterface.group).toBe('media'); + expect(fieldInterface.default).toMatchObject({ + interface: 'attachment', + type: 'belongsToMany', + target: 'attachments', + uiSchema: { + type: 'array', + 'x-component': 'Upload.Attachment', + }, + }); + expect(fieldInterface.configure?.items?.map((item) => item.name)).toEqual([ + 'target', + 'targetKey', + 'uiSchema.x-component-props.accept', + 'uiSchema.x-component-props.multiple', + ]); + }); + + it('initializes required belongsToMany keys', () => { + const fieldInterface = new AttachmentFieldInterface({} as never); + const values: Record = {}; + + fieldInterface.initialize(values); + + expect(values).toMatchObject({ + sourceKey: 'id', + targetKey: 'id', + }); + expect(values.through).toMatch(/^t_/); + expect(values.foreignKey).toMatch(/^f_/); + expect(values.otherKey).toMatch(/^f_/); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/client-v2/index.tsx b/packages/plugins/@nocobase/plugin-file-manager/src/client-v2/index.tsx index a9512d4ce3b..5046d245f31 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/client-v2/index.tsx +++ b/packages/plugins/@nocobase/plugin-file-manager/src/client-v2/index.tsx @@ -36,6 +36,7 @@ export { export type { DefaultFieldProps } from './components/DefaultField'; export type { PathFieldProps } from './components/PathField'; export { CardUpload, UploadFieldModel } from './models/UploadFieldModel'; +export { AttachmentFieldInterface } from './interfaces/attachment'; // Preview registry consumed by file-previewer plugins (e.g. plugin-file-previewer-office) // to add custom preview handlers under v2 without going through the v1 `@nocobase/plugin-file-manager/client` entry. diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/client-v2/interfaces/attachment.ts b/packages/plugins/@nocobase/plugin-file-manager/src/client-v2/interfaces/attachment.ts new file mode 100644 index 00000000000..0e83ecb8bae --- /dev/null +++ b/packages/plugins/@nocobase/plugin-file-manager/src/client-v2/interfaces/attachment.ts @@ -0,0 +1,86 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { uid } from '@formily/shared'; +import { CollectionFieldInterface } from '@nocobase/client-v2'; +import { tExpr } from '../locale'; + +export class AttachmentFieldInterface extends CollectionFieldInterface { + name = 'attachment'; + type = 'object'; + group = 'media'; + title = tExpr('Attachment'); + isAssociation = true; + default = { + interface: 'attachment', + type: 'belongsToMany', + target: 'attachments', + uiSchema: { + type: 'array', + 'x-component': 'Upload.Attachment', + 'x-use-component-props': 'useAttachmentFieldProps', + }, + }; + availableTypes = ['belongsToMany']; + configure = { + items: [ + { + name: 'target', + title: tExpr('File collection'), + component: 'Select' as const, + required: true, + defaultValue: 'attachments', + schema: { + enum: '{{fileCollections}}', + }, + }, + { + name: 'targetKey', + defaultValue: 'id', + hidden: true, + }, + { + name: 'uiSchema.x-component-props.accept', + title: tExpr('MIME type'), + component: 'Input' as const, + componentProps: { + placeholder: 'image/*', + }, + }, + { + name: 'uiSchema.x-component-props.multiple', + title: tExpr('Allow uploading multiple files'), + component: 'Checkbox' as const, + defaultValue: true, + }, + ], + }; + filterable = { + nested: true, + children: [], + }; + + initialize(values: Record) { + if (!values.through) { + values.through = `t_${uid()}`; + } + if (!values.foreignKey) { + values.foreignKey = `f_${uid()}`; + } + if (!values.otherKey) { + values.otherKey = `f_${uid()}`; + } + if (!values.sourceKey) { + values.sourceKey = 'id'; + } + if (!values.targetKey) { + values.targetKey = 'id'; + } + } +} diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/client-v2/plugin.tsx b/packages/plugins/@nocobase/plugin-file-manager/src/client-v2/plugin.tsx index 9d4af76cbe4..ed0f669021f 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/client-v2/plugin.tsx +++ b/packages/plugins/@nocobase/plugin-file-manager/src/client-v2/plugin.tsx @@ -18,6 +18,7 @@ import { STORAGE_TYPE_TX_COS, } from '../constants'; import { NAMESPACE } from '../common/constants'; +import { AttachmentFieldInterface } from './interfaces/attachment'; import { tExpr } from './locale'; type UploadFileResult = { @@ -88,6 +89,8 @@ export class PluginFileManagerClientV2 extends Plugin, App storageTypes = new Map(); async load() { + this.app.addFieldInterfaces([AttachmentFieldInterface]); + const title = this.app.i18n.t('File manager', { ns: NAMESPACE }); const dataSourceManager = (this.app.pm.get('@nocobase/plugin-data-source-manager') || this.app.pm.get('data-source-manager')) as diff --git a/packages/plugins/@nocobase/plugin-public-forms/src/server/__tests__/plugin.test.ts b/packages/plugins/@nocobase/plugin-public-forms/src/server/__tests__/plugin.test.ts index 207a04c7b0e..83a487f1e43 100644 --- a/packages/plugins/@nocobase/plugin-public-forms/src/server/__tests__/plugin.test.ts +++ b/packages/plugins/@nocobase/plugin-public-forms/src/server/__tests__/plugin.test.ts @@ -10,10 +10,23 @@ import { describe, expect, it, vi } from 'vitest'; import { PluginPublicFormsServer } from '../plugin'; +type PublicFormsServerApp = ConstructorParameters[0]; + function createPlugin() { return Object.create(PluginPublicFormsServer.prototype) as PluginPublicFormsServer & Record; } +function createAclPlugin() { + return new PluginPublicFormsServer( + { + db: { + getCollection: vi.fn(() => null), + }, + } as unknown as PublicFormsServerApp, + { name: 'public-forms' }, + ); +} + function setupGetMetaPlugin(options: { password?: string; enabled?: boolean } = {}) { const plugin = createPlugin(); const visibleFlowModel = { @@ -77,6 +90,23 @@ function setupGetMetaPlugin(options: { password?: string; enabled?: boolean } = return { plugin, sign }; } +type PublicFormAclContext = { + PublicForm: { + collectionName: string; + targetCollections: string[]; + }; + action: { + resourceName: string; + actionName: string; + params: { + fileCollectionName?: string; + }; + }; + permission?: { + skip: boolean; + }; +}; + describe('PluginPublicFormsServer', () => { it('keeps primary collection options in public form meta', async () => { const plugin = createPlugin(); @@ -379,4 +409,27 @@ describe('PluginPublicFormsServer', () => { expect(plugin.isFlowModelDescendant).toHaveBeenCalledWith('pf1', 'popup-grid'); expect(findModelById).toHaveBeenCalledWith('popup-grid', { includeAsyncNode: true }); }); + + it('allows public form storage checks', async () => { + const plugin = createAclPlugin(); + const ctx: PublicFormAclContext = { + PublicForm: { + collectionName: 'orders', + targetCollections: [], + }, + action: { + resourceName: 'storages', + actionName: 'check', + params: { + fileCollectionName: 'publicFiles', + }, + }, + }; + const next = vi.fn(async () => undefined); + + await plugin.parseACL(ctx, next); + + expect(ctx.permission).toEqual({ skip: true }); + expect(next).toHaveBeenCalled(); + }); }); diff --git a/packages/plugins/@nocobase/plugin-public-forms/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-public-forms/src/server/plugin.ts index c423b1b8e5e..738615d36d0 100644 --- a/packages/plugins/@nocobase/plugin-public-forms/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-public-forms/src/server/plugin.ts @@ -347,7 +347,7 @@ export class PluginPublicFormsServer extends Plugin { } else if ( (['list', 'get'].includes(actionName) && ctx.PublicForm['targetCollections'].includes(resourceName)) || (collection?.options.template === 'file' && actionName === 'create') || - (resourceName === 'storages' && ['getBasicInfo', 'createPresignedUrl'].includes(actionName)) || + (resourceName === 'storages' && ['getBasicInfo', 'createPresignedUrl', 'check'].includes(actionName)) || (resourceName === 'vditor' && ['check'].includes(actionName)) || (resourceName === 'map-configuration' && actionName === 'get') ) {