Merge branch 'main' into next

This commit is contained in:
nocobase[bot]
2026-07-07 08:02:41 +00:00
6 changed files with 203 additions and 1 deletions
@@ -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<AttachmentFieldInterface>('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<string, unknown> = {};
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_/);
});
});
@@ -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.
@@ -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<string, unknown>) {
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';
}
}
}
@@ -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<Record<string, never>, App
storageTypes = new Map<string, StorageType>();
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
@@ -10,10 +10,23 @@
import { describe, expect, it, vi } from 'vitest';
import { PluginPublicFormsServer } from '../plugin';
type PublicFormsServerApp = ConstructorParameters<typeof PluginPublicFormsServer>[0];
function createPlugin() {
return Object.create(PluginPublicFormsServer.prototype) as PluginPublicFormsServer & Record<string, any>;
}
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();
});
});
@@ -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')
) {