fix: unit test

This commit is contained in:
dream2023 2024-01-29 17:15:41 +08:00
parent 3ad1a8e25f
commit 27f0b6010e
38 changed files with 3471 additions and 207 deletions

View File

@ -614,7 +614,7 @@ class CollectionManagerV2 {
```tsx | pure
collectionManager.getCollectionName('users'); // 'users'
collectionManager.getCollectionName('users.profileId'); // 'profiles'
collectionManager.getCollectionName('users.profiles'); // 'profiles'
```

View File

@ -1,10 +1,9 @@
import React from 'react';
import { Select, Table, TableProps } from 'antd';
import { Table, TableProps } from 'antd';
import { SchemaComponent, UseDataBlockProps, useDataBlockRequestV2, withDynamicSchemaProps } from '@nocobase/client';
import { ISchema } from '@formily/json-schema';
import { createApp } from '../../../collection/demos/createApp';
import useUrlState from '@ahooksjs/use-url-state';
const collection = 'users';
const associationField = 'roles';

View File

@ -0,0 +1,85 @@
import React, { ComponentType } from 'react';
import { render, screen } from '@nocobase/test/client';
import {
AssociationProviderV2,
CollectionManagerProviderV2,
useCollectionFieldV2,
useCollectionFieldsV2,
useCollectionV2,
} from '../../collection';
import { Application } from '../../Application';
import collections from './collections.json';
import { SchemaComponentProvider } from '../../../schema-component';
function renderApp(Demo: ComponentType, props: any = {}) {
const app = new Application({
collectionManager: {
collections: collections as any,
dataSources: [
{
name: 'a',
description: 'a',
collections: collections as any,
},
],
},
});
return render(
<div data-testid="app">
<SchemaComponentProvider designable={true}>
<CollectionManagerProviderV2 collectionManager={app.collectionManager}>
<AssociationProviderV2 {...props}>
<Demo></Demo>
</AssociationProviderV2>
</CollectionManagerProviderV2>
</SchemaComponentProvider>
</div>,
);
}
describe('AssociationProvider', () => {
test('should render', () => {
const Demo = () => {
const collection = useCollectionV2();
const collectionFiled = useCollectionFieldV2();
return (
<>
<div data-testid="collection">{collection.name}</div>
<div data-testid="field">{collectionFiled.name}</div>
</>
);
};
renderApp(Demo, { name: 'users.roles' });
expect(screen.getByTestId('collection')).toHaveTextContent('roles');
expect(screen.getByTestId('field')).toHaveTextContent('roles');
});
test('should render with dataSource', () => {
const Demo = () => {
const collection = useCollectionV2();
const collectionFiled = useCollectionFieldV2();
return (
<>
<div data-testid="collection">{collection.name}</div>
<div data-testid="field">{collectionFiled.name}</div>
</>
);
};
renderApp(Demo, { name: 'users.roles', dataSource: 'a' });
expect(screen.getByTestId('collection')).toHaveTextContent('roles');
expect(screen.getByTestId('field')).toHaveTextContent('roles');
});
test('not exists, should render `DeletedPlaceholder`', () => {
const Demo = () => {
return <div>children</div>;
};
renderApp(Demo, { name: 'users.not-exists' });
expect(screen.getByTestId('app').innerHTML).not.toContain('children');
});
});

View File

@ -0,0 +1,228 @@
import { Application } from '../../Application';
import { CollectionOptionsV2, DEFAULT_DATA_SOURCE_NAME } from '../../collection';
import collections from './collections.json';
function getCollection(collection: CollectionOptionsV2) {
const app = new Application({
collectionManager: {
collections: [collection],
},
});
return app.collectionManager.getCollection(collection.name);
}
describe('Collection', () => {
describe('getPrimaryKey()', () => {
test('Return `targetKey` if targetKey property exists', () => {
const collection = getCollection({ name: 'test', targetKey: 'a' });
expect(collection.getPrimaryKey()).toBe('a');
});
test('If targetKey does not exist, return the name of the field with `primaryKey` set to true in the fields', () => {
const collection = getCollection({
name: 'test',
fields: [{ name: 'a', primaryKey: true }, { name: 'b' }],
});
expect(collection.getPrimaryKey()).toBe('a');
});
test('If targetKey does not exist and no field has primaryKey set to true, return `id`', () => {
const collection = getCollection({ name: 'test' });
expect(collection.getPrimaryKey()).toBe('id');
});
test('cache the result', () => {
const collection = getCollection({ name: 'test' });
const spy = vitest.spyOn(collection, 'getFields');
collection.getPrimaryKey();
collection.getPrimaryKey();
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('titleField', () => {
test('return `titleField` if it exists', () => {
const collection = getCollection({ name: 'test', titleField: 'a', fields: [{ name: 'a' }] });
expect(collection.titleField).toBe('a');
});
test('if `titleField` does not exist in fields, return `primaryKey`', () => {
const collection = getCollection({ name: 'test', titleField: 'a', targetKey: 'b' });
expect(collection.titleField).toBe('b');
});
test('if `titleField` does not exist, return `primaryKey`', () => {
const collection = getCollection({ name: 'test', targetKey: 'a' });
expect(collection.titleField).toBe('a');
});
test('if `titleField` and `primaryKey` do not exist, return `id`', () => {
const collection = getCollection({ name: 'test' });
expect(collection.titleField).toBe('id');
});
});
describe('options', () => {
test('getOptions()', () => {
const collection = getCollection({ name: 'test', titleField: 'a', targetKey: 'b' });
expect(collection.getOptions()).toMatchObject({
name: 'test',
titleField: 'a',
targetKey: 'b',
dataSource: DEFAULT_DATA_SOURCE_NAME,
});
});
test('getOption()', () => {
const collection = getCollection({ name: 'test', key: 'a', model: 'b' });
expect(collection.getOption('name')).toBe('test');
expect(collection.getOption('key')).toBe('a');
expect(collection.getOption('model')).toBe('b');
expect(collection.getOption('dataSource')).toBe(DEFAULT_DATA_SOURCE_NAME);
});
});
test('dataSource', () => {
const app = new Application({
collectionManager: {
dataSources: [
{
name: 'test',
description: 'test',
collections: [{ name: 'user' }],
},
],
},
});
const user = app.collectionManager.getCollection('user', { dataSource: 'test' });
expect(user.dataSource).toBe('test');
});
describe('getFields()', () => {
test('return fields', () => {
const collection = getCollection({ name: 'test', fields: [{ name: 'a' }, { name: 'b' }] });
expect(collection.getFields()).toMatchObject([{ name: 'a' }, { name: 'b' }]);
});
test('support predicate', () => {
const collection = getCollection({
name: 'test',
fields: [
{ name: 'a', primaryKey: false },
{ name: 'b', primaryKey: true },
],
});
// { key: value }
const res1 = collection.getFields({ name: 'a' });
expect(res1.length).toBe(1);
expect(res1[0].name).toBe('a');
// key === { key: true }
const res2 = collection.getFields('primaryKey');
expect(res2.length).toBe(1);
expect(res2[0].name).toBe('b');
const res3 = collection.getFields({ primaryKey: true });
expect(res3.length).toBe(1);
expect(res3[0].name).toBe('b');
// function
const res4 = collection.getFields((field) => field.name === 'a');
expect(res4.length).toBe(1);
expect(res4[0].name).toBe('a');
});
});
describe('getField()', () => {
test('return field', () => {
const collection = getCollection({ name: 'test', fields: [{ name: 'a' }, { name: 'b' }] });
expect(collection.getField('a')).toMatchObject({ name: 'a' });
expect(collection.getField('test.a')).toMatchObject({ name: 'a' });
});
test('support dot notation', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const users = app.collectionManager.getCollection('users');
expect(users.getField('roles.name')).toMatchObject({ name: 'name', collectionName: 'roles' });
});
test('return undefined if field does not exist', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const users = app.collectionManager.getCollection('users');
expect(users.getField('no-exist')).toBeUndefined();
expect(users.getField('no-exist.c')).toBeUndefined();
expect(users.getField('id.no-exist')).toBeUndefined();
expect(users.getField('roles.no-exist')).toBeUndefined();
});
});
describe('hasField()', () => {
test('return true if field exists', () => {
const collection = getCollection({ name: 'test', fields: [{ name: 'a' }, { name: 'b' }] });
expect(collection.hasField('a')).toBe(true);
});
test('return false if field does not exist', () => {
const collection = getCollection({ name: 'test', fields: [{ name: 'a' }, { name: 'b' }] });
expect(collection.hasField('c')).toBe(false);
});
});
test('properties', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const users = app.collectionManager.getCollection('users');
const usersOptions: any = collections.find((c) => c.name === 'users');
expect(users.sourceKey).toBe(usersOptions.sourceKey);
expect(users.name).toBe(usersOptions.name);
expect(users.key).toBe(usersOptions.key);
expect(users.title).toBe(usersOptions.title);
expect(users.inherit).toBe(usersOptions.inherit);
expect(users.hidden).toBe(usersOptions.hidden);
expect(users.description).toBe(usersOptions.description);
expect(users.duplicator).toBe(usersOptions.duplicator);
expect(users.category).toBe(usersOptions.category);
expect(users.targetKey).toBe(usersOptions.targetKey);
expect(users.model).toBe(usersOptions.model);
expect(users.createdBy).toBe(usersOptions.createdBy);
expect(users.updatedBy).toBe(usersOptions.updatedBy);
expect(users.logging).toBe(usersOptions.logging);
expect(users.from).toBe(usersOptions.from);
expect(users.rawTitle).toBe(usersOptions.rawTitle);
expect(users.isLocal).toBe(usersOptions.isLocal);
expect(users.inherits).toMatchObject([]);
expect(users.sources).toMatchObject([]);
expect(users.fields).toMatchObject(usersOptions.fields);
expect(users.tableName).toBe(usersOptions.tableName);
expect(users.viewName).toBe(usersOptions.viewName);
expect(users.writableView).toBe(usersOptions.writableView);
expect(users.filterTargetKey).toBe(usersOptions.filterTargetKey);
expect(users.sortable).toBe(usersOptions.sortable);
expect(users.autoGenId).toBe(usersOptions.autoGenId);
expect(users.magicAttribute).toBe(usersOptions.magicAttribute);
expect(users.tree).toBe(usersOptions.tree);
expect(users.isThrough).toBe(usersOptions.isThrough);
expect(users.autoCreate).toBe(usersOptions.autoCreate);
expect(users.resource).toBe(usersOptions.resource);
});
});

View File

@ -0,0 +1,45 @@
import React from 'react';
import { render, screen } from '@nocobase/test/client';
import { CollectionFieldV2, CollectionManagerProviderV2, CollectionProviderV2 } from '../../collection';
import { Application } from '../../Application';
import collections from './collections.json';
import { FormItem, Input, SchemaComponent, SchemaComponentProvider } from '../../../schema-component';
function renderApp() {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const schema = {
name: 'root',
type: 'object',
properties: {
nickname: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
},
};
return render(
<div data-testid="app">
<SchemaComponentProvider designable={true}>
<CollectionManagerProviderV2 collectionManager={app.collectionManager}>
<CollectionProviderV2 name="users">
<SchemaComponent schema={schema} components={{ CollectionField: CollectionFieldV2, FormItem, Input }} />
</CollectionProviderV2>
</CollectionManagerProviderV2>
</SchemaComponentProvider>
</div>,
);
}
describe('CollectionField', () => {
it('works', async () => {
renderApp();
expect(screen.getByText('Nickname')).toBeInTheDocument();
expect(screen.getByRole('textbox')).toHaveClass('ant-input');
});
});

View File

@ -0,0 +1,60 @@
import React, { ComponentType } from 'react';
import { render, screen } from '@nocobase/test/client';
import {
CollectionFieldProviderV2,
CollectionManagerProviderV2,
CollectionProviderV2,
useCollectionFieldV2,
} from '../../collection';
import { Application } from '../../Application';
import collections from './collections.json';
import { SchemaComponentProvider } from '../../../schema-component';
function renderApp(Demo: ComponentType, name?: string) {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
return render(
<div data-testid="app">
<SchemaComponentProvider designable={true}>
<CollectionManagerProviderV2 collectionManager={app.collectionManager}>
<CollectionProviderV2 name="users">
<CollectionFieldProviderV2 name={name}>
<Demo></Demo>
</CollectionFieldProviderV2>
</CollectionProviderV2>
</CollectionManagerProviderV2>
</SchemaComponentProvider>
</div>,
);
}
describe('CollectionFieldProvider', () => {
test('useCollectionFieldV2() should get current field', () => {
const Demo = () => {
const field = useCollectionFieldV2();
return (
<>
<div data-testid="demo">{field.name}</div>
</>
);
};
renderApp(Demo, 'nickname');
expect(screen.getByTestId('demo')).toHaveTextContent('nickname');
});
test('field not exists, should render `DeletedPlaceholder`', () => {
const Demo = () => {
return <div>children</div>;
};
renderApp(Demo, 'not-exists');
expect(screen.getByTestId('app').innerHTML).toContain('ant-result');
expect(screen.getByTestId('app').innerHTML).not.toContain('children');
});
});

View File

@ -0,0 +1,928 @@
import {
Application,
CollectionFieldInterfaceBase,
CollectionTemplateBase,
CollectionV2,
DEFAULT_DATA_SOURCE_NAME,
Plugin,
} from '@nocobase/client';
import collections from './collections.json';
describe('CollectionManager', () => {
const collectionLength = collections.length;
describe('collections', () => {
describe('init', () => {
test('init should work', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
expect(app.collectionManager.getCollections().length).toBe(collectionLength);
});
test('plugin should work', async () => {
class MyPlugin extends Plugin {
async load() {
this.app.collectionManager.addCollections(collections as any);
}
}
const app = new Application({
plugins: [MyPlugin],
});
await app.load();
expect(app.collectionManager.getCollections().length).toBe(collectionLength);
});
test('collection should be instantiated', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const collectionInstances = app.collectionManager.getCollections();
collectionInstances.forEach((collection) => {
expect(collection).toBeInstanceOf(CollectionV2);
});
});
});
describe('addCollection()', () => {
test('addCollections(collections)', async () => {
class MyPlugin extends Plugin {
async load() {
this.app.collectionManager.addCollections(collections as any);
}
}
const app = new Application({
plugins: [MyPlugin],
});
await app.load();
expect(app.collectionManager.getCollections().length).toBe(collectionLength);
});
test('addCollections(collections) should deduplicate when adding collections', () => {
const app = new Application();
app.collectionManager.addCollections([collections[0]] as any);
app.collectionManager.addCollections([collections[1]] as any);
app.collectionManager.addCollections(collections as any);
expect(app.collectionManager.getCollections().length).toBe(collectionLength);
});
test('addCollections(collections, { dataSource })', () => {
const app = new Application({
collectionManager: {
dataSources: [
{
name: 'a',
description: 'A',
},
],
},
});
app.collectionManager.addCollections(collections as any, { dataSource: 'a' });
expect(app.collectionManager.getCollections({ dataSource: 'a' }).length).toBe(collectionLength);
});
});
describe('setCollections()', () => {
test('setCollections(collections) will reset the corresponding data source content', () => {
const app = new Application();
app.collectionManager.setCollections(collections as any);
app.collectionManager.setCollections([collections[1] as any]);
const collectionInstances = app.collectionManager.getCollections();
expect(collectionInstances.length).toBe(1);
expect(collectionInstances[0].name).toBe(collections[1]['name']);
});
test('setCollections(collections, { dataSource })', () => {
const app = new Application({
collectionManager: {
dataSources: [
{
name: 'a',
description: 'A',
},
],
},
});
app.collectionManager.addCollections(collections as any, { dataSource: 'a' });
app.collectionManager.setCollections([collections[1]] as any, { dataSource: 'a' });
const collectionInstances = app.collectionManager.getCollections({ dataSource: 'a' });
expect(collectionInstances.length).toBe(1);
expect(collectionInstances[0].name).toBe(collections[1]['name']);
});
});
describe('getCollections()', () => {
test('getCollections({ dataSource })', () => {
const app = new Application({
collectionManager: {
dataSources: [
{
name: 'a',
description: 'A',
},
],
},
});
app.collectionManager.addCollections(collections as any);
app.collectionManager.addCollections(collections as any, { dataSource: 'a' });
expect(app.collectionManager.getCollections().length).toBe(collectionLength);
expect(app.collectionManager.getCollections({ dataSource: 'a' }).length).toBe(collectionLength);
});
test('getCollections({ predicate })', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
expect(app.collectionManager.getCollections().length).toBe(collectionLength);
expect(
app.collectionManager.getCollections({
predicate: (collection) => collection.name === collections[0]['name'],
}).length,
).toBe(1);
expect(
app.collectionManager.getCollections({ predicate: (collection) => collection.hidden === false }).length,
).toBe(2);
});
});
describe('getAllCollections', () => {
test('getAllCollections() should work', () => {
const app = new Application({
collectionManager: {
dataSources: [
{
name: 'a',
description: 'A',
},
],
},
});
app.collectionManager.addCollections(collections as any);
app.collectionManager.addCollections([collections[0]] as any, { dataSource: 'a' });
const allCollections = app.collectionManager.getAllCollections();
expect(allCollections.length).toBe(2);
expect(allCollections[0].name).toBe(DEFAULT_DATA_SOURCE_NAME);
expect(allCollections[1].name).toBe('a');
expect(allCollections[0].collections.length).toBe(2);
expect(allCollections[1].collections.length).toBe(1);
});
});
describe('getCollection()', () => {
test('getCollection("collectionName")', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const collection = app.collectionManager.getCollection('users');
expect(collection instanceof CollectionV2).toBeTruthy();
expect(collection.name).toBe('users');
});
test('getCollection("collectionName.associationFieldName")', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const collection = app.collectionManager.getCollection('users.roles');
expect(collection instanceof CollectionV2).toBeTruthy();
expect(collection.name).toBe('roles');
});
test('getCollection(object) should return an instance without performing a lookup', () => {
const app = new Application();
const collection = app.collectionManager.getCollection(collections[0] as any);
expect(collection instanceof CollectionV2).toBeTruthy();
expect(collection.name).toBe('users');
});
test('getCollection("collectionName", { dataSource })', () => {
const app = new Application({
collectionManager: {
dataSources: [
{
name: 'a',
description: 'A',
},
],
},
});
app.collectionManager.addCollections(collections as any, { dataSource: 'a' });
const collection = app.collectionManager.getCollection('users', { dataSource: 'a' });
expect(collection instanceof CollectionV2).toBeTruthy();
expect(collection.name).toBe('users');
});
test('getCollection("not-exists") should return undefined', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const collection1 = app.collectionManager.getCollection('not-exists');
const collection2 = app.collectionManager.getCollection('users.not-exists');
expect(collection1).toBeUndefined();
expect(collection2).toBeUndefined();
});
test('getCollection(undefined) should return undefined', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const collection = app.collectionManager.getCollection(undefined);
expect(collection).toBeUndefined();
});
});
describe('getCollectionName()', () => {
test('getCollectionName("collectionName")', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const collectionName = app.collectionManager.getCollectionName('users');
expect(collectionName).toBe('users');
});
test('getCollectionName("collectionName.associationFieldName")', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const collectionName = app.collectionManager.getCollectionName('users.roles');
expect(collectionName).toBe('roles');
});
test('getCollectionName("collectionName", { dataSource })', () => {
const app = new Application({
collectionManager: {
dataSources: [
{
name: 'a',
description: 'A',
},
],
},
});
app.collectionManager.addCollections(collections as any, { dataSource: 'a' });
const collectionName = app.collectionManager.getCollectionName('users', { dataSource: 'a' });
expect(collectionName).toBe('users');
});
test('getCollectionName("not-exists") should return undefined', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const collection1 = app.collectionManager.getCollectionName('not-exists');
const collection2 = app.collectionManager.getCollectionName('users.not-exists');
expect(collection1).toBeUndefined();
expect(collection2).toBeUndefined();
});
});
describe('getCollectionField()', () => {
test('getCollectionField("collectionName.fieldName")', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const field = app.collectionManager.getCollectionField('users.nickname');
expect(field).toBeTruthy();
expect(field.name).toBe('nickname');
});
test('getCollectionField("collectionName.associationFieldName.fieldName")', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const field = app.collectionManager.getCollectionField('users.roles.name');
expect(field).toBeTruthy();
expect(field.name).toBe('name');
expect(field.collectionName).toBe('roles');
});
test('getCollectionField("collectionName.fieldName", { dataSource })', () => {
const app = new Application({
collectionManager: {
dataSources: [
{
name: 'a',
description: 'A',
collections: collections as any,
},
],
},
});
const field = app.collectionManager.getCollectionField('users.nickname', { dataSource: 'a' });
expect(field).toBeTruthy();
expect(field.name).toBe('nickname');
});
test('getCollectionField("not-exists") should return undefined', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const field1 = app.collectionManager.getCollectionField('not-exists');
const field2 = app.collectionManager.getCollectionField('users.not-exists');
const field3 = app.collectionManager.getCollectionField('not-exists.not-exists');
expect(field1).toBeUndefined();
expect(field2).toBeUndefined();
expect(field3).toBeUndefined();
});
test('getCollectionField(undefined) should return undefined', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const field = app.collectionManager.getCollectionField(undefined);
expect(field).toBeUndefined();
});
test('getCollectionField(object) should return object', () => {
const app = new Application();
const obj = {
name: 'nickname',
type: 'string',
};
const field = app.collectionManager.getCollectionField(obj);
expect(field).toEqual(obj);
});
});
describe('getCollectionFields()', () => {
const roles = collections.find((item) => item.name === 'roles');
const users = collections.find((item) => item.name === 'users');
test('getCollectionFields("collectionName")', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const fields = app.collectionManager.getCollectionFields('users');
expect(fields).toEqual(users.fields);
});
test('getCollectionFields("collectionName", { dataSource })', () => {
const app = new Application({
collectionManager: {
dataSources: [
{
name: 'a',
description: 'A',
collections: collections as any,
},
],
},
});
const fields = app.collectionManager.getCollectionFields('users', { dataSource: 'a' });
expect(fields).toEqual(users.fields);
});
test('getCollectionFields("collectionName.associationFieldName")', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const fields = app.collectionManager.getCollectionFields('users.roles');
expect(fields).toEqual(roles.fields);
});
test('getCollectionFields("not-exists")', () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const fields = app.collectionManager.getCollectionFields('not-exists');
expect(Array.isArray(fields)).toBeTruthy();
expect(fields.length).toBe(0);
});
});
});
describe('mixins', () => {
test('init should work', () => {
class DemoCollectionMixin extends CollectionV2 {
a() {
return 'test-' + this.name;
}
}
const app = new Application({
collectionManager: {
collections: collections as any,
collectionMixins: [DemoCollectionMixin],
},
});
const user = app.collectionManager.getCollection<DemoCollectionMixin>('users');
expect(user.a()).toBe('test-users');
});
test('plugin should work', async () => {
class DemoCollectionMixin extends CollectionV2 {
b() {
return 'test-' + this.name;
}
}
class MyPlugin extends Plugin {
async load() {
this.app.collectionManager.addCollections(collections as any);
this.app.collectionManager.addCollectionMixins([DemoCollectionMixin]);
}
}
const app = new Application({
plugins: [MyPlugin],
});
await app.load();
const user = app.collectionManager.getCollection<DemoCollectionMixin>('users');
expect(user.b()).toBe('test-users');
});
test('multiple mixins should work', () => {
class DemoCollectionMixin1 extends CollectionV2 {
c() {
return 'test1-' + this.name;
}
}
class DemoCollectionMixin2 extends CollectionV2 {
d() {
return 'test2-' + this.name;
}
}
const app = new Application({
collectionManager: {
collections: collections as any,
collectionMixins: [DemoCollectionMixin1, DemoCollectionMixin2],
},
});
const user = app.collectionManager.getCollection<DemoCollectionMixin1 & DemoCollectionMixin2>('users');
expect(user.c()).toBe('test1-users');
expect(user.d()).toBe('test2-users');
});
test('after add mixins, collection should be re-instantiated', () => {
class DemoCollectionMixin extends CollectionV2 {
e() {
return 'test-' + this.name;
}
}
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const user = app.collectionManager.getCollection<DemoCollectionMixin>('users');
expect(user.e).toBeUndefined();
app.collectionManager.addCollectionMixins([DemoCollectionMixin]);
const user2 = app.collectionManager.getCollection<DemoCollectionMixin>('users');
expect(user2.e()).toBe('test-users');
});
});
describe('templates', () => {
test('init should work', () => {
class DemoTemplate extends CollectionTemplateBase {
name = 'demo';
title = 'Demo';
}
const app = new Application({
collectionManager: {
collectionTemplates: [DemoTemplate],
},
});
const templates = app.collectionManager.getCollectionTemplates();
expect(templates.length).toBe(1);
expect(templates[0].name).toBe('demo');
});
test('plugin should work', async () => {
class DemoTemplate extends CollectionTemplateBase {
name = 'demo';
title = 'Demo';
}
class MyPlugin extends Plugin {
async load() {
this.app.collectionManager.addCollectionTemplates([DemoTemplate]);
}
}
const app = new Application({
plugins: [MyPlugin],
});
await app.load();
const templates = app.collectionManager.getCollectionTemplates();
expect(templates.length).toBe(1);
expect(templates[0].name).toBe('demo');
});
test('If the Template has a Collection property and the template property of collections is equal to Template.name, use the custom Collection for initialization', () => {
class CustomCollection extends CollectionV2 {
custom() {
return 'custom-' + this.name;
}
}
class DemoTemplate extends CollectionTemplateBase {
name = 'demo';
title = 'Demo';
Collection = CustomCollection;
}
const app = new Application({
collectionManager: {
collections: collections.map((item) => ({ ...item, template: 'demo' })) as any,
collectionTemplates: [DemoTemplate],
},
});
const user = app.collectionManager.getCollection<CustomCollection>('users');
expect(user.custom()).toBe('custom-users');
});
test('after add templates, collection should be re-instantiated', () => {
class CustomCollection extends CollectionV2 {
custom() {
return 'custom-' + this.name;
}
}
class DemoTemplate extends CollectionTemplateBase {
name = 'demo';
title = 'Demo';
Collection = CustomCollection;
}
const app = new Application({
collectionManager: {
collections: collections.map((item) => ({ ...item, template: 'demo' })) as any,
},
});
const user = app.collectionManager.getCollection<CustomCollection>('users');
expect(user.custom).toBeUndefined();
app.collectionManager.addCollectionTemplates([DemoTemplate]);
const user2 = app.collectionManager.getCollection<CustomCollection>('users');
expect(user2.custom()).toBe('custom-users');
});
test('getCollectionTemplate', () => {
class DemoTemplate extends CollectionTemplateBase {
name = 'demo';
title = 'Demo';
}
const app = new Application({
collectionManager: {
collectionTemplates: [DemoTemplate],
},
});
const template = app.collectionManager.getCollectionTemplate('demo');
expect(template.name).toBe('demo');
});
test('transformCollection', () => {
const mockFn = vitest.fn();
class DemoTemplate extends CollectionTemplateBase {
name = 'demo';
title = 'Demo';
transform(collection) {
mockFn(collection);
return collection;
}
}
new Application({
collectionManager: {
collections: collections.map((item) => ({ ...item, template: 'demo' })) as any,
collectionTemplates: [DemoTemplate],
},
});
expect(mockFn).toBeCalledTimes(collectionLength);
});
});
describe('field interface', () => {
test('init should work', () => {
class DemoFieldInterface extends CollectionFieldInterfaceBase {
name = 'demo';
title = 'Demo';
}
const app = new Application({
collectionManager: {
fieldInterfaces: [DemoFieldInterface],
},
});
const fieldInterfaces = app.collectionManager.getFieldInterfaces();
expect(Object.keys(fieldInterfaces).length).toBe(1);
expect(fieldInterfaces.demo instanceof DemoFieldInterface).toBeTruthy();
});
test('plugin should work', async () => {
class DemoFieldInterface extends CollectionFieldInterfaceBase {
name = 'demo';
title = 'Demo';
}
class MyPlugin extends Plugin {
async load() {
this.app.collectionManager.addFieldInterfaces([DemoFieldInterface]);
}
}
const app = new Application({
plugins: [MyPlugin],
});
await app.load();
const fieldInterfaces = app.collectionManager.getFieldInterfaces();
expect(Object.keys(fieldInterfaces).length).toBe(1);
expect(fieldInterfaces.demo instanceof DemoFieldInterface).toBeTruthy();
});
test('getFieldInterface()', () => {
class DemoFieldInterface extends CollectionFieldInterfaceBase {
name = 'demo';
title = 'Demo';
}
const app = new Application({
collectionManager: {
fieldInterfaces: [DemoFieldInterface],
},
});
const fieldInterface = app.collectionManager.getFieldInterface('demo');
expect(fieldInterface.name).toBe('demo');
expect(fieldInterface instanceof DemoFieldInterface).toBeTruthy();
});
test('getFieldInterfaces()', () => {
class DemoFieldInterface extends CollectionFieldInterfaceBase {
name = 'demo';
title = 'Demo';
}
class Demo2FieldInterface extends CollectionFieldInterfaceBase {
name = 'demo2';
title = 'Demo';
}
const app = new Application({
collectionManager: {
fieldInterfaces: [DemoFieldInterface, Demo2FieldInterface],
},
});
const fieldInterfaces = app.collectionManager.getFieldInterfaces();
expect(Object.keys(fieldInterfaces).length).toBe(2);
});
});
describe('field groups', () => {
test('init add', () => {
const app = new Application({
collectionManager: {
fieldGroups: {
demo: {
label: 'Demo',
order: 1,
},
},
},
});
const fieldGroups = app.collectionManager.getFieldGroups();
expect(Object.keys(fieldGroups).length).toBe(1);
expect(fieldGroups.demo).toBeTruthy();
});
test('plugin add', async () => {
class MyPlugin extends Plugin {
async load() {
this.app.collectionManager.addFieldGroups({
demo: {
label: 'Demo',
order: 1,
},
});
}
}
const app = new Application({
plugins: [MyPlugin],
});
await app.load();
const fieldGroups = app.collectionManager.getFieldGroups();
expect(Object.keys(fieldGroups).length).toBe(1);
expect(fieldGroups.demo).toBeTruthy();
});
test('getFieldGroup(name)', () => {
const app = new Application({
collectionManager: {
fieldGroups: {
demo: {
label: 'Demo',
order: 1,
},
},
},
});
const fieldGroup = app.collectionManager.getFieldGroup('demo');
expect(fieldGroup.label).toBe('Demo');
});
});
describe('dataSource and reload', () => {
test('reload main', async () => {
const app = new Application({
collectionManager: {
collections: [collections[0]] as any,
},
});
const mainDataSourceFn = () => {
return Promise.resolve([collections[1]] as any);
};
const mockFn = vitest.fn();
const mockFn2 = vitest.fn();
app.collectionManager.setMainDataSource(mainDataSourceFn);
app.collectionManager.addReloadCallback(mockFn);
await app.collectionManager.reloadMain(mockFn2);
const collectionInstances = app.collectionManager.getCollections();
expect(collectionInstances.length).toBe(1);
expect(collectionInstances[0].name).toBe(collections[1]['name']);
expect(mockFn).toBeCalledTimes(1);
expect(mockFn2).toBeCalledTimes(1);
});
test('reload third dataSource', async () => {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
const dataSourceFn = () => {
return Promise.resolve([
{
name: 'a',
description: 'a',
collections: collections as any,
},
]);
};
const mockFn = vitest.fn();
const mockFn2 = vitest.fn();
app.collectionManager.setThirdDataSource(dataSourceFn);
app.collectionManager.addReloadCallback(mockFn, 'a');
await app.collectionManager.reloadThirdDataSource(mockFn2);
expect(app.collectionManager.getCollections().length).toBe(collectionLength);
expect(app.collectionManager.getCollections({ dataSource: 'a' }).length).toBe(collectionLength);
expect(app.collectionManager.getDataSources().length).toBe(2);
expect(app.collectionManager.getDataSource('a').name).toBe('a');
expect(mockFn).toBeCalledTimes(1);
expect(mockFn2).toBeCalledTimes(1);
});
test('reload all', async () => {
const app = new Application();
app.collectionManager.setMainDataSource(() => {
return Promise.resolve(collections as any);
});
app.collectionManager.setThirdDataSource(() => {
return Promise.resolve([
{
name: 'a',
description: 'a',
collections: collections as any,
},
]);
});
const mockFn = vitest.fn();
const mockFn2 = vitest.fn();
app.collectionManager.addReloadCallback(mockFn);
app.collectionManager.addReloadCallback(mockFn, 'a');
await app.collectionManager.reloadAll(mockFn2);
expect(app.collectionManager.getCollections().length).toBe(collectionLength);
expect(app.collectionManager.getCollections({ dataSource: 'a' }).length).toBe(collectionLength);
expect(app.collectionManager.getDataSources().length).toBe(2);
expect(app.collectionManager.getDataSource('a').name).toBe('a');
expect(mockFn).toBeCalledTimes(2);
expect(mockFn2).toBeCalledTimes(1);
});
test('not set reload fn', async () => {
const app = new Application();
const mockFn = vitest.fn();
const mockFn2 = vitest.fn();
app.collectionManager.addReloadCallback(mockFn);
app.collectionManager.addReloadCallback(mockFn, 'a');
await app.collectionManager.reloadAll(mockFn2);
expect(mockFn).toBeCalledTimes(0);
expect(mockFn2).toBeCalledTimes(1);
});
});
describe('inherit', () => {
test('inherit', async () => {
const app = new Application({
collectionManager: {
collections: [collections[0]] as any,
},
});
const mockFn = vitest.fn();
const mainDataSourceFn = () => {
return Promise.resolve([collections[0]] as any);
};
app.collectionManager.setMainDataSource(mainDataSourceFn);
const cm = app.collectionManager.inherit({
collections: [collections[1]] as any,
reloadCallback: mockFn,
});
expect(cm.getCollections().length).toBe(2);
await cm.reloadAll();
expect(cm.getCollections().length).toBe(1);
expect(mockFn).toBeCalledTimes(1);
expect(app.collectionManager.getCollections().length).toBe(1);
});
});
});

