mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 05:29:26 +08:00
parent
b65ee6a602
commit
229e5d1a40
26
.vscode/launch.json
vendored
26
.vscode/launch.json
vendored
@ -21,6 +21,19 @@
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"type": "node"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Tests watch mode",
|
||||
"runtimeExecutable": "yarn",
|
||||
"runtimeArgs": ["run", "test", "-w", "${file}"],
|
||||
"skipFiles": ["<node_internals>/**", "**/node_modules/**", "**/dist/**", "**/lib/**", "**/es/**"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"windows": {
|
||||
"runtimeArgs": ["run", "test", "${relativeFile}"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
@ -34,6 +47,19 @@
|
||||
"runtimeArgs": ["run", "test", "${relativeFile}"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug E2E Tests UI",
|
||||
"runtimeExecutable": "yarn",
|
||||
"runtimeArgs": ["e2e", "test", "${file}", "--ui"],
|
||||
"skipFiles": ["<node_internals>/**", "**/node_modules/**", "**/dist/**", "**/lib/**", "**/es/**"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"windows": {
|
||||
"runtimeArgs": ["e2e", "test", "${fileBasename}", "--ui"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
|
@ -217,6 +217,7 @@ exports.genTsConfigPaths = function genTsConfigPaths() {
|
||||
if (packageJsonName === '@nocobase/test') {
|
||||
paths[`${packageJsonName}/server`] = [`${relativePath}/src/server`];
|
||||
paths[`${packageJsonName}/e2e`] = [`${relativePath}/src/e2e`];
|
||||
paths[`${packageJsonName}/web`] = [`${relativePath}/src/web`];
|
||||
}
|
||||
if (packageJsonName === '@nocobase/plugin-workflow-test') {
|
||||
paths[`${packageJsonName}/e2e`] = [`${relativePath}/src/e2e`];
|
||||
|
@ -1,3 +1,6 @@
|
||||
import path from 'path';
|
||||
import glob from 'glob';
|
||||
import _ from 'lodash'
|
||||
import { getUmiConfig } from '@nocobase/devtools/umiConfig';
|
||||
import { defineConfig } from 'dumi';
|
||||
import { defineThemeConfig } from 'dumi-theme-nocobase';
|
||||
@ -8,6 +11,19 @@ const lang = process.env.DOC_LANG;
|
||||
|
||||
console.log('process.env.DOC_LANG', lang);
|
||||
|
||||
const componentsDir = 'src/schema-component/antd';
|
||||
|
||||
function getComponentsMenu() {
|
||||
const cwd = path.join(__dirname, componentsDir);
|
||||
const ignores = ['table/index.md', 'form/index.md']; // 老版本,不需要展示
|
||||
const files = glob.sync('*/index.md', { cwd, ignore: ignores });
|
||||
|
||||
return files.map((file) => ({
|
||||
title: _.upperFirst(_.camelCase(file.replace('/index.md', ''))),
|
||||
link: `/components/${file.replace('/index.md', '')}`,
|
||||
}));
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
hash: true,
|
||||
alias: {
|
||||
@ -21,7 +37,10 @@ export default defineConfig({
|
||||
cacheDirectoryPath: `node_modules/.docs-client-${lang}-cache`,
|
||||
outputPath: `./dist/${lang}`,
|
||||
resolve: {
|
||||
docDirs: [`./docs/${lang}`]
|
||||
docDirs: [`./docs/${lang}`],
|
||||
atomDirs: [
|
||||
{ type: 'component', dir: componentsDir },
|
||||
],
|
||||
},
|
||||
locales: [
|
||||
{ id: 'en-US', name: 'English' },
|
||||
@ -38,6 +57,10 @@ export default defineConfig({
|
||||
title: 'API',
|
||||
link: '/core/application/application',
|
||||
},
|
||||
{
|
||||
title: 'Components',
|
||||
link: '/components/action',
|
||||
}
|
||||
// {
|
||||
// title: 'UI Schema',
|
||||
// link: '/ui-schema',
|
||||
@ -202,6 +225,7 @@ export default defineConfig({
|
||||
]
|
||||
}
|
||||
],
|
||||
'/components': getComponentsMenu(),
|
||||
// '/ui-schema': [
|
||||
// {
|
||||
// title: 'Overview',
|
||||
|
212
packages/core/client/docs/en-US/Demo.tsx
Normal file
212
packages/core/client/docs/en-US/Demo.tsx
Normal file
@ -0,0 +1,212 @@
|
||||
import {
|
||||
getApp,
|
||||
getAppComponent,
|
||||
getAppComponentWithSchemaSettings,
|
||||
getReadPrettyAppComponent,
|
||||
withSchema,
|
||||
} from '@nocobase/test/web';
|
||||
import {
|
||||
ACLMenuItemProvider,
|
||||
AdminLayout,
|
||||
BlockSchemaComponentPlugin,
|
||||
CurrentUserProvider,
|
||||
DocumentTitleProvider,
|
||||
EditComponent,
|
||||
EditDefaultValue,
|
||||
EditOperator,
|
||||
EditPattern,
|
||||
EditTitle,
|
||||
EditTitleField,
|
||||
EditValidationRules,
|
||||
FilterFormBlockProvider,
|
||||
FixedBlock,
|
||||
Form,
|
||||
FormBlockProvider,
|
||||
FormItem,
|
||||
FormV2,
|
||||
Grid,
|
||||
IconPicker,
|
||||
Input,
|
||||
InternalAdminLayout,
|
||||
NanoIDInput,
|
||||
Page,
|
||||
RouteSchemaComponent,
|
||||
SchemaInitializerPlugin,
|
||||
TableBlockProvider,
|
||||
TableV2,
|
||||
VariablesProvider,
|
||||
fieldSettingsFormItem,
|
||||
tableActionColumnInitializers,
|
||||
tableActionInitializers,
|
||||
tableColumnInitializers,
|
||||
useTableBlockDecoratorProps,
|
||||
} from '@nocobase/client';
|
||||
import { observer } from '@formily/reactive-react';
|
||||
import React, { ComponentType } from 'react';
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import axios from 'axios';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
const App = getAppComponent({
|
||||
designable: true,
|
||||
schema: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
'x-acl-action': 'users:create',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
dataSource: 'main',
|
||||
collection: 'users',
|
||||
},
|
||||
'x-component': 'div',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
'0s3tm262rre': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-use-component-props': 'useCreateFormBlockProps',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
grid: {
|
||||
'x-uid': 'h38s9pa4ik5',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-linkage-rules': [
|
||||
{
|
||||
condition: {
|
||||
$and: [
|
||||
{
|
||||
username: {
|
||||
$eq: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
targetFields: ['nickname'],
|
||||
operator: 'none',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
udpf3e45i3d: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
hhc0bsk1roi: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
username: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'users.username',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-uid': '71x74r4t4g0',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'ophjdttgmo5',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'ta1vq3qr1sd',
|
||||
'x-async': false,
|
||||
'x-index': 3,
|
||||
},
|
||||
row_rpkxgfonud3: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-index': 4,
|
||||
properties: {
|
||||
mmo2k17b0q1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
properties: {
|
||||
nickname: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-uid': 'bcowga6nzzy',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'l1awt5at07z',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'y1tdyhcwhhi',
|
||||
'x-async': false,
|
||||
},
|
||||
},
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
'0m1r08p58e9': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
layout: 'one-column',
|
||||
style: {
|
||||
marginTop: 24,
|
||||
},
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-uid': 't4gxf0xxaxc',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid': 'yk2fivh9hgb',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'aqbi3avt3kb',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
appOptions: {
|
||||
plugins: [BlockSchemaComponentPlugin],
|
||||
providers: [VariablesProvider],
|
||||
},
|
||||
});
|
||||
|
||||
export default App;
|
2
packages/core/client/docs/en-US/test.md
Normal file
2
packages/core/client/docs/en-US/test.md
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
<code src="./Demo.tsx"></code>
|
183
packages/core/client/docs/zh-CN/Demo.tsx
Normal file
183
packages/core/client/docs/zh-CN/Demo.tsx
Normal file
@ -0,0 +1,183 @@
|
||||
import {
|
||||
getApp,
|
||||
getAppComponent,
|
||||
getAppComponentWithSchemaSettings,
|
||||
getReadPrettyAppComponent,
|
||||
withSchema,
|
||||
} from '@nocobase/test/web';
|
||||
import {
|
||||
ACLMenuItemProvider,
|
||||
AdminLayout,
|
||||
BlockSchemaComponentPlugin,
|
||||
CurrentUserProvider,
|
||||
DocumentTitleProvider,
|
||||
EditComponent,
|
||||
EditDefaultValue,
|
||||
EditOperator,
|
||||
EditPattern,
|
||||
EditTitle,
|
||||
EditTitleField,
|
||||
EditValidationRules,
|
||||
FilterFormBlockProvider,
|
||||
FixedBlock,
|
||||
Form,
|
||||
FormBlockProvider,
|
||||
FormItem,
|
||||
FormV2,
|
||||
Grid,
|
||||
IconPicker,
|
||||
Input,
|
||||
InternalAdminLayout,
|
||||
NanoIDInput,
|
||||
Page,
|
||||
RouteSchemaComponent,
|
||||
SchemaInitializerPlugin,
|
||||
TableBlockProvider,
|
||||
TableV2,
|
||||
VariablesProvider,
|
||||
fieldSettingsFormItem,
|
||||
tableActionColumnInitializers,
|
||||
tableActionInitializers,
|
||||
tableColumnInitializers,
|
||||
useTableBlockDecoratorProps,
|
||||
} from '@nocobase/client';
|
||||
import { observer } from '@formily/reactive-react';
|
||||
import React, { ComponentType } from 'react';
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import axios from 'axios';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
const FormBlockProviderWithSchema = withSchema(FormBlockProvider);
|
||||
|
||||
const App = getAppComponent({
|
||||
designable: true,
|
||||
schema: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
'x-acl-action': 'users:create',
|
||||
'x-decorator': 'FormBlockProviderWithSchema',
|
||||
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
dataSource: 'main',
|
||||
collection: 'users',
|
||||
},
|
||||
'x-toolbar': 'BlockSchemaToolbar',
|
||||
'x-settings': 'blockSettings:createForm',
|
||||
'x-component': 'CardItem',
|
||||
properties: {
|
||||
grid: {
|
||||
'x-uid': 'h38s9pa4ik5',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
udpf3e45i3d: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
hhc0bsk1roi: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
username: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'users.username',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-uid': '71x74r4t4g0',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'ophjdttgmo5',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'ta1vq3qr1sd',
|
||||
'x-async': false,
|
||||
'x-index': 3,
|
||||
},
|
||||
row_rpkxgfonud3: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-index': 4,
|
||||
properties: {
|
||||
mmo2k17b0q1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
properties: {
|
||||
nickname: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-uid': 'bcowga6nzzy',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'l1awt5at07z',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'y1tdyhcwhhi',
|
||||
'x-async': false,
|
||||
},
|
||||
},
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
'0m1r08p58e9': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
layout: 'one-column',
|
||||
style: {
|
||||
marginTop: 24,
|
||||
},
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-uid': 't4gxf0xxaxc',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
plugins: [BlockSchemaComponentPlugin],
|
||||
providers: [VariablesProvider],
|
||||
components: {
|
||||
FormBlockProviderWithSchema,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default App;
|
2
packages/core/client/docs/zh-CN/test.md
Normal file
2
packages/core/client/docs/zh-CN/test.md
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
<code src="./Demo.tsx"></code>
|
@ -198,8 +198,9 @@ export function useACLRoleContext() {
|
||||
|
||||
export const ACLCollectionProvider = (props) => {
|
||||
const { allowAll, parseAction } = useACLRoleContext();
|
||||
const app = useApp();
|
||||
const schema = useFieldSchema();
|
||||
if (allowAll) {
|
||||
if (allowAll || app.disableAcl) {
|
||||
return props.children;
|
||||
}
|
||||
let actionPath = schema?.['x-acl-action'] || props.actionPath;
|
||||
|
@ -60,6 +60,7 @@ export interface ApplicationOptions {
|
||||
loadRemotePlugins?: boolean;
|
||||
devDynamicImport?: DevDynamicImport;
|
||||
dataSourceManager?: DataSourceManagerOptions;
|
||||
disableAcl?: boolean;
|
||||
}
|
||||
|
||||
export class Application {
|
||||
@ -93,6 +94,9 @@ export class Application {
|
||||
get pm() {
|
||||
return this.pluginManager;
|
||||
}
|
||||
get disableAcl() {
|
||||
return this.options.disableAcl;
|
||||
}
|
||||
|
||||
constructor(protected options: ApplicationOptions = {}) {
|
||||
this.initRequireJs();
|
||||
|
@ -24,6 +24,7 @@ export interface MemoryRouterOptions extends Omit<MemoryRouterProps, 'children'>
|
||||
}
|
||||
export type RouterOptions = (HashRouterOptions | BrowserRouterOptions | MemoryRouterOptions) & {
|
||||
renderComponent?: RenderComponentType;
|
||||
routes?: Record<string, RouteType>;
|
||||
};
|
||||
export type ComponentTypeAndString<T = any> = ComponentType<T> | string;
|
||||
export interface RouteType extends Omit<RouteObject, 'children' | 'Component'> {
|
||||
@ -39,6 +40,7 @@ export class RouterManager {
|
||||
constructor(options: RouterOptions = {}, app: Application) {
|
||||
this.options = options;
|
||||
this.app = app;
|
||||
this.routes = options.routes || {};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* istanbul ignore file */
|
||||
/* istanbul ignore file -- @preserve */
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
|
@ -82,7 +82,11 @@ export class CollectionPlugin extends Plugin {
|
||||
this.addCollectionTemplates();
|
||||
this.addFieldInterfaces();
|
||||
this.addFieldInterfaceGroups();
|
||||
this.addMainDataSource();
|
||||
}
|
||||
|
||||
addMainDataSource() {
|
||||
if (this.options?.config?.enableRemoteDataSource === false) return;
|
||||
this.dataSourceManager.addDataSource(MainDataSource, {
|
||||
key: DEFAULT_DATA_SOURCE_KEY,
|
||||
displayName: DEFAULT_DATA_SOURCE_TITLE,
|
||||
|
@ -28,10 +28,12 @@ export function SelectWithTitle({ title, defaultValue, onChange, options, fieldN
|
||||
{title}
|
||||
<Select
|
||||
open={open}
|
||||
data-testid={`select-${title}`}
|
||||
popupMatchSelectWidth={false}
|
||||
bordered={false}
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
popupClassName={`select-popup-${title.replaceAll(' ', '-')}`}
|
||||
fieldNames={fieldNames}
|
||||
options={options}
|
||||
style={{ textAlign: 'right', minWidth: 100 }}
|
||||
|
@ -20,7 +20,7 @@ export const DataSourceProvider: FC<DataSourceProviderProps> = ({ children, data
|
||||
const { t } = useTranslation();
|
||||
const { refresh } = useSchemaComponentContext();
|
||||
const [_, setRandom] = React.useState(0);
|
||||
const dataSourceValue = dataSourceManager.getDataSource(dataSource);
|
||||
const dataSourceValue = dataSourceManager?.getDataSource(dataSource);
|
||||
|
||||
if (!dataSourceValue) {
|
||||
return <CollectionDeletedPlaceholder type="Data Source" name={dataSource} />;
|
||||
|
@ -0,0 +1,562 @@
|
||||
import {
|
||||
screen,
|
||||
checkSettings,
|
||||
renderSingleSettings,
|
||||
waitFor,
|
||||
userEvent,
|
||||
renderReadPrettySingleSettings,
|
||||
renderSettings,
|
||||
renderReadPrettySettings,
|
||||
checkFieldTitle,
|
||||
} from '@nocobase/test/client';
|
||||
import { FilterFormBlockProvider, FormBlockProvider, FormItem, fieldSettingsFormItem } from '@nocobase/client';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { observer } from '@formily/reactive-react';
|
||||
import React from 'react';
|
||||
|
||||
describe('FieldSettingsFormItem', () => {
|
||||
function commonFieldOptions(isFilterForm?: boolean) {
|
||||
return {
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-decorator': isFilterForm ? 'FilterFormBlockProvider' : 'FormBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
},
|
||||
properties: {
|
||||
nickname: {
|
||||
type: 'string',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
'x-settings': 'fieldSettings:FormItem',
|
||||
},
|
||||
},
|
||||
},
|
||||
schemaSettings: fieldSettingsFormItem,
|
||||
appOptions: {
|
||||
components: {
|
||||
FilterFormBlockProvider,
|
||||
FormBlockProvider,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function associationFieldOptions(showSchema = false) {
|
||||
const FormItemWithSchema = observer(({ children }) => {
|
||||
const schema = useFieldSchema();
|
||||
return (
|
||||
<>
|
||||
<div>mode: {schema?.['x-component-props']?.['mode']}</div>
|
||||
<FormItem>{children}</FormItem>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
designable: true,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
},
|
||||
properties: {
|
||||
roles: {
|
||||
type: 'string',
|
||||
'x-collection-field': 'users.roles',
|
||||
'x-decorator': showSchema ? 'FormItemWithSchema' : 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
'x-settings': 'fieldSettings:FormItem',
|
||||
},
|
||||
},
|
||||
},
|
||||
schemaSettings: fieldSettingsFormItem,
|
||||
appOptions: {
|
||||
components: {
|
||||
FormItemWithSchema,
|
||||
FormBlockProvider,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('menu list', () => {
|
||||
describe('edit mode', () => {
|
||||
it('common field', async () => {
|
||||
await renderSettings(commonFieldOptions());
|
||||
|
||||
await checkSettings(
|
||||
[
|
||||
{
|
||||
title: 'Edit field title',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Display title',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
title: 'Edit description',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Edit tooltip',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Required',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
title: 'Set default value',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Pattern',
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
title: 'Set validation rules',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Delete',
|
||||
type: 'delete',
|
||||
},
|
||||
],
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('association field', async () => {
|
||||
await renderSettings(associationFieldOptions());
|
||||
|
||||
await checkSettings(
|
||||
[
|
||||
{
|
||||
title: 'Edit field title',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Display title',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
title: 'Edit description',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Edit tooltip',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Required',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
title: 'Set default value',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Pattern',
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
title: 'Field component',
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
title: 'Set the data scope',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Set default sorting rules',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Quick create',
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
title: 'Allow multiple',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
title: 'Title field',
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
title: 'Delete',
|
||||
type: 'delete',
|
||||
},
|
||||
],
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('read pretty mode', () => {
|
||||
it('common field', async () => {
|
||||
await renderReadPrettySettings(commonFieldOptions());
|
||||
|
||||
await checkSettings(
|
||||
[
|
||||
{
|
||||
title: 'Edit field title',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Display title',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
title: 'Edit description',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Edit tooltip',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Set default value',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Pattern',
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
title: 'Set validation rules',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Delete',
|
||||
type: 'delete',
|
||||
},
|
||||
],
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('association field', async () => {
|
||||
await renderReadPrettySettings(associationFieldOptions());
|
||||
|
||||
await checkSettings(
|
||||
[
|
||||
{
|
||||
title: 'Edit field title',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Display title',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
title: 'Edit description',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Edit tooltip',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Set default value',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Pattern',
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
title: 'Field component',
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
title: 'Title field',
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
title: 'Enable link',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
title: 'Delete',
|
||||
type: 'delete',
|
||||
},
|
||||
],
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('menu items', () => {
|
||||
test('Edit field title', async () => {
|
||||
await renderSettings(commonFieldOptions());
|
||||
await checkFieldTitle('Nickname');
|
||||
});
|
||||
|
||||
test('Display title', async () => {
|
||||
await renderSettings(commonFieldOptions());
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'switch',
|
||||
title: 'Display title',
|
||||
beforeClick() {
|
||||
expect(document.querySelector('.ant-formily-item-label')).toHaveStyle({ display: 'flex' });
|
||||
},
|
||||
async afterFirstClick() {
|
||||
expect(document.querySelector('.ant-formily-item-label')).toHaveStyle({ display: 'none' });
|
||||
},
|
||||
afterSecondClick() {
|
||||
expect(document.querySelector('.ant-formily-item-label')).toHaveStyle({ display: 'flex' });
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Edit description', async () => {
|
||||
const newValue = 'new test';
|
||||
await renderSettings(commonFieldOptions());
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'modal',
|
||||
title: 'Edit description',
|
||||
modalChecker: {
|
||||
modalTitle: 'Edit description',
|
||||
formItems: [
|
||||
{
|
||||
type: 'textarea',
|
||||
newValue,
|
||||
},
|
||||
],
|
||||
afterSubmit() {
|
||||
expect(screen.queryByText(newValue)).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Edit tooltip', async () => {
|
||||
const newValue = 'new test';
|
||||
await renderSettings(commonFieldOptions());
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'modal',
|
||||
title: 'Edit tooltip',
|
||||
modalChecker: {
|
||||
modalTitle: 'Edit tooltip',
|
||||
formItems: [
|
||||
{
|
||||
type: 'textarea',
|
||||
newValue,
|
||||
},
|
||||
],
|
||||
async afterSubmit() {
|
||||
expect(document.querySelector('.anticon-question-circle')).toBeInTheDocument();
|
||||
await userEvent.hover(document.querySelector('.anticon-question-circle'));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(newValue)).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Required', async () => {
|
||||
await renderSettings(commonFieldOptions());
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'switch',
|
||||
title: 'Required',
|
||||
afterFirstClick() {
|
||||
expect(document.querySelector('.ant-formily-item-asterisk')).toBeInTheDocument();
|
||||
},
|
||||
afterSecondClick() {
|
||||
expect(document.querySelector('.ant-formily-item-asterisk')).not.toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Set default value', async () => {
|
||||
await renderSettings(commonFieldOptions());
|
||||
const newValue = 'new test';
|
||||
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'modal',
|
||||
title: 'Set default value',
|
||||
modalChecker: {
|
||||
modalTitle: 'Set default value',
|
||||
formItems: [
|
||||
{
|
||||
type: 'collectionField',
|
||||
field: 'users.nickname',
|
||||
newValue,
|
||||
},
|
||||
],
|
||||
afterSubmit() {
|
||||
expect(screen.queryByRole('textbox')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('textbox')).toHaveValue(newValue);
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Pattern', async () => {
|
||||
await renderSettings(associationFieldOptions());
|
||||
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'select',
|
||||
title: 'Pattern',
|
||||
oldValue: 'Editable',
|
||||
options: [
|
||||
{
|
||||
label: 'Readonly',
|
||||
checker() {
|
||||
expect(document.querySelector('.nb-form-item input')).toHaveAttribute('disabled');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Easy-reading',
|
||||
checker() {
|
||||
expect(document.querySelector('.nb-form-item input')).not.toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Editable',
|
||||
checker() {
|
||||
expect(document.querySelector('.nb-form-item input')).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('EditValidationRules', async () => {
|
||||
await renderSingleSettings(commonFieldOptions(true));
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'modal',
|
||||
title: 'Set validation rules',
|
||||
modalChecker: {
|
||||
modalTitle: 'Set validation rules',
|
||||
contentText: 'Add validation rule',
|
||||
async beforeCheck() {
|
||||
await userEvent.click(screen.getByText('Add validation rule'));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Min length')).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
formItems: [
|
||||
{
|
||||
type: 'number',
|
||||
label: 'Min length',
|
||||
newValue: 2,
|
||||
},
|
||||
],
|
||||
async afterSubmit() {
|
||||
await userEvent.type(document.querySelector('.nb-form-item input'), '1');
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('The length or number of entries must be at least 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.type(document.querySelector('.nb-form-item input'), '12');
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText('The length or number of entries must be at least 2'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Field component', async () => {
|
||||
await renderSingleSettings(associationFieldOptions(true));
|
||||
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'select',
|
||||
title: 'Field component',
|
||||
oldValue: 'Select',
|
||||
beforeSelect() {
|
||||
expect(screen.queryByTestId('select-object-multiple')).toBeInTheDocument();
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: 'Record picker',
|
||||
async checker() {
|
||||
expect(screen.queryByText('mode: Picker')).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Sub-table',
|
||||
checker() {
|
||||
expect(screen.queryByText('mode: SubTable')).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Sub-form',
|
||||
checker() {
|
||||
expect(screen.queryByText('mode: Nester')).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Sub-form(Popover)',
|
||||
checker() {
|
||||
expect(screen.queryByText('mode: PopoverNester')).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Title field', async () => {
|
||||
await renderSettings(associationFieldOptions());
|
||||
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'select',
|
||||
title: 'Title field',
|
||||
// oldValue: 'Role name',
|
||||
async beforeSelect() {
|
||||
expect(screen.queryByTestId('select-object-multiple')).toBeInTheDocument();
|
||||
await userEvent.click(document.querySelector('.ant-select-selector'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Admin')).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: 'Role UID',
|
||||
async checker() {
|
||||
expect(screen.queryByTestId('select-object-multiple')).toBeInTheDocument();
|
||||
await userEvent.click(document.querySelector('.ant-select-selector'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('admin')).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -7,7 +7,7 @@ import {
|
||||
useCollection_deprecated,
|
||||
useSortFields,
|
||||
} from '../../../../collection-manager';
|
||||
import { FilterBlockType } from '../../../../filter-provider';
|
||||
import { FilterBlockType } from '../../../../filter-provider/utils';
|
||||
import { useDesignable, removeNullCondition } from '../../../../schema-component';
|
||||
import {
|
||||
SchemaSettingsBlockTitleItem,
|
||||
|
@ -0,0 +1,194 @@
|
||||
import { ACLMenuItemProvider, AdminLayout, BlockSchemaComponentPlugin, CurrentUserProvider } from '@nocobase/client';
|
||||
import { renderApp, waitFor, screen } from '@nocobase/test/client';
|
||||
import React from 'react';
|
||||
|
||||
describe('AdminLayout', () => {
|
||||
it('should render correctly', async () => {
|
||||
await renderApp({
|
||||
designable: true,
|
||||
noWrapperSchema: true,
|
||||
appOptions: {
|
||||
plugins: [BlockSchemaComponentPlugin],
|
||||
components: { ACLMenuItemProvider },
|
||||
providers: [CurrentUserProvider],
|
||||
router: {
|
||||
type: 'memory',
|
||||
initialEntries: ['/admin/9zva4x7mblv'],
|
||||
routes: {
|
||||
admin: {
|
||||
path: '/admin',
|
||||
element: <AdminLayout />,
|
||||
},
|
||||
'admin.name': {
|
||||
path: '/admin/:name',
|
||||
element: <div />,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
apis: {
|
||||
'app:getInfo': {
|
||||
data: {
|
||||
database: {
|
||||
dialect: 'sqlite',
|
||||
},
|
||||
version: '0.21.0-alpha.5',
|
||||
lang: 'en-US',
|
||||
name: 'main',
|
||||
theme: 'default',
|
||||
},
|
||||
},
|
||||
'/uiSchemas:getJsonSchema/nocobase-admin-menu': {
|
||||
data: {
|
||||
type: 'void',
|
||||
'x-component': 'Menu',
|
||||
'x-designer': 'Menu.Designer',
|
||||
'x-component-props': {
|
||||
mode: 'mix',
|
||||
theme: 'dark',
|
||||
onSelect: '{{ onSelect }}',
|
||||
sideMenuRefScopeKey: 'sideMenuRef',
|
||||
},
|
||||
properties: {
|
||||
'9zva4x7mblv': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: 'header title',
|
||||
'x-component': 'Menu.SubMenu',
|
||||
'x-decorator': 'ACLMenuItemProvider',
|
||||
'x-component-props': {},
|
||||
'x-server-hooks': [
|
||||
{
|
||||
type: 'onSelfCreate',
|
||||
method: 'bindMenuToRole',
|
||||
},
|
||||
{
|
||||
type: 'onSelfSave',
|
||||
method: 'extractTextToLocale',
|
||||
},
|
||||
],
|
||||
'x-app-version': '0.21.0-alpha.5',
|
||||
properties: {
|
||||
yhk3dzb3474: {
|
||||
'x-uid': 'qzb0p475ld3',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: 'side title1',
|
||||
'x-component': 'Menu.Item',
|
||||
'x-decorator': 'ACLMenuItemProvider',
|
||||
'x-component-props': {},
|
||||
'x-server-hooks': [
|
||||
{
|
||||
type: 'onSelfSave',
|
||||
method: 'extractTextToLocale',
|
||||
},
|
||||
],
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
'2kudkwbme4m': {
|
||||
'x-uid': 'u9gw7d2d3x5',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: 'side title2',
|
||||
'x-component': 'Menu.Item',
|
||||
'x-decorator': 'ACLMenuItemProvider',
|
||||
'x-component-props': {},
|
||||
'x-server-hooks': [
|
||||
{
|
||||
type: 'onSelfSave',
|
||||
method: 'extractTextToLocale',
|
||||
},
|
||||
],
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid': '4ic78z722oh',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
name: 'pv2f6fp7r65',
|
||||
'x-uid': 'nocobase-admin-menu',
|
||||
'x-async': false,
|
||||
},
|
||||
},
|
||||
'roles:check': {
|
||||
data: {
|
||||
role: 'root',
|
||||
strategy: {},
|
||||
actions: {},
|
||||
snippets: ['pm', 'pm.*', 'ui.*'],
|
||||
availableActions: ['create', 'view', 'update', 'destroy', 'export', 'importXlsx'],
|
||||
resources: [],
|
||||
actionAlias: {},
|
||||
allowAll: true,
|
||||
allowConfigure: null,
|
||||
allowMenuItemIds: [],
|
||||
allowAnonymous: false,
|
||||
},
|
||||
meta: {
|
||||
dataSources: {},
|
||||
},
|
||||
},
|
||||
'auth:check': {
|
||||
data: {
|
||||
createdAt: '2024-04-07 06:50:37.797 +00:00',
|
||||
updatedAt: '2024-04-07 06:50:37.797 +00:00',
|
||||
appLang: null,
|
||||
createdById: null,
|
||||
email: 'xxx@nocobase.com',
|
||||
f_1gx8uyn3wva: 1,
|
||||
id: 1,
|
||||
nickname: 'Super Admin',
|
||||
password: 'xxx',
|
||||
phone: null,
|
||||
resetToken: null,
|
||||
sort: 1,
|
||||
systemSettings: '{}',
|
||||
updatedById: null,
|
||||
username: 'nocobase',
|
||||
},
|
||||
},
|
||||
'systemSettings:get/1': {
|
||||
data: {
|
||||
id: 1,
|
||||
createdAt: '2024-04-07T06:50:37.584Z',
|
||||
updatedAt: '2024-04-07T06:50:37.594Z',
|
||||
title: 'NocoBase',
|
||||
showLogoOnly: null,
|
||||
allowSignUp: true,
|
||||
smsAuthEnabled: false,
|
||||
logoId: 1,
|
||||
enabledLanguages: ['en-US'],
|
||||
appLang: 'en-US',
|
||||
options: {},
|
||||
},
|
||||
meta: {
|
||||
allowedActions: {
|
||||
view: [1],
|
||||
update: [1],
|
||||
destroy: [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
'uiSchemaTemplates:list': {
|
||||
data: [],
|
||||
},
|
||||
'collectionCategories:list': {
|
||||
data: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('header title')).toBeInTheDocument();
|
||||
expect(screen.queryByText('side title1')).toBeInTheDocument();
|
||||
expect(screen.queryByText('side title2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,45 @@
|
||||
import { RouteSchemaComponent } from '@nocobase/client';
|
||||
import { renderApp, waitFor, screen } from '@nocobase/test/client';
|
||||
import React from 'react';
|
||||
|
||||
describe('route-schema-component', () => {
|
||||
it('should render correctly', async () => {
|
||||
await renderApp({
|
||||
designable: true,
|
||||
noWrapperSchema: true,
|
||||
appOptions: {
|
||||
router: {
|
||||
type: 'memory',
|
||||
initialEntries: ['/admin/test'],
|
||||
routes: {
|
||||
test: {
|
||||
path: '/admin/:name',
|
||||
element: <RouteSchemaComponent />,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
apis: {
|
||||
'/uiSchemas:getProperties/test': {
|
||||
data: {
|
||||
type: 'void',
|
||||
properties: {
|
||||
test: {
|
||||
'x-component': 'div',
|
||||
'x-content': 'test',
|
||||
'x-component-props': {
|
||||
'data-testid': 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('test')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('test')).toHaveTextContent('test');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,51 @@
|
||||
import { screen, checkSettings, renderSingleSettings, checkModalSetting } from '@nocobase/test/client';
|
||||
import { ButtonEditor } from '../Action.Designer';
|
||||
|
||||
describe('Action.Designer', () => {
|
||||
test('ButtonEditor', async () => {
|
||||
const oldTitle = 'title';
|
||||
const newTitle = 'new title';
|
||||
const newIcon = 'alipay-circle';
|
||||
const newBackgroundColor = 'Danger red';
|
||||
|
||||
const { container } = await renderSingleSettings({
|
||||
Component: ButtonEditor,
|
||||
schema: {
|
||||
title: oldTitle,
|
||||
'x-component': 'Action',
|
||||
},
|
||||
});
|
||||
|
||||
await checkModalSetting({
|
||||
title: 'Edit button',
|
||||
modalChecker: {
|
||||
formItems: [
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Button title',
|
||||
oldValue: oldTitle,
|
||||
newValue: newTitle,
|
||||
},
|
||||
{
|
||||
type: 'icon',
|
||||
label: 'Button icon',
|
||||
newValue: newIcon,
|
||||
},
|
||||
{
|
||||
type: 'radio',
|
||||
label: 'Button background color',
|
||||
oldValue: 'Default',
|
||||
newValue: newBackgroundColor,
|
||||
},
|
||||
],
|
||||
afterSubmit: () => {
|
||||
const button = container.querySelector(`button[aria-label="action-Action-${newTitle}"]`);
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveTextContent(newTitle);
|
||||
expect(button).toHaveClass('ant-btn-dangerous');
|
||||
expect(button.querySelector(`span[aria-label=${newIcon}]`)).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
@ -16,6 +16,7 @@ export const useGetAriaLabelOfAction = (title: string) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const compile = useCompile();
|
||||
const component = fieldSchema['x-component'];
|
||||
const componentName = typeof component === 'string' ? component : component?.displayName || component?.name;
|
||||
let recordName = record?.name || record?.title || (recordIndex != null ? String(recordIndex) : '');
|
||||
let action = fieldSchema['x-action'];
|
||||
let { name: collectionName } = useCollection_deprecated();
|
||||
@ -29,9 +30,9 @@ export const useGetAriaLabelOfAction = (title: string) => {
|
||||
const getAriaLabel = useCallback(
|
||||
(postfix?: string) => {
|
||||
postfix = postfix ? `-${postfix}` : '';
|
||||
return `action-${component}-${actionTitle}${action}${collectionName}${blockName}${recordName}${postfix}`;
|
||||
return `action-${componentName}-${actionTitle}${action}${collectionName}${blockName}${recordName}${postfix}`;
|
||||
},
|
||||
[action, actionTitle, blockName, collectionName, component, recordName],
|
||||
[action, actionTitle, blockName, collectionName, componentName, recordName],
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -10,6 +10,7 @@ import { useCompile } from '../../../hooks';
|
||||
export const useGetAriaLabelOfDrawer = () => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const component = fieldSchema['x-component'];
|
||||
const componentName = typeof component === 'string' ? component : component?.displayName || component?.name;
|
||||
const compile = useCompile();
|
||||
let { name: collectionName } = useCollection_deprecated();
|
||||
let title = compile(fieldSchema.title);
|
||||
@ -19,9 +20,9 @@ export const useGetAriaLabelOfDrawer = () => {
|
||||
const getAriaLabel = useCallback(
|
||||
(postfix?: string) => {
|
||||
postfix = postfix ? `-${postfix}` : '';
|
||||
return `drawer-${component}${collectionName}${title}${postfix}`;
|
||||
return `drawer-${componentName}${collectionName}${title}${postfix}`;
|
||||
},
|
||||
[collectionName, component, title],
|
||||
[collectionName, componentName, title],
|
||||
);
|
||||
|
||||
return { getAriaLabel };
|
||||
|
@ -10,6 +10,7 @@ import { useCompile } from '../../../hooks';
|
||||
export const useGetAriaLabelOfModal = () => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const component = fieldSchema['x-component'];
|
||||
const componentName = typeof component === 'string' ? component : component?.displayName || component?.name;
|
||||
const compile = useCompile();
|
||||
let { name: collectionName } = useCollection_deprecated();
|
||||
let title = compile(fieldSchema.title);
|
||||
@ -19,9 +20,9 @@ export const useGetAriaLabelOfModal = () => {
|
||||
const getAriaLabel = useCallback(
|
||||
(postfix?: string) => {
|
||||
postfix = postfix ? `-${postfix}` : '';
|
||||
return `modal-${component}${collectionName}${title}${postfix}`;
|
||||
return `modal-${componentName}${collectionName}${title}${postfix}`;
|
||||
},
|
||||
[collectionName, component, title],
|
||||
[collectionName, componentName, title],
|
||||
);
|
||||
|
||||
return { getAriaLabel };
|
||||
|
@ -10,6 +10,7 @@ import { useCompile } from '../../../hooks';
|
||||
export const useGetAriaLabelOfPopover = () => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const component = fieldSchema['x-component'];
|
||||
const componentName = typeof component === 'string' ? component : component?.displayName || component?.name;
|
||||
const compile = useCompile();
|
||||
let { name: collectionName } = useCollection_deprecated();
|
||||
let title = compile(fieldSchema.title);
|
||||
@ -19,9 +20,9 @@ export const useGetAriaLabelOfPopover = () => {
|
||||
const getAriaLabel = useCallback(
|
||||
(postfix?: string) => {
|
||||
postfix = postfix ? `-${postfix}` : '';
|
||||
return `popover-${component}${collectionName}${title}${postfix}`;
|
||||
return `popover-${componentName}${collectionName}${title}${postfix}`;
|
||||
},
|
||||
[collectionName, component, title],
|
||||
[collectionName, componentName, title],
|
||||
);
|
||||
|
||||
return { getAriaLabel };
|
||||
|
@ -4,7 +4,7 @@ group:
|
||||
order: 3
|
||||
---
|
||||
|
||||
# Action <Badge>待定</Badge>
|
||||
# Action
|
||||
|
||||
## Nodes
|
||||
|
||||
|
@ -9,7 +9,7 @@ import { Button, Card, Divider, Tooltip } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FormActiveFieldsProvider } from '../../../block-provider';
|
||||
import { FormActiveFieldsProvider } from '../../../block-provider/hooks/useFormActiveFields';
|
||||
import {
|
||||
useCollectionRecord,
|
||||
useCollectionRecordData,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { APIClientProvider, AssociationSelect, FormProvider, SchemaComponent } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { mockAPIClient } from '../../../../testUtils';
|
||||
import { sleep } from '@nocobase/test/client';
|
||||
import { sleep } from '@nocobase/test/web';
|
||||
|
||||
const { apiClient, mockRequest } = mockAPIClient();
|
||||
mockRequest.onGet('/posts:list').reply(async () => {
|
||||
|
@ -12,9 +12,8 @@ import { useCompile } from '../../../hooks';
|
||||
export const useGetAriaLabelOfBlockItem = (name?: string) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const compile = useCompile();
|
||||
const component = _.isString(fieldSchema['x-component'])
|
||||
? fieldSchema['x-component']
|
||||
: fieldSchema['x-component']?.displayName;
|
||||
const component = fieldSchema['x-component'];
|
||||
const componentName = typeof component === 'string' ? component : component?.displayName;
|
||||
const collectionField = compile(fieldSchema['x-collection-field']);
|
||||
let { name: blockName } = useBlockContext() || {};
|
||||
// eslint-disable-next-line prefer-const
|
||||
@ -26,11 +25,11 @@ export const useGetAriaLabelOfBlockItem = (name?: string) => {
|
||||
const getAriaLabel = useCallback(
|
||||
(postfix?: string) => {
|
||||
postfix = postfix ? `-${postfix}` : '';
|
||||
return ['block-item', component, collectionName, blockName, collectionField, title, postfix]
|
||||
return ['block-item', componentName, collectionName, blockName, collectionField, title, postfix]
|
||||
.filter(Boolean)
|
||||
.join('-');
|
||||
},
|
||||
[component, collectionName, blockName, collectionField, title],
|
||||
[componentName, collectionName, blockName, collectionField, title],
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -4,7 +4,7 @@ group:
|
||||
order: 3
|
||||
---
|
||||
|
||||
# BlockItem <Badge>待定</Badge>
|
||||
# BlockItem
|
||||
|
||||
普通的装饰器(Decorator)组件,无特殊 UI 效果,一般用在 x-decorator 中。用于提供区块的管理,如拖拽功能、当前节点的 SettingsForm。CardItem 和 FormItem 组件都是基于 BlockItem 实现,也具备以上相同功能。
|
||||
|
||||
|
@ -105,16 +105,11 @@ function useDataSourceOptions({ filter }: DataSourceSelectProps) {
|
||||
const dataSourceManager = useDataSourceManager();
|
||||
const dataSources = dataSourceManager.getDataSources();
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
label: compile('Main'),
|
||||
value: 'main',
|
||||
},
|
||||
...(typeof filter === 'function' ? dataSources.filter(filter) : dataSources).map((item) => ({
|
||||
label: item.displayName,
|
||||
() =>
|
||||
(typeof filter === 'function' ? dataSources.filter(filter) : dataSources).map((item) => ({
|
||||
label: compile(item.displayName),
|
||||
value: item.key,
|
||||
})),
|
||||
],
|
||||
[dataSources, filter],
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
import { renderApp, screen, userEvent, waitFor } from '@nocobase/test/client';
|
||||
import { DataSourceCollectionCascader } from '../CollectionSelect';
|
||||
|
||||
describe('DataSourceCollectionCascader', () => {
|
||||
test('should works', async () => {
|
||||
await renderApp({
|
||||
enableMultipleDataSource: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
'x-component': DataSourceCollectionCascader,
|
||||
},
|
||||
});
|
||||
|
||||
await userEvent.click(document.querySelector('.ant-select-selector'));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Main')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Data Source 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText('Main'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Users')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Roles')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText('Users'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.ant-select-selector')).toHaveTextContent('Main / Users');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,81 @@
|
||||
import { renderApp, screen, userEvent, waitFor, renderReadPrettyApp } from '@nocobase/test/client';
|
||||
import { DataSourceSelect } from '../CollectionSelect';
|
||||
|
||||
describe('DataSourceSelect', () => {
|
||||
test('single', async () => {
|
||||
await renderApp({
|
||||
Component: DataSourceSelect,
|
||||
});
|
||||
|
||||
await userEvent.click(document.querySelector('.ant-select-selector'));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Main')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('multiple', async () => {
|
||||
await renderApp({
|
||||
enableMultipleDataSource: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
'x-component': DataSourceSelect,
|
||||
},
|
||||
});
|
||||
|
||||
await userEvent.click(document.querySelector('.ant-select-selector'));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Main')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Data Source 2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('change', async () => {
|
||||
await renderApp({
|
||||
enableMultipleDataSource: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
'x-component': DataSourceSelect,
|
||||
},
|
||||
});
|
||||
|
||||
await userEvent.click(document.querySelector('.ant-select-selector'));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Main')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('filter', async () => {
|
||||
await renderApp({
|
||||
enableMultipleDataSource: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
'x-component': DataSourceSelect,
|
||||
'x-component-props': {
|
||||
filter(item: any) {
|
||||
return item.key === 'main';
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await userEvent.click(document.querySelector('.ant-select-selector'));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Main')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Data Source 2')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// 组件并没有实现这个功能,所以 skip
|
||||
test.skip('read pretty', async () => {
|
||||
await renderReadPrettyApp({
|
||||
value: 'dataSource2',
|
||||
enableMultipleDataSource: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
'x-component': DataSourceSelect,
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Data Source 2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -1,30 +0,0 @@
|
||||
import { FormItem } from '@formily/antd-v5';
|
||||
import { ExtendCollectionsProvider, CollectionSelect, FormProvider, SchemaComponent } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const collections = [];
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
select: {
|
||||
type: 'string',
|
||||
title: 'Collection',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionSelect',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<FormProvider>
|
||||
<ExtendCollectionsProvider collections={collections as any}>
|
||||
<SchemaComponent components={{ FormItem, CollectionSelect }} scope={{ t }} schema={schema} />
|
||||
</ExtendCollectionsProvider>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
@ -6,5 +6,3 @@ group:
|
||||
# CollectionSelect
|
||||
|
||||
## Example
|
||||
|
||||
<code src="./demos/demo1.tsx"></code>
|
||||
|
@ -1,24 +0,0 @@
|
||||
import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__';
|
||||
import type { ColorPickerProps as AntdColorPickerProps } from 'antd/es/color-picker';
|
||||
import cls from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
type Composed = {
|
||||
ColorPicker: React.FC<AntdColorPickerProps>;
|
||||
};
|
||||
|
||||
export const ReadPretty: Composed = () => null;
|
||||
|
||||
ReadPretty.ColorPicker = function ColorPicker(props: any) {
|
||||
const prefixCls = usePrefixCls('description-color-picker', props);
|
||||
|
||||
if (!props.value) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cls(prefixCls, props.className)}>
|
||||
<ColorPicker showText disabled value={props.value} size="small" />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,160 +0,0 @@
|
||||
import { dayjs, getDefaultFormat, str2moment, toGmt, toLocal } from '@nocobase/utils/client';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
const toStringByPicker = (value, picker, timezone: 'gmt' | 'local') => {
|
||||
if (!dayjs.isDayjs(value)) return value;
|
||||
if (timezone === 'local') {
|
||||
const offset = new Date().getTimezoneOffset();
|
||||
return dayjs(toStringByPicker(value, picker, 'gmt'))
|
||||
.add(offset, 'minutes')
|
||||
.toISOString();
|
||||
}
|
||||
|
||||
if (picker === 'year') {
|
||||
return value.format('YYYY') + '-01-01T00:00:00.000Z';
|
||||
}
|
||||
if (picker === 'month') {
|
||||
return value.format('YYYY-MM') + '-01T00:00:00.000Z';
|
||||
}
|
||||
if (picker === 'quarter') {
|
||||
return value.startOf('quarter').format('YYYY-MM') + '-01T00:00:00.000Z';
|
||||
}
|
||||
if (picker === 'week') {
|
||||
return value.startOf('week').add(1, 'day').format('YYYY-MM-DD') + 'T00:00:00.000Z';
|
||||
}
|
||||
return value.format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z';
|
||||
};
|
||||
|
||||
const toGmtByPicker = (value: Dayjs, picker?: any) => {
|
||||
if (!value || !dayjs.isDayjs(value)) {
|
||||
return value;
|
||||
}
|
||||
return toStringByPicker(value, picker, 'gmt');
|
||||
};
|
||||
|
||||
const toLocalByPicker = (value: Dayjs, picker?: any) => {
|
||||
if (!value || !dayjs.isDayjs(value)) {
|
||||
return value;
|
||||
}
|
||||
return toStringByPicker(value, picker, 'local');
|
||||
};
|
||||
|
||||
export interface Moment2strOptions {
|
||||
showTime?: boolean;
|
||||
gmt?: boolean;
|
||||
utc?: boolean;
|
||||
picker?: 'year' | 'month' | 'week' | 'quarter';
|
||||
}
|
||||
|
||||
export const moment2str = (value?: Dayjs | null, options: Moment2strOptions = {}) => {
|
||||
const { showTime, gmt, picker, utc = true } = options;
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
if (!utc) {
|
||||
const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
|
||||
return value.format(format);
|
||||
}
|
||||
if (showTime) {
|
||||
return gmt ? toGmt(value) : toLocal(value);
|
||||
}
|
||||
if (typeof gmt === 'boolean') {
|
||||
return gmt ? toGmtByPicker(value, picker) : toLocalByPicker(value, picker);
|
||||
}
|
||||
return toGmtByPicker(value, picker);
|
||||
};
|
||||
|
||||
export const mapDatePicker = function () {
|
||||
return (props: any) => {
|
||||
const format = getDefaultFormat(props) as any;
|
||||
const onChange = props.onChange;
|
||||
|
||||
return {
|
||||
...props,
|
||||
format: format,
|
||||
value: str2moment(props.value, props),
|
||||
onChange: (value: Dayjs | null) => {
|
||||
if (onChange) {
|
||||
if (!props.showTime && value) {
|
||||
value = value.startOf('day');
|
||||
}
|
||||
onChange(moment2str(value, props));
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const mapRangePicker = function () {
|
||||
return (props: any) => {
|
||||
const format = getDefaultFormat(props) as any;
|
||||
const onChange = props.onChange;
|
||||
|
||||
return {
|
||||
...props,
|
||||
format: format,
|
||||
value: str2moment(props.value, props),
|
||||
onChange: (value: Dayjs[]) => {
|
||||
if (onChange) {
|
||||
onChange(
|
||||
value
|
||||
? [moment2str(getRangeStart(value[0], props), props), moment2str(getRangeEnd(value[1], props), props)]
|
||||
: [],
|
||||
);
|
||||
}
|
||||
},
|
||||
} as any;
|
||||
};
|
||||
};
|
||||
|
||||
function getRangeStart(value: Dayjs, options: Moment2strOptions) {
|
||||
const { showTime } = options;
|
||||
if (showTime) {
|
||||
return value;
|
||||
}
|
||||
return value.startOf('day');
|
||||
}
|
||||
|
||||
function getRangeEnd(value: Dayjs, options: Moment2strOptions) {
|
||||
const { showTime } = options;
|
||||
if (showTime) {
|
||||
return value;
|
||||
}
|
||||
return value.endOf('day');
|
||||
}
|
||||
|
||||
const getStart = (offset: any, unit: any) => {
|
||||
return dayjs()
|
||||
.add(offset, unit === 'isoWeek' ? 'week' : unit)
|
||||
.startOf(unit);
|
||||
};
|
||||
|
||||
const getEnd = (offset: any, unit: any) => {
|
||||
return dayjs()
|
||||
.add(offset, unit === 'isoWeek' ? 'week' : unit)
|
||||
.endOf(unit);
|
||||
};
|
||||
|
||||
export const getDateRanges = () => {
|
||||
return {
|
||||
today: () => [getStart(0, 'day'), getEnd(0, 'day')],
|
||||
lastWeek: () => [getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')],
|
||||
thisWeek: () => [getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')],
|
||||
nextWeek: () => [getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')],
|
||||
lastMonth: () => [getStart(-1, 'month'), getEnd(-1, 'month')],
|
||||
thisMonth: () => [getStart(0, 'month'), getEnd(0, 'month')],
|
||||
nextMonth: () => [getStart(1, 'month'), getEnd(1, 'month')],
|
||||
lastQuarter: () => [getStart(-1, 'quarter'), getEnd(-1, 'quarter')],
|
||||
thisQuarter: () => [getStart(0, 'quarter'), getEnd(0, 'quarter')],
|
||||
nextQuarter: () => [getStart(1, 'quarter'), getEnd(1, 'quarter')],
|
||||
lastYear: () => [getStart(-1, 'year'), getEnd(-1, 'year')],
|
||||
thisYear: () => [getStart(0, 'year'), getEnd(0, 'year')],
|
||||
nextYear: () => [getStart(1, 'year'), getEnd(1, 'year')],
|
||||
last7Days: () => [getStart(-6, 'days'), getEnd(0, 'days')],
|
||||
next7Days: () => [getStart(1, 'day'), getEnd(7, 'days')],
|
||||
last30Days: () => [getStart(-29, 'days'), getEnd(0, 'days')],
|
||||
next30Days: () => [getStart(1, 'day'), getEnd(30, 'days')],
|
||||
last90Days: () => [getStart(-89, 'days'), getEnd(0, 'days')],
|
||||
next90Days: () => [getStart(1, 'day'), getEnd(90, 'days')],
|
||||
};
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import { str2moment } from '@nocobase/utils/client';
|
||||
import dayjs from 'dayjs';
|
||||
import { moment2str } from '../util';
|
||||
import { str2moment } from '@nocobase/utils/client';
|
||||
import { getDateRanges, moment2str } from '../util';
|
||||
|
||||
describe('str2moment', () => {
|
||||
describe('string value', () => {
|
||||
@ -138,3 +138,152 @@ describe('moment2str', () => {
|
||||
expect(m).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// CI 环境会报错,可能因为时区问题
|
||||
describe.skip('getDateRanges', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2024-03-15T10:10:10.000Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('now', () => {
|
||||
const now = getDateRanges().now();
|
||||
expect(now.valueOf()).toMatchInlineSnapshot(`"2024-03-15T10:10:10.000Z"`);
|
||||
});
|
||||
|
||||
test('today', () => {
|
||||
const [start, end] = getDateRanges().today();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1710432000000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1710518399999`);
|
||||
});
|
||||
|
||||
test('lastWeek', () => {
|
||||
const [start, end] = getDateRanges().lastWeek();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1709481600000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1710086399999`);
|
||||
});
|
||||
|
||||
test('thisWeek', () => {
|
||||
const [start, end] = getDateRanges().thisWeek();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1710086400000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1710691199999`);
|
||||
});
|
||||
|
||||
test('nextWeek', () => {
|
||||
const [start, end] = getDateRanges().nextWeek();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1710691200000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1711295999999`);
|
||||
});
|
||||
|
||||
test('thisIsoWeek', () => {
|
||||
const [start, end] = getDateRanges().thisIsoWeek();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1710086400000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1710691199999`);
|
||||
});
|
||||
|
||||
test('lastIsoWeek', () => {
|
||||
const [start, end] = getDateRanges().lastIsoWeek();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1709481600000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1710086399999`);
|
||||
});
|
||||
|
||||
test('nextIsoWeek', () => {
|
||||
const [start, end] = getDateRanges().nextIsoWeek();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1710691200000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1711295999999`);
|
||||
});
|
||||
|
||||
test('lastMonth', () => {
|
||||
const [start, end] = getDateRanges().lastMonth();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1706716800000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1709222399999`);
|
||||
});
|
||||
|
||||
test('thisMonth', () => {
|
||||
const [start, end] = getDateRanges().thisMonth();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1709222400000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1711900799999`);
|
||||
});
|
||||
|
||||
test('nextMonth', () => {
|
||||
const [start, end] = getDateRanges().nextMonth();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1711900800000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1714492799999`);
|
||||
});
|
||||
|
||||
test('lastQuarter', () => {
|
||||
const [start, end] = getDateRanges().lastQuarter();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1696089600000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1704038399999`);
|
||||
});
|
||||
|
||||
test('thisQuarter', () => {
|
||||
const [start, end] = getDateRanges().thisQuarter();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1704038400000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1711900799999`);
|
||||
});
|
||||
|
||||
test('nextQuarter', () => {
|
||||
const [start, end] = getDateRanges().nextQuarter();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1711900800000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1719763199999`);
|
||||
});
|
||||
|
||||
test('lastYear', () => {
|
||||
const [start, end] = getDateRanges().lastYear();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1672502400000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1704038399999`);
|
||||
});
|
||||
|
||||
test('thisYear', () => {
|
||||
const [start, end] = getDateRanges().thisYear();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1704038400000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1735660799999`);
|
||||
});
|
||||
|
||||
test('nextYear', () => {
|
||||
const [start, end] = getDateRanges().nextYear();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1735660800000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1767196799999`);
|
||||
});
|
||||
|
||||
test('last7Days', () => {
|
||||
const [start, end] = getDateRanges().last7Days();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1709913600000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1710518399999`);
|
||||
});
|
||||
|
||||
test('next7Days', () => {
|
||||
const [start, end] = getDateRanges().next7Days();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1710518400000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1711123199999`);
|
||||
});
|
||||
|
||||
test('last30Days', () => {
|
||||
const [start, end] = getDateRanges().last30Days();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1707926400000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1710518399999`);
|
||||
});
|
||||
|
||||
test('next30Days', () => {
|
||||
const [start, end] = getDateRanges().next30Days();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1710518400000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1713110399999`);
|
||||
});
|
||||
|
||||
test('last90Days', () => {
|
||||
const [start, end] = getDateRanges().last90Days();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1702742400000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1710518399999`);
|
||||
});
|
||||
|
||||
test('next90Days', () => {
|
||||
const [start, end] = getDateRanges().next90Days();
|
||||
expect(start.valueOf()).toMatchInlineSnapshot(`1710518400000`);
|
||||
expect(end.valueOf()).toMatchInlineSnapshot(`1718294399999`);
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,657 @@
|
||||
import {
|
||||
screen,
|
||||
checkSettings,
|
||||
renderSingleSettings,
|
||||
waitFor,
|
||||
userEvent,
|
||||
renderReadPrettySingleSettings,
|
||||
} from '@nocobase/test/client';
|
||||
import {
|
||||
EditComponent,
|
||||
EditDefaultValue,
|
||||
EditDescription,
|
||||
EditOperator,
|
||||
EditPattern,
|
||||
EditRequired,
|
||||
EditTitle,
|
||||
EditTitleField,
|
||||
EditTooltip,
|
||||
EditValidationRules,
|
||||
} from '../SchemaSettingOptions';
|
||||
import { FilterFormBlockProvider, FormBlockProvider, FormItem } from '@nocobase/client';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { observer } from '@formily/reactive-react';
|
||||
import React from 'react';
|
||||
|
||||
describe('SchemaSettingOptions', () => {
|
||||
describe('EditTitle', () => {
|
||||
test('should work', async () => {
|
||||
const oldValue = 'Nickname';
|
||||
const newValue = 'new test';
|
||||
await renderSingleSettings({
|
||||
Component: EditTitle,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
name: 'nickname',
|
||||
default: oldValue,
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
},
|
||||
});
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'modal',
|
||||
title: 'Edit field title',
|
||||
modalChecker: {
|
||||
modalTitle: 'Edit field title',
|
||||
formItems: [
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Field title',
|
||||
oldValue,
|
||||
newValue,
|
||||
},
|
||||
],
|
||||
async afterSubmit() {
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(newValue)).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('visible: false', async () => {
|
||||
const fieldName = 'not-exist';
|
||||
await renderSingleSettings({
|
||||
Component: EditTitle,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
name: fieldName,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Edit field title')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('EditDescription', () => {
|
||||
test('should work', async () => {
|
||||
const newValue = 'new test';
|
||||
await renderSingleSettings({
|
||||
Component: EditDescription,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
name: 'nickname',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
},
|
||||
});
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'modal',
|
||||
title: 'Edit description',
|
||||
modalChecker: {
|
||||
modalTitle: 'Edit description',
|
||||
formItems: [
|
||||
{
|
||||
type: 'textarea',
|
||||
newValue,
|
||||
},
|
||||
],
|
||||
afterSubmit() {
|
||||
expect(screen.queryByText(newValue)).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('read pretty mode should not render', async () => {
|
||||
await renderSingleSettings({
|
||||
Component: EditDescription,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
name: 'nickname',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Edit description')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('EditTooltip', () => {
|
||||
test('should work', async () => {
|
||||
const newValue = 'new test';
|
||||
await renderSingleSettings({
|
||||
Component: EditTooltip,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
name: 'nickname',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
});
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'modal',
|
||||
title: 'Edit tooltip',
|
||||
modalChecker: {
|
||||
modalTitle: 'Edit description',
|
||||
formItems: [
|
||||
{
|
||||
type: 'textarea',
|
||||
newValue,
|
||||
},
|
||||
],
|
||||
async afterSubmit() {
|
||||
expect(document.querySelector('.anticon-question-circle')).toBeInTheDocument();
|
||||
await userEvent.hover(document.querySelector('.anticon-question-circle'));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(newValue)).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('read pretty mode should not render', async () => {
|
||||
await renderSingleSettings({
|
||||
Component: EditTooltip,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
name: 'nickname',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Edit tooltip')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('EditRequired', () => {
|
||||
test('should work', async () => {
|
||||
await renderSingleSettings({
|
||||
Component: EditRequired,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
name: 'nickname',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
},
|
||||
});
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'switch',
|
||||
title: 'Required',
|
||||
afterFirstClick() {
|
||||
expect(document.querySelector('.ant-formily-item-asterisk')).toBeInTheDocument();
|
||||
},
|
||||
afterSecondClick() {
|
||||
expect(document.querySelector('.ant-formily-item-asterisk')).not.toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('read pretty mode should not render', async () => {
|
||||
await renderSingleSettings({
|
||||
Component: EditRequired,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
name: 'nickname',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Edit tooltip')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('EditDefaultValue', () => {
|
||||
test('should work', async () => {
|
||||
await renderSingleSettings({
|
||||
Component: EditDefaultValue,
|
||||
settingPath: 'properties.nickname',
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
},
|
||||
properties: {
|
||||
nickname: {
|
||||
type: 'string',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
components: {
|
||||
FormBlockProvider,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newValue = 'new test';
|
||||
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'modal',
|
||||
title: 'Set default value',
|
||||
modalChecker: {
|
||||
modalTitle: 'Set default value',
|
||||
formItems: [
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Default value',
|
||||
newValue,
|
||||
},
|
||||
],
|
||||
afterSubmit() {
|
||||
expect(screen.queryByRole('textbox')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('textbox')).toHaveValue(newValue);
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EditPattern', () => {
|
||||
test('should work', async () => {
|
||||
await renderSingleSettings({
|
||||
Component: EditPattern,
|
||||
settingPath: 'properties.nickname',
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
},
|
||||
properties: {
|
||||
nickname: {
|
||||
type: 'string',
|
||||
default: 'nickname value',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
components: {
|
||||
FormBlockProvider,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'select',
|
||||
title: 'Pattern',
|
||||
oldValue: 'Editable',
|
||||
options: [
|
||||
{
|
||||
label: 'Readonly',
|
||||
checker() {
|
||||
expect(screen.queryByRole('textbox')).toHaveAttribute('disabled');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Easy-reading',
|
||||
checker() {
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('nickname value')).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Editable',
|
||||
checker() {
|
||||
expect(screen.queryByRole('textbox')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('textbox')).toHaveValue('nickname value');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EditComponent', () => {
|
||||
test('should work', async () => {
|
||||
const FormItemWithSchema = observer(({ children }) => {
|
||||
const schema = useFieldSchema();
|
||||
return (
|
||||
<>
|
||||
<div>mode: {schema?.['x-component-props']?.['mode']}</div>
|
||||
<FormItem>{children}</FormItem>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
await renderSingleSettings({
|
||||
Component: EditComponent,
|
||||
settingPath: 'properties.roles',
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
},
|
||||
properties: {
|
||||
roles: {
|
||||
type: 'string',
|
||||
'x-collection-field': 'users.roles',
|
||||
'x-decorator': 'FormItemWithSchema',
|
||||
'x-component': 'CollectionField',
|
||||
'x-component-props': {
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
components: {
|
||||
FormBlockProvider,
|
||||
FormItemWithSchema,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'select',
|
||||
title: 'Field component',
|
||||
oldValue: 'Select',
|
||||
beforeSelect() {
|
||||
expect(screen.queryByTestId('select-object-multiple')).toBeInTheDocument();
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: 'Record picker',
|
||||
async checker() {
|
||||
expect(screen.queryByText('mode: Picker')).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Sub-table',
|
||||
checker() {
|
||||
expect(screen.queryByText('mode: SubTable')).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Sub-form',
|
||||
checker() {
|
||||
expect(screen.queryByText('mode: Nester')).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Sub-form(Popover)',
|
||||
checker() {
|
||||
expect(screen.queryByText('mode: PopoverNester')).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EditTitleField', () => {
|
||||
test('should work', async () => {
|
||||
await renderSingleSettings({
|
||||
Component: EditTitleField,
|
||||
settingPath: 'properties.roles',
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
},
|
||||
properties: {
|
||||
roles: {
|
||||
type: 'string',
|
||||
'x-collection-field': 'users.roles',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
'x-component-props': {
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
components: {
|
||||
FormBlockProvider,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'select',
|
||||
title: 'Title field',
|
||||
oldValue: 'Role UID',
|
||||
async beforeSelect() {
|
||||
expect(screen.queryByTestId('select-object-multiple')).toBeInTheDocument();
|
||||
await userEvent.click(document.querySelector('.ant-select-selector'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('admin')).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: 'Role name',
|
||||
async checker() {
|
||||
expect(screen.queryByTestId('select-object-multiple')).toBeInTheDocument();
|
||||
await userEvent.click(document.querySelector('.ant-select-selector'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Admin')).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EditValidationRules', () => {
|
||||
test('should work', async () => {
|
||||
await renderSingleSettings({
|
||||
Component: EditValidationRules,
|
||||
settingPath: 'properties.nickname',
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
},
|
||||
properties: {
|
||||
nickname: {
|
||||
type: 'string',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
components: {
|
||||
FormBlockProvider,
|
||||
},
|
||||
},
|
||||
});
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'modal',
|
||||
title: 'Set validation rules',
|
||||
modalChecker: {
|
||||
modalTitle: 'Set validation rules',
|
||||
contentText: 'Add validation rule',
|
||||
async beforeCheck() {
|
||||
await userEvent.click(screen.getByText('Add validation rule'));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Min length')).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
formItems: [
|
||||
{
|
||||
type: 'number',
|
||||
label: 'Min length',
|
||||
newValue: 2,
|
||||
},
|
||||
],
|
||||
async afterSubmit() {
|
||||
await userEvent.type(screen.getByRole('textbox'), '1');
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('The length or number of entries must be at least 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.type(screen.getByRole('textbox'), '12');
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText('The length or number of entries must be at least 2'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('read pretty mode should not render', async () => {
|
||||
await renderReadPrettySingleSettings({
|
||||
Component: EditValidationRules,
|
||||
settingPath: 'properties.nickname',
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
},
|
||||
properties: {
|
||||
nickname: {
|
||||
type: 'string',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
components: {
|
||||
FormBlockProvider,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Edit tooltip')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('EditOperator', () => {
|
||||
test('should work', async () => {
|
||||
await renderSingleSettings({
|
||||
Component: EditOperator,
|
||||
settingPath: 'properties.nickname',
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-decorator': 'FilterFormBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
},
|
||||
properties: {
|
||||
nickname: {
|
||||
type: 'string',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
components: {
|
||||
FilterFormBlockProvider,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'select',
|
||||
title: 'Operator',
|
||||
oldValue: 'contains',
|
||||
options: [
|
||||
{
|
||||
label: 'does not contain',
|
||||
},
|
||||
{
|
||||
label: 'is',
|
||||
},
|
||||
{
|
||||
label: 'is not',
|
||||
},
|
||||
{
|
||||
label: 'is empty',
|
||||
},
|
||||
{
|
||||
label: 'is not empty',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,219 @@
|
||||
import { BlockSchemaComponentPlugin, FormBlockProvider, VariablesProvider } from '@nocobase/client';
|
||||
import { checkSettings, renderApp } from '@nocobase/test/client';
|
||||
import { withSchema } from '@nocobase/test/web';
|
||||
|
||||
describe('form.settings', () => {
|
||||
test('new schema version', async () => {
|
||||
const FormBlockProviderWithSchema = withSchema(FormBlockProvider);
|
||||
await renderApp({
|
||||
designable: true,
|
||||
schema: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
'x-acl-action': 'users:create',
|
||||
'x-decorator': 'FormBlockProviderWithSchema',
|
||||
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
dataSource: 'main',
|
||||
collection: 'users',
|
||||
},
|
||||
'x-toolbar': 'BlockSchemaToolbar',
|
||||
'x-settings': 'blockSettings:createForm',
|
||||
'x-component': 'CardItem',
|
||||
properties: {
|
||||
grid: {
|
||||
'x-uid': 'h38s9pa4ik5',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
udpf3e45i3d: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
hhc0bsk1roi: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
username: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'users.username',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-uid': '71x74r4t4g0',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'ophjdttgmo5',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'ta1vq3qr1sd',
|
||||
'x-async': false,
|
||||
'x-index': 3,
|
||||
},
|
||||
row_rpkxgfonud3: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-index': 4,
|
||||
properties: {
|
||||
mmo2k17b0q1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
properties: {
|
||||
nickname: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-uid': 'bcowga6nzzy',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'l1awt5at07z',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'y1tdyhcwhhi',
|
||||
'x-async': false,
|
||||
},
|
||||
},
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
'0m1r08p58e9': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
layout: 'one-column',
|
||||
style: {
|
||||
marginTop: 24,
|
||||
},
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-uid': 't4gxf0xxaxc',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
plugins: [BlockSchemaComponentPlugin],
|
||||
providers: [VariablesProvider],
|
||||
components: {
|
||||
FormBlockProviderWithSchema,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkSettings([
|
||||
{
|
||||
title: 'Edit block title',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Linkage rules',
|
||||
type: 'modal',
|
||||
modalChecker: {
|
||||
modalTitle: 'Linkage rules',
|
||||
contentText: 'Add linkage rule',
|
||||
async customCheck() {
|
||||
// await userEvent.click(screen.getByText('Add linkage rule'));
|
||||
// await waitFor(() => {
|
||||
// expect(screen.queryByText('Add condition')).toBeInTheDocument();
|
||||
// })
|
||||
// await userEvent.click(screen.getByText('Add condition'));
|
||||
// await waitFor(() => {
|
||||
// expect(screen.queryByText('Select field')).toBeInTheDocument();
|
||||
// })
|
||||
// await userEvent.click(screen.getByText('Select field'));
|
||||
// await waitFor(() => {
|
||||
// expect(screen.queryByTitle('Username')).toBeInTheDocument();
|
||||
// })
|
||||
// await userEvent.click(screen.getByTitle('Username'));
|
||||
// const dialog = screen.queryByRole('dialog');
|
||||
// await userEvent.type(dialog.querySelectorAll('.ant-input')[1], '1');
|
||||
// const properties = screen.queryByTestId('select-linkage-property-field');
|
||||
// await userEvent.click(screen.getByText('Select field'));
|
||||
// await waitFor(() => {
|
||||
// expect(properties.querySelector(`[title=Nickname]`)).toBeInTheDocument();
|
||||
// })
|
||||
// await userEvent.click(properties.querySelector(`[title=Nickname]`));
|
||||
// await userEvent.click(screen.getByText('action'));
|
||||
// await waitFor(() => {
|
||||
// expect(screen.queryByText('Hidden')).toBeInTheDocument();
|
||||
// })
|
||||
// await userEvent.click(screen.getByText('Hidden'));
|
||||
},
|
||||
// async afterSubmit() {
|
||||
// await checkSchema({
|
||||
// "x-linkage-rules": [
|
||||
// {
|
||||
// "condition": {
|
||||
// "$and": [
|
||||
// {
|
||||
// "username": {}
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// "actions": [
|
||||
// {
|
||||
// "targetFields": [
|
||||
// "nickname"
|
||||
// ],
|
||||
// "operator": "none"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// })
|
||||
// },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Form data templates',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Save as block template',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Delete',
|
||||
type: 'delete',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// test('old schema version', async () => {});
|
||||
});
|
@ -8,6 +8,7 @@ import App5 from '../demos/demo5';
|
||||
import App6 from '../demos/demo6';
|
||||
import App7 from '../demos/demo7';
|
||||
import App8 from '../demos/demo8';
|
||||
import { renderDemo9 } from '../demos/demo9';
|
||||
|
||||
describe('Form', () => {
|
||||
it('basic', async () => {
|
||||
@ -151,4 +152,25 @@ describe('Form', () => {
|
||||
expect(closeBtn).toBeInTheDocument();
|
||||
expect(screen.getByText(/drawer title/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('linkage', async () => {
|
||||
await renderDemo9();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.ant-input')).toBeInTheDocument();
|
||||
expect(document.querySelectorAll('.ant-input')).toHaveLength(2);
|
||||
});
|
||||
|
||||
await userEvent.type(document.querySelector('.ant-input'), 'test');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelectorAll('.ant-input')).toHaveLength(1);
|
||||
});
|
||||
|
||||
await userEvent.clear(document.querySelector('.ant-input'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelectorAll('.ant-input')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,165 @@
|
||||
import { BlockSchemaComponentPlugin, VariablesProvider } from '@nocobase/client';
|
||||
import { renderApp } from '@nocobase/test/client';
|
||||
|
||||
export const renderDemo9 = () =>
|
||||
renderApp({
|
||||
designable: true,
|
||||
schema: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
'x-acl-action': 'users:create',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
dataSource: 'main',
|
||||
collection: 'users',
|
||||
},
|
||||
'x-component': 'div',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
'0s3tm262rre': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-use-component-props': 'useCreateFormBlockProps',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
grid: {
|
||||
'x-uid': 'h38s9pa4ik5',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-linkage-rules': [
|
||||
{
|
||||
condition: {
|
||||
$and: [
|
||||
{
|
||||
username: {
|
||||
$eq: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
targetFields: ['nickname'],
|
||||
operator: 'none',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
udpf3e45i3d: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
hhc0bsk1roi: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
username: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'users.username',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-uid': '71x74r4t4g0',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'ophjdttgmo5',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'ta1vq3qr1sd',
|
||||
'x-async': false,
|
||||
'x-index': 3,
|
||||
},
|
||||
row_rpkxgfonud3: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-index': 4,
|
||||
properties: {
|
||||
mmo2k17b0q1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
properties: {
|
||||
nickname: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-uid': 'bcowga6nzzy',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'l1awt5at07z',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'y1tdyhcwhhi',
|
||||
'x-async': false,
|
||||
},
|
||||
},
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
'0m1r08p58e9': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
layout: 'one-column',
|
||||
style: {
|
||||
marginTop: 24,
|
||||
},
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-uid': 't4gxf0xxaxc',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid': 'yk2fivh9hgb',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'aqbi3avt3kb',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
appOptions: {
|
||||
plugins: [BlockSchemaComponentPlugin],
|
||||
providers: [VariablesProvider],
|
||||
},
|
||||
});
|
@ -2,7 +2,7 @@ import { FormLayout } from '@formily/antd-v5';
|
||||
import { createForm } from '@formily/core';
|
||||
import { FormContext, useField, useFieldSchema } from '@formily/react';
|
||||
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
||||
import { BlockProvider, useBlockRequestContext, useParsedFilter } from '../../../block-provider';
|
||||
import { BlockProvider, useBlockRequestContext } from '../../../block-provider/BlockProvider';
|
||||
import useStyles from './GridCard.Decorator.style';
|
||||
import { useGridCardBlockParams } from '../../../modules/blocks/data-blocks/grid-card/hooks/useGridCardBlockParams';
|
||||
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
|
||||
|
@ -1,9 +1,312 @@
|
||||
import { render } from '@nocobase/test/client';
|
||||
import React from 'react';
|
||||
import App1 from '../demos/demo1';
|
||||
import { BlockSchemaComponentPlugin } from '@nocobase/client';
|
||||
import { renderApp, waitFor, screen, userEvent } from '@nocobase/test/client';
|
||||
|
||||
describe('GridCard', () => {
|
||||
it('should render correctly', () => {
|
||||
render(<App1 />);
|
||||
it('should render correctly', async () => {
|
||||
await renderApp({
|
||||
designable: true,
|
||||
schema: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-acl-action': 'users:view',
|
||||
'x-decorator': 'GridCard.Decorator',
|
||||
'x-use-decorator-props': 'useGridCardBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
dataSource: 'main',
|
||||
readPretty: true,
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 12,
|
||||
},
|
||||
runWhenParamsChanged: true,
|
||||
rowKey: 'id',
|
||||
},
|
||||
'x-component': 'div',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
actionBar: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
},
|
||||
list: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'array',
|
||||
'x-component': 'GridCard',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
item: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'object',
|
||||
'x-component': 'GridCard.Item',
|
||||
'x-read-pretty': true,
|
||||
'x-use-component-props': 'useGridCardItemProps',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
grid: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
b8r0aisveq3: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
h3ycwb9e5qv: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
id: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'users.id',
|
||||
'x-component-props': {},
|
||||
'x-read-pretty': true,
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ycbv8h4ymzy: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
'2bz1mvhxhsw': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
nickname: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
vd06ptpdvjd: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
knl514ethip: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
username: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'users.username',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
actionBar: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-align': 'left',
|
||||
'x-component': 'ActionBar',
|
||||
'x-use-component-props': 'useGridCardActionBarProps',
|
||||
'x-component-props': {
|
||||
layout: 'one-column',
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
jquyomz6ipk: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("View") }}',
|
||||
'x-action': 'view',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
},
|
||||
'x-align': 'left',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
drawer: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("View record") }}',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
tabs: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
tab1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{t("Details")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
grid: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bc60nzpw94v: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Edit") }}',
|
||||
'x-action': 'update',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
icon: 'EditOutlined',
|
||||
},
|
||||
'x-align': 'left',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
drawer: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Edit record") }}',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
tabs: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
tab1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{t("Edit")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
grid: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
plugins: [BlockSchemaComponentPlugin],
|
||||
scopes: {},
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('ID')).toBeInTheDocument();
|
||||
expect(screen.queryByText('1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Nickname')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Super Admin')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Username')).toBeInTheDocument();
|
||||
expect(screen.queryByText('nocobase')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('View')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Edit')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText('View'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('tab')).toHaveTextContent('Details');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default () => {
|
||||
return <div>TODO</div>;
|
||||
};
|
@ -4,5 +4,3 @@ group:
|
||||
---
|
||||
|
||||
# GridCard
|
||||
|
||||
<code src="./demos/demo1.tsx"></code>
|
||||
|
@ -4,8 +4,9 @@ import { createForm } from '@formily/core';
|
||||
import { FormContext, useField } from '@formily/react';
|
||||
import _ from 'lodash';
|
||||
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
||||
import { BlockProvider, useBlockRequestContext, useParsedFilter } from '../../../block-provider';
|
||||
import { BlockProvider, useBlockRequestContext } from '../../../block-provider/BlockProvider';
|
||||
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
|
||||
import { useParsedFilter } from '../../../block-provider/hooks/useParsedFilter';
|
||||
|
||||
export const ListBlockContext = createContext<any>({});
|
||||
ListBlockContext.displayName = 'ListBlockContext';
|
||||
|
@ -1,9 +1,237 @@
|
||||
import { render } from '@nocobase/test/client';
|
||||
import React from 'react';
|
||||
import App1 from '../demos/demo1';
|
||||
import { BlockSchemaComponentPlugin } from '@nocobase/client';
|
||||
import { renderApp, waitFor, screen, userEvent } from '@nocobase/test/client';
|
||||
|
||||
describe('List', () => {
|
||||
it('should render correctly', () => {
|
||||
render(<App1 />);
|
||||
it('should render correctly', async () => {
|
||||
await renderApp({
|
||||
designable: true,
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-acl-action': 'users:view',
|
||||
'x-decorator': 'List.Decorator',
|
||||
'x-use-decorator-props': 'useListBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
dataSource: 'main',
|
||||
readPretty: true,
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 10,
|
||||
},
|
||||
runWhenParamsChanged: true,
|
||||
rowKey: 'id',
|
||||
},
|
||||
'x-component': 'CardItem',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
actionBar: {
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
},
|
||||
list: {
|
||||
type: 'array',
|
||||
'x-component': 'List',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
item: {
|
||||
type: 'object',
|
||||
'x-component': 'List.Item',
|
||||
'x-read-pretty': true,
|
||||
'x-use-component-props': 'useListItemProps',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
grid: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
'48x3suuacem': {
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
z48z4iekhr0: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'users.id',
|
||||
'x-component-props': {},
|
||||
'x-read-pretty': true,
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
afcj7ty3jkw: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
phs2rix7vvp: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
nickname: {
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'users.nickname',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
actionBar: {
|
||||
type: 'void',
|
||||
'x-align': 'left',
|
||||
'x-component': 'ActionBar',
|
||||
'x-use-component-props': 'useListActionBarProps',
|
||||
'x-component-props': {
|
||||
layout: 'one-column',
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
'7se3eremb6i': {
|
||||
type: 'void',
|
||||
title: '{{ t("View") }}',
|
||||
'x-action': 'view',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
},
|
||||
'x-align': 'left',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
title: '{{ t("View record") }}',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
tabs: {
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
tab1: {
|
||||
type: 'void',
|
||||
title: '{{t("Details")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
grid: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
og4zgn4noul: {
|
||||
type: 'void',
|
||||
title: '{{ t("Edit") }}',
|
||||
'x-action': 'update',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
icon: 'EditOutlined',
|
||||
},
|
||||
'x-align': 'left',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
title: '{{ t("Edit record") }}',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
tabs: {
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
tab1: {
|
||||
type: 'void',
|
||||
title: '{{t("Edit")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
grid: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
plugins: [BlockSchemaComponentPlugin],
|
||||
scopes: {},
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('ID')).toBeInTheDocument();
|
||||
expect(screen.queryByText('1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Nickname')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Super Admin')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('View')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Edit')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText('View'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('tab')).toHaveTextContent('Details');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default () => {
|
||||
return <div>TODO</div>;
|
||||
};
|
@ -6,5 +6,3 @@ group:
|
||||
# List
|
||||
|
||||
## Example
|
||||
|
||||
<code src="./demos/demo1.tsx"></code>
|
||||
|
@ -1,3 +1,6 @@
|
||||
/* istanbul ignore file -- @preserve */
|
||||
// 因为这里有 commonjs,在 vitest 下会报错,所以忽略这个文件
|
||||
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import Mermaid from 'mermaid';
|
||||
|
||||
|
@ -0,0 +1,122 @@
|
||||
import { BlockSchemaComponentPlugin } from '@nocobase/client';
|
||||
import { screen, renderApp, renderReadPrettyApp, userEvent, waitFor } from '@nocobase/test/client';
|
||||
|
||||
describe('NanoIDInput', () => {
|
||||
test('basic', async () => {
|
||||
await renderApp({
|
||||
designable: true,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
dataSource: 'main',
|
||||
collection: 'interfaces',
|
||||
},
|
||||
'x-component': 'div',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
'28rbti2f9jx': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-use-component-props': 'useCreateFormBlockProps',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
'nano-iD': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'interfaces.nano-iD',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
plugins: [BlockSchemaComponentPlugin],
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const input: any = screen.queryByRole('textbox');
|
||||
expect(input).toBeInTheDocument();
|
||||
const value = input.value;
|
||||
expect(value).toHaveLength(21);
|
||||
});
|
||||
|
||||
await userEvent.clear(screen.getByRole('textbox'));
|
||||
await userEvent.type(screen.getByRole('textbox'), '123');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Field value size is 21')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.clear(screen.queryByRole('textbox'));
|
||||
await userEvent.type(screen.queryByRole('textbox'), 'rdQ1G9iPEtjR6BpIAPilZ');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Field value size is 21')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('read pretty', async () => {
|
||||
await renderReadPrettyApp({
|
||||
designable: true,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
dataSource: 'main',
|
||||
collection: 'interfaces',
|
||||
},
|
||||
'x-component': 'div',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
'28rbti2f9jx': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-use-component-props': 'useCreateFormBlockProps',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
'nano-iD': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-toolbar': 'FormItemSchemaToolbar',
|
||||
'x-settings': 'fieldSettings:FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
default: 'rdQ1G9iPEtjR6BpIAPilZ',
|
||||
'x-read-pretty': true,
|
||||
'x-collection-field': 'interfaces.nano-iD',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
plugins: [BlockSchemaComponentPlugin],
|
||||
},
|
||||
});
|
||||
|
||||
expect(document.querySelector('.ant-description-input')?.textContent).toBe('rdQ1G9iPEtjR6BpIAPilZ');
|
||||
});
|
||||
});
|
@ -57,7 +57,7 @@ export const FixedBlockWrapper: React.FC = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface FixedBlockProps {
|
||||
export interface FixedBlockProps {
|
||||
height: number | string;
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ const fixedBlockCss = css`
|
||||
}
|
||||
`;
|
||||
|
||||
const FixedBlock: React.FC<FixedBlockProps> = (props) => {
|
||||
export const FixedBlock: React.FC<FixedBlockProps> = (props) => {
|
||||
const { height } = props;
|
||||
const [fixedBlockUID, _setFixedBlock] = useState<false | string>(false);
|
||||
const fixedBlockUIDRef = useRef(fixedBlockUID);
|
||||
|
@ -0,0 +1,101 @@
|
||||
import { screen, checkSettings, renderSettings, checkModal } from '@nocobase/test/client';
|
||||
import { Page } from '../Page';
|
||||
import { pageSettings } from '../Page.Settings';
|
||||
|
||||
describe('Page.Settings', () => {
|
||||
it('should works', async () => {
|
||||
const title = 'title test';
|
||||
await renderSettings({
|
||||
schema: {
|
||||
title,
|
||||
'x-component': Page,
|
||||
},
|
||||
schemaSettings: pageSettings,
|
||||
appOptions: {
|
||||
designable: true,
|
||||
},
|
||||
apis: {
|
||||
'/uiSchemas:insertAdjacent/test?position=beforeEnd': { data: { result: 'ok' } },
|
||||
},
|
||||
});
|
||||
|
||||
await checkSettings([
|
||||
{
|
||||
title: 'Enable page header',
|
||||
type: 'switch',
|
||||
beforeClick() {
|
||||
expect(screen.getByTitle(title)).toBeInTheDocument();
|
||||
},
|
||||
afterFirstClick() {
|
||||
expect(screen.queryByText(title)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Display page title')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Edit page title')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Enable page tabs')).not.toBeInTheDocument();
|
||||
},
|
||||
afterSecondClick() {
|
||||
expect(screen.getByTitle(title)).toBeInTheDocument();
|
||||
expect(screen.getByText('Display page title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Edit page title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Enable page tabs')).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Display page title',
|
||||
type: 'switch',
|
||||
beforeClick() {
|
||||
expect(screen.getByTitle(title)).toBeInTheDocument();
|
||||
},
|
||||
afterFirstClick() {
|
||||
expect(screen.queryByText(title)).not.toBeInTheDocument();
|
||||
},
|
||||
afterSecondClick() {
|
||||
expect(screen.getByTitle(title)).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Edit page title',
|
||||
type: 'modal',
|
||||
modalChecker: {
|
||||
modalTitle: 'Edit page title',
|
||||
formItems: [
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Title',
|
||||
newValue: 'new title',
|
||||
},
|
||||
],
|
||||
afterSubmit() {
|
||||
expect(screen.queryByTitle('new title')).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Enable page tabs',
|
||||
type: 'switch',
|
||||
beforeClick() {
|
||||
expect(screen.queryByText('Add tab')).not.toBeInTheDocument();
|
||||
},
|
||||
async afterFirstClick() {
|
||||
await checkModal({
|
||||
triggerText: 'Add tab',
|
||||
modalTitle: 'Add tab',
|
||||
formItems: [
|
||||
{
|
||||
label: 'Tab name',
|
||||
type: 'input',
|
||||
newValue: 'Tab 1',
|
||||
},
|
||||
],
|
||||
afterSubmit() {
|
||||
expect(screen.queryByRole('tab')).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab')).toHaveTextContent('Tab 1');
|
||||
},
|
||||
});
|
||||
},
|
||||
afterSecondClick() {
|
||||
expect(screen.queryByTitle('Add tab')).not.toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
@ -0,0 +1,54 @@
|
||||
import { screen, checkSettings, renderSettings } from '@nocobase/test/client';
|
||||
import { Page } from '../Page';
|
||||
import { pageTabSettings } from '../PageTab.Settings';
|
||||
|
||||
describe('PageTab.Settings', () => {
|
||||
test('should works', async () => {
|
||||
await renderSettings({
|
||||
container: () => screen.getByRole('tab'),
|
||||
schema: {
|
||||
title: 'page title',
|
||||
'x-component': Page,
|
||||
'x-component-props': {
|
||||
enablePageTabs: true,
|
||||
},
|
||||
properties: {
|
||||
tab1: {
|
||||
'x-component': 'div',
|
||||
title: 'tab1 title',
|
||||
},
|
||||
},
|
||||
},
|
||||
schemaSettings: pageTabSettings,
|
||||
});
|
||||
|
||||
await checkSettings([
|
||||
{
|
||||
title: 'Edit',
|
||||
type: 'modal',
|
||||
modalChecker: {
|
||||
modalTitle: 'Edit tab',
|
||||
formItems: [
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Tab name',
|
||||
oldValue: 'tab1 title',
|
||||
newValue: 'new tab1 title',
|
||||
},
|
||||
],
|
||||
afterSubmit: () => {
|
||||
expect(screen.queryByText('new tab1 title')).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Delete',
|
||||
type: 'delete',
|
||||
modalChecker: {
|
||||
confirmTitle: 'Delete block',
|
||||
},
|
||||
deletedText: 'new tab1 title',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
@ -94,6 +94,9 @@ describe('Page', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
apis: {
|
||||
'/uiSchemas:insertAdjacent/test': { data: { result: 'ok' } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByRole('tablist')).toBeInTheDocument();
|
||||
@ -126,6 +129,9 @@ describe('Page', () => {
|
||||
Grid,
|
||||
},
|
||||
},
|
||||
apis: {
|
||||
'/uiSchemas:insertAdjacent/test?position=beforeEnd': { data: { result: 'ok' } },
|
||||
},
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText('Add tab'));
|
||||
|
@ -0,0 +1,153 @@
|
||||
import { BlockSchemaComponentPlugin } from '@nocobase/client';
|
||||
import { screen, renderApp, sleep, renderReadPrettyApp, userEvent, waitFor } from '@nocobase/test/client';
|
||||
|
||||
describe('QuickEdit', () => {
|
||||
function getRenderOptions(readPretty = false) {
|
||||
return {
|
||||
designable: true,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
dataSource: 'main',
|
||||
collection: 'users',
|
||||
},
|
||||
'x-component': 'div',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
'45i9guirvtz': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-use-component-props': 'useCreateFormBlockProps',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
roles: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'users.roles',
|
||||
'x-component-props': {
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'name',
|
||||
},
|
||||
addMode: 'modalAdd',
|
||||
mode: 'SubTable',
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
default: null,
|
||||
properties: {
|
||||
e2l1f5wo2st: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'AssociationField.SubTable',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
'9x9jysv3hka': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'TableV2.Column.Decorator',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
'long-text': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
default: readPretty ? 'aaa' : null,
|
||||
'x-collection-field': 'roles.long-text',
|
||||
'x-component': 'CollectionField',
|
||||
'x-component-props': {
|
||||
ellipsis: true,
|
||||
},
|
||||
'x-decorator': 'QuickEdit',
|
||||
'x-decorator-props': {
|
||||
labelStyle: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-read-pretty': readPretty,
|
||||
'x-disabled': false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
uwe6lq47y0t: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
'x-action': 'create',
|
||||
title: "{{t('Add new')}}",
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
type: 'default',
|
||||
component: 'CreateRecordAction',
|
||||
},
|
||||
type: 'void',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
plugins: [BlockSchemaComponentPlugin],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it('basic', async () => {
|
||||
await renderApp(getRenderOptions());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.ant-table-footer button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(document.querySelector('.ant-table-footer button'));
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.ant-table-row')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(document.querySelector('.ant-description-textarea'));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('textbox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.type(screen.queryByRole('textbox'), 'hello world');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.ant-description-textarea')).toHaveTextContent('hello world');
|
||||
});
|
||||
});
|
||||
|
||||
it('read pretty', async () => {
|
||||
await renderApp(getRenderOptions(true));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.ant-table-footer button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(document.querySelector('.ant-table-footer button'));
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.ant-table-row')).toBeInTheDocument();
|
||||
expect(document.querySelector('.ant-description-textarea')).toHaveTextContent('aaa');
|
||||
});
|
||||
|
||||
await userEvent.click(document.querySelector('.ant-description-textarea'));
|
||||
await sleep(100);
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -4,7 +4,7 @@ group:
|
||||
order: 3
|
||||
---
|
||||
|
||||
# RecordPicker <Badge>待定</Badge>
|
||||
# RecordPicker
|
||||
|
||||
## JSON Schema
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { APIClientProvider, FormProvider, RemoteSelect, SchemaComponent } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { mockAPIClient } from '../../../../testUtils';
|
||||
import { sleep } from '@nocobase/test/client';
|
||||
import { sleep } from '@nocobase/test/web';
|
||||
|
||||
const { apiClient, mockRequest } = mockAPIClient();
|
||||
mockRequest.onGet('/posts:list').reply(async () => {
|
||||
|
@ -0,0 +1,308 @@
|
||||
import { BlockSchemaComponentPlugin, TableV2, useTableBlockDecoratorProps } from '@nocobase/client';
|
||||
import { checkSettings, renderSettings, checkSchema, screen, waitFor } from '@nocobase/test/client';
|
||||
import { withSchema } from '@nocobase/test/web';
|
||||
|
||||
describe('Table.Column.settings', () => {
|
||||
const TableColumnDecoratorWithSchema = withSchema(TableV2.Column.Decorator);
|
||||
|
||||
const getRenderOptions = (isOld?: boolean, field = 'nickname') => {
|
||||
const schema = isOld
|
||||
? {
|
||||
'x-designer': 'TableV2.Column.Designer',
|
||||
}
|
||||
: {
|
||||
'x-toolbar': 'TableColumnSchemaToolbar',
|
||||
'x-settings': 'fieldSettings:TableColumn',
|
||||
};
|
||||
return {
|
||||
designable: true,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'TableBlockProvider',
|
||||
'x-use-decorator-props': 'useTableBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
dataSource: 'main',
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 20,
|
||||
},
|
||||
rowKey: 'id',
|
||||
showIndex: true,
|
||||
dragSort: false,
|
||||
},
|
||||
'x-component': 'div',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
f8bvd77sp6p: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'array',
|
||||
'x-component': 'TableV2',
|
||||
'x-use-component-props': 'useTableBlockProps',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
ct00e0xr996: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'TableColumnDecoratorWithSchema',
|
||||
...schema,
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
[field]: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
'x-collection-field': `users.${field}`,
|
||||
'x-component': 'CollectionField',
|
||||
'x-component-props': {
|
||||
ellipsis: true,
|
||||
},
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': null,
|
||||
'x-decorator-props': {
|
||||
labelStyle: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-async': false,
|
||||
'x-index': 3,
|
||||
},
|
||||
},
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
appOptions: {
|
||||
components: {
|
||||
TableColumnDecoratorWithSchema,
|
||||
},
|
||||
plugins: [BlockSchemaComponentPlugin],
|
||||
scopes: {
|
||||
useTableBlockDecoratorProps,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const checkCommonField = () => {
|
||||
return checkSettings([
|
||||
{
|
||||
type: 'modal',
|
||||
title: 'Custom column title',
|
||||
modalChecker: {
|
||||
modalTitle: 'Custom column title',
|
||||
formItems: [
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Column title',
|
||||
newValue: 'test',
|
||||
},
|
||||
],
|
||||
async afterSubmit() {
|
||||
await checkSchema({
|
||||
title: 'test',
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'modal',
|
||||
title: 'Column width',
|
||||
modalChecker: {
|
||||
modalTitle: 'Column width',
|
||||
formItems: [
|
||||
{
|
||||
type: 'number',
|
||||
newValue: '300',
|
||||
},
|
||||
],
|
||||
async afterSubmit() {
|
||||
await checkSchema({
|
||||
'x-component-props': {
|
||||
width: 300,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
title: 'Sortable',
|
||||
async afterFirstClick() {
|
||||
await checkSchema({
|
||||
'x-component-props': {
|
||||
sorter: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
async afterSecondClick() {
|
||||
await checkSchema({
|
||||
'x-component-props': {
|
||||
sorter: false,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const checkAssociationField = () => {
|
||||
return checkSettings([
|
||||
{
|
||||
type: 'modal',
|
||||
title: 'Custom column title',
|
||||
modalChecker: {
|
||||
modalTitle: 'Custom column title',
|
||||
formItems: [
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Column title',
|
||||
newValue: 'test',
|
||||
},
|
||||
],
|
||||
async afterSubmit() {
|
||||
await checkSchema({
|
||||
title: 'test',
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'modal',
|
||||
title: 'Column width',
|
||||
modalChecker: {
|
||||
modalTitle: 'Column width',
|
||||
formItems: [
|
||||
{
|
||||
type: 'number',
|
||||
newValue: '300',
|
||||
},
|
||||
],
|
||||
async afterSubmit() {
|
||||
await checkSchema({
|
||||
'x-component-props': {
|
||||
width: 300,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
title: 'Enable link',
|
||||
async afterFirstClick() {
|
||||
expect(screen.queryByText('Admin').tagName).toBe('SPAN');
|
||||
},
|
||||
async afterSecondClick() {
|
||||
expect(screen.queryByText('Admin').tagName).toBe('A');
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
title: 'Field component',
|
||||
oldValue: 'Title',
|
||||
options: [
|
||||
{
|
||||
label: 'Tag',
|
||||
async checker() {
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Admin')).toHaveClass('ant-tag');
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Title',
|
||||
async checker() {
|
||||
await waitFor(() => {
|
||||
const el = screen.queryByText('Admin');
|
||||
expect(el?.tagName).toBe('A');
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Tag',
|
||||
async checker() {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
title: 'Tag color field',
|
||||
options: [
|
||||
{
|
||||
label: 'color',
|
||||
async checker() {
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Admin')).toHaveStyle('background-color: rgb(22, 119, 255);');
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
title: 'Title field',
|
||||
options: [
|
||||
{
|
||||
label: 'Role UID',
|
||||
async checker() {
|
||||
expect(screen.queryByText('admin')).toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Role name',
|
||||
async checker() {
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Admin')).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
describe('new version schema', () => {
|
||||
test('common field', async () => {
|
||||
await renderSettings(getRenderOptions());
|
||||
await checkCommonField();
|
||||
});
|
||||
|
||||
test('association field', async () => {
|
||||
await renderSettings(getRenderOptions(false, 'roles'));
|
||||
await checkAssociationField();
|
||||
});
|
||||
});
|
||||
|
||||
describe('old version schema', () => {
|
||||
test('common field', async () => {
|
||||
await renderSettings(getRenderOptions(true));
|
||||
await checkCommonField();
|
||||
});
|
||||
|
||||
test('association field', async () => {
|
||||
await renderSettings(getRenderOptions(true, 'roles'));
|
||||
await checkAssociationField();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,307 @@
|
||||
import {
|
||||
BlockSchemaComponentPlugin,
|
||||
FixedBlock,
|
||||
TableBlockProvider,
|
||||
useTableBlockDecoratorProps,
|
||||
} from '@nocobase/client';
|
||||
import {
|
||||
checkSettings,
|
||||
renderSettings,
|
||||
checkSchema,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
CheckSettingsOptions,
|
||||
} from '@nocobase/test/client';
|
||||
import { withSchema } from '@nocobase/test/web';
|
||||
|
||||
describe('Table.settings', () => {
|
||||
const TableBlockProviderWithSchema = withSchema(TableBlockProvider);
|
||||
|
||||
const checkTableSettings = (more: CheckSettingsOptions[] = []) => {
|
||||
return checkSettings(
|
||||
[
|
||||
{
|
||||
title: 'Edit block title',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Enable drag and drop sorting',
|
||||
type: 'switch',
|
||||
async afterFirstClick() {
|
||||
await checkSchema({
|
||||
'x-decorator-props': {
|
||||
dragSort: true,
|
||||
},
|
||||
});
|
||||
expect(screen.queryByText('Drag and drop sorting field')).toBeInTheDocument();
|
||||
|
||||
await checkSettings([
|
||||
{
|
||||
title: 'Drag and drop sorting field',
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: 'sort',
|
||||
async checker() {
|
||||
await checkSchema({
|
||||
'x-decorator-props': {
|
||||
dragSortBy: 'sortName',
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
},
|
||||
async afterSecondClick() {
|
||||
await checkSchema({
|
||||
'x-decorator-props': {
|
||||
dragSort: false,
|
||||
},
|
||||
});
|
||||
expect(screen.queryByText('Drag and drop sorting field')).not.toBeInTheDocument();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Fix block',
|
||||
type: 'switch',
|
||||
async afterFirstClick() {
|
||||
await checkSchema({
|
||||
'x-decorator-props': {
|
||||
fixedBlock: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
async afterSecondClick() {
|
||||
await checkSchema({
|
||||
'x-decorator-props': {
|
||||
fixedBlock: false,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Set the data scope',
|
||||
type: 'modal',
|
||||
modalChecker: {
|
||||
modalTitle: 'Set the data scope',
|
||||
async beforeCheck() {
|
||||
await userEvent.click(screen.getByText('Add condition'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('select-filter-field')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const field = screen.queryByTestId('select-filter-field').querySelector('input');
|
||||
|
||||
await userEvent.click(field);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTitle('ID')).toBeInTheDocument();
|
||||
});
|
||||
await userEvent.click(screen.getByTitle('ID'));
|
||||
|
||||
const value = document.querySelector('input[role=spinbutton]');
|
||||
await userEvent.type(value, '1');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('input[role=spinbutton]')).toHaveValue('1');
|
||||
});
|
||||
},
|
||||
async afterSubmit() {
|
||||
await checkSchema({
|
||||
'x-decorator-props': {
|
||||
params: {
|
||||
filter: {
|
||||
$and: [
|
||||
{
|
||||
id: {
|
||||
$eq: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Set default sorting rules',
|
||||
type: 'modal',
|
||||
modalChecker: {
|
||||
modalTitle: 'Set default sorting rules',
|
||||
contentText: 'Add sort field',
|
||||
async beforeCheck() {
|
||||
await userEvent.click(screen.getByText('Add sort field'));
|
||||
const dialog = screen.getByRole('dialog');
|
||||
await waitFor(() => {
|
||||
expect(dialog.querySelector('.ant-select-selector')).toBeInTheDocument();
|
||||
});
|
||||
await userEvent.click(dialog.querySelector('.ant-select-selector'));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('ID')).toBeInTheDocument();
|
||||
});
|
||||
await userEvent.click(screen.getByText('ID'));
|
||||
|
||||
await userEvent.click(screen.getByText('DESC'));
|
||||
},
|
||||
async afterSubmit() {
|
||||
await checkSchema({
|
||||
'x-decorator-props': {
|
||||
params: {
|
||||
sort: ['-id'],
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Set data loading mode',
|
||||
type: 'modal',
|
||||
modalChecker: {
|
||||
modalTitle: 'Data loading mode',
|
||||
async beforeCheck() {
|
||||
await userEvent.click(screen.getByText('Load data after filtering'));
|
||||
},
|
||||
async afterSubmit() {
|
||||
await checkSchema({
|
||||
'x-decorator-props': {
|
||||
dataLoadingMode: 'manual',
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Records per page',
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: '10',
|
||||
async checker() {
|
||||
await checkSchema({
|
||||
'x-decorator-props': {
|
||||
params: {
|
||||
pageSize: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '20',
|
||||
},
|
||||
{
|
||||
label: '50',
|
||||
},
|
||||
{
|
||||
label: '100',
|
||||
},
|
||||
{
|
||||
label: '100',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Save as template',
|
||||
type: 'modal',
|
||||
},
|
||||
{
|
||||
title: 'Delete',
|
||||
type: 'delete',
|
||||
},
|
||||
...more,
|
||||
],
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
const getRenderSettingsOptions = (isOld?: boolean, collection = 'users') => {
|
||||
const toolbarSchema = isOld
|
||||
? {
|
||||
'x-designer': 'TableBlockDesigner',
|
||||
}
|
||||
: {
|
||||
'x-toolbar': 'BlockSchemaToolbar',
|
||||
'x-settings': 'blockSettings:table',
|
||||
};
|
||||
|
||||
return {
|
||||
designable: true,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-component': 'FixedBlock',
|
||||
properties: {
|
||||
table: {
|
||||
type: 'void',
|
||||
'x-decorator': 'TableBlockProviderWithSchema',
|
||||
'x-use-decorator-props': 'useTableBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
collection: collection,
|
||||
dataSource: 'main',
|
||||
action: 'list',
|
||||
rowKey: 'id',
|
||||
showIndex: true,
|
||||
dragSort: false,
|
||||
params: {
|
||||
pageSize: 20,
|
||||
},
|
||||
},
|
||||
...toolbarSchema,
|
||||
'x-component': 'CardItem',
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
components: {
|
||||
TableBlockProviderWithSchema,
|
||||
FixedBlock,
|
||||
},
|
||||
plugins: [BlockSchemaComponentPlugin],
|
||||
scopes: {
|
||||
useTableBlockDecoratorProps,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
test('menu list', async () => {
|
||||
await renderSettings(getRenderSettingsOptions());
|
||||
await checkTableSettings();
|
||||
});
|
||||
|
||||
test('old schema', async () => {
|
||||
await renderSettings(getRenderSettingsOptions(true));
|
||||
await checkTableSettings();
|
||||
});
|
||||
|
||||
test('tree collection', async () => {
|
||||
await renderSettings(getRenderSettingsOptions(false, 'tree'));
|
||||
await checkSettings([
|
||||
{
|
||||
title: 'Tree table',
|
||||
type: 'switch',
|
||||
async afterFirstClick() {
|
||||
await checkSchema({
|
||||
'x-decorator-props': {
|
||||
treeTable: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
async afterSecondClick() {
|
||||
await checkSchema({
|
||||
'x-decorator-props': {
|
||||
treeTable: false,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
@ -0,0 +1,118 @@
|
||||
import {
|
||||
FixedBlock,
|
||||
BlockSchemaComponentPlugin,
|
||||
SchemaInitializerPlugin,
|
||||
TableBlockProvider,
|
||||
tableActionColumnInitializers,
|
||||
tableActionInitializers,
|
||||
tableColumnInitializers,
|
||||
useTableBlockDecoratorProps,
|
||||
} from '@nocobase/client';
|
||||
|
||||
export const tableOptions = {
|
||||
designable: true,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-component': 'FixedBlock',
|
||||
properties: {
|
||||
table: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'TableBlockProvider',
|
||||
'x-use-decorator-props': 'useTableBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
dataSource: 'main',
|
||||
action: 'list',
|
||||
rowKey: 'id',
|
||||
showIndex: true,
|
||||
dragSort: false,
|
||||
},
|
||||
'x-toolbar': 'BlockSchemaToolbar',
|
||||
'x-settings': 'blockSettings:table',
|
||||
'x-component': 'CardItem',
|
||||
'x-app-version': '0.21.0-alpha.11',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-initializer': 'table:configureActions',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.11',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
qmew562ea9w: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'array',
|
||||
'x-initializer': 'table:configureColumns',
|
||||
'x-component': 'TableV2',
|
||||
'x-use-component-props': 'useTableBlockProps',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.11',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Actions") }}',
|
||||
'x-action-column': 'actions',
|
||||
'x-decorator': 'TableV2.Column.ActionBar',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-designer': 'TableV2.ActionColumnDesigner',
|
||||
'x-initializer': 'table:configureItemActions',
|
||||
'x-app-version': '0.21.0-alpha.11',
|
||||
properties: {
|
||||
glmtz7t8dm4: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'DndContext',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.11',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
components: {
|
||||
TableBlockProvider,
|
||||
FixedBlock,
|
||||
},
|
||||
plugins: [BlockSchemaComponentPlugin, SchemaInitializerPlugin],
|
||||
schemaInitializers: [tableActionInitializers, tableColumnInitializers, tableActionColumnInitializers],
|
||||
scopes: {
|
||||
useTableBlockDecoratorProps,
|
||||
},
|
||||
},
|
||||
};
|
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
@ -3,6 +3,9 @@ import React from 'react';
|
||||
import App1 from '../demos/demo1';
|
||||
import App2 from '../demos/demo2';
|
||||
|
||||
import { BlockSchemaComponentPlugin } from '@nocobase/client';
|
||||
import { screen, renderApp, renderReadPrettyApp, userEvent, waitFor } from '@nocobase/test/client';
|
||||
|
||||
describe('Upload', () => {
|
||||
it('basic', () => {
|
||||
render(<App1 />);
|
||||
@ -11,4 +14,276 @@ describe('Upload', () => {
|
||||
it('uploading', () => {
|
||||
render(<App2 />);
|
||||
});
|
||||
|
||||
it('upload single', async () => {
|
||||
await renderApp({
|
||||
designable: true,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
dataSource: 'main',
|
||||
collection: 'interfaces',
|
||||
},
|
||||
'x-component': 'div',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
'28rbti2f9jx': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-use-component-props': 'useCreateFormBlockProps',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
attachment: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-read-pretty': false,
|
||||
'x-collection-field': 'interfaces.attachment',
|
||||
'x-component-props': {
|
||||
action: 'attachments:create',
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-disabled': false,
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
plugins: [BlockSchemaComponentPlugin],
|
||||
},
|
||||
apis: {
|
||||
'attachments:create': {
|
||||
data: {
|
||||
id: 3,
|
||||
title: '微信图片_20240131154451',
|
||||
filename: '234ead512e44bf944689069ce2b41a95.png',
|
||||
extname: '.png',
|
||||
path: '',
|
||||
size: 841380,
|
||||
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
mimetype: 'image/png',
|
||||
meta: {},
|
||||
storageId: 1,
|
||||
updatedAt: '2024-04-21T01:26:02.961Z',
|
||||
createdAt: '2024-04-21T01:26:02.961Z',
|
||||
createdById: 1,
|
||||
updatedById: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const file = new File(['hello'], './hello.png', { type: 'image/png' });
|
||||
|
||||
await waitFor(() => {
|
||||
const input = document.querySelector('input[type="file"]');
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(document.querySelector('.ant-upload'));
|
||||
await userEvent.upload(document.querySelector('input[type="file"]'), file);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.ant-upload-list-item-image')).toBeInTheDocument();
|
||||
expect(document.querySelector('.ant-upload-list-item-image')).toHaveAttribute(
|
||||
'src',
|
||||
'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
);
|
||||
});
|
||||
|
||||
// upload another file
|
||||
await userEvent.click(document.querySelector('.ant-upload'));
|
||||
await userEvent.upload(document.querySelector('input[type="file"]'), file);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelectorAll('.ant-upload-list-item-image')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('upload multi', async () => {
|
||||
await renderApp({
|
||||
designable: true,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
dataSource: 'main',
|
||||
collection: 'interfaces',
|
||||
},
|
||||
'x-component': 'div',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
'28rbti2f9jx': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-use-component-props': 'useCreateFormBlockProps',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
attachment: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-read-pretty': false,
|
||||
'x-collection-field': 'interfaces.attachment',
|
||||
'x-component-props': {
|
||||
action: 'attachments:create',
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-disabled': false,
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
plugins: [BlockSchemaComponentPlugin],
|
||||
},
|
||||
apis: {
|
||||
'attachments:create': {
|
||||
data: {
|
||||
id: 3,
|
||||
title: '微信图片_20240131154451',
|
||||
filename: '234ead512e44bf944689069ce2b41a95.png',
|
||||
extname: '.png',
|
||||
path: '',
|
||||
size: 841380,
|
||||
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
mimetype: 'image/png',
|
||||
meta: {},
|
||||
storageId: 1,
|
||||
updatedAt: '2024-04-21T01:26:02.961Z',
|
||||
createdAt: '2024-04-21T01:26:02.961Z',
|
||||
createdById: 1,
|
||||
updatedById: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const files = [
|
||||
new File(['hello1'], './hello.png', { type: 'image/png' }),
|
||||
new File(['hello2'], './hello.png', { type: 'image/png' }),
|
||||
];
|
||||
|
||||
await waitFor(() => {
|
||||
const input = document.querySelector('input[type="file"]');
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(document.querySelector('.ant-upload'));
|
||||
await userEvent.upload(document.querySelector('input[type="file"]'), files);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelectorAll('.ant-upload-list-item-image')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('delete', async () => {
|
||||
await renderApp({
|
||||
designable: true,
|
||||
enableUserListDataBlock: true,
|
||||
schema: {
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
dataSource: 'main',
|
||||
collection: 'interfaces',
|
||||
},
|
||||
'x-component': 'div',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
'28rbti2f9jx': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-use-component-props': 'useCreateFormBlockProps',
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
properties: {
|
||||
attachment: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
default: {
|
||||
id: 1,
|
||||
title: '微信图片_20240131154451',
|
||||
filename: '234ead512e44bf944689069ce2b41a95.png',
|
||||
extname: '.png',
|
||||
path: '',
|
||||
size: 841380,
|
||||
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
mimetype: 'image/png',
|
||||
meta: {},
|
||||
storageId: 1,
|
||||
updatedAt: '2024-04-21T01:26:02.961Z',
|
||||
createdAt: '2024-04-21T01:26:02.961Z',
|
||||
createdById: 1,
|
||||
updatedById: 1,
|
||||
},
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-read-pretty': false,
|
||||
'x-collection-field': 'interfaces.attachment',
|
||||
'x-component-props': {
|
||||
action: 'attachments:create',
|
||||
},
|
||||
'x-app-version': '0.21.0-alpha.10',
|
||||
'x-disabled': false,
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
appOptions: {
|
||||
plugins: [BlockSchemaComponentPlugin],
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.ant-upload-list-item-image')).toBeInTheDocument();
|
||||
expect(document.querySelector('.ant-upload-list-item-image')).toHaveAttribute(
|
||||
'src',
|
||||
'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
);
|
||||
});
|
||||
|
||||
await userEvent.click(document.querySelector('.anticon-delete'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.ant-upload-list-item-image')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,4 +4,4 @@ group:
|
||||
order: 3
|
||||
---
|
||||
|
||||
# DndContext <Badge>待定</Badge>
|
||||
# DndContext
|
||||
|
@ -13,11 +13,13 @@ export const useGetAriaLabelOfSchemaInitializer = () => {
|
||||
const getAriaLabel = useCallback(
|
||||
(postfix?: string) => {
|
||||
if (!fieldSchema) return '';
|
||||
const component = fieldSchema['x-component'];
|
||||
const componentStr = typeof component === 'string' ? component : component?.displayName || component.name;
|
||||
const initializer = fieldSchema['x-initializer'] ? `-${fieldSchema['x-initializer']}` : '';
|
||||
const collectionName = name ? `-${name}` : '';
|
||||
postfix = postfix ? `-${postfix}` : '';
|
||||
|
||||
return `schema-initializer-${fieldSchema['x-component']}${initializer}${collectionName}${postfix}`;
|
||||
return `schema-initializer-${componentStr}${initializer}${collectionName}${postfix}`;
|
||||
},
|
||||
[fieldSchema, name],
|
||||
);
|
||||
|
@ -54,34 +54,30 @@ import {
|
||||
createDesignable,
|
||||
findFormBlock,
|
||||
useAPIClient,
|
||||
useBlockRequestContext,
|
||||
useCollectionManager_deprecated,
|
||||
useCollectionRecord,
|
||||
useCollection_deprecated,
|
||||
useCompile,
|
||||
useDataBlockProps,
|
||||
useDesignable,
|
||||
useFilterBlock,
|
||||
useGlobalTheme,
|
||||
useLinkageCollectionFilterOptions,
|
||||
useRecord,
|
||||
useSortFields,
|
||||
} from '..';
|
||||
import {
|
||||
BlockContext,
|
||||
BlockRequestContext_deprecated,
|
||||
FormBlockContext,
|
||||
useBlockContext,
|
||||
useFormBlockContext,
|
||||
useFormBlockType,
|
||||
useTableBlockContext,
|
||||
} from '../block-provider';
|
||||
import { FormBlockContext, useFormBlockContext, useFormBlockType, useTableBlockContext } from '../block-provider';
|
||||
import {
|
||||
FormActiveFieldsProvider,
|
||||
findFilterTargets,
|
||||
updateFilterTargets,
|
||||
useFormActiveFields,
|
||||
} from '../block-provider/hooks';
|
||||
import {
|
||||
useBlockRequestContext,
|
||||
BlockRequestContext_deprecated,
|
||||
useBlockContext,
|
||||
BlockContext,
|
||||
} from '../block-provider/BlockProvider';
|
||||
import { SelectWithTitle, SelectWithTitleProps } from '../common/SelectWithTitle';
|
||||
import { useNiceDropdownMaxHeight } from '../common/useNiceDropdownHeight';
|
||||
import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider';
|
||||
@ -93,6 +89,7 @@ import {
|
||||
isSameCollection,
|
||||
useSupportedBlocks,
|
||||
} from '../filter-provider/utils';
|
||||
import { useFilterBlock } from '../filter-provider/FilterProvider';
|
||||
import { FlagProvider } from '../flag-provider';
|
||||
import { useCollectMenuItem, useCollectMenuItems, useMenuItem } from '../hooks/useMenuItem';
|
||||
import { DeclareVariable } from '../modules/variable/DeclareVariable';
|
||||
@ -187,7 +184,13 @@ export const SchemaSettingsDropdown: React.FC<SchemaSettingsProps> = (props) =>
|
||||
overflow-y: auto;
|
||||
}
|
||||
`}
|
||||
menu={{ items, style: { maxHeight: dropdownMaxHeight, overflowY: 'auto' } }}
|
||||
menu={
|
||||
{
|
||||
items,
|
||||
'data-testid': 'schema-settings-menu',
|
||||
style: { maxHeight: dropdownMaxHeight, overflowY: 'auto' },
|
||||
} as any
|
||||
}
|
||||
>
|
||||
<div data-testid={props['data-testid']}>{typeof title === 'string' ? <span>{title}</span> : title}</div>
|
||||
</Dropdown>
|
||||
|
@ -14,13 +14,14 @@ export const useGetAriaLabelOfDesigner = () => {
|
||||
if (!fieldSchema) return '';
|
||||
|
||||
const component = fieldSchema['x-component'];
|
||||
const componentName = typeof component === 'string' ? component : component?.displayName || component?.name;
|
||||
const designer = fieldSchema['x-designer'] ? `-${fieldSchema['x-designer']}` : '';
|
||||
const settings = fieldSchema['x-settings'] ? `-${fieldSchema['x-settings']}` : '';
|
||||
const collectionField = fieldSchema['x-collection-field'] ? `-${fieldSchema['x-collection-field']}` : '';
|
||||
const collectionName = _collectionName ? `-${_collectionName}` : '';
|
||||
postfix = postfix ? `-${postfix}` : '';
|
||||
|
||||
return `designer-${name}-${component}${designer}${settings}${collectionName}${collectionField}${postfix}`;
|
||||
return `designer-${name}-${componentName}${designer}${settings}${collectionName}${collectionField}${postfix}`;
|
||||
},
|
||||
[fieldSchema, _collectionName],
|
||||
);
|
||||
|
@ -0,0 +1,29 @@
|
||||
import { waitFor, screen } from '@testing-library/react';
|
||||
import { checkSettings } from '../settingsChecker';
|
||||
import { expectNoTsError } from '../utils';
|
||||
|
||||
export async function checkBlockTitle(oldValue?: string) {
|
||||
const newValue = 'new test';
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'modal',
|
||||
title: 'Edit block title',
|
||||
modalChecker: {
|
||||
modalTitle: 'Edit block title',
|
||||
formItems: [
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Block title',
|
||||
oldValue,
|
||||
newValue,
|
||||
},
|
||||
],
|
||||
async afterSubmit() {
|
||||
await waitFor(() => {
|
||||
expectNoTsError(screen.queryByText(newValue)).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import { waitFor, screen } from '@testing-library/react';
|
||||
import { checkSettings } from '../settingsChecker';
|
||||
import { expectNoTsError } from '../utils';
|
||||
|
||||
export async function checkFieldTitle(oldValue?: string) {
|
||||
const newValue = 'new test';
|
||||
await checkSettings([
|
||||
{
|
||||
type: 'modal',
|
||||
title: 'Edit field title',
|
||||
modalChecker: {
|
||||
modalTitle: 'Edit field title',
|
||||
formItems: [
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Field title',
|
||||
oldValue,
|
||||
newValue,
|
||||
},
|
||||
],
|
||||
async afterSubmit() {
|
||||
await waitFor(() => {
|
||||
expectNoTsError(screen.queryByText(newValue)).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './blockTitle';
|
||||
export * from './fieldTitle';
|
@ -0,0 +1,23 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { CommonFormItemCheckerOptions, getFormItemElement } from './common';
|
||||
import { expectNoTsError } from '../utils';
|
||||
|
||||
export interface CollectionFieldCheckOptions extends CommonFormItemCheckerOptions {
|
||||
field: string;
|
||||
}
|
||||
|
||||
export async function collectionFieldChecker(options: CollectionFieldCheckOptions) {
|
||||
const formItem = getFormItemElement({ Component: 'CollectionField', label: options.field, ...options });
|
||||
|
||||
const input = formItem.querySelector('input');
|
||||
|
||||
if (options.oldValue) {
|
||||
expectNoTsError(input).toHaveValue(options.oldValue);
|
||||
}
|
||||
|
||||
if (options.newValue) {
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, options.newValue);
|
||||
}
|
||||
}
|
24
packages/core/test/src/client/formItemChecker/common.tsx
Normal file
24
packages/core/test/src/client/formItemChecker/common.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { expectNoTsError } from '../utils';
|
||||
|
||||
export interface CommonFormItemCheckerOptions {
|
||||
label?: string;
|
||||
container?: HTMLElement;
|
||||
newValue?: any;
|
||||
oldValue?: any;
|
||||
Component?: string;
|
||||
}
|
||||
|
||||
export interface GetFormItemElementOptions {
|
||||
container?: HTMLElement;
|
||||
Component: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function getFormItemElement({ container = document.body, Component, label }: GetFormItemElementOptions) {
|
||||
const preSelector = `div[aria-label^="block-item-${Component}-"]`;
|
||||
const selector = label ? `${preSelector}[aria-label$="${label}"]` : preSelector;
|
||||
const formItem = container.querySelector(selector);
|
||||
expectNoTsError(formItem).toBeInTheDocument();
|
||||
|
||||
return formItem;
|
||||
}
|
32
packages/core/test/src/client/formItemChecker/icon.tsx
Normal file
32
packages/core/test/src/client/formItemChecker/icon.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { CommonFormItemCheckerOptions, getFormItemElement } from './common';
|
||||
import { waitFor, screen } from '@testing-library/react';
|
||||
import { expectNoTsError } from '../utils';
|
||||
|
||||
export type IconCheckOptions = CommonFormItemCheckerOptions;
|
||||
|
||||
export async function iconChecker(options: IconCheckOptions) {
|
||||
const formItem = getFormItemElement({ Component: 'IconPicker', ...options });
|
||||
|
||||
if (options.oldValue) {
|
||||
expectNoTsError(formItem.querySelector('span[role=img]')).toHaveAttribute('aria-label', options.oldValue);
|
||||
}
|
||||
|
||||
if (options.newValue) {
|
||||
await userEvent.click(formItem.querySelector('span[role=img]') || formItem.querySelector('button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expectNoTsError(screen.queryByRole('tooltip')).toBeInTheDocument();
|
||||
expectNoTsError(screen.getByRole('tooltip').querySelector('.ant-popover-title')).toHaveTextContent('Icon');
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole('tooltip').querySelector(`span[aria-label="${options.newValue}"]`));
|
||||
|
||||
await waitFor(() => {
|
||||
expectNoTsError(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expectNoTsError(formItem.querySelector('span[role=img]')).toHaveAttribute('aria-label', options.newValue);
|
||||
}
|
||||
}
|
35
packages/core/test/src/client/formItemChecker/index.ts
Normal file
35
packages/core/test/src/client/formItemChecker/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { iconChecker, IconCheckOptions } from './icon';
|
||||
import { radioChecker, RadioCheckOptions } from './radio';
|
||||
import { inputChecker, InputCheckOptions } from './input';
|
||||
import { numberChecker, NumberCheckOptions } from './number';
|
||||
import { textareaChecker, TextareaCheckOptions } from './textarea';
|
||||
import { CollectionFieldCheckOptions, collectionFieldChecker } from './collectionField';
|
||||
|
||||
export * from './icon';
|
||||
export * from './radio';
|
||||
export * from './icon';
|
||||
|
||||
export type FormItemCheckOptions =
|
||||
| ({ type: 'icon' } & IconCheckOptions)
|
||||
| ({ type: 'radio' } & RadioCheckOptions)
|
||||
| ({ type: 'collectionField' } & CollectionFieldCheckOptions)
|
||||
| ({ type: 'input' } & InputCheckOptions)
|
||||
| ({ type: 'number' } & NumberCheckOptions)
|
||||
| ({ type: 'textarea' } & TextareaCheckOptions);
|
||||
|
||||
const checkers = {
|
||||
icon: iconChecker,
|
||||
radio: radioChecker,
|
||||
input: inputChecker,
|
||||
collectionField: collectionFieldChecker,
|
||||
number: numberChecker,
|
||||
textarea: textareaChecker,
|
||||
};
|
||||
|
||||
export async function checkFormItems(list: FormItemCheckOptions[]) {
|
||||
for (const item of list) {
|
||||
const type = item.type;
|
||||
const checker = checkers[type];
|
||||
await checker(item as any);
|
||||
}
|
||||
}
|
21
packages/core/test/src/client/formItemChecker/input.tsx
Normal file
21
packages/core/test/src/client/formItemChecker/input.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { CommonFormItemCheckerOptions, getFormItemElement } from './common';
|
||||
import { expectNoTsError } from '../utils';
|
||||
|
||||
export type InputCheckOptions = CommonFormItemCheckerOptions;
|
||||
|
||||
export async function inputChecker(options: InputCheckOptions) {
|
||||
const formItem = getFormItemElement({ Component: 'Input', ...options });
|
||||
|
||||
const input = formItem.querySelector('input');
|
||||
|
||||
if (options.oldValue) {
|
||||
expectNoTsError(input).toHaveValue(options.oldValue);
|
||||
}
|
||||
|
||||
if (options.newValue) {
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, options.newValue);
|
||||
}
|
||||
}
|
21
packages/core/test/src/client/formItemChecker/number.ts
Normal file
21
packages/core/test/src/client/formItemChecker/number.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { CommonFormItemCheckerOptions, getFormItemElement } from './common';
|
||||
import { expectNoTsError } from '../utils';
|
||||
|
||||
export type NumberCheckOptions = CommonFormItemCheckerOptions;
|
||||
|
||||
export async function numberChecker(options: NumberCheckOptions) {
|
||||
const formItem = getFormItemElement({ Component: 'InputNumber', ...options });
|
||||
|
||||
const input = formItem.querySelector('input');
|
||||
|
||||
if (options.oldValue) {
|
||||
expectNoTsError(input).toHaveValue(options.oldValue);
|
||||
}
|
||||
|
||||
if (options.newValue) {
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, String(options.newValue));
|
||||
}
|
||||
}
|
29
packages/core/test/src/client/formItemChecker/radio.tsx
Normal file
29
packages/core/test/src/client/formItemChecker/radio.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { CommonFormItemCheckerOptions, getFormItemElement } from './common';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { expectNoTsError } from '../utils';
|
||||
|
||||
export type RadioCheckOptions = CommonFormItemCheckerOptions;
|
||||
|
||||
export async function radioChecker(options: CommonFormItemCheckerOptions) {
|
||||
const formItem = getFormItemElement({ Component: 'Radio.Group', ...options });
|
||||
|
||||
const radioGroup = formItem.querySelector('.ant-radio-group');
|
||||
|
||||
if (options.oldValue) {
|
||||
expectNoTsError(radioGroup.querySelector('.ant-radio-wrapper-checked')).toHaveTextContent(options.oldValue);
|
||||
}
|
||||
|
||||
if (options.newValue) {
|
||||
const el = [...radioGroup.querySelectorAll('.ant-radio-wrapper')].find((el) => el.textContent === options.newValue);
|
||||
|
||||
expectNoTsError(el).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(el);
|
||||
|
||||
await waitFor(() => {
|
||||
expectNoTsError(radioGroup.querySelector('.ant-radio-wrapper-checked')).toHaveTextContent(options.newValue);
|
||||
});
|
||||
}
|
||||
}
|
21
packages/core/test/src/client/formItemChecker/textarea.tsx
Normal file
21
packages/core/test/src/client/formItemChecker/textarea.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { CommonFormItemCheckerOptions, getFormItemElement } from './common';
|
||||
import { expectNoTsError } from '../utils';
|
||||
|
||||
export type TextareaCheckOptions = CommonFormItemCheckerOptions;
|
||||
|
||||
export async function textareaChecker(options: TextareaCheckOptions) {
|
||||
const formItem = getFormItemElement({ Component: 'Input.TextArea', ...options });
|
||||
|
||||
const textarea = formItem.querySelector('textarea');
|
||||
|
||||
if (options.oldValue) {
|
||||
expectNoTsError(textarea).toHaveValue(options.oldValue);
|
||||
}
|
||||
|
||||
if (options.newValue) {
|
||||
await userEvent.clear(textarea);
|
||||
await userEvent.type(textarea, options.newValue);
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
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';
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { sleep } from '../web';
|
||||
|
||||
export * from './utils';
|
||||
export { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
function customRender(ui: React.ReactElement, options = {}) {
|
||||
@ -17,53 +16,11 @@ function customRender(ui: React.ReactElement, options = {}) {
|
||||
export * from '@testing-library/react';
|
||||
export { default as userEvent } from '@testing-library/user-event';
|
||||
// override render export
|
||||
export { customRender as render };
|
||||
export { customRender as render, sleep };
|
||||
|
||||
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<GetAppOptions, 'value' | 'onChange'> {
|
||||
hook: () => any;
|
||||
props?: any;
|
||||
Wrapper?: FC<{ children: React.ReactNode }>;
|
||||
}
|
||||
|
||||
export const renderHookWithApp = async (options: RenderHookOptions) => {
|
||||
const { hook: useHook, props, Wrapper = Fragment, ...otherOptions } = options;
|
||||
const { App } = getApp(otherOptions);
|
||||
const WrapperValue: FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
<App>
|
||||
<Wrapper>{children}</Wrapper>
|
||||
</App>
|
||||
);
|
||||
|
||||
const res = renderHook(() => useHook(), { wrapper: WrapperValue, initialProps: props });
|
||||
|
||||
await WaitApp();
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const renderApp = async (options: GetAppComponentOptions) => {
|
||||
const App = getAppComponent(options);
|
||||
|
||||
const res = render(<App />);
|
||||
|
||||
await WaitApp();
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const renderReadPrettyApp = (options: GetAppComponentOptions) => {
|
||||
return renderApp({ ...options, schema: { ...(options.schema || {}), 'x-read-pretty': true } });
|
||||
};
|
||||
export * from './renderApp';
|
||||
export * from './renderHookWithApp';
|
||||
export * from './renderSettings';
|
||||
export * from './renderSingleSettings';
|
||||
export * from './settingsChecker';
|
||||
export * from './commonSettingsChecker';
|
||||
|
18
packages/core/test/src/client/renderApp.tsx
Normal file
18
packages/core/test/src/client/renderApp.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { GetAppComponentOptions, addXReadPrettyToEachLayer, getAppComponent } from '../web';
|
||||
import { WaitApp } from './utils';
|
||||
|
||||
export const renderApp = async (options: GetAppComponentOptions) => {
|
||||
const App = getAppComponent(options);
|
||||
|
||||
const res = render(<App />);
|
||||
|
||||
await WaitApp();
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const renderReadPrettyApp = (options: GetAppComponentOptions) => {
|
||||
return renderApp({ ...options, schema: addXReadPrettyToEachLayer(options.schema) });
|
||||
};
|
26
packages/core/test/src/client/renderHookWithApp.tsx
Normal file
26
packages/core/test/src/client/renderHookWithApp.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React, { FC, Fragment } from 'react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { GetAppOptions, getApp } from '../web';
|
||||
import { WaitApp } from './utils';
|
||||
|
||||
interface RenderHookOptions extends Omit<GetAppOptions, 'value' | 'onChange'> {
|
||||
hook: () => any;
|
||||
props?: any;
|
||||
Wrapper?: FC<{ children: React.ReactNode }>;
|
||||
}
|
||||
|
||||
export const renderHookWithApp = async (options: RenderHookOptions) => {
|
||||
const { hook: useHook, props, Wrapper = Fragment, ...otherOptions } = options;
|
||||
const { App } = getApp(otherOptions);
|
||||
const WrapperValue: FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
<App>
|
||||
<Wrapper>{children}</Wrapper>
|
||||
</App>
|
||||
);
|
||||
|
||||
const res = renderHook(() => useHook(), { wrapper: WrapperValue, initialProps: props });
|
||||
|
||||
await WaitApp();
|
||||
|
||||
return res;
|
||||
};
|
42
packages/core/test/src/client/renderSettings.tsx
Normal file
42
packages/core/test/src/client/renderSettings.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { waitFor, screen } from '@testing-library/react';
|
||||
import { GetAppComponentOptions } from '../web';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderApp, renderReadPrettyApp } from './renderApp';
|
||||
import { expectNoTsError } from './utils';
|
||||
|
||||
export async function showSettingsMenu(container: HTMLElement | Document = document) {
|
||||
await waitFor(() => {
|
||||
expectNoTsError(container.querySelector('[aria-label^="designer-schema-settings-"]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.hover(container.querySelector('[aria-label^="designer-schema-settings-"]'));
|
||||
|
||||
await waitFor(() => {
|
||||
expectNoTsError(screen.queryByTestId('schema-settings-menu')).toBeInTheDocument();
|
||||
});
|
||||
}
|
||||
|
||||
export interface RenderSettingsOptions extends GetAppComponentOptions {
|
||||
container?: () => HTMLElement;
|
||||
}
|
||||
export const renderSettings = async (options: RenderSettingsOptions = {}) => {
|
||||
const { container = () => document, ...appOptions } = options;
|
||||
const result = await renderApp({ ...appOptions, designable: true });
|
||||
|
||||
const containerElement = container();
|
||||
|
||||
await showSettingsMenu(containerElement);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const renderReadPrettySettings = async (options: RenderSettingsOptions = {}) => {
|
||||
const { container = () => document, ...appOptions } = options;
|
||||
const result = await renderReadPrettyApp({ ...appOptions, designable: true });
|
||||
|
||||
const containerElement = container();
|
||||
|
||||
await showSettingsMenu(containerElement);
|
||||
|
||||
return result;
|
||||
};
|
20
packages/core/test/src/client/renderSingleSettings.tsx
Normal file
20
packages/core/test/src/client/renderSingleSettings.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { RenderSettingsOptions, renderSettings } from './renderSettings';
|
||||
import { addXReadPrettyToEachLayer, setSchemaWithSettings } from '../web';
|
||||
|
||||
interface RenderSingleSettingsOptions extends Omit<RenderSettingsOptions, 'schemaSettings'> {
|
||||
settingPath?: string;
|
||||
}
|
||||
|
||||
export const renderSingleSettings = (options: RenderSingleSettingsOptions) => {
|
||||
setSchemaWithSettings(options);
|
||||
|
||||
return renderSettings(options);
|
||||
};
|
||||
|
||||
export const renderReadPrettySingleSettings = (options: RenderSingleSettingsOptions) => {
|
||||
setSchemaWithSettings(options);
|
||||
|
||||
options.schema = addXReadPrettyToEachLayer(options.schema);
|
||||
|
||||
return renderSettings(options);
|
||||
};
|
30
packages/core/test/src/client/settingsChecker/delete.tsx
Normal file
30
packages/core/test/src/client/settingsChecker/delete.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { CheckModalOptions, checkModal, expectNoTsError } from '../utils';
|
||||
|
||||
export interface CheckDeleteSettingOptions {
|
||||
title: string;
|
||||
deletedText?: string;
|
||||
afterClick?: () => Promise<void> | void;
|
||||
modalChecker?: Omit<CheckModalOptions, 'triggerText'>;
|
||||
}
|
||||
|
||||
export async function checkDeleteSetting(options: CheckDeleteSettingOptions) {
|
||||
if (options.modalChecker) {
|
||||
await checkModal({
|
||||
triggerText: options.title,
|
||||
contentText: 'Are you sure you want to delete it?',
|
||||
...options.modalChecker,
|
||||
async afterSubmit() {
|
||||
if (options.modalChecker.afterSubmit) {
|
||||
await options.modalChecker.afterSubmit();
|
||||
}
|
||||
if (options.deletedText) {
|
||||
expectNoTsError(screen.queryByText(options.deletedText)).not.toBeInTheDocument();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
if (options.afterClick) {
|
||||
await options.afterClick();
|
||||
}
|
||||
}
|
41
packages/core/test/src/client/settingsChecker/index.ts
Normal file
41
packages/core/test/src/client/settingsChecker/index.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { expect } from 'vitest';
|
||||
import { screen } from '@testing-library/react';
|
||||
|
||||
import { CheckDeleteSettingOptions, checkDeleteSetting } from './delete';
|
||||
import { CheckModalSettingOptions, checkModalSetting } from './modal';
|
||||
import { CheckSwitchSettingOptions, checkSwitchSetting } from './switch';
|
||||
import { SelectSettingOptions, checkSelectSetting } from './select';
|
||||
import { showSettingsMenu } from '../renderSettings';
|
||||
|
||||
export * from './delete';
|
||||
export * from './modal';
|
||||
export * from './switch';
|
||||
export * from './select';
|
||||
|
||||
export type CheckSettingsOptions =
|
||||
| ({ type: 'switch' } & CheckSwitchSettingOptions)
|
||||
| ({ type: 'modal' } & CheckModalSettingOptions)
|
||||
| ({ type: 'select' } & SelectSettingOptions)
|
||||
| ({ type: 'delete' } & CheckDeleteSettingOptions);
|
||||
|
||||
const types = {
|
||||
switch: checkSwitchSetting,
|
||||
modal: checkModalSetting,
|
||||
delete: checkDeleteSetting,
|
||||
select: checkSelectSetting,
|
||||
};
|
||||
|
||||
export async function checkSettings(list: CheckSettingsOptions[], checkLength = false) {
|
||||
if (checkLength) {
|
||||
const menuList = screen.getByTestId('schema-settings-menu');
|
||||
expect(menuList.querySelectorAll('li[role="menuitem"]')).toHaveLength(list.length);
|
||||
}
|
||||
for (const item of list) {
|
||||
if (!screen.queryByTestId('schema-settings-menu')) {
|
||||
await showSettingsMenu();
|
||||
}
|
||||
const type = item.type;
|
||||
const checker = types[type];
|
||||
await checker(item as any);
|
||||
}
|
||||
}
|
21
packages/core/test/src/client/settingsChecker/modal.tsx
Normal file
21
packages/core/test/src/client/settingsChecker/modal.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { CheckModalOptions, checkModal } from '../utils';
|
||||
|
||||
export interface CheckModalSettingOptions {
|
||||
title: string;
|
||||
beforeClick?: () => Promise<void> | void;
|
||||
afterClick?: () => Promise<void> | void;
|
||||
modalChecker?: Omit<CheckModalOptions, 'triggerText'>;
|
||||
}
|
||||
|
||||
export async function checkModalSetting(options: CheckModalSettingOptions) {
|
||||
if (options.modalChecker) {
|
||||
await checkModal({
|
||||
triggerText: options.title,
|
||||
...options.modalChecker,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.afterClick) {
|
||||
await options.afterClick();
|
||||
}
|
||||
}
|
58
packages/core/test/src/client/settingsChecker/select.tsx
Normal file
58
packages/core/test/src/client/settingsChecker/select.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { expectNoTsError } from '../utils';
|
||||
|
||||
export interface SelectSettingOptions {
|
||||
title: string;
|
||||
beforeSelect?: () => Promise<void> | void;
|
||||
oldValue?: string;
|
||||
options?: { label: string; checker?: () => void | Promise<void> }[];
|
||||
}
|
||||
|
||||
export async function checkSelectSetting(options: SelectSettingOptions) {
|
||||
if (options.beforeSelect) {
|
||||
await options.beforeSelect();
|
||||
}
|
||||
|
||||
const formItem = screen.getByTitle(options.title);
|
||||
|
||||
if (options.oldValue) {
|
||||
expectNoTsError(formItem).toHaveTextContent(options.oldValue);
|
||||
}
|
||||
|
||||
if (options.options) {
|
||||
const getListbox = () => document.querySelector(`.select-popup-${options.title.replaceAll(' ', '-')}`);
|
||||
|
||||
// 打开下拉框
|
||||
expectNoTsError(formItem.querySelector('.ant-select-selector')).toBeInTheDocument();
|
||||
await userEvent.click(formItem.querySelector('.ant-select-selector'));
|
||||
await waitFor(() => {
|
||||
expectNoTsError(getListbox()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
for (const option of options.options) {
|
||||
const listbox = getListbox();
|
||||
expectNoTsError(listbox).toHaveTextContent(option.label);
|
||||
|
||||
if (option.checker) {
|
||||
const item = [...listbox.querySelectorAll('.ant-select-item-option-content')].find(
|
||||
(item) => item.textContent === option.label,
|
||||
);
|
||||
await userEvent.click(item);
|
||||
|
||||
// 等到下拉框关闭,并且值更新
|
||||
await waitFor(() => {
|
||||
expectNoTsError(screen.getByTitle(options.title)).toHaveTextContent(option.label);
|
||||
});
|
||||
|
||||
await option.checker();
|
||||
|
||||
// 重新打开下拉框
|
||||
await userEvent.click(screen.getByTitle(options.title).querySelector('.ant-select-selection-item'));
|
||||
await waitFor(() => {
|
||||
expectNoTsError(getListbox()).toBeInTheDocument();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
64
packages/core/test/src/client/settingsChecker/switch.tsx
Normal file
64
packages/core/test/src/client/settingsChecker/switch.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { expect } from 'vitest';
|
||||
import { waitFor, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { showSettingsMenu } from '../renderSettings';
|
||||
|
||||
export interface CheckSwitchSettingOptions {
|
||||
title: string;
|
||||
beforeClick?: () => Promise<void> | void;
|
||||
afterFirstClick?: () => Promise<void> | void;
|
||||
afterSecondClick?: () => Promise<void> | void;
|
||||
afterThirdClick?: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
export async function checkSwitchSetting(options: CheckSwitchSettingOptions) {
|
||||
if (options.beforeClick) {
|
||||
await options.beforeClick();
|
||||
}
|
||||
|
||||
// 先获取 switch 元素,记录 checked 状态
|
||||
const formItem = screen.getByTitle(options.title);
|
||||
const switchElement = formItem.querySelector('button[role=switch]');
|
||||
let oldChecked = switchElement.getAttribute('aria-checked');
|
||||
const afterClick = async () => {
|
||||
const formItem = screen.queryByTitle(options.title);
|
||||
if (formItem) {
|
||||
const switchElement = formItem.querySelector('button[role=switch]');
|
||||
const newChecked = switchElement.getAttribute('aria-checked');
|
||||
expect(newChecked).not.toBe(oldChecked);
|
||||
oldChecked = newChecked;
|
||||
} else {
|
||||
// 重新打开设置菜单
|
||||
await showSettingsMenu();
|
||||
}
|
||||
};
|
||||
|
||||
// 第一次点击
|
||||
if (options.afterFirstClick) {
|
||||
await userEvent.click(formItem.querySelector('button[role=switch]'));
|
||||
await waitFor(async () => {
|
||||
await afterClick();
|
||||
});
|
||||
|
||||
await options.afterFirstClick();
|
||||
}
|
||||
|
||||
// 第二次点击
|
||||
if (options.afterSecondClick) {
|
||||
await userEvent.click(screen.getByText(options.title));
|
||||
await waitFor(async () => {
|
||||
await afterClick();
|
||||
});
|
||||
await options.afterSecondClick();
|
||||
}
|
||||
|
||||
// 第三次点击
|
||||
if (options.afterThirdClick) {
|
||||
await userEvent.click(screen.getByText(options.title));
|
||||
await waitFor(async () => {
|
||||
await afterClick();
|
||||
});
|
||||
|
||||
await options.afterThirdClick();
|
||||
}
|
||||
}
|
66
packages/core/test/src/client/utils/checkModal.tsx
Normal file
66
packages/core/test/src/client/utils/checkModal.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { waitFor, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { FormItemCheckOptions, checkFormItems } from '../formItemChecker';
|
||||
import { expectNoTsError } from './utils';
|
||||
import { sleep } from '../../web';
|
||||
|
||||
export interface CheckModalOptions {
|
||||
triggerText?: string;
|
||||
modalTitle?: string;
|
||||
confirmTitle?: string;
|
||||
submitText?: string;
|
||||
contentText?: string;
|
||||
beforeCheck?: () => Promise<void> | void;
|
||||
customCheck?: () => Promise<void> | void;
|
||||
formItems?: FormItemCheckOptions[];
|
||||
afterSubmit?: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
export async function checkModal(options: CheckModalOptions) {
|
||||
const { triggerText, modalTitle, confirmTitle, submitText = 'OK', formItems = [] } = options;
|
||||
|
||||
await waitFor(() => {
|
||||
expectNoTsError(screen.queryByText(triggerText)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText(triggerText));
|
||||
|
||||
await waitFor(() => {
|
||||
expectNoTsError(screen.queryByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const dialog = screen.getByRole('dialog');
|
||||
|
||||
if (modalTitle) {
|
||||
expectNoTsError(dialog.querySelector('.ant-modal-title')).toHaveTextContent(modalTitle);
|
||||
}
|
||||
|
||||
if (confirmTitle) {
|
||||
expectNoTsError(dialog.querySelector('.ant-modal-confirm-title')).toHaveTextContent(confirmTitle);
|
||||
}
|
||||
|
||||
if (options.contentText) {
|
||||
expectNoTsError(dialog).toHaveTextContent(options.contentText);
|
||||
}
|
||||
|
||||
if (options.beforeCheck) {
|
||||
await options.beforeCheck();
|
||||
}
|
||||
|
||||
if (options.customCheck) {
|
||||
await options.customCheck();
|
||||
}
|
||||
|
||||
await checkFormItems(formItems);
|
||||
|
||||
await userEvent.click(screen.getByText(submitText));
|
||||
|
||||
await waitFor(() => {
|
||||
expectNoTsError(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
if (options.afterSubmit) {
|
||||
await sleep(100);
|
||||
await options.afterSubmit();
|
||||
}
|
||||
}
|
10
packages/core/test/src/client/utils/checkSchema.tsx
Normal file
10
packages/core/test/src/client/utils/checkSchema.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { waitFor, screen } from '@testing-library/react';
|
||||
import { expect } from 'vitest';
|
||||
|
||||
export async function checkSchema(matchObj?: Record<string, any>, name?: string) {
|
||||
const objText = screen.queryByTestId(name ? `test-schema-${name}` : `test-schema`);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(JSON.parse(objText.textContent)).toMatchObject(matchObj);
|
||||
});
|
||||
}
|
3
packages/core/test/src/client/utils/index.ts
Normal file
3
packages/core/test/src/client/utils/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './utils';
|
||||
export * from './checkModal';
|
||||
export * from './checkSchema';
|
21
packages/core/test/src/client/utils/utils.ts
Normal file
21
packages/core/test/src/client/utils/utils.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { expect } from 'vitest';
|
||||
import { waitFor, screen } from '@testing-library/react';
|
||||
|
||||
// @ts-ignore
|
||||
export const expectNoTsError: any = expect;
|
||||
|
||||
export const WaitApp = async () => {
|
||||
await waitFor(() => {
|
||||
expectNoTsError(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const loadError = screen.queryByText('Load Plugin Error');
|
||||
if (loadError) {
|
||||
expectNoTsError(screen.queryByText('Load Plugin Error')).not.toBeInTheDocument();
|
||||
}
|
||||
|
||||
const renderError = screen.queryByText('Render Failed');
|
||||
if (renderError) {
|
||||
expectNoTsError(screen.queryByText('Render Failed')).not.toBeInTheDocument();
|
||||
}
|
||||
};
|
@ -1,34 +1,14 @@
|
||||
[
|
||||
{
|
||||
"key": "h7b9i8khc3q",
|
||||
"key": "yzowed2vee0",
|
||||
"name": "users",
|
||||
"title": "{{t(\"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",
|
||||
"key": "6m3kn2pytkc",
|
||||
"name": "id",
|
||||
"type": "bigInt",
|
||||
"interface": "id",
|
||||
@ -38,36 +18,31 @@
|
||||
"reverseKey": null,
|
||||
"autoIncrement": true,
|
||||
"primaryKey": true,
|
||||
"allowNull": false
|
||||
"allowNull": false,
|
||||
"uiSchema": {
|
||||
"type": "number",
|
||||
"title": "{{t(\"ID\")}}",
|
||||
"x-component": "InputNumber",
|
||||
"x-read-pretty": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"title": "{{t(\"Nickname\")}}",
|
||||
"x-component": "Input",
|
||||
"rawTitle": "{{t(\"Nickname\")}}"
|
||||
},
|
||||
"key": "vrv7yjue90g",
|
||||
"key": "8douy9r69x5",
|
||||
"name": "nickname",
|
||||
"type": "string",
|
||||
"interface": "input",
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null
|
||||
},
|
||||
{
|
||||
"reverseKey": null,
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"title": "{{t(\"Username\")}}",
|
||||
"x-component": "Input",
|
||||
"x-validator": {
|
||||
"username": true
|
||||
"title": "{{t(\"Nickname\")}}",
|
||||
"x-component": "Input"
|
||||
}
|
||||
},
|
||||
"required": true,
|
||||
"rawTitle": "{{t(\"Username\")}}"
|
||||
},
|
||||
"key": "2ccs6evyrub",
|
||||
{
|
||||
"key": "vp191ptc0d7",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"interface": "input",
|
||||
@ -75,18 +50,19 @@
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"unique": true,
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"title": "{{t(\"Email\")}}",
|
||||
"title": "{{t(\"Username\")}}",
|
||||
"x-component": "Input",
|
||||
"x-validator": "email",
|
||||
"required": true,
|
||||
"rawTitle": "{{t(\"Email\")}}"
|
||||
"x-validator": {
|
||||
"username": true
|
||||
},
|
||||
"key": "rrskwjl5wt1",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "47o82qhkvdm",
|
||||
"name": "email",
|
||||
"type": "string",
|
||||
"interface": "email",
|
||||
@ -94,10 +70,150 @@
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"unique": true
|
||||
"unique": true,
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"title": "{{t(\"Email\")}}",
|
||||
"x-component": "Input",
|
||||
"x-validator": "email",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "t09bauwm0wb",
|
||||
"key": "q5i9ynhq325",
|
||||
"name": "phone",
|
||||
"type": "string",
|
||||
"interface": "phone",
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"unique": true,
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"title": "{{t(\"Phone\")}}",
|
||||
"x-component": "Input",
|
||||
"x-validator": "phone",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "cc4aslvh9dv",
|
||||
"name": "password",
|
||||
"type": "password",
|
||||
"interface": "password",
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"hidden": true,
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"title": "{{t(\"Password\")}}",
|
||||
"x-component": "Password"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "e87ndyttazh",
|
||||
"name": "appLang",
|
||||
"type": "string",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null
|
||||
},
|
||||
{
|
||||
"key": "snfbet0pe49",
|
||||
"name": "resetToken",
|
||||
"type": "string",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"unique": true,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"key": "j78yhz6wifd",
|
||||
"name": "systemSettings",
|
||||
"type": "json",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"defaultValue": {}
|
||||
},
|
||||
{
|
||||
"key": "lkxqml8gchd",
|
||||
"name": "sort",
|
||||
"type": "sort",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"key": "lt3pcrjngzc",
|
||||
"name": "createdById",
|
||||
"type": "context",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"dataType": "bigInt",
|
||||
"dataIndex": "state.currentUser.id",
|
||||
"createOnly": true,
|
||||
"visible": true,
|
||||
"index": true
|
||||
},
|
||||
{
|
||||
"key": "lcfaf27uxyz",
|
||||
"name": "createdBy",
|
||||
"type": "belongsTo",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"target": "users",
|
||||
"foreignKey": "createdById",
|
||||
"targetKey": "id"
|
||||
},
|
||||
{
|
||||
"key": "y6lwdb31r5t",
|
||||
"name": "updatedById",
|
||||
"type": "context",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"dataType": "bigInt",
|
||||
"dataIndex": "state.currentUser.id",
|
||||
"visible": true,
|
||||
"index": true
|
||||
},
|
||||
{
|
||||
"key": "exhbmthsin0",
|
||||
"name": "updatedBy",
|
||||
"type": "belongsTo",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"target": "users",
|
||||
"foreignKey": "updatedById",
|
||||
"targetKey": "id"
|
||||
},
|
||||
{
|
||||
"key": "s921nnlzdwi",
|
||||
"name": "roles",
|
||||
"type": "belongsToMany",
|
||||
"interface": "m2m",
|
||||
@ -126,60 +242,80 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "1pz0art9mt7",
|
||||
"name": "f_n2fu6hvprct",
|
||||
"type": "string",
|
||||
"interface": "select",
|
||||
"key": "qe7b1rsct5h",
|
||||
"name": "jobs",
|
||||
"type": "belongsToMany",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "t_vwpds9fs4xs",
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"through": "users_jobs",
|
||||
"foreignKey": "userId",
|
||||
"sourceKey": "id",
|
||||
"otherKey": "jobId",
|
||||
"targetKey": "id",
|
||||
"target": "jobs"
|
||||
},
|
||||
{
|
||||
"key": "vt0n1l1ruyz",
|
||||
"name": "usersJobs",
|
||||
"type": "hasMany",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"target": "users_jobs",
|
||||
"foreignKey": "userId",
|
||||
"sourceKey": "id",
|
||||
"targetKey": "id"
|
||||
},
|
||||
{
|
||||
"key": "ekol7p60nry",
|
||||
"name": "sortName",
|
||||
"type": "sort",
|
||||
"interface": "sort",
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"uiSchema": {
|
||||
"enum": [
|
||||
{
|
||||
"value": "test1",
|
||||
"label": "test1"
|
||||
"type": "number",
|
||||
"x-component": "InputNumber",
|
||||
"x-component-props": {
|
||||
"stringMode": true,
|
||||
"step": "1"
|
||||
},
|
||||
{
|
||||
"value": "test2",
|
||||
"label": "test2"
|
||||
"x-validator": "integer",
|
||||
"title": "sort"
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "string",
|
||||
"x-component": "Select",
|
||||
"title": "test"
|
||||
}
|
||||
}
|
||||
]
|
||||
"category": [],
|
||||
"origin": "@nocobase/plugin-users",
|
||||
"dumpRules": {
|
||||
"group": "user"
|
||||
},
|
||||
"sortable": "sort",
|
||||
"model": "UserModel",
|
||||
"createdBy": true,
|
||||
"updatedBy": true,
|
||||
"logging": true,
|
||||
"shared": true,
|
||||
"from": "db2cm",
|
||||
"filterTargetKey": "id"
|
||||
},
|
||||
{
|
||||
"key": "pqnenvqrzxr",
|
||||
"key": "rmx938ttbue",
|
||||
"name": "roles",
|
||||
"title": "{{t(\"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",
|
||||
"key": "l6thu4n5u6x",
|
||||
"name": "name",
|
||||
"type": "uid",
|
||||
"interface": "input",
|
||||
@ -188,16 +324,15 @@
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"prefix": "r_",
|
||||
"primaryKey": true
|
||||
},
|
||||
{
|
||||
"primaryKey": true,
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"title": "{{t(\"Role name\")}}",
|
||||
"x-component": "Input",
|
||||
"rawTitle": "{{t(\"Role name\")}}"
|
||||
"title": "{{t(\"Role UID\")}}",
|
||||
"x-component": "Input"
|
||||
}
|
||||
},
|
||||
"key": "faywtz4sf3u",
|
||||
{
|
||||
"key": "yhfq9yv8z0p",
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"interface": "input",
|
||||
@ -206,10 +341,15 @@
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"unique": true,
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"title": "{{t(\"Role name\")}}",
|
||||
"x-component": "Input"
|
||||
},
|
||||
"translation": true
|
||||
},
|
||||
{
|
||||
"key": "1enkovm9sye",
|
||||
"key": "vnhjlmopfuz",
|
||||
"name": "description",
|
||||
"type": "string",
|
||||
"interface": null,
|
||||
@ -217,7 +357,463 @@
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null
|
||||
},
|
||||
{
|
||||
"key": "s4iqsehgxo6",
|
||||
"name": "strategy",
|
||||
"type": "json",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null
|
||||
},
|
||||
{
|
||||
"key": "75e4wnv873m",
|
||||
"name": "default",
|
||||
"type": "boolean",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"key": "nofdv0gte68",
|
||||
"name": "hidden",
|
||||
"type": "boolean",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"key": "bogzo1uvk84",
|
||||
"name": "allowConfigure",
|
||||
"type": "boolean",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null
|
||||
},
|
||||
{
|
||||
"key": "k3fvj8ddpp9",
|
||||
"name": "allowNewMenu",
|
||||
"type": "boolean",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null
|
||||
},
|
||||
{
|
||||
"key": "v1ditqsv1uk",
|
||||
"name": "menuUiSchemas",
|
||||
"type": "belongsToMany",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"target": "uiSchemas",
|
||||
"targetKey": "x-uid",
|
||||
"foreignKey": "roleName",
|
||||
"sourceKey": "name",
|
||||
"otherKey": "uiSchemaXUid",
|
||||
"through": "rolesUischemas"
|
||||
},
|
||||
{
|
||||
"key": "ccqhlcvgnz8",
|
||||
"name": "resources",
|
||||
"type": "hasMany",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"target": "dataSourcesRolesResources",
|
||||
"sourceKey": "name",
|
||||
"foreignKey": "roleName",
|
||||
"targetKey": "id"
|
||||
},
|
||||
{
|
||||
"key": "s4fshtx7oxv",
|
||||
"name": "snippets",
|
||||
"type": "set",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"defaultValue": ["!ui.*", "!pm", "!pm.*"]
|
||||
},
|
||||
{
|
||||
"key": "oyzvbhc60mp",
|
||||
"name": "users",
|
||||
"type": "belongsToMany",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"target": "users",
|
||||
"foreignKey": "roleName",
|
||||
"otherKey": "userId",
|
||||
"onDelete": "CASCADE",
|
||||
"sourceKey": "name",
|
||||
"targetKey": "id",
|
||||
"through": "rolesUsers"
|
||||
},
|
||||
{
|
||||
"key": "89yklh7lm3p",
|
||||
"name": "sort",
|
||||
"type": "sort",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"key": "iz5s22dinui",
|
||||
"name": "color",
|
||||
"type": "string",
|
||||
"interface": "color",
|
||||
"description": null,
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"defaultValue": "#1677FF",
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"x-component": "ColorPicker",
|
||||
"default": "#1677FF",
|
||||
"title": "color"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "o5nyb6isl62",
|
||||
"name": "long-text",
|
||||
"type": "text",
|
||||
"interface": "textarea",
|
||||
"description": null,
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"x-component": "Input.TextArea",
|
||||
"title": "Long text"
|
||||
}
|
||||
}
|
||||
],
|
||||
"category": [],
|
||||
"origin": "@nocobase/plugin-acl",
|
||||
"dumpRules": "required",
|
||||
"autoGenId": false,
|
||||
"model": "RoleModel",
|
||||
"filterTargetKey": "name",
|
||||
"sortable": true,
|
||||
"from": "db2cm"
|
||||
},
|
||||
{
|
||||
"key": "24gntrrr5a6",
|
||||
"name": "tree",
|
||||
"title": "TreeCollection",
|
||||
"inherit": false,
|
||||
"hidden": false,
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"key": "pcea3h3ivkg",
|
||||
"name": "parentId",
|
||||
"type": "bigInt",
|
||||
"interface": "integer",
|
||||
"description": null,
|
||||
"collectionName": "t_4uamm7v51dj",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"isForeignKey": true,
|
||||
"uiSchema": {
|
||||
"type": "number",
|
||||
"title": "{{t(\"Parent ID\")}}",
|
||||
"x-component": "InputNumber",
|
||||
"x-read-pretty": true
|
||||
},
|
||||
"target": "t_4uamm7v51dj"
|
||||
},
|
||||
{
|
||||
"key": "185j2rf7o68",
|
||||
"name": "parent",
|
||||
"type": "belongsTo",
|
||||
"interface": "m2o",
|
||||
"description": null,
|
||||
"collectionName": "t_4uamm7v51dj",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"foreignKey": "parentId",
|
||||
"treeParent": true,
|
||||
"onDelete": "CASCADE",
|
||||
"uiSchema": {
|
||||
"title": "{{t(\"Parent\")}}",
|
||||
"x-component": "AssociationField",
|
||||
"x-component-props": {
|
||||
"multiple": false,
|
||||
"fieldNames": {
|
||||
"label": "id",
|
||||
"value": "id"
|
||||
}
|
||||
}
|
||||
},
|
||||
"target": "t_4uamm7v51dj",
|
||||
"targetKey": "id"
|
||||
},
|
||||
{
|
||||
"key": "gjvso3p9sjn",
|
||||
"name": "children",
|
||||
"type": "hasMany",
|
||||
"interface": "o2m",
|
||||
"description": null,
|
||||
"collectionName": "t_4uamm7v51dj",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"foreignKey": "parentId",
|
||||
"treeChildren": true,
|
||||
"onDelete": "CASCADE",
|
||||
"uiSchema": {
|
||||
"title": "{{t(\"Children\")}}",
|
||||
"x-component": "AssociationField",
|
||||
"x-component-props": {
|
||||
"multiple": true,
|
||||
"fieldNames": {
|
||||
"label": "id",
|
||||
"value": "id"
|
||||
}
|
||||
}
|
||||
},
|
||||
"target": "t_4uamm7v51dj",
|
||||
"targetKey": "id",
|
||||
"sourceKey": "id"
|
||||
},
|
||||
{
|
||||
"key": "j50f3am3c88",
|
||||
"name": "id",
|
||||
"type": "bigInt",
|
||||
"interface": "integer",
|
||||
"description": null,
|
||||
"collectionName": "t_4uamm7v51dj",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"autoIncrement": true,
|
||||
"primaryKey": true,
|
||||
"allowNull": false,
|
||||
"uiSchema": {
|
||||
"type": "number",
|
||||
"title": "{{t(\"ID\")}}",
|
||||
"x-component": "InputNumber",
|
||||
"x-read-pretty": true
|
||||
},
|
||||
"target": "t_4uamm7v51dj"
|
||||
},
|
||||
{
|
||||
"key": "9szmn2ecqgs",
|
||||
"name": "f_y99u3pyj0bt",
|
||||
"type": "string",
|
||||
"interface": "input",
|
||||
"description": null,
|
||||
"collectionName": "t_4uamm7v51dj",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"x-component": "Input",
|
||||
"title": "Single line text"
|
||||
}
|
||||
}
|
||||
],
|
||||
"category": [],
|
||||
"logging": true,
|
||||
"autoGenId": true,
|
||||
"createdAt": false,
|
||||
"createdBy": false,
|
||||
"updatedAt": false,
|
||||
"updatedBy": false,
|
||||
"template": "tree",
|
||||
"view": false,
|
||||
"tree": "adjacencyList",
|
||||
"filterTargetKey": "id"
|
||||
},
|
||||
{
|
||||
"key": "16ocj2rsg3t",
|
||||
"name": "interfaces",
|
||||
"title": "Interfaces",
|
||||
"inherit": false,
|
||||
"hidden": false,
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"key": "k2v39l19inp",
|
||||
"name": "id",
|
||||
"type": "bigInt",
|
||||
"interface": "integer",
|
||||
"description": null,
|
||||
"collectionName": "interfaces",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"autoIncrement": true,
|
||||
"primaryKey": true,
|
||||
"allowNull": false,
|
||||
"uiSchema": {
|
||||
"type": "number",
|
||||
"title": "{{t(\"ID\")}}",
|
||||
"x-component": "InputNumber",
|
||||
"x-read-pretty": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "gfnqdj8sd01",
|
||||
"name": "createdAt",
|
||||
"type": "date",
|
||||
"interface": "createdAt",
|
||||
"description": null,
|
||||
"collectionName": "interfaces",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"field": "createdAt",
|
||||
"uiSchema": {
|
||||
"type": "datetime",
|
||||
"title": "{{t(\"Created at\")}}",
|
||||
"x-component": "DatePicker",
|
||||
"x-component-props": {},
|
||||
"x-read-pretty": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "3ddqrb1lle5",
|
||||
"name": "createdBy",
|
||||
"type": "belongsTo",
|
||||
"interface": "createdBy",
|
||||
"description": null,
|
||||
"collectionName": "interfaces",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"target": "users",
|
||||
"foreignKey": "createdById",
|
||||
"uiSchema": {
|
||||
"type": "object",
|
||||
"title": "{{t(\"Created by\")}}",
|
||||
"x-component": "AssociationField",
|
||||
"x-component-props": {
|
||||
"fieldNames": {
|
||||
"value": "id",
|
||||
"label": "nickname"
|
||||
}
|
||||
},
|
||||
"x-read-pretty": true
|
||||
},
|
||||
"targetKey": "id"
|
||||
},
|
||||
{
|
||||
"key": "9nc7gqqw0ht",
|
||||
"name": "updatedAt",
|
||||
"type": "date",
|
||||
"interface": "updatedAt",
|
||||
"description": null,
|
||||
"collectionName": "interfaces",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"field": "updatedAt",
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"title": "{{t(\"Last updated at\")}}",
|
||||
"x-component": "DatePicker",
|
||||
"x-component-props": {},
|
||||
"x-read-pretty": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "06wr4f2qi4w",
|
||||
"name": "updatedBy",
|
||||
"type": "belongsTo",
|
||||
"interface": "updatedBy",
|
||||
"description": null,
|
||||
"collectionName": "interfaces",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"target": "users",
|
||||
"foreignKey": "updatedById",
|
||||
"uiSchema": {
|
||||
"type": "object",
|
||||
"title": "{{t(\"Last updated by\")}}",
|
||||
"x-component": "AssociationField",
|
||||
"x-component-props": {
|
||||
"fieldNames": {
|
||||
"value": "id",
|
||||
"label": "nickname"
|
||||
}
|
||||
},
|
||||
"x-read-pretty": true
|
||||
},
|
||||
"targetKey": "id"
|
||||
},
|
||||
{
|
||||
"key": "g5beggm3aln",
|
||||
"name": "nano-iD",
|
||||
"type": "nanoid",
|
||||
"interface": "nanoid",
|
||||
"description": null,
|
||||
"collectionName": "interfaces",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"customAlphabet": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
||||
"size": 21,
|
||||
"autoFill": true,
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"x-component": "NanoIDInput",
|
||||
"title": "Nano ID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "wchbz23orvg",
|
||||
"name": "attachment",
|
||||
"type": "belongsToMany",
|
||||
"interface": "attachment",
|
||||
"description": null,
|
||||
"collectionName": "interfaces",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"uiSchema": {
|
||||
"x-component-props": {
|
||||
"accept": "image/*",
|
||||
"multiple": true
|
||||
},
|
||||
"type": "array",
|
||||
"x-component": "Upload.Attachment",
|
||||
"title": "Attachment"
|
||||
},
|
||||
"target": "attachments",
|
||||
"through": "t_zj0zu7maytd",
|
||||
"foreignKey": "f_iek1e32gsq0",
|
||||
"otherKey": "f_dtc6w8dzyoo",
|
||||
"targetKey": "id",
|
||||
"sourceKey": "id"
|
||||
}
|
||||
],
|
||||
"category": [],
|
||||
"logging": true,
|
||||
"autoGenId": true,
|
||||
"createdAt": true,
|
||||
"createdBy": true,
|
||||
"updatedAt": true,
|
||||
"updatedBy": true,
|
||||
"template": "general",
|
||||
"view": false,
|
||||
"filterTargetKey": "id"
|
||||
}
|
||||
]
|
||||
|
186
packages/core/test/src/web/dataSourceMainData.json
Normal file
186
packages/core/test/src/web/dataSourceMainData.json
Normal file
@ -0,0 +1,186 @@
|
||||
{
|
||||
"users:list": {
|
||||
"data": [
|
||||
{
|
||||
"createdAt": "2024-04-07T06:50:37.797Z",
|
||||
"updatedAt": "2024-04-07T06:50:37.797Z",
|
||||
"appLang": null,
|
||||
"createdById": null,
|
||||
"email": "admin@nocobase.com",
|
||||
"f_1gx8uyn3wva": 1,
|
||||
"id": 1,
|
||||
"nickname": "Super Admin",
|
||||
"phone": null,
|
||||
"systemSettings": {},
|
||||
"updatedById": null,
|
||||
"username": "nocobase",
|
||||
"roles": [
|
||||
{
|
||||
"createdAt": "2024-04-07T06:50:37.700Z",
|
||||
"updatedAt": "2024-04-07T06:50:37.700Z",
|
||||
"allowConfigure": null,
|
||||
"allowNewMenu": true,
|
||||
"default": true,
|
||||
"description": null,
|
||||
"hidden": false,
|
||||
"color": "#1677FF",
|
||||
"name": "member",
|
||||
"snippets": ["!pm", "!pm.*", "!ui.*"],
|
||||
"strategy": {
|
||||
"actions": ["view", "update:own", "destroy:own", "create"]
|
||||
},
|
||||
"title": "{{t(\"Member\")}}",
|
||||
"rolesUsers": {
|
||||
"createdAt": "2024-04-07T06:50:37.854Z",
|
||||
"updatedAt": "2024-04-07T06:50:37.854Z",
|
||||
"default": null,
|
||||
"roleName": "member",
|
||||
"userId": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"createdAt": "2024-04-07T06:50:37.622Z",
|
||||
"updatedAt": "2024-04-07T06:50:37.622Z",
|
||||
"allowConfigure": null,
|
||||
"allowNewMenu": null,
|
||||
"default": false,
|
||||
"description": null,
|
||||
"hidden": true,
|
||||
"color": "#1677FF",
|
||||
"name": "root",
|
||||
"snippets": ["pm", "pm.*", "ui.*"],
|
||||
"strategy": null,
|
||||
"title": "{{t(\"Root\")}}",
|
||||
"rolesUsers": {
|
||||
"createdAt": "2024-04-07T06:50:38.152Z",
|
||||
"updatedAt": "2024-04-07T06:50:38.186Z",
|
||||
"default": true,
|
||||
"roleName": "root",
|
||||
"userId": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"createdAt": "2024-04-07T06:50:37.657Z",
|
||||
"updatedAt": "2024-04-07T06:50:37.657Z",
|
||||
"allowConfigure": true,
|
||||
"allowNewMenu": true,
|
||||
"default": false,
|
||||
"description": null,
|
||||
"hidden": false,
|
||||
"name": "admin",
|
||||
"color": "#1677FF",
|
||||
"snippets": ["pm", "pm.*", "ui.*"],
|
||||
"strategy": {
|
||||
"actions": ["create", "view", "update", "destroy"]
|
||||
},
|
||||
"title": "{{t(\"Admin\")}}",
|
||||
"rolesUsers": {
|
||||
"createdAt": "2024-04-07T06:50:38.152Z",
|
||||
"updatedAt": "2024-04-07T06:50:38.152Z",
|
||||
"default": null,
|
||||
"roleName": "admin",
|
||||
"userId": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"count": 1,
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"totalPage": 1,
|
||||
"allowedActions": {
|
||||
"view": [1],
|
||||
"update": [1],
|
||||
"destroy": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"roles:list": {
|
||||
"data": [
|
||||
{
|
||||
"createdAt": "2024-04-07T06:50:37.622Z",
|
||||
"updatedAt": "2024-04-07T06:50:37.622Z",
|
||||
"allowConfigure": null,
|
||||
"allowNewMenu": null,
|
||||
"default": false,
|
||||
"description": null,
|
||||
"hidden": true,
|
||||
"name": "root",
|
||||
"color": "#1677FF",
|
||||
"snippets": ["pm", "pm.*", "ui.*"],
|
||||
"strategy": null,
|
||||
"title": "{{t(\"Root\")}}"
|
||||
},
|
||||
{
|
||||
"createdAt": "2024-04-07T06:50:37.657Z",
|
||||
"updatedAt": "2024-04-07T06:50:37.657Z",
|
||||
"allowConfigure": true,
|
||||
"allowNewMenu": true,
|
||||
"default": false,
|
||||
"description": null,
|
||||
"hidden": false,
|
||||
"color": "#1677FF",
|
||||
"name": "admin",
|
||||
"snippets": ["pm", "pm.*", "ui.*"],
|
||||
"strategy": {
|
||||
"actions": ["create", "view", "update", "destroy"]
|
||||
},
|
||||
"title": "{{t(\"Admin\")}}"
|
||||
},
|
||||
{
|
||||
"createdAt": "2024-04-07T06:50:37.700Z",
|
||||
"updatedAt": "2024-04-07T06:50:37.700Z",
|
||||
"allowConfigure": null,
|
||||
"allowNewMenu": true,
|
||||
"default": true,
|
||||
"description": null,
|
||||
"color": "#1677FF",
|
||||
"hidden": false,
|
||||
"name": "member",
|
||||
"snippets": ["!pm", "!pm.*", "!ui.*"],
|
||||
"strategy": {
|
||||
"actions": ["view", "update:own", "destroy:own", "create"]
|
||||
},
|
||||
"title": "{{t(\"Member\")}}"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"count": 3,
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"totalPage": 1,
|
||||
"allowedActions": {
|
||||
"view": ["root", "admin", "member"],
|
||||
"update": ["root", "admin", "member"],
|
||||
"destroy": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"tree:list": {
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"parentId": null,
|
||||
"f_y99u3pyj0bt": "1"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"parentId": 1,
|
||||
"f_y99u3pyj0bt": "2"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"count": 2,
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"totalPage": 1,
|
||||
"allowedActions": {
|
||||
"view": [1, 2],
|
||||
"update": [1, 2],
|
||||
"destroy": [1, 2]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,34 @@
|
||||
import React, { ComponentType } from 'react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import { set, get, pick } from 'lodash';
|
||||
import { useFieldSchema, observer } from '@formily/react';
|
||||
|
||||
// @ts-ignore
|
||||
import { Application, ApplicationOptions, DataBlockProvider, LocalDataSource, SchemaComponent } from '@nocobase/client';
|
||||
import {
|
||||
AntdSchemaComponentPlugin,
|
||||
Application,
|
||||
ApplicationOptions,
|
||||
CollectionPlugin,
|
||||
DataBlockProvider,
|
||||
LocalDataSource,
|
||||
SchemaComponent,
|
||||
SchemaSettings,
|
||||
SchemaSettingsPlugin,
|
||||
// @ts-ignore
|
||||
} from '@nocobase/client';
|
||||
|
||||
import dataSourceMainCollections from './dataSourceMainCollections.json';
|
||||
import dataSource2 from './dataSource2.json';
|
||||
import usersListData from './usersListData.json';
|
||||
import dataSourceMainData from './dataSourceMainData.json';
|
||||
import _ from 'lodash';
|
||||
|
||||
const defaultApis = {
|
||||
'uiSchemas:patch': { data: { result: 'ok' } },
|
||||
'uiSchemas:saveAsTemplate': { data: { result: 'ok' } },
|
||||
...dataSourceMainData,
|
||||
};
|
||||
|
||||
export * from './utils';
|
||||
|
||||
type URL = string;
|
||||
type ResponseData = any;
|
||||
@ -15,15 +36,29 @@ type ResponseData = any;
|
||||
type MockApis = Record<URL, ResponseData>;
|
||||
type AppOrOptions = Application | ApplicationOptions;
|
||||
|
||||
function getProcessMockData(apis: Record<string, any>, key: string) {
|
||||
return (config: AxiosRequestConfig) => {
|
||||
if (!apis[key]) return [404, { data: { message: 'mock data not found' } }];
|
||||
if (config?.params?.pageSize || config?.params?.page) {
|
||||
const { data, meta } = apis[key];
|
||||
|
||||
const pageSize = config.params.pageSize || meta?.pageSize;
|
||||
const page = config.params.page || meta?.page;
|
||||
return [200, { data: data.slice(pageSize * (page - 1), pageSize), meta: { ...meta, page, pageSize } }];
|
||||
}
|
||||
return [200, apis[key]];
|
||||
};
|
||||
}
|
||||
|
||||
export const mockApi = (axiosInstance: AxiosInstance, apis: MockApis = {}) => {
|
||||
const mock = new MockAdapter(axiosInstance);
|
||||
Object.keys(apis).forEach((key) => {
|
||||
mock.onAny(key).reply(200, apis[key]);
|
||||
mock.onAny(key).reply(getProcessMockData(apis, key));
|
||||
});
|
||||
|
||||
return (apis: MockApis = {}) => {
|
||||
Object.keys(apis).forEach((key) => {
|
||||
mock.onAny(key).reply(200, apis[key]);
|
||||
mock.onAny(key).reply(getProcessMockData(apis, key));
|
||||
});
|
||||
};
|
||||
};
|
||||
@ -37,23 +72,48 @@ export interface GetAppOptions {
|
||||
appOptions?: AppOrOptions;
|
||||
providers?: (ComponentType | [ComponentType, any])[];
|
||||
apis?: MockApis;
|
||||
enableUserListDataBlock?: boolean;
|
||||
designable?: boolean;
|
||||
schemaSettings?: SchemaSettings;
|
||||
disableAcl?: boolean;
|
||||
enableMultipleDataSource?: boolean;
|
||||
}
|
||||
|
||||
export const getApp = (options: GetAppOptions) => {
|
||||
const { appOptions, enableUserListDataBlock, providers, apis, enableMultipleDataSource } = options;
|
||||
const app = appOptions instanceof Application ? appOptions : new Application(appOptions);
|
||||
const {
|
||||
appOptions = {},
|
||||
schemaSettings,
|
||||
providers,
|
||||
disableAcl = true,
|
||||
apis: optionsApis = {},
|
||||
enableMultipleDataSource,
|
||||
designable,
|
||||
} = options;
|
||||
const app =
|
||||
appOptions instanceof Application
|
||||
? appOptions
|
||||
: new Application({
|
||||
...appOptions,
|
||||
disableAcl: appOptions.disableAcl || disableAcl,
|
||||
designable: appOptions.designable || designable,
|
||||
});
|
||||
if (providers) {
|
||||
app.addProviders(providers);
|
||||
}
|
||||
|
||||
app.getCollectionManager().addCollections(dataSourceMainCollections as any);
|
||||
|
||||
if (enableUserListDataBlock && !apis['users:list']) {
|
||||
apis['users:list'] = usersListData;
|
||||
if (schemaSettings) {
|
||||
app.schemaSettingsManager.add(schemaSettings);
|
||||
}
|
||||
|
||||
app.addComponents({ CommonSchemaComponent });
|
||||
|
||||
app.pluginManager.add(AntdSchemaComponentPlugin);
|
||||
app.pluginManager.add(SchemaSettingsPlugin);
|
||||
app.pluginManager.add(CollectionPlugin, { config: { enableRemoteDataSource: false } });
|
||||
|
||||
const apis = Object.assign({}, defaultApis, optionsApis);
|
||||
|
||||
app.getCollectionManager().addCollections(dataSourceMainCollections as any);
|
||||
|
||||
if (enableMultipleDataSource) {
|
||||
app.dataSourceManager.addDataSource(LocalDataSource, dataSource2 as any);
|
||||
}
|
||||
@ -61,36 +121,40 @@ export const getApp = (options: GetAppOptions) => {
|
||||
mockAppApi(app, apis);
|
||||
|
||||
const App = app.getRootComponent();
|
||||
|
||||
return {
|
||||
App,
|
||||
app,
|
||||
};
|
||||
};
|
||||
|
||||
export interface GetAppComponentOptions<V = any, Props = {}> {
|
||||
export interface GetAppComponentOptions<V = any, Props = {}> extends GetAppOptions {
|
||||
schema?: any;
|
||||
appOptions?: AppOrOptions;
|
||||
apis?: MockApis;
|
||||
Component?: ComponentType<Props>;
|
||||
value?: V;
|
||||
props?: Props;
|
||||
onChange?: (value: V) => void;
|
||||
noWrapperSchema?: boolean;
|
||||
enableUserListDataBlock?: boolean;
|
||||
enableMultipleDataSource?: boolean;
|
||||
onChange?: (value: V) => void;
|
||||
}
|
||||
|
||||
export const getAppComponent = (options: GetAppComponentOptions) => {
|
||||
const {
|
||||
schema: optionsSchema = {},
|
||||
Component,
|
||||
enableUserListDataBlock,
|
||||
enableMultipleDataSource,
|
||||
value,
|
||||
props,
|
||||
appOptions,
|
||||
apis,
|
||||
onChange,
|
||||
schema: optionsSchema = {},
|
||||
noWrapperSchema,
|
||||
enableUserListDataBlock,
|
||||
...otherOptions
|
||||
} = options;
|
||||
|
||||
if (noWrapperSchema) {
|
||||
const { App } = getApp(options);
|
||||
return App;
|
||||
}
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
name: 'test',
|
||||
@ -107,6 +171,10 @@ export const getAppComponent = (options: GetAppComponentOptions) => {
|
||||
schema.name = 'test';
|
||||
}
|
||||
|
||||
if (!schema['x-uid']) {
|
||||
schema['x-uid'] = 'test';
|
||||
}
|
||||
|
||||
if (!schema.type) {
|
||||
schema.type = 'void';
|
||||
}
|
||||
@ -123,16 +191,114 @@ export const getAppComponent = (options: GetAppComponentOptions) => {
|
||||
};
|
||||
|
||||
const { App } = getApp({
|
||||
appOptions,
|
||||
apis,
|
||||
...otherOptions,
|
||||
providers: [TestDemo],
|
||||
enableMultipleDataSource,
|
||||
enableUserListDataBlock,
|
||||
});
|
||||
|
||||
return App;
|
||||
};
|
||||
|
||||
export function addXReadPrettyToEachLayer(obj: Record<string, any> = {}) {
|
||||
// 为当前层添加 'x-read-pretty' 属性
|
||||
obj['x-read-pretty'] = true;
|
||||
|
||||
// 递归遍历对象的每个属性
|
||||
_.forOwn(obj, (value, key) => {
|
||||
if (_.isObject(value)) {
|
||||
addXReadPrettyToEachLayer(value);
|
||||
}
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
export const getReadPrettyAppComponent = (options: GetAppComponentOptions) => {
|
||||
return getAppComponent({ ...options, schema: { ...(options.schema || {}), 'x-read-pretty': true } });
|
||||
return getAppComponent({ ...options, schema: addXReadPrettyToEachLayer(options.schema) });
|
||||
};
|
||||
|
||||
interface GetAppComponentWithSchemaSettingsOptions extends GetAppComponentOptions {
|
||||
settingPath?: string;
|
||||
}
|
||||
|
||||
export function setSchemaWithSettings(options: GetAppComponentWithSchemaSettingsOptions) {
|
||||
const { Component, settingPath } = options;
|
||||
const SINGLE_SETTINGS_NAME = 'testSettings';
|
||||
const testSettings = new SchemaSettings({
|
||||
name: SINGLE_SETTINGS_NAME,
|
||||
items: [
|
||||
{
|
||||
name: 'test',
|
||||
Component,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!options.schema) {
|
||||
options.schema = {};
|
||||
}
|
||||
|
||||
if (settingPath) {
|
||||
const schema = get(options.schema, settingPath);
|
||||
schema['x-settings'] = SINGLE_SETTINGS_NAME;
|
||||
} else {
|
||||
options.schema['x-settings'] = SINGLE_SETTINGS_NAME;
|
||||
}
|
||||
|
||||
if (!options.appOptions) {
|
||||
options.appOptions = {};
|
||||
}
|
||||
|
||||
if (options.appOptions instanceof Application) {
|
||||
options.appOptions.schemaSettingsManager.add(testSettings);
|
||||
} else {
|
||||
if (!options.appOptions.schemaSettings) {
|
||||
options.appOptions.schemaSettings = [];
|
||||
}
|
||||
options.appOptions.schemaSettings.push(testSettings);
|
||||
}
|
||||
}
|
||||
|
||||
export const getAppComponentWithSchemaSettings = (options: GetAppComponentWithSchemaSettingsOptions) => {
|
||||
setSchemaWithSettings(options);
|
||||
const App = getAppComponent(options);
|
||||
return App;
|
||||
};
|
||||
|
||||
export const getReadPrettyAppComponentWithSchemaSettings = (options: GetAppComponentWithSchemaSettingsOptions) => {
|
||||
setSchemaWithSettings(options);
|
||||
|
||||
set(options.schema, 'x-read-pretty', true);
|
||||
|
||||
const App = getAppComponent(options);
|
||||
return App;
|
||||
};
|
||||
|
||||
export function withSchema(Component: ComponentType, name?: string) {
|
||||
const ComponentValue = observer((props) => {
|
||||
const schema = useFieldSchema();
|
||||
const schemaValue = pick(schema.toJSON(), [
|
||||
'title',
|
||||
'description',
|
||||
'enum',
|
||||
'x-component-props',
|
||||
'x-decorator-props',
|
||||
'x-linkage-rules',
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
<pre data-testid={name ? `test-schema-${name}` : `test-schema`}>
|
||||
{JSON.stringify(schemaValue, undefined, 2)}
|
||||
</pre>
|
||||
<Component {...props} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ComponentValue.displayName = `withSchema(${Component.displayName || Component.name})`;
|
||||
|
||||
return ComponentValue;
|
||||
}
|
||||
|
||||
export const CommonSchemaComponent = withSchema(function CommonSchemaComponent(props: any) {
|
||||
return <>{props.children}</>;
|
||||
});
|
||||
|
@ -1,30 +0,0 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"f_o3y6p9gf1gx": null,
|
||||
"createdAt": "2023-03-30T07:53:10.941Z",
|
||||
"updatedAt": "2024-04-12T03:27:45.748Z",
|
||||
"appLang": "zh-CN",
|
||||
"createdById": null,
|
||||
"email": "admin@nocobase.com",
|
||||
"f_2ytvt3phlp2": null,
|
||||
"f_3jl554hv7lt": null,
|
||||
"f_51qityssoq1": null,
|
||||
"f_dybwctlb233": null,
|
||||
"f_hbegrnglpv2": null,
|
||||
"f_ndkyrfvh9il": null,
|
||||
"f_o33xmbd62fj": null,
|
||||
"f_t52vqdtfv4h": null,
|
||||
"f_vak0o8efq4v": [],
|
||||
"id": 1,
|
||||
"nickname": "Super Admin",
|
||||
"phone": null,
|
||||
"systemSettings": {
|
||||
"theme": "compact",
|
||||
"themeId": 1
|
||||
},
|
||||
"updatedById": 1,
|
||||
"username": "nocobase"
|
||||
}
|
||||
]
|
||||
}
|
5
packages/core/test/src/web/utils.ts
Normal file
5
packages/core/test/src/web/utils.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const sleep = async (timeout = 0) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, timeout);
|
||||
});
|
||||
};
|
@ -86,7 +86,6 @@ const defineCommonConfig = () => {
|
||||
provider: 'istanbul',
|
||||
include: ['packages/**/src/**/*.{ts,tsx}'],
|
||||
exclude: [
|
||||
'**/requirejs.ts',
|
||||
'**/demos/**',
|
||||
'**/swagger/**',
|
||||
'**/.dumi/**',
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { css } from '@nocobase/client';
|
||||
import { DEFAULT_DATA_SOURCE_KEY, css } from '@nocobase/client';
|
||||
|
||||
import { Instruction, WorkflowVariableRawTextArea, defaultFieldNames } from '@nocobase/plugin-workflow/client';
|
||||
|
||||
@ -22,7 +22,7 @@ export default class extends Instruction {
|
||||
'x-component-props': {
|
||||
className: 'auto-width',
|
||||
filter(item) {
|
||||
return item.options.isDBInstance;
|
||||
return item.options.isDBInstance || item.key === DEFAULT_DATA_SOURCE_KEY;
|
||||
},
|
||||
},
|
||||
default: 'main',
|
||||
|
Loading…
x
Reference in New Issue
Block a user