client unit test (#4150)

* fix: add more unit test
This commit is contained in:
jack zhang 2024-04-24 20:33:14 +08:00 committed by GitHub
parent b65ee6a602
commit 229e5d1a40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
98 changed files with 6471 additions and 512 deletions

26
.vscode/launch.json vendored
View File

@ -21,6 +21,19 @@
"skipFiles": ["<node_internals>/**"], "skipFiles": ["<node_internals>/**"],
"type": "node" "type": "node"
}, },
{
"type": "node",
"request": "launch",
"name": "Debug Tests watch mode",
"runtimeExecutable": "yarn",
"runtimeArgs": ["run", "test", "-w", "${file}"],
"skipFiles": ["<node_internals>/**", "**/node_modules/**", "**/dist/**", "**/lib/**", "**/es/**"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"windows": {
"runtimeArgs": ["run", "test", "${relativeFile}"]
}
},
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
@ -34,6 +47,19 @@
"runtimeArgs": ["run", "test", "${relativeFile}"] "runtimeArgs": ["run", "test", "${relativeFile}"]
} }
}, },
{
"type": "node",
"request": "launch",
"name": "Debug E2E Tests UI",
"runtimeExecutable": "yarn",
"runtimeArgs": ["e2e", "test", "${file}", "--ui"],
"skipFiles": ["<node_internals>/**", "**/node_modules/**", "**/dist/**", "**/lib/**", "**/es/**"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"windows": {
"runtimeArgs": ["e2e", "test", "${fileBasename}", "--ui"]
}
},
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",

View File

@ -217,6 +217,7 @@ exports.genTsConfigPaths = function genTsConfigPaths() {
if (packageJsonName === '@nocobase/test') { if (packageJsonName === '@nocobase/test') {
paths[`${packageJsonName}/server`] = [`${relativePath}/src/server`]; paths[`${packageJsonName}/server`] = [`${relativePath}/src/server`];
paths[`${packageJsonName}/e2e`] = [`${relativePath}/src/e2e`]; paths[`${packageJsonName}/e2e`] = [`${relativePath}/src/e2e`];
paths[`${packageJsonName}/web`] = [`${relativePath}/src/web`];
} }
if (packageJsonName === '@nocobase/plugin-workflow-test') { if (packageJsonName === '@nocobase/plugin-workflow-test') {
paths[`${packageJsonName}/e2e`] = [`${relativePath}/src/e2e`]; paths[`${packageJsonName}/e2e`] = [`${relativePath}/src/e2e`];

View File

@ -1,3 +1,6 @@
import path from 'path';
import glob from 'glob';
import _ from 'lodash'
import { getUmiConfig } from '@nocobase/devtools/umiConfig'; import { getUmiConfig } from '@nocobase/devtools/umiConfig';
import { defineConfig } from 'dumi'; import { defineConfig } from 'dumi';
import { defineThemeConfig } from 'dumi-theme-nocobase'; import { defineThemeConfig } from 'dumi-theme-nocobase';
@ -8,6 +11,19 @@ const lang = process.env.DOC_LANG;
console.log('process.env.DOC_LANG', lang); console.log('process.env.DOC_LANG', lang);
const componentsDir = 'src/schema-component/antd';
function getComponentsMenu() {
const cwd = path.join(__dirname, componentsDir);
const ignores = ['table/index.md', 'form/index.md']; // 老版本,不需要展示
const files = glob.sync('*/index.md', { cwd, ignore: ignores });
return files.map((file) => ({
title: _.upperFirst(_.camelCase(file.replace('/index.md', ''))),
link: `/components/${file.replace('/index.md', '')}`,
}));
}
export default defineConfig({ export default defineConfig({
hash: true, hash: true,
alias: { alias: {
@ -21,7 +37,10 @@ export default defineConfig({
cacheDirectoryPath: `node_modules/.docs-client-${lang}-cache`, cacheDirectoryPath: `node_modules/.docs-client-${lang}-cache`,
outputPath: `./dist/${lang}`, outputPath: `./dist/${lang}`,
resolve: { resolve: {
docDirs: [`./docs/${lang}`] docDirs: [`./docs/${lang}`],
atomDirs: [
{ type: 'component', dir: componentsDir },
],
}, },
locales: [ locales: [
{ id: 'en-US', name: 'English' }, { id: 'en-US', name: 'English' },
@ -38,6 +57,10 @@ export default defineConfig({
title: 'API', title: 'API',
link: '/core/application/application', link: '/core/application/application',
}, },
{
title: 'Components',
link: '/components/action',
}
// { // {
// title: 'UI Schema', // title: 'UI Schema',
// link: '/ui-schema', // link: '/ui-schema',
@ -202,6 +225,7 @@ export default defineConfig({
] ]
} }
], ],
'/components': getComponentsMenu(),
// '/ui-schema': [ // '/ui-schema': [
// { // {
// title: 'Overview', // title: 'Overview',

View File

@ -0,0 +1,212 @@
import {
getApp,
getAppComponent,
getAppComponentWithSchemaSettings,
getReadPrettyAppComponent,
withSchema,
} from '@nocobase/test/web';
import {
ACLMenuItemProvider,
AdminLayout,
BlockSchemaComponentPlugin,
CurrentUserProvider,
DocumentTitleProvider,
EditComponent,
EditDefaultValue,
EditOperator,
EditPattern,
EditTitle,
EditTitleField,
EditValidationRules,
FilterFormBlockProvider,
FixedBlock,
Form,
FormBlockProvider,
FormItem,
FormV2,
Grid,
IconPicker,
Input,
InternalAdminLayout,
NanoIDInput,
Page,
RouteSchemaComponent,
SchemaInitializerPlugin,
TableBlockProvider,
TableV2,
VariablesProvider,
fieldSettingsFormItem,
tableActionColumnInitializers,
tableActionInitializers,
tableColumnInitializers,
useTableBlockDecoratorProps,
} from '@nocobase/client';
import { observer } from '@formily/reactive-react';
import React, { ComponentType } from 'react';
import { useField, useFieldSchema } from '@formily/react';
import axios from 'axios';
import { pick } from 'lodash';
const App = getAppComponent({
designable: true,
schema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-acl-action-props': {
skipScopeCheck: true,
},
'x-acl-action': 'users:create',
'x-decorator': 'FormBlockProvider',
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: 'users',
},
'x-component': 'div',
'x-app-version': '0.21.0-alpha.10',
properties: {
'0s3tm262rre': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useCreateFormBlockProps',
'x-app-version': '0.21.0-alpha.10',
properties: {
grid: {
'x-uid': 'h38s9pa4ik5',
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-app-version': '0.21.0-alpha.10',
'x-linkage-rules': [
{
condition: {
$and: [
{
username: {
$eq: 'test',
},
},
],
},
actions: [
{
targetFields: ['nickname'],
operator: 'none',
},
],
},
],
properties: {
udpf3e45i3d: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '0.21.0-alpha.10',
properties: {
hhc0bsk1roi: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '0.21.0-alpha.10',
properties: {
username: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.username',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
'x-uid': '71x74r4t4g0',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ophjdttgmo5',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ta1vq3qr1sd',
'x-async': false,
'x-index': 3,
},
row_rpkxgfonud3: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-index': 4,
properties: {
mmo2k17b0q1: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
nickname: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.nickname',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
'x-uid': 'bcowga6nzzy',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'l1awt5at07z',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'y1tdyhcwhhi',
'x-async': false,
},
},
'x-async': false,
'x-index': 1,
},
'0m1r08p58e9': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
style: {
marginTop: 24,
},
},
'x-app-version': '0.21.0-alpha.10',
'x-uid': 't4gxf0xxaxc',
'x-async': false,
'x-index': 2,
},
},
'x-uid': 'yk2fivh9hgb',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'aqbi3avt3kb',
'x-async': false,
'x-index': 1,
},
appOptions: {
plugins: [BlockSchemaComponentPlugin],
providers: [VariablesProvider],
},
});
export default App;

View File

@ -0,0 +1,2 @@
<code src="./Demo.tsx"></code>

View File

@ -0,0 +1,183 @@
import {
getApp,
getAppComponent,
getAppComponentWithSchemaSettings,
getReadPrettyAppComponent,
withSchema,
} from '@nocobase/test/web';
import {
ACLMenuItemProvider,
AdminLayout,
BlockSchemaComponentPlugin,
CurrentUserProvider,
DocumentTitleProvider,
EditComponent,
EditDefaultValue,
EditOperator,
EditPattern,
EditTitle,
EditTitleField,
EditValidationRules,
FilterFormBlockProvider,
FixedBlock,
Form,
FormBlockProvider,
FormItem,
FormV2,
Grid,
IconPicker,
Input,
InternalAdminLayout,
NanoIDInput,
Page,
RouteSchemaComponent,
SchemaInitializerPlugin,
TableBlockProvider,
TableV2,
VariablesProvider,
fieldSettingsFormItem,
tableActionColumnInitializers,
tableActionInitializers,
tableColumnInitializers,
useTableBlockDecoratorProps,
} from '@nocobase/client';
import { observer } from '@formily/reactive-react';
import React, { ComponentType } from 'react';
import { useField, useFieldSchema } from '@formily/react';
import axios from 'axios';
import { pick } from 'lodash';
const FormBlockProviderWithSchema = withSchema(FormBlockProvider);
const App = getAppComponent({
designable: true,
schema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-acl-action-props': {
skipScopeCheck: true,
},
'x-acl-action': 'users:create',
'x-decorator': 'FormBlockProviderWithSchema',
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: 'users',
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:createForm',
'x-component': 'CardItem',
properties: {
grid: {
'x-uid': 'h38s9pa4ik5',
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-app-version': '0.21.0-alpha.10',
properties: {
udpf3e45i3d: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '0.21.0-alpha.10',
properties: {
hhc0bsk1roi: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '0.21.0-alpha.10',
properties: {
username: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.username',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
'x-uid': '71x74r4t4g0',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ophjdttgmo5',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ta1vq3qr1sd',
'x-async': false,
'x-index': 3,
},
row_rpkxgfonud3: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-index': 4,
properties: {
mmo2k17b0q1: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
nickname: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.nickname',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
'x-uid': 'bcowga6nzzy',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'l1awt5at07z',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'y1tdyhcwhhi',
'x-async': false,
},
},
'x-async': false,
'x-index': 1,
},
'0m1r08p58e9': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
style: {
marginTop: 24,
},
},
'x-app-version': '0.21.0-alpha.10',
'x-uid': 't4gxf0xxaxc',
'x-async': false,
'x-index': 2,
},
},
},
appOptions: {
plugins: [BlockSchemaComponentPlugin],
providers: [VariablesProvider],
components: {
FormBlockProviderWithSchema,
},
},
});
export default App;

View File

@ -0,0 +1,2 @@
<code src="./Demo.tsx"></code>

View File

@ -198,8 +198,9 @@ export function useACLRoleContext() {
export const ACLCollectionProvider = (props) => { export const ACLCollectionProvider = (props) => {
const { allowAll, parseAction } = useACLRoleContext(); const { allowAll, parseAction } = useACLRoleContext();
const app = useApp();
const schema = useFieldSchema(); const schema = useFieldSchema();
if (allowAll) { if (allowAll || app.disableAcl) {
return props.children; return props.children;
} }
let actionPath = schema?.['x-acl-action'] || props.actionPath; let actionPath = schema?.['x-acl-action'] || props.actionPath;

View File

@ -60,6 +60,7 @@ export interface ApplicationOptions {
loadRemotePlugins?: boolean; loadRemotePlugins?: boolean;
devDynamicImport?: DevDynamicImport; devDynamicImport?: DevDynamicImport;
dataSourceManager?: DataSourceManagerOptions; dataSourceManager?: DataSourceManagerOptions;
disableAcl?: boolean;
} }
export class Application { export class Application {
@ -93,6 +94,9 @@ export class Application {
get pm() { get pm() {
return this.pluginManager; return this.pluginManager;
} }
get disableAcl() {
return this.options.disableAcl;
}
constructor(protected options: ApplicationOptions = {}) { constructor(protected options: ApplicationOptions = {}) {
this.initRequireJs(); this.initRequireJs();

View File

@ -24,6 +24,7 @@ export interface MemoryRouterOptions extends Omit<MemoryRouterProps, 'children'>
} }
export type RouterOptions = (HashRouterOptions | BrowserRouterOptions | MemoryRouterOptions) & { export type RouterOptions = (HashRouterOptions | BrowserRouterOptions | MemoryRouterOptions) & {
renderComponent?: RenderComponentType; renderComponent?: RenderComponentType;
routes?: Record<string, RouteType>;
}; };
export type ComponentTypeAndString<T = any> = ComponentType<T> | string; export type ComponentTypeAndString<T = any> = ComponentType<T> | string;
export interface RouteType extends Omit<RouteObject, 'children' | 'Component'> { export interface RouteType extends Omit<RouteObject, 'children' | 'Component'> {
@ -39,6 +40,7 @@ export class RouterManager {
constructor(options: RouterOptions = {}, app: Application) { constructor(options: RouterOptions = {}, app: Application) {
this.options = options; this.options = options;
this.app = app; this.app = app;
this.routes = options.routes || {};
} }
/** /**

View File

@ -1,4 +1,4 @@
/* istanbul ignore file */ /* istanbul ignore file -- @preserve */
// @ts-nocheck // @ts-nocheck
/* eslint-disable */ /* eslint-disable */
/* prettier-ignore */ /* prettier-ignore */

View File

@ -82,7 +82,11 @@ export class CollectionPlugin extends Plugin {
this.addCollectionTemplates(); this.addCollectionTemplates();
this.addFieldInterfaces(); this.addFieldInterfaces();
this.addFieldInterfaceGroups(); this.addFieldInterfaceGroups();
this.addMainDataSource();
}
addMainDataSource() {
if (this.options?.config?.enableRemoteDataSource === false) return;
this.dataSourceManager.addDataSource(MainDataSource, { this.dataSourceManager.addDataSource(MainDataSource, {
key: DEFAULT_DATA_SOURCE_KEY, key: DEFAULT_DATA_SOURCE_KEY,
displayName: DEFAULT_DATA_SOURCE_TITLE, displayName: DEFAULT_DATA_SOURCE_TITLE,

View File

@ -28,10 +28,12 @@ export function SelectWithTitle({ title, defaultValue, onChange, options, fieldN
{title} {title}
<Select <Select
open={open} open={open}
data-testid={`select-${title}`}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
bordered={false} bordered={false}
defaultValue={defaultValue} defaultValue={defaultValue}
onChange={onChange} onChange={onChange}
popupClassName={`select-popup-${title.replaceAll(' ', '-')}`}
fieldNames={fieldNames} fieldNames={fieldNames}
options={options} options={options}
style={{ textAlign: 'right', minWidth: 100 }} style={{ textAlign: 'right', minWidth: 100 }}

View File

@ -20,7 +20,7 @@ export const DataSourceProvider: FC<DataSourceProviderProps> = ({ children, data
const { t } = useTranslation(); const { t } = useTranslation();
const { refresh } = useSchemaComponentContext(); const { refresh } = useSchemaComponentContext();
const [_, setRandom] = React.useState(0); const [_, setRandom] = React.useState(0);
const dataSourceValue = dataSourceManager.getDataSource(dataSource); const dataSourceValue = dataSourceManager?.getDataSource(dataSource);
if (!dataSourceValue) { if (!dataSourceValue) {
return <CollectionDeletedPlaceholder type="Data Source" name={dataSource} />; return <CollectionDeletedPlaceholder type="Data Source" name={dataSource} />;

View File

@ -0,0 +1,562 @@
import {
screen,
checkSettings,
renderSingleSettings,
waitFor,
userEvent,
renderReadPrettySingleSettings,
renderSettings,
renderReadPrettySettings,
checkFieldTitle,
} from '@nocobase/test/client';
import { FilterFormBlockProvider, FormBlockProvider, FormItem, fieldSettingsFormItem } from '@nocobase/client';
import { useFieldSchema } from '@formily/react';
import { observer } from '@formily/reactive-react';
import React from 'react';
describe('FieldSettingsFormItem', () => {
function commonFieldOptions(isFilterForm?: boolean) {
return {
enableUserListDataBlock: true,
schema: {
type: 'void',
'x-component': 'FormV2',
'x-decorator': isFilterForm ? 'FilterFormBlockProvider' : 'FormBlockProvider',
'x-decorator-props': {
collection: 'users',
},
properties: {
nickname: {
type: 'string',
'x-collection-field': 'users.nickname',
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
'x-settings': 'fieldSettings:FormItem',
},
},
},
schemaSettings: fieldSettingsFormItem,
appOptions: {
components: {
FilterFormBlockProvider,
FormBlockProvider,
},
},
};
}
function associationFieldOptions(showSchema = false) {
const FormItemWithSchema = observer(({ children }) => {
const schema = useFieldSchema();
return (
<>
<div>mode: {schema?.['x-component-props']?.['mode']}</div>
<FormItem>{children}</FormItem>
</>
);
});
return {
designable: true,
enableUserListDataBlock: true,
schema: {
type: 'void',
'x-component': 'FormV2',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
collection: 'users',
},
properties: {
roles: {
type: 'string',
'x-collection-field': 'users.roles',
'x-decorator': showSchema ? 'FormItemWithSchema' : 'FormItem',
'x-component': 'CollectionField',
'x-settings': 'fieldSettings:FormItem',
},
},
},
schemaSettings: fieldSettingsFormItem,
appOptions: {
components: {
FormItemWithSchema,
FormBlockProvider,
},
},
};
}
describe('menu list', () => {
describe('edit mode', () => {
it('common field', async () => {
await renderSettings(commonFieldOptions());
await checkSettings(
[
{
title: 'Edit field title',
type: 'modal',
},
{
title: 'Display title',
type: 'switch',
},
{
title: 'Edit description',
type: 'modal',
},
{
title: 'Edit tooltip',
type: 'modal',
},
{
title: 'Required',
type: 'switch',
},
{
title: 'Set default value',
type: 'modal',
},
{
title: 'Pattern',
type: 'select',
},
{
title: 'Set validation rules',
type: 'modal',
},
{
title: 'Delete',
type: 'delete',
},
],
true,
);
});
it('association field', async () => {
await renderSettings(associationFieldOptions());
await checkSettings(
[
{
title: 'Edit field title',
type: 'modal',
},
{
title: 'Display title',
type: 'switch',
},
{
title: 'Edit description',
type: 'modal',
},
{
title: 'Edit tooltip',
type: 'modal',
},
{
title: 'Required',
type: 'switch',
},
{
title: 'Set default value',
type: 'modal',
},
{
title: 'Pattern',
type: 'select',
},
{
title: 'Field component',
type: 'select',
},
{
title: 'Set the data scope',
type: 'modal',
},
{
title: 'Set default sorting rules',
type: 'modal',
},
{
title: 'Quick create',
type: 'select',
},
{
title: 'Allow multiple',
type: 'switch',
},
{
title: 'Title field',
type: 'select',
},
{
title: 'Delete',
type: 'delete',
},
],
true,
);
});
});
describe('read pretty mode', () => {
it('common field', async () => {
await renderReadPrettySettings(commonFieldOptions());
await checkSettings(
[
{
title: 'Edit field title',
type: 'modal',
},
{
title: 'Display title',
type: 'switch',
},
{
title: 'Edit description',
type: 'modal',
},
{
title: 'Edit tooltip',
type: 'modal',
},
{
title: 'Set default value',
type: 'modal',
},
{
title: 'Pattern',
type: 'select',
},
{
title: 'Set validation rules',
type: 'modal',
},
{
title: 'Delete',
type: 'delete',
},
],
true,
);
});
it('association field', async () => {
await renderReadPrettySettings(associationFieldOptions());
await checkSettings(
[
{
title: 'Edit field title',
type: 'modal',
},
{
title: 'Display title',
type: 'switch',
},
{
title: 'Edit description',
type: 'modal',
},
{
title: 'Edit tooltip',
type: 'modal',
},
{
title: 'Set default value',
type: 'modal',
},
{
title: 'Pattern',
type: 'select',
},
{
title: 'Field component',
type: 'select',
},
{
title: 'Title field',
type: 'select',
},
{
title: 'Enable link',
type: 'switch',
},
{
title: 'Delete',
type: 'delete',
},
],
true,
);
});
});
});
describe('menu items', () => {
test('Edit field title', async () => {
await renderSettings(commonFieldOptions());
await checkFieldTitle('Nickname');
});
test('Display title', async () => {
await renderSettings(commonFieldOptions());
await checkSettings([
{
type: 'switch',
title: 'Display title',
beforeClick() {
expect(document.querySelector('.ant-formily-item-label')).toHaveStyle({ display: 'flex' });
},
async afterFirstClick() {
expect(document.querySelector('.ant-formily-item-label')).toHaveStyle({ display: 'none' });
},
afterSecondClick() {
expect(document.querySelector('.ant-formily-item-label')).toHaveStyle({ display: 'flex' });
},
},
]);
});
test('Edit description', async () => {
const newValue = 'new test';
await renderSettings(commonFieldOptions());
await checkSettings([
{
type: 'modal',
title: 'Edit description',
modalChecker: {
modalTitle: 'Edit description',
formItems: [
{
type: 'textarea',
newValue,
},
],
afterSubmit() {
expect(screen.queryByText(newValue)).toBeInTheDocument();
},
},
},
]);
});
test('Edit tooltip', async () => {
const newValue = 'new test';
await renderSettings(commonFieldOptions());
await checkSettings([
{
type: 'modal',
title: 'Edit tooltip',
modalChecker: {
modalTitle: 'Edit tooltip',
formItems: [
{
type: 'textarea',
newValue,
},
],
async afterSubmit() {
expect(document.querySelector('.anticon-question-circle')).toBeInTheDocument();
await userEvent.hover(document.querySelector('.anticon-question-circle'));
await waitFor(() => {
expect(screen.queryByText(newValue)).toBeInTheDocument();
});
},
},
},
]);
});
test('Required', async () => {
await renderSettings(commonFieldOptions());
await checkSettings([
{
type: 'switch',
title: 'Required',
afterFirstClick() {
expect(document.querySelector('.ant-formily-item-asterisk')).toBeInTheDocument();
},
afterSecondClick() {
expect(document.querySelector('.ant-formily-item-asterisk')).not.toBeInTheDocument();
},
},
]);
});
test('Set default value', async () => {
await renderSettings(commonFieldOptions());
const newValue = 'new test';
await checkSettings([
{
type: 'modal',
title: 'Set default value',
modalChecker: {
modalTitle: 'Set default value',
formItems: [
{
type: 'collectionField',
field: 'users.nickname',
newValue,
},
],
afterSubmit() {
expect(screen.queryByRole('textbox')).toBeInTheDocument();
expect(screen.queryByRole('textbox')).toHaveValue(newValue);
},
},
},
]);
});
test('Pattern', async () => {
await renderSettings(associationFieldOptions());
await checkSettings([
{
type: 'select',
title: 'Pattern',
oldValue: 'Editable',
options: [
{
label: 'Readonly',
checker() {
expect(document.querySelector('.nb-form-item input')).toHaveAttribute('disabled');
},
},
{
label: 'Easy-reading',
checker() {
expect(document.querySelector('.nb-form-item input')).not.toBeInTheDocument();
},
},
{
label: 'Editable',
checker() {
expect(document.querySelector('.nb-form-item input')).toBeInTheDocument();
},
},
],
},
]);
});
test('EditValidationRules', async () => {
await renderSingleSettings(commonFieldOptions(true));
await checkSettings([
{
type: 'modal',
title: 'Set validation rules',
modalChecker: {
modalTitle: 'Set validation rules',
contentText: 'Add validation rule',
async beforeCheck() {
await userEvent.click(screen.getByText('Add validation rule'));
await waitFor(() => {
expect(screen.queryByText('Min length')).toBeInTheDocument();
});
},
formItems: [
{
type: 'number',
label: 'Min length',
newValue: 2,
},
],
async afterSubmit() {
await userEvent.type(document.querySelector('.nb-form-item input'), '1');
await waitFor(() => {
expect(screen.queryByText('The length or number of entries must be at least 2')).toBeInTheDocument();
});
await userEvent.type(document.querySelector('.nb-form-item input'), '12');
await waitFor(() => {
expect(
screen.queryByText('The length or number of entries must be at least 2'),
).not.toBeInTheDocument();
});
},
},
},
]);
});
test('Field component', async () => {
await renderSingleSettings(associationFieldOptions(true));
await checkSettings([
{
type: 'select',
title: 'Field component',
oldValue: 'Select',
beforeSelect() {
expect(screen.queryByTestId('select-object-multiple')).toBeInTheDocument();
},
options: [
{
label: 'Record picker',
async checker() {
expect(screen.queryByText('mode: Picker')).toBeInTheDocument();
},
},
{
label: 'Sub-table',
checker() {
expect(screen.queryByText('mode: SubTable')).toBeInTheDocument();
},
},
{
label: 'Sub-form',
checker() {
expect(screen.queryByText('mode: Nester')).toBeInTheDocument();
},
},
{
label: 'Sub-form(Popover)',
checker() {
expect(screen.queryByText('mode: PopoverNester')).toBeInTheDocument();
},
},
],
},
]);
});
test('Title field', async () => {
await renderSettings(associationFieldOptions());
await checkSettings([
{
type: 'select',
title: 'Title field',
// oldValue: 'Role name',
async beforeSelect() {
expect(screen.queryByTestId('select-object-multiple')).toBeInTheDocument();
await userEvent.click(document.querySelector('.ant-select-selector'));
await waitFor(() => {
expect(screen.queryByText('Admin')).toBeInTheDocument();
});
},
options: [
{
label: 'Role UID',
async checker() {
expect(screen.queryByTestId('select-object-multiple')).toBeInTheDocument();
await userEvent.click(document.querySelector('.ant-select-selector'));
await waitFor(() => {
expect(screen.queryByText('admin')).toBeInTheDocument();
});
},
},
],
},
]);
});
});
});

View File

@ -7,7 +7,7 @@ import {
useCollection_deprecated, useCollection_deprecated,
useSortFields, useSortFields,
} from '../../../../collection-manager'; } from '../../../../collection-manager';
import { FilterBlockType } from '../../../../filter-provider'; import { FilterBlockType } from '../../../../filter-provider/utils';
import { useDesignable, removeNullCondition } from '../../../../schema-component'; import { useDesignable, removeNullCondition } from '../../../../schema-component';
import { import {
SchemaSettingsBlockTitleItem, SchemaSettingsBlockTitleItem,

View File

@ -0,0 +1,194 @@
import { ACLMenuItemProvider, AdminLayout, BlockSchemaComponentPlugin, CurrentUserProvider } from '@nocobase/client';
import { renderApp, waitFor, screen } from '@nocobase/test/client';
import React from 'react';
describe('AdminLayout', () => {
it('should render correctly', async () => {
await renderApp({
designable: true,
noWrapperSchema: true,
appOptions: {
plugins: [BlockSchemaComponentPlugin],
components: { ACLMenuItemProvider },
providers: [CurrentUserProvider],
router: {
type: 'memory',
initialEntries: ['/admin/9zva4x7mblv'],
routes: {
admin: {
path: '/admin',
element: <AdminLayout />,
},
'admin.name': {
path: '/admin/:name',
element: <div />,
},
},
},
},
apis: {
'app:getInfo': {
data: {
database: {
dialect: 'sqlite',
},
version: '0.21.0-alpha.5',
lang: 'en-US',
name: 'main',
theme: 'default',
},
},
'/uiSchemas:getJsonSchema/nocobase-admin-menu': {
data: {
type: 'void',
'x-component': 'Menu',
'x-designer': 'Menu.Designer',
'x-component-props': {
mode: 'mix',
theme: 'dark',
onSelect: '{{ onSelect }}',
sideMenuRefScopeKey: 'sideMenuRef',
},
properties: {
'9zva4x7mblv': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: 'header title',
'x-component': 'Menu.SubMenu',
'x-decorator': 'ACLMenuItemProvider',
'x-component-props': {},
'x-server-hooks': [
{
type: 'onSelfCreate',
method: 'bindMenuToRole',
},
{
type: 'onSelfSave',
method: 'extractTextToLocale',
},
],
'x-app-version': '0.21.0-alpha.5',
properties: {
yhk3dzb3474: {
'x-uid': 'qzb0p475ld3',
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: 'side title1',
'x-component': 'Menu.Item',
'x-decorator': 'ACLMenuItemProvider',
'x-component-props': {},
'x-server-hooks': [
{
type: 'onSelfSave',
method: 'extractTextToLocale',
},
],
'x-async': false,
'x-index': 1,
},
'2kudkwbme4m': {
'x-uid': 'u9gw7d2d3x5',
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: 'side title2',
'x-component': 'Menu.Item',
'x-decorator': 'ACLMenuItemProvider',
'x-component-props': {},
'x-server-hooks': [
{
type: 'onSelfSave',
method: 'extractTextToLocale',
},
],
'x-async': false,
'x-index': 2,
},
},
'x-uid': '4ic78z722oh',
'x-async': false,
'x-index': 1,
},
},
name: 'pv2f6fp7r65',
'x-uid': 'nocobase-admin-menu',
'x-async': false,
},
},
'roles:check': {
data: {
role: 'root',
strategy: {},
actions: {},
snippets: ['pm', 'pm.*', 'ui.*'],
availableActions: ['create', 'view', 'update', 'destroy', 'export', 'importXlsx'],
resources: [],
actionAlias: {},
allowAll: true,
allowConfigure: null,
allowMenuItemIds: [],
allowAnonymous: false,
},
meta: {
dataSources: {},
},
},
'auth:check': {
data: {
createdAt: '2024-04-07 06:50:37.797 +00:00',
updatedAt: '2024-04-07 06:50:37.797 +00:00',
appLang: null,
createdById: null,
email: 'xxx@nocobase.com',
f_1gx8uyn3wva: 1,
id: 1,
nickname: 'Super Admin',
password: 'xxx',
phone: null,
resetToken: null,
sort: 1,
systemSettings: '{}',
updatedById: null,
username: 'nocobase',
},
},
'systemSettings:get/1': {
data: {
id: 1,
createdAt: '2024-04-07T06:50:37.584Z',
updatedAt: '2024-04-07T06:50:37.594Z',
title: 'NocoBase',
showLogoOnly: null,
allowSignUp: true,
smsAuthEnabled: false,
logoId: 1,
enabledLanguages: ['en-US'],
appLang: 'en-US',
options: {},
},
meta: {
allowedActions: {
view: [1],
update: [1],
destroy: [1],
},
},
},
'uiSchemaTemplates:list': {
data: [],
},
'collectionCategories:list': {
data: [],
},
},
});
await waitFor(() => {
expect(screen.queryByText('header title')).toBeInTheDocument();
expect(screen.queryByText('side title1')).toBeInTheDocument();
expect(screen.queryByText('side title2')).toBeInTheDocument();
});
});
});

View File

@ -0,0 +1,45 @@
import { RouteSchemaComponent } from '@nocobase/client';
import { renderApp, waitFor, screen } from '@nocobase/test/client';
import React from 'react';
describe('route-schema-component', () => {
it('should render correctly', async () => {
await renderApp({
designable: true,
noWrapperSchema: true,
appOptions: {
router: {
type: 'memory',
initialEntries: ['/admin/test'],
routes: {
test: {
path: '/admin/:name',
element: <RouteSchemaComponent />,
},
},
},
},
apis: {
'/uiSchemas:getProperties/test': {
data: {
type: 'void',
properties: {
test: {
'x-component': 'div',
'x-content': 'test',
'x-component-props': {
'data-testid': 'test',
},
},
},
},
},
},
});
await waitFor(() => {
expect(screen.queryByTestId('test')).toBeInTheDocument();
expect(screen.queryByTestId('test')).toHaveTextContent('test');
});
});
});

View File

@ -0,0 +1,51 @@
import { screen, checkSettings, renderSingleSettings, checkModalSetting } from '@nocobase/test/client';
import { ButtonEditor } from '../Action.Designer';
describe('Action.Designer', () => {
test('ButtonEditor', async () => {
const oldTitle = 'title';
const newTitle = 'new title';
const newIcon = 'alipay-circle';
const newBackgroundColor = 'Danger red';
const { container } = await renderSingleSettings({
Component: ButtonEditor,
schema: {
title: oldTitle,
'x-component': 'Action',
},
});
await checkModalSetting({
title: 'Edit button',
modalChecker: {
formItems: [
{
type: 'input',
label: 'Button title',
oldValue: oldTitle,
newValue: newTitle,
},
{
type: 'icon',
label: 'Button icon',
newValue: newIcon,
},
{
type: 'radio',
label: 'Button background color',
oldValue: 'Default',
newValue: newBackgroundColor,
},
],
afterSubmit: () => {
const button = container.querySelector(`button[aria-label="action-Action-${newTitle}"]`);
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent(newTitle);
expect(button).toHaveClass('ant-btn-dangerous');
expect(button.querySelector(`span[aria-label=${newIcon}]`)).toBeInTheDocument();
},
},
});
});
});

View File

@ -16,6 +16,7 @@ export const useGetAriaLabelOfAction = (title: string) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const compile = useCompile(); const compile = useCompile();
const component = fieldSchema['x-component']; const component = fieldSchema['x-component'];
const componentName = typeof component === 'string' ? component : component?.displayName || component?.name;
let recordName = record?.name || record?.title || (recordIndex != null ? String(recordIndex) : ''); let recordName = record?.name || record?.title || (recordIndex != null ? String(recordIndex) : '');
let action = fieldSchema['x-action']; let action = fieldSchema['x-action'];
let { name: collectionName } = useCollection_deprecated(); let { name: collectionName } = useCollection_deprecated();
@ -29,9 +30,9 @@ export const useGetAriaLabelOfAction = (title: string) => {
const getAriaLabel = useCallback( const getAriaLabel = useCallback(
(postfix?: string) => { (postfix?: string) => {
postfix = postfix ? `-${postfix}` : ''; postfix = postfix ? `-${postfix}` : '';
return `action-${component}-${actionTitle}${action}${collectionName}${blockName}${recordName}${postfix}`; return `action-${componentName}-${actionTitle}${action}${collectionName}${blockName}${recordName}${postfix}`;
}, },
[action, actionTitle, blockName, collectionName, component, recordName], [action, actionTitle, blockName, collectionName, componentName, recordName],
); );
return { return {

View File

@ -10,6 +10,7 @@ import { useCompile } from '../../../hooks';
export const useGetAriaLabelOfDrawer = () => { export const useGetAriaLabelOfDrawer = () => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const component = fieldSchema['x-component']; const component = fieldSchema['x-component'];
const componentName = typeof component === 'string' ? component : component?.displayName || component?.name;
const compile = useCompile(); const compile = useCompile();
let { name: collectionName } = useCollection_deprecated(); let { name: collectionName } = useCollection_deprecated();
let title = compile(fieldSchema.title); let title = compile(fieldSchema.title);
@ -19,9 +20,9 @@ export const useGetAriaLabelOfDrawer = () => {
const getAriaLabel = useCallback( const getAriaLabel = useCallback(
(postfix?: string) => { (postfix?: string) => {
postfix = postfix ? `-${postfix}` : ''; postfix = postfix ? `-${postfix}` : '';
return `drawer-${component}${collectionName}${title}${postfix}`; return `drawer-${componentName}${collectionName}${title}${postfix}`;
}, },
[collectionName, component, title], [collectionName, componentName, title],
); );
return { getAriaLabel }; return { getAriaLabel };

View File

@ -10,6 +10,7 @@ import { useCompile } from '../../../hooks';
export const useGetAriaLabelOfModal = () => { export const useGetAriaLabelOfModal = () => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const component = fieldSchema['x-component']; const component = fieldSchema['x-component'];
const componentName = typeof component === 'string' ? component : component?.displayName || component?.name;
const compile = useCompile(); const compile = useCompile();
let { name: collectionName } = useCollection_deprecated(); let { name: collectionName } = useCollection_deprecated();
let title = compile(fieldSchema.title); let title = compile(fieldSchema.title);
@ -19,9 +20,9 @@ export const useGetAriaLabelOfModal = () => {
const getAriaLabel = useCallback( const getAriaLabel = useCallback(
(postfix?: string) => { (postfix?: string) => {
postfix = postfix ? `-${postfix}` : ''; postfix = postfix ? `-${postfix}` : '';
return `modal-${component}${collectionName}${title}${postfix}`; return `modal-${componentName}${collectionName}${title}${postfix}`;
}, },
[collectionName, component, title], [collectionName, componentName, title],
); );
return { getAriaLabel }; return { getAriaLabel };

View File

@ -10,6 +10,7 @@ import { useCompile } from '../../../hooks';
export const useGetAriaLabelOfPopover = () => { export const useGetAriaLabelOfPopover = () => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const component = fieldSchema['x-component']; const component = fieldSchema['x-component'];
const componentName = typeof component === 'string' ? component : component?.displayName || component?.name;
const compile = useCompile(); const compile = useCompile();
let { name: collectionName } = useCollection_deprecated(); let { name: collectionName } = useCollection_deprecated();
let title = compile(fieldSchema.title); let title = compile(fieldSchema.title);
@ -19,9 +20,9 @@ export const useGetAriaLabelOfPopover = () => {
const getAriaLabel = useCallback( const getAriaLabel = useCallback(
(postfix?: string) => { (postfix?: string) => {
postfix = postfix ? `-${postfix}` : ''; postfix = postfix ? `-${postfix}` : '';
return `popover-${component}${collectionName}${title}${postfix}`; return `popover-${componentName}${collectionName}${title}${postfix}`;
}, },
[collectionName, component, title], [collectionName, componentName, title],
); );
return { getAriaLabel }; return { getAriaLabel };

View File

@ -4,7 +4,7 @@ group:
order: 3 order: 3
--- ---
# Action <Badge>待定</Badge> # Action
## Nodes ## Nodes

View File

@ -9,7 +9,7 @@ import { Button, Card, Divider, Tooltip } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useCallback, useContext } from 'react'; import React, { useCallback, useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FormActiveFieldsProvider } from '../../../block-provider'; import { FormActiveFieldsProvider } from '../../../block-provider/hooks/useFormActiveFields';
import { import {
useCollectionRecord, useCollectionRecord,
useCollectionRecordData, useCollectionRecordData,

View File

@ -1,7 +1,7 @@
import { APIClientProvider, AssociationSelect, FormProvider, SchemaComponent } from '@nocobase/client'; import { APIClientProvider, AssociationSelect, FormProvider, SchemaComponent } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { mockAPIClient } from '../../../../testUtils'; import { mockAPIClient } from '../../../../testUtils';
import { sleep } from '@nocobase/test/client'; import { sleep } from '@nocobase/test/web';
const { apiClient, mockRequest } = mockAPIClient(); const { apiClient, mockRequest } = mockAPIClient();
mockRequest.onGet('/posts:list').reply(async () => { mockRequest.onGet('/posts:list').reply(async () => {

View File

@ -12,9 +12,8 @@ import { useCompile } from '../../../hooks';
export const useGetAriaLabelOfBlockItem = (name?: string) => { export const useGetAriaLabelOfBlockItem = (name?: string) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const compile = useCompile(); const compile = useCompile();
const component = _.isString(fieldSchema['x-component']) const component = fieldSchema['x-component'];
? fieldSchema['x-component'] const componentName = typeof component === 'string' ? component : component?.displayName;
: fieldSchema['x-component']?.displayName;
const collectionField = compile(fieldSchema['x-collection-field']); const collectionField = compile(fieldSchema['x-collection-field']);
let { name: blockName } = useBlockContext() || {}; let { name: blockName } = useBlockContext() || {};
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
@ -26,11 +25,11 @@ export const useGetAriaLabelOfBlockItem = (name?: string) => {
const getAriaLabel = useCallback( const getAriaLabel = useCallback(
(postfix?: string) => { (postfix?: string) => {
postfix = postfix ? `-${postfix}` : ''; postfix = postfix ? `-${postfix}` : '';
return ['block-item', component, collectionName, blockName, collectionField, title, postfix] return ['block-item', componentName, collectionName, blockName, collectionField, title, postfix]
.filter(Boolean) .filter(Boolean)
.join('-'); .join('-');
}, },
[component, collectionName, blockName, collectionField, title], [componentName, collectionName, blockName, collectionField, title],
); );
return { return {

View File

@ -4,7 +4,7 @@ group:
order: 3 order: 3
--- ---
# BlockItem <Badge>待定</Badge> # BlockItem
普通的装饰器Decorator组件无特殊 UI 效果,一般用在 x-decorator 中。用于提供区块的管理,如拖拽功能、当前节点的 SettingsForm。CardItem 和 FormItem 组件都是基于 BlockItem 实现,也具备以上相同功能。 普通的装饰器Decorator组件无特殊 UI 效果,一般用在 x-decorator 中。用于提供区块的管理,如拖拽功能、当前节点的 SettingsForm。CardItem 和 FormItem 组件都是基于 BlockItem 实现,也具备以上相同功能。

View File

@ -105,16 +105,11 @@ function useDataSourceOptions({ filter }: DataSourceSelectProps) {
const dataSourceManager = useDataSourceManager(); const dataSourceManager = useDataSourceManager();
const dataSources = dataSourceManager.getDataSources(); const dataSources = dataSourceManager.getDataSources();
return useMemo( return useMemo(
() => [ () =>
{ (typeof filter === 'function' ? dataSources.filter(filter) : dataSources).map((item) => ({
label: compile('Main'), label: compile(item.displayName),
value: 'main',
},
...(typeof filter === 'function' ? dataSources.filter(filter) : dataSources).map((item) => ({
label: item.displayName,
value: item.key, value: item.key,
})), })),
],
[dataSources, filter], [dataSources, filter],
); );
} }

View File

@ -0,0 +1,33 @@
import { renderApp, screen, userEvent, waitFor } from '@nocobase/test/client';
import { DataSourceCollectionCascader } from '../CollectionSelect';
describe('DataSourceCollectionCascader', () => {
test('should works', async () => {
await renderApp({
enableMultipleDataSource: true,
schema: {
type: 'string',
'x-component': DataSourceCollectionCascader,
},
});
await userEvent.click(document.querySelector('.ant-select-selector'));
await waitFor(() => {
expect(screen.queryByText('Main')).toBeInTheDocument();
expect(screen.queryByText('Data Source 2')).toBeInTheDocument();
});
await userEvent.click(screen.getByText('Main'));
await waitFor(() => {
expect(screen.queryByText('Users')).toBeInTheDocument();
expect(screen.queryByText('Roles')).toBeInTheDocument();
});
await userEvent.click(screen.getByText('Users'));
await waitFor(() => {
expect(document.querySelector('.ant-select-selector')).toHaveTextContent('Main / Users');
});
});
});

View File

@ -0,0 +1,81 @@
import { renderApp, screen, userEvent, waitFor, renderReadPrettyApp } from '@nocobase/test/client';
import { DataSourceSelect } from '../CollectionSelect';
describe('DataSourceSelect', () => {
test('single', async () => {
await renderApp({
Component: DataSourceSelect,
});
await userEvent.click(document.querySelector('.ant-select-selector'));
await waitFor(() => {
expect(screen.queryByText('Main')).toBeInTheDocument();
});
});
test('multiple', async () => {
await renderApp({
enableMultipleDataSource: true,
schema: {
type: 'string',
'x-component': DataSourceSelect,
},
});
await userEvent.click(document.querySelector('.ant-select-selector'));
await waitFor(() => {
expect(screen.queryByText('Main')).toBeInTheDocument();
expect(screen.queryByText('Data Source 2')).toBeInTheDocument();
});
});
test('change', async () => {
await renderApp({
enableMultipleDataSource: true,
schema: {
type: 'string',
'x-component': DataSourceSelect,
},
});
await userEvent.click(document.querySelector('.ant-select-selector'));
await waitFor(() => {
expect(screen.queryByText('Main')).toBeInTheDocument();
});
});
test('filter', async () => {
await renderApp({
enableMultipleDataSource: true,
schema: {
type: 'string',
'x-component': DataSourceSelect,
'x-component-props': {
filter(item: any) {
return item.key === 'main';
},
},
},
});
await userEvent.click(document.querySelector('.ant-select-selector'));
await waitFor(() => {
expect(screen.queryByText('Main')).toBeInTheDocument();
expect(screen.queryByText('Data Source 2')).not.toBeInTheDocument();
});
});
// 组件并没有实现这个功能,所以 skip
test.skip('read pretty', async () => {
await renderReadPrettyApp({
value: 'dataSource2',
enableMultipleDataSource: true,
schema: {
type: 'string',
'x-component': DataSourceSelect,
},
});
expect(screen.queryByText('Data Source 2')).toBeInTheDocument();
});
});

View File

@ -1,30 +0,0 @@
import { FormItem } from '@formily/antd-v5';
import { ExtendCollectionsProvider, CollectionSelect, FormProvider, SchemaComponent } from '@nocobase/client';
import React from 'react';
import { useTranslation } from 'react-i18next';
const collections = [];
const schema = {
type: 'object',
properties: {
select: {
type: 'string',
title: 'Collection',
'x-decorator': 'FormItem',
'x-component': 'CollectionSelect',
},
},
};
export default () => {
const { t } = useTranslation();
return (
<FormProvider>
<ExtendCollectionsProvider collections={collections as any}>
<SchemaComponent components={{ FormItem, CollectionSelect }} scope={{ t }} schema={schema} />
</ExtendCollectionsProvider>
</FormProvider>
);
};

View File

@ -6,5 +6,3 @@ group:
# CollectionSelect # CollectionSelect
## Example ## Example
<code src="./demos/demo1.tsx"></code>

View File

@ -1,24 +0,0 @@
import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__';
import type { ColorPickerProps as AntdColorPickerProps } from 'antd/es/color-picker';
import cls from 'classnames';
import React from 'react';
type Composed = {
ColorPicker: React.FC<AntdColorPickerProps>;
};
export const ReadPretty: Composed = () => null;
ReadPretty.ColorPicker = function ColorPicker(props: any) {
const prefixCls = usePrefixCls('description-color-picker', props);
if (!props.value) {
return <div></div>;
}
return (
<div className={cls(prefixCls, props.className)}>
<ColorPicker showText disabled value={props.value} size="small" />
</div>
);
};

View File

@ -1,160 +0,0 @@
import { dayjs, getDefaultFormat, str2moment, toGmt, toLocal } from '@nocobase/utils/client';
import type { Dayjs } from 'dayjs';
const toStringByPicker = (value, picker, timezone: 'gmt' | 'local') => {
if (!dayjs.isDayjs(value)) return value;
if (timezone === 'local') {
const offset = new Date().getTimezoneOffset();
return dayjs(toStringByPicker(value, picker, 'gmt'))
.add(offset, 'minutes')
.toISOString();
}
if (picker === 'year') {
return value.format('YYYY') + '-01-01T00:00:00.000Z';
}
if (picker === 'month') {
return value.format('YYYY-MM') + '-01T00:00:00.000Z';
}
if (picker === 'quarter') {
return value.startOf('quarter').format('YYYY-MM') + '-01T00:00:00.000Z';
}
if (picker === 'week') {
return value.startOf('week').add(1, 'day').format('YYYY-MM-DD') + 'T00:00:00.000Z';
}
return value.format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z';
};
const toGmtByPicker = (value: Dayjs, picker?: any) => {
if (!value || !dayjs.isDayjs(value)) {
return value;
}
return toStringByPicker(value, picker, 'gmt');
};
const toLocalByPicker = (value: Dayjs, picker?: any) => {
if (!value || !dayjs.isDayjs(value)) {
return value;
}
return toStringByPicker(value, picker, 'local');
};
export interface Moment2strOptions {
showTime?: boolean;
gmt?: boolean;
utc?: boolean;
picker?: 'year' | 'month' | 'week' | 'quarter';
}
export const moment2str = (value?: Dayjs | null, options: Moment2strOptions = {}) => {
const { showTime, gmt, picker, utc = true } = options;
if (!value) {
return value;
}
if (!utc) {
const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
return value.format(format);
}
if (showTime) {
return gmt ? toGmt(value) : toLocal(value);
}
if (typeof gmt === 'boolean') {
return gmt ? toGmtByPicker(value, picker) : toLocalByPicker(value, picker);
}
return toGmtByPicker(value, picker);
};
export const mapDatePicker = function () {
return (props: any) => {
const format = getDefaultFormat(props) as any;
const onChange = props.onChange;
return {
...props,
format: format,
value: str2moment(props.value, props),
onChange: (value: Dayjs | null) => {
if (onChange) {
if (!props.showTime && value) {
value = value.startOf('day');
}
onChange(moment2str(value, props));
}
},
};
};
};
export const mapRangePicker = function () {
return (props: any) => {
const format = getDefaultFormat(props) as any;
const onChange = props.onChange;
return {
...props,
format: format,
value: str2moment(props.value, props),
onChange: (value: Dayjs[]) => {
if (onChange) {
onChange(
value
? [moment2str(getRangeStart(value[0], props), props), moment2str(getRangeEnd(value[1], props), props)]
: [],
);
}
},
} as any;
};
};
function getRangeStart(value: Dayjs, options: Moment2strOptions) {
const { showTime } = options;
if (showTime) {
return value;
}
return value.startOf('day');
}
function getRangeEnd(value: Dayjs, options: Moment2strOptions) {
const { showTime } = options;
if (showTime) {
return value;
}
return value.endOf('day');
}
const getStart = (offset: any, unit: any) => {
return dayjs()
.add(offset, unit === 'isoWeek' ? 'week' : unit)
.startOf(unit);
};
const getEnd = (offset: any, unit: any) => {
return dayjs()
.add(offset, unit === 'isoWeek' ? 'week' : unit)
.endOf(unit);
};
export const getDateRanges = () => {
return {
today: () => [getStart(0, 'day'), getEnd(0, 'day')],
lastWeek: () => [getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')],
thisWeek: () => [getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')],
nextWeek: () => [getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')],
lastMonth: () => [getStart(-1, 'month'), getEnd(-1, 'month')],
thisMonth: () => [getStart(0, 'month'), getEnd(0, 'month')],
nextMonth: () => [getStart(1, 'month'), getEnd(1, 'month')],
lastQuarter: () => [getStart(-1, 'quarter'), getEnd(-1, 'quarter')],
thisQuarter: () => [getStart(0, 'quarter'), getEnd(0, 'quarter')],
nextQuarter: () => [getStart(1, 'quarter'), getEnd(1, 'quarter')],
lastYear: () => [getStart(-1, 'year'), getEnd(-1, 'year')],
thisYear: () => [getStart(0, 'year'), getEnd(0, 'year')],
nextYear: () => [getStart(1, 'year'), getEnd(1, 'year')],
last7Days: () => [getStart(-6, 'days'), getEnd(0, 'days')],
next7Days: () => [getStart(1, 'day'), getEnd(7, 'days')],
last30Days: () => [getStart(-29, 'days'), getEnd(0, 'days')],
next30Days: () => [getStart(1, 'day'), getEnd(30, 'days')],
last90Days: () => [getStart(-89, 'days'), getEnd(0, 'days')],
next90Days: () => [getStart(1, 'day'), getEnd(90, 'days')],
};
};

View File

@ -1,6 +1,6 @@
import { str2moment } from '@nocobase/utils/client';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { moment2str } from '../util'; import { str2moment } from '@nocobase/utils/client';
import { getDateRanges, moment2str } from '../util';
describe('str2moment', () => { describe('str2moment', () => {
describe('string value', () => { describe('string value', () => {
@ -138,3 +138,152 @@ describe('moment2str', () => {
expect(m).toBeNull(); expect(m).toBeNull();
}); });
}); });
// CI 环境会报错,可能因为时区问题
describe.skip('getDateRanges', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2024-03-15T10:10:10.000Z'));
});
afterEach(() => {
vi.useRealTimers();
});
test('now', () => {
const now = getDateRanges().now();
expect(now.valueOf()).toMatchInlineSnapshot(`"2024-03-15T10:10:10.000Z"`);
});
test('today', () => {
const [start, end] = getDateRanges().today();
expect(start.valueOf()).toMatchInlineSnapshot(`1710432000000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1710518399999`);
});
test('lastWeek', () => {
const [start, end] = getDateRanges().lastWeek();
expect(start.valueOf()).toMatchInlineSnapshot(`1709481600000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1710086399999`);
});
test('thisWeek', () => {
const [start, end] = getDateRanges().thisWeek();
expect(start.valueOf()).toMatchInlineSnapshot(`1710086400000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1710691199999`);
});
test('nextWeek', () => {
const [start, end] = getDateRanges().nextWeek();
expect(start.valueOf()).toMatchInlineSnapshot(`1710691200000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1711295999999`);
});
test('thisIsoWeek', () => {
const [start, end] = getDateRanges().thisIsoWeek();
expect(start.valueOf()).toMatchInlineSnapshot(`1710086400000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1710691199999`);
});
test('lastIsoWeek', () => {
const [start, end] = getDateRanges().lastIsoWeek();
expect(start.valueOf()).toMatchInlineSnapshot(`1709481600000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1710086399999`);
});
test('nextIsoWeek', () => {
const [start, end] = getDateRanges().nextIsoWeek();
expect(start.valueOf()).toMatchInlineSnapshot(`1710691200000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1711295999999`);
});
test('lastMonth', () => {
const [start, end] = getDateRanges().lastMonth();
expect(start.valueOf()).toMatchInlineSnapshot(`1706716800000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1709222399999`);
});
test('thisMonth', () => {
const [start, end] = getDateRanges().thisMonth();
expect(start.valueOf()).toMatchInlineSnapshot(`1709222400000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1711900799999`);
});
test('nextMonth', () => {
const [start, end] = getDateRanges().nextMonth();
expect(start.valueOf()).toMatchInlineSnapshot(`1711900800000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1714492799999`);
});
test('lastQuarter', () => {
const [start, end] = getDateRanges().lastQuarter();
expect(start.valueOf()).toMatchInlineSnapshot(`1696089600000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1704038399999`);
});
test('thisQuarter', () => {
const [start, end] = getDateRanges().thisQuarter();
expect(start.valueOf()).toMatchInlineSnapshot(`1704038400000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1711900799999`);
});
test('nextQuarter', () => {
const [start, end] = getDateRanges().nextQuarter();
expect(start.valueOf()).toMatchInlineSnapshot(`1711900800000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1719763199999`);
});
test('lastYear', () => {
const [start, end] = getDateRanges().lastYear();
expect(start.valueOf()).toMatchInlineSnapshot(`1672502400000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1704038399999`);
});
test('thisYear', () => {
const [start, end] = getDateRanges().thisYear();
expect(start.valueOf()).toMatchInlineSnapshot(`1704038400000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1735660799999`);
});
test('nextYear', () => {
const [start, end] = getDateRanges().nextYear();
expect(start.valueOf()).toMatchInlineSnapshot(`1735660800000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1767196799999`);
});
test('last7Days', () => {
const [start, end] = getDateRanges().last7Days();
expect(start.valueOf()).toMatchInlineSnapshot(`1709913600000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1710518399999`);
});
test('next7Days', () => {
const [start, end] = getDateRanges().next7Days();
expect(start.valueOf()).toMatchInlineSnapshot(`1710518400000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1711123199999`);
});
test('last30Days', () => {
const [start, end] = getDateRanges().last30Days();
expect(start.valueOf()).toMatchInlineSnapshot(`1707926400000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1710518399999`);
});
test('next30Days', () => {
const [start, end] = getDateRanges().next30Days();
expect(start.valueOf()).toMatchInlineSnapshot(`1710518400000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1713110399999`);
});
test('last90Days', () => {
const [start, end] = getDateRanges().last90Days();
expect(start.valueOf()).toMatchInlineSnapshot(`1702742400000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1710518399999`);
});
test('next90Days', () => {
const [start, end] = getDateRanges().next90Days();
expect(start.valueOf()).toMatchInlineSnapshot(`1710518400000`);
expect(end.valueOf()).toMatchInlineSnapshot(`1718294399999`);
});
});

View File

@ -0,0 +1,657 @@
import {
screen,
checkSettings,
renderSingleSettings,
waitFor,
userEvent,
renderReadPrettySingleSettings,
} from '@nocobase/test/client';
import {
EditComponent,
EditDefaultValue,
EditDescription,
EditOperator,
EditPattern,
EditRequired,
EditTitle,
EditTitleField,
EditTooltip,
EditValidationRules,
} from '../SchemaSettingOptions';
import { FilterFormBlockProvider, FormBlockProvider, FormItem } from '@nocobase/client';
import { useFieldSchema } from '@formily/react';
import { observer } from '@formily/reactive-react';
import React from 'react';
describe('SchemaSettingOptions', () => {
describe('EditTitle', () => {
test('should work', async () => {
const oldValue = 'Nickname';
const newValue = 'new test';
await renderSingleSettings({
Component: EditTitle,
enableUserListDataBlock: true,
schema: {
type: 'string',
name: 'nickname',
default: oldValue,
'x-collection-field': 'users.nickname',
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
},
});
await checkSettings([
{
type: 'modal',
title: 'Edit field title',
modalChecker: {
modalTitle: 'Edit field title',
formItems: [
{
type: 'input',
label: 'Field title',
oldValue,
newValue,
},
],
async afterSubmit() {
await waitFor(() => {
expect(screen.queryByText(newValue)).toBeInTheDocument();
});
},
},
},
]);
});
test('visible: false', async () => {
const fieldName = 'not-exist';
await renderSingleSettings({
Component: EditTitle,
enableUserListDataBlock: true,
schema: {
type: 'string',
name: fieldName,
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
},
});
expect(screen.queryByText('Edit field title')).not.toBeInTheDocument();
});
});
describe('EditDescription', () => {
test('should work', async () => {
const newValue = 'new test';
await renderSingleSettings({
Component: EditDescription,
enableUserListDataBlock: true,
schema: {
type: 'string',
name: 'nickname',
'x-collection-field': 'users.nickname',
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
},
});
await checkSettings([
{
type: 'modal',
title: 'Edit description',
modalChecker: {
modalTitle: 'Edit description',
formItems: [
{
type: 'textarea',
newValue,
},
],
afterSubmit() {
expect(screen.queryByText(newValue)).toBeInTheDocument();
},
},
},
]);
});
test('read pretty mode should not render', async () => {
await renderSingleSettings({
Component: EditDescription,
enableUserListDataBlock: true,
schema: {
type: 'string',
name: 'nickname',
'x-collection-field': 'users.nickname',
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
});
expect(screen.queryByText('Edit description')).not.toBeInTheDocument();
});
});
describe('EditTooltip', () => {
test('should work', async () => {
const newValue = 'new test';
await renderSingleSettings({
Component: EditTooltip,
enableUserListDataBlock: true,
schema: {
type: 'string',
name: 'nickname',
'x-collection-field': 'users.nickname',
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
});
await checkSettings([
{
type: 'modal',
title: 'Edit tooltip',
modalChecker: {
modalTitle: 'Edit description',
formItems: [
{
type: 'textarea',
newValue,
},
],
async afterSubmit() {
expect(document.querySelector('.anticon-question-circle')).toBeInTheDocument();
await userEvent.hover(document.querySelector('.anticon-question-circle'));
await waitFor(() => {
expect(screen.queryByText(newValue)).toBeInTheDocument();
});
},
},
},
]);
});
test('read pretty mode should not render', async () => {
await renderSingleSettings({
Component: EditTooltip,
enableUserListDataBlock: true,
schema: {
type: 'string',
name: 'nickname',
'x-collection-field': 'users.nickname',
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
},
});
expect(screen.queryByText('Edit tooltip')).not.toBeInTheDocument();
});
});
describe('EditRequired', () => {
test('should work', async () => {
await renderSingleSettings({
Component: EditRequired,
enableUserListDataBlock: true,
schema: {
type: 'string',
name: 'nickname',
'x-collection-field': 'users.nickname',
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
},
});
await checkSettings([
{
type: 'switch',
title: 'Required',
afterFirstClick() {
expect(document.querySelector('.ant-formily-item-asterisk')).toBeInTheDocument();
},
afterSecondClick() {
expect(document.querySelector('.ant-formily-item-asterisk')).not.toBeInTheDocument();
},
},
]);
});
test('read pretty mode should not render', async () => {
await renderSingleSettings({
Component: EditRequired,
enableUserListDataBlock: true,
schema: {
type: 'string',
name: 'nickname',
'x-collection-field': 'users.nickname',
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
});
expect(screen.queryByText('Edit tooltip')).not.toBeInTheDocument();
});
});
describe('EditDefaultValue', () => {
test('should work', async () => {
await renderSingleSettings({
Component: EditDefaultValue,
settingPath: 'properties.nickname',
enableUserListDataBlock: true,
schema: {
type: 'void',
'x-component': 'FormV2',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
collection: 'users',
},
properties: {
nickname: {
type: 'string',
'x-collection-field': 'users.nickname',
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
},
},
},
appOptions: {
components: {
FormBlockProvider,
},
},
});
const newValue = 'new test';
await checkSettings([
{
type: 'modal',
title: 'Set default value',
modalChecker: {
modalTitle: 'Set default value',
formItems: [
{
type: 'input',
label: 'Default value',
newValue,
},
],
afterSubmit() {
expect(screen.queryByRole('textbox')).toBeInTheDocument();
expect(screen.queryByRole('textbox')).toHaveValue(newValue);
},
},
},
]);
});
});
describe('EditPattern', () => {
test('should work', async () => {
await renderSingleSettings({
Component: EditPattern,
settingPath: 'properties.nickname',
enableUserListDataBlock: true,
schema: {
type: 'void',
'x-component': 'FormV2',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
collection: 'users',
},
properties: {
nickname: {
type: 'string',
default: 'nickname value',
'x-collection-field': 'users.nickname',
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
},
},
},
appOptions: {
components: {
FormBlockProvider,
},
},
});
await checkSettings([
{
type: 'select',
title: 'Pattern',
oldValue: 'Editable',
options: [
{
label: 'Readonly',
checker() {
expect(screen.queryByRole('textbox')).toHaveAttribute('disabled');
},
},
{
label: 'Easy-reading',
checker() {
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
expect(screen.queryByText('nickname value')).toBeInTheDocument();
},
},
{
label: 'Editable',
checker() {
expect(screen.queryByRole('textbox')).toBeInTheDocument();
expect(screen.queryByRole('textbox')).toHaveValue('nickname value');
},
},
],
},
]);
});
});
describe('EditComponent', () => {
test('should work', async () => {
const FormItemWithSchema = observer(({ children }) => {
const schema = useFieldSchema();
return (
<>
<div>mode: {schema?.['x-component-props']?.['mode']}</div>
<FormItem>{children}</FormItem>
</>
);
});
await renderSingleSettings({
Component: EditComponent,
settingPath: 'properties.roles',
enableUserListDataBlock: true,
schema: {
type: 'void',
'x-component': 'FormV2',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
collection: 'users',
},
properties: {
roles: {
type: 'string',
'x-collection-field': 'users.roles',
'x-decorator': 'FormItemWithSchema',
'x-component': 'CollectionField',
'x-component-props': {
fieldNames: {
label: 'name',
value: 'name',
},
},
},
},
},
appOptions: {
components: {
FormBlockProvider,
FormItemWithSchema,
},
},
});
await checkSettings([
{
type: 'select',
title: 'Field component',
oldValue: 'Select',
beforeSelect() {
expect(screen.queryByTestId('select-object-multiple')).toBeInTheDocument();
},
options: [
{
label: 'Record picker',
async checker() {
expect(screen.queryByText('mode: Picker')).toBeInTheDocument();
},
},
{
label: 'Sub-table',
checker() {
expect(screen.queryByText('mode: SubTable')).toBeInTheDocument();
},
},
{
label: 'Sub-form',
checker() {
expect(screen.queryByText('mode: Nester')).toBeInTheDocument();
},
},
{
label: 'Sub-form(Popover)',
checker() {
expect(screen.queryByText('mode: PopoverNester')).toBeInTheDocument();
},
},
],
},
]);
});
});
describe('EditTitleField', () => {
test('should work', async () => {
await renderSingleSettings({
Component: EditTitleField,
settingPath: 'properties.roles',
enableUserListDataBlock: true,
schema: {
type: 'void',
'x-component': 'FormV2',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
collection: 'users',
},
properties: {
roles: {
type: 'string',
'x-collection-field': 'users.roles',
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
'x-component-props': {
fieldNames: {
label: 'name',
value: 'name',
},
},
},
},
},
appOptions: {
components: {
FormBlockProvider,
},
},
});
await checkSettings([
{
type: 'select',
title: 'Title field',
oldValue: 'Role UID',
async beforeSelect() {
expect(screen.queryByTestId('select-object-multiple')).toBeInTheDocument();
await userEvent.click(document.querySelector('.ant-select-selector'));
await waitFor(() => {
expect(screen.queryByText('admin')).toBeInTheDocument();
});
},
options: [
{
label: 'Role name',
async checker() {
expect(screen.queryByTestId('select-object-multiple')).toBeInTheDocument();
await userEvent.click(document.querySelector('.ant-select-selector'));
await waitFor(() => {
expect(screen.queryByText('Admin')).toBeInTheDocument();
});
},
},
],
},
]);
});
});
describe('EditValidationRules', () => {
test('should work', async () => {
await renderSingleSettings({
Component: EditValidationRules,
settingPath: 'properties.nickname',
enableUserListDataBlock: true,
schema: {
type: 'void',
'x-component': 'FormV2',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
collection: 'users',
},
properties: {
nickname: {
type: 'string',
'x-collection-field': 'users.nickname',
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
},
},
},
appOptions: {
components: {
FormBlockProvider,
},
},
});
await checkSettings([
{
type: 'modal',
title: 'Set validation rules',
modalChecker: {
modalTitle: 'Set validation rules',
contentText: 'Add validation rule',
async beforeCheck() {
await userEvent.click(screen.getByText('Add validation rule'));
await waitFor(() => {
expect(screen.queryByText('Min length')).toBeInTheDocument();
});
},
formItems: [
{
type: 'number',
label: 'Min length',
newValue: 2,
},
],
async afterSubmit() {
await userEvent.type(screen.getByRole('textbox'), '1');
await waitFor(() => {
expect(screen.queryByText('The length or number of entries must be at least 2')).toBeInTheDocument();
});
await userEvent.type(screen.getByRole('textbox'), '12');
await waitFor(() => {
expect(
screen.queryByText('The length or number of entries must be at least 2'),
).not.toBeInTheDocument();
});
},
},
},
]);
});
test('read pretty mode should not render', async () => {
await renderReadPrettySingleSettings({
Component: EditValidationRules,
settingPath: 'properties.nickname',
enableUserListDataBlock: true,
schema: {
type: 'void',
'x-component': 'FormV2',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
collection: 'users',
},
properties: {
nickname: {
type: 'string',
'x-collection-field': 'users.nickname',
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
},
},
},
appOptions: {
components: {
FormBlockProvider,
},
},
});
expect(screen.queryByText('Edit tooltip')).not.toBeInTheDocument();
});
});
describe('EditOperator', () => {
test('should work', async () => {
await renderSingleSettings({
Component: EditOperator,
settingPath: 'properties.nickname',
enableUserListDataBlock: true,
schema: {
type: 'void',
'x-component': 'FormV2',
'x-decorator': 'FilterFormBlockProvider',
'x-decorator-props': {
collection: 'users',
},
properties: {
nickname: {
type: 'string',
'x-collection-field': 'users.nickname',
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
},
},
},
appOptions: {
components: {
FilterFormBlockProvider,
},
},
});
await checkSettings([
{
type: 'select',
title: 'Operator',
oldValue: 'contains',
options: [
{
label: 'does not contain',
},
{
label: 'is',
},
{
label: 'is not',
},
{
label: 'is empty',
},
{
label: 'is not empty',
},
],
},
]);
});
});
});

View File

@ -0,0 +1,219 @@
import { BlockSchemaComponentPlugin, FormBlockProvider, VariablesProvider } from '@nocobase/client';
import { checkSettings, renderApp } from '@nocobase/test/client';
import { withSchema } from '@nocobase/test/web';
describe('form.settings', () => {
test('new schema version', async () => {
const FormBlockProviderWithSchema = withSchema(FormBlockProvider);
await renderApp({
designable: true,
schema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-acl-action-props': {
skipScopeCheck: true,
},
'x-acl-action': 'users:create',
'x-decorator': 'FormBlockProviderWithSchema',
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: 'users',
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:createForm',
'x-component': 'CardItem',
properties: {
grid: {
'x-uid': 'h38s9pa4ik5',
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-app-version': '0.21.0-alpha.10',
properties: {
udpf3e45i3d: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '0.21.0-alpha.10',
properties: {
hhc0bsk1roi: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '0.21.0-alpha.10',
properties: {
username: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.username',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
'x-uid': '71x74r4t4g0',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ophjdttgmo5',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ta1vq3qr1sd',
'x-async': false,
'x-index': 3,
},
row_rpkxgfonud3: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-index': 4,
properties: {
mmo2k17b0q1: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
nickname: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.nickname',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
'x-uid': 'bcowga6nzzy',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'l1awt5at07z',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'y1tdyhcwhhi',
'x-async': false,
},
},
'x-async': false,
'x-index': 1,
},
'0m1r08p58e9': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
style: {
marginTop: 24,
},
},
'x-app-version': '0.21.0-alpha.10',
'x-uid': 't4gxf0xxaxc',
'x-async': false,
'x-index': 2,
},
},
},
appOptions: {
plugins: [BlockSchemaComponentPlugin],
providers: [VariablesProvider],
components: {
FormBlockProviderWithSchema,
},
},
});
await checkSettings([
{
title: 'Edit block title',
type: 'modal',
},
{
title: 'Linkage rules',
type: 'modal',
modalChecker: {
modalTitle: 'Linkage rules',
contentText: 'Add linkage rule',
async customCheck() {
// await userEvent.click(screen.getByText('Add linkage rule'));
// await waitFor(() => {
// expect(screen.queryByText('Add condition')).toBeInTheDocument();
// })
// await userEvent.click(screen.getByText('Add condition'));
// await waitFor(() => {
// expect(screen.queryByText('Select field')).toBeInTheDocument();
// })
// await userEvent.click(screen.getByText('Select field'));
// await waitFor(() => {
// expect(screen.queryByTitle('Username')).toBeInTheDocument();
// })
// await userEvent.click(screen.getByTitle('Username'));
// const dialog = screen.queryByRole('dialog');
// await userEvent.type(dialog.querySelectorAll('.ant-input')[1], '1');
// const properties = screen.queryByTestId('select-linkage-property-field');
// await userEvent.click(screen.getByText('Select field'));
// await waitFor(() => {
// expect(properties.querySelector(`[title=Nickname]`)).toBeInTheDocument();
// })
// await userEvent.click(properties.querySelector(`[title=Nickname]`));
// await userEvent.click(screen.getByText('action'));
// await waitFor(() => {
// expect(screen.queryByText('Hidden')).toBeInTheDocument();
// })
// await userEvent.click(screen.getByText('Hidden'));
},
// async afterSubmit() {
// await checkSchema({
// "x-linkage-rules": [
// {
// "condition": {
// "$and": [
// {
// "username": {}
// }
// ]
// },
// "actions": [
// {
// "targetFields": [
// "nickname"
// ],
// "operator": "none"
// }
// ]
// }
// ]
// })
// },
},
},
{
title: 'Form data templates',
type: 'modal',
},
{
title: 'Save as block template',
type: 'modal',
},
{
title: 'Delete',
type: 'delete',
},
]);
});
// test('old schema version', async () => {});
});

View File

@ -8,6 +8,7 @@ import App5 from '../demos/demo5';
import App6 from '../demos/demo6'; import App6 from '../demos/demo6';
import App7 from '../demos/demo7'; import App7 from '../demos/demo7';
import App8 from '../demos/demo8'; import App8 from '../demos/demo8';
import { renderDemo9 } from '../demos/demo9';
describe('Form', () => { describe('Form', () => {
it('basic', async () => { it('basic', async () => {
@ -151,4 +152,25 @@ describe('Form', () => {
expect(closeBtn).toBeInTheDocument(); expect(closeBtn).toBeInTheDocument();
expect(screen.getByText(/drawer title/i)).toBeInTheDocument(); expect(screen.getByText(/drawer title/i)).toBeInTheDocument();
}); });
it('linkage', async () => {
await renderDemo9();
await waitFor(() => {
expect(document.querySelector('.ant-input')).toBeInTheDocument();
expect(document.querySelectorAll('.ant-input')).toHaveLength(2);
});
await userEvent.type(document.querySelector('.ant-input'), 'test');
await waitFor(() => {
expect(document.querySelectorAll('.ant-input')).toHaveLength(1);
});
await userEvent.clear(document.querySelector('.ant-input'));
await waitFor(() => {
expect(document.querySelectorAll('.ant-input')).toHaveLength(2);
});
});
}); });

View File

@ -0,0 +1,165 @@
import { BlockSchemaComponentPlugin, VariablesProvider } from '@nocobase/client';
import { renderApp } from '@nocobase/test/client';
export const renderDemo9 = () =>
renderApp({
designable: true,
schema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-acl-action-props': {
skipScopeCheck: true,
},
'x-acl-action': 'users:create',
'x-decorator': 'FormBlockProvider',
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: 'users',
},
'x-component': 'div',
'x-app-version': '0.21.0-alpha.10',
properties: {
'0s3tm262rre': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useCreateFormBlockProps',
'x-app-version': '0.21.0-alpha.10',
properties: {
grid: {
'x-uid': 'h38s9pa4ik5',
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-app-version': '0.21.0-alpha.10',
'x-linkage-rules': [
{
condition: {
$and: [
{
username: {
$eq: 'test',
},
},
],
},
actions: [
{
targetFields: ['nickname'],
operator: 'none',
},
],
},
],
properties: {
udpf3e45i3d: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '0.21.0-alpha.10',
properties: {
hhc0bsk1roi: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '0.21.0-alpha.10',
properties: {
username: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.username',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
'x-uid': '71x74r4t4g0',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ophjdttgmo5',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ta1vq3qr1sd',
'x-async': false,
'x-index': 3,
},
row_rpkxgfonud3: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-index': 4,
properties: {
mmo2k17b0q1: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
nickname: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.nickname',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
'x-uid': 'bcowga6nzzy',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'l1awt5at07z',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'y1tdyhcwhhi',
'x-async': false,
},
},
'x-async': false,
'x-index': 1,
},
'0m1r08p58e9': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
style: {
marginTop: 24,
},
},
'x-app-version': '0.21.0-alpha.10',
'x-uid': 't4gxf0xxaxc',
'x-async': false,
'x-index': 2,
},
},
'x-uid': 'yk2fivh9hgb',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'aqbi3avt3kb',
'x-async': false,
'x-index': 1,
},
appOptions: {
plugins: [BlockSchemaComponentPlugin],
providers: [VariablesProvider],
},
});

View File

@ -2,7 +2,7 @@ import { FormLayout } from '@formily/antd-v5';
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { FormContext, useField, useFieldSchema } from '@formily/react'; import { FormContext, useField, useFieldSchema } from '@formily/react';
import React, { createContext, useContext, useEffect, useMemo } from 'react'; import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { BlockProvider, useBlockRequestContext, useParsedFilter } from '../../../block-provider'; import { BlockProvider, useBlockRequestContext } from '../../../block-provider/BlockProvider';
import useStyles from './GridCard.Decorator.style'; import useStyles from './GridCard.Decorator.style';
import { useGridCardBlockParams } from '../../../modules/blocks/data-blocks/grid-card/hooks/useGridCardBlockParams'; import { useGridCardBlockParams } from '../../../modules/blocks/data-blocks/grid-card/hooks/useGridCardBlockParams';
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps'; import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';

View File

@ -1,9 +1,312 @@
import { render } from '@nocobase/test/client'; import { BlockSchemaComponentPlugin } from '@nocobase/client';
import React from 'react'; import { renderApp, waitFor, screen, userEvent } from '@nocobase/test/client';
import App1 from '../demos/demo1';
describe('GridCard', () => { describe('GridCard', () => {
it('should render correctly', () => { it('should render correctly', async () => {
render(<App1 />); await renderApp({
designable: true,
schema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-acl-action': 'users:view',
'x-decorator': 'GridCard.Decorator',
'x-use-decorator-props': 'useGridCardBlockDecoratorProps',
'x-decorator-props': {
collection: 'users',
dataSource: 'main',
readPretty: true,
action: 'list',
params: {
pageSize: 12,
},
runWhenParamsChanged: true,
rowKey: 'id',
},
'x-component': 'div',
'x-app-version': '0.21.0-alpha.10',
properties: {
actionBar: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
'x-app-version': '0.21.0-alpha.10',
},
list: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'array',
'x-component': 'GridCard',
'x-app-version': '0.21.0-alpha.10',
properties: {
item: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'object',
'x-component': 'GridCard.Item',
'x-read-pretty': true,
'x-use-component-props': 'useGridCardItemProps',
'x-app-version': '0.21.0-alpha.10',
properties: {
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-app-version': '0.21.0-alpha.10',
properties: {
b8r0aisveq3: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '0.21.0-alpha.10',
properties: {
h3ycwb9e5qv: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '0.21.0-alpha.10',
properties: {
id: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.id',
'x-component-props': {},
'x-read-pretty': true,
'x-app-version': '0.21.0-alpha.10',
},
},
},
},
},
ycbv8h4ymzy: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '0.21.0-alpha.10',
properties: {
'2bz1mvhxhsw': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '0.21.0-alpha.10',
properties: {
nickname: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.nickname',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
},
},
},
},
},
vd06ptpdvjd: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '0.21.0-alpha.10',
properties: {
knl514ethip: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '0.21.0-alpha.10',
properties: {
username: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.username',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
},
},
},
},
},
},
},
actionBar: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-align': 'left',
'x-component': 'ActionBar',
'x-use-component-props': 'useGridCardActionBarProps',
'x-component-props': {
layout: 'one-column',
},
'x-app-version': '0.21.0-alpha.10',
properties: {
jquyomz6ipk: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("View") }}',
'x-action': 'view',
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
},
'x-align': 'left',
'x-app-version': '0.21.0-alpha.10',
properties: {
drawer: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("View record") }}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
'x-app-version': '0.21.0-alpha.10',
properties: {
tabs: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
properties: {
tab1: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{t("Details")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
properties: {
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-app-version': '0.21.0-alpha.10',
},
},
},
},
},
},
},
},
},
bc60nzpw94v: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("Edit") }}',
'x-action': 'update',
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
icon: 'EditOutlined',
},
'x-align': 'left',
'x-app-version': '0.21.0-alpha.10',
properties: {
drawer: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("Edit record") }}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
'x-app-version': '0.21.0-alpha.10',
properties: {
tabs: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
properties: {
tab1: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{t("Edit")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
properties: {
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-app-version': '0.21.0-alpha.10',
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
appOptions: {
plugins: [BlockSchemaComponentPlugin],
scopes: {},
},
});
await waitFor(() => {
expect(screen.queryByText('ID')).toBeInTheDocument();
expect(screen.queryByText('1')).toBeInTheDocument();
});
expect(screen.queryByText('Nickname')).toBeInTheDocument();
expect(screen.queryByText('Super Admin')).toBeInTheDocument();
expect(screen.queryByText('Username')).toBeInTheDocument();
expect(screen.queryByText('nocobase')).toBeInTheDocument();
expect(screen.queryByText('View')).toBeInTheDocument();
expect(screen.queryByText('Edit')).toBeInTheDocument();
await userEvent.click(screen.getByText('View'));
await waitFor(() => {
expect(screen.queryByRole('tab')).toHaveTextContent('Details');
});
}); });
}); });

View File

@ -1,5 +0,0 @@
import React from 'react';
export default () => {
return <div>TODO</div>;
};

View File

@ -4,5 +4,3 @@ group:
--- ---
# GridCard # GridCard
<code src="./demos/demo1.tsx"></code>

View File

@ -4,8 +4,9 @@ import { createForm } from '@formily/core';
import { FormContext, useField } from '@formily/react'; import { FormContext, useField } from '@formily/react';
import _ from 'lodash'; import _ from 'lodash';
import React, { createContext, useContext, useEffect, useMemo } from 'react'; import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { BlockProvider, useBlockRequestContext, useParsedFilter } from '../../../block-provider'; import { BlockProvider, useBlockRequestContext } from '../../../block-provider/BlockProvider';
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps'; import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
import { useParsedFilter } from '../../../block-provider/hooks/useParsedFilter';
export const ListBlockContext = createContext<any>({}); export const ListBlockContext = createContext<any>({});
ListBlockContext.displayName = 'ListBlockContext'; ListBlockContext.displayName = 'ListBlockContext';

View File

@ -1,9 +1,237 @@
import { render } from '@nocobase/test/client'; import { BlockSchemaComponentPlugin } from '@nocobase/client';
import React from 'react'; import { renderApp, waitFor, screen, userEvent } from '@nocobase/test/client';
import App1 from '../demos/demo1';
describe('List', () => { describe('List', () => {
it('should render correctly', () => { it('should render correctly', async () => {
render(<App1 />); await renderApp({
designable: true,
schema: {
type: 'void',
'x-acl-action': 'users:view',
'x-decorator': 'List.Decorator',
'x-use-decorator-props': 'useListBlockDecoratorProps',
'x-decorator-props': {
collection: 'users',
dataSource: 'main',
readPretty: true,
action: 'list',
params: {
pageSize: 10,
},
runWhenParamsChanged: true,
rowKey: 'id',
},
'x-component': 'CardItem',
'x-app-version': '0.21.0-alpha.10',
properties: {
actionBar: {
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
'x-app-version': '0.21.0-alpha.10',
},
list: {
type: 'array',
'x-component': 'List',
'x-app-version': '0.21.0-alpha.10',
properties: {
item: {
type: 'object',
'x-component': 'List.Item',
'x-read-pretty': true,
'x-use-component-props': 'useListItemProps',
'x-app-version': '0.21.0-alpha.10',
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-app-version': '0.21.0-alpha.10',
properties: {
'48x3suuacem': {
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '0.21.0-alpha.10',
properties: {
z48z4iekhr0: {
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '0.21.0-alpha.10',
properties: {
id: {
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.id',
'x-component-props': {},
'x-read-pretty': true,
'x-app-version': '0.21.0-alpha.10',
},
},
},
},
},
afcj7ty3jkw: {
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '0.21.0-alpha.10',
properties: {
phs2rix7vvp: {
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '0.21.0-alpha.10',
properties: {
nickname: {
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.nickname',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
},
},
},
},
},
},
},
actionBar: {
type: 'void',
'x-align': 'left',
'x-component': 'ActionBar',
'x-use-component-props': 'useListActionBarProps',
'x-component-props': {
layout: 'one-column',
},
'x-app-version': '0.21.0-alpha.10',
properties: {
'7se3eremb6i': {
type: 'void',
title: '{{ t("View") }}',
'x-action': 'view',
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
},
'x-align': 'left',
'x-app-version': '0.21.0-alpha.10',
properties: {
drawer: {
type: 'void',
title: '{{ t("View record") }}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
'x-app-version': '0.21.0-alpha.10',
properties: {
tabs: {
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
properties: {
tab1: {
type: 'void',
title: '{{t("Details")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-app-version': '0.21.0-alpha.10',
},
},
},
},
},
},
},
},
},
og4zgn4noul: {
type: 'void',
title: '{{ t("Edit") }}',
'x-action': 'update',
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
icon: 'EditOutlined',
},
'x-align': 'left',
'x-app-version': '0.21.0-alpha.10',
properties: {
drawer: {
type: 'void',
title: '{{ t("Edit record") }}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
'x-app-version': '0.21.0-alpha.10',
properties: {
tabs: {
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
properties: {
tab1: {
type: 'void',
title: '{{t("Edit")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-app-version': '0.21.0-alpha.10',
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
appOptions: {
plugins: [BlockSchemaComponentPlugin],
scopes: {},
},
});
await waitFor(() => {
expect(screen.queryByText('ID')).toBeInTheDocument();
expect(screen.queryByText('1')).toBeInTheDocument();
});
expect(screen.queryByText('Nickname')).toBeInTheDocument();
expect(screen.queryByText('Super Admin')).toBeInTheDocument();
expect(screen.queryByText('View')).toBeInTheDocument();
expect(screen.queryByText('Edit')).toBeInTheDocument();
await userEvent.click(screen.getByText('View'));
await waitFor(() => {
expect(screen.queryByRole('tab')).toHaveTextContent('Details');
});
}); });
}); });

View File

@ -1,5 +0,0 @@
import React from 'react';
export default () => {
return <div>TODO</div>;
};

View File

@ -6,5 +6,3 @@ group:
# List # List
## Example ## Example
<code src="./demos/demo1.tsx"></code>

View File

@ -1,3 +1,6 @@
/* istanbul ignore file -- @preserve */
// 因为这里有 commonjs在 vitest 下会报错,所以忽略这个文件
import MarkdownIt from 'markdown-it'; import MarkdownIt from 'markdown-it';
import Mermaid from 'mermaid'; import Mermaid from 'mermaid';

View File

@ -0,0 +1,122 @@
import { BlockSchemaComponentPlugin } from '@nocobase/client';
import { screen, renderApp, renderReadPrettyApp, userEvent, waitFor } from '@nocobase/test/client';
describe('NanoIDInput', () => {
test('basic', async () => {
await renderApp({
designable: true,
enableUserListDataBlock: true,
schema: {
version: '2.0',
type: 'void',
'x-decorator': 'FormBlockProvider',
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: 'interfaces',
},
'x-component': 'div',
'x-app-version': '0.21.0-alpha.10',
properties: {
'28rbti2f9jx': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useCreateFormBlockProps',
'x-app-version': '0.21.0-alpha.10',
properties: {
'nano-iD': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'interfaces.nano-iD',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
},
},
},
},
},
appOptions: {
plugins: [BlockSchemaComponentPlugin],
},
});
await waitFor(() => {
const input: any = screen.queryByRole('textbox');
expect(input).toBeInTheDocument();
const value = input.value;
expect(value).toHaveLength(21);
});
await userEvent.clear(screen.getByRole('textbox'));
await userEvent.type(screen.getByRole('textbox'), '123');
await waitFor(() => {
expect(screen.queryByText('Field value size is 21')).toBeInTheDocument();
});
await userEvent.clear(screen.queryByRole('textbox'));
await userEvent.type(screen.queryByRole('textbox'), 'rdQ1G9iPEtjR6BpIAPilZ');
await waitFor(() => {
expect(screen.queryByText('Field value size is 21')).not.toBeInTheDocument();
});
});
test('read pretty', async () => {
await renderReadPrettyApp({
designable: true,
enableUserListDataBlock: true,
schema: {
version: '2.0',
type: 'void',
'x-acl-action-props': {
skipScopeCheck: true,
},
'x-decorator': 'FormBlockProvider',
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: 'interfaces',
},
'x-component': 'div',
'x-app-version': '0.21.0-alpha.10',
properties: {
'28rbti2f9jx': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useCreateFormBlockProps',
'x-app-version': '0.21.0-alpha.10',
properties: {
'nano-iD': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
default: 'rdQ1G9iPEtjR6BpIAPilZ',
'x-read-pretty': true,
'x-collection-field': 'interfaces.nano-iD',
'x-component-props': {},
'x-app-version': '0.21.0-alpha.10',
},
},
},
},
},
appOptions: {
plugins: [BlockSchemaComponentPlugin],
},
});
expect(document.querySelector('.ant-description-input')?.textContent).toBe('rdQ1G9iPEtjR6BpIAPilZ');
});
});

View File

@ -57,7 +57,7 @@ export const FixedBlockWrapper: React.FC = (props) => {
); );
}; };
interface FixedBlockProps { export interface FixedBlockProps {
height: number | string; height: number | string;
} }
@ -81,7 +81,7 @@ const fixedBlockCss = css`
} }
`; `;
const FixedBlock: React.FC<FixedBlockProps> = (props) => { export const FixedBlock: React.FC<FixedBlockProps> = (props) => {
const { height } = props; const { height } = props;
const [fixedBlockUID, _setFixedBlock] = useState<false | string>(false); const [fixedBlockUID, _setFixedBlock] = useState<false | string>(false);
const fixedBlockUIDRef = useRef(fixedBlockUID); const fixedBlockUIDRef = useRef(fixedBlockUID);

View File

@ -0,0 +1,101 @@
import { screen, checkSettings, renderSettings, checkModal } from '@nocobase/test/client';
import { Page } from '../Page';
import { pageSettings } from '../Page.Settings';
describe('Page.Settings', () => {
it('should works', async () => {
const title = 'title test';
await renderSettings({
schema: {
title,
'x-component': Page,
},
schemaSettings: pageSettings,
appOptions: {
designable: true,
},
apis: {
'/uiSchemas:insertAdjacent/test?position=beforeEnd': { data: { result: 'ok' } },
},
});
await checkSettings([
{
title: 'Enable page header',
type: 'switch',
beforeClick() {
expect(screen.getByTitle(title)).toBeInTheDocument();
},
afterFirstClick() {
expect(screen.queryByText(title)).not.toBeInTheDocument();
expect(screen.queryByText('Display page title')).not.toBeInTheDocument();
expect(screen.queryByText('Edit page title')).not.toBeInTheDocument();
expect(screen.queryByText('Enable page tabs')).not.toBeInTheDocument();
},
afterSecondClick() {
expect(screen.getByTitle(title)).toBeInTheDocument();
expect(screen.getByText('Display page title')).toBeInTheDocument();
expect(screen.getByText('Edit page title')).toBeInTheDocument();
expect(screen.getByText('Enable page tabs')).toBeInTheDocument();
},
},
{
title: 'Display page title',
type: 'switch',
beforeClick() {
expect(screen.getByTitle(title)).toBeInTheDocument();
},
afterFirstClick() {
expect(screen.queryByText(title)).not.toBeInTheDocument();
},
afterSecondClick() {
expect(screen.getByTitle(title)).toBeInTheDocument();
},
},
{
title: 'Edit page title',
type: 'modal',
modalChecker: {
modalTitle: 'Edit page title',
formItems: [
{
type: 'input',
label: 'Title',
newValue: 'new title',
},
],
afterSubmit() {
expect(screen.queryByTitle('new title')).toBeInTheDocument();
},
},
},
{
title: 'Enable page tabs',
type: 'switch',
beforeClick() {
expect(screen.queryByText('Add tab')).not.toBeInTheDocument();
},
async afterFirstClick() {
await checkModal({
triggerText: 'Add tab',
modalTitle: 'Add tab',
formItems: [
{
label: 'Tab name',
type: 'input',
newValue: 'Tab 1',
},
],
afterSubmit() {
expect(screen.queryByRole('tab')).toBeInTheDocument();
expect(screen.getByRole('tab')).toHaveTextContent('Tab 1');
},
});
},
afterSecondClick() {
expect(screen.queryByTitle('Add tab')).not.toBeInTheDocument();
},
},
]);
});
});

View File

@ -0,0 +1,54 @@
import { screen, checkSettings, renderSettings } from '@nocobase/test/client';
import { Page } from '../Page';
import { pageTabSettings } from '../PageTab.Settings';
describe('PageTab.Settings', () => {
test('should works', async () => {
await renderSettings({
container: () => screen.getByRole('tab'),
schema: {
title: 'page title',
'x-component': Page,
'x-component-props': {
enablePageTabs: true,
},
properties: {
tab1: {
'x-component': 'div',
title: 'tab1 title',
},
},
},
schemaSettings: pageTabSettings,
});
await checkSettings([
{
title: 'Edit',
type: 'modal',
modalChecker: {
modalTitle: 'Edit tab',
formItems: [
{
type: 'input',
label: 'Tab name',
oldValue: 'tab1 title',
newValue: 'new tab1 title',
},
],
afterSubmit: () => {
expect(screen.queryByText('new tab1 title')).toBeInTheDocument();
},
},
},
{
title: 'Delete',
type: 'delete',
modalChecker: {
confirmTitle: 'Delete block',
},
deletedText: 'new tab1 title',
},
]);
});
});

View File

@ -94,6 +94,9 @@ describe('Page', () => {
}, },
}, },
}, },
apis: {
'/uiSchemas:insertAdjacent/test': { data: { result: 'ok' } },
},
}); });
expect(screen.getByRole('tablist')).toBeInTheDocument(); expect(screen.getByRole('tablist')).toBeInTheDocument();
@ -126,6 +129,9 @@ describe('Page', () => {
Grid, Grid,
}, },
}, },
apis: {
'/uiSchemas:insertAdjacent/test?position=beforeEnd': { data: { result: 'ok' } },
},
}); });
await userEvent.click(screen.getByText('Add tab')); await userEvent.click(screen.getByText('Add tab'));

View File

@ -0,0 +1,153 @@
import { BlockSchemaComponentPlugin } from '@nocobase/client';
import { screen, renderApp, sleep, renderReadPrettyApp, userEvent, waitFor } from '@nocobase/test/client';
describe('QuickEdit', () => {
function getRenderOptions(readPretty = false) {
return {
designable: true,
enableUserListDataBlock: true,
schema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'FormBlockProvider',
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: 'users',
},
'x-component': 'div',
'x-app-version': '0.21.0-alpha.10',
properties: {
'45i9guirvtz': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useCreateFormBlockProps',
'x-app-version': '0.21.0-alpha.10',
properties: {
roles: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.roles',
'x-component-props': {
fieldNames: {
label: 'name',
value: 'name',
},
addMode: 'modalAdd',
mode: 'SubTable',
},
'x-app-version': '0.21.0-alpha.10',
default: null,
properties: {
e2l1f5wo2st: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'AssociationField.SubTable',
'x-app-version': '0.21.0-alpha.10',
properties: {
'9x9jysv3hka': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
'x-app-version': '0.21.0-alpha.10',
properties: {
'long-text': {
_isJSONSchemaObject: true,
version: '2.0',
default: readPretty ? 'aaa' : null,
'x-collection-field': 'roles.long-text',
'x-component': 'CollectionField',
'x-component-props': {
ellipsis: true,
},
'x-decorator': 'QuickEdit',
'x-decorator-props': {
labelStyle: {
display: 'none',
},
},
'x-app-version': '0.21.0-alpha.10',
'x-read-pretty': readPretty,
'x-disabled': false,
},
},
},
},
},
uwe6lq47y0t: {
_isJSONSchemaObject: true,
version: '2.0',
'x-action': 'create',
title: "{{t('Add new')}}",
'x-component': 'Action',
'x-component-props': {
openMode: 'drawer',
type: 'default',
component: 'CreateRecordAction',
},
type: 'void',
'x-app-version': '0.21.0-alpha.10',
},
},
},
},
},
},
},
appOptions: {
plugins: [BlockSchemaComponentPlugin],
},
};
}
it('basic', async () => {
await renderApp(getRenderOptions());
await waitFor(() => {
expect(document.querySelector('.ant-table-footer button')).toBeInTheDocument();
});
await userEvent.click(document.querySelector('.ant-table-footer button'));
await waitFor(() => {
expect(document.querySelector('.ant-table-row')).toBeInTheDocument();
});
await userEvent.click(document.querySelector('.ant-description-textarea'));
await waitFor(() => {
expect(screen.queryByRole('textbox')).toBeInTheDocument();
});
await userEvent.type(screen.queryByRole('textbox'), 'hello world');
await waitFor(() => {
expect(document.querySelector('.ant-description-textarea')).toHaveTextContent('hello world');
});
});
it('read pretty', async () => {
await renderApp(getRenderOptions(true));
await waitFor(() => {
expect(document.querySelector('.ant-table-footer button')).toBeInTheDocument();
});
await userEvent.click(document.querySelector('.ant-table-footer button'));
await waitFor(() => {
expect(document.querySelector('.ant-table-row')).toBeInTheDocument();
expect(document.querySelector('.ant-description-textarea')).toHaveTextContent('aaa');
});
await userEvent.click(document.querySelector('.ant-description-textarea'));
await sleep(100);
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
});
});

View File

@ -4,7 +4,7 @@ group:
order: 3 order: 3
--- ---
# RecordPicker <Badge>待定</Badge> # RecordPicker
## JSON Schema ## JSON Schema

View File

@ -1,7 +1,7 @@
import { APIClientProvider, FormProvider, RemoteSelect, SchemaComponent } from '@nocobase/client'; import { APIClientProvider, FormProvider, RemoteSelect, SchemaComponent } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { mockAPIClient } from '../../../../testUtils'; import { mockAPIClient } from '../../../../testUtils';
import { sleep } from '@nocobase/test/client'; import { sleep } from '@nocobase/test/web';
const { apiClient, mockRequest } = mockAPIClient(); const { apiClient, mockRequest } = mockAPIClient();
mockRequest.onGet('/posts:list').reply(async () => { mockRequest.onGet('/posts:list').reply(async () => {

View File

@ -0,0 +1,308 @@
import { BlockSchemaComponentPlugin, TableV2, useTableBlockDecoratorProps } from '@nocobase/client';
import { checkSettings, renderSettings, checkSchema, screen, waitFor } from '@nocobase/test/client';
import { withSchema } from '@nocobase/test/web';
describe('Table.Column.settings', () => {
const TableColumnDecoratorWithSchema = withSchema(TableV2.Column.Decorator);
const getRenderOptions = (isOld?: boolean, field = 'nickname') => {
const schema = isOld
? {
'x-designer': 'TableV2.Column.Designer',
}
: {
'x-toolbar': 'TableColumnSchemaToolbar',
'x-settings': 'fieldSettings:TableColumn',
};
return {
designable: true,
enableUserListDataBlock: true,
schema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TableBlockProvider',
'x-use-decorator-props': 'useTableBlockDecoratorProps',
'x-decorator-props': {
collection: 'users',
dataSource: 'main',
action: 'list',
params: {
pageSize: 20,
},
rowKey: 'id',
showIndex: true,
dragSort: false,
},
'x-component': 'div',
'x-app-version': '0.21.0-alpha.10',
properties: {
f8bvd77sp6p: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'array',
'x-component': 'TableV2',
'x-use-component-props': 'useTableBlockProps',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
},
'x-app-version': '0.21.0-alpha.10',
properties: {
ct00e0xr996: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TableColumnDecoratorWithSchema',
...schema,
'x-component': 'TableV2.Column',
'x-app-version': '0.21.0-alpha.10',
'x-component-props': {},
properties: {
[field]: {
_isJSONSchemaObject: true,
version: '2.0',
'x-collection-field': `users.${field}`,
'x-component': 'CollectionField',
'x-component-props': {
ellipsis: true,
},
'x-read-pretty': true,
'x-decorator': null,
'x-decorator-props': {
labelStyle: {
display: 'none',
},
},
'x-app-version': '0.21.0-alpha.10',
'x-async': false,
'x-index': 1,
},
},
'x-async': false,
'x-index': 3,
},
},
'x-async': false,
'x-index': 2,
},
},
'x-async': false,
'x-index': 1,
},
appOptions: {
components: {
TableColumnDecoratorWithSchema,
},
plugins: [BlockSchemaComponentPlugin],
scopes: {
useTableBlockDecoratorProps,
},
},
};
};
const checkCommonField = () => {
return checkSettings([
{
type: 'modal',
title: 'Custom column title',
modalChecker: {
modalTitle: 'Custom column title',
formItems: [
{
type: 'input',
label: 'Column title',
newValue: 'test',
},
],
async afterSubmit() {
await checkSchema({
title: 'test',
});
},
},
},
{
type: 'modal',
title: 'Column width',
modalChecker: {
modalTitle: 'Column width',
formItems: [
{
type: 'number',
newValue: '300',
},
],
async afterSubmit() {
await checkSchema({
'x-component-props': {
width: 300,
},
});
},
},
},
{
type: 'switch',
title: 'Sortable',
async afterFirstClick() {
await checkSchema({
'x-component-props': {
sorter: true,
},
});
},
async afterSecondClick() {
await checkSchema({
'x-component-props': {
sorter: false,
},
});
},
},
]);
};
const checkAssociationField = () => {
return checkSettings([
{
type: 'modal',
title: 'Custom column title',
modalChecker: {
modalTitle: 'Custom column title',
formItems: [
{
type: 'input',
label: 'Column title',
newValue: 'test',
},
],
async afterSubmit() {
await checkSchema({
title: 'test',
});
},
},
},
{
type: 'modal',
title: 'Column width',
modalChecker: {
modalTitle: 'Column width',
formItems: [
{
type: 'number',
newValue: '300',
},
],
async afterSubmit() {
await checkSchema({
'x-component-props': {
width: 300,
},
});
},
},
},
{
type: 'switch',
title: 'Enable link',
async afterFirstClick() {
expect(screen.queryByText('Admin').tagName).toBe('SPAN');
},
async afterSecondClick() {
expect(screen.queryByText('Admin').tagName).toBe('A');
},
},
{
type: 'select',
title: 'Field component',
oldValue: 'Title',
options: [
{
label: 'Tag',
async checker() {
await waitFor(() => {
expect(screen.queryByText('Admin')).toHaveClass('ant-tag');
});
},
},
{
label: 'Title',
async checker() {
await waitFor(() => {
const el = screen.queryByText('Admin');
expect(el?.tagName).toBe('A');
});
},
},
{
label: 'Tag',
async checker() {},
},
],
},
{
type: 'select',
title: 'Tag color field',
options: [
{
label: 'color',
async checker() {
await waitFor(() => {
expect(screen.queryByText('Admin')).toHaveStyle('background-color: rgb(22, 119, 255);');
});
},
},
],
},
{
type: 'select',
title: 'Title field',
options: [
{
label: 'Role UID',
async checker() {
expect(screen.queryByText('admin')).toBeInTheDocument();
},
},
{
label: 'Role name',
async checker() {
await waitFor(() => {
expect(screen.queryByText('Admin')).toBeInTheDocument();
});
},
},
],
},
]);
};
describe('new version schema', () => {
test('common field', async () => {
await renderSettings(getRenderOptions());
await checkCommonField();
});
test('association field', async () => {
await renderSettings(getRenderOptions(false, 'roles'));
await checkAssociationField();
});
});
describe('old version schema', () => {
test('common field', async () => {
await renderSettings(getRenderOptions(true));
await checkCommonField();
});
test('association field', async () => {
await renderSettings(getRenderOptions(true, 'roles'));
await checkAssociationField();
});
});
});

View File

@ -0,0 +1,307 @@
import {
BlockSchemaComponentPlugin,
FixedBlock,
TableBlockProvider,
useTableBlockDecoratorProps,
} from '@nocobase/client';
import {
checkSettings,
renderSettings,
checkSchema,
screen,
userEvent,
waitFor,
CheckSettingsOptions,
} from '@nocobase/test/client';
import { withSchema } from '@nocobase/test/web';
describe('Table.settings', () => {
const TableBlockProviderWithSchema = withSchema(TableBlockProvider);
const checkTableSettings = (more: CheckSettingsOptions[] = []) => {
return checkSettings(
[
{
title: 'Edit block title',
type: 'modal',
},
{
title: 'Enable drag and drop sorting',
type: 'switch',
async afterFirstClick() {
await checkSchema({
'x-decorator-props': {
dragSort: true,
},
});
expect(screen.queryByText('Drag and drop sorting field')).toBeInTheDocument();
await checkSettings([
{
title: 'Drag and drop sorting field',
type: 'select',
options: [
{
label: 'sort',
async checker() {
await checkSchema({
'x-decorator-props': {
dragSortBy: 'sortName',
},
});
},
},
],
},
]);
},
async afterSecondClick() {
await checkSchema({
'x-decorator-props': {
dragSort: false,
},
});
expect(screen.queryByText('Drag and drop sorting field')).not.toBeInTheDocument();
},
},
{
title: 'Fix block',
type: 'switch',
async afterFirstClick() {
await checkSchema({
'x-decorator-props': {
fixedBlock: true,
},
});
},
async afterSecondClick() {
await checkSchema({
'x-decorator-props': {
fixedBlock: false,
},
});
},
},
{
title: 'Set the data scope',
type: 'modal',
modalChecker: {
modalTitle: 'Set the data scope',
async beforeCheck() {
await userEvent.click(screen.getByText('Add condition'));
await waitFor(() => {
expect(screen.queryByTestId('select-filter-field')).toBeInTheDocument();
});
const field = screen.queryByTestId('select-filter-field').querySelector('input');
await userEvent.click(field);
await waitFor(() => {
expect(screen.queryByTitle('ID')).toBeInTheDocument();
});
await userEvent.click(screen.getByTitle('ID'));
const value = document.querySelector('input[role=spinbutton]');
await userEvent.type(value, '1');
await waitFor(() => {
expect(document.querySelector('input[role=spinbutton]')).toHaveValue('1');
});
},
async afterSubmit() {
await checkSchema({
'x-decorator-props': {
params: {
filter: {
$and: [
{
id: {
$eq: 1,
},
},
],
},
},
},
});
},
},
},
{
title: 'Set default sorting rules',
type: 'modal',
modalChecker: {
modalTitle: 'Set default sorting rules',
contentText: 'Add sort field',
async beforeCheck() {
await userEvent.click(screen.getByText('Add sort field'));
const dialog = screen.getByRole('dialog');
await waitFor(() => {
expect(dialog.querySelector('.ant-select-selector')).toBeInTheDocument();
});
await userEvent.click(dialog.querySelector('.ant-select-selector'));
await waitFor(() => {
expect(screen.queryByText('ID')).toBeInTheDocument();
});
await userEvent.click(screen.getByText('ID'));
await userEvent.click(screen.getByText('DESC'));
},
async afterSubmit() {
await checkSchema({
'x-decorator-props': {
params: {
sort: ['-id'],
},
},
});
},
},
},
{
title: 'Set data loading mode',
type: 'modal',
modalChecker: {
modalTitle: 'Data loading mode',
async beforeCheck() {
await userEvent.click(screen.getByText('Load data after filtering'));
},
async afterSubmit() {
await checkSchema({
'x-decorator-props': {
dataLoadingMode: 'manual',
},
});
},
},
},
{
title: 'Records per page',
type: 'select',
options: [
{
label: '10',
async checker() {
await checkSchema({
'x-decorator-props': {
params: {
pageSize: 10,
},
},
});
},
},
{
label: '20',
},
{
label: '50',
},
{
label: '100',
},
{
label: '100',
},
],
},
{
title: 'Save as template',
type: 'modal',
},
{
title: 'Delete',
type: 'delete',
},
...more,
],
true,
);
};
const getRenderSettingsOptions = (isOld?: boolean, collection = 'users') => {
const toolbarSchema = isOld
? {
'x-designer': 'TableBlockDesigner',
}
: {
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:table',
};
return {
designable: true,
enableUserListDataBlock: true,
schema: {
type: 'void',
'x-component': 'FixedBlock',
properties: {
table: {
type: 'void',
'x-decorator': 'TableBlockProviderWithSchema',
'x-use-decorator-props': 'useTableBlockDecoratorProps',
'x-decorator-props': {
collection: collection,
dataSource: 'main',
action: 'list',
rowKey: 'id',
showIndex: true,
dragSort: false,
params: {
pageSize: 20,
},
},
...toolbarSchema,
'x-component': 'CardItem',
'x-index': 1,
},
},
},
appOptions: {
components: {
TableBlockProviderWithSchema,
FixedBlock,
},
plugins: [BlockSchemaComponentPlugin],
scopes: {
useTableBlockDecoratorProps,
},
},
};
};
test('menu list', async () => {
await renderSettings(getRenderSettingsOptions());
await checkTableSettings();
});
test('old schema', async () => {
await renderSettings(getRenderSettingsOptions(true));
await checkTableSettings();
});
test('tree collection', async () => {
await renderSettings(getRenderSettingsOptions(false, 'tree'));
await checkSettings([
{
title: 'Tree table',
type: 'switch',
async afterFirstClick() {
await checkSchema({
'x-decorator-props': {
treeTable: true,
},
});
},
async afterSecondClick() {
await checkSchema({
'x-decorator-props': {
treeTable: false,
},
});
},
},
]);
});
});

View File

@ -0,0 +1,118 @@
import {
FixedBlock,
BlockSchemaComponentPlugin,
SchemaInitializerPlugin,
TableBlockProvider,
tableActionColumnInitializers,
tableActionInitializers,
tableColumnInitializers,
useTableBlockDecoratorProps,
} from '@nocobase/client';
export const tableOptions = {
designable: true,
enableUserListDataBlock: true,
schema: {
type: 'void',
'x-component': 'FixedBlock',
properties: {
table: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TableBlockProvider',
'x-use-decorator-props': 'useTableBlockDecoratorProps',
'x-decorator-props': {
collection: 'users',
dataSource: 'main',
action: 'list',
rowKey: 'id',
showIndex: true,
dragSort: false,
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:table',
'x-component': 'CardItem',
'x-app-version': '0.21.0-alpha.11',
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'table:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
'x-app-version': '0.21.0-alpha.11',
'x-async': false,
'x-index': 1,
},
qmew562ea9w: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'array',
'x-initializer': 'table:configureColumns',
'x-component': 'TableV2',
'x-use-component-props': 'useTableBlockProps',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
},
'x-app-version': '0.21.0-alpha.11',
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("Actions") }}',
'x-action-column': 'actions',
'x-decorator': 'TableV2.Column.ActionBar',
'x-component': 'TableV2.Column',
'x-designer': 'TableV2.ActionColumnDesigner',
'x-initializer': 'table:configureItemActions',
'x-app-version': '0.21.0-alpha.11',
properties: {
glmtz7t8dm4: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
'x-app-version': '0.21.0-alpha.11',
'x-async': false,
'x-index': 1,
},
},
'x-async': false,
'x-index': 1,
},
},
'x-async': false,
'x-index': 2,
},
},
'x-async': false,
'x-index': 1,
},
},
},
appOptions: {
components: {
TableBlockProvider,
FixedBlock,
},
plugins: [BlockSchemaComponentPlugin, SchemaInitializerPlugin],
schemaInitializers: [tableActionInitializers, tableColumnInitializers, tableActionColumnInitializers],
scopes: {
useTableBlockDecoratorProps,
},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -3,6 +3,9 @@ import React from 'react';
import App1 from '../demos/demo1'; import App1 from '../demos/demo1';
import App2 from '../demos/demo2'; import App2 from '../demos/demo2';
import { BlockSchemaComponentPlugin } from '@nocobase/client';
import { screen, renderApp, renderReadPrettyApp, userEvent, waitFor } from '@nocobase/test/client';
describe('Upload', () => { describe('Upload', () => {
it('basic', () => { it('basic', () => {
render(<App1 />); render(<App1 />);
@ -11,4 +14,276 @@ describe('Upload', () => {
it('uploading', () => { it('uploading', () => {
render(<App2 />); render(<App2 />);
}); });
it('upload single', async () => {
await renderApp({
designable: true,
enableUserListDataBlock: true,
schema: {
version: '2.0',
type: 'void',
'x-acl-action-props': {
skipScopeCheck: true,
},
'x-decorator': 'FormBlockProvider',
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: 'interfaces',
},
'x-component': 'div',
'x-app-version': '0.21.0-alpha.10',
properties: {
'28rbti2f9jx': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useCreateFormBlockProps',
'x-app-version': '0.21.0-alpha.10',
properties: {
attachment: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-read-pretty': false,
'x-collection-field': 'interfaces.attachment',
'x-component-props': {
action: 'attachments:create',
},
'x-app-version': '0.21.0-alpha.10',
'x-disabled': false,
'x-async': false,
'x-index': 1,
},
},
},
},
},
appOptions: {
plugins: [BlockSchemaComponentPlugin],
},
apis: {
'attachments:create': {
data: {
id: 3,
title: '微信图片_20240131154451',
filename: '234ead512e44bf944689069ce2b41a95.png',
extname: '.png',
path: '',
size: 841380,
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
mimetype: 'image/png',
meta: {},
storageId: 1,
updatedAt: '2024-04-21T01:26:02.961Z',
createdAt: '2024-04-21T01:26:02.961Z',
createdById: 1,
updatedById: 1,
},
},
},
});
const file = new File(['hello'], './hello.png', { type: 'image/png' });
await waitFor(() => {
const input = document.querySelector('input[type="file"]');
expect(input).toBeInTheDocument();
});
await userEvent.click(document.querySelector('.ant-upload'));
await userEvent.upload(document.querySelector('input[type="file"]'), file);
await waitFor(() => {
expect(document.querySelector('.ant-upload-list-item-image')).toBeInTheDocument();
expect(document.querySelector('.ant-upload-list-item-image')).toHaveAttribute(
'src',
'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
);
});
// upload another file
await userEvent.click(document.querySelector('.ant-upload'));
await userEvent.upload(document.querySelector('input[type="file"]'), file);
await waitFor(() => {
expect(document.querySelectorAll('.ant-upload-list-item-image')).toHaveLength(2);
});
});
it('upload multi', async () => {
await renderApp({
designable: true,
enableUserListDataBlock: true,
schema: {
version: '2.0',
type: 'void',
'x-acl-action-props': {
skipScopeCheck: true,
},
'x-decorator': 'FormBlockProvider',
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: 'interfaces',
},
'x-component': 'div',
'x-app-version': '0.21.0-alpha.10',
properties: {
'28rbti2f9jx': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useCreateFormBlockProps',
'x-app-version': '0.21.0-alpha.10',
properties: {
attachment: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-read-pretty': false,
'x-collection-field': 'interfaces.attachment',
'x-component-props': {
action: 'attachments:create',
},
'x-app-version': '0.21.0-alpha.10',
'x-disabled': false,
'x-async': false,
'x-index': 1,
},
},
},
},
},
appOptions: {
plugins: [BlockSchemaComponentPlugin],
},
apis: {
'attachments:create': {
data: {
id: 3,
title: '微信图片_20240131154451',
filename: '234ead512e44bf944689069ce2b41a95.png',
extname: '.png',
path: '',
size: 841380,
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
mimetype: 'image/png',
meta: {},
storageId: 1,
updatedAt: '2024-04-21T01:26:02.961Z',
createdAt: '2024-04-21T01:26:02.961Z',
createdById: 1,
updatedById: 1,
},
},
},
});
const files = [
new File(['hello1'], './hello.png', { type: 'image/png' }),
new File(['hello2'], './hello.png', { type: 'image/png' }),
];
await waitFor(() => {
const input = document.querySelector('input[type="file"]');
expect(input).toBeInTheDocument();
});
await userEvent.click(document.querySelector('.ant-upload'));
await userEvent.upload(document.querySelector('input[type="file"]'), files);
await waitFor(() => {
expect(document.querySelectorAll('.ant-upload-list-item-image')).toHaveLength(2);
});
});
it('delete', async () => {
await renderApp({
designable: true,
enableUserListDataBlock: true,
schema: {
version: '2.0',
type: 'void',
'x-acl-action-props': {
skipScopeCheck: true,
},
'x-decorator': 'FormBlockProvider',
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: 'interfaces',
},
'x-component': 'div',
'x-app-version': '0.21.0-alpha.10',
properties: {
'28rbti2f9jx': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useCreateFormBlockProps',
'x-app-version': '0.21.0-alpha.10',
properties: {
attachment: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
default: {
id: 1,
title: '微信图片_20240131154451',
filename: '234ead512e44bf944689069ce2b41a95.png',
extname: '.png',
path: '',
size: 841380,
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
mimetype: 'image/png',
meta: {},
storageId: 1,
updatedAt: '2024-04-21T01:26:02.961Z',
createdAt: '2024-04-21T01:26:02.961Z',
createdById: 1,
updatedById: 1,
},
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-read-pretty': false,
'x-collection-field': 'interfaces.attachment',
'x-component-props': {
action: 'attachments:create',
},
'x-app-version': '0.21.0-alpha.10',
'x-disabled': false,
'x-async': false,
'x-index': 1,
},
},
},
},
},
appOptions: {
plugins: [BlockSchemaComponentPlugin],
},
});
await waitFor(() => {
expect(document.querySelector('.ant-upload-list-item-image')).toBeInTheDocument();
expect(document.querySelector('.ant-upload-list-item-image')).toHaveAttribute(
'src',
'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
);
});
await userEvent.click(document.querySelector('.anticon-delete'));
await waitFor(() => {
expect(document.querySelector('.ant-upload-list-item-image')).not.toBeInTheDocument();
});
});
}); });

View File

@ -4,4 +4,4 @@ group:
order: 3 order: 3
--- ---
# DndContext <Badge>待定</Badge> # DndContext

View File

@ -13,11 +13,13 @@ export const useGetAriaLabelOfSchemaInitializer = () => {
const getAriaLabel = useCallback( const getAriaLabel = useCallback(
(postfix?: string) => { (postfix?: string) => {
if (!fieldSchema) return ''; if (!fieldSchema) return '';
const component = fieldSchema['x-component'];
const componentStr = typeof component === 'string' ? component : component?.displayName || component.name;
const initializer = fieldSchema['x-initializer'] ? `-${fieldSchema['x-initializer']}` : ''; const initializer = fieldSchema['x-initializer'] ? `-${fieldSchema['x-initializer']}` : '';
const collectionName = name ? `-${name}` : ''; const collectionName = name ? `-${name}` : '';
postfix = postfix ? `-${postfix}` : ''; postfix = postfix ? `-${postfix}` : '';
return `schema-initializer-${fieldSchema['x-component']}${initializer}${collectionName}${postfix}`; return `schema-initializer-${componentStr}${initializer}${collectionName}${postfix}`;
}, },
[fieldSchema, name], [fieldSchema, name],
); );

View File

@ -54,34 +54,30 @@ import {
createDesignable, createDesignable,
findFormBlock, findFormBlock,
useAPIClient, useAPIClient,
useBlockRequestContext,
useCollectionManager_deprecated, useCollectionManager_deprecated,
useCollectionRecord, useCollectionRecord,
useCollection_deprecated, useCollection_deprecated,
useCompile, useCompile,
useDataBlockProps, useDataBlockProps,
useDesignable, useDesignable,
useFilterBlock,
useGlobalTheme, useGlobalTheme,
useLinkageCollectionFilterOptions, useLinkageCollectionFilterOptions,
useRecord, useRecord,
useSortFields, useSortFields,
} from '..'; } from '..';
import { import { FormBlockContext, useFormBlockContext, useFormBlockType, useTableBlockContext } from '../block-provider';
BlockContext,
BlockRequestContext_deprecated,
FormBlockContext,
useBlockContext,
useFormBlockContext,
useFormBlockType,
useTableBlockContext,
} from '../block-provider';
import { import {
FormActiveFieldsProvider, FormActiveFieldsProvider,
findFilterTargets, findFilterTargets,
updateFilterTargets, updateFilterTargets,
useFormActiveFields, useFormActiveFields,
} from '../block-provider/hooks'; } from '../block-provider/hooks';
import {
useBlockRequestContext,
BlockRequestContext_deprecated,
useBlockContext,
BlockContext,
} from '../block-provider/BlockProvider';
import { SelectWithTitle, SelectWithTitleProps } from '../common/SelectWithTitle'; import { SelectWithTitle, SelectWithTitleProps } from '../common/SelectWithTitle';
import { useNiceDropdownMaxHeight } from '../common/useNiceDropdownHeight'; import { useNiceDropdownMaxHeight } from '../common/useNiceDropdownHeight';
import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider'; import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider';
@ -93,6 +89,7 @@ import {
isSameCollection, isSameCollection,
useSupportedBlocks, useSupportedBlocks,
} from '../filter-provider/utils'; } from '../filter-provider/utils';
import { useFilterBlock } from '../filter-provider/FilterProvider';
import { FlagProvider } from '../flag-provider'; import { FlagProvider } from '../flag-provider';
import { useCollectMenuItem, useCollectMenuItems, useMenuItem } from '../hooks/useMenuItem'; import { useCollectMenuItem, useCollectMenuItems, useMenuItem } from '../hooks/useMenuItem';
import { DeclareVariable } from '../modules/variable/DeclareVariable'; import { DeclareVariable } from '../modules/variable/DeclareVariable';
@ -187,7 +184,13 @@ export const SchemaSettingsDropdown: React.FC<SchemaSettingsProps> = (props) =>
overflow-y: auto; overflow-y: auto;
} }
`} `}
menu={{ items, style: { maxHeight: dropdownMaxHeight, overflowY: 'auto' } }} menu={
{
items,
'data-testid': 'schema-settings-menu',
style: { maxHeight: dropdownMaxHeight, overflowY: 'auto' },
} as any
}
> >
<div data-testid={props['data-testid']}>{typeof title === 'string' ? <span>{title}</span> : title}</div> <div data-testid={props['data-testid']}>{typeof title === 'string' ? <span>{title}</span> : title}</div>
</Dropdown> </Dropdown>

View File

@ -14,13 +14,14 @@ export const useGetAriaLabelOfDesigner = () => {
if (!fieldSchema) return ''; if (!fieldSchema) return '';
const component = fieldSchema['x-component']; const component = fieldSchema['x-component'];
const componentName = typeof component === 'string' ? component : component?.displayName || component?.name;
const designer = fieldSchema['x-designer'] ? `-${fieldSchema['x-designer']}` : ''; const designer = fieldSchema['x-designer'] ? `-${fieldSchema['x-designer']}` : '';
const settings = fieldSchema['x-settings'] ? `-${fieldSchema['x-settings']}` : ''; const settings = fieldSchema['x-settings'] ? `-${fieldSchema['x-settings']}` : '';
const collectionField = fieldSchema['x-collection-field'] ? `-${fieldSchema['x-collection-field']}` : ''; const collectionField = fieldSchema['x-collection-field'] ? `-${fieldSchema['x-collection-field']}` : '';
const collectionName = _collectionName ? `-${_collectionName}` : ''; const collectionName = _collectionName ? `-${_collectionName}` : '';
postfix = postfix ? `-${postfix}` : ''; postfix = postfix ? `-${postfix}` : '';
return `designer-${name}-${component}${designer}${settings}${collectionName}${collectionField}${postfix}`; return `designer-${name}-${componentName}${designer}${settings}${collectionName}${collectionField}${postfix}`;
}, },
[fieldSchema, _collectionName], [fieldSchema, _collectionName],
); );

View File

@ -0,0 +1,29 @@
import { waitFor, screen } from '@testing-library/react';
import { checkSettings } from '../settingsChecker';
import { expectNoTsError } from '../utils';
export async function checkBlockTitle(oldValue?: string) {
const newValue = 'new test';
await checkSettings([
{
type: 'modal',
title: 'Edit block title',
modalChecker: {
modalTitle: 'Edit block title',
formItems: [
{
type: 'input',
label: 'Block title',
oldValue,
newValue,
},
],
async afterSubmit() {
await waitFor(() => {
expectNoTsError(screen.queryByText(newValue)).toBeInTheDocument();
});
},
},
},
]);
}

View File

@ -0,0 +1,29 @@
import { waitFor, screen } from '@testing-library/react';
import { checkSettings } from '../settingsChecker';
import { expectNoTsError } from '../utils';
export async function checkFieldTitle(oldValue?: string) {
const newValue = 'new test';
await checkSettings([
{
type: 'modal',
title: 'Edit field title',
modalChecker: {
modalTitle: 'Edit field title',
formItems: [
{
type: 'input',
label: 'Field title',
oldValue,
newValue,
},
],
async afterSubmit() {
await waitFor(() => {
expectNoTsError(screen.queryByText(newValue)).toBeInTheDocument();
});
},
},
},
]);
}

View File

@ -0,0 +1,2 @@
export * from './blockTitle';
export * from './fieldTitle';

View File

@ -0,0 +1,23 @@
import userEvent from '@testing-library/user-event';
import { CommonFormItemCheckerOptions, getFormItemElement } from './common';
import { expectNoTsError } from '../utils';
export interface CollectionFieldCheckOptions extends CommonFormItemCheckerOptions {
field: string;
}
export async function collectionFieldChecker(options: CollectionFieldCheckOptions) {
const formItem = getFormItemElement({ Component: 'CollectionField', label: options.field, ...options });
const input = formItem.querySelector('input');
if (options.oldValue) {
expectNoTsError(input).toHaveValue(options.oldValue);
}
if (options.newValue) {
await userEvent.clear(input);
await userEvent.type(input, options.newValue);
}
}

View File

@ -0,0 +1,24 @@
import { expectNoTsError } from '../utils';
export interface CommonFormItemCheckerOptions {
label?: string;
container?: HTMLElement;
newValue?: any;
oldValue?: any;
Component?: string;
}
export interface GetFormItemElementOptions {
container?: HTMLElement;
Component: string;
label?: string;
}
export function getFormItemElement({ container = document.body, Component, label }: GetFormItemElementOptions) {
const preSelector = `div[aria-label^="block-item-${Component}-"]`;
const selector = label ? `${preSelector}[aria-label$="${label}"]` : preSelector;
const formItem = container.querySelector(selector);
expectNoTsError(formItem).toBeInTheDocument();
return formItem;
}

View File

@ -0,0 +1,32 @@
import userEvent from '@testing-library/user-event';
import { CommonFormItemCheckerOptions, getFormItemElement } from './common';
import { waitFor, screen } from '@testing-library/react';
import { expectNoTsError } from '../utils';
export type IconCheckOptions = CommonFormItemCheckerOptions;
export async function iconChecker(options: IconCheckOptions) {
const formItem = getFormItemElement({ Component: 'IconPicker', ...options });
if (options.oldValue) {
expectNoTsError(formItem.querySelector('span[role=img]')).toHaveAttribute('aria-label', options.oldValue);
}
if (options.newValue) {
await userEvent.click(formItem.querySelector('span[role=img]') || formItem.querySelector('button'));
await waitFor(() => {
expectNoTsError(screen.queryByRole('tooltip')).toBeInTheDocument();
expectNoTsError(screen.getByRole('tooltip').querySelector('.ant-popover-title')).toHaveTextContent('Icon');
});
await userEvent.click(screen.getByRole('tooltip').querySelector(`span[aria-label="${options.newValue}"]`));
await waitFor(() => {
expectNoTsError(screen.queryByRole('tooltip')).not.toBeInTheDocument();
});
expectNoTsError(formItem.querySelector('span[role=img]')).toHaveAttribute('aria-label', options.newValue);
}
}

View File

@ -0,0 +1,35 @@
import { iconChecker, IconCheckOptions } from './icon';
import { radioChecker, RadioCheckOptions } from './radio';
import { inputChecker, InputCheckOptions } from './input';
import { numberChecker, NumberCheckOptions } from './number';
import { textareaChecker, TextareaCheckOptions } from './textarea';
import { CollectionFieldCheckOptions, collectionFieldChecker } from './collectionField';
export * from './icon';
export * from './radio';
export * from './icon';
export type FormItemCheckOptions =
| ({ type: 'icon' } & IconCheckOptions)
| ({ type: 'radio' } & RadioCheckOptions)
| ({ type: 'collectionField' } & CollectionFieldCheckOptions)
| ({ type: 'input' } & InputCheckOptions)
| ({ type: 'number' } & NumberCheckOptions)
| ({ type: 'textarea' } & TextareaCheckOptions);
const checkers = {
icon: iconChecker,
radio: radioChecker,
input: inputChecker,
collectionField: collectionFieldChecker,
number: numberChecker,
textarea: textareaChecker,
};
export async function checkFormItems(list: FormItemCheckOptions[]) {
for (const item of list) {
const type = item.type;
const checker = checkers[type];
await checker(item as any);
}
}

View File

@ -0,0 +1,21 @@
import userEvent from '@testing-library/user-event';
import { CommonFormItemCheckerOptions, getFormItemElement } from './common';
import { expectNoTsError } from '../utils';
export type InputCheckOptions = CommonFormItemCheckerOptions;
export async function inputChecker(options: InputCheckOptions) {
const formItem = getFormItemElement({ Component: 'Input', ...options });
const input = formItem.querySelector('input');
if (options.oldValue) {
expectNoTsError(input).toHaveValue(options.oldValue);
}
if (options.newValue) {
await userEvent.clear(input);
await userEvent.type(input, options.newValue);
}
}

View File

@ -0,0 +1,21 @@
import userEvent from '@testing-library/user-event';
import { CommonFormItemCheckerOptions, getFormItemElement } from './common';
import { expectNoTsError } from '../utils';
export type NumberCheckOptions = CommonFormItemCheckerOptions;
export async function numberChecker(options: NumberCheckOptions) {
const formItem = getFormItemElement({ Component: 'InputNumber', ...options });
const input = formItem.querySelector('input');
if (options.oldValue) {
expectNoTsError(input).toHaveValue(options.oldValue);
}
if (options.newValue) {
await userEvent.clear(input);
await userEvent.type(input, String(options.newValue));
}
}

View File

@ -0,0 +1,29 @@
import userEvent from '@testing-library/user-event';
import { CommonFormItemCheckerOptions, getFormItemElement } from './common';
import { waitFor } from '@testing-library/react';
import { expectNoTsError } from '../utils';
export type RadioCheckOptions = CommonFormItemCheckerOptions;
export async function radioChecker(options: CommonFormItemCheckerOptions) {
const formItem = getFormItemElement({ Component: 'Radio.Group', ...options });
const radioGroup = formItem.querySelector('.ant-radio-group');
if (options.oldValue) {
expectNoTsError(radioGroup.querySelector('.ant-radio-wrapper-checked')).toHaveTextContent(options.oldValue);
}
if (options.newValue) {
const el = [...radioGroup.querySelectorAll('.ant-radio-wrapper')].find((el) => el.textContent === options.newValue);
expectNoTsError(el).toBeInTheDocument();
await userEvent.click(el);
await waitFor(() => {
expectNoTsError(radioGroup.querySelector('.ant-radio-wrapper-checked')).toHaveTextContent(options.newValue);
});
}
}

View File

@ -0,0 +1,21 @@
import userEvent from '@testing-library/user-event';
import { CommonFormItemCheckerOptions, getFormItemElement } from './common';
import { expectNoTsError } from '../utils';
export type TextareaCheckOptions = CommonFormItemCheckerOptions;
export async function textareaChecker(options: TextareaCheckOptions) {
const formItem = getFormItemElement({ Component: 'Input.TextArea', ...options });
const textarea = formItem.querySelector('textarea');
if (options.oldValue) {
expectNoTsError(textarea).toHaveValue(options.oldValue);
}
if (options.newValue) {
await userEvent.clear(textarea);
await userEvent.type(textarea, options.newValue);
}
}

View File

@ -1,9 +1,8 @@
import { expect } from 'vitest'; import React from 'react';
import React, { FC, Fragment } from 'react'; import { render } from '@testing-library/react';
import { render, waitFor, screen } from '@testing-library/react'; import { sleep } from '../web';
import { renderHook } from '@testing-library/react-hooks';
import { GetAppComponentOptions, GetAppOptions, getApp, getAppComponent } from '../web';
export * from './utils';
export { renderHook } from '@testing-library/react-hooks'; export { renderHook } from '@testing-library/react-hooks';
function customRender(ui: React.ReactElement, options = {}) { function customRender(ui: React.ReactElement, options = {}) {
@ -17,53 +16,11 @@ function customRender(ui: React.ReactElement, options = {}) {
export * from '@testing-library/react'; export * from '@testing-library/react';
export { default as userEvent } from '@testing-library/user-event'; export { default as userEvent } from '@testing-library/user-event';
// override render export // override render export
export { customRender as render }; export { customRender as render, sleep };
export const sleep = async (timeout = 0) => { export * from './renderApp';
return new Promise((resolve) => { export * from './renderHookWithApp';
setTimeout(resolve, timeout); export * from './renderSettings';
}); export * from './renderSingleSettings';
}; export * from './settingsChecker';
export * from './commonSettingsChecker';
export const WaitApp = async () => {
await waitFor(() => {
// @ts-ignore
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
};
interface RenderHookOptions extends Omit<GetAppOptions, 'value' | 'onChange'> {
hook: () => any;
props?: any;
Wrapper?: FC<{ children: React.ReactNode }>;
}
export const renderHookWithApp = async (options: RenderHookOptions) => {
const { hook: useHook, props, Wrapper = Fragment, ...otherOptions } = options;
const { App } = getApp(otherOptions);
const WrapperValue: FC<{ children: React.ReactNode }> = ({ children }) => (
<App>
<Wrapper>{children}</Wrapper>
</App>
);
const res = renderHook(() => useHook(), { wrapper: WrapperValue, initialProps: props });
await WaitApp();
return res;
};
export const renderApp = async (options: GetAppComponentOptions) => {
const App = getAppComponent(options);
const res = render(<App />);
await WaitApp();
return res;
};
export const renderReadPrettyApp = (options: GetAppComponentOptions) => {
return renderApp({ ...options, schema: { ...(options.schema || {}), 'x-read-pretty': true } });
};

View File

@ -0,0 +1,18 @@
import React from 'react';
import { render } from '@testing-library/react';
import { GetAppComponentOptions, addXReadPrettyToEachLayer, getAppComponent } from '../web';
import { WaitApp } from './utils';
export const renderApp = async (options: GetAppComponentOptions) => {
const App = getAppComponent(options);
const res = render(<App />);
await WaitApp();
return res;
};
export const renderReadPrettyApp = (options: GetAppComponentOptions) => {
return renderApp({ ...options, schema: addXReadPrettyToEachLayer(options.schema) });
};

View File

@ -0,0 +1,26 @@
import React, { FC, Fragment } from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { GetAppOptions, getApp } from '../web';
import { WaitApp } from './utils';
interface RenderHookOptions extends Omit<GetAppOptions, 'value' | 'onChange'> {
hook: () => any;
props?: any;
Wrapper?: FC<{ children: React.ReactNode }>;
}
export const renderHookWithApp = async (options: RenderHookOptions) => {
const { hook: useHook, props, Wrapper = Fragment, ...otherOptions } = options;
const { App } = getApp(otherOptions);
const WrapperValue: FC<{ children: React.ReactNode }> = ({ children }) => (
<App>
<Wrapper>{children}</Wrapper>
</App>
);
const res = renderHook(() => useHook(), { wrapper: WrapperValue, initialProps: props });
await WaitApp();
return res;
};

View File

@ -0,0 +1,42 @@
import { waitFor, screen } from '@testing-library/react';
import { GetAppComponentOptions } from '../web';
import userEvent from '@testing-library/user-event';
import { renderApp, renderReadPrettyApp } from './renderApp';
import { expectNoTsError } from './utils';
export async function showSettingsMenu(container: HTMLElement | Document = document) {
await waitFor(() => {
expectNoTsError(container.querySelector('[aria-label^="designer-schema-settings-"]')).toBeInTheDocument();
});
await userEvent.hover(container.querySelector('[aria-label^="designer-schema-settings-"]'));
await waitFor(() => {
expectNoTsError(screen.queryByTestId('schema-settings-menu')).toBeInTheDocument();
});
}
export interface RenderSettingsOptions extends GetAppComponentOptions {
container?: () => HTMLElement;
}
export const renderSettings = async (options: RenderSettingsOptions = {}) => {
const { container = () => document, ...appOptions } = options;
const result = await renderApp({ ...appOptions, designable: true });
const containerElement = container();
await showSettingsMenu(containerElement);
return result;
};
export const renderReadPrettySettings = async (options: RenderSettingsOptions = {}) => {
const { container = () => document, ...appOptions } = options;
const result = await renderReadPrettyApp({ ...appOptions, designable: true });
const containerElement = container();
await showSettingsMenu(containerElement);
return result;
};

View File

@ -0,0 +1,20 @@
import { RenderSettingsOptions, renderSettings } from './renderSettings';
import { addXReadPrettyToEachLayer, setSchemaWithSettings } from '../web';
interface RenderSingleSettingsOptions extends Omit<RenderSettingsOptions, 'schemaSettings'> {
settingPath?: string;
}
export const renderSingleSettings = (options: RenderSingleSettingsOptions) => {
setSchemaWithSettings(options);
return renderSettings(options);
};
export const renderReadPrettySingleSettings = (options: RenderSingleSettingsOptions) => {
setSchemaWithSettings(options);
options.schema = addXReadPrettyToEachLayer(options.schema);
return renderSettings(options);
};

View File

@ -0,0 +1,30 @@
import { screen } from '@testing-library/react';
import { CheckModalOptions, checkModal, expectNoTsError } from '../utils';
export interface CheckDeleteSettingOptions {
title: string;
deletedText?: string;
afterClick?: () => Promise<void> | void;
modalChecker?: Omit<CheckModalOptions, 'triggerText'>;
}
export async function checkDeleteSetting(options: CheckDeleteSettingOptions) {
if (options.modalChecker) {
await checkModal({
triggerText: options.title,
contentText: 'Are you sure you want to delete it?',
...options.modalChecker,
async afterSubmit() {
if (options.modalChecker.afterSubmit) {
await options.modalChecker.afterSubmit();
}
if (options.deletedText) {
expectNoTsError(screen.queryByText(options.deletedText)).not.toBeInTheDocument();
}
},
});
}
if (options.afterClick) {
await options.afterClick();
}
}

View File

@ -0,0 +1,41 @@
import { expect } from 'vitest';
import { screen } from '@testing-library/react';
import { CheckDeleteSettingOptions, checkDeleteSetting } from './delete';
import { CheckModalSettingOptions, checkModalSetting } from './modal';
import { CheckSwitchSettingOptions, checkSwitchSetting } from './switch';
import { SelectSettingOptions, checkSelectSetting } from './select';
import { showSettingsMenu } from '../renderSettings';
export * from './delete';
export * from './modal';
export * from './switch';
export * from './select';
export type CheckSettingsOptions =
| ({ type: 'switch' } & CheckSwitchSettingOptions)
| ({ type: 'modal' } & CheckModalSettingOptions)
| ({ type: 'select' } & SelectSettingOptions)
| ({ type: 'delete' } & CheckDeleteSettingOptions);
const types = {
switch: checkSwitchSetting,
modal: checkModalSetting,
delete: checkDeleteSetting,
select: checkSelectSetting,
};
export async function checkSettings(list: CheckSettingsOptions[], checkLength = false) {
if (checkLength) {
const menuList = screen.getByTestId('schema-settings-menu');
expect(menuList.querySelectorAll('li[role="menuitem"]')).toHaveLength(list.length);
}
for (const item of list) {
if (!screen.queryByTestId('schema-settings-menu')) {
await showSettingsMenu();
}
const type = item.type;
const checker = types[type];
await checker(item as any);
}
}

View File

@ -0,0 +1,21 @@
import { CheckModalOptions, checkModal } from '../utils';
export interface CheckModalSettingOptions {
title: string;
beforeClick?: () => Promise<void> | void;
afterClick?: () => Promise<void> | void;
modalChecker?: Omit<CheckModalOptions, 'triggerText'>;
}
export async function checkModalSetting(options: CheckModalSettingOptions) {
if (options.modalChecker) {
await checkModal({
triggerText: options.title,
...options.modalChecker,
});
}
if (options.afterClick) {
await options.afterClick();
}
}

View File

@ -0,0 +1,58 @@
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expectNoTsError } from '../utils';
export interface SelectSettingOptions {
title: string;
beforeSelect?: () => Promise<void> | void;
oldValue?: string;
options?: { label: string; checker?: () => void | Promise<void> }[];
}
export async function checkSelectSetting(options: SelectSettingOptions) {
if (options.beforeSelect) {
await options.beforeSelect();
}
const formItem = screen.getByTitle(options.title);
if (options.oldValue) {
expectNoTsError(formItem).toHaveTextContent(options.oldValue);
}
if (options.options) {
const getListbox = () => document.querySelector(`.select-popup-${options.title.replaceAll(' ', '-')}`);
// 打开下拉框
expectNoTsError(formItem.querySelector('.ant-select-selector')).toBeInTheDocument();
await userEvent.click(formItem.querySelector('.ant-select-selector'));
await waitFor(() => {
expectNoTsError(getListbox()).toBeInTheDocument();
});
for (const option of options.options) {
const listbox = getListbox();
expectNoTsError(listbox).toHaveTextContent(option.label);
if (option.checker) {
const item = [...listbox.querySelectorAll('.ant-select-item-option-content')].find(
(item) => item.textContent === option.label,
);
await userEvent.click(item);
// 等到下拉框关闭,并且值更新
await waitFor(() => {
expectNoTsError(screen.getByTitle(options.title)).toHaveTextContent(option.label);
});
await option.checker();
// 重新打开下拉框
await userEvent.click(screen.getByTitle(options.title).querySelector('.ant-select-selection-item'));
await waitFor(() => {
expectNoTsError(getListbox()).toBeInTheDocument();
});
}
}
}
}

View File

@ -0,0 +1,64 @@
import { expect } from 'vitest';
import { waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { showSettingsMenu } from '../renderSettings';
export interface CheckSwitchSettingOptions {
title: string;
beforeClick?: () => Promise<void> | void;
afterFirstClick?: () => Promise<void> | void;
afterSecondClick?: () => Promise<void> | void;
afterThirdClick?: () => Promise<void> | void;
}
export async function checkSwitchSetting(options: CheckSwitchSettingOptions) {
if (options.beforeClick) {
await options.beforeClick();
}
// 先获取 switch 元素,记录 checked 状态
const formItem = screen.getByTitle(options.title);
const switchElement = formItem.querySelector('button[role=switch]');
let oldChecked = switchElement.getAttribute('aria-checked');
const afterClick = async () => {
const formItem = screen.queryByTitle(options.title);
if (formItem) {
const switchElement = formItem.querySelector('button[role=switch]');
const newChecked = switchElement.getAttribute('aria-checked');
expect(newChecked).not.toBe(oldChecked);
oldChecked = newChecked;
} else {
// 重新打开设置菜单
await showSettingsMenu();
}
};
// 第一次点击
if (options.afterFirstClick) {
await userEvent.click(formItem.querySelector('button[role=switch]'));
await waitFor(async () => {
await afterClick();
});
await options.afterFirstClick();
}
// 第二次点击
if (options.afterSecondClick) {
await userEvent.click(screen.getByText(options.title));
await waitFor(async () => {
await afterClick();
});
await options.afterSecondClick();
}
// 第三次点击
if (options.afterThirdClick) {
await userEvent.click(screen.getByText(options.title));
await waitFor(async () => {
await afterClick();
});
await options.afterThirdClick();
}
}

View File

@ -0,0 +1,66 @@
import { waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { FormItemCheckOptions, checkFormItems } from '../formItemChecker';
import { expectNoTsError } from './utils';
import { sleep } from '../../web';
export interface CheckModalOptions {
triggerText?: string;
modalTitle?: string;
confirmTitle?: string;
submitText?: string;
contentText?: string;
beforeCheck?: () => Promise<void> | void;
customCheck?: () => Promise<void> | void;
formItems?: FormItemCheckOptions[];
afterSubmit?: () => Promise<void> | void;
}
export async function checkModal(options: CheckModalOptions) {
const { triggerText, modalTitle, confirmTitle, submitText = 'OK', formItems = [] } = options;
await waitFor(() => {
expectNoTsError(screen.queryByText(triggerText)).toBeInTheDocument();
});
await userEvent.click(screen.getByText(triggerText));
await waitFor(() => {
expectNoTsError(screen.queryByRole('dialog')).toBeInTheDocument();
});
const dialog = screen.getByRole('dialog');
if (modalTitle) {
expectNoTsError(dialog.querySelector('.ant-modal-title')).toHaveTextContent(modalTitle);
}
if (confirmTitle) {
expectNoTsError(dialog.querySelector('.ant-modal-confirm-title')).toHaveTextContent(confirmTitle);
}
if (options.contentText) {
expectNoTsError(dialog).toHaveTextContent(options.contentText);
}
if (options.beforeCheck) {
await options.beforeCheck();
}
if (options.customCheck) {
await options.customCheck();
}
await checkFormItems(formItems);
await userEvent.click(screen.getByText(submitText));
await waitFor(() => {
expectNoTsError(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
if (options.afterSubmit) {
await sleep(100);
await options.afterSubmit();
}
}

View File

@ -0,0 +1,10 @@
import { waitFor, screen } from '@testing-library/react';
import { expect } from 'vitest';
export async function checkSchema(matchObj?: Record<string, any>, name?: string) {
const objText = screen.queryByTestId(name ? `test-schema-${name}` : `test-schema`);
await waitFor(() => {
expect(JSON.parse(objText.textContent)).toMatchObject(matchObj);
});
}

View File

@ -0,0 +1,3 @@
export * from './utils';
export * from './checkModal';
export * from './checkSchema';

View File

@ -0,0 +1,21 @@
import { expect } from 'vitest';
import { waitFor, screen } from '@testing-library/react';
// @ts-ignore
export const expectNoTsError: any = expect;
export const WaitApp = async () => {
await waitFor(() => {
expectNoTsError(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
const loadError = screen.queryByText('Load Plugin Error');
if (loadError) {
expectNoTsError(screen.queryByText('Load Plugin Error')).not.toBeInTheDocument();
}
const renderError = screen.queryByText('Render Failed');
if (renderError) {
expectNoTsError(screen.queryByText('Render Failed')).not.toBeInTheDocument();
}
};

View File

@ -1,34 +1,14 @@
[ [
{ {
"key": "h7b9i8khc3q", "key": "yzowed2vee0",
"name": "users", "name": "users",
"title": "{{t(\"Users\")}}",
"inherit": false, "inherit": false,
"hidden": false, "hidden": false,
"description": null, "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": [ "fields": [
{ {
"uiSchema": { "key": "6m3kn2pytkc",
"type": "number",
"title": "{{t(\"ID\")}}",
"x-component": "InputNumber",
"x-read-pretty": true,
"rawTitle": "{{t(\"ID\")}}"
},
"key": "ffp1f2sula0",
"name": "id", "name": "id",
"type": "bigInt", "type": "bigInt",
"interface": "id", "interface": "id",
@ -38,36 +18,31 @@
"reverseKey": null, "reverseKey": null,
"autoIncrement": true, "autoIncrement": true,
"primaryKey": true, "primaryKey": true,
"allowNull": false "allowNull": false,
"uiSchema": {
"type": "number",
"title": "{{t(\"ID\")}}",
"x-component": "InputNumber",
"x-read-pretty": true
}
}, },
{ {
"uiSchema": { "key": "8douy9r69x5",
"type": "string",
"title": "{{t(\"Nickname\")}}",
"x-component": "Input",
"rawTitle": "{{t(\"Nickname\")}}"
},
"key": "vrv7yjue90g",
"name": "nickname", "name": "nickname",
"type": "string", "type": "string",
"interface": "input", "interface": "input",
"description": null, "description": null,
"collectionName": "users", "collectionName": "users",
"parentKey": null, "parentKey": null,
"reverseKey": null "reverseKey": null,
},
{
"uiSchema": { "uiSchema": {
"type": "string", "type": "string",
"title": "{{t(\"Username\")}}", "title": "{{t(\"Nickname\")}}",
"x-component": "Input", "x-component": "Input"
"x-validator": { }
"username": true },
}, {
"required": true, "key": "vp191ptc0d7",
"rawTitle": "{{t(\"Username\")}}"
},
"key": "2ccs6evyrub",
"name": "username", "name": "username",
"type": "string", "type": "string",
"interface": "input", "interface": "input",
@ -75,18 +50,19 @@
"collectionName": "users", "collectionName": "users",
"parentKey": null, "parentKey": null,
"reverseKey": null, "reverseKey": null,
"unique": true "unique": true,
},
{
"uiSchema": { "uiSchema": {
"type": "string", "type": "string",
"title": "{{t(\"Email\")}}", "title": "{{t(\"Username\")}}",
"x-component": "Input", "x-component": "Input",
"x-validator": "email", "x-validator": {
"required": true, "username": true
"rawTitle": "{{t(\"Email\")}}" },
}, "required": true
"key": "rrskwjl5wt1", }
},
{
"key": "47o82qhkvdm",
"name": "email", "name": "email",
"type": "string", "type": "string",
"interface": "email", "interface": "email",
@ -94,10 +70,150 @@
"collectionName": "users", "collectionName": "users",
"parentKey": null, "parentKey": null,
"reverseKey": null, "reverseKey": null,
"unique": true "unique": true,
"uiSchema": {
"type": "string",
"title": "{{t(\"Email\")}}",
"x-component": "Input",
"x-validator": "email",
"required": true
}
}, },
{ {
"key": "t09bauwm0wb", "key": "q5i9ynhq325",
"name": "phone",
"type": "string",
"interface": "phone",
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"unique": true,
"uiSchema": {
"type": "string",
"title": "{{t(\"Phone\")}}",
"x-component": "Input",
"x-validator": "phone",
"required": true
}
},
{
"key": "cc4aslvh9dv",
"name": "password",
"type": "password",
"interface": "password",
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"hidden": true,
"uiSchema": {
"type": "string",
"title": "{{t(\"Password\")}}",
"x-component": "Password"
}
},
{
"key": "e87ndyttazh",
"name": "appLang",
"type": "string",
"interface": null,
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null
},
{
"key": "snfbet0pe49",
"name": "resetToken",
"type": "string",
"interface": null,
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"unique": true,
"hidden": true
},
{
"key": "j78yhz6wifd",
"name": "systemSettings",
"type": "json",
"interface": null,
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"defaultValue": {}
},
{
"key": "lkxqml8gchd",
"name": "sort",
"type": "sort",
"interface": null,
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"hidden": true
},
{
"key": "lt3pcrjngzc",
"name": "createdById",
"type": "context",
"interface": null,
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"dataType": "bigInt",
"dataIndex": "state.currentUser.id",
"createOnly": true,
"visible": true,
"index": true
},
{
"key": "lcfaf27uxyz",
"name": "createdBy",
"type": "belongsTo",
"interface": null,
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"target": "users",
"foreignKey": "createdById",
"targetKey": "id"
},
{
"key": "y6lwdb31r5t",
"name": "updatedById",
"type": "context",
"interface": null,
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"dataType": "bigInt",
"dataIndex": "state.currentUser.id",
"visible": true,
"index": true
},
{
"key": "exhbmthsin0",
"name": "updatedBy",
"type": "belongsTo",
"interface": null,
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"target": "users",
"foreignKey": "updatedById",
"targetKey": "id"
},
{
"key": "s921nnlzdwi",
"name": "roles", "name": "roles",
"type": "belongsToMany", "type": "belongsToMany",
"interface": "m2m", "interface": "m2m",
@ -126,60 +242,80 @@
} }
}, },
{ {
"key": "1pz0art9mt7", "key": "qe7b1rsct5h",
"name": "f_n2fu6hvprct", "name": "jobs",
"type": "string", "type": "belongsToMany",
"interface": "select", "interface": null,
"description": null, "description": null,
"collectionName": "t_vwpds9fs4xs", "collectionName": "users",
"parentKey": null,
"reverseKey": null,
"through": "users_jobs",
"foreignKey": "userId",
"sourceKey": "id",
"otherKey": "jobId",
"targetKey": "id",
"target": "jobs"
},
{
"key": "vt0n1l1ruyz",
"name": "usersJobs",
"type": "hasMany",
"interface": null,
"description": null,
"collectionName": "users",
"parentKey": null,
"reverseKey": null,
"target": "users_jobs",
"foreignKey": "userId",
"sourceKey": "id",
"targetKey": "id"
},
{
"key": "ekol7p60nry",
"name": "sortName",
"type": "sort",
"interface": "sort",
"description": null,
"collectionName": "users",
"parentKey": null, "parentKey": null,
"reverseKey": null, "reverseKey": null,
"uiSchema": { "uiSchema": {
"enum": [ "type": "number",
{ "x-component": "InputNumber",
"value": "test1", "x-component-props": {
"label": "test1" "stringMode": true,
}, "step": "1"
{ },
"value": "test2", "x-validator": "integer",
"label": "test2" "title": "sort"
}
],
"type": "string",
"x-component": "Select",
"title": "test"
} }
} }
] ],
"category": [],
"origin": "@nocobase/plugin-users",
"dumpRules": {
"group": "user"
},
"sortable": "sort",
"model": "UserModel",
"createdBy": true,
"updatedBy": true,
"logging": true,
"shared": true,
"from": "db2cm",
"filterTargetKey": "id"
}, },
{ {
"key": "pqnenvqrzxr", "key": "rmx938ttbue",
"name": "roles", "name": "roles",
"title": "{{t(\"Roles\")}}",
"inherit": false, "inherit": false,
"hidden": false, "hidden": false,
"description": null, "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": [ "fields": [
{ {
"uiSchema": { "key": "l6thu4n5u6x",
"type": "string",
"title": "{{t(\"Role UID\")}}",
"x-component": "Input",
"rawTitle": "{{t(\"Role UID\")}}"
},
"key": "jbz9m80bxmp",
"name": "name", "name": "name",
"type": "uid", "type": "uid",
"interface": "input", "interface": "input",
@ -188,16 +324,15 @@
"parentKey": null, "parentKey": null,
"reverseKey": null, "reverseKey": null,
"prefix": "r_", "prefix": "r_",
"primaryKey": true "primaryKey": true,
},
{
"uiSchema": { "uiSchema": {
"type": "string", "type": "string",
"title": "{{t(\"Role name\")}}", "title": "{{t(\"Role UID\")}}",
"x-component": "Input", "x-component": "Input"
"rawTitle": "{{t(\"Role name\")}}" }
}, },
"key": "faywtz4sf3u", {
"key": "yhfq9yv8z0p",
"name": "title", "name": "title",
"type": "string", "type": "string",
"interface": "input", "interface": "input",
@ -206,10 +341,15 @@
"parentKey": null, "parentKey": null,
"reverseKey": null, "reverseKey": null,
"unique": true, "unique": true,
"uiSchema": {
"type": "string",
"title": "{{t(\"Role name\")}}",
"x-component": "Input"
},
"translation": true "translation": true
}, },
{ {
"key": "1enkovm9sye", "key": "vnhjlmopfuz",
"name": "description", "name": "description",
"type": "string", "type": "string",
"interface": null, "interface": null,
@ -217,7 +357,463 @@
"collectionName": "roles", "collectionName": "roles",
"parentKey": null, "parentKey": null,
"reverseKey": null "reverseKey": null
},
{
"key": "s4iqsehgxo6",
"name": "strategy",
"type": "json",
"interface": null,
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null
},
{
"key": "75e4wnv873m",
"name": "default",
"type": "boolean",
"interface": null,
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null,
"defaultValue": false
},
{
"key": "nofdv0gte68",
"name": "hidden",
"type": "boolean",
"interface": null,
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null,
"defaultValue": false
},
{
"key": "bogzo1uvk84",
"name": "allowConfigure",
"type": "boolean",
"interface": null,
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null
},
{
"key": "k3fvj8ddpp9",
"name": "allowNewMenu",
"type": "boolean",
"interface": null,
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null
},
{
"key": "v1ditqsv1uk",
"name": "menuUiSchemas",
"type": "belongsToMany",
"interface": null,
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null,
"target": "uiSchemas",
"targetKey": "x-uid",
"foreignKey": "roleName",
"sourceKey": "name",
"otherKey": "uiSchemaXUid",
"through": "rolesUischemas"
},
{
"key": "ccqhlcvgnz8",
"name": "resources",
"type": "hasMany",
"interface": null,
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null,
"target": "dataSourcesRolesResources",
"sourceKey": "name",
"foreignKey": "roleName",
"targetKey": "id"
},
{
"key": "s4fshtx7oxv",
"name": "snippets",
"type": "set",
"interface": null,
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null,
"defaultValue": ["!ui.*", "!pm", "!pm.*"]
},
{
"key": "oyzvbhc60mp",
"name": "users",
"type": "belongsToMany",
"interface": null,
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null,
"target": "users",
"foreignKey": "roleName",
"otherKey": "userId",
"onDelete": "CASCADE",
"sourceKey": "name",
"targetKey": "id",
"through": "rolesUsers"
},
{
"key": "89yklh7lm3p",
"name": "sort",
"type": "sort",
"interface": null,
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null,
"hidden": true
},
{
"key": "iz5s22dinui",
"name": "color",
"type": "string",
"interface": "color",
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null,
"defaultValue": "#1677FF",
"uiSchema": {
"type": "string",
"x-component": "ColorPicker",
"default": "#1677FF",
"title": "color"
}
},
{
"key": "o5nyb6isl62",
"name": "long-text",
"type": "text",
"interface": "textarea",
"description": null,
"collectionName": "roles",
"parentKey": null,
"reverseKey": null,
"uiSchema": {
"type": "string",
"x-component": "Input.TextArea",
"title": "Long text"
}
} }
] ],
"category": [],
"origin": "@nocobase/plugin-acl",
"dumpRules": "required",
"autoGenId": false,
"model": "RoleModel",
"filterTargetKey": "name",
"sortable": true,
"from": "db2cm"
},
{
"key": "24gntrrr5a6",
"name": "tree",
"title": "TreeCollection",
"inherit": false,
"hidden": false,
"description": null,
"fields": [
{
"key": "pcea3h3ivkg",
"name": "parentId",
"type": "bigInt",
"interface": "integer",
"description": null,
"collectionName": "t_4uamm7v51dj",
"parentKey": null,
"reverseKey": null,
"isForeignKey": true,
"uiSchema": {
"type": "number",
"title": "{{t(\"Parent ID\")}}",
"x-component": "InputNumber",
"x-read-pretty": true
},
"target": "t_4uamm7v51dj"
},
{
"key": "185j2rf7o68",
"name": "parent",
"type": "belongsTo",
"interface": "m2o",
"description": null,
"collectionName": "t_4uamm7v51dj",
"parentKey": null,
"reverseKey": null,
"foreignKey": "parentId",
"treeParent": true,
"onDelete": "CASCADE",
"uiSchema": {
"title": "{{t(\"Parent\")}}",
"x-component": "AssociationField",
"x-component-props": {
"multiple": false,
"fieldNames": {
"label": "id",
"value": "id"
}
}
},
"target": "t_4uamm7v51dj",
"targetKey": "id"
},
{
"key": "gjvso3p9sjn",
"name": "children",
"type": "hasMany",
"interface": "o2m",
"description": null,
"collectionName": "t_4uamm7v51dj",
"parentKey": null,
"reverseKey": null,
"foreignKey": "parentId",
"treeChildren": true,
"onDelete": "CASCADE",
"uiSchema": {
"title": "{{t(\"Children\")}}",
"x-component": "AssociationField",
"x-component-props": {
"multiple": true,
"fieldNames": {
"label": "id",
"value": "id"
}
}
},
"target": "t_4uamm7v51dj",
"targetKey": "id",
"sourceKey": "id"
},
{
"key": "j50f3am3c88",
"name": "id",
"type": "bigInt",
"interface": "integer",
"description": null,
"collectionName": "t_4uamm7v51dj",
"parentKey": null,
"reverseKey": null,
"autoIncrement": true,
"primaryKey": true,
"allowNull": false,
"uiSchema": {
"type": "number",
"title": "{{t(\"ID\")}}",
"x-component": "InputNumber",
"x-read-pretty": true
},
"target": "t_4uamm7v51dj"
},
{
"key": "9szmn2ecqgs",
"name": "f_y99u3pyj0bt",
"type": "string",
"interface": "input",
"description": null,
"collectionName": "t_4uamm7v51dj",
"parentKey": null,
"reverseKey": null,
"uiSchema": {
"type": "string",
"x-component": "Input",
"title": "Single line text"
}
}
],
"category": [],
"logging": true,
"autoGenId": true,
"createdAt": false,
"createdBy": false,
"updatedAt": false,
"updatedBy": false,
"template": "tree",
"view": false,
"tree": "adjacencyList",
"filterTargetKey": "id"
},
{
"key": "16ocj2rsg3t",
"name": "interfaces",
"title": "Interfaces",
"inherit": false,
"hidden": false,
"description": null,
"fields": [
{
"key": "k2v39l19inp",
"name": "id",
"type": "bigInt",
"interface": "integer",
"description": null,
"collectionName": "interfaces",
"parentKey": null,
"reverseKey": null,
"autoIncrement": true,
"primaryKey": true,
"allowNull": false,
"uiSchema": {
"type": "number",
"title": "{{t(\"ID\")}}",
"x-component": "InputNumber",
"x-read-pretty": true
}
},
{
"key": "gfnqdj8sd01",
"name": "createdAt",
"type": "date",
"interface": "createdAt",
"description": null,
"collectionName": "interfaces",
"parentKey": null,
"reverseKey": null,
"field": "createdAt",
"uiSchema": {
"type": "datetime",
"title": "{{t(\"Created at\")}}",
"x-component": "DatePicker",
"x-component-props": {},
"x-read-pretty": true
}
},
{
"key": "3ddqrb1lle5",
"name": "createdBy",
"type": "belongsTo",
"interface": "createdBy",
"description": null,
"collectionName": "interfaces",
"parentKey": null,
"reverseKey": null,
"target": "users",
"foreignKey": "createdById",
"uiSchema": {
"type": "object",
"title": "{{t(\"Created by\")}}",
"x-component": "AssociationField",
"x-component-props": {
"fieldNames": {
"value": "id",
"label": "nickname"
}
},
"x-read-pretty": true
},
"targetKey": "id"
},
{
"key": "9nc7gqqw0ht",
"name": "updatedAt",
"type": "date",
"interface": "updatedAt",
"description": null,
"collectionName": "interfaces",
"parentKey": null,
"reverseKey": null,
"field": "updatedAt",
"uiSchema": {
"type": "string",
"title": "{{t(\"Last updated at\")}}",
"x-component": "DatePicker",
"x-component-props": {},
"x-read-pretty": true
}
},
{
"key": "06wr4f2qi4w",
"name": "updatedBy",
"type": "belongsTo",
"interface": "updatedBy",
"description": null,
"collectionName": "interfaces",
"parentKey": null,
"reverseKey": null,
"target": "users",
"foreignKey": "updatedById",
"uiSchema": {
"type": "object",
"title": "{{t(\"Last updated by\")}}",
"x-component": "AssociationField",
"x-component-props": {
"fieldNames": {
"value": "id",
"label": "nickname"
}
},
"x-read-pretty": true
},
"targetKey": "id"
},
{
"key": "g5beggm3aln",
"name": "nano-iD",
"type": "nanoid",
"interface": "nanoid",
"description": null,
"collectionName": "interfaces",
"parentKey": null,
"reverseKey": null,
"customAlphabet": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
"size": 21,
"autoFill": true,
"uiSchema": {
"type": "string",
"x-component": "NanoIDInput",
"title": "Nano ID"
}
},
{
"key": "wchbz23orvg",
"name": "attachment",
"type": "belongsToMany",
"interface": "attachment",
"description": null,
"collectionName": "interfaces",
"parentKey": null,
"reverseKey": null,
"uiSchema": {
"x-component-props": {
"accept": "image/*",
"multiple": true
},
"type": "array",
"x-component": "Upload.Attachment",
"title": "Attachment"
},
"target": "attachments",
"through": "t_zj0zu7maytd",
"foreignKey": "f_iek1e32gsq0",
"otherKey": "f_dtc6w8dzyoo",
"targetKey": "id",
"sourceKey": "id"
}
],
"category": [],
"logging": true,
"autoGenId": true,
"createdAt": true,
"createdBy": true,
"updatedAt": true,
"updatedBy": true,
"template": "general",
"view": false,
"filterTargetKey": "id"
} }
] ]

View File

@ -0,0 +1,186 @@
{
"users:list": {
"data": [
{
"createdAt": "2024-04-07T06:50:37.797Z",
"updatedAt": "2024-04-07T06:50:37.797Z",
"appLang": null,
"createdById": null,
"email": "admin@nocobase.com",
"f_1gx8uyn3wva": 1,
"id": 1,
"nickname": "Super Admin",
"phone": null,
"systemSettings": {},
"updatedById": null,
"username": "nocobase",
"roles": [
{
"createdAt": "2024-04-07T06:50:37.700Z",
"updatedAt": "2024-04-07T06:50:37.700Z",
"allowConfigure": null,
"allowNewMenu": true,
"default": true,
"description": null,
"hidden": false,
"color": "#1677FF",
"name": "member",
"snippets": ["!pm", "!pm.*", "!ui.*"],
"strategy": {
"actions": ["view", "update:own", "destroy:own", "create"]
},
"title": "{{t(\"Member\")}}",
"rolesUsers": {
"createdAt": "2024-04-07T06:50:37.854Z",
"updatedAt": "2024-04-07T06:50:37.854Z",
"default": null,
"roleName": "member",
"userId": 1
}
},
{
"createdAt": "2024-04-07T06:50:37.622Z",
"updatedAt": "2024-04-07T06:50:37.622Z",
"allowConfigure": null,
"allowNewMenu": null,
"default": false,
"description": null,
"hidden": true,
"color": "#1677FF",
"name": "root",
"snippets": ["pm", "pm.*", "ui.*"],
"strategy": null,
"title": "{{t(\"Root\")}}",
"rolesUsers": {
"createdAt": "2024-04-07T06:50:38.152Z",
"updatedAt": "2024-04-07T06:50:38.186Z",
"default": true,
"roleName": "root",
"userId": 1
}
},
{
"createdAt": "2024-04-07T06:50:37.657Z",
"updatedAt": "2024-04-07T06:50:37.657Z",
"allowConfigure": true,
"allowNewMenu": true,
"default": false,
"description": null,
"hidden": false,
"name": "admin",
"color": "#1677FF",
"snippets": ["pm", "pm.*", "ui.*"],
"strategy": {
"actions": ["create", "view", "update", "destroy"]
},
"title": "{{t(\"Admin\")}}",
"rolesUsers": {
"createdAt": "2024-04-07T06:50:38.152Z",
"updatedAt": "2024-04-07T06:50:38.152Z",
"default": null,
"roleName": "admin",
"userId": 1
}
}
]
}
],
"meta": {
"count": 1,
"page": 1,
"pageSize": 20,
"totalPage": 1,
"allowedActions": {
"view": [1],
"update": [1],
"destroy": []
}
}
},
"roles:list": {
"data": [
{
"createdAt": "2024-04-07T06:50:37.622Z",
"updatedAt": "2024-04-07T06:50:37.622Z",
"allowConfigure": null,
"allowNewMenu": null,
"default": false,
"description": null,
"hidden": true,
"name": "root",
"color": "#1677FF",
"snippets": ["pm", "pm.*", "ui.*"],
"strategy": null,
"title": "{{t(\"Root\")}}"
},
{
"createdAt": "2024-04-07T06:50:37.657Z",
"updatedAt": "2024-04-07T06:50:37.657Z",
"allowConfigure": true,
"allowNewMenu": true,
"default": false,
"description": null,
"hidden": false,
"color": "#1677FF",
"name": "admin",
"snippets": ["pm", "pm.*", "ui.*"],
"strategy": {
"actions": ["create", "view", "update", "destroy"]
},
"title": "{{t(\"Admin\")}}"
},
{
"createdAt": "2024-04-07T06:50:37.700Z",
"updatedAt": "2024-04-07T06:50:37.700Z",
"allowConfigure": null,
"allowNewMenu": true,
"default": true,
"description": null,
"color": "#1677FF",
"hidden": false,
"name": "member",
"snippets": ["!pm", "!pm.*", "!ui.*"],
"strategy": {
"actions": ["view", "update:own", "destroy:own", "create"]
},
"title": "{{t(\"Member\")}}"
}
],
"meta": {
"count": 3,
"page": 1,
"pageSize": 20,
"totalPage": 1,
"allowedActions": {
"view": ["root", "admin", "member"],
"update": ["root", "admin", "member"],
"destroy": []
}
}
},
"tree:list": {
"data": [
{
"id": 1,
"parentId": null,
"f_y99u3pyj0bt": "1"
},
{
"id": 2,
"parentId": 1,
"f_y99u3pyj0bt": "2"
}
],
"meta": {
"count": 2,
"page": 1,
"pageSize": 20,
"totalPage": 1,
"allowedActions": {
"view": [1, 2],
"update": [1, 2],
"destroy": [1, 2]
}
}
}
}

View File

@ -1,13 +1,34 @@
import React, { ComponentType } from 'react'; import React, { ComponentType } from 'react';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { AxiosInstance } from 'axios'; import { AxiosInstance, AxiosRequestConfig } from 'axios';
import { set, get, pick } from 'lodash';
import { useFieldSchema, observer } from '@formily/react';
// @ts-ignore import {
import { Application, ApplicationOptions, DataBlockProvider, LocalDataSource, SchemaComponent } from '@nocobase/client'; AntdSchemaComponentPlugin,
Application,
ApplicationOptions,
CollectionPlugin,
DataBlockProvider,
LocalDataSource,
SchemaComponent,
SchemaSettings,
SchemaSettingsPlugin,
// @ts-ignore
} from '@nocobase/client';
import dataSourceMainCollections from './dataSourceMainCollections.json'; import dataSourceMainCollections from './dataSourceMainCollections.json';
import dataSource2 from './dataSource2.json'; import dataSource2 from './dataSource2.json';
import usersListData from './usersListData.json'; import dataSourceMainData from './dataSourceMainData.json';
import _ from 'lodash';
const defaultApis = {
'uiSchemas:patch': { data: { result: 'ok' } },
'uiSchemas:saveAsTemplate': { data: { result: 'ok' } },
...dataSourceMainData,
};
export * from './utils';
type URL = string; type URL = string;
type ResponseData = any; type ResponseData = any;
@ -15,15 +36,29 @@ type ResponseData = any;
type MockApis = Record<URL, ResponseData>; type MockApis = Record<URL, ResponseData>;
type AppOrOptions = Application | ApplicationOptions; type AppOrOptions = Application | ApplicationOptions;
function getProcessMockData(apis: Record<string, any>, key: string) {
return (config: AxiosRequestConfig) => {
if (!apis[key]) return [404, { data: { message: 'mock data not found' } }];
if (config?.params?.pageSize || config?.params?.page) {
const { data, meta } = apis[key];
const pageSize = config.params.pageSize || meta?.pageSize;
const page = config.params.page || meta?.page;
return [200, { data: data.slice(pageSize * (page - 1), pageSize), meta: { ...meta, page, pageSize } }];
}
return [200, apis[key]];
};
}
export const mockApi = (axiosInstance: AxiosInstance, apis: MockApis = {}) => { export const mockApi = (axiosInstance: AxiosInstance, apis: MockApis = {}) => {
const mock = new MockAdapter(axiosInstance); const mock = new MockAdapter(axiosInstance);
Object.keys(apis).forEach((key) => { Object.keys(apis).forEach((key) => {
mock.onAny(key).reply(200, apis[key]); mock.onAny(key).reply(getProcessMockData(apis, key));
}); });
return (apis: MockApis = {}) => { return (apis: MockApis = {}) => {
Object.keys(apis).forEach((key) => { Object.keys(apis).forEach((key) => {
mock.onAny(key).reply(200, apis[key]); mock.onAny(key).reply(getProcessMockData(apis, key));
}); });
}; };
}; };
@ -37,23 +72,48 @@ export interface GetAppOptions {
appOptions?: AppOrOptions; appOptions?: AppOrOptions;
providers?: (ComponentType | [ComponentType, any])[]; providers?: (ComponentType | [ComponentType, any])[];
apis?: MockApis; apis?: MockApis;
enableUserListDataBlock?: boolean; designable?: boolean;
schemaSettings?: SchemaSettings;
disableAcl?: boolean;
enableMultipleDataSource?: boolean; enableMultipleDataSource?: boolean;
} }
export const getApp = (options: GetAppOptions) => { export const getApp = (options: GetAppOptions) => {
const { appOptions, enableUserListDataBlock, providers, apis, enableMultipleDataSource } = options; const {
const app = appOptions instanceof Application ? appOptions : new Application(appOptions); appOptions = {},
schemaSettings,
providers,
disableAcl = true,
apis: optionsApis = {},
enableMultipleDataSource,
designable,
} = options;
const app =
appOptions instanceof Application
? appOptions
: new Application({
...appOptions,
disableAcl: appOptions.disableAcl || disableAcl,
designable: appOptions.designable || designable,
});
if (providers) { if (providers) {
app.addProviders(providers); app.addProviders(providers);
} }
app.getCollectionManager().addCollections(dataSourceMainCollections as any); if (schemaSettings) {
app.schemaSettingsManager.add(schemaSettings);
if (enableUserListDataBlock && !apis['users:list']) {
apis['users:list'] = usersListData;
} }
app.addComponents({ CommonSchemaComponent });
app.pluginManager.add(AntdSchemaComponentPlugin);
app.pluginManager.add(SchemaSettingsPlugin);
app.pluginManager.add(CollectionPlugin, { config: { enableRemoteDataSource: false } });
const apis = Object.assign({}, defaultApis, optionsApis);
app.getCollectionManager().addCollections(dataSourceMainCollections as any);
if (enableMultipleDataSource) { if (enableMultipleDataSource) {
app.dataSourceManager.addDataSource(LocalDataSource, dataSource2 as any); app.dataSourceManager.addDataSource(LocalDataSource, dataSource2 as any);
} }
@ -61,36 +121,40 @@ export const getApp = (options: GetAppOptions) => {
mockAppApi(app, apis); mockAppApi(app, apis);
const App = app.getRootComponent(); const App = app.getRootComponent();
return { return {
App, App,
app, app,
}; };
}; };
export interface GetAppComponentOptions<V = any, Props = {}> { export interface GetAppComponentOptions<V = any, Props = {}> extends GetAppOptions {
schema?: any; schema?: any;
appOptions?: AppOrOptions;
apis?: MockApis;
Component?: ComponentType<Props>; Component?: ComponentType<Props>;
value?: V; value?: V;
props?: Props; props?: Props;
onChange?: (value: V) => void; noWrapperSchema?: boolean;
enableUserListDataBlock?: boolean; enableUserListDataBlock?: boolean;
enableMultipleDataSource?: boolean; onChange?: (value: V) => void;
} }
export const getAppComponent = (options: GetAppComponentOptions) => { export const getAppComponent = (options: GetAppComponentOptions) => {
const { const {
schema: optionsSchema = {},
Component, Component,
enableUserListDataBlock,
enableMultipleDataSource,
value, value,
props, props,
appOptions,
apis,
onChange, onChange,
schema: optionsSchema = {}, noWrapperSchema,
enableUserListDataBlock,
...otherOptions
} = options; } = options;
if (noWrapperSchema) {
const { App } = getApp(options);
return App;
}
const schema = { const schema = {
type: 'object', type: 'object',
name: 'test', name: 'test',
@ -107,6 +171,10 @@ export const getAppComponent = (options: GetAppComponentOptions) => {
schema.name = 'test'; schema.name = 'test';
} }
if (!schema['x-uid']) {
schema['x-uid'] = 'test';
}
if (!schema.type) { if (!schema.type) {
schema.type = 'void'; schema.type = 'void';
} }
@ -123,16 +191,114 @@ export const getAppComponent = (options: GetAppComponentOptions) => {
}; };
const { App } = getApp({ const { App } = getApp({
appOptions, ...otherOptions,
apis,
providers: [TestDemo], providers: [TestDemo],
enableMultipleDataSource,
enableUserListDataBlock,
}); });
return App; return App;
}; };
export function addXReadPrettyToEachLayer(obj: Record<string, any> = {}) {
// 为当前层添加 'x-read-pretty' 属性
obj['x-read-pretty'] = true;
// 递归遍历对象的每个属性
_.forOwn(obj, (value, key) => {
if (_.isObject(value)) {
addXReadPrettyToEachLayer(value);
}
});
return obj;
}
export const getReadPrettyAppComponent = (options: GetAppComponentOptions) => { export const getReadPrettyAppComponent = (options: GetAppComponentOptions) => {
return getAppComponent({ ...options, schema: { ...(options.schema || {}), 'x-read-pretty': true } }); return getAppComponent({ ...options, schema: addXReadPrettyToEachLayer(options.schema) });
}; };
interface GetAppComponentWithSchemaSettingsOptions extends GetAppComponentOptions {
settingPath?: string;
}
export function setSchemaWithSettings(options: GetAppComponentWithSchemaSettingsOptions) {
const { Component, settingPath } = options;
const SINGLE_SETTINGS_NAME = 'testSettings';
const testSettings = new SchemaSettings({
name: SINGLE_SETTINGS_NAME,
items: [
{
name: 'test',
Component,
},
],
});
if (!options.schema) {
options.schema = {};
}
if (settingPath) {
const schema = get(options.schema, settingPath);
schema['x-settings'] = SINGLE_SETTINGS_NAME;
} else {
options.schema['x-settings'] = SINGLE_SETTINGS_NAME;
}
if (!options.appOptions) {
options.appOptions = {};
}
if (options.appOptions instanceof Application) {
options.appOptions.schemaSettingsManager.add(testSettings);
} else {
if (!options.appOptions.schemaSettings) {
options.appOptions.schemaSettings = [];
}
options.appOptions.schemaSettings.push(testSettings);
}
}
export const getAppComponentWithSchemaSettings = (options: GetAppComponentWithSchemaSettingsOptions) => {
setSchemaWithSettings(options);
const App = getAppComponent(options);
return App;
};
export const getReadPrettyAppComponentWithSchemaSettings = (options: GetAppComponentWithSchemaSettingsOptions) => {
setSchemaWithSettings(options);
set(options.schema, 'x-read-pretty', true);
const App = getAppComponent(options);
return App;
};
export function withSchema(Component: ComponentType, name?: string) {
const ComponentValue = observer((props) => {
const schema = useFieldSchema();
const schemaValue = pick(schema.toJSON(), [
'title',
'description',
'enum',
'x-component-props',
'x-decorator-props',
'x-linkage-rules',
]);
return (
<>
<pre data-testid={name ? `test-schema-${name}` : `test-schema`}>
{JSON.stringify(schemaValue, undefined, 2)}
</pre>
<Component {...props} />
</>
);
});
ComponentValue.displayName = `withSchema(${Component.displayName || Component.name})`;
return ComponentValue;
}
export const CommonSchemaComponent = withSchema(function CommonSchemaComponent(props: any) {
return <>{props.children}</>;
});

View File

@ -1,30 +0,0 @@
{
"data": [
{
"f_o3y6p9gf1gx": null,
"createdAt": "2023-03-30T07:53:10.941Z",
"updatedAt": "2024-04-12T03:27:45.748Z",
"appLang": "zh-CN",
"createdById": null,
"email": "admin@nocobase.com",
"f_2ytvt3phlp2": null,
"f_3jl554hv7lt": null,
"f_51qityssoq1": null,
"f_dybwctlb233": null,
"f_hbegrnglpv2": null,
"f_ndkyrfvh9il": null,
"f_o33xmbd62fj": null,
"f_t52vqdtfv4h": null,
"f_vak0o8efq4v": [],
"id": 1,
"nickname": "Super Admin",
"phone": null,
"systemSettings": {
"theme": "compact",
"themeId": 1
},
"updatedById": 1,
"username": "nocobase"
}
]
}

View File

@ -0,0 +1,5 @@
export const sleep = async (timeout = 0) => {
return new Promise((resolve) => {
setTimeout(resolve, timeout);
});
};

View File

@ -86,7 +86,6 @@ const defineCommonConfig = () => {
provider: 'istanbul', provider: 'istanbul',
include: ['packages/**/src/**/*.{ts,tsx}'], include: ['packages/**/src/**/*.{ts,tsx}'],
exclude: [ exclude: [
'**/requirejs.ts',
'**/demos/**', '**/demos/**',
'**/swagger/**', '**/swagger/**',
'**/.dumi/**', '**/.dumi/**',

View File

@ -1,4 +1,4 @@
import { css } from '@nocobase/client'; import { DEFAULT_DATA_SOURCE_KEY, css } from '@nocobase/client';
import { Instruction, WorkflowVariableRawTextArea, defaultFieldNames } from '@nocobase/plugin-workflow/client'; import { Instruction, WorkflowVariableRawTextArea, defaultFieldNames } from '@nocobase/plugin-workflow/client';
@ -22,7 +22,7 @@ export default class extends Instruction {
'x-component-props': { 'x-component-props': {
className: 'auto-width', className: 'auto-width',
filter(item) { filter(item) {
return item.options.isDBInstance; return item.options.isDBInstance || item.key === DEFAULT_DATA_SOURCE_KEY;
}, },
}, },
default: 'main', default: 'main',