View File

@ -0,0 +1,80 @@
import React, { ComponentType, useEffect } from 'react';
import { render, screen } from '@nocobase/test/client';
import {
CollectionManagerProviderV2,
CollectionManagerV2,
useCollectionManagerV2,
useCollectionsV2,
} from '../../collection';
import { Application } from '../../Application';
import collections from './collections.json';
function renderApp(Demo: ComponentType) {
const app = new Application({
collectionManager: {
collections: collections as any,
dataSources: [
{
name: 'a',
description: 'a',
collections: [collections[0]] as any,
},
],
},
});
return render(
<CollectionManagerProviderV2 collectionManager={app.collectionManager}>
<Demo></Demo>
</CollectionManagerProviderV2>,
);
}
describe('CollectionManagerProvider', () => {
test('should render', () => {
const Demo = () => {
const cm = useCollectionManagerV2();
useEffect(() => {
expect(cm instanceof CollectionManagerV2).toBeTruthy();
}, [cm]);
const users = cm.getCollection('users');
return <div data-testid="demo">{users.name}</div>;
};
renderApp(Demo);
expect(screen.getByTestId('demo')).toHaveTextContent('users');
});
test('useCollectionsV2()', () => {
const Demo = () => {
const collections = useCollectionsV2();
return <div data-testid="demo">{collections.length}</div>;
};
renderApp(Demo);
expect(screen.getByTestId('demo')).toHaveTextContent('2');
});
test('useCollectionsV2({ predicate })', () => {
const Demo = () => {
const collections = useCollectionsV2({
predicate: (collection) => collection.name === 'users',
});
return <div data-testid="demo">{collections.length}</div>;
};
renderApp(Demo);
expect(screen.getByTestId('demo')).toHaveTextContent('1');
});
test('useCollectionsV2({ dataSource })', () => {
const Demo = () => {
const collections = useCollectionsV2({
dataSource: 'a',
});
return <div data-testid="demo">{collections.length}</div>;
};
renderApp(Demo);
expect(screen.getByTestId('demo')).toHaveTextContent('1');
});
});

