mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 21:49:25 +08:00
test: add frontend unit test (#3991)
* test: add unit test * fix: build error * fix: exclude requirejs * fix: e2e bug * fix: block delete refresh(T-3936) * fix: add test utils * fix: build bug * fix: remove test only * fix: kanban bug * fix: add more unit tests * fix: coverage bug * fix: update * fix: refactor * fix: add more tests * fix: unit test bug * fix: refactor code * fix: refactor nocobase test * test: add test case --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
17793c2ab9
commit
91254bdf55
@ -4,15 +4,21 @@ import { PkgLog, UserConfig } from './utils';
|
|||||||
import { build as viteBuild } from 'vite';
|
import { build as viteBuild } from 'vite';
|
||||||
import fg from 'fast-glob';
|
import fg from 'fast-glob';
|
||||||
|
|
||||||
|
const clientExt = '.{ts,tsx,js,jsx}';
|
||||||
|
|
||||||
|
function getSingleEntry(file: string, cwd: string) {
|
||||||
|
return fg.sync([`${file}${clientExt}`], { cwd, absolute: true, onlyFiles: true })?.[0]?.replaceAll(/\\/g, '/');
|
||||||
|
}
|
||||||
|
|
||||||
export async function buildEsm(cwd: string, userConfig: UserConfig, sourcemap: boolean = false, log: PkgLog) {
|
export async function buildEsm(cwd: string, userConfig: UserConfig, sourcemap: boolean = false, log: PkgLog) {
|
||||||
log('build esm');
|
log('build esm');
|
||||||
|
|
||||||
const indexEntry = path.join(cwd, 'src/index.ts').replaceAll(/\\/g, '/');
|
const indexEntry = getSingleEntry('src/index', cwd);
|
||||||
const outDir = path.resolve(cwd, 'es');
|
const outDir = path.resolve(cwd, 'es');
|
||||||
|
|
||||||
await build(cwd, indexEntry, outDir, userConfig, sourcemap, log);
|
await build(cwd, indexEntry, outDir, userConfig, sourcemap, log);
|
||||||
|
|
||||||
const clientEntry = fg.sync(['src/client/index.ts', 'src/client.ts'], { cwd, absolute: true, onlyFiles: true })?.[0]?.replaceAll(/\\/g, '/');
|
const clientEntry = getSingleEntry('src/client/index', cwd) || getSingleEntry('src/client', cwd);
|
||||||
const clientOutDir = path.resolve(cwd, 'es/client');
|
const clientOutDir = path.resolve(cwd, 'es/client');
|
||||||
if (clientEntry) {
|
if (clientEntry) {
|
||||||
await build(cwd, clientEntry, clientOutDir, userConfig, sourcemap, log);
|
await build(cwd, clientEntry, clientOutDir, userConfig, sourcemap, log);
|
||||||
@ -20,9 +26,13 @@ export async function buildEsm(cwd: string, userConfig: UserConfig, sourcemap: b
|
|||||||
|
|
||||||
const pkg = require(path.join(cwd, 'package.json'));
|
const pkg = require(path.join(cwd, 'package.json'));
|
||||||
if (pkg.name === '@nocobase/test') {
|
if (pkg.name === '@nocobase/test') {
|
||||||
const e2eEntry = path.join(cwd, 'src/e2e/index.ts').replaceAll(/\\/g, '/');
|
const e2eEntry = getSingleEntry('src/e2e/index', cwd);
|
||||||
const e2eOutDir = path.resolve(cwd, 'es/e2e');
|
const e2eOutDir = path.resolve(cwd, 'es/e2e');
|
||||||
await build(cwd, e2eEntry, e2eOutDir, userConfig, sourcemap, log);
|
await build(cwd, e2eEntry, e2eOutDir, userConfig, sourcemap, log);
|
||||||
|
|
||||||
|
const webEntry = getSingleEntry('src/web/index', cwd);
|
||||||
|
const webOutDir = path.resolve(cwd, 'es/web');
|
||||||
|
await build(cwd, webEntry, webOutDir, userConfig, sourcemap, log);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* defaultShowCode: true
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Action,
|
||||||
|
Application,
|
||||||
|
CardItem,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Grid,
|
||||||
|
Input,
|
||||||
|
SchemaInitializer,
|
||||||
|
SchemaInitializerActionModal,
|
||||||
|
useSchemaInitializer,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { appOptions } from './schema-initializer-common';
|
||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
|
|
||||||
|
const myInitializer = new SchemaInitializer({
|
||||||
|
name: 'MyInitializer',
|
||||||
|
wrap: Grid.wrap,
|
||||||
|
Component: () => {
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
return (
|
||||||
|
<SchemaInitializerActionModal
|
||||||
|
title="Add Card"
|
||||||
|
buttonText="Add Card"
|
||||||
|
onSubmit={({ title }) => {
|
||||||
|
insert({
|
||||||
|
type: 'void',
|
||||||
|
title,
|
||||||
|
'x-decorator': 'CardItem',
|
||||||
|
'x-component': 'Hello',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
schema={{
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Title',
|
||||||
|
required: true,
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
></SchemaInitializerActionModal>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Hello = () => {
|
||||||
|
const schema = useFieldSchema();
|
||||||
|
return <h1>Hello, world! {schema.title}</h1>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = new Application({
|
||||||
|
...appOptions,
|
||||||
|
components: {
|
||||||
|
FormItem,
|
||||||
|
Action,
|
||||||
|
Input,
|
||||||
|
Form,
|
||||||
|
Hello,
|
||||||
|
CardItem,
|
||||||
|
},
|
||||||
|
schemaInitializers: [myInitializer],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app.getRootComponent();
|
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* defaultShowCode: true
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Action,
|
||||||
|
Application,
|
||||||
|
CardItem,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Grid,
|
||||||
|
Input,
|
||||||
|
SchemaInitializer,
|
||||||
|
SchemaInitializerActionModal,
|
||||||
|
useSchemaInitializer,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { appOptions } from './schema-initializer-common';
|
||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
|
|
||||||
|
const myInitializer = new SchemaInitializer({
|
||||||
|
name: 'MyInitializer',
|
||||||
|
title: 'Button Text',
|
||||||
|
wrap: Grid.wrap,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
Component: () => {
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
return (
|
||||||
|
<SchemaInitializerActionModal
|
||||||
|
title="Add Card"
|
||||||
|
buttonText="Add Card"
|
||||||
|
isItem
|
||||||
|
onSubmit={({ title }) => {
|
||||||
|
insert({
|
||||||
|
type: 'void',
|
||||||
|
title,
|
||||||
|
'x-decorator': 'CardItem',
|
||||||
|
'x-component': 'Hello',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
schema={{
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Title',
|
||||||
|
required: true,
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
></SchemaInitializerActionModal>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Hello = () => {
|
||||||
|
const schema = useFieldSchema();
|
||||||
|
return <h1>Hello, world! {schema.title}</h1>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = new Application({
|
||||||
|
...appOptions,
|
||||||
|
components: {
|
||||||
|
FormItem,
|
||||||
|
Action,
|
||||||
|
Input,
|
||||||
|
Form,
|
||||||
|
Hello,
|
||||||
|
CardItem,
|
||||||
|
},
|
||||||
|
schemaInitializers: [myInitializer],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app.getRootComponent();
|
@ -48,15 +48,16 @@ const myInitializer = new SchemaInitializer({
|
|||||||
name: 'c',
|
name: 'c',
|
||||||
Component: () => {
|
Component: () => {
|
||||||
return (
|
return (
|
||||||
<SchemaInitializerItemGroup title="C Group Title">
|
<SchemaInitializerItemGroup
|
||||||
{[
|
title="C Group Title"
|
||||||
|
items={[
|
||||||
{
|
{
|
||||||
name: 'c1',
|
name: 'c1',
|
||||||
type: 'item',
|
type: 'item',
|
||||||
title: 'C1',
|
title: 'C1',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
</SchemaInitializerItemGroup>
|
></SchemaInitializerItemGroup>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* defaultShowCode: true
|
* defaultShowCode: true
|
||||||
*/
|
*/
|
||||||
import { Application, SchemaInitializer } from '@nocobase/client';
|
import { Application, SchemaInitializer, SchemaInitializerSubMenu } from '@nocobase/client';
|
||||||
import { appOptions } from './schema-initializer-common';
|
import { appOptions } from './schema-initializer-common';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
const myInitializer = new SchemaInitializer({
|
const myInitializer = new SchemaInitializer({
|
||||||
name: 'MyInitializer',
|
name: 'MyInitializer',
|
||||||
@ -42,6 +43,24 @@ const myInitializer = new SchemaInitializer({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'c',
|
||||||
|
Component: () => {
|
||||||
|
return (
|
||||||
|
<SchemaInitializerSubMenu
|
||||||
|
name={'c'}
|
||||||
|
title="C subMenu"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
name: 'c1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'C1',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -657,6 +657,18 @@ interface SchemaInitializerSwitchItemProps extends SchemaInitializerItemProps {
|
|||||||
|
|
||||||
<code src="./demos/schema-initializer-components-divider.tsx"></code>
|
<code src="./demos/schema-initializer-components-divider.tsx"></code>
|
||||||
|
|
||||||
|
### `type: 'actionModal'` & SchemaInitializerActionModal
|
||||||
|
|
||||||
|
#### Component Mode
|
||||||
|
|
||||||
|
<code src="./demos/schema-initializer-components-action-modal-1.tsx"></code>
|
||||||
|
|
||||||
|
#### Item Mode
|
||||||
|
|
||||||
|
`SchemaInitializerActionModal` 需要加上 `isItem` 属性
|
||||||
|
|
||||||
|
<code src="./demos/schema-initializer-components-action-modal-2.tsx"></code>
|
||||||
|
|
||||||
## 渲染组件
|
## 渲染组件
|
||||||
|
|
||||||
### SchemaInitializerChildren
|
### SchemaInitializerChildren
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* defaultShowCode: true
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Action,
|
||||||
|
Application,
|
||||||
|
CardItem,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Grid,
|
||||||
|
Input,
|
||||||
|
SchemaInitializer,
|
||||||
|
SchemaInitializerActionModal,
|
||||||
|
useSchemaInitializer,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { appOptions } from './schema-initializer-common';
|
||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
|
|
||||||
|
const myInitializer = new SchemaInitializer({
|
||||||
|
name: 'MyInitializer',
|
||||||
|
wrap: Grid.wrap,
|
||||||
|
Component: () => {
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
return (
|
||||||
|
<SchemaInitializerActionModal
|
||||||
|
title="Add Card"
|
||||||
|
buttonText="Add Card"
|
||||||
|
onSubmit={({ title }) => {
|
||||||
|
insert({
|
||||||
|
type: 'void',
|
||||||
|
title,
|
||||||
|
'x-decorator': 'CardItem',
|
||||||
|
'x-component': 'Hello',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
schema={{
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Title',
|
||||||
|
required: true,
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
></SchemaInitializerActionModal>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Hello = () => {
|
||||||
|
const schema = useFieldSchema();
|
||||||
|
return <h1>Hello, world! {schema.title}</h1>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = new Application({
|
||||||
|
...appOptions,
|
||||||
|
components: {
|
||||||
|
FormItem,
|
||||||
|
Action,
|
||||||
|
Input,
|
||||||
|
Form,
|
||||||
|
Hello,
|
||||||
|
CardItem,
|
||||||
|
},
|
||||||
|
schemaInitializers: [myInitializer],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app.getRootComponent();
|
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* defaultShowCode: true
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Action,
|
||||||
|
Application,
|
||||||
|
CardItem,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Grid,
|
||||||
|
Input,
|
||||||
|
SchemaInitializer,
|
||||||
|
SchemaInitializerActionModal,
|
||||||
|
useSchemaInitializer,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { appOptions } from './schema-initializer-common';
|
||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
|
|
||||||
|
const myInitializer = new SchemaInitializer({
|
||||||
|
name: 'MyInitializer',
|
||||||
|
title: 'Button Text',
|
||||||
|
wrap: Grid.wrap,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
Component: () => {
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
return (
|
||||||
|
<SchemaInitializerActionModal
|
||||||
|
title="Add Card"
|
||||||
|
buttonText="Add Card"
|
||||||
|
isItem
|
||||||
|
onSubmit={({ title }) => {
|
||||||
|
insert({
|
||||||
|
type: 'void',
|
||||||
|
title,
|
||||||
|
'x-decorator': 'CardItem',
|
||||||
|
'x-component': 'Hello',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
schema={{
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Title',
|
||||||
|
required: true,
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
></SchemaInitializerActionModal>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Hello = () => {
|
||||||
|
const schema = useFieldSchema();
|
||||||
|
return <h1>Hello, world! {schema.title}</h1>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = new Application({
|
||||||
|
...appOptions,
|
||||||
|
components: {
|
||||||
|
FormItem,
|
||||||
|
Action,
|
||||||
|
Input,
|
||||||
|
Form,
|
||||||
|
Hello,
|
||||||
|
CardItem,
|
||||||
|
},
|
||||||
|
schemaInitializers: [myInitializer],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app.getRootComponent();
|
@ -48,15 +48,16 @@ const myInitializer = new SchemaInitializer({
|
|||||||
name: 'c',
|
name: 'c',
|
||||||
Component: () => {
|
Component: () => {
|
||||||
return (
|
return (
|
||||||
<SchemaInitializerItemGroup title="C Group Title">
|
<SchemaInitializerItemGroup
|
||||||
{[
|
title="C Group Title"
|
||||||
|
items={[
|
||||||
{
|
{
|
||||||
name: 'c1',
|
name: 'c1',
|
||||||
type: 'item',
|
type: 'item',
|
||||||
title: 'C1',
|
title: 'C1',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
</SchemaInitializerItemGroup>
|
></SchemaInitializerItemGroup>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* defaultShowCode: true
|
* defaultShowCode: true
|
||||||
*/
|
*/
|
||||||
import { Application, SchemaInitializer } from '@nocobase/client';
|
import { Application, SchemaInitializer, SchemaInitializerSubMenu } from '@nocobase/client';
|
||||||
import { appOptions } from './schema-initializer-common';
|
import { appOptions } from './schema-initializer-common';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
const myInitializer = new SchemaInitializer({
|
const myInitializer = new SchemaInitializer({
|
||||||
name: 'MyInitializer',
|
name: 'MyInitializer',
|
||||||
@ -42,6 +43,24 @@ const myInitializer = new SchemaInitializer({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'c',
|
||||||
|
Component: () => {
|
||||||
|
return (
|
||||||
|
<SchemaInitializerSubMenu
|
||||||
|
name={'c'}
|
||||||
|
title="C subMenu"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
name: 'c1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'C1',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -657,6 +657,18 @@ interface SchemaInitializerSwitchItemProps extends SchemaInitializerItemProps {
|
|||||||
|
|
||||||
<code src="./demos/schema-initializer-components-divider.tsx"></code>
|
<code src="./demos/schema-initializer-components-divider.tsx"></code>
|
||||||
|
|
||||||
|
### `type: 'actionModal'` & SchemaInitializerActionModal
|
||||||
|
|
||||||
|
#### Component Mode
|
||||||
|
|
||||||
|
<code src="./demos/schema-initializer-components-action-modal-1.tsx"></code>
|
||||||
|
|
||||||
|
#### Item Mode
|
||||||
|
|
||||||
|
`SchemaInitializerActionModal` 需要加上 `isItem` 属性
|
||||||
|
|
||||||
|
<code src="./demos/schema-initializer-components-action-modal-2.tsx"></code>
|
||||||
|
|
||||||
## 渲染组件
|
## 渲染组件
|
||||||
|
|
||||||
### SchemaInitializerChildren
|
### SchemaInitializerChildren
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import { renderHookWithApp, waitFor } from '@nocobase/test/client';
|
||||||
|
import { CurrentAppInfoProvider, useCurrentAppInfo } from '@nocobase/client';
|
||||||
|
|
||||||
|
describe('CurrentAppInfoProvider', () => {
|
||||||
|
it('should work', async () => {
|
||||||
|
const { result } = await renderHookWithApp({
|
||||||
|
hook: useCurrentAppInfo,
|
||||||
|
Wrapper: CurrentAppInfoProvider,
|
||||||
|
apis: {
|
||||||
|
'app:getInfo': {
|
||||||
|
database: {
|
||||||
|
dialect: 'mysql',
|
||||||
|
},
|
||||||
|
lang: 'zh-CN',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
database: {
|
||||||
|
dialect: 'mysql',
|
||||||
|
},
|
||||||
|
lang: 'zh-CN',
|
||||||
|
version: '1.0.0',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -4,7 +4,7 @@ import { i18n as i18next } from 'i18next';
|
|||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import set from 'lodash/set';
|
import set from 'lodash/set';
|
||||||
import React, { ComponentType, FC, ReactElement } from 'react';
|
import React, { ComponentType, FC, ReactElement, ReactNode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import { Link, NavLink, Navigate } from 'react-router-dom';
|
import { Link, NavLink, Navigate } from 'react-router-dom';
|
||||||
@ -177,7 +177,7 @@ export class Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRouteUrl(pathname: string) {
|
getRouteUrl(pathname: string) {
|
||||||
return this.options.publicPath.replace(/\/$/g, '') + pathname;
|
return this.getPublicPath().replace(/\/$/g, '') + pathname;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCollectionManager(dataSource?: string) {
|
getCollectionManager(dataSource?: string) {
|
||||||
@ -288,8 +288,8 @@ export class Application {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderComponent<T extends {}>(Component: ComponentTypeAndString, props?: T): ReactElement {
|
renderComponent<T extends {}>(Component: ComponentTypeAndString, props?: T, children?: ReactNode): ReactElement {
|
||||||
return React.createElement(this.getComponent(Component), props);
|
return React.createElement(this.getComponent(Component), props, children);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -315,7 +315,9 @@ export class Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRootComponent() {
|
getRootComponent() {
|
||||||
const Root: FC = () => <AppComponent app={this} />;
|
const Root: FC<{ children?: React.ReactNode }> = ({ children }) => (
|
||||||
|
<AppComponent app={this}>{children}</AppComponent>
|
||||||
|
);
|
||||||
return Root;
|
return Root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ export class RouterManager {
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
getRouterComponent() {
|
getRouterComponent(children?: React.ReactNode) {
|
||||||
const { type = 'browser', ...opts } = this.options;
|
const { type = 'browser', ...opts } = this.options;
|
||||||
const Routers = {
|
const Routers = {
|
||||||
hash: HashRouter,
|
hash: HashRouter,
|
||||||
@ -134,6 +134,7 @@ export class RouterManager {
|
|||||||
<ReactRouter {...opts}>
|
<ReactRouter {...opts}>
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<RenderRoutes />
|
<RenderRoutes />
|
||||||
|
{children}
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
</ReactRouter>
|
</ReactRouter>
|
||||||
</RouterContextCleaner>
|
</RouterContextCleaner>
|
||||||
|
@ -6,6 +6,7 @@ import { Link, Outlet } from 'react-router-dom';
|
|||||||
import { describe } from 'vitest';
|
import { describe } from 'vitest';
|
||||||
import { Application } from '../Application';
|
import { Application } from '../Application';
|
||||||
import { Plugin } from '../Plugin';
|
import { Plugin } from '../Plugin';
|
||||||
|
import { useApp } from '../hooks';
|
||||||
|
|
||||||
describe('Application', () => {
|
describe('Application', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@ -18,7 +19,9 @@ describe('Application', () => {
|
|||||||
const router: any = { type: 'memory', initialEntries: ['/'] };
|
const router: any = { type: 'memory', initialEntries: ['/'] };
|
||||||
const initialProvidersLength = 7;
|
const initialProvidersLength = 7;
|
||||||
it('basic', () => {
|
it('basic', () => {
|
||||||
const app = new Application({ router });
|
const options = { router };
|
||||||
|
const app = new Application(options);
|
||||||
|
expect(app.getOptions()).toEqual(options);
|
||||||
expect(app.i18n).toBeDefined();
|
expect(app.i18n).toBeDefined();
|
||||||
expect(app.apiClient).toBeDefined();
|
expect(app.apiClient).toBeDefined();
|
||||||
expect(app.components).toBeDefined();
|
expect(app.components).toBeDefined();
|
||||||
@ -30,6 +33,26 @@ describe('Application', () => {
|
|||||||
expect(Object.keys(app.components).length).toBeGreaterThan(1);
|
expect(Object.keys(app.components).length).toBeGreaterThan(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('publicPath', () => {
|
||||||
|
it('default', () => {
|
||||||
|
const app = new Application({});
|
||||||
|
expect(app.getPublicPath()).toBe('/');
|
||||||
|
expect(app.getRouteUrl('/test')).toBe('/test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('custom', () => {
|
||||||
|
const app = new Application({ publicPath: '/admin' });
|
||||||
|
expect(app.getPublicPath()).toBe('/admin');
|
||||||
|
expect(app.getRouteUrl('/test')).toBe('/admin/test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('custom end with /', () => {
|
||||||
|
const app = new Application({ publicPath: '/admin/' });
|
||||||
|
expect(app.getPublicPath()).toBe('/admin/');
|
||||||
|
expect(app.getRouteUrl('/test')).toBe('/admin/test');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('components', () => {
|
describe('components', () => {
|
||||||
const Hello = () => <div>Hello</div>;
|
const Hello = () => <div>Hello</div>;
|
||||||
Hello.displayName = 'Hello';
|
Hello.displayName = 'Hello';
|
||||||
@ -242,6 +265,30 @@ describe('Application', () => {
|
|||||||
expect(screen.getByText('AboutComponent')).toBeInTheDocument();
|
expect(screen.getByText('AboutComponent')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Root with children', async () => {
|
||||||
|
const app = new Application({ name: 'test' });
|
||||||
|
|
||||||
|
const Demo = () => {
|
||||||
|
const app = useApp();
|
||||||
|
return <div>{app.name}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Root = app.getRootComponent();
|
||||||
|
render(
|
||||||
|
<Root>
|
||||||
|
<Demo />
|
||||||
|
</Root>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('test')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('mount', async () => {
|
it('mount', async () => {
|
||||||
const Hello = () => <div>Hello</div>;
|
const Hello = () => <div>Hello</div>;
|
||||||
const app = new Application({
|
const app = new Application({
|
||||||
|
@ -159,4 +159,21 @@ describe('PluginManager', () => {
|
|||||||
await app.load();
|
await app.load();
|
||||||
expect(app.pm.get('demo')).toBeInstanceOf(DemoPlugin);
|
expect(app.pm.get('demo')).toBeInstanceOf(DemoPlugin);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('i18n', async () => {
|
||||||
|
class DemoPlugin extends Plugin {
|
||||||
|
async load() {
|
||||||
|
expect(this.t('test', { lng: 'zh-CN' })).toBe('测试');
|
||||||
|
expect(this.t('test', { lng: 'en' })).toBe('test');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const app = new Application({ plugins: [[DemoPlugin, { packageName: 'plugin-demo' }]] });
|
||||||
|
app.i18n.addResourceBundle('zh-CN', 'plugin-demo', {
|
||||||
|
test: '测试',
|
||||||
|
});
|
||||||
|
app.i18n.addResourceBundle('en', 'plugin-demo', {
|
||||||
|
test: 'test',
|
||||||
|
});
|
||||||
|
await app.load();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -86,6 +86,32 @@ describe('PluginSettingsManager', () => {
|
|||||||
expect(app.pluginSettingsManager.get('test1').children[0]).toMatchObject(test2);
|
expect(app.pluginSettingsManager.get('test1').children[0]).toMatchObject(test2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('children should be sorted in asc order', () => {
|
||||||
|
const test3 = {
|
||||||
|
title: 'test3 title',
|
||||||
|
sort: 2,
|
||||||
|
Component: () => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const test4 = {
|
||||||
|
title: 'test4 title',
|
||||||
|
sort: 1,
|
||||||
|
Component: () => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
app.pluginSettingsManager.add('test1', test1);
|
||||||
|
app.pluginSettingsManager.add('test1.test2', test2);
|
||||||
|
app.pluginSettingsManager.add('test1.test3', test3);
|
||||||
|
app.pluginSettingsManager.add('test1.test4', test4);
|
||||||
|
|
||||||
|
expect(app.pluginSettingsManager.get('test1').children.length).toBe(3);
|
||||||
|
expect(app.pluginSettingsManager.get('test1').children.map((item) => item.name)).toEqual([
|
||||||
|
'test1.test2',
|
||||||
|
'test1.test4',
|
||||||
|
'test1.test3',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('remove', () => {
|
it('remove', () => {
|
||||||
app.pluginSettingsManager.add('test1', test1);
|
app.pluginSettingsManager.add('test1', test1);
|
||||||
app.pluginSettingsManager.add('test1.test2', test2);
|
app.pluginSettingsManager.add('test1.test2', test2);
|
||||||
@ -121,6 +147,15 @@ describe('PluginSettingsManager', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('no acl', () => {
|
||||||
|
app.pluginSettingsManager.setAclSnippets(['!pm.*']);
|
||||||
|
|
||||||
|
app.pluginSettingsManager.add('test', test);
|
||||||
|
expect(app.pluginSettingsManager.get('test')).toBeFalsy();
|
||||||
|
expect(app.pluginSettingsManager.hasAuth('test')).toBeFalsy();
|
||||||
|
expect(app.pluginSettingsManager.has('test')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
it('getAclSnippet()', () => {
|
it('getAclSnippet()', () => {
|
||||||
app.pluginSettingsManager.add('test1', test1);
|
app.pluginSettingsManager.add('test1', test1);
|
||||||
app.pluginSettingsManager.add('test2', {
|
app.pluginSettingsManager.add('test2', {
|
||||||
|
@ -188,6 +188,7 @@ describe('Router', () => {
|
|||||||
const RouterComponent = router.getRouterComponent();
|
const RouterComponent = router.getRouterComponent();
|
||||||
render(<RouterComponent />);
|
render(<RouterComponent />);
|
||||||
expect(screen.queryByTestId('content')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('content')).not.toBeInTheDocument();
|
||||||
|
expect(router.getBasename()).toBe('/admin');
|
||||||
|
|
||||||
window.location.hash = '#/admin';
|
window.location.hash = '#/admin';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
import { render } from '@nocobase/test/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { SchemaComponent, SchemaComponentProvider } from '../../../schema-component';
|
import { SchemaComponent, SchemaComponentProvider } from '../../../schema-component';
|
||||||
import { render } from '@nocobase/test/client';
|
|
||||||
import { withDynamicSchemaProps } from '../../hoc';
|
import { withDynamicSchemaProps } from '../../hoc';
|
||||||
|
|
||||||
const HelloComponent = withDynamicSchemaProps((props: any) => (
|
const HelloComponent = withDynamicSchemaProps((props: any) => (
|
||||||
@ -213,4 +213,41 @@ describe('withDynamicSchemaProps', () => {
|
|||||||
const { getByTestId } = render(<Demo />);
|
const { getByTestId } = render(<Demo />);
|
||||||
expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ b: 'b' }));
|
expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ b: 'b' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('override scope', () => {
|
||||||
|
function useDecoratorProps() {
|
||||||
|
return {
|
||||||
|
b: 'b',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function useDecoratorProps2() {
|
||||||
|
return {
|
||||||
|
c: 'c',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const schema = {
|
||||||
|
'x-use-decorator-props': 'cm.useDecoratorProps',
|
||||||
|
};
|
||||||
|
const scopes = { cm: { useDecoratorProps } };
|
||||||
|
const Demo = function () {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ HelloComponent, HelloDecorator }} scope={scopes}>
|
||||||
|
<SchemaComponent
|
||||||
|
scope={{
|
||||||
|
cm: { useDecoratorProps: useDecoratorProps2 },
|
||||||
|
}}
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
name: 'hello',
|
||||||
|
'x-component': 'HelloComponent',
|
||||||
|
'x-decorator': 'HelloDecorator',
|
||||||
|
...schema,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const { getByTestId } = render(<Demo />);
|
||||||
|
expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ c: 'c' }));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
import { SchemaInitializer } from '@nocobase/client';
|
||||||
|
|
||||||
|
describe('SchemaInitializer', () => {
|
||||||
|
let initializer: SchemaInitializer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
initializer = new SchemaInitializer({ name: 'test' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initializes with default options if none are provided', () => {
|
||||||
|
expect(initializer).toBeDefined();
|
||||||
|
expect(initializer.name).toEqual('test');
|
||||||
|
expect(initializer.items).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds an item', () => {
|
||||||
|
initializer.add('item1', { title: 'title' });
|
||||||
|
expect(initializer.items).toEqual([{ name: 'item1', title: 'title' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updates an item if it already exists', () => {
|
||||||
|
initializer.add('item1', { title: 'title' });
|
||||||
|
initializer.add('item1', { title: 'new title' });
|
||||||
|
expect(initializer.items).toEqual([{ name: 'item1', title: 'new title' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds a nested item', () => {
|
||||||
|
initializer.add('parent', { title: 'parent title' });
|
||||||
|
initializer.add('parent.item1', { title: 'item1 title' });
|
||||||
|
expect(initializer.get('parent')).toEqual({
|
||||||
|
name: 'parent',
|
||||||
|
title: 'parent title',
|
||||||
|
children: [{ name: 'item1', title: 'item1 title' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updates a nested item if it already exists', () => {
|
||||||
|
initializer.add('parent', { title: 'parent title' });
|
||||||
|
initializer.add('parent.item1', { title: 'title' });
|
||||||
|
initializer.add('parent.item1', { title: 'new title' });
|
||||||
|
expect(initializer.get('parent')).toEqual({
|
||||||
|
name: 'parent',
|
||||||
|
title: 'parent title',
|
||||||
|
children: [{ name: 'item1', title: 'new title' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gets an item', () => {
|
||||||
|
initializer.add('item1', { title: 'title' });
|
||||||
|
expect(initializer.get('item1')).toEqual({ name: 'item1', title: 'title' });
|
||||||
|
expect(initializer.get('no-exist')).toEqual(undefined);
|
||||||
|
expect(initializer.get(undefined)).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gets a nested item', () => {
|
||||||
|
initializer.add('parent', { title: 'parent title' });
|
||||||
|
initializer.add('parent.item1', { title: 'title' });
|
||||||
|
expect(initializer.get('parent.item1')).toEqual({
|
||||||
|
name: 'item1',
|
||||||
|
title: 'title',
|
||||||
|
});
|
||||||
|
expect(initializer.get('parent.item1.not-exist')).toEqual(undefined);
|
||||||
|
expect(initializer.get('parent.not-exist')).toEqual(undefined);
|
||||||
|
expect(initializer.get('parent.not-exist.not-exist')).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns undefined for a non-existent item', () => {
|
||||||
|
expect(initializer.get('nonExistent')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removes an item', () => {
|
||||||
|
initializer.add('item1', { title: 'title' });
|
||||||
|
initializer.remove('item1');
|
||||||
|
expect(initializer.items).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removes a nested item', () => {
|
||||||
|
initializer.add('parent', { title: 'title' });
|
||||||
|
initializer.add('parent.item1', { title: 'title' });
|
||||||
|
initializer.remove('parent');
|
||||||
|
expect(initializer.get('parent')).toEqual(undefined);
|
||||||
|
expect(initializer.get('parent.item1')).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removes a nested child item', () => {
|
||||||
|
initializer.add('parent', { title: 'title' });
|
||||||
|
initializer.add('parent.item1', { title: 'title' });
|
||||||
|
initializer.remove('parent.item1');
|
||||||
|
expect(initializer.get('parent')).toBeDefined();
|
||||||
|
expect(initializer.get('parent.item1')).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does nothing when removing a non-existent item', () => {
|
||||||
|
const initialItems = [...initializer.items];
|
||||||
|
initializer.remove('nonExistent');
|
||||||
|
expect(initializer.items).toEqual(initialItems);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,89 @@
|
|||||||
|
import { SchemaInitializer, Application } from '@nocobase/client';
|
||||||
|
|
||||||
|
describe('SchemaInitializerManager', () => {
|
||||||
|
let app: Application;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
app = new Application();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add schema initializers correctly', () => {
|
||||||
|
const demo = new SchemaInitializer({ name: 'test' });
|
||||||
|
app.schemaInitializerManager.add(demo);
|
||||||
|
|
||||||
|
expect(app.schemaInitializerManager.has('test')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle addition of items to non-existent schema initializers', () => {
|
||||||
|
app.schemaInitializerManager.addItem('test', 'item1', {});
|
||||||
|
expect(app.schemaInitializerManager.has('test')).toBeFalsy();
|
||||||
|
|
||||||
|
const demo = new SchemaInitializer({ name: 'test' });
|
||||||
|
app.schemaInitializerManager.add(demo);
|
||||||
|
expect(app.schemaInitializerManager.has('test')).toBeTruthy();
|
||||||
|
const demoRes = app.schemaInitializerManager.get('test');
|
||||||
|
expect(demoRes.get('item1')).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add item to existing schema initializer', () => {
|
||||||
|
const mockInitializer = new SchemaInitializer({ name: 'test' });
|
||||||
|
const fn = vitest.spyOn(mockInitializer, 'add');
|
||||||
|
app.schemaInitializerManager.add(mockInitializer);
|
||||||
|
|
||||||
|
app.schemaInitializerManager.addItem('test', 'item1', {});
|
||||||
|
expect(fn).toHaveBeenCalledWith('item1', {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a specific schema initializer when it exists', () => {
|
||||||
|
const mockInitializer = new SchemaInitializer({ name: 'test' });
|
||||||
|
app.schemaInitializerManager.add(mockInitializer);
|
||||||
|
|
||||||
|
const fetchedInitializer = app.schemaInitializerManager.get('test');
|
||||||
|
expect(fetchedInitializer).toBe(mockInitializer);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when schema initializer does not exist', () => {
|
||||||
|
const fetchedInitializer = app.schemaInitializerManager.get('nonExistent');
|
||||||
|
expect(fetchedInitializer).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all schema initializers', () => {
|
||||||
|
const mockInitializer1 = new SchemaInitializer({ name: 'test1' });
|
||||||
|
const mockInitializer2 = new SchemaInitializer({ name: 'test2' });
|
||||||
|
app.schemaInitializerManager.add(mockInitializer1, mockInitializer2);
|
||||||
|
|
||||||
|
const initializers = app.schemaInitializerManager.getAll();
|
||||||
|
expect(initializers).toEqual({
|
||||||
|
test1: mockInitializer1,
|
||||||
|
test2: mockInitializer2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly check if schema initializer exists', () => {
|
||||||
|
const mockInitializer = new SchemaInitializer({ name: 'test' });
|
||||||
|
app.schemaInitializerManager.add(mockInitializer);
|
||||||
|
|
||||||
|
expect(app.schemaInitializerManager.has('test')).toBeTruthy();
|
||||||
|
expect(app.schemaInitializerManager.has('nonExistent')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove schema initializer', () => {
|
||||||
|
const mockInitializer = new SchemaInitializer({ name: 'test' });
|
||||||
|
app.schemaInitializerManager.add(mockInitializer);
|
||||||
|
|
||||||
|
app.schemaInitializerManager.remove('test');
|
||||||
|
expect(app.schemaInitializerManager.has('test')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle removal of items for non-existent schema initializers', () => {
|
||||||
|
expect(() => app.schemaInitializerManager.removeItem('nonExistent', 'item1')).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove item from existing schema initializer', () => {
|
||||||
|
const mockInitializer = new SchemaInitializer({ name: 'test' });
|
||||||
|
const fn = vitest.spyOn(mockInitializer, 'remove');
|
||||||
|
app.schemaInitializerManager.add(mockInitializer);
|
||||||
|
app.schemaInitializerManager.removeItem('test', 'item1');
|
||||||
|
expect(fn).toHaveBeenCalledWith('item1');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,118 @@
|
|||||||
|
import { screen, userEvent, waitFor } from '@nocobase/test/client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Action, Form, FormItem, Input, SchemaInitializerActionModal } from '@nocobase/client';
|
||||||
|
|
||||||
|
import { createApp } from '../fixures/createApp';
|
||||||
|
import { createAndHover } from './fixtures/createAppAndHover';
|
||||||
|
|
||||||
|
describe('SchemaInitializerDivider', () => {
|
||||||
|
it('component mode', async () => {
|
||||||
|
const onSubmit = vitest.fn();
|
||||||
|
const Demo = () => {
|
||||||
|
return (
|
||||||
|
<SchemaInitializerActionModal
|
||||||
|
title="Modal title"
|
||||||
|
buttonText="button text"
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
schema={{
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Title',
|
||||||
|
required: true,
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
></SchemaInitializerActionModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await createApp(
|
||||||
|
{
|
||||||
|
Component: Demo,
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
FormItem,
|
||||||
|
Action,
|
||||||
|
Input,
|
||||||
|
Form,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('button text')).toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByText('button text'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Modal title')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.type(screen.getByRole('textbox'), 'test');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('textbox')).toHaveValue('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Submit'));
|
||||||
|
|
||||||
|
expect(onSubmit).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('item mode', async () => {
|
||||||
|
const onSubmit = vitest.fn();
|
||||||
|
const Demo = () => {
|
||||||
|
return (
|
||||||
|
<SchemaInitializerActionModal
|
||||||
|
title="Modal title"
|
||||||
|
buttonText="button text"
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
isItem
|
||||||
|
schema={{
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Title',
|
||||||
|
required: true,
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
></SchemaInitializerActionModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await createAndHover(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
Component: Demo,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
FormItem,
|
||||||
|
Action,
|
||||||
|
Input,
|
||||||
|
Form,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('button text')).toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByText('button text'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Modal title')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.type(screen.getByRole('textbox'), 'test');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('textbox')).toHaveValue('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Submit'));
|
||||||
|
|
||||||
|
expect(onSubmit).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,353 @@
|
|||||||
|
import { screen, userEvent } from '@nocobase/test/client';
|
||||||
|
|
||||||
|
import { SchemaInitializerItem, useSchemaInitializerItem } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createAndHover } from './fixtures/createAppAndHover';
|
||||||
|
|
||||||
|
describe('SchemaInitializerChildren', () => {
|
||||||
|
it('basic', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item2',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item2')).toBeInTheDocument();
|
||||||
|
expect(document.body.querySelector('.ant-popover-content').textContent).toBe('Item1Item2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sort', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item1',
|
||||||
|
sort: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item2',
|
||||||
|
sort: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item2')).toBeInTheDocument();
|
||||||
|
expect(document.body.querySelector('.ant-popover-content').textContent).toBe('Item2Item1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('component', async () => {
|
||||||
|
await createAndHover(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item1',
|
||||||
|
// 小写
|
||||||
|
component: () => {
|
||||||
|
return <div>Item1</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item2',
|
||||||
|
// 大写
|
||||||
|
Component: () => {
|
||||||
|
return <div>Item2</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item3',
|
||||||
|
Component: 'Item3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item4',
|
||||||
|
Component: 'not-exists',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
Item3: () => {
|
||||||
|
return <div>Item3</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item2')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item3')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('useVisible()', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item1',
|
||||||
|
useVisible() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item2',
|
||||||
|
useVisible() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item2')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('children', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: 'A',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(screen.queryByText('A')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('useChildren()', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: 'A',
|
||||||
|
useChildren() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(screen.queryByText('A')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge `children` and `useChildren()`', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: 'A',
|
||||||
|
useChildren() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'item3',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item3',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(screen.queryByText('A')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item2')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item3')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hideIfNoChildren', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: 'A',
|
||||||
|
hideIfNoChildren: true,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'b',
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: 'B',
|
||||||
|
hideIfNoChildren: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'c',
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: 'C',
|
||||||
|
hideIfNoChildren: true,
|
||||||
|
useChildren() {
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'd',
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: 'D',
|
||||||
|
hideIfNoChildren: true,
|
||||||
|
useChildren() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(screen.queryByText('A')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('B')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('C')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('D')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('componentProps', async () => {
|
||||||
|
await createAndHover(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
Component: 'CommonDemo',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
CommonDemo: (props) => {
|
||||||
|
return <SchemaInitializerItem title={props.title} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('useComponentProps', async () => {
|
||||||
|
await createAndHover(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
Component: 'CommonDemo',
|
||||||
|
useComponentProps() {
|
||||||
|
return {
|
||||||
|
title: 'Item1',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
CommonDemo: (props) => {
|
||||||
|
return <SchemaInitializerItem title={props.title} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge `componentProps` and `useComponentProps()`', async () => {
|
||||||
|
const onClick = vitest.fn();
|
||||||
|
await createAndHover(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
Component: 'CommonDemo',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
useComponentProps() {
|
||||||
|
return {
|
||||||
|
onClick,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
CommonDemo: (props) => {
|
||||||
|
return <SchemaInitializerItem title={props.title} onClick={props.onClick} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Item1'));
|
||||||
|
expect(onClick).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
item: expect.objectContaining({
|
||||||
|
title: 'Item1',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('public props', async () => {
|
||||||
|
await createAndHover(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
Component: 'CommonDemo',
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
CommonDemo: () => {
|
||||||
|
const { title } = useSchemaInitializerItem();
|
||||||
|
return <SchemaInitializerItem title={title} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,28 @@
|
|||||||
|
import { screen } from '@nocobase/test/client';
|
||||||
|
|
||||||
|
import { createAndHover } from './fixtures/createAppAndHover';
|
||||||
|
|
||||||
|
describe('SchemaInitializerDivider', () => {
|
||||||
|
it('basic', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
type: 'item',
|
||||||
|
title: 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
name: 'divider1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'b',
|
||||||
|
type: 'item',
|
||||||
|
title: 'B',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(screen.queryByText('A')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('B')).toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.ant-divider')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,80 @@
|
|||||||
|
import { screen, userEvent, sleep, act } from '@nocobase/test/client';
|
||||||
|
|
||||||
|
import { SchemaInitializerItem } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createAndHover } from './fixtures/createAppAndHover';
|
||||||
|
|
||||||
|
describe('SchemaInitializerItem', () => {
|
||||||
|
it('basic', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
Component: () => {
|
||||||
|
return <SchemaInitializerItem title="Item2" />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item3',
|
||||||
|
icon: 'ApiOutlined',
|
||||||
|
title: 'Item3',
|
||||||
|
type: 'item',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item2')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item3')).toBeInTheDocument();
|
||||||
|
expect(document.querySelector('svg')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('items', async () => {
|
||||||
|
const fn = vitest.fn();
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'Item1',
|
||||||
|
onClick: fn,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'aaa',
|
||||||
|
value: 'aaa',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'bbb',
|
||||||
|
value: 'bbb',
|
||||||
|
icon: 'ApiOutlined',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
await act(async () => {
|
||||||
|
await user.hover(screen.getByText('Item1'));
|
||||||
|
await sleep(300);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByText('aaa')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('bbb')).toBeInTheDocument();
|
||||||
|
expect(document.querySelector('svg')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.click(screen.getByText('aaa'));
|
||||||
|
await sleep(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fn).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
item: {
|
||||||
|
label: 'aaa',
|
||||||
|
value: 'aaa',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,83 @@
|
|||||||
|
import { screen } from '@nocobase/test/client';
|
||||||
|
|
||||||
|
import { SchemaInitializerItemGroup } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createAndHover } from './fixtures/createAppAndHover';
|
||||||
|
|
||||||
|
describe('SchemaInitializerItemGroup', () => {
|
||||||
|
it('basic', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
title: 'A Group Title',
|
||||||
|
type: 'itemGroup',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'a1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'A1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'b',
|
||||||
|
title: 'B Group Title',
|
||||||
|
type: 'itemGroup',
|
||||||
|
divider: true,
|
||||||
|
useChildren() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'b1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'B1',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'c',
|
||||||
|
Component: () => {
|
||||||
|
return (
|
||||||
|
<SchemaInitializerItemGroup title="C Group Title" divider>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
name: 'c1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'C1',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
</SchemaInitializerItemGroup>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'd',
|
||||||
|
Component: () => {
|
||||||
|
return (
|
||||||
|
<SchemaInitializerItemGroup
|
||||||
|
title="D Group Title"
|
||||||
|
divider
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
name: 'd1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'D1',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
></SchemaInitializerItemGroup>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(screen.queryByText('A Group Title')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('A1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('B Group Title')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('B1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('C Group Title')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('C1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('D Group Title')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('D1')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(document.querySelectorAll('.ant-divider')).toHaveLength(3);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,149 @@
|
|||||||
|
import { screen, userEvent, waitFor } from '@nocobase/test/client';
|
||||||
|
|
||||||
|
import { SchemaInitializerSubMenu } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createAndHover } from './fixtures/createAppAndHover';
|
||||||
|
|
||||||
|
describe('SchemaInitializerSubMenu', () => {
|
||||||
|
async function valid(onClick) {
|
||||||
|
expect(screen.getByText('A Title')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.hover(screen.getByText('A Title'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('A1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('A2')).not.toBeInTheDocument();
|
||||||
|
expect(document.querySelector('svg')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('A1'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onClick).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
item: expect.objectContaining({
|
||||||
|
name: 'a1',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('basic', async () => {
|
||||||
|
const onClick = vitest.fn();
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
title: 'A Title',
|
||||||
|
type: 'subMenu',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'a1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'A1',
|
||||||
|
icon: 'ApiOutlined',
|
||||||
|
onClick: onClick,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'a2',
|
||||||
|
type: 'item',
|
||||||
|
title: 'A2',
|
||||||
|
useVisible() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await valid(onClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('component mode', async () => {
|
||||||
|
const onClick = vitest.fn();
|
||||||
|
const TestDemo = () => {
|
||||||
|
return (
|
||||||
|
<SchemaInitializerSubMenu
|
||||||
|
name="a"
|
||||||
|
title="A Title"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
name: 'a1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'A1',
|
||||||
|
onClick: onClick,
|
||||||
|
useVisible() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'a2',
|
||||||
|
type: 'item',
|
||||||
|
title: 'A2',
|
||||||
|
useVisible() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
></SchemaInitializerSubMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await createAndHover(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
Component: 'TestDemo',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
TestDemo,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await valid(onClick);
|
||||||
|
});
|
||||||
|
test('component children mode', async () => {
|
||||||
|
const onClick = vitest.fn();
|
||||||
|
const TestDemo = () => {
|
||||||
|
return (
|
||||||
|
<SchemaInitializerSubMenu name="a" title="A Title">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
name: 'a1',
|
||||||
|
type: 'item',
|
||||||
|
title: 'A1',
|
||||||
|
onClick: onClick,
|
||||||
|
useVisible() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'a2',
|
||||||
|
type: 'item',
|
||||||
|
title: 'A2',
|
||||||
|
useVisible() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
</SchemaInitializerSubMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await createAndHover(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
Component: 'TestDemo',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
TestDemo,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await valid(onClick);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,103 @@
|
|||||||
|
import { screen, userEvent, waitFor } from '@nocobase/test/client';
|
||||||
|
|
||||||
|
import { useSchemaInitializer, useCurrentSchema, SchemaInitializerSwitch } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createAndHover } from './fixtures/createAppAndHover';
|
||||||
|
|
||||||
|
describe('SchemaInitializerSwitch', () => {
|
||||||
|
async function valid() {
|
||||||
|
expect(screen.getByText('A Title')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByRole('switch').getAttribute('aria-checked')).toBe('false');
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('A Title'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('A-Content')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('switch').getAttribute('aria-checked')).toBe('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('A Title'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('A-Content')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('switch').getAttribute('aria-checked')).toBe('false');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionKey = 'x-action';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'void',
|
||||||
|
[actionKey]: 'create',
|
||||||
|
title: "{{t('Add New')}}",
|
||||||
|
'x-component': 'div',
|
||||||
|
'x-content': 'A-Content',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('component mode', async () => {
|
||||||
|
const AddNewButton = () => {
|
||||||
|
// 判断是否已插入
|
||||||
|
const { exists, remove } = useCurrentSchema(schema[actionKey], actionKey);
|
||||||
|
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
return (
|
||||||
|
<SchemaInitializerSwitch
|
||||||
|
checked={exists}
|
||||||
|
title={'A Title'}
|
||||||
|
onClick={() => {
|
||||||
|
// 如果已插入,则移除
|
||||||
|
if (exists) {
|
||||||
|
return remove();
|
||||||
|
}
|
||||||
|
// 新插入子节点
|
||||||
|
insert(schema);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await createAndHover(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
Component: AddNewButton,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
AddNewButton,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await valid();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type mode', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
type: 'switch',
|
||||||
|
useComponentProps() {
|
||||||
|
const { exists, remove } = useCurrentSchema(schema[actionKey], actionKey);
|
||||||
|
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
return {
|
||||||
|
checked: exists,
|
||||||
|
title: 'A Title',
|
||||||
|
onClick() {
|
||||||
|
// 如果已插入,则移除
|
||||||
|
if (exists) {
|
||||||
|
return remove();
|
||||||
|
}
|
||||||
|
// 新插入子节点
|
||||||
|
insert(schema);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await valid();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,13 @@
|
|||||||
|
import { screen, userEvent, waitFor } from '@nocobase/test/client';
|
||||||
|
|
||||||
|
import { createApp } from '../../fixures/createApp';
|
||||||
|
import { SchemaInitializerItemType } from '@nocobase/client';
|
||||||
|
|
||||||
|
export async function createAndHover(items: SchemaInitializerItemType[], appOptions: any = {}) {
|
||||||
|
await createApp({ items }, appOptions);
|
||||||
|
await userEvent.hover(screen.getByText('Test'));
|
||||||
|
|
||||||
|
await waitFor(async () => {
|
||||||
|
expect(screen.queryByRole('tooltip')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { observer } from '@formily/reactive-react';
|
||||||
|
import { renderApp } from '@nocobase/test/client';
|
||||||
|
import {
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaInitializer,
|
||||||
|
useSchemaInitializer,
|
||||||
|
useSchemaInitializerItem,
|
||||||
|
SchemaInitializerItem,
|
||||||
|
useSchemaInitializerRender,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
|
||||||
|
export async function createApp(options = {}, appOptions = {}) {
|
||||||
|
const testInitializers = new SchemaInitializer({
|
||||||
|
name: 'test',
|
||||||
|
title: 'Test',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'demo',
|
||||||
|
title: 'Item',
|
||||||
|
Component: () => {
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
const { title } = useSchemaInitializerItem();
|
||||||
|
const handleClick = () => {
|
||||||
|
insert({
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'div',
|
||||||
|
'x-content': 'Hello World!',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return <SchemaInitializerItem title={title} onClick={handleClick} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
const AddBlockButton = observer(() => {
|
||||||
|
const { render } = useSchemaInitializerRender('test');
|
||||||
|
|
||||||
|
return <div data-testid="render">{render()}</div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const WrapDemo = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>WrapDemo</h1>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Page = observer(
|
||||||
|
(props) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{props.children}
|
||||||
|
<AddBlockButton />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ displayName: 'Page' },
|
||||||
|
);
|
||||||
|
|
||||||
|
const Root = () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
components={{ Page, WrapDemo }}
|
||||||
|
schema={{
|
||||||
|
name: 'root',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Page',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await renderApp({
|
||||||
|
appOptions: {
|
||||||
|
providers: [Root],
|
||||||
|
schemaInitializers: [testInitializers],
|
||||||
|
designable: true,
|
||||||
|
...appOptions,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import { useAriaAttributeOfMenuItem, SchemaInitializerMenuProvider } from '@nocobase/client';
|
||||||
|
|
||||||
|
describe('useAriaAttributeOfMenuItem', () => {
|
||||||
|
test('should return attribute with role "menuitem" when not in menu', () => {
|
||||||
|
const { result } = renderHook(() => useAriaAttributeOfMenuItem());
|
||||||
|
expect(result.current.attribute).toEqual({ role: 'menuitem' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty attribute when in menu', () => {
|
||||||
|
const { result } = renderHook(() => useAriaAttributeOfMenuItem(), {
|
||||||
|
wrapper: SchemaInitializerMenuProvider,
|
||||||
|
});
|
||||||
|
expect(result.current.attribute).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,319 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react-hooks';
|
||||||
|
import { useGetSchemaInitializerMenuItems, useSchemaInitializerMenuItems } from '@nocobase/client'; // Adjust the import according to the file structure
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
describe('useGetSchemaInitializerMenuItems', () => {
|
||||||
|
const mockOnClick = vitest.fn();
|
||||||
|
it('should compile and return an empty array if items are empty', () => {
|
||||||
|
const { result } = renderHook(() => useGetSchemaInitializerMenuItems(mockOnClick));
|
||||||
|
act(() => {
|
||||||
|
const menuItems = result.current([], 'parent-0');
|
||||||
|
expect(menuItems).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process and return compiled items with a divider', () => {
|
||||||
|
const items = [{ type: 'divider' }];
|
||||||
|
const { result } = renderHook(() => useGetSchemaInitializerMenuItems(mockOnClick));
|
||||||
|
act(() => {
|
||||||
|
const menuItems = result.current(items, 'parent-1');
|
||||||
|
expect(menuItems).toEqual([{ type: 'divider', key: 'divider-0' }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns items and click', () => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
title: 'item1',
|
||||||
|
onClick: vitest.fn(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'item2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'item3',
|
||||||
|
associationField: 'a.b',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'item4',
|
||||||
|
key: 'item4',
|
||||||
|
component: () => <div>item4</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'item5',
|
||||||
|
component: () => <div>item5</div>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useGetSchemaInitializerMenuItems(mockOnClick));
|
||||||
|
act(() => {
|
||||||
|
const menuItems = result.current(items, 'parent-2');
|
||||||
|
menuItems[0].onClick({ domEvent: { stopPropagation: vitest.fn() }, key: menuItems[0].key });
|
||||||
|
|
||||||
|
expect(items[0].onClick).toHaveBeenCalled();
|
||||||
|
expect(result.current(items, 'parent-2')).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "parent-2-item1-0",
|
||||||
|
"label": "item1",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "parent-2-item2-1",
|
||||||
|
"label": "item2",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"associationField": "a.b",
|
||||||
|
"key": "parent-2-item3-2",
|
||||||
|
"label": "item3",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "item4",
|
||||||
|
"label": <Memo(SchemaInitializerChild)
|
||||||
|
component={[Function]}
|
||||||
|
label="item4"
|
||||||
|
title="item4"
|
||||||
|
type="item"
|
||||||
|
/>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "item5-4",
|
||||||
|
"label": <Memo(SchemaInitializerChild)
|
||||||
|
component={[Function]}
|
||||||
|
label="item5"
|
||||||
|
title="item5"
|
||||||
|
type="item"
|
||||||
|
/>,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
expect(mockOnClick).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
menuItems[1].onClick({ domEvent: { stopPropagation: vitest.fn() }, key: menuItems[1].key });
|
||||||
|
expect(mockOnClick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles item group with children', () => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: 'Group',
|
||||||
|
key: 'group-0',
|
||||||
|
children: [{ type: 'item', title: 'Item 1' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: () => <div>123</div>,
|
||||||
|
children: [{ type: 'item', title: 'Item 1' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: 'Group3',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const { result } = renderHook(() => useGetSchemaInitializerMenuItems());
|
||||||
|
act(() => {
|
||||||
|
const menuItems = result.current(items, 'parent');
|
||||||
|
expect(menuItems).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"key": "group-0-Item 1-0",
|
||||||
|
"label": "Item 1",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"key": "group-0",
|
||||||
|
"label": "Group",
|
||||||
|
"type": "group",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"key": "parent-item-group-1-Item 1-0",
|
||||||
|
"label": "Item 1",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"key": "parent-item-group-1",
|
||||||
|
"label": [Function],
|
||||||
|
"type": "group",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"key": "parent-item-group-2",
|
||||||
|
"label": "Group3",
|
||||||
|
"type": "group",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles subMenu with compiled title and children', () => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
type: 'subMenu',
|
||||||
|
title: 'SubMenu1',
|
||||||
|
name: 'submenu-1',
|
||||||
|
children: [{ type: 'item', title: 'SubItem 1' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'subMenu',
|
||||||
|
title: 'SubMenu2',
|
||||||
|
name: 'submenu-2',
|
||||||
|
children: [{ type: 'item', title: 'SubItem 1' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'subMenu',
|
||||||
|
title: 'SubMenu3',
|
||||||
|
key: 'submenu-3',
|
||||||
|
children: [{ type: 'item', title: 'SubItem 1' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'subMenu',
|
||||||
|
title: 'SubMenu4',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const { result } = renderHook(() => useGetSchemaInitializerMenuItems());
|
||||||
|
act(() => {
|
||||||
|
const menuItems = result.current(items, 'parent');
|
||||||
|
expect(menuItems).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"key": "submenu-1-SubItem 1-0",
|
||||||
|
"label": "SubItem 1",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"key": "submenu-1",
|
||||||
|
"label": "SubMenu1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"key": "submenu-2-SubItem 1-0",
|
||||||
|
"label": "SubItem 1",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"key": "submenu-2",
|
||||||
|
"label": "SubMenu2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"key": "submenu-3-SubItem 1-0",
|
||||||
|
"label": "SubItem 1",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"key": "submenu-3",
|
||||||
|
"label": "SubMenu3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"key": "parent-sub-menu-3",
|
||||||
|
"label": "SubMenu4",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('processes items with isMenuType property and excludes it in the return', () => {
|
||||||
|
const items = [{ isMenuType: true, type: 'item', title: 'Special Item', key: 'special-0' }];
|
||||||
|
const { result } = renderHook(() => useGetSchemaInitializerMenuItems(mockOnClick));
|
||||||
|
act(() => {
|
||||||
|
const menuItems = result.current(items, 'parent');
|
||||||
|
expect(menuItems).toEqual([
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
title: 'Special Item',
|
||||||
|
key: 'special-0',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns items with associationField', () => {
|
||||||
|
const items = [{ type: 'item', title: 'Item w/ Association', associationField: 'field-1' }];
|
||||||
|
const { result } = renderHook(() => useGetSchemaInitializerMenuItems(mockOnClick));
|
||||||
|
act(() => {
|
||||||
|
const menuItems = result.current(items, 'parent');
|
||||||
|
expect(menuItems[0]).toHaveProperty('associationField', 'field-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useSchemaInitializerMenuItems', () => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
key: 1,
|
||||||
|
label: 'item1',
|
||||||
|
onClick: vitest.fn(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
key: 2,
|
||||||
|
label: 'item2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
it('should call useGetSchemaInitializerMenuItems with provided items and name', () => {
|
||||||
|
const name = 'TestName';
|
||||||
|
const { result } = renderHook(() => useSchemaInitializerMenuItems(items, name));
|
||||||
|
expect(result.current).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": 1,
|
||||||
|
"label": "item1",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 2,
|
||||||
|
"label": "item2",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should recompute getMenuItems when items or name changes', () => {
|
||||||
|
const { rerender, result } = renderHook(({ items, name }) => useSchemaInitializerMenuItems(items, name), {
|
||||||
|
initialProps: { items, name: 'InitialName' },
|
||||||
|
});
|
||||||
|
const res1 = result.current;
|
||||||
|
rerender({ items, name: 'NewName' });
|
||||||
|
const res2 = result.current;
|
||||||
|
|
||||||
|
expect(res1).not.toEqual(res2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle onClick event properly', () => {
|
||||||
|
const mockOnClick = vitest.fn();
|
||||||
|
const name = 'TestName';
|
||||||
|
|
||||||
|
renderHook(() => useSchemaInitializerMenuItems(items, name, mockOnClick));
|
||||||
|
expect(mockOnClick).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
items[0].onClick();
|
||||||
|
items[0].onClick({ domEvent: { stopPropagation: vitest.fn() }, key: items[0].key });
|
||||||
|
|
||||||
|
expect(items[0].onClick).toHaveBeenCalled();
|
||||||
|
expect(mockOnClick).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,101 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Application, SchemaInitializer, useSchemaInitializerRender } from '@nocobase/client';
|
||||||
|
import { render, waitFor, screen } from '@nocobase/test/client';
|
||||||
|
|
||||||
|
describe('useSchemaInitializerRender', () => {
|
||||||
|
async function createApp(DemoComponent: any) {
|
||||||
|
const testInitializers = new SchemaInitializer({
|
||||||
|
name: 'test',
|
||||||
|
title: 'Test',
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = new Application({
|
||||||
|
providers: [DemoComponent],
|
||||||
|
schemaInitializers: [testInitializers],
|
||||||
|
designable: true,
|
||||||
|
});
|
||||||
|
const Root = app.getRootComponent();
|
||||||
|
render(<Root />);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should return exists as false and render as null when name is not provided', async () => {
|
||||||
|
const Demo = () => {
|
||||||
|
const { exists, render } = useSchemaInitializerRender(undefined);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div data-testid="exists">{JSON.stringify(exists)}</div>
|
||||||
|
<div data-testid="render">{render()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await createApp(Demo);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('exists').textContent).toBe('false');
|
||||||
|
expect(screen.getByTestId('render').textContent).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log an error if the initializer is not found', async () => {
|
||||||
|
const consoleErrorSpy = vitest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
const Demo = () => {
|
||||||
|
const { exists, render } = useSchemaInitializerRender('nonexistent');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div data-testid="exists">{JSON.stringify(exists)}</div>
|
||||||
|
<div data-testid="render">{render()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await createApp(Demo);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('exists').textContent).toBe('false');
|
||||||
|
expect(screen.getByTestId('render').textContent).toBe('');
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith('[nocobase]: SchemaInitializer "nonexistent" not found');
|
||||||
|
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the initializer component with name registered', async () => {
|
||||||
|
const Demo = () => {
|
||||||
|
const { exists, render } = useSchemaInitializerRender('test');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div data-testid="exists">{JSON.stringify(exists)}</div>
|
||||||
|
<div data-testid="render">{render()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await createApp(Demo);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('exists').textContent).toBe('true');
|
||||||
|
expect(screen.getByTestId('render').textContent).toBe('Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render custom options', async () => {
|
||||||
|
const Demo = () => {
|
||||||
|
const { render } = useSchemaInitializerRender('test', { componentProps: { className: 'test' } });
|
||||||
|
|
||||||
|
return <div data-testid="render">{render()}</div>;
|
||||||
|
};
|
||||||
|
await createApp(Demo);
|
||||||
|
|
||||||
|
expect(document.querySelector('.test')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should override custom props', async () => {
|
||||||
|
const Demo = () => {
|
||||||
|
const { render } = useSchemaInitializerRender('test', { componentProps: { className: 'test' } });
|
||||||
|
|
||||||
|
return <div data-testid="render">{render({ componentProps: { className: 'test2' } })}</div>;
|
||||||
|
};
|
||||||
|
await createApp(Demo);
|
||||||
|
|
||||||
|
expect(document.querySelector('.test2')).toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.test')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,132 @@
|
|||||||
|
import { screen, userEvent, sleep, act } from '@nocobase/test/client';
|
||||||
|
import { ISchema } from '@formily/json-schema';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
import { createApp } from './fixures/createApp';
|
||||||
|
|
||||||
|
describe('withInitializer', () => {
|
||||||
|
it('renders the component with initializer', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
await createApp();
|
||||||
|
expect(screen.getByTestId('render')).toHaveTextContent('Test');
|
||||||
|
expect(screen.queryByText('Item')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.hover(screen.getByText('Test'));
|
||||||
|
await sleep(100);
|
||||||
|
});
|
||||||
|
expect(screen.queryByText('Item')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.click(screen.getByText('Item'));
|
||||||
|
await sleep(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByText('Hello World!')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when app designable is false, the component will not be rendered', async () => {
|
||||||
|
await createApp({}, { designable: false });
|
||||||
|
expect(screen.queryByText('Test')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when SchemaInitializer config designable is true, but app is false, the component will be rendered', async () => {
|
||||||
|
await createApp({ designable: true }, { designable: false });
|
||||||
|
expect(screen.queryByText('Test')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when popover is false, only render button', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
await createApp({ popover: false });
|
||||||
|
|
||||||
|
expect(screen.queryByText('Test')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.hover(screen.getByText('Test'));
|
||||||
|
await sleep(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByText('Item')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wrap', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
await createApp({
|
||||||
|
wrap: (schema: ISchema) => {
|
||||||
|
return {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'WrapDemo',
|
||||||
|
properties: {
|
||||||
|
[schema.name || uid()]: schema,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.hover(screen.getByText('Test'));
|
||||||
|
await sleep(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.click(screen.getByText('Item'));
|
||||||
|
await sleep(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByText('WrapDemo')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Hello World!')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('insert', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
const insert = vitest.fn();
|
||||||
|
await createApp({
|
||||||
|
insert,
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.hover(screen.getByText('Test'));
|
||||||
|
await sleep(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.click(screen.getByText('Item'));
|
||||||
|
await sleep(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(insert).toBeCalledWith({
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'div',
|
||||||
|
'x-content': 'Hello World!',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('userInsert', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
const insert = vitest.fn();
|
||||||
|
await createApp({
|
||||||
|
useInsert: () => insert,
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.hover(screen.getByText('Test'));
|
||||||
|
await sleep(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.click(screen.getByText('Item'));
|
||||||
|
await sleep(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(insert).toBeCalledWith({
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'div',
|
||||||
|
'x-content': 'Hello World!',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,94 @@
|
|||||||
|
import { SchemaSettings } from '@nocobase/client';
|
||||||
|
|
||||||
|
describe('SchemaSettings', () => {
|
||||||
|
let schemaSettings: SchemaSettings;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
schemaSettings = new SchemaSettings({ name: 'test', items: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('schemaSettings with default options if none are provided', () => {
|
||||||
|
expect(schemaSettings).toBeDefined();
|
||||||
|
expect(schemaSettings.name).toEqual('test');
|
||||||
|
expect(schemaSettings.items).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds an item', () => {
|
||||||
|
schemaSettings.add('item1', { type: 'item' });
|
||||||
|
expect(schemaSettings.items).toEqual([{ name: 'item1', type: 'item' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updates an item if it already exists', () => {
|
||||||
|
schemaSettings.add('item1', { type: 'item', componentProps: { title: '123' } });
|
||||||
|
schemaSettings.add('item1', { type: 'item', componentProps: { title: '456' } });
|
||||||
|
expect(schemaSettings.items).toEqual([{ name: 'item1', type: 'item', componentProps: { title: '456' } }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds a nested item', () => {
|
||||||
|
schemaSettings.add('parent', { type: 'itemGroup' });
|
||||||
|
schemaSettings.add('parent.item1', { type: 'item' });
|
||||||
|
expect(schemaSettings.get('parent')).toEqual({
|
||||||
|
name: 'parent',
|
||||||
|
type: 'itemGroup',
|
||||||
|
children: [{ name: 'item1', type: 'item' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
schemaSettings.add('parent.item1', { type: 'item', componentProps: { title: '123' } });
|
||||||
|
expect(schemaSettings.get('parent')).toEqual({
|
||||||
|
name: 'parent',
|
||||||
|
type: 'itemGroup',
|
||||||
|
children: [{ name: 'item1', type: 'item', componentProps: { title: '123' } }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gets an item', () => {
|
||||||
|
schemaSettings.add('item1', { type: 'item' });
|
||||||
|
expect(schemaSettings.get('item1')).toEqual({ name: 'item1', type: 'item' });
|
||||||
|
expect(schemaSettings.get('no-exist')).toEqual(undefined);
|
||||||
|
expect(schemaSettings.get(undefined)).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gets a nested item', () => {
|
||||||
|
schemaSettings.add('parent', { type: 'itemGroup' });
|
||||||
|
schemaSettings.add('parent.item1', { type: 'item' });
|
||||||
|
expect(schemaSettings.get('parent.item1')).toEqual({
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
});
|
||||||
|
expect(schemaSettings.get('parent.item1.not-exist')).toEqual(undefined);
|
||||||
|
expect(schemaSettings.get('parent.not-exist')).toEqual(undefined);
|
||||||
|
expect(schemaSettings.get('parent.not-exist.not-exist')).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns undefined for a non-existent item', () => {
|
||||||
|
expect(schemaSettings.get('nonExistent')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removes an item', () => {
|
||||||
|
schemaSettings.add('item1', { type: 'item' });
|
||||||
|
schemaSettings.remove('item1');
|
||||||
|
expect(schemaSettings.items).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removes a nested item', () => {
|
||||||
|
schemaSettings.add('parent', { type: 'item' });
|
||||||
|
schemaSettings.add('parent.item1', { type: 'item' });
|
||||||
|
schemaSettings.remove('parent');
|
||||||
|
expect(schemaSettings.get('parent')).toEqual(undefined);
|
||||||
|
expect(schemaSettings.get('parent.item1')).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removes a nested child item', () => {
|
||||||
|
schemaSettings.add('parent', { type: 'item' });
|
||||||
|
schemaSettings.add('parent.item1', { type: 'item' });
|
||||||
|
schemaSettings.remove('parent.item1');
|
||||||
|
expect(schemaSettings.get('parent')).toBeDefined();
|
||||||
|
expect(schemaSettings.get('parent.item1')).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does nothing when removing a non-existent item', () => {
|
||||||
|
const initialItems = [...schemaSettings.items];
|
||||||
|
schemaSettings.remove('nonExistent');
|
||||||
|
expect(schemaSettings.items).toEqual(initialItems);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,89 @@
|
|||||||
|
import { SchemaSettings, Application } from '@nocobase/client';
|
||||||
|
|
||||||
|
describe('SchemaSettingsManager', () => {
|
||||||
|
let app: Application;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
app = new Application();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add schema initializers correctly', () => {
|
||||||
|
const demo = new SchemaSettings({ name: 'test', items: [] });
|
||||||
|
app.schemaSettingsManager.add(demo);
|
||||||
|
|
||||||
|
expect(app.schemaSettingsManager.has('test')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle addition of items to non-existent schema initializers', () => {
|
||||||
|
app.schemaSettingsManager.addItem('test', 'item1', { type: 'item' });
|
||||||
|
expect(app.schemaSettingsManager.has('test')).toBeFalsy();
|
||||||
|
|
||||||
|
const demo = new SchemaSettings({ name: 'test', items: [] });
|
||||||
|
app.schemaSettingsManager.add(demo);
|
||||||
|
expect(app.schemaSettingsManager.has('test')).toBeTruthy();
|
||||||
|
const demoRes = app.schemaSettingsManager.get('test');
|
||||||
|
expect(demoRes.get('item1')).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add item to existing schema settings', () => {
|
||||||
|
const mockInitializer = new SchemaSettings({ name: 'test', items: [] });
|
||||||
|
const fn = vitest.spyOn(mockInitializer, 'add');
|
||||||
|
app.schemaSettingsManager.add(mockInitializer);
|
||||||
|
|
||||||
|
app.schemaSettingsManager.addItem('test', 'item1', { type: 'item' });
|
||||||
|
expect(fn).toHaveBeenCalledWith('item1', { type: 'item' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a specific schema settings when it exists', () => {
|
||||||
|
const mockInitializer = new SchemaSettings({ name: 'test', items: [] });
|
||||||
|
app.schemaSettingsManager.add(mockInitializer);
|
||||||
|
|
||||||
|
const fetchedInitializer = app.schemaSettingsManager.get('test');
|
||||||
|
expect(fetchedInitializer).toBe(mockInitializer);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when schema settings does not exist', () => {
|
||||||
|
const fetchedInitializer = app.schemaSettingsManager.get('nonExistent');
|
||||||
|
expect(fetchedInitializer).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all schema initializers', () => {
|
||||||
|
const mockInitializer1 = new SchemaSettings({ name: 'test1', items: [] });
|
||||||
|
const mockInitializer2 = new SchemaSettings({ name: 'test2', items: [] });
|
||||||
|
app.schemaSettingsManager.add(mockInitializer1, mockInitializer2);
|
||||||
|
|
||||||
|
const initializers = app.schemaSettingsManager.getAll();
|
||||||
|
expect(initializers).toEqual({
|
||||||
|
test1: mockInitializer1,
|
||||||
|
test2: mockInitializer2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly check if schema settings exists', () => {
|
||||||
|
const mockInitializer = new SchemaSettings({ name: 'test', items: [] });
|
||||||
|
app.schemaSettingsManager.add(mockInitializer);
|
||||||
|
|
||||||
|
expect(app.schemaSettingsManager.has('test')).toBeTruthy();
|
||||||
|
expect(app.schemaSettingsManager.has('nonExistent')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove schema settings', () => {
|
||||||
|
const mockInitializer = new SchemaSettings({ name: 'test', items: [] });
|
||||||
|
app.schemaSettingsManager.add(mockInitializer);
|
||||||
|
|
||||||
|
app.schemaSettingsManager.remove('test');
|
||||||
|
expect(app.schemaSettingsManager.has('test')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle removal of items for non-existent schema initializers', () => {
|
||||||
|
expect(() => app.schemaSettingsManager.removeItem('nonExistent', 'item1')).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove item from existing schema settings', () => {
|
||||||
|
const mockInitializer = new SchemaSettings({ name: 'test', items: [] });
|
||||||
|
const fn = vitest.spyOn(mockInitializer, 'remove');
|
||||||
|
app.schemaSettingsManager.add(mockInitializer);
|
||||||
|
app.schemaSettingsManager.removeItem('test', 'item1');
|
||||||
|
expect(fn).toHaveBeenCalledWith('item1');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,386 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { screen, userEvent } from '@nocobase/test/client';
|
||||||
|
import { SchemaInitializerItem, useSchemaSettingsItem } from '@nocobase/client';
|
||||||
|
|
||||||
|
import { createAndHover } from './fixtures/createAppAndHover';
|
||||||
|
|
||||||
|
describe('SchemaSettingsChildren', () => {
|
||||||
|
it('basic', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
type: 'item',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item2')).toBeInTheDocument();
|
||||||
|
expect(document.body.querySelector('.ant-dropdown-menu').textContent).toBe('Item1Item2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sort', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
sort: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
type: 'item',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item2',
|
||||||
|
},
|
||||||
|
sort: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item2')).toBeInTheDocument();
|
||||||
|
expect(document.body.querySelector('.ant-dropdown-menu').textContent).toBe('Item2Item1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Component', async () => {
|
||||||
|
await createAndHover(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
Component: () => {
|
||||||
|
return <div>Item1</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
Component: 'Item2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item3',
|
||||||
|
Component: 'not-exists',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item4',
|
||||||
|
} as any,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
Item2: () => {
|
||||||
|
return <div>Item2</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('useVisible()', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
useVisible() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
type: 'item',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item2',
|
||||||
|
},
|
||||||
|
useVisible() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item2')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('children', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
type: 'itemGroup',
|
||||||
|
componentProps: {
|
||||||
|
title: 'A',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
type: 'item',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(screen.queryByText('A')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('useChildren()', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
type: 'itemGroup',
|
||||||
|
componentProps: {
|
||||||
|
title: 'A',
|
||||||
|
},
|
||||||
|
useChildren() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
type: 'item',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(screen.queryByText('A')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge `children` and `useChildren()`', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
type: 'itemGroup',
|
||||||
|
componentProps: {
|
||||||
|
title: 'A',
|
||||||
|
},
|
||||||
|
useChildren() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
type: 'item',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
type: 'item',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'item3',
|
||||||
|
type: 'item',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(screen.queryByText('A')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item2')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Item3')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hideIfNoChildren', async () => {
|
||||||
|
await createAndHover([
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
type: 'itemGroup',
|
||||||
|
componentProps: {
|
||||||
|
title: 'A',
|
||||||
|
},
|
||||||
|
hideIfNoChildren: true,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'b',
|
||||||
|
type: 'itemGroup',
|
||||||
|
componentProps: {
|
||||||
|
title: 'B',
|
||||||
|
},
|
||||||
|
hideIfNoChildren: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'c',
|
||||||
|
type: 'itemGroup',
|
||||||
|
componentProps: {
|
||||||
|
title: 'C',
|
||||||
|
},
|
||||||
|
hideIfNoChildren: true,
|
||||||
|
useChildren() {
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'd',
|
||||||
|
type: 'itemGroup',
|
||||||
|
componentProps: {
|
||||||
|
title: 'D',
|
||||||
|
},
|
||||||
|
hideIfNoChildren: true,
|
||||||
|
useChildren() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(screen.queryByText('A')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('B')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('C')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('D')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('componentProps', async () => {
|
||||||
|
await createAndHover(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
Component: 'CommonDemo',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
CommonDemo: (props) => {
|
||||||
|
return <SchemaInitializerItem title={props.title} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('useComponentProps', async () => {
|
||||||
|
await createAndHover(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
Component: 'CommonDemo',
|
||||||
|
useComponentProps() {
|
||||||
|
return {
|
||||||
|
title: 'Item1',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
CommonDemo: (props) => {
|
||||||
|
return <SchemaInitializerItem title={props.title} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge `componentProps` and `useComponentProps()`', async () => {
|
||||||
|
const onClick = vitest.fn();
|
||||||
|
await createAndHover(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
Component: 'CommonDemo',
|
||||||
|
componentProps: {
|
||||||
|
title: 'Item1',
|
||||||
|
},
|
||||||
|
useComponentProps() {
|
||||||
|
return {
|
||||||
|
onClick,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
CommonDemo: (props) => {
|
||||||
|
return <SchemaInitializerItem title={props.title} onClick={props.onClick} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Item1'));
|
||||||
|
expect(onClick).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
item: expect.objectContaining({
|
||||||
|
title: 'Item1',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('public props', async () => {
|
||||||
|
await createAndHover(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
Component: 'CommonDemo',
|
||||||
|
title: 'Item1',
|
||||||
|
} as any,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
CommonDemo: () => {
|
||||||
|
const { title } = useSchemaSettingsItem<any>();
|
||||||
|
return <SchemaInitializerItem title={title} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Item1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { screen, userEvent, sleep, act, render, waitFor } from '@nocobase/test/client';
|
||||||
|
|
||||||
|
import { SchemaSettingsItemType, SchemaSettings, Application, useSchemaSettingsRender } from '@nocobase/client';
|
||||||
|
|
||||||
|
export async function createAndHover(items: SchemaSettingsItemType[], appOptions: any = {}) {
|
||||||
|
const testSettings = new SchemaSettings({
|
||||||
|
name: 'test',
|
||||||
|
items,
|
||||||
|
});
|
||||||
|
const Demo = () => {
|
||||||
|
const { render } = useSchemaSettingsRender('test');
|
||||||
|
|
||||||
|
return render();
|
||||||
|
};
|
||||||
|
const app = new Application({
|
||||||
|
providers: [Demo],
|
||||||
|
schemaSettings: [testSettings],
|
||||||
|
designable: true,
|
||||||
|
...appOptions,
|
||||||
|
});
|
||||||
|
const Root = app.getRootComponent();
|
||||||
|
render(<Root />);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
const user = userEvent.setup();
|
||||||
|
await user.hover(screen.getByRole('button'));
|
||||||
|
await waitFor(async () => {
|
||||||
|
expect(screen.queryByRole('menu')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Application, SchemaSettings, useSchemaSettingsRender } from '@nocobase/client';
|
||||||
|
import { render, screen, waitFor } from '@nocobase/test/client';
|
||||||
|
|
||||||
|
describe('useSchemaSettingsRender', () => {
|
||||||
|
async function createApp(DemoComponent: any) {
|
||||||
|
const testSettings = new SchemaSettings({
|
||||||
|
name: 'test',
|
||||||
|
Component: (props) => <div {...props}>Test</div>,
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = new Application({
|
||||||
|
providers: [DemoComponent],
|
||||||
|
schemaSettings: [testSettings],
|
||||||
|
designable: true,
|
||||||
|
});
|
||||||
|
const Root = app.getRootComponent();
|
||||||
|
render(<Root />);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should return exists as false and render as null when name is not provided', async () => {
|
||||||
|
const Demo = () => {
|
||||||
|
const { exists, render } = useSchemaSettingsRender(undefined);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div data-testid="exists">{JSON.stringify(exists)}</div>
|
||||||
|
<div data-testid="render">{render()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await createApp(Demo);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('exists').textContent).toBe('false');
|
||||||
|
expect(screen.getByTestId('render').textContent).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log an error if the initializer is not found', async () => {
|
||||||
|
const consoleErrorSpy = vitest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
const Demo = () => {
|
||||||
|
const { exists, render } = useSchemaSettingsRender('nonexistent');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div data-testid="exists">{JSON.stringify(exists)}</div>
|
||||||
|
<div data-testid="render">{render()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await createApp(Demo);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('exists').textContent).toBe('false');
|
||||||
|
expect(screen.getByTestId('render').textContent).toBe('');
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith('[nocobase]: SchemaSettings "nonexistent" not found');
|
||||||
|
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the settings component with name registered', async () => {
|
||||||
|
const Demo = () => {
|
||||||
|
const { exists, render } = useSchemaSettingsRender('test');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div data-testid="exists">{JSON.stringify(exists)}</div>
|
||||||
|
<div data-testid="render">{render()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await createApp(Demo);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('exists').textContent).toBe('true');
|
||||||
|
expect(screen.getByTestId('render').textContent).toBe('Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render custom options', async () => {
|
||||||
|
const Demo = () => {
|
||||||
|
const { render } = useSchemaSettingsRender('test', { componentProps: { className: 'test' } });
|
||||||
|
|
||||||
|
return <div data-testid="render">{render()}</div>;
|
||||||
|
};
|
||||||
|
await createApp(Demo);
|
||||||
|
|
||||||
|
expect(document.querySelector('.test')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should override custom props', async () => {
|
||||||
|
const Demo = () => {
|
||||||
|
const { render } = useSchemaSettingsRender('test', { componentProps: { className: 'test' } });
|
||||||
|
|
||||||
|
return <div data-testid="render">{render({ componentProps: { className: 'test2' } })}</div>;
|
||||||
|
};
|
||||||
|
await createApp(Demo);
|
||||||
|
|
||||||
|
expect(document.querySelector('.test2')).toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.test')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -9,7 +9,7 @@ export interface AppComponentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AppComponent: FC<AppComponentProps> = observer(
|
export const AppComponent: FC<AppComponentProps> = observer(
|
||||||
(props) => {
|
({ children, ...props }) => {
|
||||||
const { app } = props;
|
const { app } = props;
|
||||||
const handleErrors = useCallback((error: Error, info: { componentStack: string }) => {
|
const handleErrors = useCallback((error: Error, info: { componentStack: string }) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -33,7 +33,7 @@ export const AppComponent: FC<AppComponentProps> = observer(
|
|||||||
>
|
>
|
||||||
<ApplicationContext.Provider value={app}>
|
<ApplicationContext.Provider value={app}>
|
||||||
{app.maintained && app.maintaining && app.renderComponent('AppMaintainingDialog', { app })}
|
{app.maintained && app.maintaining && app.renderComponent('AppMaintainingDialog', { app })}
|
||||||
{app.renderComponent('AppMain')}
|
{app.renderComponent('AppMain', undefined, children)}
|
||||||
</ApplicationContext.Provider>
|
</ApplicationContext.Provider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useApp } from '../hooks';
|
import { useApp } from '../hooks';
|
||||||
|
|
||||||
export const MainComponent = React.memo(() => {
|
export const MainComponent = React.memo(({ children }) => {
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
const Router = useMemo(() => app.router.getRouterComponent(), [app]);
|
const Router = useMemo(() => app.router.getRouterComponent(children), [app]);
|
||||||
const Providers = useMemo(() => app.getComposeProviders(), [app]);
|
const Providers = useMemo(() => app.getComposeProviders(), [app]);
|
||||||
return <Router BaseLayout={Providers} />;
|
return <Router BaseLayout={Providers} />;
|
||||||
});
|
});
|
||||||
|
@ -2,12 +2,15 @@ import React, { FC } from 'react';
|
|||||||
import { MainComponent } from './MainComponent';
|
import { MainComponent } from './MainComponent';
|
||||||
|
|
||||||
const Loading: FC = () => <div>Loading...</div>;
|
const Loading: FC = () => <div>Loading...</div>;
|
||||||
const AppError: FC<{ error: Error }> = ({ error }) => (
|
const AppError: FC<{ error: Error }> = ({ error }) => {
|
||||||
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>Load Plugin Error</div>
|
<div>Load Plugin Error</div>
|
||||||
{error?.message}
|
{error?.message}
|
||||||
|
{process.env.__TEST__ && error?.stack}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const AppNotFound: FC = () => <div></div>;
|
const AppNotFound: FC = () => <div></div>;
|
||||||
|
|
||||||
|
@ -32,7 +32,8 @@ export class SchemaInitializer<P1 = ButtonProps, P2 = {}> {
|
|||||||
if (!parentItem.children) {
|
if (!parentItem.children) {
|
||||||
parentItem.children = [];
|
parentItem.children = [];
|
||||||
}
|
}
|
||||||
const index = parentItem.children.findIndex((item: any) => item.name === name);
|
const childrenName = name.replace(`${parentItem.name}.`, '');
|
||||||
|
const index = parentItem.children.findIndex((item: any) => item.name === childrenName);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
parentItem.children.push(data);
|
parentItem.children.push(data);
|
||||||
} else {
|
} else {
|
||||||
@ -42,6 +43,7 @@ export class SchemaInitializer<P1 = ButtonProps, P2 = {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(nestedName: string): SchemaInitializerItemType | undefined {
|
get(nestedName: string): SchemaInitializerItemType | undefined {
|
||||||
|
if (!nestedName) return undefined;
|
||||||
const arr = nestedName.split('.');
|
const arr = nestedName.split('.');
|
||||||
let current: any = this.items;
|
let current: any = this.items;
|
||||||
|
|
||||||
@ -58,8 +60,6 @@ export class SchemaInitializer<P1 = ButtonProps, P2 = {}> {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return current;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(nestedName: string) {
|
remove(nestedName: string) {
|
||||||
|
@ -2,6 +2,7 @@ import { useForm } from '@formily/react';
|
|||||||
import React, { FC, useCallback, useMemo } from 'react';
|
import React, { FC, useCallback, useMemo } from 'react';
|
||||||
import { useActionContext, SchemaComponent } from '../../../schema-component';
|
import { useActionContext, SchemaComponent } from '../../../schema-component';
|
||||||
import { useSchemaInitializerItem } from '../context';
|
import { useSchemaInitializerItem } from '../context';
|
||||||
|
import { SchemaInitializerItem } from './SchemaInitializerItem';
|
||||||
|
|
||||||
export interface SchemaInitializerActionModalProps {
|
export interface SchemaInitializerActionModalProps {
|
||||||
title: string;
|
title: string;
|
||||||
@ -10,10 +11,24 @@ export interface SchemaInitializerActionModalProps {
|
|||||||
onSubmit?: (values: any) => void;
|
onSubmit?: (values: any) => void;
|
||||||
buttonText?: any;
|
buttonText?: any;
|
||||||
component?: any;
|
component?: any;
|
||||||
|
isItem?: boolean;
|
||||||
}
|
}
|
||||||
export const SchemaInitializerActionModal: FC<SchemaInitializerActionModalProps> = (props) => {
|
|
||||||
const { title, schema, buttonText, component, onCancel, onSubmit } = props;
|
|
||||||
|
|
||||||
|
const SchemaInitializerActionModalItemComponent = React.forwardRef((props: any, ref) => {
|
||||||
|
const { onClick, title, ...others } = props;
|
||||||
|
return (
|
||||||
|
<SchemaInitializerItem
|
||||||
|
ref={ref}
|
||||||
|
{...others}
|
||||||
|
onClick={(e) => {
|
||||||
|
onClick?.(e.event);
|
||||||
|
}}
|
||||||
|
></SchemaInitializerItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SchemaInitializerActionModal: FC<SchemaInitializerActionModalProps> = (props) => {
|
||||||
|
const { title, schema, buttonText, isItem, component, onCancel, onSubmit } = props;
|
||||||
const useCancelAction = useCallback(() => {
|
const useCancelAction = useCallback(() => {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
@ -53,6 +68,11 @@ export const SchemaInitializerActionModal: FC<SchemaInitializerActionModalProps>
|
|||||||
? {
|
? {
|
||||||
component,
|
component,
|
||||||
}
|
}
|
||||||
|
: isItem
|
||||||
|
? {
|
||||||
|
title: buttonText,
|
||||||
|
component: SchemaInitializerActionModalItemComponent,
|
||||||
|
}
|
||||||
: {
|
: {
|
||||||
icon: 'PlusOutlined',
|
icon: 'PlusOutlined',
|
||||||
style: {
|
style: {
|
||||||
|
@ -21,6 +21,7 @@ const typeComponentMap: Record<string, string> = {
|
|||||||
item: 'SchemaInitializerItemInternal',
|
item: 'SchemaInitializerItemInternal',
|
||||||
itemGroup: 'SchemaInitializerItemGroupInternal',
|
itemGroup: 'SchemaInitializerItemGroupInternal',
|
||||||
divider: 'SchemaInitializerDivider',
|
divider: 'SchemaInitializerDivider',
|
||||||
|
switch: 'SchemaInitializerSwitchInternal',
|
||||||
subMenu: 'SchemaInitializerSubMenuInternal',
|
subMenu: 'SchemaInitializerSubMenuInternal',
|
||||||
actionModal: 'SchemaInitializerActionModalInternal',
|
actionModal: 'SchemaInitializerActionModalInternal',
|
||||||
};
|
};
|
||||||
@ -70,7 +71,7 @@ export const SchemaInitializerChild: FC<SchemaInitializerItemType> = memo((props
|
|||||||
if (!C) {
|
if (!C) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (hideIfNoChildren && Array.isArray(componentChildren) && componentChildren.length === 0) {
|
if (hideIfNoChildren && !componentChildren) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +76,9 @@ export const SchemaInitializerItem = memo(
|
|||||||
);
|
);
|
||||||
SchemaInitializerItem.displayName = 'SchemaInitializerItem';
|
SchemaInitializerItem.displayName = 'SchemaInitializerItem';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
export const SchemaInitializerItemInternal = () => {
|
export const SchemaInitializerItemInternal = () => {
|
||||||
const itemConfig = useSchemaInitializerItem();
|
const itemConfig = useSchemaInitializerItem();
|
||||||
return <SchemaInitializerItem {...itemConfig} />;
|
return <SchemaInitializerItem {...itemConfig} />;
|
||||||
|
@ -10,10 +10,16 @@ import { useSchemaInitializerStyles } from './style';
|
|||||||
export interface SchemaInitializerItemGroupProps {
|
export interface SchemaInitializerItemGroupProps {
|
||||||
title: string;
|
title: string;
|
||||||
children?: SchemaInitializerOptions['items'];
|
children?: SchemaInitializerOptions['items'];
|
||||||
|
items?: SchemaInitializerOptions['items'];
|
||||||
divider?: boolean;
|
divider?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SchemaInitializerItemGroup: FC<SchemaInitializerItemGroupProps> = ({ children, title, divider }) => {
|
export const SchemaInitializerItemGroup: FC<SchemaInitializerItemGroupProps> = ({
|
||||||
|
children,
|
||||||
|
items,
|
||||||
|
title,
|
||||||
|
divider,
|
||||||
|
}) => {
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { componentCls } = useSchemaInitializerStyles();
|
const { componentCls } = useSchemaInitializerStyles();
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
@ -21,11 +27,14 @@ export const SchemaInitializerItemGroup: FC<SchemaInitializerItemGroupProps> = (
|
|||||||
<div style={{ marginInline: token.marginXXS }}>
|
<div style={{ marginInline: token.marginXXS }}>
|
||||||
{divider && <SchemaInitializerDivider />}
|
{divider && <SchemaInitializerDivider />}
|
||||||
<div className={`${componentCls}-group-title`}>{compile(title)}</div>
|
<div className={`${componentCls}-group-title`}>{compile(title)}</div>
|
||||||
<SchemaInitializerChildren>{children}</SchemaInitializerChildren>
|
<SchemaInitializerChildren>{items || children}</SchemaInitializerChildren>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
export const SchemaInitializerItemGroupInternal = () => {
|
export const SchemaInitializerItemGroupInternal = () => {
|
||||||
const itemConfig = useSchemaInitializerItem<SchemaInitializerItemGroupProps>();
|
const itemConfig = useSchemaInitializerItem<SchemaInitializerItemGroupProps>();
|
||||||
return <SchemaInitializerItemGroup {...itemConfig} />;
|
return <SchemaInitializerItemGroup {...itemConfig} />;
|
||||||
|
@ -55,6 +55,9 @@ export const SchemaInitializerSelect: FC<SchemaInitializerSelectItemProps> = (pr
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
export const SchemaInitializerSelectInternal = () => {
|
export const SchemaInitializerSelectInternal = () => {
|
||||||
const itemConfig = useSchemaInitializerItem<SchemaInitializerSelectItemProps>();
|
const itemConfig = useSchemaInitializerItem<SchemaInitializerSelectItemProps>();
|
||||||
return <SchemaInitializerSelect {...itemConfig} />;
|
return <SchemaInitializerSelect {...itemConfig} />;
|
||||||
|
@ -10,16 +10,20 @@ import { SchemaInitializerOptions } from '../types';
|
|||||||
import { useSchemaInitializerStyles } from './style';
|
import { useSchemaInitializerStyles } from './style';
|
||||||
|
|
||||||
export interface SchemaInitializerSubMenuProps {
|
export interface SchemaInitializerSubMenuProps {
|
||||||
name: string;
|
name?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
onClick?: (args: any) => void;
|
onClick?: (args: any) => void;
|
||||||
onOpenChange?: (openKeys: string[]) => void;
|
onOpenChange?: (openKeys: string[]) => void;
|
||||||
icon?: string | ReactNode;
|
icon?: string | ReactNode;
|
||||||
children?: SchemaInitializerOptions['items'];
|
children?: SchemaInitializerOptions['items'];
|
||||||
|
items?: SchemaInitializerOptions['items'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const SchemaInitializerSubMenuContext = React.createContext<{ isInMenu?: true }>({});
|
const SchemaInitializerSubMenuContext = React.createContext<{ isInMenu?: true }>({});
|
||||||
const SchemaInitializerMenuProvider = (props) => {
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const SchemaInitializerMenuProvider = (props) => {
|
||||||
return (
|
return (
|
||||||
<SchemaInitializerSubMenuContext.Provider value={{ isInMenu: true }}>
|
<SchemaInitializerSubMenuContext.Provider value={{ isInMenu: true }}>
|
||||||
{props.children}
|
{props.children}
|
||||||
@ -71,25 +75,30 @@ export const SchemaInitializerMenu: FC<MenuProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SchemaInitializerSubMenu: FC<SchemaInitializerSubMenuProps> = (props) => {
|
export const SchemaInitializerSubMenu: FC<SchemaInitializerSubMenuProps> = (props) => {
|
||||||
const { children, title, name = uid(), onOpenChange, icon, ...others } = props;
|
const { children, items: propItems, title, name, onOpenChange, icon, ...others } = props;
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const validChildren = children?.filter((item) => (item.useVisible ? item.useVisible() : true));
|
const schemaInitializerItem = useSchemaInitializerItem();
|
||||||
|
const nameValue = useMemo(() => name || schemaInitializerItem?.name || uid(), [name, schemaInitializerItem]);
|
||||||
|
const validChildren = (propItems || children)?.filter((item) => (item.useVisible ? item.useVisible() : true));
|
||||||
const childrenItems = useSchemaInitializerMenuItems(validChildren, name);
|
const childrenItems = useSchemaInitializerMenuItems(validChildren, name);
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
...others,
|
...others,
|
||||||
key: name,
|
key: nameValue,
|
||||||
label: compile(title),
|
label: compile(title),
|
||||||
icon: typeof icon === 'string' ? <Icon type={icon as string} /> : icon,
|
icon: typeof icon === 'string' ? <Icon type={icon as string} /> : icon,
|
||||||
children: childrenItems,
|
children: childrenItems,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [childrenItems, compile, icon, name, others, title]);
|
}, [childrenItems, compile, icon, nameValue, others, title]);
|
||||||
return <SchemaInitializerMenu onOpenChange={onOpenChange} items={items}></SchemaInitializerMenu>;
|
return <SchemaInitializerMenu onOpenChange={onOpenChange} items={items}></SchemaInitializerMenu>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
export const SchemaInitializerSubMenuInternal = () => {
|
export const SchemaInitializerSubMenuInternal = () => {
|
||||||
const itemConfig = useSchemaInitializerItem<SchemaInitializerSubMenuProps>();
|
const itemConfig = useSchemaInitializerItem<SchemaInitializerSubMenuProps>();
|
||||||
return <SchemaInitializerSubMenu {...itemConfig}></SchemaInitializerSubMenu>;
|
return <SchemaInitializerSubMenu {...itemConfig}></SchemaInitializerSubMenu>;
|
||||||
|
@ -21,6 +21,9 @@ export const SchemaInitializerSwitch: FC<SchemaInitializerSwitchItemProps> = (pr
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
export const SchemaInitializerSwitchInternal = () => {
|
export const SchemaInitializerSwitchInternal = () => {
|
||||||
const itemConfig = useSchemaInitializerItem<SchemaInitializerSwitchItemProps>();
|
const itemConfig = useSchemaInitializerItem<SchemaInitializerSwitchItemProps>();
|
||||||
return <SchemaInitializerSwitch {...itemConfig} />;
|
return <SchemaInitializerSwitch {...itemConfig} />;
|
||||||
|
@ -1,174 +1,3 @@
|
|||||||
import { ButtonProps } from 'antd';
|
|
||||||
import React, { FC, ReactNode, useCallback, useMemo } from 'react';
|
|
||||||
import { useCompile } from '../../../schema-component';
|
|
||||||
import { useApp } from '../../hooks';
|
|
||||||
import { SchemaInitializerChild, SchemaInitializerItems } from '../components';
|
|
||||||
import { SchemaInitializerButton } from '../components/SchemaInitializerButton';
|
|
||||||
import { withInitializer } from '../hoc';
|
|
||||||
import { SchemaInitializerOptions } from '../types';
|
|
||||||
|
|
||||||
export * from './useAriaAttributeOfMenuItem';
|
export * from './useAriaAttributeOfMenuItem';
|
||||||
|
export * from './useSchemaInitializerRender';
|
||||||
/**
|
export * from './useGetSchemaInitializerMenuItems';
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
export function useSchemaInitializerMenuItems(items: any[], name?: string, onClick?: (args: any) => void) {
|
|
||||||
const getMenuItems = useGetSchemaInitializerMenuItems(onClick);
|
|
||||||
return useMemo(() => getMenuItems(items, name), [getMenuItems, items, name]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
export function useGetSchemaInitializerMenuItems(onClick?: (args: any) => void) {
|
|
||||||
const compile = useCompile();
|
|
||||||
|
|
||||||
const getMenuItems = useCallback(
|
|
||||||
(items: any[], parentKey: string) => {
|
|
||||||
if (!items?.length) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return items.map((item: any, indexA) => {
|
|
||||||
const ItemComponent = item.component || item.Component;
|
|
||||||
let element: ReactNode;
|
|
||||||
const compiledTitle = item.title || item.label ? compile(item.title || item.label) : undefined;
|
|
||||||
if (ItemComponent) {
|
|
||||||
element = React.createElement(SchemaInitializerChild, { ...item, title: compiledTitle });
|
|
||||||
if (!element) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.type === 'divider') {
|
|
||||||
return { type: 'divider', key: item.key || `divider-${indexA}` };
|
|
||||||
}
|
|
||||||
if (item.type === 'item' && ItemComponent) {
|
|
||||||
if (!item.key) {
|
|
||||||
item.key = `${compiledTitle}-${indexA}`;
|
|
||||||
}
|
|
||||||
return item.associationField
|
|
||||||
? {
|
|
||||||
key: item.key,
|
|
||||||
label: element,
|
|
||||||
associationField: item.associationField,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
key: item.key,
|
|
||||||
label: element,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (item.type === 'itemGroup') {
|
|
||||||
const label = typeof compiledTitle === 'string' ? compiledTitle : item.title;
|
|
||||||
const key = item.key || `${parentKey}-item-group-${indexA}`;
|
|
||||||
return {
|
|
||||||
type: 'group',
|
|
||||||
key,
|
|
||||||
label,
|
|
||||||
// className: styles.nbMenuItemGroup,
|
|
||||||
children: item?.children.length ? getMenuItems(item.children, key) : [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (item.type === 'subMenu') {
|
|
||||||
const label = compiledTitle;
|
|
||||||
const key = item.key || item.name || `${parentKey}-sub-menu-${indexA}`;
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
label,
|
|
||||||
children: item?.children.length ? getMenuItems(item.children, key) : [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (item.isMenuType) {
|
|
||||||
const { isMenuType, ...menuData } = item;
|
|
||||||
return menuData;
|
|
||||||
}
|
|
||||||
|
|
||||||
const label = element || compiledTitle || item.label;
|
|
||||||
const key = item.key || `${parentKey}-${compiledTitle}-${indexA}`;
|
|
||||||
const handleClick = (info) => {
|
|
||||||
info.domEvent.stopPropagation();
|
|
||||||
if (info.key !== key) return;
|
|
||||||
if (item.onClick) {
|
|
||||||
item.onClick({ ...info, item });
|
|
||||||
} else {
|
|
||||||
onClick?.({ ...info, item });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return item.associationField
|
|
||||||
? {
|
|
||||||
key,
|
|
||||||
label,
|
|
||||||
associationField: item.associationField,
|
|
||||||
onClick: handleClick,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
key,
|
|
||||||
label,
|
|
||||||
onClick: handleClick,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[compile, onClick],
|
|
||||||
);
|
|
||||||
|
|
||||||
return getMenuItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
const InitializerComponent: FC<SchemaInitializerOptions<any, any>> = React.memo((options) => {
|
|
||||||
const Component: any = options.Component || SchemaInitializerButton;
|
|
||||||
|
|
||||||
const ItemsComponent: any = options.ItemsComponent || SchemaInitializerItems;
|
|
||||||
const itemsComponentProps: any = {
|
|
||||||
...options.itemsComponentProps,
|
|
||||||
options,
|
|
||||||
items: options.items,
|
|
||||||
style: options.itemsComponentStyle,
|
|
||||||
};
|
|
||||||
|
|
||||||
const C = useMemo(() => withInitializer(Component), [Component]);
|
|
||||||
|
|
||||||
return React.createElement(C, options, React.createElement(ItemsComponent, itemsComponentProps));
|
|
||||||
});
|
|
||||||
InitializerComponent.displayName = 'InitializerComponent';
|
|
||||||
|
|
||||||
export function useSchemaInitializerRender<P1 = ButtonProps, P2 = {}>(
|
|
||||||
name: string,
|
|
||||||
options?: Omit<SchemaInitializerOptions<P1, P2>, 'name'>,
|
|
||||||
) {
|
|
||||||
const app = useApp();
|
|
||||||
const renderCache = React.useRef<Record<string, React.FunctionComponentElement<any>>>({});
|
|
||||||
const initializer = useMemo(
|
|
||||||
() => app.schemaInitializerManager.get<P1, P2>(name),
|
|
||||||
[app.schemaInitializerManager, name],
|
|
||||||
);
|
|
||||||
const res = useMemo(() => {
|
|
||||||
if (!name) {
|
|
||||||
return {
|
|
||||||
exists: false,
|
|
||||||
render: () => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!initializer) {
|
|
||||||
console.error(`[nocobase]: SchemaInitializer "${name}" not found`);
|
|
||||||
return {
|
|
||||||
exists: false,
|
|
||||||
render: () => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
exists: true,
|
|
||||||
render: (props?: Omit<SchemaInitializerOptions<P1, P2>, 'name'>) => {
|
|
||||||
const key = JSON.stringify(props) || '{}';
|
|
||||||
if (renderCache.current[key]) {
|
|
||||||
return renderCache.current[key];
|
|
||||||
}
|
|
||||||
return (renderCache.current[key] = React.createElement(InitializerComponent, {
|
|
||||||
...initializer.options,
|
|
||||||
...options,
|
|
||||||
...props,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [initializer, name, options]);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,105 @@
|
|||||||
|
import React, { ReactNode, useCallback, useMemo } from 'react';
|
||||||
|
import { useCompile } from '../../../schema-component';
|
||||||
|
import { SchemaInitializerChild } from '../components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export function useSchemaInitializerMenuItems(items: any[], name?: string, onClick?: (args: any) => void) {
|
||||||
|
const getMenuItems = useGetSchemaInitializerMenuItems(onClick);
|
||||||
|
return useMemo(() => getMenuItems(items, name), [getMenuItems, items, name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export function useGetSchemaInitializerMenuItems(onClick?: (args: any) => void) {
|
||||||
|
const compile = useCompile();
|
||||||
|
|
||||||
|
const getMenuItems = useCallback(
|
||||||
|
(items: any[], parentKey: string) => {
|
||||||
|
if (!items?.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return items.map((item: any, indexA) => {
|
||||||
|
const ItemComponent = item.component || item.Component;
|
||||||
|
let element: ReactNode;
|
||||||
|
const compiledTitle = item.title || item.label ? compile(item.title || item.label) : undefined;
|
||||||
|
if (ItemComponent) {
|
||||||
|
element = React.createElement(SchemaInitializerChild, { ...item, title: compiledTitle });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === 'divider') {
|
||||||
|
return { type: 'divider', key: item.key || `divider-${indexA}` };
|
||||||
|
}
|
||||||
|
if (item.type === 'item' && ItemComponent) {
|
||||||
|
if (!item.key) {
|
||||||
|
item.key = `${compiledTitle}-${indexA}`;
|
||||||
|
}
|
||||||
|
return item.associationField
|
||||||
|
? {
|
||||||
|
key: item.key,
|
||||||
|
label: element,
|
||||||
|
associationField: item.associationField,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
key: item.key,
|
||||||
|
label: element,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (item.type === 'itemGroup') {
|
||||||
|
const label = typeof compiledTitle === 'string' ? compiledTitle : item.title;
|
||||||
|
const key = item.key || `${parentKey}-item-group-${indexA}`;
|
||||||
|
return {
|
||||||
|
type: 'group',
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
// className: styles.nbMenuItemGroup,
|
||||||
|
children: item?.children?.length ? getMenuItems(item.children, key) : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (item.type === 'subMenu') {
|
||||||
|
const label = compiledTitle;
|
||||||
|
const key = item.key || item.name || `${parentKey}-sub-menu-${indexA}`;
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
children: item?.children?.length ? getMenuItems(item.children, key) : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (item.isMenuType) {
|
||||||
|
const { isMenuType, ...menuData } = item;
|
||||||
|
return menuData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = element || compiledTitle;
|
||||||
|
const key = item.key || `${parentKey}-${compiledTitle}-${indexA}`;
|
||||||
|
const handleClick = (info) => {
|
||||||
|
info.domEvent.stopPropagation();
|
||||||
|
if (info.key !== key) return;
|
||||||
|
if (item.onClick) {
|
||||||
|
item.onClick({ ...info, item });
|
||||||
|
} else {
|
||||||
|
onClick?.({ ...info, item });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return item.associationField
|
||||||
|
? {
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
associationField: item.associationField,
|
||||||
|
onClick: handleClick,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
onClick: handleClick,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[compile, onClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
return getMenuItems;
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
import { ButtonProps } from 'antd';
|
||||||
|
import React, { FC, useMemo } from 'react';
|
||||||
|
import { useApp } from '../../hooks';
|
||||||
|
import { SchemaInitializerItems } from '../components';
|
||||||
|
import { SchemaInitializerButton } from '../components/SchemaInitializerButton';
|
||||||
|
import { withInitializer } from '../withInitializer';
|
||||||
|
import { SchemaInitializerOptions } from '../types';
|
||||||
|
|
||||||
|
const InitializerComponent: FC<SchemaInitializerOptions<any, any>> = React.memo((options) => {
|
||||||
|
const Component: any = options.Component || SchemaInitializerButton;
|
||||||
|
|
||||||
|
const ItemsComponent: any = options.ItemsComponent || SchemaInitializerItems;
|
||||||
|
const itemsComponentProps: any = {
|
||||||
|
...options.itemsComponentProps,
|
||||||
|
options,
|
||||||
|
items: options.items,
|
||||||
|
style: options.itemsComponentStyle,
|
||||||
|
};
|
||||||
|
|
||||||
|
const C = useMemo(() => withInitializer(Component), [Component]);
|
||||||
|
|
||||||
|
return React.createElement(C, options, React.createElement(ItemsComponent, itemsComponentProps));
|
||||||
|
});
|
||||||
|
InitializerComponent.displayName = 'InitializerComponent';
|
||||||
|
|
||||||
|
export function useSchemaInitializerRender<P1 = ButtonProps, P2 = {}>(
|
||||||
|
name: string,
|
||||||
|
options?: Omit<SchemaInitializerOptions<P1, P2>, 'name'>,
|
||||||
|
) {
|
||||||
|
const app = useApp();
|
||||||
|
const initializer = useMemo(
|
||||||
|
() => app.schemaInitializerManager.get<P1, P2>(name),
|
||||||
|
[app.schemaInitializerManager, name],
|
||||||
|
);
|
||||||
|
const res = useMemo(() => {
|
||||||
|
if (!name) {
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
render: () => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initializer) {
|
||||||
|
console.error(`[nocobase]: SchemaInitializer "${name}" not found`);
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
render: () => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
exists: true,
|
||||||
|
render: (props?: Omit<SchemaInitializerOptions<P1, P2>, 'name'>) => {
|
||||||
|
return React.createElement(InitializerComponent, {
|
||||||
|
...initializer.options,
|
||||||
|
...options,
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [initializer, name, options]);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
export * from './hoc';
|
export * from './withInitializer';
|
||||||
export * from './hooks';
|
export * from './hooks';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './context';
|
export * from './context';
|
||||||
|
@ -3,12 +3,12 @@ import { ConfigProvider, Popover, theme } from 'antd';
|
|||||||
import React, { ComponentType, useCallback, useMemo, useState } from 'react';
|
import React, { ComponentType, useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useNiceDropdownMaxHeight } from '../../../common/useNiceDropdownHeight';
|
import { useNiceDropdownMaxHeight } from '../../common/useNiceDropdownHeight';
|
||||||
import { useFlag } from '../../../flag-provider';
|
import { useFlag } from '../../flag-provider';
|
||||||
import { useDesignable } from '../../../schema-component';
|
import { useDesignable } from '../../schema-component';
|
||||||
import { useSchemaInitializerStyles } from '../components/style';
|
import { useSchemaInitializerStyles } from './components/style';
|
||||||
import { SchemaInitializerContext } from '../context';
|
import { SchemaInitializerContext } from './context';
|
||||||
import { SchemaInitializerOptions } from '../types';
|
import { SchemaInitializerOptions } from './types';
|
||||||
|
|
||||||
const defaultWrap = (s: ISchema) => s;
|
const defaultWrap = (s: ISchema) => s;
|
||||||
|
|
@ -32,7 +32,8 @@ export class SchemaSettings<T = {}> {
|
|||||||
if (!parentItem.children) {
|
if (!parentItem.children) {
|
||||||
parentItem.children = [];
|
parentItem.children = [];
|
||||||
}
|
}
|
||||||
const index = parentItem.children.findIndex((item: any) => item.name === name);
|
const childrenName = name.replace(`${parentItem.name}.`, '');
|
||||||
|
const index = parentItem.children.findIndex((item: any) => item.name === childrenName);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
parentItem.children.push(data);
|
parentItem.children.push(data);
|
||||||
} else {
|
} else {
|
||||||
@ -42,6 +43,7 @@ export class SchemaSettings<T = {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(nestedName: string): SchemaSettingsItemType | undefined {
|
get(nestedName: string): SchemaSettingsItemType | undefined {
|
||||||
|
if (!nestedName) return undefined;
|
||||||
const arr = nestedName.split('.');
|
const arr = nestedName.split('.');
|
||||||
let current: any = this.items;
|
let current: any = this.items;
|
||||||
|
|
||||||
@ -58,8 +60,6 @@ export class SchemaSettings<T = {}> {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return current;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(nestedName: string) {
|
remove(nestedName: string) {
|
||||||
|
@ -78,7 +78,7 @@ export const SchemaSettingsChild: FC<SchemaSettingsItemType> = memo((props) => {
|
|||||||
type,
|
type,
|
||||||
Component,
|
Component,
|
||||||
children,
|
children,
|
||||||
hideIfNoChildren = true,
|
hideIfNoChildren,
|
||||||
componentProps,
|
componentProps,
|
||||||
} = props as any;
|
} = props as any;
|
||||||
const useChildrenRes = useChildren();
|
const useChildrenRes = useChildren();
|
||||||
@ -100,7 +100,7 @@ export const SchemaSettingsChild: FC<SchemaSettingsItemType> = memo((props) => {
|
|||||||
if (!C) {
|
if (!C) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (hideIfNoChildren && Array.isArray(componentChildren) && componentChildren.length === 0) {
|
if (hideIfNoChildren && !componentChildren) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import { SchemaSettingsItemType } from '../types';
|
||||||
|
|
||||||
|
export const SchemaSettingItemContext = createContext<SchemaSettingsItemType>({} as any);
|
||||||
|
SchemaSettingItemContext.displayName = 'SchemaSettingItemContext';
|
||||||
|
|
||||||
|
export function useSchemaSettingsItem<T = {}>() {
|
||||||
|
return useContext(SchemaSettingItemContext) as T;
|
||||||
|
}
|
@ -1,9 +1 @@
|
|||||||
import { createContext, useContext } from 'react';
|
export * from './SchemaSettingItemContext';
|
||||||
import { SchemaSettingsItemType } from '../types';
|
|
||||||
|
|
||||||
export const SchemaSettingItemContext = createContext<SchemaSettingsItemType>({} as any);
|
|
||||||
SchemaSettingItemContext.displayName = 'SchemaSettingItemContext';
|
|
||||||
|
|
||||||
export function useSchemaSettingsItem() {
|
|
||||||
return useContext(SchemaSettingItemContext) as SchemaSettingsItemType;
|
|
||||||
}
|
|
||||||
|
@ -1,50 +1 @@
|
|||||||
import { useMemo } from 'react';
|
export * from './useSchemaSettingsRender';
|
||||||
import { useApp } from '../../hooks';
|
|
||||||
import { SchemaSettingOptions } from '../types';
|
|
||||||
import React from 'react';
|
|
||||||
import { SchemaSettingsWrapper } from '../components';
|
|
||||||
import { SchemaSettingsProps } from '../../../schema-settings';
|
|
||||||
import { Schema } from '@formily/json-schema';
|
|
||||||
import { GeneralField } from '@formily/core';
|
|
||||||
import { Designable } from '../../../schema-component';
|
|
||||||
|
|
||||||
type UseSchemaSettingsRenderOptions<T = {}> = Omit<SchemaSettingOptions<T>, 'name' | 'items'> &
|
|
||||||
Omit<SchemaSettingsProps, 'title' | 'children'> & {
|
|
||||||
fieldSchema?: Schema;
|
|
||||||
field?: GeneralField;
|
|
||||||
dn?: Designable;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useSchemaSettingsRender<T = {}>(name: string, options?: UseSchemaSettingsRenderOptions<T>) {
|
|
||||||
const app = useApp();
|
|
||||||
const schemaSetting = useMemo(() => app.schemaSettingsManager.get<T>(name), [app.schemaSettingsManager, name]);
|
|
||||||
const renderCache = React.useRef<Record<string, React.FunctionComponentElement<any>>>({});
|
|
||||||
if (!name) {
|
|
||||||
return {
|
|
||||||
exists: false,
|
|
||||||
render: () => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!schemaSetting) {
|
|
||||||
console.error(`[nocobase]: SchemaSettings "${name}" not found`);
|
|
||||||
return {
|
|
||||||
exists: false,
|
|
||||||
render: () => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
exists: true,
|
|
||||||
render: (options2: UseSchemaSettingsRenderOptions = {}) => {
|
|
||||||
const key = JSON.stringify(options2);
|
|
||||||
if (key && renderCache.current[key]) {
|
|
||||||
return renderCache.current[key];
|
|
||||||
}
|
|
||||||
return (renderCache.current[key] = React.createElement(SchemaSettingsWrapper, {
|
|
||||||
...schemaSetting.options,
|
|
||||||
...options,
|
|
||||||
...options2,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useApp } from '../../hooks';
|
||||||
|
import { SchemaSettingOptions } from '../types';
|
||||||
|
import React from 'react';
|
||||||
|
import { SchemaSettingsWrapper } from '../components';
|
||||||
|
import { SchemaSettingsProps } from '../../../schema-settings';
|
||||||
|
import { Schema } from '@formily/json-schema';
|
||||||
|
import { GeneralField } from '@formily/core';
|
||||||
|
import { Designable } from '../../../schema-component';
|
||||||
|
|
||||||
|
type UseSchemaSettingsRenderOptions<T = {}> = Omit<SchemaSettingOptions<T>, 'name' | 'items'> &
|
||||||
|
Omit<SchemaSettingsProps, 'title' | 'children'> & {
|
||||||
|
fieldSchema?: Schema;
|
||||||
|
field?: GeneralField;
|
||||||
|
dn?: Designable;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useSchemaSettingsRender<T = {}>(name: string, options?: UseSchemaSettingsRenderOptions<T>) {
|
||||||
|
const app = useApp();
|
||||||
|
const schemaSetting = useMemo(() => app.schemaSettingsManager.get<T>(name), [app.schemaSettingsManager, name]);
|
||||||
|
if (!name) {
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
render: () => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!schemaSetting) {
|
||||||
|
console.error(`[nocobase]: SchemaSettings "${name}" not found`);
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
render: () => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
exists: true,
|
||||||
|
render: (options2: UseSchemaSettingsRenderOptions = {}) => {
|
||||||
|
return React.createElement(SchemaSettingsWrapper, {
|
||||||
|
...schemaSetting.options,
|
||||||
|
...options,
|
||||||
|
...options2,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -94,6 +94,9 @@ describe('AssociationProvider', () => {
|
|||||||
};
|
};
|
||||||
renderApp(Demo, { name: 'users.not-exists' });
|
renderApp(Demo, { name: 'users.not-exists' });
|
||||||
|
|
||||||
expect(document.body.innerHTML).toContain('ant-result');
|
expect(screen.getByText('Delete')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText('The collection "users.not-exists" may have been deleted. Please remove this block.'),
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Application, CollectionManager, CollectionTemplate, Collection } from '@nocobase/client';
|
import { Application, CollectionManager, CollectionTemplate, Collection } from '@nocobase/client';
|
||||||
import collections from '../collections.json';
|
import collections from '../collections.json';
|
||||||
import { app } from '../../../application/demos/demo3';
|
|
||||||
|
|
||||||
describe('CollectionManager', () => {
|
describe('CollectionManager', () => {
|
||||||
let collectionManager: CollectionManager;
|
let collectionManager: CollectionManager;
|
||||||
@ -227,4 +226,71 @@ describe('CollectionManager', () => {
|
|||||||
expect(clone.dataSource).toBe(collectionManager.dataSource);
|
expect(clone.dataSource).toBe(collectionManager.dataSource);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getSourceKeyByAssociation', () => {
|
||||||
|
let collectionManager: CollectionManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const app = new Application({
|
||||||
|
dataSourceManager: {
|
||||||
|
collections: collections as any,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
collectionManager = app.getCollectionManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getSourceKeyByAssociation', () => {
|
||||||
|
it('should return undefined when associationName is not provided', () => {
|
||||||
|
const result = collectionManager.getSourceKeyByAssociation('');
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when field is not found', () => {
|
||||||
|
const result = collectionManager.getSourceKeyByAssociation('not-exist');
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
|
||||||
|
const result2 = collectionManager.getSourceKeyByAssociation('users.not-exist');
|
||||||
|
expect(result2).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return field sourceKey when it exists', () => {
|
||||||
|
const result = collectionManager.getSourceKeyByAssociation('users.roles');
|
||||||
|
expect(result).toBe('id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return source collection filterTargetKey when field sourceKey does not exist', () => {
|
||||||
|
const field = collectionManager.getCollectionField('users.roles');
|
||||||
|
const users = collectionManager.getCollection('users');
|
||||||
|
|
||||||
|
delete field.sourceKey;
|
||||||
|
const options = users.getOptions();
|
||||||
|
options.filterTargetKey = 'filterTargetKey';
|
||||||
|
|
||||||
|
const result = collectionManager.getSourceKeyByAssociation('users.roles');
|
||||||
|
expect(result).toBe('filterTargetKey');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return source collection primary key when field sourceKey and filterTargetKey do not exist', () => {
|
||||||
|
const field = collectionManager.getCollectionField('users.roles');
|
||||||
|
|
||||||
|
delete field.sourceKey;
|
||||||
|
|
||||||
|
const result = collectionManager.getSourceKeyByAssociation('users.roles');
|
||||||
|
expect(result).toBe('id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "id" when field sourceKey, filterTargetKey, and primary key do not exist', () => {
|
||||||
|
const field = collectionManager.getCollectionField('users.roles');
|
||||||
|
const collection = collectionManager.getCollection('roles');
|
||||||
|
const roleName = collectionManager.getCollectionField('roles.name');
|
||||||
|
|
||||||
|
delete field.sourceKey;
|
||||||
|
delete roleName.primaryKey;
|
||||||
|
delete collection.getOptions().filterTargetKey;
|
||||||
|
|
||||||
|
const result = collectionManager.getSourceKeyByAssociation('users.roles');
|
||||||
|
expect(result).toBe('id');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -84,7 +84,7 @@ describe('CollectionManagerProvider', () => {
|
|||||||
|
|
||||||
const Wrapper = () => {
|
const Wrapper = () => {
|
||||||
return (
|
return (
|
||||||
<CollectionManagerProvider collections={[collections[1] as any]}>
|
<CollectionManagerProvider>
|
||||||
<Demo></Demo>
|
<Demo></Demo>
|
||||||
</CollectionManagerProvider>
|
</CollectionManagerProvider>
|
||||||
);
|
);
|
||||||
|
@ -80,7 +80,7 @@ describe('CollectionProvider', () => {
|
|||||||
|
|
||||||
renderApp(Demo, { name: 'not-exists', allowNull: false });
|
renderApp(Demo, { name: 'not-exists', allowNull: false });
|
||||||
|
|
||||||
expect(document.body.innerHTML).toContain('ant-result');
|
expect(screen.getByText('Delete')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('useCollectionFields() support predicate', () => {
|
test('useCollectionFields() support predicate', () => {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { render, screen } from '@nocobase/test/client';
|
import { render, screen, userEvent, waitFor } from '@nocobase/test/client';
|
||||||
import { CollectionDeletedPlaceholder, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
import { CollectionDeletedPlaceholder, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import { App } from 'antd';
|
||||||
|
|
||||||
function renderApp(name?: any, designable?: boolean) {
|
function renderApp(name?: any, designable?: boolean) {
|
||||||
const schema = {
|
const schema = {
|
||||||
@ -16,31 +17,45 @@ function renderApp(name?: any, designable?: boolean) {
|
|||||||
|
|
||||||
render(
|
render(
|
||||||
<div data-testid="app">
|
<div data-testid="app">
|
||||||
|
<App>
|
||||||
<SchemaComponentProvider designable={designable}>
|
<SchemaComponentProvider designable={designable}>
|
||||||
<SchemaComponent schema={schema} components={{ CollectionDeletedPlaceholder }} />
|
<SchemaComponent schema={schema} components={{ CollectionDeletedPlaceholder }} />
|
||||||
</SchemaComponentProvider>
|
</SchemaComponentProvider>
|
||||||
|
</App>
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('CollectionDeletedPlaceholder', () => {
|
describe('CollectionDeletedPlaceholder', () => {
|
||||||
test('name is undefined, render `Result` component', () => {
|
test('name is undefined, render `Result` component', async () => {
|
||||||
renderApp(undefined, true);
|
renderApp(undefined, true);
|
||||||
|
|
||||||
expect(document.body.innerHTML).toContain('ant-result');
|
expect(screen.getByText('Delete')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Collection name is required')).toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByText('Delete'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Are you sure you want to delete it?')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('OK'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Delete')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('name exist', () => {
|
|
||||||
test('designable: true, render `Result` component', () => {
|
test('designable: true, render `Result` component', () => {
|
||||||
renderApp('test', true);
|
renderApp('test', true);
|
||||||
|
expect(screen.getByText('Delete')).toBeInTheDocument();
|
||||||
expect(document.body.innerHTML).toContain('ant-result');
|
expect(
|
||||||
|
screen.getByText('The collection "test" may have been deleted. Please remove this block.'),
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('designable: false, render nothing', () => {
|
test('designable: false, render nothing', () => {
|
||||||
renderApp('test', false);
|
renderApp('test', false);
|
||||||
|
|
||||||
expect(screen.getByTestId('app').innerHTML.length).toBe(0);
|
expect(screen.queryByText('Delete')).not.toBeInTheDocument();
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
import { Application, Collection, DEFAULT_DATA_SOURCE_KEY, LocalDataSource, Plugin } from '@nocobase/client';
|
import {
|
||||||
|
Application,
|
||||||
|
Collection,
|
||||||
|
DEFAULT_DATA_SOURCE_KEY,
|
||||||
|
DataSourceOptions,
|
||||||
|
LocalDataSource,
|
||||||
|
Plugin,
|
||||||
|
} from '@nocobase/client';
|
||||||
import collections from '../collections.json';
|
import collections from '../collections.json';
|
||||||
|
|
||||||
describe('DataSourceManager', () => {
|
describe('DataSourceManager', () => {
|
||||||
@ -119,6 +126,24 @@ describe('DataSourceManager', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getDataSources', () => {
|
||||||
|
test('filter', () => {
|
||||||
|
const app = new Application({
|
||||||
|
dataSourceManager: {
|
||||||
|
dataSources: [
|
||||||
|
{
|
||||||
|
key: 'a',
|
||||||
|
displayName: 'a',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(app.dataSourceManager.getDataSources()).toHaveLength(2);
|
||||||
|
expect(app.dataSourceManager.getDataSources((dataSource) => dataSource.key === 'a')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('addDataSource', () => {
|
describe('addDataSource', () => {
|
||||||
test('should add a data source', () => {
|
test('should add a data source', () => {
|
||||||
const app = new Application();
|
const app = new Application();
|
||||||
@ -184,5 +209,32 @@ describe('DataSourceManager', () => {
|
|||||||
expect(reloadSpy1).toHaveBeenCalledTimes(1);
|
expect(reloadSpy1).toHaveBeenCalledTimes(1);
|
||||||
expect(reloadSpy2).toHaveBeenCalledTimes(1);
|
expect(reloadSpy2).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('multi data sources', async () => {
|
||||||
|
const app = new Application();
|
||||||
|
const dataSourceManager = app.dataSourceManager;
|
||||||
|
const getThirdDataSource = (): Promise<DataSourceOptions[]> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve([
|
||||||
|
{
|
||||||
|
key: 'a',
|
||||||
|
displayName: 'a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'b',
|
||||||
|
displayName: 'b',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
dataSourceManager.addDataSources(getThirdDataSource, LocalDataSource);
|
||||||
|
|
||||||
|
await dataSourceManager.reload();
|
||||||
|
|
||||||
|
expect(dataSourceManager.getDataSources()).toHaveLength(3);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { DataSourceManagerProvider, useDataSourceManager } from '@nocobase/client';
|
import { Application, DataSourceManagerProvider, useDataSourceManager } from '@nocobase/client';
|
||||||
|
|
||||||
describe('DataSourceManagerProvider', () => {
|
describe('DataSourceManagerProvider', () => {
|
||||||
test('should render children', () => {
|
test('should render children', () => {
|
||||||
|
@ -5,10 +5,9 @@ import {
|
|||||||
DataSourceOptions,
|
DataSourceOptions,
|
||||||
DataSource,
|
DataSource,
|
||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
SchemaComponentProvider,
|
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { DataSourceProvider, useDataSourceKey } from '../../data-source/DataSourceProvider';
|
import { DataSourceProvider, useDataSourceKey } from '../../data-source/DataSourceProvider';
|
||||||
import { render, screen } from '@nocobase/test/client';
|
import { render, screen, userEvent, waitFor } from '@nocobase/test/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AppSchemaComponentProvider } from '../../../application/AppSchemaComponentProvider';
|
import { AppSchemaComponentProvider } from '../../../application/AppSchemaComponentProvider';
|
||||||
|
|
||||||
@ -54,6 +53,8 @@ describe('DataSourceProvider', () => {
|
|||||||
</DataSourceManagerProvider>
|
</DataSourceManagerProvider>
|
||||||
</AppSchemaComponentProvider>,
|
</AppSchemaComponentProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return app.dataSourceManager.getDataSource(dataSource);
|
||||||
}
|
}
|
||||||
it('should render default dataSource', () => {
|
it('should render default dataSource', () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
@ -67,12 +68,34 @@ describe('DataSourceProvider', () => {
|
|||||||
|
|
||||||
it('should render error state when data source is not found', () => {
|
it('should render error state when data source is not found', () => {
|
||||||
renderComponent('non-existent');
|
renderComponent('non-existent');
|
||||||
expect(document.querySelector('.ant-result')).toBeInTheDocument();
|
expect(screen.getByText('Delete')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText('The data source "non-existent" may have been deleted. Please remove this block.'),
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render loading state when data source is loading', () => {
|
it('should render loading state when data source is loading', async () => {
|
||||||
renderComponent('test', 'loading');
|
const ds = renderComponent('test', 'loading');
|
||||||
|
const fn = vitest.spyOn(ds, 'reload');
|
||||||
expect(screen.getByText('Test data source loading...')).toBeInTheDocument();
|
expect(screen.getByText('Test data source loading...')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Refresh'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fn).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render loading state when data source is loading', async () => {
|
||||||
|
const ds = renderComponent('test', 'reloading');
|
||||||
|
const fn = vitest.spyOn(ds, 'reload');
|
||||||
|
expect(screen.getByText('Test data source loading...')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Refresh'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fn).toBeCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render error state when data source loading fails', () => {
|
it('should render error state when data source loading fails', () => {
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { CollectionFieldInterface, isTitleField, Application } from '@nocobase/client';
|
import {
|
||||||
|
CollectionFieldInterface,
|
||||||
|
isTitleField,
|
||||||
|
Application,
|
||||||
|
useDataSourceHeaders,
|
||||||
|
DEFAULT_DATA_SOURCE_KEY,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { renderHook } from '@nocobase/test/client';
|
||||||
|
|
||||||
import collections from './collections.json';
|
import collections from './collections.json';
|
||||||
|
|
||||||
describe('utils', () => {
|
describe('utils', () => {
|
||||||
@ -42,4 +50,50 @@ describe('utils', () => {
|
|||||||
expect(isTitleField(dm, field)).toBeTruthy();
|
expect(isTitleField(dm, field)).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('useDataSourceHeaders', () => {
|
||||||
|
test('should return undefined when dataSource is not provided', () => {
|
||||||
|
const { result } = renderHook(() => useDataSourceHeaders());
|
||||||
|
|
||||||
|
expect(result.current).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return undefined when dataSource is the default key', () => {
|
||||||
|
const { result } = renderHook(() => useDataSourceHeaders(DEFAULT_DATA_SOURCE_KEY));
|
||||||
|
|
||||||
|
expect(result.current).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return correct headers when dataSource is provided and not the default key', () => {
|
||||||
|
const dataSource = 'customDataSource';
|
||||||
|
const { result } = renderHook(() => useDataSourceHeaders(dataSource));
|
||||||
|
|
||||||
|
expect(result.current).toEqual({ 'x-data-source': dataSource });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return undefined when dataSource is an empty string', () => {
|
||||||
|
const { result } = renderHook(() => useDataSourceHeaders(''));
|
||||||
|
|
||||||
|
expect(result.current).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return undefined when dataSource is null', () => {
|
||||||
|
// This test will always pass in TypeScript since the type doesn't allow null, but included for comprehensive testing if the dataSource type changes.
|
||||||
|
const { result } = renderHook(() => useDataSourceHeaders(null as unknown as string));
|
||||||
|
|
||||||
|
expect(result.current).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle dataSource change', () => {
|
||||||
|
const { result, rerender } = renderHook(({ dataSource }) => useDataSourceHeaders(dataSource), {
|
||||||
|
initialProps: { dataSource: 'initialDataSource' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current).toEqual({ 'x-data-source': 'initialDataSource' });
|
||||||
|
|
||||||
|
// Change the dataSource prop
|
||||||
|
rerender({ dataSource: 'updatedDataSource' });
|
||||||
|
expect(result.current).toEqual({ 'x-data-source': 'updatedDataSource' });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -37,7 +37,6 @@ export class CollectionFieldInterfaceManager {
|
|||||||
if (item.notSupportDataSourceType) {
|
if (item.notSupportDataSourceType) {
|
||||||
return !item.notSupportDataSourceType?.includes(dataSourceType);
|
return !item.notSupportDataSourceType?.includes(dataSourceType);
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,6 @@ export class CollectionTemplateManager {
|
|||||||
if (item.notSupportDataSourceType) {
|
if (item.notSupportDataSourceType) {
|
||||||
return !item.notSupportDataSourceType?.includes(dataSourceType);
|
return !item.notSupportDataSourceType?.includes(dataSourceType);
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
})
|
})
|
||||||
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||||
}
|
}
|
||||||
|
@ -131,7 +131,7 @@ export class CollectionManager {
|
|||||||
return this.getCollection(collectionName)?.getFields(predicate) || [];
|
return this.getCollection(collectionName)?.getFields(predicate) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getSourceKeyByAssocation(associationName: string) {
|
getSourceKeyByAssociation(associationName: string) {
|
||||||
if (!associationName) {
|
if (!associationName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import { DEFAULT_DATA_SOURCE_KEY } from '../../data-source/data-source/DataSourc
|
|||||||
import { useCollection } from '../collection';
|
import { useCollection } from '../collection';
|
||||||
|
|
||||||
export interface CollectionDeletedPlaceholderProps {
|
export interface CollectionDeletedPlaceholderProps {
|
||||||
type: 'Collection' | 'Field' | 'DataSource';
|
type: 'Collection' | 'Field' | 'Data Source' | 'Block template';
|
||||||
name?: string | number;
|
name?: string | number;
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
@ -60,7 +60,7 @@ export const CollectionDeletedPlaceholder: FC<CollectionDeletedPlaceholderProps>
|
|||||||
return t(`The {{type}} "{{name}}" may have been deleted. Please remove this {{blockType}}.`, {
|
return t(`The {{type}} "{{name}}" may have been deleted. Please remove this {{blockType}}.`, {
|
||||||
type: t(type).toLocaleLowerCase(),
|
type: t(type).toLocaleLowerCase(),
|
||||||
name: nameValue,
|
name: nameValue,
|
||||||
blockType: t(blockType),
|
blockType: t(blockType).toLocaleLowerCase(),
|
||||||
}).replaceAll('>', '>');
|
}).replaceAll('>', '>');
|
||||||
}, [message, nameValue, type, t, blockType]);
|
}, [message, nameValue, type, t, blockType]);
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ export const DataBlockResourceProvider: FC<{ children?: ReactNode }> = ({ childr
|
|||||||
return sourceId;
|
return sourceId;
|
||||||
}
|
}
|
||||||
if (association && parentRecord) {
|
if (association && parentRecord) {
|
||||||
const sourceKey = cm.getSourceKeyByAssocation(association);
|
const sourceKey = cm.getSourceKeyByAssociation(association);
|
||||||
const parentRecordData = parentRecord instanceof CollectionRecord ? parentRecord.data : parentRecord;
|
const parentRecordData = parentRecord instanceof CollectionRecord ? parentRecord.data : parentRecord;
|
||||||
return parentRecordData[sourceKey];
|
return parentRecordData[sourceKey];
|
||||||
}
|
}
|
||||||
|
@ -56,9 +56,9 @@ export class DataSourceManager {
|
|||||||
this.getDataSources().forEach((dataSource) => dataSource.collectionManager.reAddCollections());
|
this.getDataSources().forEach((dataSource) => dataSource.collectionManager.reAddCollections());
|
||||||
}
|
}
|
||||||
|
|
||||||
getDataSources(filterCollection?: (dataSource: DataSource) => boolean) {
|
getDataSources(filterDataSource?: (dataSource: DataSource) => boolean) {
|
||||||
const allDataSources = Object.values(this.dataSourceInstancesMap);
|
const allDataSources = Object.values(this.dataSourceInstancesMap);
|
||||||
return filterCollection ? _.filter(allDataSources, filterCollection) : allDataSources;
|
return filterDataSource ? _.filter(allDataSources, filterDataSource) : allDataSources;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDataSource(key?: string) {
|
getDataSource(key?: string) {
|
||||||
|
@ -500,6 +500,7 @@
|
|||||||
"Save as template": "Save as template",
|
"Save as template": "Save as template",
|
||||||
"Save as block template": "Save as block template",
|
"Save as block template": "Save as block template",
|
||||||
"Block templates": "Block templates",
|
"Block templates": "Block templates",
|
||||||
|
"Block template": "Block template",
|
||||||
"Convert reference to duplicate": "Convert reference to duplicate",
|
"Convert reference to duplicate": "Convert reference to duplicate",
|
||||||
"Template name": "Template name",
|
"Template name": "Template name",
|
||||||
"Block type": "Block type",
|
"Block type": "Block type",
|
||||||
@ -801,6 +802,7 @@
|
|||||||
"loading": "loading",
|
"loading": "loading",
|
||||||
"name is required": "name is required",
|
"name is required": "name is required",
|
||||||
"data source": "data source",
|
"data source": "data source",
|
||||||
|
"Data source": "Data source",
|
||||||
"DataSource": "DataSource",
|
"DataSource": "DataSource",
|
||||||
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.",
|
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.",
|
||||||
"Preset fields": "Preset fields",
|
"Preset fields": "Preset fields",
|
||||||
|
@ -471,6 +471,7 @@
|
|||||||
"Save as template": "Guardar como plantilla",
|
"Save as template": "Guardar como plantilla",
|
||||||
"Save as block template": "Guardar como plantilla de bloque",
|
"Save as block template": "Guardar como plantilla de bloque",
|
||||||
"Block templates": "Bloquear plantillas",
|
"Block templates": "Bloquear plantillas",
|
||||||
|
"Block template": "Plantilla de bloque",
|
||||||
"Convert reference to duplicate": "Convertir referencia a duplicado",
|
"Convert reference to duplicate": "Convertir referencia a duplicado",
|
||||||
"Template name": "Nombre de plantilla",
|
"Template name": "Nombre de plantilla",
|
||||||
"Block type": "Tipo de bloque",
|
"Block type": "Tipo de bloque",
|
||||||
@ -749,6 +750,7 @@
|
|||||||
"name is required": "el nombre es obligatorio",
|
"name is required": "el nombre es obligatorio",
|
||||||
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "El {{type}} \"{{name}}\" puede haber sido eliminado. Por favor, elimine este {{blockType}}.",
|
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "El {{type}} \"{{name}}\" puede haber sido eliminado. Por favor, elimine este {{blockType}}.",
|
||||||
"data source": "fuente de datos",
|
"data source": "fuente de datos",
|
||||||
|
"Data source": "fuente de datos",
|
||||||
"DataSource": "Fuente de datos",
|
"DataSource": "Fuente de datos",
|
||||||
"Home page": "Página de inicio",
|
"Home page": "Página de inicio",
|
||||||
"Handbook": "Manual de usuario",
|
"Handbook": "Manual de usuario",
|
||||||
|
@ -486,6 +486,7 @@
|
|||||||
"Save as template": "Enregistrer en tant que modèle",
|
"Save as template": "Enregistrer en tant que modèle",
|
||||||
"Save as block template": "Enregistrer en tant que modèle de bloc",
|
"Save as block template": "Enregistrer en tant que modèle de bloc",
|
||||||
"Block templates": "Modèles de bloc",
|
"Block templates": "Modèles de bloc",
|
||||||
|
"Block template": "Modèle de bloc",
|
||||||
"Convert reference to duplicate": "Convertir la référence en doublon",
|
"Convert reference to duplicate": "Convertir la référence en doublon",
|
||||||
"Template name": "Nom du modèle",
|
"Template name": "Nom du modèle",
|
||||||
"Block type": "Type de bloc",
|
"Block type": "Type de bloc",
|
||||||
@ -767,6 +768,7 @@
|
|||||||
"loading": "chargement",
|
"loading": "chargement",
|
||||||
"name is required": "le nom est requis",
|
"name is required": "le nom est requis",
|
||||||
"data source": "source de données",
|
"data source": "source de données",
|
||||||
|
"Data source": "source de données",
|
||||||
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "Le {{type}} \"{{name}}\" a peut-être été supprimé. Veuillez supprimer ce {{blockType}}.",
|
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "Le {{type}} \"{{name}}\" a peut-être été supprimé. Veuillez supprimer ce {{blockType}}.",
|
||||||
"DataSource": "Source de données",
|
"DataSource": "Source de données",
|
||||||
"Allow selection of existing records":"Permet de sélectionner des données existantes",
|
"Allow selection of existing records":"Permet de sélectionner des données existantes",
|
||||||
|
@ -398,6 +398,7 @@
|
|||||||
"Save as template": "テンプレートとして保存",
|
"Save as template": "テンプレートとして保存",
|
||||||
"Save as block template": "ブロックテンプレートとして保存",
|
"Save as block template": "ブロックテンプレートとして保存",
|
||||||
"Block templates": "ブロックテンプレート",
|
"Block templates": "ブロックテンプレート",
|
||||||
|
"Block template": "ブロックテンプレート",
|
||||||
"Convert reference to duplicate": "参照を複製に変換",
|
"Convert reference to duplicate": "参照を複製に変換",
|
||||||
"Template name": "テンプレート名",
|
"Template name": "テンプレート名",
|
||||||
"Block type": "ブロックタイプ",
|
"Block type": "ブロックタイプ",
|
||||||
@ -687,8 +688,9 @@
|
|||||||
"loading": "ロード中",
|
"loading": "ロード中",
|
||||||
"name is required": "名前は必須です",
|
"name is required": "名前は必須です",
|
||||||
"data source": "データソース",
|
"data source": "データソース",
|
||||||
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" は削除されている可能性があります。この {{blockType}} を削除してください。",
|
"Data source": "データソース",
|
||||||
"DataSource": "データソース",
|
"DataSource": "データソース",
|
||||||
|
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" は削除されている可能性があります。この {{blockType}} を削除してください。",
|
||||||
"Home page": "ホームページ",
|
"Home page": "ホームページ",
|
||||||
"Handbook": "ユーザーマニュアル",
|
"Handbook": "ユーザーマニュアル",
|
||||||
"License": "ライセンス"
|
"License": "ライセンス"
|
||||||
|
@ -518,6 +518,7 @@
|
|||||||
"Save as template": "템플릿으로 저장",
|
"Save as template": "템플릿으로 저장",
|
||||||
"Save as block template": "블록 템플릿으로 저장",
|
"Save as block template": "블록 템플릿으로 저장",
|
||||||
"Block templates": "블록 템플릿",
|
"Block templates": "블록 템플릿",
|
||||||
|
"Block template": "블록 템플릿",
|
||||||
"Convert reference to duplicate": "참조를 복제로 변환",
|
"Convert reference to duplicate": "참조를 복제로 변환",
|
||||||
"Template name": "템플릿 이름",
|
"Template name": "템플릿 이름",
|
||||||
"Block type": "블록 타입",
|
"Block type": "블록 타입",
|
||||||
@ -859,8 +860,9 @@
|
|||||||
"loading": "로드 중",
|
"loading": "로드 중",
|
||||||
"name is required": "이름은 필수입니다",
|
"name is required": "이름은 필수입니다",
|
||||||
"data source": "데이터 소스",
|
"data source": "데이터 소스",
|
||||||
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\"이(가) 삭제되었을 수 있습니다. 이 {{blockType}}을(를) 제거하십시오.",
|
"Data source": "데이터 소스",
|
||||||
"DataSource": "데이터 소스",
|
"DataSource": "데이터 소스",
|
||||||
|
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\"이(가) 삭제되었을 수 있습니다. 이 {{blockType}}을(를) 제거하십시오.",
|
||||||
"Home page": "홈페이지",
|
"Home page": "홈페이지",
|
||||||
"Handbook": "사용자 매뉴얼",
|
"Handbook": "사용자 매뉴얼",
|
||||||
"License": "라이선스"
|
"License": "라이선스"
|
||||||
|
@ -434,6 +434,7 @@
|
|||||||
"Save as template": "Salvar como modelo",
|
"Save as template": "Salvar como modelo",
|
||||||
"Save as block template": "Salvar como modelo de bloco",
|
"Save as block template": "Salvar como modelo de bloco",
|
||||||
"Block templates": "Modelos de bloco",
|
"Block templates": "Modelos de bloco",
|
||||||
|
"Block template": "Modelo de bloco",
|
||||||
"Convert reference to duplicate": "Converter referência em duplicado",
|
"Convert reference to duplicate": "Converter referência em duplicado",
|
||||||
"Template name": "Nome do modelo",
|
"Template name": "Nome do modelo",
|
||||||
"Block type": "Tipo de bloco",
|
"Block type": "Tipo de bloco",
|
||||||
@ -726,8 +727,9 @@
|
|||||||
"loading": "carregando",
|
"loading": "carregando",
|
||||||
"name is required": "nome é obrigatório",
|
"name is required": "nome é obrigatório",
|
||||||
"data source": "fonte de dados",
|
"data source": "fonte de dados",
|
||||||
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "O {{type}} \"{{name}}\" pode ter sido excluído. Por favor, remova este {{blockType}}.",
|
"Data source": "fonte de dados",
|
||||||
"DataSource": "Fonte de dados",
|
"DataSource": "Fonte de dados",
|
||||||
|
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "O {{type}} \"{{name}}\" pode ter sido excluído. Por favor, remova este {{blockType}}.",
|
||||||
"Allow selection of existing records":"Permitir a selecção dos registos existentes",
|
"Allow selection of existing records":"Permitir a selecção dos registos existentes",
|
||||||
"Home page": "Página inicial",
|
"Home page": "Página inicial",
|
||||||
"Handbook": "Manual do usuário",
|
"Handbook": "Manual do usuário",
|
||||||
|
@ -337,7 +337,7 @@
|
|||||||
"Other records": "Другие записи",
|
"Other records": "Другие записи",
|
||||||
"Save as template": "Сохранить как шаблон",
|
"Save as template": "Сохранить как шаблон",
|
||||||
"Save as block template": "Сохранить как шаблон Блока",
|
"Save as block template": "Сохранить как шаблон Блока",
|
||||||
"Block templates": "Шаблоны Блока",
|
"Block templates": "Шаблоны блоков",
|
||||||
"Convert reference to duplicate": "Преобразовать ссылку в дубликат",
|
"Convert reference to duplicate": "Преобразовать ссылку в дубликат",
|
||||||
"Template name": "Имя Шаблона",
|
"Template name": "Имя Шаблона",
|
||||||
"Block type": "Тип Блока",
|
"Block type": "Тип Блока",
|
||||||
@ -563,6 +563,7 @@
|
|||||||
"loading": "загрузка",
|
"loading": "загрузка",
|
||||||
"name is required": "имя обязательно",
|
"name is required": "имя обязательно",
|
||||||
"data source": "источник данных",
|
"data source": "источник данных",
|
||||||
|
"Data source": "источник данных",
|
||||||
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" возможно был удален. Пожалуйста, удалите этот {{blockType}}.",
|
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" возможно был удален. Пожалуйста, удалите этот {{blockType}}.",
|
||||||
"DataSource": "Источник данных",
|
"DataSource": "Источник данных",
|
||||||
"Home page": "Домашняя страница",
|
"Home page": "Домашняя страница",
|
||||||
|
@ -337,6 +337,7 @@
|
|||||||
"Save as template": "Şablon olarak kaydet",
|
"Save as template": "Şablon olarak kaydet",
|
||||||
"Save as block template": "Blok şablonu olarak kaydet",
|
"Save as block template": "Blok şablonu olarak kaydet",
|
||||||
"Block templates": "Blok şablonları",
|
"Block templates": "Blok şablonları",
|
||||||
|
"Block template": "Blok şablonu",
|
||||||
"Convert reference to duplicate": "Referansı kopyaya dönüştür",
|
"Convert reference to duplicate": "Referansı kopyaya dönüştür",
|
||||||
"Template name": "Şablon adı",
|
"Template name": "Şablon adı",
|
||||||
"Block type": "Blok türü",
|
"Block type": "Blok türü",
|
||||||
@ -560,8 +561,9 @@
|
|||||||
"loading": "yükleniyor",
|
"loading": "yükleniyor",
|
||||||
"name is required": "ad gereklidir",
|
"name is required": "ad gereklidir",
|
||||||
"data source": "veri kaynağı",
|
"data source": "veri kaynağı",
|
||||||
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" silinmiş olabilir. Lütfen bu {{blockType}}'yi kaldırın.",
|
"Data source": "veri kaynağı",
|
||||||
"DataSource": "Veri Kaynağı",
|
"DataSource": "Veri Kaynağı",
|
||||||
|
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" silinmiş olabilir. Lütfen bu {{blockType}}'yi kaldırın.",
|
||||||
"Home page": "Anasayfa",
|
"Home page": "Anasayfa",
|
||||||
"Handbook": "Kullanıcı kılavuzu",
|
"Handbook": "Kullanıcı kılavuzu",
|
||||||
"License": "Lisans"
|
"License": "Lisans"
|
||||||
|
@ -488,6 +488,7 @@
|
|||||||
"Save as template": "Зберегти як шаблон",
|
"Save as template": "Зберегти як шаблон",
|
||||||
"Save as block template": "Зберегти як шаблон блока",
|
"Save as block template": "Зберегти як шаблон блока",
|
||||||
"Block templates": "Шаблони блоків",
|
"Block templates": "Шаблони блоків",
|
||||||
|
"Block template": "Шаблон блока",
|
||||||
"Convert reference to duplicate": "Конвертувати посилання на дублікат",
|
"Convert reference to duplicate": "Конвертувати посилання на дублікат",
|
||||||
"Template name": "Назва шаблону",
|
"Template name": "Назва шаблону",
|
||||||
"Block type": "Тип блока",
|
"Block type": "Тип блока",
|
||||||
@ -768,8 +769,9 @@
|
|||||||
"loading": "завантаження",
|
"loading": "завантаження",
|
||||||
"name is required": "ім'я обов'язкове",
|
"name is required": "ім'я обов'язкове",
|
||||||
"data source": "джерело даних",
|
"data source": "джерело даних",
|
||||||
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" може бути видалено. Будь ласка, видаліть цей {{blockType}}.",
|
"Data source": "джерело даних",
|
||||||
"DataSource": "Джерело даних",
|
"DataSource": "Джерело даних",
|
||||||
|
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" може бути видалено. Будь ласка, видаліть цей {{blockType}}.",
|
||||||
"Home page": "Домашня сторінка",
|
"Home page": "Домашня сторінка",
|
||||||
"Handbook": "Посібник користувача",
|
"Handbook": "Посібник користувача",
|
||||||
"License": "Ліцензія"
|
"License": "Ліцензія"
|
||||||
|
@ -521,6 +521,7 @@
|
|||||||
"Save as template": "保存为模板",
|
"Save as template": "保存为模板",
|
||||||
"Save as block template": "保存为区块模板",
|
"Save as block template": "保存为区块模板",
|
||||||
"Block templates": "区块模板",
|
"Block templates": "区块模板",
|
||||||
|
"Block template": "区块模板",
|
||||||
"Convert reference to duplicate": "模板引用转为复制",
|
"Convert reference to duplicate": "模板引用转为复制",
|
||||||
"Template name": "模板名称",
|
"Template name": "模板名称",
|
||||||
"Block type": "区块类型",
|
"Block type": "区块类型",
|
||||||
@ -864,6 +865,7 @@
|
|||||||
"loading": "加载中",
|
"loading": "加载中",
|
||||||
"name is required": "名称不能为空",
|
"name is required": "名称不能为空",
|
||||||
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" 可能已被删除。请删除当前{{blockType}}.",
|
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" 可能已被删除。请删除当前{{blockType}}.",
|
||||||
|
"data source": "数据源",
|
||||||
"Data source": "数据源",
|
"Data source": "数据源",
|
||||||
"DataSource": "数据源",
|
"DataSource": "数据源",
|
||||||
"Data model": "数据模型",
|
"Data model": "数据模型",
|
||||||
|
@ -518,6 +518,7 @@
|
|||||||
"Save as template": "儲存為模板",
|
"Save as template": "儲存為模板",
|
||||||
"Save as block template": "儲存為區塊模板",
|
"Save as block template": "儲存為區塊模板",
|
||||||
"Block templates": "區塊模板",
|
"Block templates": "區塊模板",
|
||||||
|
"Block template": "區塊模板",
|
||||||
"Convert reference to duplicate": "模板引用轉為複製",
|
"Convert reference to duplicate": "模板引用轉為複製",
|
||||||
"Template name": "模板名稱",
|
"Template name": "模板名稱",
|
||||||
"Block type": "區塊型別",
|
"Block type": "區塊型別",
|
||||||
@ -856,6 +857,9 @@
|
|||||||
"Permission denied": "沒有權限",
|
"Permission denied": "沒有權限",
|
||||||
"Allow add new":"允許新增",
|
"Allow add new":"允許新增",
|
||||||
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" 可能已被刪除,請移除這個 {{blockType}}。",
|
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" 可能已被刪除,請移除這個 {{blockType}}。",
|
||||||
|
"data source": "數據源",
|
||||||
|
"Data source": "數據源",
|
||||||
|
"DataSource": "數據源",
|
||||||
"Allow selection of existing records":"允許選擇已有資料",
|
"Allow selection of existing records":"允許選擇已有資料",
|
||||||
"Home page": "主頁",
|
"Home page": "主頁",
|
||||||
"Handbook": "使用手冊",
|
"Handbook": "使用手冊",
|
||||||
|
@ -7,5 +7,5 @@ import { useCollectionManager } from '../../data-source/collection/CollectionMan
|
|||||||
*/
|
*/
|
||||||
export const useSourceKey = (association: string) => {
|
export const useSourceKey = (association: string) => {
|
||||||
const cm = useCollectionManager();
|
const cm = useCollectionManager();
|
||||||
return cm.getSourceKeyByAssocation(association);
|
return cm.getSourceKeyByAssociation(association);
|
||||||
};
|
};
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
// TODO: 因他们之间功能相同,所以先直接复用,后续有需要再拆分
|
// TODO: 因他们之间功能相同,所以先直接复用,后续有需要再拆分
|
||||||
export { TableBlockProvider as AssociationFilterProvider } from '../../../block-provider';
|
export { TableBlockProvider as AssociationFilterProvider } from '../../../block-provider/TableBlockProvider';
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { render, screen, userEvent } from '@nocobase/test/client';
|
import { render, renderReadPrettyApp, screen, userEvent } from '@nocobase/test/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import App1 from '../demos/checkbox';
|
import App1 from '../demos/checkbox';
|
||||||
import App2 from '../demos/checkbox.group';
|
import App2 from '../demos/checkbox.group';
|
||||||
|
import { Checkbox } from '@nocobase/client';
|
||||||
|
|
||||||
describe('Checkbox', () => {
|
describe('Checkbox', () => {
|
||||||
it('should display the title', () => {
|
it('should display the title', () => {
|
||||||
@ -37,6 +38,67 @@ describe('Checkbox', () => {
|
|||||||
await userEvent.click(input);
|
await userEvent.click(input);
|
||||||
expect(container.querySelector('svg')).toBeNull();
|
expect(container.querySelector('svg')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('read pretty', () => {
|
||||||
|
it('true', async () => {
|
||||||
|
const { container } = await renderReadPrettyApp({
|
||||||
|
Component: Checkbox,
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelector('svg')).toMatchInlineSnapshot(`
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
data-icon="check"
|
||||||
|
fill="currentColor"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false', async () => {
|
||||||
|
const { container } = await renderReadPrettyApp({
|
||||||
|
Component: Checkbox,
|
||||||
|
value: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelector('svg')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false and showUnchecked', async () => {
|
||||||
|
const { container } = await renderReadPrettyApp({
|
||||||
|
Component: Checkbox,
|
||||||
|
value: false,
|
||||||
|
props: {
|
||||||
|
showUnchecked: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelector('svg')).toMatchInlineSnapshot(`
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
data-icon="close"
|
||||||
|
fill="currentColor"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Checkbox.Group', () => {
|
describe('Checkbox.Group', () => {
|
||||||
|
@ -1,22 +1,255 @@
|
|||||||
import { render, screen, userEvent } from '@nocobase/test/client';
|
import { renderReadPrettyApp, renderApp, screen, userEvent, waitFor } from '@nocobase/test/client';
|
||||||
import React from 'react';
|
import { FormItem, CollectionSelect } from '@nocobase/client';
|
||||||
import App1 from '../demos/demo1';
|
|
||||||
|
|
||||||
describe.skip('CollectionSelect', () => {
|
describe('CollectionSelect', () => {
|
||||||
it('should works', async () => {
|
it('should works', async () => {
|
||||||
render(<App1 />);
|
const { container } = await renderApp({
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
test: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'demo title',
|
||||||
|
'x-decorator': FormItem,
|
||||||
|
'x-component': CollectionSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// 标题
|
expect(screen.getByText('demo title')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Collection')).toBeInTheDocument();
|
|
||||||
|
|
||||||
const selector = document.querySelector('.ant-select-selector');
|
await userEvent.click(document.querySelector('.ant-select-selector'));
|
||||||
expect(selector).toBeInTheDocument();
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByRole('listbox')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
await userEvent.click(selector);
|
expect(screen.queryByText('Users')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Roles')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Users'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(document.body.querySelector('.ant-select-selection-item')).toHaveTextContent('Users');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="css-dev-only-do-not-override-1mw46su ant-app"
|
||||||
|
style="height: 100%;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="block-item-demo title"
|
||||||
|
class="nb-sortable-designer nb-block-item nb-form-item css-1hvilub"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="css-1nrq807 ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-1mw46su"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-formily-item-label"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-formily-item-label-content"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<label>
|
||||||
|
demo title
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="ant-formily-item-colon"
|
||||||
|
>
|
||||||
|
:
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="ant-formily-item-control"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-formily-item-control-content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-formily-item-control-content-component"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-select css-dev-only-do-not-override-1mw46su ant-select-focused ant-select-single ant-select-show-arrow ant-select-show-search"
|
||||||
|
data-testid="select-collection"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-live="polite"
|
||||||
|
style="width: 0px; height: 0px; position: absolute; overflow: hidden; opacity: 0;"
|
||||||
|
>
|
||||||
|
Users
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="ant-select-selector"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="ant-select-selection-search"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-controls="rc_select_TEST_OR_SSR_list"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-owns="rc_select_TEST_OR_SSR_list"
|
||||||
|
autocomplete="off"
|
||||||
|
class="ant-select-selection-search-input"
|
||||||
|
id="rc_select_TEST_OR_SSR"
|
||||||
|
role="button"
|
||||||
|
type="search"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="ant-select-selection-item"
|
||||||
|
title="Users"
|
||||||
|
>
|
||||||
|
Users
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="ant-select-arrow"
|
||||||
|
style="user-select: none;"
|
||||||
|
unselectable="on"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-label="down"
|
||||||
|
class="anticon anticon-down ant-select-suffix"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
data-icon="down"
|
||||||
|
fill="currentColor"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('read pretty', async () => {
|
||||||
|
const { container } = await renderReadPrettyApp({
|
||||||
|
value: {
|
||||||
|
test: 'users',
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
test: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'demo title',
|
||||||
|
'x-decorator': FormItem,
|
||||||
|
'x-component': CollectionSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('demo title')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Users')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="css-dev-only-do-not-override-1mw46su ant-app"
|
||||||
|
style="height: 100%;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="block-item-demo title"
|
||||||
|
class="nb-sortable-designer nb-block-item nb-form-item css-1hvilub"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="css-1nrq807 ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-1mw46su"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-formily-item-label"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-formily-item-label-content"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<label>
|
||||||
|
demo title
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="ant-formily-item-colon"
|
||||||
|
>
|
||||||
|
:
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="ant-formily-item-control"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-formily-item-control-content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-formily-item-control-content-component"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="ant-tag css-dev-only-do-not-override-1mw46su"
|
||||||
|
>
|
||||||
|
Users
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('read pretty: multiple', async () => {
|
||||||
|
await renderReadPrettyApp({
|
||||||
|
value: {
|
||||||
|
test: ['users', 'roles'],
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
test: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'demo title',
|
||||||
|
'x-decorator': FormItem,
|
||||||
|
'x-component': CollectionSelect,
|
||||||
|
'x-component-props': {
|
||||||
|
mode: 'multiple',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// 下拉框内容
|
|
||||||
expect(screen.getByText('Users')).toBeInTheDocument();
|
expect(screen.getByText('Users')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Roles')).toBeInTheDocument();
|
expect(screen.getByText('Roles')).toBeInTheDocument();
|
||||||
expect(screen.getByText('测试表')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,109 @@
|
|||||||
|
import { ColorPicker } from '@nocobase/client';
|
||||||
|
import { renderApp, screen, renderReadPrettyApp, userEvent, waitFor } from '@nocobase/test/client';
|
||||||
|
|
||||||
|
describe('ColorPicker', () => {
|
||||||
|
test('basic', async () => {
|
||||||
|
const { container } = await renderApp({
|
||||||
|
Component: ColorPicker,
|
||||||
|
value: 'rgb(139, 187, 17)',
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(container.querySelector('.ant-color-picker-color-block-inner')).toHaveAttribute(
|
||||||
|
'style',
|
||||||
|
'background: rgb(139, 187, 17);',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="css-dev-only-do-not-override-1mw46su ant-app"
|
||||||
|
style="height: 100%;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="color-picker-normal"
|
||||||
|
role="button"
|
||||||
|
style="display: inline-block;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-color-picker-trigger css-dev-only-do-not-override-1mw46su"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-color-picker-color-block"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-color-picker-color-block-inner"
|
||||||
|
style="background: rgb(139, 187, 17);"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('change', async () => {
|
||||||
|
const { container } = await renderApp({
|
||||||
|
Component: ColorPicker,
|
||||||
|
value: 'rgb(139, 187, 17)',
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.hover(screen.getByRole('button').querySelector('.ant-color-picker-trigger'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(document.querySelector('.ant-color-picker-input')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = document.querySelector('.ant-color-picker-input').querySelector('input');
|
||||||
|
await userEvent.clear(input);
|
||||||
|
await userEvent.type(input, '123123'); // 对应的 rgb(18, 49, 35)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(container.querySelector('.ant-color-picker-color-block-inner')).toHaveAttribute(
|
||||||
|
'style',
|
||||||
|
'background: rgb(18, 49, 35);',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('read pretty', async () => {
|
||||||
|
const { container } = await renderReadPrettyApp({
|
||||||
|
Component: ColorPicker,
|
||||||
|
value: 'rgb(139, 187, 17)',
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(container.querySelector('.ant-color-picker-color-block-inner')).toHaveAttribute(
|
||||||
|
'style',
|
||||||
|
'background: rgb(139, 187, 17);',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="css-dev-only-do-not-override-1mw46su ant-app"
|
||||||
|
style="height: 100%;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="color-picker-read-pretty"
|
||||||
|
class="ant-description-color-picker css-gy8kge"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-color-picker-trigger ant-color-picker-sm css-dev-only-do-not-override-1mw46su ant-color-picker-trigger-disabled"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-color-picker-color-block"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-color-picker-color-block-inner"
|
||||||
|
style="background: rgb(139, 187, 17);"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
@ -960,8 +960,11 @@ function useFormItemCollectionField() {
|
|||||||
const { getCollectionJoinField } = useCollectionManager_deprecated();
|
const { getCollectionJoinField } = useCollectionManager_deprecated();
|
||||||
const { getField } = useCollection_deprecated();
|
const { getField } = useCollection_deprecated();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
|
||||||
const { collectionField: columnCollectionField } = useColumnSchema();
|
const { collectionField: columnCollectionField } = useColumnSchema();
|
||||||
|
const collectionField = fieldSchema
|
||||||
|
? getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field'])
|
||||||
|
: null;
|
||||||
|
if (!fieldSchema) return null;
|
||||||
return collectionField || columnCollectionField;
|
return collectionField || columnCollectionField;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,8 +18,8 @@ describe('FormV2', () => {
|
|||||||
|
|
||||||
await userEvent.type(input, '李四');
|
await userEvent.type(input, '李四');
|
||||||
|
|
||||||
await waitFor(async () => {
|
|
||||||
await userEvent.click(screen.getByText('Submit'));
|
await userEvent.click(screen.getByText('Submit'));
|
||||||
|
await waitFor(async () => {
|
||||||
// notification 的内容
|
// notification 的内容
|
||||||
expect(screen.getByText(/\{"nickname":"李四"\}/i)).toBeInTheDocument();
|
expect(screen.getByText(/\{"nickname":"李四"\}/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,106 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { render, sleep, userEvent, waitFor, screen, act } from '@nocobase/test/client';
|
||||||
|
import { EllipsisWithTooltip } from '@nocobase/client';
|
||||||
|
|
||||||
|
describe('EllipsisWithTooltip Component', () => {
|
||||||
|
const text = 'Text';
|
||||||
|
|
||||||
|
const Demo = (props) => {
|
||||||
|
const ref = useRef<any>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current.setPopoverVisible(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EllipsisWithTooltip ref={ref} {...props}>
|
||||||
|
{text}
|
||||||
|
</EllipsisWithTooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function setHasEllipsis() {
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { configurable: true, value: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNoEllipsis() {
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 500 });
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { configurable: true, value: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetEllipsis() {
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { configurable: true, value: 0 });
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function noPopoverCheck() {
|
||||||
|
expect(screen.queryByText(text)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.hover(screen.getByText(text));
|
||||||
|
});
|
||||||
|
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hasPopoverCheck(popoverContent?: string) {
|
||||||
|
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.hover(screen.getByText(text));
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(300);
|
||||||
|
await waitFor(async () => {
|
||||||
|
expect(screen.queryByRole('tooltip')).toHaveTextContent(popoverContent || text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalCreateRange: any = document.createRange;
|
||||||
|
beforeAll(() => {
|
||||||
|
const mockRange = {
|
||||||
|
selectNodeContents: vitest.fn(),
|
||||||
|
getBoundingClientRect: vitest.fn(() => ({
|
||||||
|
width: 100,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
document.createRange = vitest.fn(() => mockRange) as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
document.createRange = originalCreateRange;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
resetEllipsis();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders children without ellipsis by default', async () => {
|
||||||
|
render(<Demo />);
|
||||||
|
await noPopoverCheck();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows Popover when ellipsis is true and content overflows', async () => {
|
||||||
|
setHasEllipsis();
|
||||||
|
|
||||||
|
render(<Demo ellipsis />);
|
||||||
|
|
||||||
|
await hasPopoverCheck();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show Popover when content does not overflow', async () => {
|
||||||
|
setNoEllipsis();
|
||||||
|
|
||||||
|
render(<Demo ellipsis />);
|
||||||
|
await noPopoverCheck();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses popoverContent when provided', async () => {
|
||||||
|
setHasEllipsis();
|
||||||
|
|
||||||
|
const popoverContent = 'Custom Popover Content';
|
||||||
|
|
||||||
|
render(<Demo ellipsis popoverContent={popoverContent} />);
|
||||||
|
await hasPopoverCheck(popoverContent);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,83 @@
|
|||||||
|
import { Schema } from '@formily/json-schema';
|
||||||
|
|
||||||
|
import { findMenuItem } from '../util';
|
||||||
|
|
||||||
|
describe('findMenuItem', () => {
|
||||||
|
test('should return null for invalid schema', () => {
|
||||||
|
const result = findMenuItem(null);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null if no Menu.Item schema found', () => {
|
||||||
|
const schema = new Schema({
|
||||||
|
properties: {
|
||||||
|
prop1: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
prop2: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = findMenuItem(schema);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the first Menu.Item schema found', () => {
|
||||||
|
const schema = new Schema({
|
||||||
|
properties: {
|
||||||
|
prop1: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
prop2: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
menuItem: {
|
||||||
|
type: 'object',
|
||||||
|
'x-uid': 'test',
|
||||||
|
'x-component': 'Menu.Item',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = findMenuItem(schema);
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'object',
|
||||||
|
'x-uid': 'test',
|
||||||
|
'x-component': 'Menu.Item',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the first Menu.Item schema found recursively', () => {
|
||||||
|
const schema = new Schema({
|
||||||
|
properties: {
|
||||||
|
prop1: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
prop2: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
menuItem: {
|
||||||
|
type: 'object',
|
||||||
|
'x-component': 'test',
|
||||||
|
properties: {
|
||||||
|
nestedMenuItem: {
|
||||||
|
type: 'object',
|
||||||
|
'x-uid': 'test',
|
||||||
|
'x-component': 'Menu.Item',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = findMenuItem(schema);
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'object',
|
||||||
|
'x-uid': 'test',
|
||||||
|
'x-component': 'Menu.Item',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,8 @@
|
|||||||
import { render, screen, waitFor } from '@nocobase/test/client';
|
import { render, screen, waitFor, renderApp, userEvent } from '@nocobase/test/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import App1 from '../demos/demo1';
|
import App1 from '../demos/demo1';
|
||||||
|
import { Page } from '../Page';
|
||||||
|
import { DocumentTitleProvider, Form, FormItem, Grid, IconPicker, Input } from '@nocobase/client';
|
||||||
|
|
||||||
describe('Page', () => {
|
describe('Page', () => {
|
||||||
it('should render correctly', async () => {
|
it('should render correctly', async () => {
|
||||||
@ -16,4 +18,129 @@ describe('Page', () => {
|
|||||||
expect(document.title).toBe('Page Title - NocoBase');
|
expect(document.title).toBe('Page Title - NocoBase');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Page Component', () => {
|
||||||
|
const title = 'Test Title';
|
||||||
|
test('schema title', async () => {
|
||||||
|
await renderApp({
|
||||||
|
schema: {
|
||||||
|
type: 'void',
|
||||||
|
title,
|
||||||
|
'x-component': Page,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText(title)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hide title', async () => {
|
||||||
|
await renderApp({
|
||||||
|
schema: {
|
||||||
|
type: 'void',
|
||||||
|
title,
|
||||||
|
'x-component': Page,
|
||||||
|
'x-component-props': {
|
||||||
|
hidePageTitle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByText('Test Title')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should request remote schema when no title', async () => {
|
||||||
|
await renderApp({
|
||||||
|
schema: {
|
||||||
|
type: 'void',
|
||||||
|
'x-uid': 'test',
|
||||||
|
'x-component': Page,
|
||||||
|
'x-decorator': DocumentTitleProvider,
|
||||||
|
},
|
||||||
|
apis: {
|
||||||
|
'/uiSchemas:getParentJsonSchema/test': {
|
||||||
|
data: {
|
||||||
|
title: 'remote title',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('remote title')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enablePageTabs', async () => {
|
||||||
|
await renderApp({
|
||||||
|
schema: {
|
||||||
|
type: 'void',
|
||||||
|
title,
|
||||||
|
'x-decorator': DocumentTitleProvider,
|
||||||
|
'x-component': Page,
|
||||||
|
'x-component-props': {
|
||||||
|
enablePageTabs: true,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
tab1: {
|
||||||
|
type: 'void',
|
||||||
|
title: 'tab1 title',
|
||||||
|
'x-component': 'div',
|
||||||
|
'x-content': 'tab1 content',
|
||||||
|
},
|
||||||
|
tab2: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'div',
|
||||||
|
'x-content': 'tab2 content',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByRole('tablist')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText('tab1 title')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('tab1 content')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// 没有 title 的时候会使用 Unnamed
|
||||||
|
expect(screen.getByText('Unnamed')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add tab', async () => {
|
||||||
|
await renderApp({
|
||||||
|
schema: {
|
||||||
|
type: 'void',
|
||||||
|
title,
|
||||||
|
'x-decorator': DocumentTitleProvider,
|
||||||
|
'x-component': Page,
|
||||||
|
'x-component-props': {
|
||||||
|
enablePageTabs: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
appOptions: {
|
||||||
|
designable: true,
|
||||||
|
components: {
|
||||||
|
Input,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
IconPicker,
|
||||||
|
Grid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Add tab'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Tab name')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.type(screen.queryByRole('textbox'), 'tab1');
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('OK'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('tab1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,131 @@
|
|||||||
|
import { renderApp } from '@nocobase/test/client';
|
||||||
|
import { Pagination } from '@nocobase/client';
|
||||||
|
|
||||||
|
describe('Pagination', () => {
|
||||||
|
it('renders without errors', async () => {
|
||||||
|
const { container } = await renderApp({
|
||||||
|
Component: Pagination,
|
||||||
|
props: {
|
||||||
|
total: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="css-dev-only-do-not-override-1mw46su ant-app"
|
||||||
|
style="height: 100%;"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<ul
|
||||||
|
class="ant-pagination css-dev-only-do-not-override-1mw46su"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
aria-disabled="true"
|
||||||
|
class="ant-pagination-prev ant-pagination-disabled"
|
||||||
|
title="Previous Page"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="ant-pagination-item-link"
|
||||||
|
disabled=""
|
||||||
|
tabindex="-1"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-label="left"
|
||||||
|
class="anticon anticon-left"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
data-icon="left"
|
||||||
|
fill="currentColor"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="ant-pagination-item ant-pagination-item-1 ant-pagination-item-active"
|
||||||
|
tabindex="0"
|
||||||
|
title="1"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
rel="nofollow"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="ant-pagination-item ant-pagination-item-2"
|
||||||
|
tabindex="0"
|
||||||
|
title="2"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
rel="nofollow"
|
||||||
|
>
|
||||||
|
2
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
aria-disabled="false"
|
||||||
|
class="ant-pagination-next"
|
||||||
|
tabindex="0"
|
||||||
|
title="Next Page"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="ant-pagination-item-link"
|
||||||
|
tabindex="-1"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-label="right"
|
||||||
|
class="anticon anticon-right"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
data-icon="right"
|
||||||
|
fill="currentColor"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides when hidden prop is true', async () => {
|
||||||
|
const { container } = await renderApp({
|
||||||
|
Component: Pagination,
|
||||||
|
props: {
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(container).toMatchInlineSnapshot(`
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="css-dev-only-do-not-override-1mw46su ant-app"
|
||||||
|
style="height: 100%;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { PasswordStrength } from '../PasswordStrength';
|
||||||
|
|
||||||
|
describe('PasswordStrength', () => {
|
||||||
|
it('renders children with strength value', () => {
|
||||||
|
const strengthValue = 20;
|
||||||
|
const childrenMock = vitest.fn().mockReturnValue(<div>Strength: {strengthValue}</div>);
|
||||||
|
const { getByText } = render(<PasswordStrength value="password">{childrenMock}</PasswordStrength>);
|
||||||
|
expect(childrenMock).toHaveBeenCalledWith(strengthValue);
|
||||||
|
expect(getByText(`Strength: ${strengthValue}`)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders children without strength value', () => {
|
||||||
|
const childrenMock = vitest.fn().mockReturnValue(<div>No strength value</div>);
|
||||||
|
const { getByText } = render(<PasswordStrength>{childrenMock}</PasswordStrength>);
|
||||||
|
expect(childrenMock).toHaveBeenCalledWith(0);
|
||||||
|
expect(getByText('No strength value')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders children without function', () => {
|
||||||
|
const childrenMock = <div>Children without function</div>;
|
||||||
|
const { getByText } = render(<PasswordStrength>{childrenMock}</PasswordStrength>);
|
||||||
|
expect(getByText('Children without function')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -24,4 +24,60 @@ describe('getStrength', () => {
|
|||||||
it('should return 100', () => {
|
it('should return 100', () => {
|
||||||
expect(getStrength('z1234567890')).toBe(100);
|
expect(getStrength('z1234567890')).toBe(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return 0 for empty value', () => {
|
||||||
|
expect(getStrength('')).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 20 for a single digit', () => {
|
||||||
|
expect(getStrength('1')).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 20 for a single lowercase letter', () => {
|
||||||
|
expect(getStrength('a')).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 20 for a single uppercase letter', () => {
|
||||||
|
expect(getStrength('A')).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 20 for a single symbol', () => {
|
||||||
|
expect(getStrength('@')).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 60 for a combination of lowercase letters and digits', () => {
|
||||||
|
expect(getStrength('a1b2c3')).toBe(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 60 for a combination of uppercase letters and digits', () => {
|
||||||
|
expect(getStrength('A1B2C3')).toBe(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 80 for a combination of lowercase letters and symbols', () => {
|
||||||
|
expect(getStrength('a@b#c$d%')).toBe(80);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 80 for a combination of uppercase letters and symbols', () => {
|
||||||
|
expect(getStrength('A@B#C$D%')).toBe(80);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 100 for a combination of lowercase letters, uppercase letters, and digits', () => {
|
||||||
|
expect(getStrength('aA1bB2cC3')).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 100 for a combination of lowercase letters, uppercase letters, and symbols', () => {
|
||||||
|
expect(getStrength('aA@bB#cC$dD%')).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 100 for a combination of lowercase letters, digits, and symbols', () => {
|
||||||
|
expect(getStrength('a1@b2#c3$d4%')).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 100 for a combination of uppercase letters, digits, and symbols', () => {
|
||||||
|
expect(getStrength('A1@B2#C3$D4%')).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 100 for a strong password with a combination of lowercase letters, uppercase letters, digits, and symbols', () => {
|
||||||
|
expect(getStrength('aA1@bB2#cC3$dD4%')).toBe(100);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user