diff --git a/packages/core/build/src/buildEsm.ts b/packages/core/build/src/buildEsm.ts
index 8515555498..9daaa83f7c 100644
--- a/packages/core/build/src/buildEsm.ts
+++ b/packages/core/build/src/buildEsm.ts
@@ -4,15 +4,21 @@ import { PkgLog, UserConfig } from './utils';
import { build as viteBuild } from 'vite';
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) {
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');
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');
if (clientEntry) {
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'));
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');
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);
}
}
diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-action-modal-1.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-action-modal-1.tsx
new file mode 100644
index 0000000000..93159ed197
--- /dev/null
+++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-action-modal-1.tsx
@@ -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 (
+ {
+ insert({
+ type: 'void',
+ title,
+ 'x-decorator': 'CardItem',
+ 'x-component': 'Hello',
+ });
+ }}
+ schema={{
+ title: {
+ type: 'string',
+ title: 'Title',
+ required: true,
+ 'x-component': 'Input',
+ 'x-decorator': 'FormItem',
+ },
+ }}
+ >
+ );
+ },
+});
+
+const Hello = () => {
+ const schema = useFieldSchema();
+ return
Hello, world! {schema.title}
;
+};
+
+const app = new Application({
+ ...appOptions,
+ components: {
+ FormItem,
+ Action,
+ Input,
+ Form,
+ Hello,
+ CardItem,
+ },
+ schemaInitializers: [myInitializer],
+});
+
+export default app.getRootComponent();
diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-action-modal-2.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-action-modal-2.tsx
new file mode 100644
index 0000000000..1727d805e6
--- /dev/null
+++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-action-modal-2.tsx
@@ -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 (
+ {
+ insert({
+ type: 'void',
+ title,
+ 'x-decorator': 'CardItem',
+ 'x-component': 'Hello',
+ });
+ }}
+ schema={{
+ title: {
+ type: 'string',
+ title: 'Title',
+ required: true,
+ 'x-component': 'Input',
+ 'x-decorator': 'FormItem',
+ },
+ }}
+ >
+ );
+ },
+ },
+ ],
+});
+
+const Hello = () => {
+ const schema = useFieldSchema();
+ return Hello, world! {schema.title}
;
+};
+
+const app = new Application({
+ ...appOptions,
+ components: {
+ FormItem,
+ Action,
+ Input,
+ Form,
+ Hello,
+ CardItem,
+ },
+ schemaInitializers: [myInitializer],
+});
+
+export default app.getRootComponent();
diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-group.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-group.tsx
index d691ff9259..63188a7d98 100644
--- a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-group.tsx
+++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-group.tsx
@@ -48,15 +48,16 @@ const myInitializer = new SchemaInitializer({
name: 'c',
Component: () => {
return (
-
- {[
+
+ >
);
},
},
diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-menu.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-menu.tsx
index f795b02715..7ae2f71176 100644
--- a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-menu.tsx
+++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-menu.tsx
@@ -1,8 +1,9 @@
/**
* defaultShowCode: true
*/
-import { Application, SchemaInitializer } from '@nocobase/client';
+import { Application, SchemaInitializer, SchemaInitializerSubMenu } from '@nocobase/client';
import { appOptions } from './schema-initializer-common';
+import React from 'react';
const myInitializer = new SchemaInitializer({
name: 'MyInitializer',
@@ -42,6 +43,24 @@ const myInitializer = new SchemaInitializer({
},
],
},
+ {
+ name: 'c',
+ Component: () => {
+ return (
+
+ );
+ },
+ },
],
});
diff --git a/packages/core/client/docs/en-US/core/ui-schema/schema-initializer.md b/packages/core/client/docs/en-US/core/ui-schema/schema-initializer.md
index 2637b0a80b..bebd3cbcef 100644
--- a/packages/core/client/docs/en-US/core/ui-schema/schema-initializer.md
+++ b/packages/core/client/docs/en-US/core/ui-schema/schema-initializer.md
@@ -657,6 +657,18 @@ interface SchemaInitializerSwitchItemProps extends SchemaInitializerItemProps {
+### `type: 'actionModal'` & SchemaInitializerActionModal
+
+#### Component Mode
+
+
+
+#### Item Mode
+
+`SchemaInitializerActionModal` 需要加上 `isItem` 属性
+
+
+
## 渲染组件
### SchemaInitializerChildren
diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-action-modal-1.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-action-modal-1.tsx
new file mode 100644
index 0000000000..93159ed197
--- /dev/null
+++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-action-modal-1.tsx
@@ -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 (
+ {
+ insert({
+ type: 'void',
+ title,
+ 'x-decorator': 'CardItem',
+ 'x-component': 'Hello',
+ });
+ }}
+ schema={{
+ title: {
+ type: 'string',
+ title: 'Title',
+ required: true,
+ 'x-component': 'Input',
+ 'x-decorator': 'FormItem',
+ },
+ }}
+ >
+ );
+ },
+});
+
+const Hello = () => {
+ const schema = useFieldSchema();
+ return Hello, world! {schema.title}
;
+};
+
+const app = new Application({
+ ...appOptions,
+ components: {
+ FormItem,
+ Action,
+ Input,
+ Form,
+ Hello,
+ CardItem,
+ },
+ schemaInitializers: [myInitializer],
+});
+
+export default app.getRootComponent();
diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-action-modal-2.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-action-modal-2.tsx
new file mode 100644
index 0000000000..1727d805e6
--- /dev/null
+++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-action-modal-2.tsx
@@ -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 (
+ {
+ insert({
+ type: 'void',
+ title,
+ 'x-decorator': 'CardItem',
+ 'x-component': 'Hello',
+ });
+ }}
+ schema={{
+ title: {
+ type: 'string',
+ title: 'Title',
+ required: true,
+ 'x-component': 'Input',
+ 'x-decorator': 'FormItem',
+ },
+ }}
+ >
+ );
+ },
+ },
+ ],
+});
+
+const Hello = () => {
+ const schema = useFieldSchema();
+ return Hello, world! {schema.title}
;
+};
+
+const app = new Application({
+ ...appOptions,
+ components: {
+ FormItem,
+ Action,
+ Input,
+ Form,
+ Hello,
+ CardItem,
+ },
+ schemaInitializers: [myInitializer],
+});
+
+export default app.getRootComponent();
diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-group.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-group.tsx
index d691ff9259..63188a7d98 100644
--- a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-group.tsx
+++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-group.tsx
@@ -48,15 +48,16 @@ const myInitializer = new SchemaInitializer({
name: 'c',
Component: () => {
return (
-
- {[
+
+ >
);
},
},
diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-menu.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-menu.tsx
index f795b02715..7ae2f71176 100644
--- a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-menu.tsx
+++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-menu.tsx
@@ -1,8 +1,9 @@
/**
* defaultShowCode: true
*/
-import { Application, SchemaInitializer } from '@nocobase/client';
+import { Application, SchemaInitializer, SchemaInitializerSubMenu } from '@nocobase/client';
import { appOptions } from './schema-initializer-common';
+import React from 'react';
const myInitializer = new SchemaInitializer({
name: 'MyInitializer',
@@ -42,6 +43,24 @@ const myInitializer = new SchemaInitializer({
},
],
},
+ {
+ name: 'c',
+ Component: () => {
+ return (
+
+ );
+ },
+ },
],
});
diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/schema-initializer.md b/packages/core/client/docs/zh-CN/core/ui-schema/schema-initializer.md
index 2637b0a80b..bebd3cbcef 100644
--- a/packages/core/client/docs/zh-CN/core/ui-schema/schema-initializer.md
+++ b/packages/core/client/docs/zh-CN/core/ui-schema/schema-initializer.md
@@ -657,6 +657,18 @@ interface SchemaInitializerSwitchItemProps extends SchemaInitializerItemProps {
+### `type: 'actionModal'` & SchemaInitializerActionModal
+
+#### Component Mode
+
+
+
+#### Item Mode
+
+`SchemaInitializerActionModal` 需要加上 `isItem` 属性
+
+
+
## 渲染组件
### SchemaInitializerChildren
diff --git a/packages/core/client/src/appInfo/__tests__/CurrentAppInfoProvider.test.tsx b/packages/core/client/src/appInfo/__tests__/CurrentAppInfoProvider.test.tsx
new file mode 100644
index 0000000000..da37d9cfa9
--- /dev/null
+++ b/packages/core/client/src/appInfo/__tests__/CurrentAppInfoProvider.test.tsx
@@ -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',
+ });
+ });
+ });
+});
diff --git a/packages/core/client/src/application/Application.tsx b/packages/core/client/src/application/Application.tsx
index 27cf7d959e..351ef737ac 100644
--- a/packages/core/client/src/application/Application.tsx
+++ b/packages/core/client/src/application/Application.tsx
@@ -4,7 +4,7 @@ import { i18n as i18next } from 'i18next';
import get from 'lodash/get';
import merge from 'lodash/merge';
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 { I18nextProvider } from 'react-i18next';
import { Link, NavLink, Navigate } from 'react-router-dom';
@@ -177,7 +177,7 @@ export class Application {
}
getRouteUrl(pathname: string) {
- return this.options.publicPath.replace(/\/$/g, '') + pathname;
+ return this.getPublicPath().replace(/\/$/g, '') + pathname;
}
getCollectionManager(dataSource?: string) {
@@ -288,8 +288,8 @@ export class Application {
return;
}
- renderComponent(Component: ComponentTypeAndString, props?: T): ReactElement {
- return React.createElement(this.getComponent(Component), props);
+ renderComponent(Component: ComponentTypeAndString, props?: T, children?: ReactNode): ReactElement {
+ return React.createElement(this.getComponent(Component), props, children);
}
/**
@@ -315,7 +315,9 @@ export class Application {
}
getRootComponent() {
- const Root: FC = () => ;
+ const Root: FC<{ children?: React.ReactNode }> = ({ children }) => (
+ {children}
+ );
return Root;
}
diff --git a/packages/core/client/src/application/RouterManager.tsx b/packages/core/client/src/application/RouterManager.tsx
index 09d90a2fa5..cf16954dde 100644
--- a/packages/core/client/src/application/RouterManager.tsx
+++ b/packages/core/client/src/application/RouterManager.tsx
@@ -112,7 +112,7 @@ export class RouterManager {
/**
* @internal
*/
- getRouterComponent() {
+ getRouterComponent(children?: React.ReactNode) {
const { type = 'browser', ...opts } = this.options;
const Routers = {
hash: HashRouter,
@@ -134,6 +134,7 @@ export class RouterManager {
+ {children}
diff --git a/packages/core/client/src/application/__tests__/Application.test.tsx b/packages/core/client/src/application/__tests__/Application.test.tsx
index 2d39cb0047..c087236692 100644
--- a/packages/core/client/src/application/__tests__/Application.test.tsx
+++ b/packages/core/client/src/application/__tests__/Application.test.tsx
@@ -6,6 +6,7 @@ import { Link, Outlet } from 'react-router-dom';
import { describe } from 'vitest';
import { Application } from '../Application';
import { Plugin } from '../Plugin';
+import { useApp } from '../hooks';
describe('Application', () => {
beforeAll(() => {
@@ -18,7 +19,9 @@ describe('Application', () => {
const router: any = { type: 'memory', initialEntries: ['/'] };
const initialProvidersLength = 7;
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.apiClient).toBeDefined();
expect(app.components).toBeDefined();
@@ -30,6 +33,26 @@ describe('Application', () => {
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', () => {
const Hello = () => Hello
;
Hello.displayName = 'Hello';
@@ -242,6 +265,30 @@ describe('Application', () => {
expect(screen.getByText('AboutComponent')).toBeInTheDocument();
});
+ it('Root with children', async () => {
+ const app = new Application({ name: 'test' });
+
+ const Demo = () => {
+ const app = useApp();
+ return {app.name}
;
+ };
+
+ const Root = app.getRootComponent();
+ render(
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText('test')).toBeInTheDocument();
+ });
+ });
+
it('mount', async () => {
const Hello = () => Hello
;
const app = new Application({
diff --git a/packages/core/client/src/application/__tests__/Plugin.test.ts b/packages/core/client/src/application/__tests__/Plugin.test.ts
index 836a4ff78c..e870a323cc 100644
--- a/packages/core/client/src/application/__tests__/Plugin.test.ts
+++ b/packages/core/client/src/application/__tests__/Plugin.test.ts
@@ -159,4 +159,21 @@ describe('PluginManager', () => {
await app.load();
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();
+ });
});
diff --git a/packages/core/client/src/application/__tests__/PluginSettingsManager.test.ts b/packages/core/client/src/application/__tests__/PluginSettingsManager.test.ts
index f001237d80..3c846e07d3 100644
--- a/packages/core/client/src/application/__tests__/PluginSettingsManager.test.ts
+++ b/packages/core/client/src/application/__tests__/PluginSettingsManager.test.ts
@@ -86,6 +86,32 @@ describe('PluginSettingsManager', () => {
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', () => {
app.pluginSettingsManager.add('test1', test1);
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()', () => {
app.pluginSettingsManager.add('test1', test1);
app.pluginSettingsManager.add('test2', {
diff --git a/packages/core/client/src/application/__tests__/RouterManager.test.tsx b/packages/core/client/src/application/__tests__/RouterManager.test.tsx
index b711fa4b23..542dd40ca5 100644
--- a/packages/core/client/src/application/__tests__/RouterManager.test.tsx
+++ b/packages/core/client/src/application/__tests__/RouterManager.test.tsx
@@ -188,6 +188,7 @@ describe('Router', () => {
const RouterComponent = router.getRouterComponent();
render();
expect(screen.queryByTestId('content')).not.toBeInTheDocument();
+ expect(router.getBasename()).toBe('/admin');
window.location.hash = '#/admin';
diff --git a/packages/core/client/src/application/__tests__/hoc/withDynamicSchemaProps.test.tsx b/packages/core/client/src/application/__tests__/hoc/withDynamicSchemaProps.test.tsx
index 1fa07abdfb..2503da7e94 100644
--- a/packages/core/client/src/application/__tests__/hoc/withDynamicSchemaProps.test.tsx
+++ b/packages/core/client/src/application/__tests__/hoc/withDynamicSchemaProps.test.tsx
@@ -1,6 +1,6 @@
+import { render } from '@nocobase/test/client';
import React from 'react';
import { SchemaComponent, SchemaComponentProvider } from '../../../schema-component';
-import { render } from '@nocobase/test/client';
import { withDynamicSchemaProps } from '../../hoc';
const HelloComponent = withDynamicSchemaProps((props: any) => (
@@ -213,4 +213,41 @@ describe('withDynamicSchemaProps', () => {
const { getByTestId } = render();
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 (
+
+
+
+ );
+ };
+ const { getByTestId } = render();
+ expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ c: 'c' }));
+ });
});
diff --git a/packages/core/client/src/application/__tests__/schema-initializer/SchemaInitializer.test.tsx b/packages/core/client/src/application/__tests__/schema-initializer/SchemaInitializer.test.tsx
new file mode 100644
index 0000000000..751e4c3e94
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-initializer/SchemaInitializer.test.tsx
@@ -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);
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-initializer/SchemaInitializerManager.test.ts b/packages/core/client/src/application/__tests__/schema-initializer/SchemaInitializerManager.test.ts
new file mode 100644
index 0000000000..af464a2c1b
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-initializer/SchemaInitializerManager.test.ts
@@ -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');
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerActionModal.test.tsx b/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerActionModal.test.tsx
new file mode 100644
index 0000000000..60154ca194
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerActionModal.test.tsx
@@ -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 (
+
+ );
+ };
+ 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 (
+
+ );
+ };
+ 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();
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerChildren.test.tsx b/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerChildren.test.tsx
new file mode 100644
index 0000000000..d3dc2288b6
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerChildren.test.tsx
@@ -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 Item1
;
+ },
+ },
+ {
+ name: 'item2',
+ type: 'item',
+ title: 'Item2',
+ // 大写
+ Component: () => {
+ return Item2
;
+ },
+ },
+ {
+ name: 'item3',
+ Component: 'Item3',
+ },
+ {
+ name: 'item4',
+ Component: 'not-exists',
+ },
+ ],
+ {
+ components: {
+ Item3: () => {
+ return Item3
;
+ },
+ },
+ },
+ );
+ 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 ;
+ },
+ },
+ },
+ );
+
+ expect(screen.queryByText('Item1')).toBeInTheDocument();
+ });
+
+ it('useComponentProps', async () => {
+ await createAndHover(
+ [
+ {
+ name: 'item1',
+ Component: 'CommonDemo',
+ useComponentProps() {
+ return {
+ title: 'Item1',
+ };
+ },
+ },
+ ],
+ {
+ components: {
+ CommonDemo: (props) => {
+ return ;
+ },
+ },
+ },
+ );
+
+ 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 ;
+ },
+ },
+ },
+ );
+
+ 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 ;
+ },
+ },
+ },
+ );
+
+ expect(screen.queryByText('Item1')).toBeInTheDocument();
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerDivider.test.tsx b/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerDivider.test.tsx
new file mode 100644
index 0000000000..6d57fd724c
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerDivider.test.tsx
@@ -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();
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerItem.test.tsx b/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerItem.test.tsx
new file mode 100644
index 0000000000..6d8ec926a7
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerItem.test.tsx
@@ -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 ;
+ },
+ },
+ {
+ 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',
+ },
+ }),
+ );
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerItemGroup.test.tsx b/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerItemGroup.test.tsx
new file mode 100644
index 0000000000..dcb1390ab5
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerItemGroup.test.tsx
@@ -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 (
+
+ {[
+ {
+ name: 'c1',
+ type: 'item',
+ title: 'C1',
+ },
+ ]}
+
+ );
+ },
+ },
+ {
+ name: 'd',
+ Component: () => {
+ return (
+
+ );
+ },
+ },
+ ]);
+ 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);
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerSubMenu.test.tsx b/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerSubMenu.test.tsx
new file mode 100644
index 0000000000..f22e1de00e
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerSubMenu.test.tsx
@@ -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 (
+
+ );
+ };
+ await createAndHover(
+ [
+ {
+ name: 'a',
+ Component: 'TestDemo',
+ },
+ ],
+ {
+ components: {
+ TestDemo,
+ },
+ },
+ );
+
+ await valid(onClick);
+ });
+ test('component children mode', async () => {
+ const onClick = vitest.fn();
+ const TestDemo = () => {
+ return (
+
+ {[
+ {
+ name: 'a1',
+ type: 'item',
+ title: 'A1',
+ onClick: onClick,
+ useVisible() {
+ return true;
+ },
+ },
+ {
+ name: 'a2',
+ type: 'item',
+ title: 'A2',
+ useVisible() {
+ return false;
+ },
+ },
+ ]}
+
+ );
+ };
+ await createAndHover(
+ [
+ {
+ name: 'a',
+ Component: 'TestDemo',
+ },
+ ],
+ {
+ components: {
+ TestDemo,
+ },
+ },
+ );
+ await valid(onClick);
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerSwitch.test.tsx b/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerSwitch.test.tsx
new file mode 100644
index 0000000000..d6c93b329f
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-initializer/components/SchemaInitializerSwitch.test.tsx
@@ -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 (
+ {
+ // 如果已插入,则移除
+ 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();
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-initializer/components/fixtures/createAppAndHover.tsx b/packages/core/client/src/application/__tests__/schema-initializer/components/fixtures/createAppAndHover.tsx
new file mode 100644
index 0000000000..0a8095fb94
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-initializer/components/fixtures/createAppAndHover.tsx
@@ -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();
+ });
+}
diff --git a/packages/core/client/src/application/__tests__/schema-initializer/fixures/createApp.tsx b/packages/core/client/src/application/__tests__/schema-initializer/fixures/createApp.tsx
new file mode 100644
index 0000000000..feae708353
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-initializer/fixures/createApp.tsx
@@ -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 ;
+ },
+ },
+ ],
+ ...options,
+ });
+
+ const AddBlockButton = observer(() => {
+ const { render } = useSchemaInitializerRender('test');
+
+ return {render()}
;
+ });
+
+ const WrapDemo = ({ children }) => {
+ return (
+
+
WrapDemo
+ {children}
+
+ );
+ };
+
+ const Page = observer(
+ (props) => {
+ return (
+
+ );
+ },
+ { displayName: 'Page' },
+ );
+
+ const Root = () => {
+ return (
+
+ );
+ };
+ await renderApp({
+ appOptions: {
+ providers: [Root],
+ schemaInitializers: [testInitializers],
+ designable: true,
+ ...appOptions,
+ },
+ });
+}
diff --git a/packages/core/client/src/application/__tests__/schema-initializer/hooks/useAriaAttributeOfMenuItem.test.tsx b/packages/core/client/src/application/__tests__/schema-initializer/hooks/useAriaAttributeOfMenuItem.test.tsx
new file mode 100644
index 0000000000..dbc5712ad7
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-initializer/hooks/useAriaAttributeOfMenuItem.test.tsx
@@ -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({});
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-initializer/hooks/useGetSchemaInitializerMenuItems.test.tsx b/packages/core/client/src/application/__tests__/schema-initializer/hooks/useGetSchemaInitializerMenuItems.test.tsx
new file mode 100644
index 0000000000..bc7ed48a44
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-initializer/hooks/useGetSchemaInitializerMenuItems.test.tsx
@@ -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: () => item4
,
+ },
+ {
+ type: 'item',
+ label: 'item5',
+ component: () => item5
,
+ },
+ ];
+
+ 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": ,
+ },
+ {
+ "key": "item5-4",
+ "label": ,
+ },
+ ]
+ `);
+ 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: () => 123
,
+ 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();
+ });
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-initializer/hooks/useSchemaInitializerRender.test.tsx b/packages/core/client/src/application/__tests__/schema-initializer/hooks/useSchemaInitializerRender.test.tsx
new file mode 100644
index 0000000000..e9aa449b6d
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-initializer/hooks/useSchemaInitializerRender.test.tsx
@@ -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();
+ 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 (
+
+
{JSON.stringify(exists)}
+
{render()}
+
+ );
+ };
+ 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 (
+
+
{JSON.stringify(exists)}
+
{render()}
+
+ );
+ };
+ 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 (
+
+
{JSON.stringify(exists)}
+
{render()}
+
+ );
+ };
+ 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 {render()}
;
+ };
+ await createApp(Demo);
+
+ expect(document.querySelector('.test')).toBeInTheDocument();
+ });
+
+ it('should override custom props', async () => {
+ const Demo = () => {
+ const { render } = useSchemaInitializerRender('test', { componentProps: { className: 'test' } });
+
+ return {render({ componentProps: { className: 'test2' } })}
;
+ };
+ await createApp(Demo);
+
+ expect(document.querySelector('.test2')).toBeInTheDocument();
+ expect(document.querySelector('.test')).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-initializer/withInitializer.test.tsx b/packages/core/client/src/application/__tests__/schema-initializer/withInitializer.test.tsx
new file mode 100644
index 0000000000..6716fc0716
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-initializer/withInitializer.test.tsx
@@ -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!',
+ });
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-settings/SchemaSettings.test.tsx b/packages/core/client/src/application/__tests__/schema-settings/SchemaSettings.test.tsx
new file mode 100644
index 0000000000..af1c9fdaa6
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-settings/SchemaSettings.test.tsx
@@ -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);
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-settings/SchemaSettingsManager.test.ts b/packages/core/client/src/application/__tests__/schema-settings/SchemaSettingsManager.test.ts
new file mode 100644
index 0000000000..f94efd8345
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-settings/SchemaSettingsManager.test.ts
@@ -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');
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-settings/components/SchemaSettingsChildren.test.tsx b/packages/core/client/src/application/__tests__/schema-settings/components/SchemaSettingsChildren.test.tsx
new file mode 100644
index 0000000000..5f5735bdb9
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-settings/components/SchemaSettingsChildren.test.tsx
@@ -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 Item1
;
+ },
+ },
+ {
+ name: 'item2',
+ Component: 'Item2',
+ },
+ {
+ name: 'item3',
+ Component: 'not-exists',
+ },
+ {
+ name: 'item4',
+ } as any,
+ ],
+ {
+ components: {
+ Item2: () => {
+ return Item2
;
+ },
+ },
+ },
+ );
+ 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 ;
+ },
+ },
+ },
+ );
+
+ expect(screen.queryByText('Item1')).toBeInTheDocument();
+ });
+
+ it('useComponentProps', async () => {
+ await createAndHover(
+ [
+ {
+ name: 'item1',
+ Component: 'CommonDemo',
+ useComponentProps() {
+ return {
+ title: 'Item1',
+ };
+ },
+ },
+ ],
+ {
+ components: {
+ CommonDemo: (props) => {
+ return ;
+ },
+ },
+ },
+ );
+
+ 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 ;
+ },
+ },
+ },
+ );
+
+ 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();
+ return ;
+ },
+ },
+ },
+ );
+
+ expect(screen.queryByText('Item1')).toBeInTheDocument();
+ });
+});
diff --git a/packages/core/client/src/application/__tests__/schema-settings/components/fixtures/createAppAndHover.tsx b/packages/core/client/src/application/__tests__/schema-settings/components/fixtures/createAppAndHover.tsx
new file mode 100644
index 0000000000..c44cc4e2a9
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-settings/components/fixtures/createAppAndHover.tsx
@@ -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();
+ 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();
+ });
+}
diff --git a/packages/core/client/src/application/__tests__/schema-settings/hooks/useSchemaSettingsRender.test.tsx b/packages/core/client/src/application/__tests__/schema-settings/hooks/useSchemaSettingsRender.test.tsx
new file mode 100644
index 0000000000..89e00fccf6
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/schema-settings/hooks/useSchemaSettingsRender.test.tsx
@@ -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) => Test
,
+ items: [],
+ });
+
+ const app = new Application({
+ providers: [DemoComponent],
+ schemaSettings: [testSettings],
+ designable: true,
+ });
+ const Root = app.getRootComponent();
+ render();
+ 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 (
+
+
{JSON.stringify(exists)}
+
{render()}
+
+ );
+ };
+ 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 (
+
+
{JSON.stringify(exists)}
+
{render()}
+
+ );
+ };
+ 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 (
+
+
{JSON.stringify(exists)}
+
{render()}
+
+ );
+ };
+ 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 {render()}
;
+ };
+ await createApp(Demo);
+
+ expect(document.querySelector('.test')).toBeInTheDocument();
+ });
+
+ it('should override custom props', async () => {
+ const Demo = () => {
+ const { render } = useSchemaSettingsRender('test', { componentProps: { className: 'test' } });
+
+ return {render({ componentProps: { className: 'test2' } })}
;
+ };
+ await createApp(Demo);
+
+ expect(document.querySelector('.test2')).toBeInTheDocument();
+ expect(document.querySelector('.test')).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/core/client/src/application/components/AppComponent.tsx b/packages/core/client/src/application/components/AppComponent.tsx
index 686c0213a6..09c21123b5 100644
--- a/packages/core/client/src/application/components/AppComponent.tsx
+++ b/packages/core/client/src/application/components/AppComponent.tsx
@@ -9,7 +9,7 @@ export interface AppComponentProps {
}
export const AppComponent: FC = observer(
- (props) => {
+ ({ children, ...props }) => {
const { app } = props;
const handleErrors = useCallback((error: Error, info: { componentStack: string }) => {
console.error(error);
@@ -33,7 +33,7 @@ export const AppComponent: FC = observer(
>
{app.maintained && app.maintaining && app.renderComponent('AppMaintainingDialog', { app })}
- {app.renderComponent('AppMain')}
+ {app.renderComponent('AppMain', undefined, children)}
);
diff --git a/packages/core/client/src/application/components/MainComponent.tsx b/packages/core/client/src/application/components/MainComponent.tsx
index e64ab0b114..0749c31fdd 100644
--- a/packages/core/client/src/application/components/MainComponent.tsx
+++ b/packages/core/client/src/application/components/MainComponent.tsx
@@ -1,9 +1,9 @@
import React, { useMemo } from 'react';
import { useApp } from '../hooks';
-export const MainComponent = React.memo(() => {
+export const MainComponent = React.memo(({ children }) => {
const app = useApp();
- const Router = useMemo(() => app.router.getRouterComponent(), [app]);
+ const Router = useMemo(() => app.router.getRouterComponent(children), [app]);
const Providers = useMemo(() => app.getComposeProviders(), [app]);
return ;
});
diff --git a/packages/core/client/src/application/components/defaultComponents.tsx b/packages/core/client/src/application/components/defaultComponents.tsx
index 7ddcb8d98b..9be8c2a488 100644
--- a/packages/core/client/src/application/components/defaultComponents.tsx
+++ b/packages/core/client/src/application/components/defaultComponents.tsx
@@ -2,12 +2,15 @@ import React, { FC } from 'react';
import { MainComponent } from './MainComponent';
const Loading: FC = () => Loading...
;
-const AppError: FC<{ error: Error }> = ({ error }) => (
-
-
Load Plugin Error
- {error?.message}
-
-);
+const AppError: FC<{ error: Error }> = ({ error }) => {
+ return (
+
+
Load Plugin Error
+ {error?.message}
+ {process.env.__TEST__ && error?.stack}
+
+ );
+};
const AppNotFound: FC = () => ;
diff --git a/packages/core/client/src/application/schema-initializer/SchemaInitializer.tsx b/packages/core/client/src/application/schema-initializer/SchemaInitializer.tsx
index 5850aa48a4..87ea0b72bc 100644
--- a/packages/core/client/src/application/schema-initializer/SchemaInitializer.tsx
+++ b/packages/core/client/src/application/schema-initializer/SchemaInitializer.tsx
@@ -32,7 +32,8 @@ export class SchemaInitializer {
if (!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) {
parentItem.children.push(data);
} else {
@@ -42,6 +43,7 @@ export class SchemaInitializer {
}
get(nestedName: string): SchemaInitializerItemType | undefined {
+ if (!nestedName) return undefined;
const arr = nestedName.split('.');
let current: any = this.items;
@@ -58,8 +60,6 @@ export class SchemaInitializer {
return undefined;
}
}
-
- return current;
}
remove(nestedName: string) {
diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerActionModal.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerActionModal.tsx
index 1b2f0210bf..7a483f739c 100644
--- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerActionModal.tsx
+++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerActionModal.tsx
@@ -2,6 +2,7 @@ import { useForm } from '@formily/react';
import React, { FC, useCallback, useMemo } from 'react';
import { useActionContext, SchemaComponent } from '../../../schema-component';
import { useSchemaInitializerItem } from '../context';
+import { SchemaInitializerItem } from './SchemaInitializerItem';
export interface SchemaInitializerActionModalProps {
title: string;
@@ -10,10 +11,24 @@ export interface SchemaInitializerActionModalProps {
onSubmit?: (values: any) => void;
buttonText?: any;
component?: any;
+ isItem?: boolean;
}
-export const SchemaInitializerActionModal: FC = (props) => {
- const { title, schema, buttonText, component, onCancel, onSubmit } = props;
+const SchemaInitializerActionModalItemComponent = React.forwardRef((props: any, ref) => {
+ const { onClick, title, ...others } = props;
+ return (
+ {
+ onClick?.(e.event);
+ }}
+ >
+ );
+});
+
+export const SchemaInitializerActionModal: FC = (props) => {
+ const { title, schema, buttonText, isItem, component, onCancel, onSubmit } = props;
const useCancelAction = useCallback(() => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const form = useForm();
@@ -53,15 +68,20 @@ export const SchemaInitializerActionModal: FC
? {
component,
}
- : {
- icon: 'PlusOutlined',
- style: {
- borderColor: 'var(--colorSettings)',
- color: 'var(--colorSettings)',
+ : isItem
+ ? {
+ title: buttonText,
+ component: SchemaInitializerActionModalItemComponent,
+ }
+ : {
+ icon: 'PlusOutlined',
+ style: {
+ borderColor: 'var(--colorSettings)',
+ color: 'var(--colorSettings)',
+ },
+ title: buttonText,
+ type: 'dashed',
},
- title: buttonText,
- type: 'dashed',
- },
properties: {
drawer1: {
'x-decorator': 'Form',
diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerChildren.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerChildren.tsx
index 8a0642af48..70698787fb 100644
--- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerChildren.tsx
+++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerChildren.tsx
@@ -21,6 +21,7 @@ const typeComponentMap: Record = {
item: 'SchemaInitializerItemInternal',
itemGroup: 'SchemaInitializerItemGroupInternal',
divider: 'SchemaInitializerDivider',
+ switch: 'SchemaInitializerSwitchInternal',
subMenu: 'SchemaInitializerSubMenuInternal',
actionModal: 'SchemaInitializerActionModalInternal',
};
@@ -70,7 +71,7 @@ export const SchemaInitializerChild: FC = memo((props
if (!C) {
return null;
}
- if (hideIfNoChildren && Array.isArray(componentChildren) && componentChildren.length === 0) {
+ if (hideIfNoChildren && !componentChildren) {
return null;
}
diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx
index 51aec36412..3624e5de27 100644
--- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx
+++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx
@@ -76,6 +76,9 @@ export const SchemaInitializerItem = memo(
);
SchemaInitializerItem.displayName = 'SchemaInitializerItem';
+/**
+ * @internal
+ */
export const SchemaInitializerItemInternal = () => {
const itemConfig = useSchemaInitializerItem();
return ;
diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItemGroup.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItemGroup.tsx
index 728f3501eb..ce032e826e 100644
--- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItemGroup.tsx
+++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItemGroup.tsx
@@ -10,10 +10,16 @@ import { useSchemaInitializerStyles } from './style';
export interface SchemaInitializerItemGroupProps {
title: string;
children?: SchemaInitializerOptions['items'];
+ items?: SchemaInitializerOptions['items'];
divider?: boolean;
}
-export const SchemaInitializerItemGroup: FC = ({ children, title, divider }) => {
+export const SchemaInitializerItemGroup: FC = ({
+ children,
+ items,
+ title,
+ divider,
+}) => {
const compile = useCompile();
const { componentCls } = useSchemaInitializerStyles();
const { token } = theme.useToken();
@@ -21,11 +27,14 @@ export const SchemaInitializerItemGroup: FC = (
{divider &&
}
{compile(title)}
-
{children}
+
{items || children}
);
};
+/**
+ * @internal
+ */
export const SchemaInitializerItemGroupInternal = () => {
const itemConfig = useSchemaInitializerItem();
return ;
diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSelect.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSelect.tsx
index f220bcdd16..4d57c30213 100644
--- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSelect.tsx
+++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSelect.tsx
@@ -55,6 +55,9 @@ export const SchemaInitializerSelect: FC = (pr
);
};
+/**
+ * @internal
+ */
export const SchemaInitializerSelectInternal = () => {
const itemConfig = useSchemaInitializerItem();
return ;
diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx
index 606f0169b2..5954dba9b4 100644
--- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx
+++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx
@@ -10,16 +10,20 @@ import { SchemaInitializerOptions } from '../types';
import { useSchemaInitializerStyles } from './style';
export interface SchemaInitializerSubMenuProps {
- name: string;
+ name?: string;
title?: string;
onClick?: (args: any) => void;
onOpenChange?: (openKeys: string[]) => void;
icon?: string | ReactNode;
children?: SchemaInitializerOptions['items'];
+ items?: SchemaInitializerOptions['items'];
}
const SchemaInitializerSubMenuContext = React.createContext<{ isInMenu?: true }>({});
-const SchemaInitializerMenuProvider = (props) => {
+/**
+ * @internal
+ */
+export const SchemaInitializerMenuProvider = (props) => {
return (
{props.children}
@@ -71,25 +75,30 @@ export const SchemaInitializerMenu: FC = (props) => {
};
export const SchemaInitializerSubMenu: FC = (props) => {
- const { children, title, name = uid(), onOpenChange, icon, ...others } = props;
+ const { children, items: propItems, title, name, onOpenChange, icon, ...others } = props;
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 items = useMemo(() => {
return [
{
...others,
- key: name,
+ key: nameValue,
label: compile(title),
icon: typeof icon === 'string' ? : icon,
children: childrenItems,
},
];
- }, [childrenItems, compile, icon, name, others, title]);
+ }, [childrenItems, compile, icon, nameValue, others, title]);
return ;
};
+/**
+ * @internal
+ */
export const SchemaInitializerSubMenuInternal = () => {
const itemConfig = useSchemaInitializerItem();
return ;
diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSwitch.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSwitch.tsx
index 164242c6d8..fcf49a3713 100644
--- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSwitch.tsx
+++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSwitch.tsx
@@ -21,6 +21,9 @@ export const SchemaInitializerSwitch: FC = (pr
);
};
+/**
+ * @internal
+ */
export const SchemaInitializerSwitchInternal = () => {
const itemConfig = useSchemaInitializerItem();
return ;
diff --git a/packages/core/client/src/application/schema-initializer/hooks/index.tsx b/packages/core/client/src/application/schema-initializer/hooks/index.tsx
index da3cad0e77..83fda65c99 100644
--- a/packages/core/client/src/application/schema-initializer/hooks/index.tsx
+++ b/packages/core/client/src/application/schema-initializer/hooks/index.tsx
@@ -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';
-
-/**
- * @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> = 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(
- name: string,
- options?: Omit, 'name'>,
-) {
- const app = useApp();
- const renderCache = React.useRef>>({});
- const initializer = useMemo(
- () => app.schemaInitializerManager.get(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, '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;
-}
+export * from './useSchemaInitializerRender';
+export * from './useGetSchemaInitializerMenuItems';
diff --git a/packages/core/client/src/application/schema-initializer/hooks/useGetSchemaInitializerMenuItems.tsx b/packages/core/client/src/application/schema-initializer/hooks/useGetSchemaInitializerMenuItems.tsx
new file mode 100644
index 0000000000..69c2684c6b
--- /dev/null
+++ b/packages/core/client/src/application/schema-initializer/hooks/useGetSchemaInitializerMenuItems.tsx
@@ -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;
+}
diff --git a/packages/core/client/src/application/schema-initializer/hooks/useSchemaInitializerRender.tsx b/packages/core/client/src/application/schema-initializer/hooks/useSchemaInitializerRender.tsx
new file mode 100644
index 0000000000..3de839a01e
--- /dev/null
+++ b/packages/core/client/src/application/schema-initializer/hooks/useSchemaInitializerRender.tsx
@@ -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> = 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(
+ name: string,
+ options?: Omit, 'name'>,
+) {
+ const app = useApp();
+ const initializer = useMemo(
+ () => app.schemaInitializerManager.get(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, 'name'>) => {
+ return React.createElement(InitializerComponent, {
+ ...initializer.options,
+ ...options,
+ ...props,
+ });
+ },
+ };
+ }, [initializer, name, options]);
+
+ return res;
+}
diff --git a/packages/core/client/src/application/schema-initializer/index.ts b/packages/core/client/src/application/schema-initializer/index.ts
index 9bb2efd08f..5b1cc1b4a5 100644
--- a/packages/core/client/src/application/schema-initializer/index.ts
+++ b/packages/core/client/src/application/schema-initializer/index.ts
@@ -1,4 +1,4 @@
-export * from './hoc';
+export * from './withInitializer';
export * from './hooks';
export * from './types';
export * from './context';
diff --git a/packages/core/client/src/application/schema-initializer/hoc/index.tsx b/packages/core/client/src/application/schema-initializer/withInitializer.tsx
similarity index 91%
rename from packages/core/client/src/application/schema-initializer/hoc/index.tsx
rename to packages/core/client/src/application/schema-initializer/withInitializer.tsx
index 5ad695dfb3..bb6c65d73c 100644
--- a/packages/core/client/src/application/schema-initializer/hoc/index.tsx
+++ b/packages/core/client/src/application/schema-initializer/withInitializer.tsx
@@ -3,12 +3,12 @@ import { ConfigProvider, Popover, theme } from 'antd';
import React, { ComponentType, useCallback, useMemo, useState } from 'react';
import { css } from '@emotion/css';
-import { useNiceDropdownMaxHeight } from '../../../common/useNiceDropdownHeight';
-import { useFlag } from '../../../flag-provider';
-import { useDesignable } from '../../../schema-component';
-import { useSchemaInitializerStyles } from '../components/style';
-import { SchemaInitializerContext } from '../context';
-import { SchemaInitializerOptions } from '../types';
+import { useNiceDropdownMaxHeight } from '../../common/useNiceDropdownHeight';
+import { useFlag } from '../../flag-provider';
+import { useDesignable } from '../../schema-component';
+import { useSchemaInitializerStyles } from './components/style';
+import { SchemaInitializerContext } from './context';
+import { SchemaInitializerOptions } from './types';
const defaultWrap = (s: ISchema) => s;
diff --git a/packages/core/client/src/application/schema-settings/SchemaSettings.tsx b/packages/core/client/src/application/schema-settings/SchemaSettings.tsx
index d56aa81fec..2a1a4d4687 100644
--- a/packages/core/client/src/application/schema-settings/SchemaSettings.tsx
+++ b/packages/core/client/src/application/schema-settings/SchemaSettings.tsx
@@ -32,7 +32,8 @@ export class SchemaSettings {
if (!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) {
parentItem.children.push(data);
} else {
@@ -42,6 +43,7 @@ export class SchemaSettings {
}
get(nestedName: string): SchemaSettingsItemType | undefined {
+ if (!nestedName) return undefined;
const arr = nestedName.split('.');
let current: any = this.items;
@@ -58,8 +60,6 @@ export class SchemaSettings {
return undefined;
}
}
-
- return current;
}
remove(nestedName: string) {
diff --git a/packages/core/client/src/application/schema-settings/components/SchemaSettingsChildren.tsx b/packages/core/client/src/application/schema-settings/components/SchemaSettingsChildren.tsx
index bbf4a58598..c735d0a28c 100644
--- a/packages/core/client/src/application/schema-settings/components/SchemaSettingsChildren.tsx
+++ b/packages/core/client/src/application/schema-settings/components/SchemaSettingsChildren.tsx
@@ -78,7 +78,7 @@ export const SchemaSettingsChild: FC = memo((props) => {
type,
Component,
children,
- hideIfNoChildren = true,
+ hideIfNoChildren,
componentProps,
} = props as any;
const useChildrenRes = useChildren();
@@ -100,7 +100,7 @@ export const SchemaSettingsChild: FC = memo((props) => {
if (!C) {
return null;
}
- if (hideIfNoChildren && Array.isArray(componentChildren) && componentChildren.length === 0) {
+ if (hideIfNoChildren && !componentChildren) {
return null;
}
diff --git a/packages/core/client/src/application/schema-settings/context/SchemaSettingItemContext.ts b/packages/core/client/src/application/schema-settings/context/SchemaSettingItemContext.ts
new file mode 100644
index 0000000000..55e358ed1d
--- /dev/null
+++ b/packages/core/client/src/application/schema-settings/context/SchemaSettingItemContext.ts
@@ -0,0 +1,9 @@
+import { createContext, useContext } from 'react';
+import { SchemaSettingsItemType } from '../types';
+
+export const SchemaSettingItemContext = createContext({} as any);
+SchemaSettingItemContext.displayName = 'SchemaSettingItemContext';
+
+export function useSchemaSettingsItem() {
+ return useContext(SchemaSettingItemContext) as T;
+}
diff --git a/packages/core/client/src/application/schema-settings/context/index.ts b/packages/core/client/src/application/schema-settings/context/index.ts
index 5b197f7a6a..829c6c41ad 100644
--- a/packages/core/client/src/application/schema-settings/context/index.ts
+++ b/packages/core/client/src/application/schema-settings/context/index.ts
@@ -1,9 +1 @@
-import { createContext, useContext } from 'react';
-import { SchemaSettingsItemType } from '../types';
-
-export const SchemaSettingItemContext = createContext({} as any);
-SchemaSettingItemContext.displayName = 'SchemaSettingItemContext';
-
-export function useSchemaSettingsItem() {
- return useContext(SchemaSettingItemContext) as SchemaSettingsItemType;
-}
+export * from './SchemaSettingItemContext';
diff --git a/packages/core/client/src/application/schema-settings/hooks/index.tsx b/packages/core/client/src/application/schema-settings/hooks/index.tsx
index e5643ebf76..a52a2fbebc 100644
--- a/packages/core/client/src/application/schema-settings/hooks/index.tsx
+++ b/packages/core/client/src/application/schema-settings/hooks/index.tsx
@@ -1,50 +1 @@
-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 = Omit, 'name' | 'items'> &
- Omit & {
- fieldSchema?: Schema;
- field?: GeneralField;
- dn?: Designable;
- };
-
-export function useSchemaSettingsRender(name: string, options?: UseSchemaSettingsRenderOptions) {
- const app = useApp();
- const schemaSetting = useMemo(() => app.schemaSettingsManager.get(name), [app.schemaSettingsManager, name]);
- const renderCache = React.useRef>>({});
- 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,
- }));
- },
- };
-}
+export * from './useSchemaSettingsRender';
diff --git a/packages/core/client/src/application/schema-settings/hooks/useSchemaSettingsRender.tsx b/packages/core/client/src/application/schema-settings/hooks/useSchemaSettingsRender.tsx
new file mode 100644
index 0000000000..d881c842e0
--- /dev/null
+++ b/packages/core/client/src/application/schema-settings/hooks/useSchemaSettingsRender.tsx
@@ -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 = Omit, 'name' | 'items'> &
+ Omit & {
+ fieldSchema?: Schema;
+ field?: GeneralField;
+ dn?: Designable;
+ };
+
+export function useSchemaSettingsRender(name: string, options?: UseSchemaSettingsRenderOptions) {
+ const app = useApp();
+ const schemaSetting = useMemo(() => app.schemaSettingsManager.get(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,
+ });
+ },
+ };
+}
diff --git a/packages/core/client/src/data-source/__tests__/collection/AssociationProvider.test.tsx b/packages/core/client/src/data-source/__tests__/collection/AssociationProvider.test.tsx
index afe2f1b18f..d125f7fab3 100644
--- a/packages/core/client/src/data-source/__tests__/collection/AssociationProvider.test.tsx
+++ b/packages/core/client/src/data-source/__tests__/collection/AssociationProvider.test.tsx
@@ -94,6 +94,9 @@ describe('AssociationProvider', () => {
};
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();
});
});
diff --git a/packages/core/client/src/data-source/__tests__/collection/CollectionManager.test.tsx b/packages/core/client/src/data-source/__tests__/collection/CollectionManager.test.tsx
index ddfaf80c2d..d24eab8241 100644
--- a/packages/core/client/src/data-source/__tests__/collection/CollectionManager.test.tsx
+++ b/packages/core/client/src/data-source/__tests__/collection/CollectionManager.test.tsx
@@ -1,6 +1,5 @@
import { Application, CollectionManager, CollectionTemplate, Collection } from '@nocobase/client';
import collections from '../collections.json';
-import { app } from '../../../application/demos/demo3';
describe('CollectionManager', () => {
let collectionManager: CollectionManager;
@@ -227,4 +226,71 @@ describe('CollectionManager', () => {
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');
+ });
+ });
+ });
});
diff --git a/packages/core/client/src/data-source/__tests__/collection/CollectionManagerProvider.test.tsx b/packages/core/client/src/data-source/__tests__/collection/CollectionManagerProvider.test.tsx
index abe01efb83..f512aaa953 100644
--- a/packages/core/client/src/data-source/__tests__/collection/CollectionManagerProvider.test.tsx
+++ b/packages/core/client/src/data-source/__tests__/collection/CollectionManagerProvider.test.tsx
@@ -84,7 +84,7 @@ describe('CollectionManagerProvider', () => {
const Wrapper = () => {
return (
-
+
);
diff --git a/packages/core/client/src/data-source/__tests__/collection/CollectionProvider.test.tsx b/packages/core/client/src/data-source/__tests__/collection/CollectionProvider.test.tsx
index 639e336bb1..ee6ebaafe6 100644
--- a/packages/core/client/src/data-source/__tests__/collection/CollectionProvider.test.tsx
+++ b/packages/core/client/src/data-source/__tests__/collection/CollectionProvider.test.tsx
@@ -80,7 +80,7 @@ describe('CollectionProvider', () => {
renderApp(Demo, { name: 'not-exists', allowNull: false });
- expect(document.body.innerHTML).toContain('ant-result');
+ expect(screen.getByText('Delete')).toBeInTheDocument();
});
test('useCollectionFields() support predicate', () => {
diff --git a/packages/core/client/src/data-source/__tests__/components/CollectionDeletedPlaceholder.test.tsx b/packages/core/client/src/data-source/__tests__/components/CollectionDeletedPlaceholder.test.tsx
index d716197d02..1dadfc9921 100644
--- a/packages/core/client/src/data-source/__tests__/components/CollectionDeletedPlaceholder.test.tsx
+++ b/packages/core/client/src/data-source/__tests__/components/CollectionDeletedPlaceholder.test.tsx
@@ -1,7 +1,8 @@
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 { App } from 'antd';
function renderApp(name?: any, designable?: boolean) {
const schema = {
@@ -16,31 +17,45 @@ function renderApp(name?: any, designable?: boolean) {
render(
,
);
}
describe('CollectionDeletedPlaceholder', () => {
- test('name is undefined, render `Result` component', () => {
+ test('name is undefined, render `Result` component', async () => {
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', () => {
- renderApp('test', true);
+ test('designable: true, render `Result` component', () => {
+ renderApp('test', true);
+ expect(screen.getByText('Delete')).toBeInTheDocument();
+ expect(
+ screen.getByText('The collection "test" may have been deleted. Please remove this block.'),
+ ).toBeInTheDocument();
+ });
- expect(document.body.innerHTML).toContain('ant-result');
- });
+ test('designable: false, render nothing', () => {
+ renderApp('test', false);
- test('designable: false, render nothing', () => {
- renderApp('test', false);
-
- expect(screen.getByTestId('app').innerHTML.length).toBe(0);
- });
+ expect(screen.queryByText('Delete')).not.toBeInTheDocument();
});
});
diff --git a/packages/core/client/src/data-source/__tests__/data-source/DataSourceManager.test.ts b/packages/core/client/src/data-source/__tests__/data-source/DataSourceManager.test.ts
index 72aaeace17..7933735342 100644
--- a/packages/core/client/src/data-source/__tests__/data-source/DataSourceManager.test.ts
+++ b/packages/core/client/src/data-source/__tests__/data-source/DataSourceManager.test.ts
@@ -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';
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', () => {
test('should add a data source', () => {
const app = new Application();
@@ -184,5 +209,32 @@ describe('DataSourceManager', () => {
expect(reloadSpy1).toHaveBeenCalledTimes(1);
expect(reloadSpy2).toHaveBeenCalledTimes(1);
});
+
+ test('multi data sources', async () => {
+ const app = new Application();
+ const dataSourceManager = app.dataSourceManager;
+ const getThirdDataSource = (): Promise => {
+ 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);
+ });
});
});
diff --git a/packages/core/client/src/data-source/__tests__/data-source/DataSourceManagerProvider.test.tsx b/packages/core/client/src/data-source/__tests__/data-source/DataSourceManagerProvider.test.tsx
index 2f5649add0..f76a341837 100644
--- a/packages/core/client/src/data-source/__tests__/data-source/DataSourceManagerProvider.test.tsx
+++ b/packages/core/client/src/data-source/__tests__/data-source/DataSourceManagerProvider.test.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { render } from '@testing-library/react';
-import { DataSourceManagerProvider, useDataSourceManager } from '@nocobase/client';
+import { Application, DataSourceManagerProvider, useDataSourceManager } from '@nocobase/client';
describe('DataSourceManagerProvider', () => {
test('should render children', () => {
diff --git a/packages/core/client/src/data-source/__tests__/data-source/DataSourceProvider.test.tsx b/packages/core/client/src/data-source/__tests__/data-source/DataSourceProvider.test.tsx
index 8088514c38..b9fa71a452 100644
--- a/packages/core/client/src/data-source/__tests__/data-source/DataSourceProvider.test.tsx
+++ b/packages/core/client/src/data-source/__tests__/data-source/DataSourceProvider.test.tsx
@@ -5,10 +5,9 @@ import {
DataSourceOptions,
DataSource,
SchemaComponent,
- SchemaComponentProvider,
} from '@nocobase/client';
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 { AppSchemaComponentProvider } from '../../../application/AppSchemaComponentProvider';
@@ -54,6 +53,8 @@ describe('DataSourceProvider', () => {
,
);
+
+ return app.dataSourceManager.getDataSource(dataSource);
}
it('should render default dataSource', () => {
renderComponent();
@@ -67,16 +68,38 @@ describe('DataSourceProvider', () => {
it('should render error state when data source is not found', () => {
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', () => {
- renderComponent('test', 'loading');
+ it('should render loading state when data source is loading', async () => {
+ const ds = renderComponent('test', 'loading');
+ 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 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', () => {
renderComponent('test', 'loading-failed');
- expect(screen.getByText('DataSource "Test" loading failed')).toBeInTheDocument();
+ expect(screen.getByText('Data Source "Test" loading failed')).toBeInTheDocument();
});
});
diff --git a/packages/core/client/src/data-source/__tests__/utils.test.ts b/packages/core/client/src/data-source/__tests__/utils.test.ts
index d097966c26..167caec3f0 100644
--- a/packages/core/client/src/data-source/__tests__/utils.test.ts
+++ b/packages/core/client/src/data-source/__tests__/utils.test.ts
@@ -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';
describe('utils', () => {
@@ -42,4 +50,50 @@ describe('utils', () => {
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' });
+ });
+ });
});
diff --git a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterfaceManager.ts b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterfaceManager.ts
index 10089c4498..5f7a4b5b6e 100644
--- a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterfaceManager.ts
+++ b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterfaceManager.ts
@@ -37,7 +37,6 @@ export class CollectionFieldInterfaceManager {
if (item.notSupportDataSourceType) {
return !item.notSupportDataSourceType?.includes(dataSourceType);
}
- return true;
});
}
diff --git a/packages/core/client/src/data-source/collection-template/CollectionTemplateManager.ts b/packages/core/client/src/data-source/collection-template/CollectionTemplateManager.ts
index 7a52ece171..add83b3f61 100644
--- a/packages/core/client/src/data-source/collection-template/CollectionTemplateManager.ts
+++ b/packages/core/client/src/data-source/collection-template/CollectionTemplateManager.ts
@@ -46,7 +46,6 @@ export class CollectionTemplateManager {
if (item.notSupportDataSourceType) {
return !item.notSupportDataSourceType?.includes(dataSourceType);
}
- return true;
})
.sort((a, b) => (a.order || 0) - (b.order || 0));
}
diff --git a/packages/core/client/src/data-source/collection/CollectionManager.ts b/packages/core/client/src/data-source/collection/CollectionManager.ts
index e85a6487ed..5e5d5ac328 100644
--- a/packages/core/client/src/data-source/collection/CollectionManager.ts
+++ b/packages/core/client/src/data-source/collection/CollectionManager.ts
@@ -131,7 +131,7 @@ export class CollectionManager {
return this.getCollection(collectionName)?.getFields(predicate) || [];
}
- getSourceKeyByAssocation(associationName: string) {
+ getSourceKeyByAssociation(associationName: string) {
if (!associationName) {
return;
}
diff --git a/packages/core/client/src/data-source/components/CollectionDeletedPlaceholder.tsx b/packages/core/client/src/data-source/components/CollectionDeletedPlaceholder.tsx
index 33c3d8aec7..13470f53e2 100644
--- a/packages/core/client/src/data-source/components/CollectionDeletedPlaceholder.tsx
+++ b/packages/core/client/src/data-source/components/CollectionDeletedPlaceholder.tsx
@@ -8,7 +8,7 @@ import { DEFAULT_DATA_SOURCE_KEY } from '../../data-source/data-source/DataSourc
import { useCollection } from '../collection';
export interface CollectionDeletedPlaceholderProps {
- type: 'Collection' | 'Field' | 'DataSource';
+ type: 'Collection' | 'Field' | 'Data Source' | 'Block template';
name?: string | number;
message?: string;
}
@@ -27,7 +27,7 @@ export const CollectionDeletedPlaceholder: FC
const collection = useCollection();
const dataSourceManager = useDataSourceManager();
const nameValue = useMemo(() => {
- if (type === 'DataSource') {
+ if (type === 'Data Source') {
return name;
}
const dataSourcePrefix =
@@ -60,7 +60,7 @@ export const CollectionDeletedPlaceholder: FC
return t(`The {{type}} "{{name}}" may have been deleted. Please remove this {{blockType}}.`, {
type: t(type).toLocaleLowerCase(),
name: nameValue,
- blockType: t(blockType),
+ blockType: t(blockType).toLocaleLowerCase(),
}).replaceAll('>', '>');
}, [message, nameValue, type, t, blockType]);
diff --git a/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx b/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx
index 6dba28bd99..124582af84 100644
--- a/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx
+++ b/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx
@@ -23,7 +23,7 @@ export const DataBlockResourceProvider: FC<{ children?: ReactNode }> = ({ childr
return sourceId;
}
if (association && parentRecord) {
- const sourceKey = cm.getSourceKeyByAssocation(association);
+ const sourceKey = cm.getSourceKeyByAssociation(association);
const parentRecordData = parentRecord instanceof CollectionRecord ? parentRecord.data : parentRecord;
return parentRecordData[sourceKey];
}
diff --git a/packages/core/client/src/data-source/data-source/DataSourceManager.ts b/packages/core/client/src/data-source/data-source/DataSourceManager.ts
index 14b732d12c..96c78a4960 100644
--- a/packages/core/client/src/data-source/data-source/DataSourceManager.ts
+++ b/packages/core/client/src/data-source/data-source/DataSourceManager.ts
@@ -56,9 +56,9 @@ export class DataSourceManager {
this.getDataSources().forEach((dataSource) => dataSource.collectionManager.reAddCollections());
}
- getDataSources(filterCollection?: (dataSource: DataSource) => boolean) {
+ getDataSources(filterDataSource?: (dataSource: DataSource) => boolean) {
const allDataSources = Object.values(this.dataSourceInstancesMap);
- return filterCollection ? _.filter(allDataSources, filterCollection) : allDataSources;
+ return filterDataSource ? _.filter(allDataSources, filterDataSource) : allDataSources;
}
getDataSource(key?: string) {
diff --git a/packages/core/client/src/data-source/data-source/DataSourceProvider.tsx b/packages/core/client/src/data-source/data-source/DataSourceProvider.tsx
index 273886b761..ef4e4202da 100644
--- a/packages/core/client/src/data-source/data-source/DataSourceProvider.tsx
+++ b/packages/core/client/src/data-source/data-source/DataSourceProvider.tsx
@@ -23,13 +23,13 @@ export const DataSourceProvider: FC = ({ children, data
const dataSourceValue = dataSourceManager.getDataSource(dataSource);
if (!dataSourceValue) {
- return ;
+ return ;
}
if (dataSourceValue.status === 'loading-failed') {
return (
diff --git a/packages/core/client/src/locale/en_US.json b/packages/core/client/src/locale/en_US.json
index aab05b7785..15ed588803 100644
--- a/packages/core/client/src/locale/en_US.json
+++ b/packages/core/client/src/locale/en_US.json
@@ -500,6 +500,7 @@
"Save as template": "Save as template",
"Save as block template": "Save as block template",
"Block templates": "Block templates",
+ "Block template": "Block template",
"Convert reference to duplicate": "Convert reference to duplicate",
"Template name": "Template name",
"Block type": "Block type",
@@ -801,6 +802,7 @@
"loading": "loading",
"name is required": "name is required",
"data source": "data source",
+ "Data source": "Data source",
"DataSource": "DataSource",
"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",
diff --git a/packages/core/client/src/locale/es_ES.json b/packages/core/client/src/locale/es_ES.json
index 148ed25971..44c8126d06 100644
--- a/packages/core/client/src/locale/es_ES.json
+++ b/packages/core/client/src/locale/es_ES.json
@@ -471,6 +471,7 @@
"Save as template": "Guardar como plantilla",
"Save as block template": "Guardar como plantilla de bloque",
"Block templates": "Bloquear plantillas",
+ "Block template": "Plantilla de bloque",
"Convert reference to duplicate": "Convertir referencia a duplicado",
"Template name": "Nombre de plantilla",
"Block type": "Tipo de bloque",
@@ -749,6 +750,7 @@
"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}}.",
"data source": "fuente de datos",
+ "Data source": "fuente de datos",
"DataSource": "Fuente de datos",
"Home page": "Página de inicio",
"Handbook": "Manual de usuario",
diff --git a/packages/core/client/src/locale/fr_FR.json b/packages/core/client/src/locale/fr_FR.json
index 96844e314d..f43b3948cd 100644
--- a/packages/core/client/src/locale/fr_FR.json
+++ b/packages/core/client/src/locale/fr_FR.json
@@ -486,6 +486,7 @@
"Save as template": "Enregistrer en tant que modèle",
"Save as block template": "Enregistrer en tant que modèle 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",
"Template name": "Nom du modèle",
"Block type": "Type de bloc",
@@ -767,6 +768,7 @@
"loading": "chargement",
"name is required": "le nom est requis",
"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}}.",
"DataSource": "Source de données",
"Allow selection of existing records":"Permet de sélectionner des données existantes",
diff --git a/packages/core/client/src/locale/ja_JP.json b/packages/core/client/src/locale/ja_JP.json
index bd7602c985..4a69fecbee 100644
--- a/packages/core/client/src/locale/ja_JP.json
+++ b/packages/core/client/src/locale/ja_JP.json
@@ -398,6 +398,7 @@
"Save as template": "テンプレートとして保存",
"Save as block template": "ブロックテンプレートとして保存",
"Block templates": "ブロックテンプレート",
+ "Block template": "ブロックテンプレート",
"Convert reference to duplicate": "参照を複製に変換",
"Template name": "テンプレート名",
"Block type": "ブロックタイプ",
@@ -687,8 +688,9 @@
"loading": "ロード中",
"name is required": "名前は必須です",
"data source": "データソース",
- "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" は削除されている可能性があります。この {{blockType}} を削除してください。",
+ "Data source": "データソース",
"DataSource": "データソース",
+ "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" は削除されている可能性があります。この {{blockType}} を削除してください。",
"Home page": "ホームページ",
"Handbook": "ユーザーマニュアル",
"License": "ライセンス"
diff --git a/packages/core/client/src/locale/ko_KR.json b/packages/core/client/src/locale/ko_KR.json
index b6851be465..f474e41a31 100644
--- a/packages/core/client/src/locale/ko_KR.json
+++ b/packages/core/client/src/locale/ko_KR.json
@@ -518,6 +518,7 @@
"Save as template": "템플릿으로 저장",
"Save as block template": "블록 템플릿으로 저장",
"Block templates": "블록 템플릿",
+ "Block template": "블록 템플릿",
"Convert reference to duplicate": "참조를 복제로 변환",
"Template name": "템플릿 이름",
"Block type": "블록 타입",
@@ -859,8 +860,9 @@
"loading": "로드 중",
"name is required": "이름은 필수입니다",
"data source": "데이터 소스",
- "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\"이(가) 삭제되었을 수 있습니다. 이 {{blockType}}을(를) 제거하십시오.",
+ "Data source": "데이터 소스",
"DataSource": "데이터 소스",
+ "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\"이(가) 삭제되었을 수 있습니다. 이 {{blockType}}을(를) 제거하십시오.",
"Home page": "홈페이지",
"Handbook": "사용자 매뉴얼",
"License": "라이선스"
diff --git a/packages/core/client/src/locale/pt_BR.json b/packages/core/client/src/locale/pt_BR.json
index acf18447f8..a24cc333fa 100644
--- a/packages/core/client/src/locale/pt_BR.json
+++ b/packages/core/client/src/locale/pt_BR.json
@@ -434,6 +434,7 @@
"Save as template": "Salvar como modelo",
"Save as block template": "Salvar como modelo de bloco",
"Block templates": "Modelos de bloco",
+ "Block template": "Modelo de bloco",
"Convert reference to duplicate": "Converter referência em duplicado",
"Template name": "Nome do modelo",
"Block type": "Tipo de bloco",
@@ -726,8 +727,9 @@
"loading": "carregando",
"name is required": "nome é obrigatório",
"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",
+ "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",
"Home page": "Página inicial",
"Handbook": "Manual do usuário",
diff --git a/packages/core/client/src/locale/ru_RU.json b/packages/core/client/src/locale/ru_RU.json
index 31f1a6a24b..4f643ed7e1 100644
--- a/packages/core/client/src/locale/ru_RU.json
+++ b/packages/core/client/src/locale/ru_RU.json
@@ -337,7 +337,7 @@
"Other records": "Другие записи",
"Save as template": "Сохранить как шаблон",
"Save as block template": "Сохранить как шаблон Блока",
- "Block templates": "Шаблоны Блока",
+ "Block templates": "Шаблоны блоков",
"Convert reference to duplicate": "Преобразовать ссылку в дубликат",
"Template name": "Имя Шаблона",
"Block type": "Тип Блока",
@@ -563,6 +563,7 @@
"loading": "загрузка",
"name is required": "имя обязательно",
"data source": "источник данных",
+ "Data source": "источник данных",
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" возможно был удален. Пожалуйста, удалите этот {{blockType}}.",
"DataSource": "Источник данных",
"Home page": "Домашняя страница",
diff --git a/packages/core/client/src/locale/tr_TR.json b/packages/core/client/src/locale/tr_TR.json
index 1fecdf9dec..bb411c3baf 100644
--- a/packages/core/client/src/locale/tr_TR.json
+++ b/packages/core/client/src/locale/tr_TR.json
@@ -337,6 +337,7 @@
"Save as template": "Şablon olarak kaydet",
"Save as block template": "Blok şablonu olarak kaydet",
"Block templates": "Blok şablonları",
+ "Block template": "Blok şablonu",
"Convert reference to duplicate": "Referansı kopyaya dönüştür",
"Template name": "Şablon adı",
"Block type": "Blok türü",
@@ -560,8 +561,9 @@
"loading": "yükleniyor",
"name is required": "ad gereklidir",
"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ğı",
+ "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",
"Handbook": "Kullanıcı kılavuzu",
"License": "Lisans"
diff --git a/packages/core/client/src/locale/uk_UA.json b/packages/core/client/src/locale/uk_UA.json
index 1b7d9e74e5..66d8088499 100644
--- a/packages/core/client/src/locale/uk_UA.json
+++ b/packages/core/client/src/locale/uk_UA.json
@@ -488,6 +488,7 @@
"Save as template": "Зберегти як шаблон",
"Save as block template": "Зберегти як шаблон блока",
"Block templates": "Шаблони блоків",
+ "Block template": "Шаблон блока",
"Convert reference to duplicate": "Конвертувати посилання на дублікат",
"Template name": "Назва шаблону",
"Block type": "Тип блока",
@@ -768,8 +769,9 @@
"loading": "завантаження",
"name is required": "ім'я обов'язкове",
"data source": "джерело даних",
- "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" може бути видалено. Будь ласка, видаліть цей {{blockType}}.",
+ "Data source": "джерело даних",
"DataSource": "Джерело даних",
+ "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" може бути видалено. Будь ласка, видаліть цей {{blockType}}.",
"Home page": "Домашня сторінка",
"Handbook": "Посібник користувача",
"License": "Ліцензія"
diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json
index 85e11884b3..e3caf20fe0 100644
--- a/packages/core/client/src/locale/zh-CN.json
+++ b/packages/core/client/src/locale/zh-CN.json
@@ -521,6 +521,7 @@
"Save as template": "保存为模板",
"Save as block template": "保存为区块模板",
"Block templates": "区块模板",
+ "Block template": "区块模板",
"Convert reference to duplicate": "模板引用转为复制",
"Template name": "模板名称",
"Block type": "区块类型",
@@ -864,6 +865,7 @@
"loading": "加载中",
"name is required": "名称不能为空",
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" 可能已被删除。请删除当前{{blockType}}.",
+ "data source": "数据源",
"Data source": "数据源",
"DataSource": "数据源",
"Data model": "数据模型",
diff --git a/packages/core/client/src/locale/zh-TW.json b/packages/core/client/src/locale/zh-TW.json
index 5abc14367f..b62ae0a014 100644
--- a/packages/core/client/src/locale/zh-TW.json
+++ b/packages/core/client/src/locale/zh-TW.json
@@ -518,6 +518,7 @@
"Save as template": "儲存為模板",
"Save as block template": "儲存為區塊模板",
"Block templates": "區塊模板",
+ "Block template": "區塊模板",
"Convert reference to duplicate": "模板引用轉為複製",
"Template name": "模板名稱",
"Block type": "區塊型別",
@@ -856,6 +857,9 @@
"Permission denied": "沒有權限",
"Allow add new":"允許新增",
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" 可能已被刪除,請移除這個 {{blockType}}。",
+ "data source": "數據源",
+ "Data source": "數據源",
+ "DataSource": "數據源",
"Allow selection of existing records":"允許選擇已有資料",
"Home page": "主頁",
"Handbook": "使用手冊",
diff --git a/packages/core/client/src/modules/blocks/useSourceKey.ts b/packages/core/client/src/modules/blocks/useSourceKey.ts
index b42c47c3ee..5bcb804138 100644
--- a/packages/core/client/src/modules/blocks/useSourceKey.ts
+++ b/packages/core/client/src/modules/blocks/useSourceKey.ts
@@ -7,5 +7,5 @@ import { useCollectionManager } from '../../data-source/collection/CollectionMan
*/
export const useSourceKey = (association: string) => {
const cm = useCollectionManager();
- return cm.getSourceKeyByAssocation(association);
+ return cm.getSourceKeyByAssociation(association);
};
diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterProvider.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterProvider.tsx
index cf22c69fcc..472ae251ea 100644
--- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterProvider.tsx
+++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterProvider.tsx
@@ -1,2 +1,2 @@
// TODO: 因他们之间功能相同,所以先直接复用,后续有需要再拆分
-export { TableBlockProvider as AssociationFilterProvider } from '../../../block-provider';
+export { TableBlockProvider as AssociationFilterProvider } from '../../../block-provider/TableBlockProvider';
diff --git a/packages/core/client/src/schema-component/antd/checkbox/__tests__/checkbox.test.tsx b/packages/core/client/src/schema-component/antd/checkbox/__tests__/checkbox.test.tsx
index 4be0abc9c7..e772edd8fd 100644
--- a/packages/core/client/src/schema-component/antd/checkbox/__tests__/checkbox.test.tsx
+++ b/packages/core/client/src/schema-component/antd/checkbox/__tests__/checkbox.test.tsx
@@ -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 App1 from '../demos/checkbox';
import App2 from '../demos/checkbox.group';
+import { Checkbox } from '@nocobase/client';
describe('Checkbox', () => {
it('should display the title', () => {
@@ -37,6 +38,67 @@ describe('Checkbox', () => {
await userEvent.click(input);
expect(container.querySelector('svg')).toBeNull();
});
+
+ describe('read pretty', () => {
+ it('true', async () => {
+ const { container } = await renderReadPrettyApp({
+ Component: Checkbox,
+ value: true,
+ });
+
+ expect(container.querySelector('svg')).toMatchInlineSnapshot(`
+
+ `);
+ });
+
+ 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(`
+
+ `);
+ });
+ });
});
describe('Checkbox.Group', () => {
diff --git a/packages/core/client/src/schema-component/antd/collection-select/__tests__/collection-select.test.tsx b/packages/core/client/src/schema-component/antd/collection-select/__tests__/collection-select.test.tsx
index 29652ca80a..38b7242f22 100644
--- a/packages/core/client/src/schema-component/antd/collection-select/__tests__/collection-select.test.tsx
+++ b/packages/core/client/src/schema-component/antd/collection-select/__tests__/collection-select.test.tsx
@@ -1,22 +1,255 @@
-import { render, screen, userEvent } from '@nocobase/test/client';
-import React from 'react';
-import App1 from '../demos/demo1';
+import { renderReadPrettyApp, renderApp, screen, userEvent, waitFor } from '@nocobase/test/client';
+import { FormItem, CollectionSelect } from '@nocobase/client';
-describe.skip('CollectionSelect', () => {
+describe('CollectionSelect', () => {
it('should works', async () => {
- render();
+ const { container } = await renderApp({
+ schema: {
+ type: 'object',
+ properties: {
+ test: {
+ type: 'string',
+ title: 'demo title',
+ 'x-decorator': FormItem,
+ 'x-component': CollectionSelect,
+ },
+ },
+ },
+ });
- // 标题
- expect(screen.getByText('Collection')).toBeInTheDocument();
+ expect(screen.getByText('demo title')).toBeInTheDocument();
- const selector = document.querySelector('.ant-select-selector');
- expect(selector).toBeInTheDocument();
+ await userEvent.click(document.querySelector('.ant-select-selector'));
+ 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(`
+
+ `);
+ });
+
+ 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(`
+
+ `);
+ });
+
+ 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('Roles')).toBeInTheDocument();
- expect(screen.getByText('测试表')).toBeInTheDocument();
});
});
diff --git a/packages/core/client/src/schema-component/antd/color-picker/__tests__/ColorPicker.test.tsx b/packages/core/client/src/schema-component/antd/color-picker/__tests__/ColorPicker.test.tsx
new file mode 100644
index 0000000000..aca163fe64
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/color-picker/__tests__/ColorPicker.test.tsx
@@ -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(`
+
+ `);
+ });
+
+ 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(`
+
+ `);
+ });
+});
diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.Settings.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.Settings.tsx
index 50929d7a78..83e53eb71c 100644
--- a/packages/core/client/src/schema-component/antd/form-item/FormItem.Settings.tsx
+++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.Settings.tsx
@@ -960,8 +960,11 @@ function useFormItemCollectionField() {
const { getCollectionJoinField } = useCollectionManager_deprecated();
const { getField } = useCollection_deprecated();
const fieldSchema = useFieldSchema();
- const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const { collectionField: columnCollectionField } = useColumnSchema();
+ const collectionField = fieldSchema
+ ? getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field'])
+ : null;
+ if (!fieldSchema) return null;
return collectionField || columnCollectionField;
}
diff --git a/packages/core/client/src/schema-component/antd/form-v2/__tests__/form-v2.test.tsx b/packages/core/client/src/schema-component/antd/form-v2/__tests__/form-v2.test.tsx
index 0bf275a5eb..f8bf7d8af3 100644
--- a/packages/core/client/src/schema-component/antd/form-v2/__tests__/form-v2.test.tsx
+++ b/packages/core/client/src/schema-component/antd/form-v2/__tests__/form-v2.test.tsx
@@ -18,8 +18,8 @@ describe('FormV2', () => {
await userEvent.type(input, '李四');
+ await userEvent.click(screen.getByText('Submit'));
await waitFor(async () => {
- await userEvent.click(screen.getByText('Submit'));
// notification 的内容
expect(screen.getByText(/\{"nickname":"李四"\}/i)).toBeInTheDocument();
});
diff --git a/packages/core/client/src/schema-component/antd/input/__tests__/EllipsisWithTooltip.test.tsx b/packages/core/client/src/schema-component/antd/input/__tests__/EllipsisWithTooltip.test.tsx
new file mode 100644
index 0000000000..2b6689095e
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/input/__tests__/EllipsisWithTooltip.test.tsx
@@ -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();
+
+ useEffect(() => {
+ ref.current.setPopoverVisible(true);
+ }, []);
+
+ return (
+
+ {text}
+
+ );
+ };
+
+ 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();
+ await noPopoverCheck();
+ });
+
+ it('shows Popover when ellipsis is true and content overflows', async () => {
+ setHasEllipsis();
+
+ render();
+
+ await hasPopoverCheck();
+ });
+
+ it('does not show Popover when content does not overflow', async () => {
+ setNoEllipsis();
+
+ render();
+ await noPopoverCheck();
+ });
+
+ it('uses popoverContent when provided', async () => {
+ setHasEllipsis();
+
+ const popoverContent = 'Custom Popover Content';
+
+ render();
+ await hasPopoverCheck(popoverContent);
+ });
+});
diff --git a/packages/core/client/src/schema-component/antd/menu/__tests__/utils.test.ts b/packages/core/client/src/schema-component/antd/menu/__tests__/utils.test.ts
new file mode 100644
index 0000000000..a7085c2b4f
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/menu/__tests__/utils.test.ts
@@ -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',
+ }),
+ );
+ });
+});
diff --git a/packages/core/client/src/schema-component/antd/page/__tests__/page.test.tsx b/packages/core/client/src/schema-component/antd/page/__tests__/page.test.tsx
index f2518865ed..64142fba0b 100644
--- a/packages/core/client/src/schema-component/antd/page/__tests__/page.test.tsx
+++ b/packages/core/client/src/schema-component/antd/page/__tests__/page.test.tsx
@@ -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 App1 from '../demos/demo1';
+import { Page } from '../Page';
+import { DocumentTitleProvider, Form, FormItem, Grid, IconPicker, Input } from '@nocobase/client';
describe('Page', () => {
it('should render correctly', async () => {
@@ -16,4 +18,129 @@ describe('Page', () => {
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();
+ });
+ });
+ });
});
diff --git a/packages/core/client/src/schema-component/antd/pagination/__tests__/pagination.test.tsx b/packages/core/client/src/schema-component/antd/pagination/__tests__/pagination.test.tsx
new file mode 100644
index 0000000000..6f1c928cc4
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/pagination/__tests__/pagination.test.tsx
@@ -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(`
+
+ `);
+ });
+
+ it('hides when hidden prop is true', async () => {
+ const { container } = await renderApp({
+ Component: Pagination,
+ props: {
+ hidden: true,
+ },
+ });
+ expect(container).toMatchInlineSnapshot(`
+
+ `);
+ });
+});
diff --git a/packages/core/client/src/schema-component/antd/password/__tests__/PasswordStrength.test.tsx b/packages/core/client/src/schema-component/antd/password/__tests__/PasswordStrength.test.tsx
new file mode 100644
index 0000000000..efbafb07c5
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/password/__tests__/PasswordStrength.test.tsx
@@ -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(Strength: {strengthValue}
);
+ const { getByText } = render({childrenMock});
+ expect(childrenMock).toHaveBeenCalledWith(strengthValue);
+ expect(getByText(`Strength: ${strengthValue}`)).toBeInTheDocument();
+ });
+
+ it('renders children without strength value', () => {
+ const childrenMock = vitest.fn().mockReturnValue(No strength value
);
+ const { getByText } = render({childrenMock});
+ expect(childrenMock).toHaveBeenCalledWith(0);
+ expect(getByText('No strength value')).toBeInTheDocument();
+ });
+
+ it('renders children without function', () => {
+ const childrenMock = Children without function
;
+ const { getByText } = render({childrenMock});
+ expect(getByText('Children without function')).toBeInTheDocument();
+ });
+});
diff --git a/packages/core/client/src/schema-component/antd/password/__tests__/utils.test.ts b/packages/core/client/src/schema-component/antd/password/__tests__/utils.test.ts
index 0aceb2a11a..8b0201498b 100644
--- a/packages/core/client/src/schema-component/antd/password/__tests__/utils.test.ts
+++ b/packages/core/client/src/schema-component/antd/password/__tests__/utils.test.ts
@@ -24,4 +24,60 @@ describe('getStrength', () => {
it('should return 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);
+ });
});
diff --git a/packages/core/client/src/schema-component/antd/space/__tests__/space.test.tsx b/packages/core/client/src/schema-component/antd/space/__tests__/space.test.tsx
new file mode 100644
index 0000000000..b06db746fd
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/space/__tests__/space.test.tsx
@@ -0,0 +1,15 @@
+import { render } from '@testing-library/react';
+import React from 'react';
+import { Space } from '../index';
+
+describe('Space', () => {
+ it('renders without error', () => {
+ const { container } = render();
+ expect(container).toMatchInlineSnapshot(``);
+ });
+
+ it('renders with custom split', () => {
+ const { container } = render();
+ expect(container).toMatchInlineSnapshot(``);
+ });
+});
diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Decorator.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Decorator.tsx
index e26bed37c9..ef35a6ae4c 100644
--- a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Decorator.tsx
+++ b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Decorator.tsx
@@ -17,7 +17,7 @@ export const useColumnSchema = () => {
const compile = useCompile();
const columnSchema = useFieldSchema();
const { getCollectionJoinField } = useCollectionManager_deprecated();
- const fieldSchema = columnSchema.reduceProperties((buf, s) => {
+ const fieldSchema = columnSchema?.reduceProperties((buf, s) => {
if (isCollectionFieldComponent(s)) {
return s;
}
diff --git a/packages/core/client/src/schema-component/antd/unixTimestamp/__tests__/UnixTimestamp.test.tsx b/packages/core/client/src/schema-component/antd/unixTimestamp/__tests__/UnixTimestamp.test.tsx
new file mode 100644
index 0000000000..9e4f4bbd6a
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/unixTimestamp/__tests__/UnixTimestamp.test.tsx
@@ -0,0 +1,138 @@
+import { screen, renderApp, userEvent, waitFor, renderReadPrettyApp } from '@nocobase/test/client';
+import { UnixTimestamp } from '@nocobase/client';
+
+describe('UnixTimestamp', () => {
+ it('renders without errors', async () => {
+ const { container } = await renderApp({
+ Component: UnixTimestamp,
+ value: 0,
+ });
+ expect(container).toMatchInlineSnapshot(`
+
+ `);
+ });
+
+ it('millisecond', async () => {
+ await renderApp({
+ Component: UnixTimestamp,
+ value: 1712819630000,
+ });
+ await waitFor(() => {
+ expect(screen.getByRole('textbox')).toHaveValue('2024-04-11');
+ });
+ });
+
+ it('second', async () => {
+ await renderApp({
+ Component: UnixTimestamp,
+ value: 1712819630,
+ props: {
+ accuracy: 'second',
+ },
+ });
+
+ await waitFor(() => {
+ expect(screen.getByRole('textbox')).toHaveValue('2024-04-11');
+ });
+ });
+
+ it('string', async () => {
+ await renderApp({
+ Component: UnixTimestamp,
+ value: '2024-04-11',
+ });
+
+ await waitFor(() => {
+ expect(screen.getByRole('textbox')).toHaveValue('2024-04-11');
+ });
+ });
+
+ it('change', async () => {
+ const onChange = vitest.fn();
+ await renderApp({
+ Component: UnixTimestamp,
+ value: '2024-04-11',
+ onChange,
+ });
+ await userEvent.click(screen.getByRole('textbox'));
+
+ await waitFor(() => {
+ expect(screen.queryByRole('table')).toBeInTheDocument();
+ });
+
+ await userEvent.click(document.querySelector('td[title="2024-04-12"]'));
+
+ await waitFor(() => {
+ expect(screen.getByRole('textbox')).toHaveValue('2024-04-12');
+ });
+ expect(onChange).toBeCalledWith(1712880000000);
+ });
+
+ it('read pretty', async () => {
+ const { container } = await renderReadPrettyApp({
+ Component: UnixTimestamp,
+ value: '2024-04-11',
+ });
+
+ expect(screen.getByText('2024-04-11')).toBeInTheDocument();
+ expect(container).toMatchInlineSnapshot(`
+
+ `);
+ });
+});
diff --git a/packages/core/client/src/schema-component/types.ts b/packages/core/client/src/schema-component/types.ts
index c8a01ee8b3..54b2d152bc 100644
--- a/packages/core/client/src/schema-component/types.ts
+++ b/packages/core/client/src/schema-component/types.ts
@@ -19,6 +19,7 @@ export interface ISchemaComponentProvider {
form?: Form;
scope?: any;
components?: SchemaReactComponents;
+ children?: React.ReactNode;
}
export interface IRecursionComponentProps extends IRecursionFieldProps {
diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts
index b206af9855..34e6a713de 100644
--- a/packages/core/client/src/schema-initializer/utils.ts
+++ b/packages/core/client/src/schema-initializer/utils.ts
@@ -11,7 +11,6 @@ import {
useCollection,
useCollectionManager,
useDataSourceKey,
- useFormActiveFields,
useFormBlockContext,
} from '../';
import { FieldOptions, useCollectionManager_deprecated, useCollection_deprecated } from '../collection-manager';
@@ -20,7 +19,7 @@ import { useDataSourceManager } from '../data-source/data-source/DataSourceManag
import { isAssocField } from '../filter-provider/utils';
import { useActionContext, useCompile, useDesignable } from '../schema-component';
import { useSchemaTemplateManager } from '../schema-templates';
-
+import { useFormActiveFields } from '../block-provider/hooks/useFormActiveFields';
export const itemsMerge = (items1) => {
return items1;
};
diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx
index 68266d0dd3..a5869f83e9 100644
--- a/packages/core/client/src/schema-settings/SchemaSettings.tsx
+++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx
@@ -460,7 +460,6 @@ export const SchemaSettingsItem: FC = (props) => {
const { pushMenuItem } = useCollectMenuItems();
const { collectMenuItem } = useCollectMenuItem();
const { eventKey, title } = props;
- const { name } = useSchemaSettingsItem();
if (process.env.NODE_ENV !== 'production' && !title) {
throw new Error('SchemaSettingsItem must have a title');
diff --git a/packages/core/client/src/schema-templates/BlockTemplate.tsx b/packages/core/client/src/schema-templates/BlockTemplate.tsx
index 9edb013463..d606a0791f 100644
--- a/packages/core/client/src/schema-templates/BlockTemplate.tsx
+++ b/packages/core/client/src/schema-templates/BlockTemplate.tsx
@@ -1,6 +1,6 @@
import { observer, useField, useFieldSchema } from '@formily/react';
import React, { createContext, useContext, useMemo } from 'react';
-import { RemoteSchemaComponent, useDesignable } from '..';
+import { CollectionDeletedPlaceholder, RemoteSchemaComponent, useDesignable } from '..';
import { useSchemaTemplateManager } from './SchemaTemplateManagerProvider';
import { useTemplateBlockContext } from '../block-provider/TemplateBlockProvider';
@@ -30,7 +30,9 @@ export const BlockTemplate = observer(
- ) : null;
+ ) : (
+
+ );
},
{ displayName: 'BlockTemplate' },
);
diff --git a/packages/core/client/src/schema-templates/schemas/uiSchemaTemplates.ts b/packages/core/client/src/schema-templates/schemas/uiSchemaTemplates.ts
index d6a21af5d9..e865f2d7f5 100644
--- a/packages/core/client/src/schema-templates/schemas/uiSchemaTemplates.ts
+++ b/packages/core/client/src/schema-templates/schemas/uiSchemaTemplates.ts
@@ -4,6 +4,7 @@ import { useBulkDestroyActionProps, useDestroyActionProps, useUpdateActionProps
import { uiSchemaTemplatesCollection } from '../collections/uiSchemaTemplates';
import { CollectionTitle } from './CollectionTitle';
import { useBlockRequestContext } from '../../block-provider';
+import { useSchemaTemplateManager } from '../SchemaTemplateManagerProvider';
const useUpdateSchemaTemplateActionProps = () => {
const props = useUpdateActionProps();
@@ -18,11 +19,13 @@ const useUpdateSchemaTemplateActionProps = () => {
const useBulkDestroyTemplateProps = () => {
const props = useBulkDestroyActionProps();
+ const bm = useSchemaTemplateManager();
const { service } = useBlockRequestContext();
return {
async onClick() {
await props.onClick();
+ await bm.refresh();
service?.refresh?.();
},
};
@@ -31,9 +34,11 @@ const useBulkDestroyTemplateProps = () => {
const useDestroyTemplateProps = () => {
const props = useDestroyActionProps();
const { service } = useBlockRequestContext();
+ const bm = useSchemaTemplateManager();
return {
async onClick() {
await props.onClick();
+ await bm.refresh();
service?.refresh?.();
},
};
diff --git a/packages/core/test/package.json b/packages/core/test/package.json
index de84b3db07..8e9349f5ee 100644
--- a/packages/core/test/package.json
+++ b/packages/core/test/package.json
@@ -26,6 +26,16 @@
"default": "./es/client/index.mjs"
}
},
+ "./web": {
+ "require": {
+ "types": "./lib/web/index.d.ts",
+ "default": "./lib/web/index.js"
+ },
+ "import": {
+ "types": "./es/web/index.d.ts",
+ "default": "./es/web/index.mjs"
+ }
+ },
"./e2e": {
"require": {
"types": "./lib/e2e/index.d.ts",
@@ -40,6 +50,7 @@
"./vitest.mjs": "./vitest.mjs"
},
"dependencies": {
+ "axios-mock-adapter": "1.22.0",
"@faker-js/faker": "8.1.0",
"@nocobase/server": "0.21.0-alpha.6",
"@playwright/test": "^1.42.1",
diff --git a/packages/core/test/src/client/index.ts b/packages/core/test/src/client/index.ts
deleted file mode 100644
index 0327ed2549..0000000000
--- a/packages/core/test/src/client/index.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { render } from '@testing-library/react';
-export { renderHook } from '@testing-library/react-hooks';
-
-function customRender(ui: React.ReactElement, options = {}) {
- return render(ui, {
- // wrap provider(s) here if needed
- wrapper: ({ children }) => children,
- ...options,
- });
-}
-
-export * from '@testing-library/react';
-export { default as userEvent } from '@testing-library/user-event';
-// override render export
-export { customRender as render };
-
-export const sleep = async (timeout = 0) => {
- return new Promise((resolve) => {
- setTimeout(resolve, timeout);
- });
-};
diff --git a/packages/core/test/src/client/index.tsx b/packages/core/test/src/client/index.tsx
new file mode 100644
index 0000000000..a6e26d286a
--- /dev/null
+++ b/packages/core/test/src/client/index.tsx
@@ -0,0 +1,69 @@
+import { expect } from 'vitest';
+import React, { FC, Fragment } from 'react';
+import { render, waitFor, screen } from '@testing-library/react';
+import { renderHook } from '@testing-library/react-hooks';
+import { GetAppComponentOptions, GetAppOptions, getApp, getAppComponent } from '../web';
+
+export { renderHook } from '@testing-library/react-hooks';
+
+function customRender(ui: React.ReactElement, options = {}) {
+ return render(ui, {
+ // wrap provider(s) here if needed
+ wrapper: ({ children }) => children,
+ ...options,
+ });
+}
+
+export * from '@testing-library/react';
+export { default as userEvent } from '@testing-library/user-event';
+// override render export
+export { customRender as render };
+
+export const sleep = async (timeout = 0) => {
+ return new Promise((resolve) => {
+ setTimeout(resolve, timeout);
+ });
+};
+
+export const WaitApp = async () => {
+ await waitFor(() => {
+ // @ts-ignore
+ expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
+ });
+};
+
+interface RenderHookOptions extends Omit {
+ hook: () => any;
+ props?: any;
+ Wrapper?: FC<{ children: React.ReactNode }>;
+}
+
+export const renderHookWithApp = async (options: RenderHookOptions) => {
+ const { hook: useHook, props, Wrapper = Fragment, ...otherOptions } = options;
+ const { App } = getApp(otherOptions);
+ const WrapperValue: FC<{ children: React.ReactNode }> = ({ children }) => (
+
+ {children}
+
+ );
+
+ const res = renderHook(() => useHook(), { wrapper: WrapperValue, initialProps: props });
+
+ await WaitApp();
+
+ return res;
+};
+
+export const renderApp = async (options: GetAppComponentOptions) => {
+ const App = getAppComponent(options);
+
+ const res = render();
+
+ await WaitApp();
+
+ return res;
+};
+
+export const renderReadPrettyApp = (options: GetAppComponentOptions) => {
+ return renderApp({ ...options, schema: { ...(options.schema || {}), 'x-read-pretty': true } });
+};
diff --git a/packages/core/test/src/web/dataSource2.json b/packages/core/test/src/web/dataSource2.json
new file mode 100644
index 0000000000..fc70600ed6
--- /dev/null
+++ b/packages/core/test/src/web/dataSource2.json
@@ -0,0 +1,139 @@
+{
+ "key": "data-source2",
+ "displayName": "Data Source 2",
+ "status": "loaded",
+ "type": "postgres",
+ "isDBInstance": true,
+ "collections": [
+ {
+ "name": "test",
+ "title": "test",
+ "tableName": "test",
+ "timestamps": false,
+ "autoGenId": false,
+ "filterTargetKey": "id",
+ "fields": [
+ {
+ "name": "id",
+ "type": "integer",
+ "allowNull": false,
+ "primaryKey": false,
+ "unique": false,
+ "autoIncrement": true,
+ "possibleTypes": ["integer", "sort"],
+ "rawType": "INTEGER",
+ "interface": "integer",
+ "uiSchema": {
+ "type": "number",
+ "x-component": "InputNumber",
+ "x-component-props": {
+ "stringMode": true,
+ "step": "1"
+ },
+ "x-validator": "integer",
+ "title": "id"
+ }
+ },
+ {
+ "name": "title",
+ "type": "string",
+ "allowNull": false,
+ "primaryKey": false,
+ "unique": false,
+ "possibleTypes": ["string", "uuid", "nanoid"],
+ "rawType": "CHARACTER VARYING(255)",
+ "interface": "input",
+ "uiSchema": {
+ "x-component": "Input",
+ "x-component-props": {
+ "style": {
+ "width": "100%"
+ }
+ },
+ "title": "title"
+ }
+ },
+ {
+ "name": "content",
+ "type": "text",
+ "allowNull": true,
+ "primaryKey": false,
+ "unique": false,
+ "rawType": "TEXT",
+ "interface": "textarea",
+ "uiSchema": {
+ "type": "string",
+ "x-component": "Input.TextArea",
+ "title": "content"
+ }
+ }
+ ],
+ "introspected": true
+ },
+ {
+ "name": "test2",
+ "title": "test2",
+ "tableName": "test2",
+ "timestamps": false,
+ "autoGenId": false,
+ "filterTargetKey": "id",
+ "fields": [
+ {
+ "name": "id",
+ "type": "integer",
+ "allowNull": false,
+ "primaryKey": true,
+ "unique": false,
+ "autoIncrement": true,
+ "possibleTypes": ["integer", "sort"],
+ "rawType": "INTEGER",
+ "interface": "integer",
+ "uiSchema": {
+ "type": "number",
+ "x-component": "InputNumber",
+ "x-component-props": {
+ "stringMode": true,
+ "step": "1"
+ },
+ "x-validator": "integer",
+ "title": "id"
+ }
+ },
+ {
+ "name": "title",
+ "type": "string",
+ "allowNull": true,
+ "primaryKey": false,
+ "unique": false,
+ "possibleTypes": ["string", "uuid", "nanoid"],
+ "rawType": "CHARACTER VARYING(255)",
+ "interface": "input",
+ "uiSchema": {
+ "x-component": "Input",
+ "x-component-props": {
+ "style": {
+ "width": "100%"
+ }
+ },
+ "title": "title"
+ }
+ },
+ {
+ "name": "content",
+ "type": "text",
+ "allowNull": true,
+ "primaryKey": false,
+ "unique": false,
+ "rawType": "TEXT",
+ "interface": "textarea",
+ "uiSchema": {
+ "type": "string",
+ "x-component": "Input.TextArea",
+ "title": "content"
+ }
+ }
+ ],
+ "introspected": true
+ }
+ ]
+}
diff --git a/packages/core/test/src/web/dataSourceMainCollections.json b/packages/core/test/src/web/dataSourceMainCollections.json
new file mode 100644
index 0000000000..9d8fc5f6b5
--- /dev/null
+++ b/packages/core/test/src/web/dataSourceMainCollections.json
@@ -0,0 +1,223 @@
+[
+ {
+ "key": "h7b9i8khc3q",
+ "name": "users",
+ "inherit": false,
+ "hidden": false,
+ "description": null,
+ "category": [],
+ "namespace": "users.users",
+ "duplicator": {
+ "dumpable": "optional",
+ "with": "rolesUsers"
+ },
+ "sortable": "sort",
+ "model": "UserModel",
+ "createdBy": true,
+ "updatedBy": true,
+ "logging": true,
+ "from": "db2cm",
+ "title": "{{t(\"Users\")}}",
+ "rawTitle": "{{t(\"Users\")}}",
+ "fields": [
+ {
+ "uiSchema": {
+ "type": "number",
+ "title": "{{t(\"ID\")}}",
+ "x-component": "InputNumber",
+ "x-read-pretty": true,
+ "rawTitle": "{{t(\"ID\")}}"
+ },
+ "key": "ffp1f2sula0",
+ "name": "id",
+ "type": "bigInt",
+ "interface": "id",
+ "description": null,
+ "collectionName": "users",
+ "parentKey": null,
+ "reverseKey": null,
+ "autoIncrement": true,
+ "primaryKey": true,
+ "allowNull": false
+ },
+ {
+ "uiSchema": {
+ "type": "string",
+ "title": "{{t(\"Nickname\")}}",
+ "x-component": "Input",
+ "rawTitle": "{{t(\"Nickname\")}}"
+ },
+ "key": "vrv7yjue90g",
+ "name": "nickname",
+ "type": "string",
+ "interface": "input",
+ "description": null,
+ "collectionName": "users",
+ "parentKey": null,
+ "reverseKey": null
+ },
+ {
+ "uiSchema": {
+ "type": "string",
+ "title": "{{t(\"Username\")}}",
+ "x-component": "Input",
+ "x-validator": {
+ "username": true
+ },
+ "required": true,
+ "rawTitle": "{{t(\"Username\")}}"
+ },
+ "key": "2ccs6evyrub",
+ "name": "username",
+ "type": "string",
+ "interface": "input",
+ "description": null,
+ "collectionName": "users",
+ "parentKey": null,
+ "reverseKey": null,
+ "unique": true
+ },
+ {
+ "uiSchema": {
+ "type": "string",
+ "title": "{{t(\"Email\")}}",
+ "x-component": "Input",
+ "x-validator": "email",
+ "required": true,
+ "rawTitle": "{{t(\"Email\")}}"
+ },
+ "key": "rrskwjl5wt1",
+ "name": "email",
+ "type": "string",
+ "interface": "email",
+ "description": null,
+ "collectionName": "users",
+ "parentKey": null,
+ "reverseKey": null,
+ "unique": true
+ },
+ {
+ "key": "t09bauwm0wb",
+ "name": "roles",
+ "type": "belongsToMany",
+ "interface": "m2m",
+ "description": null,
+ "collectionName": "users",
+ "parentKey": null,
+ "reverseKey": null,
+ "target": "roles",
+ "foreignKey": "userId",
+ "otherKey": "roleName",
+ "onDelete": "CASCADE",
+ "sourceKey": "id",
+ "targetKey": "name",
+ "through": "rolesUsers",
+ "uiSchema": {
+ "type": "array",
+ "title": "{{t(\"Roles\")}}",
+ "x-component": "AssociationField",
+ "x-component-props": {
+ "multiple": true,
+ "fieldNames": {
+ "label": "title",
+ "value": "name"
+ }
+ }
+ }
+ },
+ {
+ "key": "1pz0art9mt7",
+ "name": "f_n2fu6hvprct",
+ "type": "string",
+ "interface": "select",
+ "description": null,
+ "collectionName": "t_vwpds9fs4xs",
+ "parentKey": null,
+ "reverseKey": null,
+ "uiSchema": {
+ "enum": [
+ {
+ "value": "test1",
+ "label": "test1"
+ },
+ {
+ "value": "test2",
+ "label": "test2"
+ }
+ ],
+ "type": "string",
+ "x-component": "Select",
+ "title": "test"
+ }
+ }
+ ]
+ },
+ {
+ "key": "pqnenvqrzxr",
+ "name": "roles",
+ "inherit": false,
+ "hidden": false,
+ "description": null,
+ "category": [],
+ "namespace": "acl.acl",
+ "duplicator": {
+ "dumpable": "required",
+ "with": "uiSchemas"
+ },
+ "autoGenId": false,
+ "model": "RoleModel",
+ "filterTargetKey": "name",
+ "sortable": true,
+ "from": "db2cm",
+ "title": "{{t(\"Roles\")}}",
+ "rawTitle": "{{t(\"Roles\")}}",
+ "fields": [
+ {
+ "uiSchema": {
+ "type": "string",
+ "title": "{{t(\"Role UID\")}}",
+ "x-component": "Input",
+ "rawTitle": "{{t(\"Role UID\")}}"
+ },
+ "key": "jbz9m80bxmp",
+ "name": "name",
+ "type": "uid",
+ "interface": "input",
+ "description": null,
+ "collectionName": "roles",
+ "parentKey": null,
+ "reverseKey": null,
+ "prefix": "r_",
+ "primaryKey": true
+ },
+ {
+ "uiSchema": {
+ "type": "string",
+ "title": "{{t(\"Role name\")}}",
+ "x-component": "Input",
+ "rawTitle": "{{t(\"Role name\")}}"
+ },
+ "key": "faywtz4sf3u",
+ "name": "title",
+ "type": "string",
+ "interface": "input",
+ "description": null,
+ "collectionName": "roles",
+ "parentKey": null,
+ "reverseKey": null,
+ "unique": true,
+ "translation": true
+ },
+ {
+ "key": "1enkovm9sye",
+ "name": "description",
+ "type": "string",
+ "interface": null,
+ "description": null,
+ "collectionName": "roles",
+ "parentKey": null,
+ "reverseKey": null
+ }
+ ]
+ }
+]
diff --git a/packages/core/test/src/web/index.tsx b/packages/core/test/src/web/index.tsx
new file mode 100644
index 0000000000..5370347fe2
--- /dev/null
+++ b/packages/core/test/src/web/index.tsx
@@ -0,0 +1,138 @@
+import React, { ComponentType } from 'react';
+import MockAdapter from 'axios-mock-adapter';
+import { AxiosInstance } from 'axios';
+
+// @ts-ignore
+import { Application, ApplicationOptions, DataBlockProvider, LocalDataSource, SchemaComponent } from '@nocobase/client';
+
+import dataSourceMainCollections from './dataSourceMainCollections.json';
+import dataSource2 from './dataSource2.json';
+import usersListData from './usersListData.json';
+
+type URL = string;
+type ResponseData = any;
+
+type MockApis = Record;
+type AppOrOptions = Application | ApplicationOptions;
+
+export const mockApi = (axiosInstance: AxiosInstance, apis: MockApis = {}) => {
+ const mock = new MockAdapter(axiosInstance);
+ Object.keys(apis).forEach((key) => {
+ mock.onAny(key).reply(200, apis[key]);
+ });
+
+ return (apis: MockApis = {}) => {
+ Object.keys(apis).forEach((key) => {
+ mock.onAny(key).reply(200, apis[key]);
+ });
+ };
+};
+
+export const mockAppApi = (app: Application, apis: MockApis = {}) => {
+ const mock = mockApi(app.apiClient.axios, apis);
+ return mock;
+};
+
+export interface GetAppOptions {
+ appOptions?: AppOrOptions;
+ providers?: (ComponentType | [ComponentType, any])[];
+ apis?: MockApis;
+ enableUserListDataBlock?: boolean;
+ enableMultipleDataSource?: boolean;
+}
+
+export const getApp = (options: GetAppOptions) => {
+ const { appOptions, enableUserListDataBlock, providers, apis, enableMultipleDataSource } = options;
+ const app = appOptions instanceof Application ? appOptions : new Application(appOptions);
+ if (providers) {
+ app.addProviders(providers);
+ }
+
+ app.getCollectionManager().addCollections(dataSourceMainCollections as any);
+
+ if (enableUserListDataBlock && !apis['users:list']) {
+ apis['users:list'] = usersListData;
+ }
+
+ if (enableMultipleDataSource) {
+ app.dataSourceManager.addDataSource(LocalDataSource, dataSource2 as any);
+ }
+
+ mockAppApi(app, apis);
+
+ const App = app.getRootComponent();
+ return {
+ App,
+ app,
+ };
+};
+
+export interface GetAppComponentOptions {
+ schema?: any;
+ appOptions?: AppOrOptions;
+ apis?: MockApis;
+ Component?: ComponentType;
+ value?: V;
+ props?: Props;
+ onChange?: (value: V) => void;
+ enableUserListDataBlock?: boolean;
+ enableMultipleDataSource?: boolean;
+}
+
+export const getAppComponent = (options: GetAppComponentOptions) => {
+ const {
+ Component,
+ enableUserListDataBlock,
+ enableMultipleDataSource,
+ value,
+ props,
+ appOptions,
+ apis,
+ onChange,
+ schema: optionsSchema = {},
+ } = options;
+ const schema = {
+ type: 'object',
+ name: 'test',
+ default: value,
+ 'x-component': Component,
+ 'x-component-props': {
+ onChange,
+ ...props,
+ },
+ ...optionsSchema,
+ };
+
+ if (!schema.name) {
+ schema.name = 'test';
+ }
+
+ if (!schema.type) {
+ schema.type = 'void';
+ }
+
+ const TestDemo = () => {
+ if (!enableUserListDataBlock) {
+ return ;
+ }
+ return (
+
+
+
+ );
+ };
+
+ const { App } = getApp({
+ appOptions,
+ apis,
+ providers: [TestDemo],
+ enableMultipleDataSource,
+ enableUserListDataBlock,
+ });
+
+ return App;
+};
+
+export const getReadPrettyAppComponent = (options: GetAppComponentOptions) => {
+ return getAppComponent({ ...options, schema: { ...(options.schema || {}), 'x-read-pretty': true } });
+};
diff --git a/packages/core/test/src/web/usersListData.json b/packages/core/test/src/web/usersListData.json
new file mode 100644
index 0000000000..3fedab4345
--- /dev/null
+++ b/packages/core/test/src/web/usersListData.json
@@ -0,0 +1,30 @@
+{
+ "data": [
+ {
+ "f_o3y6p9gf1gx": null,
+ "createdAt": "2023-03-30T07:53:10.941Z",
+ "updatedAt": "2024-04-12T03:27:45.748Z",
+ "appLang": "zh-CN",
+ "createdById": null,
+ "email": "admin@nocobase.com",
+ "f_2ytvt3phlp2": null,
+ "f_3jl554hv7lt": null,
+ "f_51qityssoq1": null,
+ "f_dybwctlb233": null,
+ "f_hbegrnglpv2": null,
+ "f_ndkyrfvh9il": null,
+ "f_o33xmbd62fj": null,
+ "f_t52vqdtfv4h": null,
+ "f_vak0o8efq4v": [],
+ "id": 1,
+ "nickname": "Super Admin",
+ "phone": null,
+ "systemSettings": {
+ "theme": "compact",
+ "themeId": 1
+ },
+ "updatedById": 1,
+ "username": "nocobase"
+ }
+ ]
+}
diff --git a/packages/core/test/vitest.mjs b/packages/core/test/vitest.mjs
index a9a5e09ce8..5db72435b4 100644
--- a/packages/core/test/vitest.mjs
+++ b/packages/core/test/vitest.mjs
@@ -86,6 +86,7 @@ const defineCommonConfig = () => {
provider: 'istanbul',
include: ['packages/**/src/**/*.{ts,tsx}'],
exclude: [
+ '**/requirejs.ts',
'**/demos/**',
'**/swagger/**',
'**/.dumi/**',
@@ -94,6 +95,7 @@ const defineCommonConfig = () => {
'**/lib/**',
'**/__tests__/**',
'**/e2e/**',
+ '**/__e2e__/**',
'**/client.js',
'**/server.js',
'**/*.d.ts',
@@ -149,12 +151,15 @@ const defineClientConfig = () => {
export const getFilterInclude = (isServer, isCoverage) => {
let filterFileOrDir = process.argv.slice(2).find((arg) => !arg.startsWith('-'));
- if (!filterFileOrDir) return;
+ if (!filterFileOrDir) return {};
const absPath = path.join(process.cwd(), filterFileOrDir);
const isDir = fs.existsSync(absPath) && fs.statSync(absPath).isDirectory();
// 如果是文件,则只测试当前文件
if (!isDir) {
- return [filterFileOrDir];
+ return {
+ isFile: true,
+ include: [filterFileOrDir],
+ };
}
const suffix = isCoverage ? `**/*.{ts,tsx}` : `**/__tests__/**/*.{test,spec}.{ts,tsx}`;
@@ -162,25 +167,32 @@ export const getFilterInclude = (isServer, isCoverage) => {
// 判断是否为包目录,如果不是包目录,则只测试当前目录
const isPackage = fs.existsSync(path.join(absPath, 'package.json'));
if (!isPackage) {
- return [`${filterFileOrDir}/${suffix}`];
+ return {
+ include: [`${filterFileOrDir}/${suffix}`],
+ };
}
// 判断是否为 core 包目录,不分 client 和 server
const isCore = absPath.includes('packages/core');
if (isCore) {
- return [`${filterFileOrDir}/src/${suffix}`];
+ return {
+ include: [`${filterFileOrDir}/src/${suffix}`],
+ };
}
// 插件目录,区分 client 和 server
- return [`${filterFileOrDir}/src/${isServer ? 'server' : 'client'}/${suffix}`];
+ return {
+ include: [`${filterFileOrDir}/src/${isServer ? 'server' : 'client'}/${suffix}`],
+ };
};
export const getReportsDirectory = (isServer) => {
let filterFileOrDir = process.argv.slice(2).find((arg) => !arg.startsWith('-'));
if (!filterFileOrDir) return;
+ const basePath = `./storage/coverage/`;
const isPackage = fs.existsSync(path.join(process.cwd(), filterFileOrDir, 'package.json'));
if (isPackage) {
- let reportsDirectory = `./storage/coverage/${filterFileOrDir.replace('packages/', '')}`;
+ let reportsDirectory = `${basePath}${filterFileOrDir.replace('packages/', '')}`;
const isCore = filterFileOrDir.includes('packages/core');
@@ -189,6 +201,8 @@ export const getReportsDirectory = (isServer) => {
}
return reportsDirectory;
+ } else {
+ return basePath;
}
};
@@ -198,17 +212,30 @@ export const defineConfig = () => {
mergeConfig(defineCommonConfig(), isServer ? defineServerConfig() : defineClientConfig()),
);
+ const { isFile, include: filterInclude } = getFilterInclude(isServer);
+ if (filterInclude) {
+ config.test.include = filterInclude;
+ if (isFile) {
+ // 减少收集的文件
+ config.test.root = path.dirname(filterInclude[0]);
+ config.test.exclude = [];
+ config.test.coverage = {
+ enabled: false,
+ };
+
+ return config;
+ }
+ }
+
const isCoverage = process.argv.includes('--coverage');
if (!isCoverage) {
return config;
}
- const filterInclude = getFilterInclude(isServer);
- if (filterInclude) {
- config.test.include = getFilterInclude(isServer);
+ const { include: coverageInclude } = getFilterInclude(isServer, true);
+ if (coverageInclude) {
+ config.test.coverage.include = coverageInclude;
}
-
- config.test.coverage.include = getFilterInclude(isServer, true);
const reportsDirectory = getReportsDirectory(isServer);
if (reportsDirectory) {
config.test.coverage.reportsDirectory = reportsDirectory;
diff --git a/packages/core/test/web.d.ts b/packages/core/test/web.d.ts
new file mode 100644
index 0000000000..6ee2ab6c74
--- /dev/null
+++ b/packages/core/test/web.d.ts
@@ -0,0 +1 @@
+export * from './lib/web';
diff --git a/packages/core/test/web.js b/packages/core/test/web.js
new file mode 100644
index 0000000000..29a135f81c
--- /dev/null
+++ b/packages/core/test/web.js
@@ -0,0 +1 @@
+module.exports = require('./lib/web');
diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockProvider.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockProvider.tsx
index 76f83119da..22c891b18e 100644
--- a/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockProvider.tsx
+++ b/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockProvider.tsx
@@ -144,11 +144,11 @@ export const useKanbanBlockProps = () => {
useEffect(() => {
const data = toColumns(ctx.groupField, ctx?.service?.data?.data, primaryKey);
- if (isEqual(field.value, data)) {
+ if (isEqual(field.value, data) && dataSource === field.value) {
return;
}
field.value = data;
- setDataSource(data);
+ setDataSource(field.value);
}, [ctx?.service?.loading]);
const disableCardDrag = useDisableCardDrag();
diff --git a/yarn.lock b/yarn.lock
index 06b5023a0a..a5e4b4c923 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3647,14 +3647,6 @@
bessel "^1.0.2"
jstat "^1.9.6"
-"@formulajs/formulajs@^4.2.0":
- version "4.3.4"
- resolved "https://registry.npmmirror.com/@formulajs/formulajs/-/formulajs-4.3.4.tgz#b2a48ab9dd8ad60901ae6b3ca76cfc5416bd27e0"
- integrity sha512-iETOopDEUmKaeEUofUfazebhihErh+klyRQbEemDnnBj0hcwNu3xF0oAPHVL0vU/E01rVjy/ZC+1UBMOVkXrvQ==
- dependencies:
- bessel "^1.0.2"
- jstat "^1.9.6"
-
"@gar/promisify@^1.0.1":
version "1.1.3"
resolved "https://registry.npmmirror.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
@@ -4873,6 +4865,15 @@
dependencies:
"@opentelemetry/semantic-conventions" "1.19.0"
+"@opentelemetry/exporter-prometheus@^0.46.0":
+ version "0.46.0"
+ resolved "https://registry.npmmirror.com/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.46.0.tgz#c411a1e8a5266f9f3ddc44a088a538c3c1ee4830"
+ integrity sha512-AXcoCHG31K2PLGizlJJWcfQqZsGfUZkT7ik6C8VJu7U2Cenk0Xhvd3rO+vVNSSjP1+LHkP4MQtqEXpIZttw5cw==
+ dependencies:
+ "@opentelemetry/core" "1.19.0"
+ "@opentelemetry/resources" "1.19.0"
+ "@opentelemetry/sdk-metrics" "1.19.0"
+
"@opentelemetry/instrumentation@^0.46.0":
version "0.46.0"
resolved "https://registry.npmmirror.com/@opentelemetry/instrumentation/-/instrumentation-0.46.0.tgz#a8a252306f82e2eace489312798592a14eb9830e"
@@ -4906,7 +4907,7 @@
"@opentelemetry/core" "1.19.0"
"@opentelemetry/semantic-conventions" "1.19.0"
-"@opentelemetry/sdk-metrics@^1.19.0":
+"@opentelemetry/sdk-metrics@1.19.0", "@opentelemetry/sdk-metrics@^1.19.0":
version "1.19.0"
resolved "https://registry.npmmirror.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.19.0.tgz#fe8029af29402563eb8dba75a85fc02006ea92c4"
integrity sha512-FiMii40zr0Fmys4F1i8gmuCvbinBnBsDeGBr4FQemOf0iPCLytYQm5AZJ/nn4xSc71IgKBQwTFQRAGJI7JvZ4Q==
@@ -6471,6 +6472,11 @@
resolved "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.13.tgz#e6e77ea9ecf36564980a861e24e62a095988775e"
integrity sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ==
+"@types/geojson@^7946.0.14":
+ version "7946.0.14"
+ resolved "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613"
+ integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==
+
"@types/glob-stream@*":
version "8.0.2"
resolved "https://registry.npmmirror.com/@types/glob-stream/-/glob-stream-8.0.2.tgz#56234435cd20f9b7b08c993be9267d661f9b914d"
@@ -6784,6 +6790,13 @@
resolved "https://registry.npmmirror.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190"
integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==
+"@types/node@^20.11.17":
+ version "20.12.2"
+ resolved "https://registry.npmmirror.com/@types/node/-/node-20.12.2.tgz#9facdd11102f38b21b4ebedd9d7999663343d72e"
+ integrity sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==
+ dependencies:
+ undici-types "~5.26.4"
+
"@types/nodemailer@6.4.4":
version "6.4.4"
resolved "https://registry.npmmirror.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b"
@@ -6818,6 +6831,15 @@
dependencies:
"@types/express" "*"
+"@types/pg@^8.10.9":
+ version "8.11.4"
+ resolved "https://registry.npmmirror.com/@types/pg/-/pg-8.11.4.tgz#befbe4dc0c14aa31acf86bbce83641b73ff838a7"
+ integrity sha512-yw3Bwbda6vO+NvI1Ue/YKOwtl31AYvvd/e73O3V4ZkNzuGpTDndLSyc0dQRB2xrQqDePd20pEGIfqSp/GH3pRw==
+ dependencies:
+ "@types/node" "*"
+ pg-protocol "*"
+ pg-types "^4.0.1"
+
"@types/picomatch@*":
version "2.3.3"
resolved "https://registry.npmmirror.com/@types/picomatch/-/picomatch-2.3.3.tgz#be60498568c19e989e43fb39aa84be1ed3655e92"
@@ -8774,7 +8796,7 @@ aws4@^1.8.0:
resolved "https://registry.npmmirror.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3"
integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==
-axios-mock-adapter@^1.20.0:
+axios-mock-adapter@1.22.0, axios-mock-adapter@^1.20.0:
version "1.22.0"
resolved "https://registry.npmmirror.com/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz#0f3e6be0fc9b55baab06f2d49c0b71157e7c053d"
integrity sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw==
@@ -8998,7 +9020,7 @@ bessel@^1.0.2:
resolved "https://registry.npmmirror.com/bessel/-/bessel-1.0.2.tgz#828812291e0b62e94959cdea43fac186e8a7202d"
integrity sha512-Al3nHGQGqDYqqinXhQzmwmcRToe/3WyBv4N8aZc5Pef8xw2neZlR9VPi84Sa23JtgWcucu18HxVZrnI0fn2etw==
-big-integer@^1.6.44:
+big-integer@^1.6.44, big-integer@^1.6.48:
version "1.6.52"
resolved "https://registry.npmmirror.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85"
integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==
@@ -9008,7 +9030,7 @@ big.js@^5.2.2:
resolved "https://registry.npmmirror.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
-bignumber.js@^9.0.0:
+bignumber.js@^9.0.0, bignumber.js@^9.1.2:
version "9.1.2"
resolved "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c"
integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==
@@ -11829,7 +11851,7 @@ delegates@^1.0.0:
resolved "https://registry.npmmirror.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
-denque@^2.0.1:
+denque@^2.0.1, denque@^2.1.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
@@ -16424,6 +16446,11 @@ jest-worker@^29.7.0:
merge-stream "^2.0.0"
supports-color "^8.0.0"
+jmespath@^0.16.0:
+ version "0.16.0"
+ resolved "https://registry.npmmirror.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076"
+ integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==
+
jose@^4.15.1:
version "4.15.4"
resolved "https://registry.npmmirror.com/jose/-/jose-4.15.4.tgz#02a9a763803e3872cf55f29ecef0dfdcc218cc03"
@@ -16618,6 +16645,11 @@ json5@^2.1.2, json5@^2.2.2, json5@^2.2.3:
resolved "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+jsonata@^2.0.3:
+ version "2.0.4"
+ resolved "https://registry.npmmirror.com/jsonata/-/jsonata-2.0.4.tgz#d8575d4c7695a2086162a2baebce0f0ee4e84fdb"
+ integrity sha512-vfavX4/G/yrYxE+UrmT/oUJ3ph7KqUrb0R7b0LVRcntQwxw+Z5kA1pNUIQzX5hF04Oe1eKxyoIPsmXtc2LgJTQ==
+
jsonc-parser@^3.2.0:
version "3.2.0"
resolved "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76"
@@ -16651,6 +16683,11 @@ jsonparse@^1.2.0, jsonparse@^1.3.1:
resolved "https://registry.npmmirror.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==
+jsonpath-plus@^7.2.0:
+ version "7.2.0"
+ resolved "https://registry.npmmirror.com/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz#7ad94e147b3ed42f7939c315d2b9ce490c5a3899"
+ integrity sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==
+
jsonwebtoken@^8.5.1:
version "8.5.1"
resolved "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
@@ -17422,7 +17459,7 @@ long@^4.0.0:
resolved "https://registry.npmmirror.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
-long@^5.2.0:
+long@^5.2.0, long@^5.2.1:
version "5.2.3"
resolved "https://registry.npmmirror.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"
integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
@@ -17468,7 +17505,7 @@ lowercase-keys@^2.0.0:
resolved "https://registry.npmmirror.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
-lru-cache@8.0.5:
+lru-cache@8.0.5, lru-cache@^8.0.0:
version "8.0.5"
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-8.0.5.tgz#983fe337f3e176667f8e567cfcce7cb064ea214e"
integrity sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==
@@ -17478,6 +17515,11 @@ lru-cache@^10.0.2:
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484"
integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==
+lru-cache@^10.2.0:
+ version "10.2.0"
+ resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3"
+ integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==
+
lru-cache@^4.0.1, lru-cache@^4.1.1:
version "4.1.5"
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
@@ -17689,6 +17731,17 @@ mariadb@^2.5.6:
moment-timezone "^0.5.34"
please-upgrade-node "^3.2.0"
+mariadb@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.npmmirror.com/mariadb/-/mariadb-3.3.0.tgz#e9c844965d616a1842fa05221206e8678842c09e"
+ integrity sha512-sAL4bJgbfCAtXcE8bXI+NAMzVaPNkIU8hRZUXYfgNFoWB9U57G3XQiMeCx/A6IrS6y7kGwBLylrwgsZQ8kUYlw==
+ dependencies:
+ "@types/geojson" "^7946.0.14"
+ "@types/node" "^20.11.17"
+ denque "^2.1.0"
+ iconv-lite "^0.6.3"
+ lru-cache "^10.2.0"
+
markdown-it-highlightjs@3.3.1:
version "3.3.1"
resolved "https://registry.npmmirror.com/markdown-it-highlightjs/-/markdown-it-highlightjs-3.3.1.tgz#38403610487292b8a1ae2d1acc7bb66e4ede6be8"
@@ -18858,6 +18911,20 @@ mysql2@^2.3.3:
seq-queue "^0.0.5"
sqlstring "^2.3.2"
+mysql2@^3.9.1:
+ version "3.9.3"
+ resolved "https://registry.npmmirror.com/mysql2/-/mysql2-3.9.3.tgz#72a5e0c90d78ec2d8f9846e83727067c0cc8c25e"
+ integrity sha512-+ZaoF0llESUy7BffccHG+urErHcWPZ/WuzYAA9TEeLaDYyke3/3D+VQDzK9xzRnXpd0eMtRf0WNOeo4Q1Baung==
+ dependencies:
+ denque "^2.1.0"
+ generate-function "^2.3.1"
+ iconv-lite "^0.6.3"
+ long "^5.2.1"
+ lru-cache "^8.0.0"
+ named-placeholders "^1.1.3"
+ seq-queue "^0.0.5"
+ sqlstring "^2.3.2"
+
mz@^2.7.0:
version "2.7.0"
resolved "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
@@ -18867,7 +18934,7 @@ mz@^2.7.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
-named-placeholders@^1.1.2:
+named-placeholders@^1.1.2, named-placeholders@^1.1.3:
version "1.1.3"
resolved "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351"
integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==
@@ -19099,6 +19166,13 @@ node-releases@^2.0.14:
resolved "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==
+node-sql-parser@^4.11.0:
+ version "4.18.0"
+ resolved "https://registry.npmmirror.com/node-sql-parser/-/node-sql-parser-4.18.0.tgz#516b6e633c55c5abbba1ca588ab372db81ae9318"
+ integrity sha512-2YEOR5qlI1zUFbGMLKNfsrR5JUvFg9LxIRVE+xJe962pfVLH0rnItqLzv96XVs1Y1UIR8FxsXAuvX/lYAWZ2BQ==
+ dependencies:
+ big-integer "^1.6.48"
+
node-xlsx@^0.16.1:
version "0.16.2"
resolved "https://registry.npmmirror.com/node-xlsx/-/node-xlsx-0.16.2.tgz#40f580187eae0e032cac96e958e97cb6ceca09f6"
@@ -19562,7 +19636,7 @@ object.values@^1.1.6, object.values@^1.1.7:
define-properties "^1.2.0"
es-abstract "^1.22.1"
-obuf@^1.0.0, obuf@^1.1.2:
+obuf@^1.0.0, obuf@^1.1.2, obuf@~1.1.2:
version "1.1.2"
resolved "https://registry.npmmirror.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
@@ -20391,11 +20465,21 @@ pg-int8@1.0.1:
resolved "https://registry.npmmirror.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==
+pg-numeric@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmmirror.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a"
+ integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==
+
pg-pool@^3.6.1:
version "3.6.1"
resolved "https://registry.npmmirror.com/pg-pool/-/pg-pool-3.6.1.tgz#5a902eda79a8d7e3c928b77abf776b3cb7d351f7"
integrity sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==
+pg-protocol@*:
+ version "1.6.1"
+ resolved "https://registry.npmmirror.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3"
+ integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==
+
pg-protocol@^1.6.0:
version "1.6.0"
resolved "https://registry.npmmirror.com/pg-protocol/-/pg-protocol-1.6.0.tgz#4c91613c0315349363af2084608db843502f8833"
@@ -20412,6 +20496,19 @@ pg-types@^2.1.0:
postgres-date "~1.0.4"
postgres-interval "^1.1.0"
+pg-types@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.npmmirror.com/pg-types/-/pg-types-4.0.2.tgz#399209a57c326f162461faa870145bb0f918b76d"
+ integrity sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==
+ dependencies:
+ pg-int8 "1.0.1"
+ pg-numeric "1.0.2"
+ postgres-array "~3.0.1"
+ postgres-bytea "~3.0.0"
+ postgres-date "~2.1.0"
+ postgres-interval "^3.0.0"
+ postgres-range "^1.1.1"
+
pg@^8.11.3, pg@^8.7.3:
version "8.11.3"
resolved "https://registry.npmmirror.com/pg/-/pg-8.11.3.tgz#d7db6e3fe268fcedd65b8e4599cda0b8b4bf76cb"
@@ -21010,16 +21107,33 @@ postgres-array@~2.0.0:
resolved "https://registry.npmmirror.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e"
integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==
+postgres-array@~3.0.1:
+ version "3.0.2"
+ resolved "https://registry.npmmirror.com/postgres-array/-/postgres-array-3.0.2.tgz#68d6182cb0f7f152a7e60dc6a6889ed74b0a5f98"
+ integrity sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==
+
postgres-bytea@~1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35"
integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==
+postgres-bytea@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmmirror.com/postgres-bytea/-/postgres-bytea-3.0.0.tgz#9048dc461ac7ba70a6a42d109221619ecd1cb089"
+ integrity sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==
+ dependencies:
+ obuf "~1.1.2"
+
postgres-date@~1.0.4:
version "1.0.7"
resolved "https://registry.npmmirror.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8"
integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==
+postgres-date@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmmirror.com/postgres-date/-/postgres-date-2.1.0.tgz#b85d3c1fb6fb3c6c8db1e9942a13a3bf625189d0"
+ integrity sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==
+
postgres-interval@^1.1.0:
version "1.2.0"
resolved "https://registry.npmmirror.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695"
@@ -21027,6 +21141,16 @@ postgres-interval@^1.1.0:
dependencies:
xtend "^4.0.0"
+postgres-interval@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmmirror.com/postgres-interval/-/postgres-interval-3.0.0.tgz#baf7a8b3ebab19b7f38f07566c7aab0962f0c86a"
+ integrity sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==
+
+postgres-range@^1.1.1:
+ version "1.1.4"
+ resolved "https://registry.npmmirror.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863"
+ integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==
+
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -23321,6 +23445,28 @@ sequelize@^6.26.0:
validator "^13.9.0"
wkx "^0.5.0"
+sequelize@^6.35.0:
+ version "6.37.2"
+ resolved "https://registry.npmmirror.com/sequelize/-/sequelize-6.37.2.tgz#f98052f81c40c26ba85382fcb35e7346308542f4"
+ integrity sha512-bnb7swGANONXCTrVyebpOOZssLwQrVkYX2tcC6qOIvH+P+OhsoMBi7c3GXI5bC+Z4b4tOl+kQy6yeqLCZ1YQAQ==
+ dependencies:
+ "@types/debug" "^4.1.8"
+ "@types/validator" "^13.7.17"
+ debug "^4.3.4"
+ dottie "^2.0.6"
+ inflection "^1.13.4"
+ lodash "^4.17.21"
+ moment "^2.29.4"
+ moment-timezone "^0.5.43"
+ pg-connection-string "^2.6.1"
+ retry-as-promised "^7.0.4"
+ semver "^7.5.4"
+ sequelize-pool "^7.1.0"
+ toposort-class "^1.0.1"
+ uuid "^8.3.2"
+ validator "^13.9.0"
+ wkx "^0.5.0"
+
serve-handler@6.1.3:
version "6.1.3"
resolved "https://registry.npmmirror.com/serve-handler/-/serve-handler-6.1.3.tgz#1bf8c5ae138712af55c758477533b9117f6435e8"