View File

@ -0,0 +1,89 @@
import React, { ComponentType } from 'react';
import { render, screen } from '@nocobase/test/client';
import {
CollectionManagerProviderV2,
CollectionProviderV2,
useCollectionFieldsV2,
useCollectionV2,
} from '../../collection';
import { Application } from '../../Application';
import collections from './collections.json';
import { SchemaComponentProvider } from '../../../schema-component';
function renderApp(Demo: ComponentType, props: any = {}) {
const app = new Application({
collectionManager: {
collections: collections as any,
},
});
return render(
<div data-testid="app">
<SchemaComponentProvider designable={true}>
<CollectionManagerProviderV2 collectionManager={app.collectionManager}>
<CollectionProviderV2 {...props}>
<Demo></Demo>
</CollectionProviderV2>
</CollectionManagerProviderV2>
</SchemaComponentProvider>
</div>,
);
}
describe('CollectionProvider', () => {
test('should render', () => {
const Demo = () => {
const collection = useCollectionV2();
const collectionFields = useCollectionFieldsV2();
return (
<>
<div data-testid="name">{collection.name}</div>
<div data-testid="fields">{collectionFields.length}</div>
</>
);
};
renderApp(Demo, { name: 'users' });
expect(screen.getByTestId('name')).toHaveTextContent('users');
const usersOptions = collections.find((item) => item.name === 'users');
expect(screen.getByTestId('fields')).toHaveTextContent(String(usersOptions.fields.length));
});
test('collection not exists and { allowNull: true }, should render children', () => {
const Demo = () => {
const collection = useCollectionV2();
expect(collection).toBeFalsy();
return <div data-testid="children">children</div>;
};
renderApp(Demo, { name: 'not-exists', allowNull: true });
expect(screen.getByTestId('children')).toHaveTextContent('children');
});
test('collection not exists and { allowNull: false }, should render `DeletedPlaceholder` content', () => {
const Demo = () => {
const collection = useCollectionV2();
expect(collection).toBeFalsy();
return <div>children</div>;
};
renderApp(Demo, { name: 'not-exists', allowNull: false });
expect(screen.getByText(`Collection: "not-exists" not exists`)).toBeInTheDocument();
});
test('useCollectionFieldsV2() support predicate', () => {
const Demo = () => {
const fields = useCollectionFieldsV2({ name: 'id' });
return <div data-testid="fields">{fields.length}</div>;
};
renderApp(Demo, { name: 'users' });
expect(screen.getByTestId('fields')).toHaveTextContent('1');
});
});

View File

@ -0,0 +1,69 @@
import React from 'react';
import { render, screen } from '@nocobase/test/client';
import { DeletedPlaceholder } from '../../collection/DeletedPlaceholder';
import { SchemaComponentProvider } from '../../../schema-component';
function renderApp(name?: any, designable?: boolean) {
render(
<div data-testid="app">
<SchemaComponentProvider designable={designable}>
<DeletedPlaceholder type="test" name={name}></DeletedPlaceholder>
</SchemaComponentProvider>
</div>,
);
}
describe('DeletedPlaceholder', () => {
test('name is undefined, render nothing', () => {
renderApp();
expect(screen.getByTestId('app').innerHTML.length).toBe(0);
});
describe('name exist', () => {
test("designable: true & process.env.NODE_ENV === 'development', render `Result` component", () => {
const NODE_ENV = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
renderApp('test', true);
process.env.NODE_ENV = NODE_ENV;
expect(screen.getByTestId('app').innerHTML).toContain('ant-result');
});
test("designable: false & process.env.NODE_ENV === 'development', render `Result` component", () => {
const NODE_ENV = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
renderApp('test', false);
process.env.NODE_ENV = NODE_ENV;
expect(screen.getByTestId('app').innerHTML).toContain('ant-result');
});
test("designable: true & process.env.NODE_ENV !== 'development', render `Result` component", () => {
const NODE_ENV = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
renderApp('test', true);
process.env.NODE_ENV = NODE_ENV;
expect(screen.getByTestId('app').innerHTML).toContain('ant-result');
});
test("designable: false & process.env.NODE_ENV !== 'development', render nothing", () => {
const NODE_ENV = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
renderApp('test', false);
process.env.NODE_ENV = NODE_ENV;
expect(screen.getByTestId('app').innerHTML.length).toBe(0);
});
});
});

View File

@ -0,0 +1,14 @@
import { RecordV2 } from '../../collection';
describe('Record', () => {
test('should works', () => {
const record = new RecordV2<{ id: number }, { name: string }>({ data: { id: 1 } });
expect(record.data.id).toBe(1);
record.setData({ id: 2 });
expect(record.data.id).toBe(2);
record.setParentRecord(new RecordV2({ data: { name: 'a' } }));
expect(record.parentRecord.data.name).toBe('a');
});
});

View File

@ -0,0 +1,200 @@
import React from 'react';
import { render, screen } from '@nocobase/test/client';
import {
RecordProviderV2,
RecordV2,
useParentRecordDataV2,
useParentRecordV2,
useRecordDataV2,
useRecordV2,
} from '../../collection';
describe('RecordProvider', () => {
describe('record and parentRecord', () => {
test('record parameter is a `Record` instance', () => {
const Demo = () => {
const record = useRecordV2();
return <pre data-testid="content">{JSON.stringify(record)}</pre>;
};
const record = new RecordV2({ data: { id: 1, name: 'foo' } });
render(
<RecordProviderV2 record={record}>
<Demo />
</RecordProviderV2>,
);
expect(screen.getByTestId('content')).toHaveTextContent(JSON.stringify({ id: 1, name: 'foo' }));
});
test('record parameter is a `plain object`', () => {
const Demo = () => {
const record = useRecordV2();
return <pre data-testid="content">{JSON.stringify(record)}</pre>;
};
render(
<RecordProviderV2 record={{ id: 1, name: 'foo' }}>
<Demo />
</RecordProviderV2>,
);
expect(screen.getByTestId('content')).toHaveTextContent(JSON.stringify({ id: 1, name: 'foo' }));
});
test('record parameter is a `Record` instance with parent record', () => {
const Demo = () => {
const record = useRecordV2();
return <pre data-testid="content">{JSON.stringify(record)}</pre>;
};
const parentRecord = new RecordV2({ data: { id: 1, role: 'admin' } });
const record = new RecordV2({ data: { id: 1, name: 'foo' }, parentRecord });
render(
<RecordProviderV2 record={record}>
<Demo />
</RecordProviderV2>,
);
expect(screen.getByTestId('content')).toHaveTextContent(
JSON.stringify({
data: {
id: 1,
name: 'foo',
},
parentRecord: {
data: {
id: 1,
role: 'admin',
},
},
}),
);
});
test('record parameter is a `Record` instance, parent record is passed through parentRecord parameter', () => {
const Demo = () => {
const record = useRecordV2();
return <pre data-testid="content">{JSON.stringify(record)}</pre>;
};
const parentRecord = new RecordV2({ data: { id: 1, role: 'admin' } });
const record = new RecordV2({ data: { id: 1, name: 'foo' } });
render(
<RecordProviderV2 record={record} parentRecord={parentRecord}>
<Demo />
</RecordProviderV2>,
);
expect(screen.getByTestId('content')).toHaveTextContent(
JSON.stringify({
data: {
id: 1,
name: 'foo',
},
parentRecord: {
data: {
id: 1,
role: 'admin',
},
},
}),
);
});
test('record parameter is a `plain object`, parent record is also a `plain object`', () => {
const Demo = () => {
const record = useRecordV2();
return <pre data-testid="content">{JSON.stringify(record)}</pre>;
};
render(
<RecordProviderV2 record={{ id: 1, name: 'foo' }} parentRecord={{ id: 1, role: 'admin' }}>
<Demo />
</RecordProviderV2>,
);
expect(screen.getByTestId('content')).toHaveTextContent(
JSON.stringify({
data: {
id: 1,
name: 'foo',
},
parentRecord: {
data: {
id: 1,
role: 'admin',
},
},
}),
);
});
});
describe('hooks', () => {
test('useRecordDataV2()', () => {
const Demo = () => {
const data = useRecordDataV2();
return <pre data-testid="content">{JSON.stringify(data)}</pre>;
};
const parentRecord = new RecordV2({ data: { id: 1, role: 'admin' } });
const record = new RecordV2({ data: { id: 1, name: 'foo' }, parentRecord });
render(
<RecordProviderV2 record={record}>
<Demo />
</RecordProviderV2>,
);
expect(screen.getByTestId('content')).toHaveTextContent(JSON.stringify({ id: 1, name: 'foo' }));
});
test('useParentRecordV2()', () => {
const Demo = () => {
const data = useParentRecordV2();
return <pre data-testid="content">{JSON.stringify(data)}</pre>;
};
const parentRecord = new RecordV2({ data: { id: 1, role: 'admin' } });
const record = new RecordV2({ data: { id: 1, name: 'foo' }, parentRecord });
render(
<RecordProviderV2 record={record}>
<Demo />
</RecordProviderV2>,
);
expect(screen.getByTestId('content')).toHaveTextContent(
JSON.stringify({
data: {
id: 1,
role: 'admin',
},
}),
);
});
test('useParentRecordDataV2()', () => {
const Demo = () => {
const data = useParentRecordDataV2();
return <pre data-testid="content">{JSON.stringify(data)}</pre>;
};
const parentRecord = new RecordV2({ data: { id: 1, role: 'admin' } });
const record = new RecordV2({ data: { id: 1, name: 'foo' }, parentRecord });
render(
<RecordProviderV2 record={record}>
<Demo />
</RecordProviderV2>,
);
expect(screen.getByTestId('content')).toHaveTextContent(
JSON.stringify({
id: 1,
role: 'admin',
}),
);
});
});
});

View File

@ -0,0 +1,198 @@
[
{
"key": "h7b9i8khc3q",
"name": "users",
"inherit": false,
"hidden": false,
"description": null,
"category": [],
"namespace": "users.users",
"duplicator": {
"dumpable": "optional",
"with": "rolesUsers"
},
"sortable": "sort",
"model": "UserModel",
"createdBy": true,
"updatedBy": true,
"logging": true,
"from": "db2cm",
"title": "{{t(\"Users\")}}",
"rawTitle": "{{t(\"Users\")}}",
"fields": [
{
"uiSchema": {
"type": "number",
"title": "{{t(\"ID\")}}",
"x-component": "InputNumber",
"x-read-pretty": true,
"rawTitle": "{{t(\"ID\")}}"
},
"key": "ffp1f2sula0",
"name": "id",
"type": "bigInt",
"interface": "id",
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"autoIncrement": true,
"primaryKey": true,
"allowNull": false
},
{
"uiSchema": {
"type": "string",
"title": "{{t(\"Nickname\")}}",
"x-component": "Input",
"rawTitle": "{{t(\"Nickname\")}}"
},
"key": "vrv7yjue90g",
"name": "nickname",
"type": "string",
"interface": "input",
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null
},
{
"uiSchema": {
"type": "string",
"title": "{{t(\"Username\")}}",
"x-component": "Input",
"x-validator": {
"username": true
},
"required": true,
"rawTitle": "{{t(\"Username\")}}"
},
"key": "2ccs6evyrub",
"name": "username",
"type": "string",
"interface": "input",
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"unique": true
},
{
"uiSchema": {
"type": "string",
"title": "{{t(\"Email\")}}",
"x-component": "Input",
"x-validator": "email",
"required": true,
"rawTitle": "{{t(\"Email\")}}"
},
"key": "rrskwjl5wt1",
"name": "email",
"type": "string",
"interface": "email",
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"unique": true
},
{
"key": "t09bauwm0wb",
"name": "roles",
"type": "belongsToMany",
"interface": "m2m",
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"target": "roles",
"foreignKey": "userId",
"otherKey": "roleName",
"onDelete": "CASCADE",
"sourceKey": "id",
"targetKey": "name",
"through": "rolesUsers",
"uiSchema": {
"type": "array",
"title": "{{t(\"Roles\")}}",
"x-component": "AssociationField",
"x-component-props": {
"multiple": true,
"fieldNames": {
"label": "title",
"value": "name"
}
}
}
}
]
},
{
"key": "pqnenvqrzxr",
"name": "roles",
"inherit": false,
"hidden": false,
"description": null,
"category": [],
"namespace": "acl.acl",
"duplicator": {
"dumpable": "required",
"with": "uiSchemas"
},
"autoGenId": false,
"model": "RoleModel",
"filterTargetKey": "name",
"sortable": true,
"from": "db2cm",
"title": "{{t(\"Roles\")}}",
"rawTitle": "{{t(\"Roles\")}}",
"fields": [
{
"uiSchema": {
"type": "string",
"title": "{{t(\"Role UID\")}}",
"x-component": "Input",
"rawTitle": "{{t(\"Role UID\")}}"
},
"key": "jbz9m80bxmp",
"name": "name",
"type": "uid",
"interface": "input",
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null,
"prefix": "r_",
"primaryKey": true
},
{
"uiSchema": {
"type": "string",
"title": "{{t(\"Role name\")}}",
"x-component": "Input",
"rawTitle": "{{t(\"Role name\")}}"
},
"key": "faywtz4sf3u",
"name": "title",
"type": "string",
"interface": "input",
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null,
"unique": true,
"translation": true
},
{
"key": "1enkovm9sye",
"name": "description",
"type": "string",
"interface": null,
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null
}
]
}
]

View File

@ -0,0 +1,46 @@
import { CollectionFieldInterfaceBase, isTitleField } from '../../collection';
import { Application } from '../../Application';
import collections from './collections.json';
describe('utils', () => {
describe('isTitleField', () => {
class Demo1FieldInterface extends CollectionFieldInterfaceBase {
name = 'demo1';
titleUsable = false;
}
class Demo2FieldInterface extends CollectionFieldInterfaceBase {
name = 'demo2';
titleUsable = true;
}
const cm = new Application({
collectionManager: {
collections: collections as any,
fieldInterfaces: [Demo1FieldInterface, Demo2FieldInterface],
},
}).collectionManager;
it('should return false when field is foreign key', () => {
const field = {
isForeignKey: true,
};
expect(isTitleField(cm, field)).toBeFalsy();
});
it('should return false when field interface is not title usable', () => {
const field = {
isForeignKey: false,
interface: 'demo1',
};
expect(isTitleField(cm, field)).toBeFalsy();
});
it('should return true when field is not foreign key and field interface is title usable', () => {
const field = {
isForeignKey: false,
interface: 'demo2',
};
expect(isTitleField(cm, field)).toBeTruthy();
});
});
});

View File

@ -0,0 +1,20 @@
import React from 'react';
import { render, screen } from '@nocobase/test/client';
import { CollectionDataSourceProvider, useCollectionDataSourceName } from '../../data-block';
describe('CollectionDataSourceProvider', () => {
test('should work', () => {
const Demo = () => {
const name = useCollectionDataSourceName();
return <div data-testid="content">{name}</div>;
};
render(
<CollectionDataSourceProvider dataSource={'test'}>
<Demo />
</CollectionDataSourceProvider>,
);
expect(screen.getByTestId('content')).toHaveTextContent('test');
});
});

View File

@ -0,0 +1,143 @@
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@nocobase/test/client';
import CollectionTableListDemo from './data-block-demos/collection-table-list';
import CollectionFormGetAndUpdateDemo from './data-block-demos/collection-form-get-and-update';
import CollectionFormCreateDemo from './data-block-demos/collection-form-create';
import CollectionFormRecordAndUpdateDemo from './data-block-demos/collection-form-record-and-update';
import AssociationTableListAndSourceIdDemo from './data-block-demos/association-table-list-and-source-id';
import AssociationTableListAndParentRecordDemo from './data-block-demos/association-table-list-and-parent-record';
describe('CollectionDataSourceProvider', () => {
describe('demos', () => {
describe('collection', () => {
test('Table list', async () => {
const { getByText, getByRole } = render(<CollectionTableListDemo />);
// app loading
await waitFor(() => {
expect(getByRole('table')).toBeInTheDocument();
});
expect(getByText('UserName')).toBeInTheDocument();
expect(getByText('NickName')).toBeInTheDocument();
expect(getByText('Email')).toBeInTheDocument();
// loading table data
await waitFor(() => {
const columns = screen.getByRole('table').querySelectorAll('tbody tr');
expect(columns.length).toBe(3);
});
expect(getByText('jack')).toBeInTheDocument();
expect(getByText('Jack Ma')).toBeInTheDocument();
expect(getByText('test@gmail.com')).toBeInTheDocument();
});
test('Form get & update', async () => {
render(<CollectionFormGetAndUpdateDemo />);
await waitFor(() => {
expect(screen.getByText('Username')).toBeInTheDocument();
expect(screen.getByText('Age')).toBeInTheDocument();
});
// load form data success
await waitFor(() => {
expect(document.getElementById('username')).toHaveValue('Bamboo');
expect(document.getElementById('age')).toHaveValue('18');
});
fireEvent.click(document.querySelector('button'));
await waitFor(() => {
expect(screen.getByText('Save successfully!')).toBeInTheDocument();
});
});
test('Form create', async () => {
render(<CollectionFormCreateDemo />);
await waitFor(() => {
expect(screen.getByText('Username')).toBeInTheDocument();
expect(screen.getByText('Age')).toBeInTheDocument();
});
fireEvent.change(document.getElementById('username'), { target: { value: 'Bamboo' } });
fireEvent.change(document.getElementById('age'), { target: { value: '18' } });
fireEvent.click(document.querySelector('button'));
await waitFor(() => {
expect(screen.getByText('Save successfully!')).toBeInTheDocument();
});
});
test('Form record & update', async () => {
render(<CollectionFormRecordAndUpdateDemo />);
await waitFor(() => {
expect(screen.getByText('Username')).toBeInTheDocument();
expect(screen.getByText('Age')).toBeInTheDocument();
});
await waitFor(() => {
expect(document.getElementById('username')).toHaveValue('Bamboo');
expect(document.getElementById('age')).toHaveValue('18');
});
fireEvent.click(document.querySelector('button'));
await waitFor(() => {
expect(screen.getByText('Save successfully!')).toBeInTheDocument();
});
});
});
describe('association', () => {
test('Table list & sourceId', async () => {
const { getByText, getByRole } = render(<AssociationTableListAndSourceIdDemo />);
// app loading
await waitFor(() => {
expect(getByRole('table')).toBeInTheDocument();
});
expect(getByText('Name')).toBeInTheDocument();
expect(getByText('Title')).toBeInTheDocument();
expect(getByText('Description')).toBeInTheDocument();
// loading table data
await waitFor(() => {
const columns = screen.getByRole('table').querySelectorAll('tbody tr');
expect(columns.length).toBe(2);
});
expect(getByText('admin')).toBeInTheDocument();
expect(getByText('Admin')).toBeInTheDocument();
expect(getByText('Admin description')).toBeInTheDocument();
});
test('Table list & parentRecord', async () => {
const { getByText, getByRole } = render(<AssociationTableListAndParentRecordDemo />);
// app loading
await waitFor(() => {
expect(getByRole('table')).toBeInTheDocument();
});
expect(getByText('Name')).toBeInTheDocument();
expect(getByText('Title')).toBeInTheDocument();
expect(getByText('Description')).toBeInTheDocument();
// loading table data
await waitFor(() => {
const columns = screen.getByRole('table').querySelectorAll('tbody tr');
expect(columns.length).toBe(2);
});
expect(getByText('admin')).toBeInTheDocument();
expect(getByText('Admin')).toBeInTheDocument();
expect(getByText('Admin description')).toBeInTheDocument();
});
});
});
});

View File

@ -0,0 +1,96 @@
import React from 'react';
import { Table, TableProps } from 'antd';
import { SchemaComponent, UseDataBlockProps, useDataBlockRequestV2, withDynamicSchemaProps } from '@nocobase/client';
import { ISchema } from '@formily/json-schema';
import { createApp } from './createApp';
const collection = 'users';
const associationField = 'roles';
const association = `${collection}.${associationField}`;
const action = 'list';
const schema: ISchema = {
type: 'void',
name: 'root',
'x-decorator': 'DataBlockProviderV2',
'x-use-decorator-props': 'useBlockDecoratorProps',
'x-decorator-props': {
association,
action,
},
'x-component': 'CardItem',
properties: {
demo: {
type: 'array',
'x-component': 'MyTable',
'x-use-component-props': 'useTableProps',
},
},
};
const MyTable = withDynamicSchemaProps(Table);
function useTableProps(): TableProps<any> {
const { data, loading } = useDataBlockRequestV2<any[]>();
return {
loading,
dataSource: data?.data || [],
columns: [
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Title',
dataIndex: 'title',
},
{
title: 'Description',
dataIndex: 'description',
},
],
};
}
const useBlockDecoratorProps: UseDataBlockProps<'CollectionList'> = () => {
const parentRecord = {
id: 1,
username: 'Tom',
};
return {
parentRecord,
};
};
const Demo = () => {
return <SchemaComponent schema={schema}></SchemaComponent>;
};
const mocks = {
[`${collection}/1/${associationField}:${action}`]: {
data: [
{
name: 'admin',
title: 'Admin',
description: 'Admin description',
},
{
name: 'developer',
title: 'Developer',
description: 'Developer description',
},
],
},
};
const Root = createApp(
Demo,
{
components: { MyTable },
scopes: { useTableProps, useBlockDecoratorProps },
},
mocks,
);
export default Root;

View File

@ -0,0 +1,132 @@
import React from 'react';
import { Select, Table, TableProps } from 'antd';
import { SchemaComponent, UseDataBlockProps, useDataBlockRequestV2, withDynamicSchemaProps } from '@nocobase/client';
import { ISchema } from '@formily/json-schema';
import useUrlState from '@ahooksjs/use-url-state';
import { createApp } from './createApp';
const collection = 'users';
const associationField = 'roles';
const association = `${collection}.${associationField}`;
const action = 'list';
const schema: ISchema = {
type: 'void',
name: 'root',
'x-decorator': 'DataBlockProviderV2',
'x-use-decorator-props': 'useBlockDecoratorProps',
'x-decorator-props': {
association,
action,
},
'x-component': 'CardItem',
properties: {
demo: {
type: 'array',
'x-component': 'MyTable',
'x-use-component-props': 'useTableProps',
},
},
};
const MyTable = withDynamicSchemaProps(Table);
function useTableProps(): TableProps<any> {
const { data, loading } = useDataBlockRequestV2<any[]>();
return {
loading,
dataSource: data?.data || [],
columns: [
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Title',
dataIndex: 'title',
},
{
title: 'Description',
dataIndex: 'description',
},
],
};
}
const useBlockDecoratorProps: UseDataBlockProps<'CollectionList'> = () => {
const [state] = useUrlState({ userId: '1' });
return {
sourceId: state.userId,
};
};
const Demo = () => {
const [state, setState] = useUrlState({ userId: '1' });
return (
<>
<Select
defaultValue={state.userId}
options={[
{ key: 1, value: '1', label: 'Tom' },
{ key: 2, value: '2', label: 'Jack' },
]}
onChange={(v) => {
setState({ userId: v });
}}
></Select>
<SchemaComponent schema={schema}></SchemaComponent>
</>
);
};
const mocks = {
[`${collection}/1/${associationField}:${action}`]: {
data: [
{
name: 'admin',
title: 'Admin',
description: 'Admin description',
},
{
name: 'developer',
title: 'Developer',
description: 'Developer description',
},
],
},
[`${collection}/2/${associationField}:${action}`]: {
data: [
{
name: 'developer',
title: 'Developer',
description: 'Developer description',
},
{
name: 'tester',
title: 'Tester',
description: 'Tester description',
},
],
},
[`${collection}:get/1`]: {
id: 1,
username: 'Tom',
},
[`${collection}:get/2`]: {
id: 1,
username: 'Jack',
},
};
const Root = createApp(
Demo,
{
components: { MyTable },
scopes: { useTableProps, useBlockDecoratorProps },
},
mocks,
);
export default Root;

View File

@ -0,0 +1,94 @@
import React, { FC } from 'react';
import { Button, Form, FormProps, Input, InputNumber, notification } from 'antd';
import { SchemaComponent, useDataBlockResourceV2, withDynamicSchemaProps } from '@nocobase/client';
import { ISchema } from '@formily/json-schema';
import { createApp } from './createApp';
interface DemoFormFieldType {
id: number;
username: string;
age: number;
}
type DemoFormProps = FormProps<DemoFormFieldType>;
const DemoForm: FC<DemoFormProps> = withDynamicSchemaProps((props) => {
return (
<Form labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} style={{ maxWidth: 600 }} autoComplete="off" {...props}>
<Form.Item<DemoFormFieldType>
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
<Form.Item<DemoFormFieldType>
label="Age"
name="age"
rules={[{ required: true, message: 'Please input your age!' }]}
>
<InputNumber />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
});
function useDemoFormProps(): DemoFormProps {
const resource = useDataBlockResourceV2();
const onFinish = async (values: DemoFormFieldType) => {
console.log('values', values);
await resource.create({
values,
});
notification.success({
message: 'Save successfully!',
});
};
return {
onFinish,
};
}
const collection = 'users';
const schema: ISchema = {
type: 'void',
name: 'root',
'x-decorator': 'DataBlockProviderV2',
'x-decorator-props': {
collection: collection,
},
'x-component': 'CardItem',
properties: {
demo: {
type: 'object',
'x-component': 'DemoForm',
'x-use-component-props': 'useDemoFormProps',
},
},
};
const Demo = () => {
return <SchemaComponent schema={schema}></SchemaComponent>;
};
const mocks = {
[`${collection}:create`]: (config) => {
console.log('config.data', config.data);
return [200, { msg: 'ok' }];
},
};
const Root = createApp(
Demo,
{
components: { DemoForm },
scopes: { useDemoFormProps },
},
mocks,
);
export default Root;

View File

@ -0,0 +1,158 @@
import React, { FC, useEffect } from 'react';
import { Button, Form, FormProps, Input, InputNumber, Select, notification } from 'antd';
import {
SchemaComponent,
UseDataBlockProps,
useDataBlockResourceV2,
useRecordDataV2,
withDynamicSchemaProps,
} from '@nocobase/client';
import { ISchema } from '@formily/json-schema';
import useUrlState from '@ahooksjs/use-url-state';
import { createApp } from './createApp';
interface DemoFormFieldType {
id: number;
username: string;
age: number;
}
type DemoFormProps = FormProps<DemoFormFieldType>;
const DemoForm: FC<DemoFormProps> = withDynamicSchemaProps((props) => {
return (
<Form labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} style={{ maxWidth: 600 }} autoComplete="off" {...props}>
<Form.Item<DemoFormFieldType>
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
<Form.Item<DemoFormFieldType>
label="Age"
name="age"
rules={[{ required: true, message: 'Please input your age!' }]}
>
<InputNumber />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
});
function useDemoFormProps(): DemoFormProps {
const data = useRecordDataV2<DemoFormFieldType>();
const resource = useDataBlockResourceV2();
const [form] = Form.useForm();
useEffect(() => {
form.setFieldsValue(data);
}, [data, form]);
const onFinish = async (values: DemoFormFieldType) => {
console.log('values', values);
await resource.update({
filterByTk: data.id,
values,
});
notification.success({
message: 'Save successfully!',
});
};
return {
initialValues: data,
preserve: true,
onFinish,
form,
};
}
const useBlockDecoratorProps: UseDataBlockProps<'CollectionGet'> = () => {
const [state] = useUrlState({ id: '1' });
return {
filterByTk: state.id,
};
};
const collection = 'users';
const action = 'get';
const schema: ISchema = {
type: 'void',
name: 'root',
'x-decorator': 'DataBlockProviderV2',
'x-use-decorator-props': 'useBlockDecoratorProps',
'x-decorator-props': {
collection: collection,
action: action,
},
'x-component': 'CardItem',
properties: {
demo: {
type: 'object',
'x-component': 'DemoForm',
'x-use-component-props': 'useDemoFormProps',
},
},
};
const Demo = () => {
const [state, setState] = useUrlState({ id: '1' });
return (
<>
<Select
defaultValue={state.id}
options={[
{ key: 1, value: '1', label: 'Bamboo' },
{ key: 2, value: '2', label: 'Mary' },
]}
onChange={(v) => {
setState({ id: v });
}}
></Select>
<SchemaComponent schema={schema}></SchemaComponent>
</>
);
};
const mocks = {
[`${collection}:${action}`]: function (config) {
const { filterByTk } = config.params;
return {
data:
Number(filterByTk) === 1
? {
id: 1,
username: 'Bamboo',
age: 18,
}
: {
id: 2,
username: 'Mary',
age: 25,
},
};
},
[`${collection}:update`]: function (config) {
console.log('config.data', config.data);
return {
data: 'ok',
};
},
};
const Root = createApp(
Demo,
{
components: { DemoForm },
scopes: { useDemoFormProps, useBlockDecoratorProps },
},
mocks,
);
export default Root;

View File

@ -0,0 +1,136 @@
import React, { FC, useEffect } from 'react';
import { Button, Form, FormProps, Input, InputNumber, notification } from 'antd';
import {
RecordProviderV2,
SchemaComponent,
UseDataBlockProps,
useDataBlockResourceV2,
useRecordDataV2,
withDynamicSchemaProps,
} from '@nocobase/client';
import { ISchema } from '@formily/json-schema';
import { createApp } from './createApp';
interface DemoFormFieldType {
id: number;
username: string;
age: number;
}
type DemoFormProps = FormProps<DemoFormFieldType>;
const DemoForm: FC<DemoFormProps> = withDynamicSchemaProps((props) => {
return (
<Form labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} style={{ maxWidth: 600 }} autoComplete="off" {...props}>
<Form.Item<DemoFormFieldType>
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
<Form.Item<DemoFormFieldType>
label="Age"
name="age"
rules={[{ required: true, message: 'Please input your age!' }]}
>
<InputNumber />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
});
function useDemoFormProps(): DemoFormProps {
const data = useRecordDataV2<DemoFormFieldType>();
const resource = useDataBlockResourceV2();
const [form] = Form.useForm();
useEffect(() => {
form.setFieldsValue(data);
}, [data, form]);
const onFinish = async (values: DemoFormFieldType) => {
console.log('values', values);
await resource.update({
filterByTk: data.id,
values,
});
notification.success({
message: 'Save successfully!',
});
};
return {
initialValues: data,
preserve: true,
onFinish,
form,
};
}
const useFormBlockDecoratorProps: UseDataBlockProps<'CollectionRecord'> = () => {
const record = useRecordDataV2();
return {
record,
};
};
const collection = 'users';
const action = 'get';
const schema: ISchema = {
type: 'void',
name: 'root',
'x-decorator': 'DataBlockProviderV2',
'x-use-decorator-props': 'useFormBlockDecoratorProps',
'x-decorator-props': {
collection: collection,
action: action,
},
'x-component': 'CardItem',
properties: {
demo: {
type: 'object',
'x-component': 'DemoForm',
'x-use-component-props': 'useDemoFormProps',
},
},
};
const recordData = {
id: 1,
username: 'Bamboo',
age: 18,
};
const Demo = () => {
return (
<>
<RecordProviderV2 record={recordData}>
<SchemaComponent schema={schema}></SchemaComponent>
</RecordProviderV2>
</>
);
};
const mocks = {
[`${collection}:update`]: function (config) {
console.log('config.data', config.data);
return {
data: 'ok',
};
},
};
const Root = createApp(
Demo,
{
components: { DemoForm },
scopes: { useDemoFormProps, useFormBlockDecoratorProps },
},
mocks,
);
export default Root;

View File

@ -0,0 +1,100 @@
import React from 'react';
import { SchemaComponent, useDataBlockRequestV2, withDynamicSchemaProps } from '@nocobase/client';
import { Table, TableProps } from 'antd';
import { ISchema } from '@formily/json-schema';
import { createApp } from './createApp';
const schema: ISchema = {
type: 'void',
name: 'root',
'x-decorator': 'DataBlockProviderV2',
'x-decorator-props': {
collection: 'users',
action: 'list',
},
'x-component': 'CardItem',
properties: {
demo: {
type: 'array',
'x-component': 'MyTable',
'x-use-component-props': 'useTableProps', // 动态 table 属性
},
},
};
const MyTable = withDynamicSchemaProps(Table);
function useTableProps(): TableProps<any> {
const { data, loading } = useDataBlockRequestV2<any[]>();
return {
loading,
dataSource: data?.data || [],
columns: [
{
title: 'UserName',
dataIndex: 'username',
},
{
title: 'NickName',
dataIndex: 'nickname',
},
{
title: 'Email',
dataIndex: 'email',
},
],
};
}
const Demo = () => {
return <SchemaComponent schema={schema}></SchemaComponent>;
};
const mocks = {
'users:list': {
data: [
{
id: '1',
username: 'jack',
nickname: 'Jack Ma',
email: 'test@gmail.com',
},
{
id: '2',
username: 'jim',
nickname: 'Jim Green',
},
{
id: '3',
username: 'tom',
nickname: 'Tom Cat',
email: 'tom@gmail.com',
},
],
},
'roles:list': {
data: [
{
name: 'root',
title: 'Root',
description: 'Root',
},
{
name: 'admin',
title: 'Admin',
description: 'Admin description',
},
],
},
};
const App = createApp(
Demo,
{
components: { MyTable },
scopes: { useTableProps },
},
mocks,
);
export default App;

View File

@ -0,0 +1,198 @@
[
{
"key": "h7b9i8khc3q",
"name": "users",
"inherit": false,
"hidden": false,
"description": null,
"category": [],
"namespace": "users.users",
"duplicator": {
"dumpable": "optional",
"with": "rolesUsers"
},
"sortable": "sort",
"model": "UserModel",
"createdBy": true,
"updatedBy": true,
"logging": true,
"from": "db2cm",
"title": "{{t(\"Users\")}}",
"rawTitle": "{{t(\"Users\")}}",
"fields": [
{
"uiSchema": {
"type": "number",
"title": "{{t(\"ID\")}}",
"x-component": "InputNumber",
"x-read-pretty": true,
"rawTitle": "{{t(\"ID\")}}"
},
"key": "ffp1f2sula0",
"name": "id",
"type": "bigInt",
"interface": "id",
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"autoIncrement": true,
"primaryKey": true,
"allowNull": false
},
{
"uiSchema": {
"type": "string",
"title": "{{t(\"Nickname\")}}",
"x-component": "Input",
"rawTitle": "{{t(\"Nickname\")}}"
},
"key": "vrv7yjue90g",
"name": "nickname",
"type": "string",
"interface": "input",
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null
},
{
"uiSchema": {
"type": "string",
"title": "{{t(\"Username\")}}",
"x-component": "Input",
"x-validator": {
"username": true
},
"required": true,
"rawTitle": "{{t(\"Username\")}}"
},
"key": "2ccs6evyrub",
"name": "username",
"type": "string",
"interface": "input",
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"unique": true
},
{
"uiSchema": {
"type": "string",
"title": "{{t(\"Email\")}}",
"x-component": "Input",
"x-validator": "email",
"required": true,
"rawTitle": "{{t(\"Email\")}}"
},
"key": "rrskwjl5wt1",
"name": "email",
"type": "string",
"interface": "email",
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"unique": true
},
{
"key": "t09bauwm0wb",
"name": "roles",
"type": "belongsToMany",
"interface": "m2m",
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"target": "roles",
"foreignKey": "userId",
"otherKey": "roleName",
"onDelete": "CASCADE",
"sourceKey": "id",
"targetKey": "name",
"through": "rolesUsers",
"uiSchema": {
"type": "array",
"title": "{{t(\"Roles\")}}",
"x-component": "AssociationField",
"x-component-props": {
"multiple": true,
"fieldNames": {
"label": "title",
"value": "name"
}
}
}
}
]
},
{
"key": "pqnenvqrzxr",
"name": "roles",
"inherit": false,
"hidden": false,
"description": null,
"category": [],
"namespace": "acl.acl",
"duplicator": {
"dumpable": "required",
"with": "uiSchemas"
},
"autoGenId": false,
"model": "RoleModel",
"filterTargetKey": "name",
"sortable": true,
"from": "db2cm",
"title": "{{t(\"Roles\")}}",
"rawTitle": "{{t(\"Roles\")}}",
"fields": [
{
"uiSchema": {
"type": "string",
"title": "{{t(\"Role UID\")}}",
"x-component": "Input",
"rawTitle": "{{t(\"Role UID\")}}"
},
"key": "jbz9m80bxmp",
"name": "name",
"type": "uid",
"interface": "input",
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null,
"prefix": "r_",
"primaryKey": true
},
{
"uiSchema": {
"type": "string",
"title": "{{t(\"Role name\")}}",
"x-component": "Input",
"rawTitle": "{{t(\"Role name\")}}"
},
"key": "faywtz4sf3u",
"name": "title",
"type": "string",
"interface": "input",
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null,
"unique": true,
"translation": true
},
{
"key": "1enkovm9sye",
"name": "description",
"type": "string",
"interface": null,
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null
}
]
}
]

View File

@ -0,0 +1,49 @@
import {
ApplicationOptions,
Application,
CardItem,
CollectionManagerOptionsV2,
CollectionPlugin,
DataBlockProviderV2,
} from '@nocobase/client';
import MockAdapter from 'axios-mock-adapter';
import { ComponentType } from 'react';
import collections from './collections.json';
export function createApp(Demo: ComponentType<any>, options: ApplicationOptions = {}, mocks: Record<string, any>) {
const collectionManager = {
collections: collections as any,
...(options.collectionManager as CollectionManagerOptionsV2),
};
const app = new Application({
apiClient: {
baseURL: 'http://localhost:8000',
},
providers: [Demo],
...options,
collectionManager,
components: {
...options.components,
DataBlockProviderV2,
CardItem,
},
plugins: [CollectionPlugin],
designable: true,
});
const mock = new MockAdapter(app.apiClient.axios);
Object.entries(mocks).forEach(([url, data]) => {
mock.onGet(url).reply(async (config) => {
const res = typeof data === 'function' ? data(config) : data;
return [200, res];
});
mock.onPost(url).reply(async (config) => {
const res = typeof data === 'function' ? data(config) : data;
return [200, res];
});
});
const Root = app.getRootComponent();
return Root;
}

View File

@ -0,0 +1,145 @@
import React from 'react';
import { SchemaComponent, SchemaComponentProvider } from '../../../schema-component';
import { render, screen, sleep, userEvent, waitFor } from '@nocobase/test/client';
import { withDynamicSchemaProps } from '../../hoc';
const HelloComponent = withDynamicSchemaProps((props: any) => (
<pre data-testid="component">{JSON.stringify(props)}</pre>
));
const HelloDecorator = withDynamicSchemaProps(({ children, ...others }) => (
<div>
<pre data-testid="decorator">{JSON.stringify(others)}</pre>
{children}
</div>
));
function withTestDemo(schema: any, scopes?: any) {
const Demo = () => {
return (
<SchemaComponentProvider components={{ HelloComponent, HelloDecorator }} scope={scopes}>
<SchemaComponent
schema={{
type: 'void',
name: 'hello',
'x-component': 'HelloComponent',
'x-decorator': 'HelloDecorator',
...schema,
}}
/>
</SchemaComponentProvider>
);
};
return Demo;
}
describe('withDynamicSchemaProps', () => {
test('x-use-component-props', () => {
function useComponentProps() {
return {
a: 'a',
};
}
const schema = {
'x-use-component-props': 'useComponentProps',
};
const scopes = { useComponentProps };
const Demo = withTestDemo(schema, scopes);
const { getByTestId } = render(<Demo />);
expect(getByTestId('component')).toHaveTextContent(JSON.stringify({ a: 'a' }));
});
test('x-use-component-props and x-component-props should merge', () => {
function useComponentProps() {
return {
a: 'a',
};
}
const schema = {
'x-use-component-props': 'useComponentProps',
'x-component-props': {
b: 'b',
},
};
const scopes = { useComponentProps };
const Demo = withTestDemo(schema, scopes);
const { getByTestId } = render(<Demo />);
expect(getByTestId('component')).toHaveTextContent(JSON.stringify({ a: 'a', b: 'b' }));
});
test('x-use-decorator-props', () => {
function useDecoratorProps() {
return {
a: 'a',
};
}
const schema = {
'x-use-decorator-props': 'useDecoratorProps',
};
const scopes = { useDecoratorProps };
const Demo = withTestDemo(schema, scopes);
const { getByTestId } = render(<Demo />);
expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ a: 'a' }));
});
test('x-use-decorator-props and x-decorator-props should merge', () => {
function useDecoratorProps() {
return {
a: 'a',
};
}
const schema = {
'x-use-decorator-props': 'useDecoratorProps',
'x-decorator-props': {
b: 'b',
},
};
const scopes = { useDecoratorProps };
const Demo = withTestDemo(schema, scopes);
const { getByTestId } = render(<Demo />);
expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ a: 'a', b: 'b' }));
});
test('x-use-component-props and x-use-decorator-props exist simultaneously', () => {
function useDecoratorProps() {
return {
a: 'a',
};
}
function useComponentProps() {
return {
c: 'c',
};
}
const schema = {
'x-use-decorator-props': 'useDecoratorProps',
'x-decorator-props': {
b: 'b',
},
'x-use-component-props': 'useComponentProps',
'x-component-props': {
d: 'd',
},
};
const scopes = { useDecoratorProps, useComponentProps };
const Demo = withTestDemo(schema, scopes);
const { getByTestId } = render(<Demo />);
expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ a: 'a', b: 'b' }));
expect(getByTestId('component')).toHaveTextContent(JSON.stringify({ c: 'c', d: 'd' }));
});
test('no register scope', () => {
const schema = {
'x-use-component-props': 'useComponentProps',
};
const Demo = withTestDemo(schema);
const { getByTestId } = render(<Demo />);
expect(getByTestId('component')).toHaveTextContent(JSON.stringify({}));
});
});

View File

@ -20,7 +20,7 @@ export const AssociationProviderV2: FC<AssociationProviderProps> = (props) => {
if (!collectionName) return <DeletedPlaceholder type="Collection" name={collectionName} />;
return (
<CollectionProviderV2 name={name.split('.')[0]} dataSource={dataSource}>
<CollectionProviderV2 name={String(name).split('.')[0]} dataSource={dataSource}>
<CollectionFieldProviderV2 name={name}>
<CollectionProviderV2 name={collectionName} dataSource={dataSource}>
{children}

View File

@ -90,7 +90,7 @@ export class CollectionV2 {
this.options = options;
}
get fields() {
return this.options?.fields || [];
return this.options.fields || [];
}
get dataSource() {
@ -113,9 +113,39 @@ export class CollectionV2 {
get inherit() {
return this.options.inherit;
}
get hidden() {
return this.options.hidden;
}
get description() {
return this.options.description;
}
get duplicator() {
return this.options.duplicator;
}
get category() {
return this.options.category;
}
get targetKey() {
return this.options.targetKey;
}
get model() {
return this.options.model;
}
get createdBy() {
return this.options.createdBy;
}
get updatedBy() {
return this.options.updatedBy;
}
get logging() {
return this.options.logging;
}
get from() {
return this.options.from;
}
get rawTitle() {
return this.options.rawTitle;
}
get isLocal() {
return this.options.isLocal;
}
@ -126,7 +156,7 @@ export class CollectionV2 {
if (this.options.targetKey) {
return this.options.targetKey;
}
const field = this.getFields('primaryKey')[0];
const field = this.getFields({ primaryKey: true })[0];
this.primaryKey = field ? field.name : 'id';
return this.primaryKey;
@ -137,7 +167,7 @@ export class CollectionV2 {
}
get titleField() {
return this.hasField(this.options.titleField) ? this.options.titleField : this.primaryKey;
return this.hasField(this.options.titleField) ? this.options.titleField : this.getPrimaryKey();
}
get sources() {
@ -220,6 +250,7 @@ export class CollectionV2 {
return this.fieldsMap;
}
getField(name: SchemaKey) {
if (!name) return undefined;
const fieldsMap = this.getFieldsMap();
if (typeof name === 'string' && name.startsWith(`${this.name}.`)) {
name = name.replace(`${this.name}.`, '');
@ -227,13 +258,13 @@ export class CollectionV2 {
if (String(name).split('.').length > 1) {
const [fieldName, ...others] = String(name).split('.');
const field = fieldsMap[fieldName];
if (!field) return null;
if (!field) return undefined;
const collectionName = field?.target;
if (!collectionName) return null;
if (!collectionName) return undefined;
const collection = this.collectionManager.getCollection(collectionName);
if (!collection) return null;
if (!collection) return undefined;
return collection.getField(others.join('.'));
}

View File

@ -10,7 +10,6 @@ CollectionFieldContextV2.displayName = 'CollectionFieldContextV2';
export type CollectionFieldProviderProps = {
name?: SchemaKey;
children?: ReactNode;
fallback?: React.ReactElement;
};
export const CollectionFieldProviderV2: FC<CollectionFieldProviderProps> = (props) => {

View File

@ -22,7 +22,7 @@ function applyMixins(instance: any, mixins: any[]) {
}
const defaultCollectionTransform = (collection: CollectionOptionsV2, app: Application) => {
const { rawTitle, title, fields, ...rest } = collection;
const { rawTitle, title, fields = [], ...rest } = collection;
return {
...rest,
title: rawTitle ? title : app.i18n.t(title),
@ -68,10 +68,11 @@ export interface CollectionManagerOptionsV2 {
fieldInterfaces?: (typeof CollectionFieldInterfaceBase)[];
fieldGroups?: Record<string, { label: string; order?: number }>;
collectionMixins?: CollectionMixinConstructor[];
dataSources?: DataSource[];
}
type ThirdDataResourceFn = () => Promise<DataSource[]>;
type MainDataSOurceFn = () => Promise<CollectionOptionsV2[]>;
type MainDataSourceFn = () => Promise<CollectionOptionsV2[]>;
type ReloadCallback = (collections: CollectionOptionsV2[]) => void;
export class CollectionManagerV2 {
@ -80,12 +81,17 @@ export class CollectionManagerV2 {
protected collectionTemplateInstances: Record<string, CollectionTemplateBase> = {};
protected fieldInterfaceInstances: Record<string, CollectionFieldInterfaceBase> = {};
protected collectionMixins: CollectionMixinConstructor[] = [];
protected dataSourceMap: Record<DataSourceNameType, Omit<DataSource, 'collections'>> = {};
protected dataSourceMap: Record<DataSourceNameType, Omit<DataSource, 'collections'>> = {
[DEFAULT_DATA_SOURCE_NAME]: {
name: DEFAULT_DATA_SOURCE_NAME,
description: DEFAULT_DATA_SOURCE_TITLE,
},
};
protected collectionFieldGroups: Record<string, { label: string; order?: number }> = {};
protected mainDataSourceFn: MainDataSOurceFn;
protected mainDataSourceFn: MainDataSourceFn;
protected thirdDataSourceFn: ThirdDataResourceFn;
protected reloadCallbacks: Record<string, ReloadCallback[]> = {};
protected collectionArr: Record<string, CollectionV2[]> = {};
protected collectionCachedArr: Record<string, CollectionV2[]> = {};
protected options: CollectionManagerOptionsV2 = {};
constructor(options: CollectionManagerOptionsV2 = {}, app: Application) {
@ -95,7 +101,7 @@ export class CollectionManagerV2 {
}
private init(options: CollectionManagerOptionsV2) {
this.initDataSourceMap();
this.addDataSources(options.dataSources || []);
this.collectionMixins.push(...(options.collectionMixins || []));
this.addCollectionTemplates(options.collectionTemplates || []);
this.addFieldInterfaces(options.fieldInterfaces || []);
@ -103,28 +109,19 @@ export class CollectionManagerV2 {
this.addCollections(options.collections || []);
}
private initDataSourceMap() {
this.dataSourceMap = {
[DEFAULT_DATA_SOURCE_NAME]: {
name: DEFAULT_DATA_SOURCE_NAME,
description: DEFAULT_DATA_SOURCE_TITLE,
},
};
}
// collection mixins
addCollectionMixins(mixins: CollectionMixinConstructor[]) {
if (mixins.length === 0) return;
addCollectionMixins(mixins: CollectionMixinConstructor[] = []) {
const newMixins = mixins.filter((mixin) => !this.collectionMixins.includes(mixin));
this.collectionMixins.push(...newMixins);
// 重新添加数据表
// Re-add tables
Object.keys(this.collections).forEach((dataSource) => {
const collections = this.getCollections({ dataSource }).map((item) => item.getOptions());
this.addCollections(collections, { dataSource });
});
}
// collections
protected getCollectionInstance(collection: CollectionOptionsV2, dataSource?: string) {
const collectionTemplateInstance = this.getCollectionTemplate(collection.template);
const Cls = collectionTemplateInstance?.Collection || CollectionV2;
@ -135,10 +132,9 @@ export class CollectionManagerV2 {
return instance;
}
// collections
addCollections(collections: CollectionOptionsV2[], options: GetCollectionOptions = {}) {
addCollections(collections: CollectionOptionsV2[] = [], options: GetCollectionOptions = {}) {
const { dataSource = DEFAULT_DATA_SOURCE_NAME } = options;
this.collectionArr[dataSource] = undefined;
this.collectionCachedArr[dataSource] = undefined;
collections
.map((collection) => {
@ -173,23 +169,23 @@ export class CollectionManagerV2 {
}
getCollections(options: { predicate?: (collection: CollectionV2) => boolean; dataSource?: string } = {}) {
const { dataSource = DEFAULT_DATA_SOURCE_NAME, predicate } = options;
if (!this.collectionArr[dataSource]?.length) {
this.collectionArr[dataSource] = Object.values(this.collections[dataSource] || {});
if (!this.collectionCachedArr[dataSource]?.length) {
this.collectionCachedArr[dataSource] = Object.values(this.collections[dataSource] || {});
}
if (predicate) {
return this.collectionArr[dataSource].filter(predicate);
return this.collectionCachedArr[dataSource].filter(predicate);
}
return this.collectionArr[dataSource];
return this.collectionCachedArr[dataSource];
}
/**
*
* Get a collection
* @example
* getCollection('users'); // 获取 users 表
* getCollection('users.profile'); // 获取 users 表的 profile 字段的关联表
* getCollection('a.b.c'); // 获取 a 表的 b 字段的关联表,然后 b.target 表对应的 c 字段的关联表
* getCollection('users'); // Get the 'users' collection
* getCollection('users.profile'); // Get the associated collection of the 'profile' field in the 'users' collection
* getCollection('a.b.c'); // Get the associated collection of the 'c' field in the 'a' collection, which is associated with the 'b' field in the 'a' collection
*/
getCollection<Mixins = {}>(
path: string | CollectionOptionsV2,
path: SchemaKey | CollectionOptionsV2,
options: GetCollectionOptions = {},
): (Mixins & CollectionV2) | undefined {
if (typeof path === 'object') {
@ -197,11 +193,10 @@ export class CollectionManagerV2 {
}
const { dataSource = DEFAULT_DATA_SOURCE_NAME } = options;
if (!path || typeof path !== 'string') return undefined;
if (path.split('.').length > 1) {
// 获取到关联字段
if (!path) return undefined;
if (String(path).split('.').length > 1) {
const associationField = this.getCollectionField(path);
if (!associationField) return undefined;
return this.getCollection(associationField.target, { dataSource });
}
return this.collections[dataSource]?.[path] as Mixins & CollectionV2;
@ -215,19 +210,25 @@ export class CollectionManagerV2 {
return this.getCollection(collectionName, options)?.getFields() || [];
}
/**
*
* Get collection fields
* @example
* getCollection('users.username'); // 获取 users 表的 username 字段
* getCollection('a.b.c'); // 获取 a 表的 b 字段的关联表,然后 b.target 表对应的 c 字段
* getCollection('users.username'); // Get the 'username' field of the 'users' collection
* getCollection('a.b.c'); // Get the associated collection of the 'c' field in the 'a' collection, which is associated with the 'b' field in the 'a' collection
*/
getCollectionField(path: SchemaKey, options: GetCollectionOptions = {}): CollectionFieldOptionsV2 | undefined {
getCollectionField(
path: SchemaKey | object,
options: GetCollectionOptions = {},
): CollectionFieldOptionsV2 | undefined {
if (!path) return;
if (typeof path === 'object' || String(path).split('.').length < 2) {
if (typeof path === 'object') {
return path;
}
if (String(path).split('.').length < 2) {
console.error(`[@nocobase/client]: CollectionManager.getCollectionField() path "${path}" is invalid`);
return;
}
const [collectionName, ...fieldNames] = String(path).split('.');
const { dataSource = DEFAULT_DATA_SOURCE_NAME } = options || {};
const { dataSource = DEFAULT_DATA_SOURCE_NAME } = options;
const collection = this.getCollection(collectionName, { dataSource });
if (!collection) {
return;
@ -304,7 +305,7 @@ export class CollectionManagerV2 {
return this.collectionFieldGroups[name];
}
setMainDataSource(fn: MainDataSOurceFn) {
setMainDataSource(fn: MainDataSourceFn) {
this.mainDataSourceFn = fn;
}
@ -313,26 +314,35 @@ export class CollectionManagerV2 {
}
async reloadMain(callback?: ReloadCallback) {
if (!this.mainDataSourceFn) return;
const collections = await this.mainDataSourceFn();
this.setCollections(collections);
callback && callback(collections);
this.reloadCallbacks[DEFAULT_DATA_SOURCE_NAME]?.forEach((cb) => cb(collections));
}
async reloadThirdDataSource(callback?: () => void) {
if (!this.thirdDataSourceFn) return;
const data = await this.thirdDataSourceFn();
this.initDataSourceMap();
data.forEach((dataSource) => {
const { collections: _unUse, ...rest } = dataSource;
this.dataSourceMap[dataSource.name] = rest;
});
data.forEach(({ name, collections, ...others }) => {
addDataSources(dataSources: DataSource[] = []) {
dataSources.forEach(({ name, collections, ...others }) => {
this.dataSourceMap[name] = { ...others, name };
this.setCollections(collections, { dataSource: name });
this.reloadCallbacks[name]?.forEach((cb) => cb(collections));
});
}
private initDataSource() {
this.dataSourceMap = {
[DEFAULT_DATA_SOURCE_NAME]: {
name: DEFAULT_DATA_SOURCE_NAME,
description: DEFAULT_DATA_SOURCE_TITLE,
},
};
}
async reloadThirdDataSource(callback?: () => void) {
if (!this.thirdDataSourceFn) return;
const data = await this.thirdDataSourceFn();
this.initDataSource();
this.addDataSources([...(this.options.dataSources || []), ...data]);
callback && callback();
}
@ -361,7 +371,7 @@ export class CollectionManagerV2 {
mainDataSourceFn: this.mainDataSourceFn,
thirdDataSourceFn: this.thirdDataSourceFn,
reloadCallbacks: this.reloadCallbacks,
collectionArr: this.collectionArr,
collectionCachedArr: this.collectionCachedArr,
options: this.options,
};
}

View File

@ -23,7 +23,7 @@ export function useCollectionManagerV2() {
export const useCollectionsV2 = (options?: {
predicate?: (collection: CollectionV2) => boolean;
dataSources?: string;
dataSource?: string;
}) => {
const collectionManager = useCollectionManagerV2();
const collections = useMemo(() => collectionManager.getCollections(options), [collectionManager, options]);

View File

@ -47,7 +47,9 @@ export class CollectionTemplateBase {
}
name: string;
Collection?: typeof CollectionV2;
transform?: (collection: CollectionOptionsV2, app: Application) => CollectionOptionsV2;
transform(collection: CollectionOptionsV2, app: Application): CollectionOptionsV2 {
return collection;
}
title?: string;
color?: string;
/** 排序 */

View File

@ -10,6 +10,10 @@ export const DeletedPlaceholder: FC<{ type: string; name?: string | number }> =
console.error(`DeletedPlaceholder: ${type} name is required`);
return null;
}
if (!designable && process.env.NODE_ENV !== 'development') return null;
return <Result status="warning" title={t(`${type}: "${name}" has been deleted`)} />;
if (designable || process.env.NODE_ENV === 'development') {
return <Result status="warning" title={t(`${type}: "${name}" not exists`)} />;
}
return null;
};

View File

@ -1,107 +1,10 @@
import { CascaderProps } from 'antd';
// import { CascaderProps } from 'antd';
import _ from 'lodash';
import type { CollectionManagerV2 } from './CollectionManager';
import { CollectionFieldOptionsV2 } from './Collection';
export function getCollectionFieldsOptions(
collectionName: string,
type: string | string[] = 'string',
opts: {
collectionManager: CollectionManagerV2;
compile: (str: string) => string;
cached?: Record<string, any>;
collectionNames?: string[];
/**
* true
* Array<string>
*/
association?: boolean | string[];
/**
* Max depth of recursion
*/
maxDepth?: number;
allowAllTypes?: boolean;
/**
*
*/
exceptInterfaces?: string[];
/**
* field value . a.b.c
*/
prefixFieldValue?: string;
/**
* 使 prefixFieldValue field value
*/
usePrefix?: boolean;
},
) {
const {
association = false,
cached = {},
collectionNames = [collectionName],
maxDepth = 1,
allowAllTypes = false,
exceptInterfaces = [],
prefixFieldValue = '',
compile,
collectionManager,
usePrefix = false,
} = opts || {};
if (collectionNames.length - 1 > maxDepth) {
return;
}
if (cached[collectionName]) {
// avoid infinite recursion
return _.cloneDeep(cached[collectionName]);
}
if (typeof type === 'string') {
type = [type];
}
const fields = collectionManager.getCollectionFields(collectionName);
const options = fields
?.filter(
(field) =>
field.interface &&
!exceptInterfaces.includes(field.interface) &&
(allowAllTypes ||
type.includes(field.type) ||
(association && field.target && field.target !== collectionName && Array.isArray(association)
? association.includes(field.interface)
: false)),
)
?.map((field) => {
const result: CascaderProps<any>['options'][0] = {
value: usePrefix && prefixFieldValue ? `${prefixFieldValue}.${field.name}` : field.name,
label: compile(field?.uiSchema?.title) || field.name,
...field,
};
if (association && field.target) {
result.children = collectionNames.includes(field.target)
? []
: getCollectionFieldsOptions(field.target, type, {
...opts,
cached,
collectionManager,
compile,
collectionNames: [...collectionNames, field.target],
prefixFieldValue: usePrefix ? (prefixFieldValue ? `${prefixFieldValue}.${field.name}` : field.name) : '',
usePrefix,
});
if (!result.children?.length) {
return null;
}
}
return result;
})
// 过滤 map 产生为 null 的数据
.filter(Boolean);
cached[collectionName] = options;
return options;
}
// 等把老的去掉后,再把这个函数的实现从那边移动过来
// export function getCollectionFieldsOptions(){}
export const isTitleField = (cm: CollectionManagerV2, field: CollectionFieldOptionsV2) => {
return !field.isForeignKey && cm.getFieldInterface(field.interface)?.titleUsable;

View File

@ -3,7 +3,7 @@ import React, { FC, ReactNode, createContext, useContext } from 'react';
export const CollectionDataSourceName = createContext<string>(undefined);
CollectionDataSourceName.displayName = 'CollectionDataSourceName';
export const CollectionDataSourceProvider: FC<{ dataSource: string; children: ReactNode }> = ({
export const CollectionDataSourceProvider: FC<{ dataSource: string; children?: ReactNode }> = ({
dataSource,
children,
}) => {

View File

@ -1,4 +1,4 @@
import { useDeepCompareEffect } from 'ahooks';
import { useDeepCompareEffect, useUpdateEffect } from 'ahooks';
import React, { FC, createContext, useContext, useEffect } from 'react';
import { UseRequestResult, useAPIClient, useRequest } from '../../api-client';
@ -40,7 +40,7 @@ function useCurrentRequest<T>(options: Omit<AllDataBlockProps, 'type'>) {
}
}, [params, action, record]);
useEffect(() => {
useUpdateEffect(() => {
if (action) {
request.run();
}

View File

@ -1,6 +1,6 @@
import { SchemaKey } from '@formily/react';
import { useAPIClient } from '../../api-client';
import { useCollectionV2 } from '../../application';
import { useCollectionV2 } from '../../application/collection/CollectionProvider';
import { InheritanceCollectionMixin } from '../mixins/InheritanceCollectionMixin';
import { useCallback, useMemo } from 'react';
@ -11,9 +11,9 @@ export const useCollection = () => {
const api = useAPIClient();
const resource = api?.resource(collection?.name);
const currentFields = useMemo(() => collection?.fields || [], [collection]);
const inheritedFields = useMemo(() => collection?.getInheritedFields() || [], [collection]);
const totalFields = useMemo(() => collection?.getAllFields() || [], [collection]);
const foreignKeyFields = useMemo(() => collection?.getForeignKeyFields() || [], [collection]);
const inheritedFields = useMemo(() => collection?.getInheritedFields?.() || [], [collection]);
const totalFields = useMemo(() => collection?.getAllFields?.() || collection?.getFields() || [], [collection]);
const foreignKeyFields = useMemo(() => collection?.getForeignKeyFields?.() || [], [collection]);
const getTreeParentField = useCallback(() => totalFields?.find((field) => field.treeParent), [totalFields]);
const getField = useCallback(
(name: SchemaKey) => {

View File

@ -3,7 +3,7 @@ import _ from 'lodash';
import { useCallback, useMemo, useState } from 'react';
import { useCompile, useSchemaComponentContext } from '../../schema-component';
import { CollectionFieldOptions, CollectionOptions } from '../types';
import { useCollectionManagerV2 } from '../../application';
import { useCollectionManagerV2 } from '../../application/collection/CollectionManagerProvider';
import { InheritanceCollectionMixin } from '../mixins/InheritanceCollectionMixin';
import { uid } from '@formily/shared';
import { useCollectionDataSourceName } from '../../application/data-block';
@ -56,13 +56,11 @@ export const useCollectionManager = (dataSourceName?: string) => {
const getCollectionFields = useCallback(
(name: any, customDataSource?: string): CollectionFieldOptions[] => {
if (!name) return [];
return (
cm
?.getCollection<InheritanceCollectionMixin>(name, {
dataSource: customDataSource || dataSourceNameValue,
})
?.getAllFields() || []
);
const collection = cm?.getCollection<InheritanceCollectionMixin>(name, {
dataSource: customDataSource || dataSourceNameValue,
});
return collection?.getAllFields?.() || collection?.getFields() || [];
},
[cm],
);

View File

@ -40,6 +40,7 @@ import { Router } from 'react-router-dom';
import {
APIClientProvider,
ActionContextProvider,
CollectionDataSourceProvider,
CollectionFieldOptions,
CollectionManagerProviderV2,
CollectionProvider,
@ -57,6 +58,7 @@ import {
useActionContext,
useBlockRequestContext,
useCollection,
useCollectionDataSourceName,
useCollectionManager,
useCollectionManagerV2,
useCompile,
@ -961,6 +963,7 @@ export const SchemaSettingsModalItem: FC<SchemaSettingsModalItemProps> = (props)
const ctx = useContext(BlockRequestContext);
const upLevelActiveFields = useFormActiveFields();
const { locale } = useContext(ConfigProvider.ConfigContext);
const dataSourceName = useCollectionDataSourceName();
if (hidden) {
return null;
@ -979,32 +982,34 @@ export const SchemaSettingsModalItem: FC<SchemaSettingsModalItemProps> = (props)
<FormActiveFieldsProvider name="form" getActiveFieldsName={upLevelActiveFields?.getActiveFieldsName}>
<Router location={location} navigator={null}>
<BlockRequestContext.Provider value={ctx}>
<CollectionManagerProviderV2 collectionManager={cm}>
<CollectionProvider allowNull name={collection.name}>
<SchemaComponentOptions scope={options.scope} components={options.components}>
<FormLayout
layout={'vertical'}
className={css`
// screen > 576px
@media (min-width: 576px) {
min-width: 520px;
}
<CollectionDataSourceProvider dataSource={dataSourceName}>
<CollectionManagerProviderV2 collectionManager={cm}>
<CollectionProvider allowNull name={collection.name}>
<SchemaComponentOptions scope={options.scope} components={options.components}>
<FormLayout
layout={'vertical'}
className={css`
// screen > 576px
@media (min-width: 576px) {
min-width: 520px;
}
// screen <= 576px
@media (max-width: 576px) {
min-width: 320px;
}
`}
>
<APIClientProvider apiClient={apiClient}>
<ConfigProvider locale={locale}>
<SchemaComponent components={components} scope={scope} schema={schema} />
</ConfigProvider>
</APIClientProvider>
</FormLayout>
</SchemaComponentOptions>
</CollectionProvider>
</CollectionManagerProviderV2>
// screen <= 576px
@media (max-width: 576px) {
min-width: 320px;
}
`}
>
<APIClientProvider apiClient={apiClient}>
<ConfigProvider locale={locale}>
<SchemaComponent components={components} scope={scope} schema={schema} />
</ConfigProvider>
</APIClientProvider>
</FormLayout>
</SchemaComponentOptions>
</CollectionProvider>
</CollectionManagerProviderV2>
</CollectionDataSourceProvider>
</BlockRequestContext.Provider>
</Router>
</FormActiveFieldsProvider>