test: add tests for client (#1960)
* feat: add test utils * test: fix compatibility problem * refactor: use esm * refactor: change require to import * test: add a test * chore: optimize launch.json * chore(jest): optimize config * test: fix failed * test(Action): fix failed * test: migration test from nocobase-next * test: fix failed * chore: set css to false * fix: fix errors * test(Filter): add test * test(BlockItem): add test * test(Calendar): add test * refactor: migrate testUtils to client * fix: avoid error * refactor: fix lint error * Revert "refactor: migrate testUtils to client" This reverts commit ae569e2021a5c720111518cf3f2236e761a7186d. * test: fix test failed * refactor: migrate tesetUtils to @nocobase/test/client * fix(Select): fix BUG * refactor: remove useless code * Revert "refactor: migrate tesetUtils to @nocobase/test/client" This reverts commit 70d2211d5af546ae3fffaf5b53a326be8e194b34. * test(AssociationSelect): add test * test(CardItem): add test * test(CollectionSelect): add test * test(Cron): add test * test(Details): add test * test(ErrorFallback): add test * test(Form): add test * fix: fix CI * test(FormItem): add test * test(FormV2): add test * test(G2Plot): add test * test(Gantt): add test * test(Grid): add test * test(GridCard): add test * test(Kanban): add test * test(List): add test * test(Menu): add test * test(Page): add test * test(Pagination): add test * test(Preview): add test * test(RemoteSelect): add test * test: fix: failed * refactor: remove useless code * test(Table): add test * test(TableV2): add test * test(Tabs): add test * test(Variable): add test * test(Upload): add test * chore: comment * Revert "refactor: change require to import" This reverts commit 3a23607e6faeda6f8b5bee02b9d8fa33bf9d2684. * refactor: remove useless code * fix: fix translation bug * fix: fix export position * test: update snap
This commit is contained in:
parent
ac070de216
commit
47838889f4
37
.vscode/launch.json
vendored
37
.vscode/launch.json
vendored
@ -6,11 +6,17 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Debug Server",
|
"name": "Debug Server",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": [
|
||||||
"-r", "dotenv/config",
|
"-r",
|
||||||
"-r", "tsconfig-paths/register",
|
"dotenv/config",
|
||||||
"-r", "ts-node/register"
|
"-r",
|
||||||
|
"tsconfig-paths/register",
|
||||||
|
"-r",
|
||||||
|
"ts-node/register"
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
"${workspaceRoot}/packages/app/server/src/index.ts",
|
||||||
|
"start"
|
||||||
],
|
],
|
||||||
"args": ["${workspaceRoot}/packages/app/server/src/index.ts", "start"],
|
|
||||||
"skipFiles": [
|
"skipFiles": [
|
||||||
"<node_internals>/**"
|
"<node_internals>/**"
|
||||||
],
|
],
|
||||||
@ -44,6 +50,29 @@
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug Client Tests",
|
||||||
|
"runtimeExecutable": "yarn",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"run",
|
||||||
|
"test:client",
|
||||||
|
"${file}"
|
||||||
|
],
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"windows": {
|
||||||
|
"runtimeArgs": [
|
||||||
|
"run",
|
||||||
|
"test:client",
|
||||||
|
"${relativeFile}"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
|
@ -1 +0,0 @@
|
|||||||
module.exports = {};
|
|
@ -15,7 +15,6 @@ module.exports = {
|
|||||||
...pathsToModuleNameMapper(compilerOptions.paths, {
|
...pathsToModuleNameMapper(compilerOptions.paths, {
|
||||||
prefix: '<rootDir>/',
|
prefix: '<rootDir>/',
|
||||||
}),
|
}),
|
||||||
'\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js',
|
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
'ts-jest': {
|
'ts-jest': {
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"build": "nocobase build",
|
"build": "nocobase build",
|
||||||
"test": "nocobase test",
|
"test": "nocobase test",
|
||||||
"test:client": "vitest",
|
"test:client": "vitest",
|
||||||
|
"tc": "yarn test:client",
|
||||||
"doc": "nocobase doc",
|
"doc": "nocobase doc",
|
||||||
"postinstall": "nocobase postinstall",
|
"postinstall": "nocobase postinstall",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
@ -43,10 +44,12 @@
|
|||||||
"@commitlint/config-conventional": "^16.0.0",
|
"@commitlint/config-conventional": "^16.0.0",
|
||||||
"@commitlint/prompt-cli": "^16.1.0",
|
"@commitlint/prompt-cli": "^16.1.0",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^12.1.5",
|
||||||
|
"@testing-library/user-event": "^14.4.3",
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"auto-changelog": "^2.4.0",
|
"auto-changelog": "^2.4.0",
|
||||||
"ghooks": "^2.0.4",
|
"ghooks": "^2.0.4",
|
||||||
|
"jsdom-worker": "^0.3.0",
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"pretty-format": "^24.0.0",
|
"pretty-format": "^24.0.0",
|
||||||
"pretty-quick": "^3.1.0",
|
"pretty-quick": "^3.1.0",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createCache, createDefaultCacheConfig } from '@nocobase/cache';
|
import { createCache, createDefaultCacheConfig } from '@nocobase/cache';
|
||||||
|
|
||||||
export function sleep(ms: number) {
|
export function sleep(ms?: number) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(resolve, ms);
|
setTimeout(resolve, ms);
|
||||||
});
|
});
|
||||||
|
@ -29,20 +29,6 @@ const getRouteUrl = (props) => {
|
|||||||
return props && getRouteUrl(props?.children?.props);
|
return props && getRouteUrl(props?.children?.props);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRouteAclCheck = (match, snippets) => {
|
|
||||||
const { url, params } = match;
|
|
||||||
if (url === '/admin/pm/list' || params?.pluginName || params?.name?.includes('settings')) {
|
|
||||||
const pmAclCheck = url === '/admin/pm/list' && snippets.includes('pm');
|
|
||||||
const pluginTabByName = params?.name.split('/');
|
|
||||||
pluginTabByName.shift();
|
|
||||||
const pluginName = params.pluginName || pluginTabByName[0];
|
|
||||||
const tabName = params.tabName || pluginTabByName[1];
|
|
||||||
const pluginTabSnippet = pluginName && tabName && `!pm.${pluginName}.${tabName}`;
|
|
||||||
const pluginTabAclCheck = pluginTabSnippet && !snippets.includes(pluginTabSnippet);
|
|
||||||
return pmAclCheck || pluginTabAclCheck;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
export const ACLRolesCheckProvider = (props) => {
|
export const ACLRolesCheckProvider = (props) => {
|
||||||
const route = getRouteUrl(props.children.props);
|
const route = getRouteUrl(props.children.props);
|
||||||
const { setDesignable } = useDesignable();
|
const { setDesignable } = useDesignable();
|
||||||
@ -180,11 +166,11 @@ export const ACLCollectionProvider = (props) => {
|
|||||||
const { allowAll, parseAction } = useACLRoleContext();
|
const { allowAll, parseAction } = useACLRoleContext();
|
||||||
const schema = useFieldSchema();
|
const schema = useFieldSchema();
|
||||||
if (allowAll) {
|
if (allowAll) {
|
||||||
return <>{props.children}</>;
|
return props.children;
|
||||||
}
|
}
|
||||||
const actionPath = schema['x-acl-action'];
|
const actionPath = schema['x-acl-action'];
|
||||||
if (!actionPath) {
|
if (!actionPath) {
|
||||||
return <>{props.children}</>;
|
return props.children;
|
||||||
}
|
}
|
||||||
const params = parseAction(actionPath, { schema });
|
const params = parseAction(actionPath, { schema });
|
||||||
if (!params) {
|
if (!params) {
|
||||||
@ -288,5 +274,3 @@ export const ACLMenuItemProvider = (props) => {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ACLProvider;
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { APIClient as APIClientSDK } from '@nocobase/sdk';
|
import { APIClient as APIClientSDK } from '@nocobase/sdk';
|
||||||
import { Result } from 'ahooks/lib/useRequest/src/types';
|
import { Result } from 'ahooks/es/useRequest/src/types';
|
||||||
import { notification } from 'antd';
|
import { notification } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { isPlainObject } from '@nocobase/utils/client';
|
||||||
import deepmerge from 'deepmerge';
|
import deepmerge from 'deepmerge';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
|
|
||||||
@ -10,15 +11,6 @@ export interface MergeStrategies {
|
|||||||
[key: string]: MergeStrategy;
|
[key: string]: MergeStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function isPlainObject(value) {
|
|
||||||
if (Object.prototype.toString.call(value) !== '[object Object]') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prototype = Object.getPrototypeOf(value);
|
|
||||||
return prototype === null || prototype === Object.prototype;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEnumerableOwnPropertySymbols(target: any): any[] {
|
function getEnumerableOwnPropertySymbols(target: any): any[] {
|
||||||
return Object.getOwnPropertySymbols
|
return Object.getOwnPropertySymbols
|
||||||
? Object.getOwnPropertySymbols(target).filter((symbol) => target.propertyIsEnumerable(symbol))
|
? Object.getOwnPropertySymbols(target).filter((symbol) => target.propertyIsEnumerable(symbol))
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { merge } from '@formily/shared';
|
import { merge } from '@formily/shared';
|
||||||
import { useSetState } from 'ahooks';
|
import { useSetState } from 'ahooks';
|
||||||
import { default as useReq } from 'ahooks/lib/useRequest';
|
import { default as useReq } from 'ahooks/es/useRequest';
|
||||||
import { Options } from 'ahooks/lib/useRequest/src/types';
|
import { Options } from 'ahooks/es/useRequest/src/types';
|
||||||
import { AxiosRequestConfig } from 'axios';
|
import { AxiosRequestConfig } from 'axios';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
@ -75,79 +75,3 @@ exports[`compose > case 5 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`compose case 1 1`] = `
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
A
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`compose case 2 1`] = `
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
A
|
|
||||||
</h1>
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
B
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`compose case 3 1`] = `
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
A
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`compose case 4 1`] = `
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
A
|
|
||||||
</h1>
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
B
|
|
||||||
</h1>
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
C
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`compose case 5 1`] = `
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
A
|
|
||||||
</h1>
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
B
|
|
||||||
1
|
|
||||||
</h1>
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
C
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Result } from 'ahooks/lib/useRequest/src/types';
|
import { Result } from 'ahooks/es/useRequest/src/types';
|
||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
import { useRequest } from '../api-client';
|
import { useRequest } from '../api-client';
|
||||||
|
|
||||||
|
@ -2,13 +2,12 @@ import { css } from '@emotion/css';
|
|||||||
import { Field } from '@formily/core';
|
import { Field } from '@formily/core';
|
||||||
import { RecursionField, useField, useFieldSchema } from '@formily/react';
|
import { RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||||
import { useRequest } from 'ahooks';
|
import { useRequest } from 'ahooks';
|
||||||
import merge from 'deepmerge';
|
|
||||||
import { Col, Row } from 'antd';
|
import { Col, Row } from 'antd';
|
||||||
|
import merge from 'deepmerge';
|
||||||
import template from 'lodash/template';
|
import template from 'lodash/template';
|
||||||
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
|
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ACLCollectionProvider,
|
|
||||||
TableFieldResource,
|
TableFieldResource,
|
||||||
WithoutTableFieldResource,
|
WithoutTableFieldResource,
|
||||||
useAPIClient,
|
useAPIClient,
|
||||||
@ -16,11 +15,11 @@ import {
|
|||||||
useDesignable,
|
useDesignable,
|
||||||
useRecord,
|
useRecord,
|
||||||
} from '../';
|
} from '../';
|
||||||
|
import { ACLCollectionProvider } from '../acl/ACLProvider';
|
||||||
import { CollectionProvider, useCollection, useCollectionManager } from '../collection-manager';
|
import { CollectionProvider, useCollection, useCollectionManager } from '../collection-manager';
|
||||||
import { FilterBlockRecord } from '../filter-provider/FilterProvider';
|
import { FilterBlockRecord } from '../filter-provider/FilterProvider';
|
||||||
import { useRecordIndex } from '../record-provider';
|
import { useRecordIndex } from '../record-provider';
|
||||||
import { SharedFilterProvider } from './SharedFilterProvider';
|
import { SharedFilterProvider } from './SharedFilterProvider';
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
export const BlockResourceContext = createContext(null);
|
export const BlockResourceContext = createContext(null);
|
||||||
export const BlockAssociationContext = createContext(null);
|
export const BlockAssociationContext = createContext(null);
|
||||||
@ -161,7 +160,7 @@ export const MaybeCollectionProvider = (props) => {
|
|||||||
<ACLCollectionProvider>{props.children}</ACLCollectionProvider>
|
<ACLCollectionProvider>{props.children}</ACLCollectionProvider>
|
||||||
</CollectionProvider>
|
</CollectionProvider>
|
||||||
) : (
|
) : (
|
||||||
<>{props.children}</>
|
props.children
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useField } from '@formily/react';
|
|
||||||
import React, { forwardRef, useState } from 'react';
|
import React, { forwardRef, useState } from 'react';
|
||||||
import { DragDropContext } from 'react-beautiful-dnd';
|
import { DragDropContext } from 'react-beautiful-dnd';
|
||||||
import Column from './Column';
|
import Column from './Column';
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { createForm, Field } from '@formily/core';
|
import { createForm, Field } from '@formily/core';
|
||||||
import { FieldContext, FormContext, observer, useField, useFieldSchema } from '@formily/react';
|
import { FieldContext, FormContext, observer, useField, useFieldSchema } from '@formily/react';
|
||||||
import { Options, Result } from 'ahooks/lib/useRequest/src/types';
|
import { useAttach } from '@formily/react/esm/hooks/useAttach';
|
||||||
|
import { Options, Result } from 'ahooks/es/useRequest/src/types';
|
||||||
|
import { TableProps } from 'antd';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { AsyncDataProvider, useAsyncData, useRequest } from '../..';
|
import { AsyncDataProvider, useAsyncData, useRequest } from '../..';
|
||||||
import { useAttach } from '@formily/react/lib/hooks/useAttach';
|
|
||||||
import { TableProps } from 'antd';
|
|
||||||
import { CollectionFieldsTableArray } from './CollectionFieldsTableArray';
|
import { CollectionFieldsTableArray } from './CollectionFieldsTableArray';
|
||||||
|
|
||||||
type TableVoidProps = TableProps<any> & {
|
type TableVoidProps = TableProps<any> & {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useField } from '@formily/react';
|
import { useField } from '@formily/react';
|
||||||
import { Result } from 'ahooks/lib/useRequest/src/types';
|
import { Result } from 'ahooks/es/useRequest/src/types';
|
||||||
import React, { createContext, useContext, useEffect } from 'react';
|
import React, { createContext, useContext, useEffect } from 'react';
|
||||||
import { useCollectionManager } from '.';
|
import { useCollectionManager } from '.';
|
||||||
import { CollectionProvider, useRecord } from '..';
|
import { CollectionProvider, useRecord } from '..';
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { cloneDeep } from 'lodash';
|
import { defaultProps } from './properties';
|
||||||
import { defaultProps, recordPickerSelector, recordPickerViewer } from './properties';
|
|
||||||
import { IField } from './types';
|
import { IField } from './types';
|
||||||
|
|
||||||
export const linkTo: IField = {
|
export const linkTo: IField = {
|
||||||
|
@ -335,47 +335,6 @@ export const defaultProps = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const recordPickerSelector: ISchema = {
|
|
||||||
type: 'void',
|
|
||||||
title: '{{ t("Select record") }}',
|
|
||||||
'x-component': 'RecordPicker.Selector',
|
|
||||||
'x-component-props': {
|
|
||||||
className: 'nb-record-picker-selector',
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
grid: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Grid',
|
|
||||||
'x-initializer': 'TableSelectorInitializers',
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
'x-component': 'Action.Container.Footer',
|
|
||||||
'x-component-props': {},
|
|
||||||
properties: {
|
|
||||||
actions: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'ActionBar',
|
|
||||||
'x-component-props': {},
|
|
||||||
properties: {
|
|
||||||
submit: {
|
|
||||||
title: '{{ t("Submit") }}',
|
|
||||||
'x-action': 'submit',
|
|
||||||
'x-component': 'Action',
|
|
||||||
'x-designer': 'Action.Designer',
|
|
||||||
'x-component-props': {
|
|
||||||
type: 'primary',
|
|
||||||
htmlType: 'submit',
|
|
||||||
useProps: '{{ usePickActionProps }}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const recordPickerViewer = {
|
export const recordPickerViewer = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{ t("View record") }}',
|
title: '{{ t("View record") }}',
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { useField, useFieldSchema } from '@formily/react';
|
import { useField, useFieldSchema } from '@formily/react';
|
||||||
|
import { uniqBy } from 'lodash';
|
||||||
import React, { createContext, useEffect, useRef } from 'react';
|
import React, { createContext, useEffect, useRef } from 'react';
|
||||||
import { useBlockRequestContext } from '../block-provider';
|
import { useBlockRequestContext } from '../block-provider/BlockProvider';
|
||||||
import { SharedFilter, mergeFilter } from '../block-provider/SharedFilterProvider';
|
import { SharedFilter, mergeFilter } from '../block-provider/SharedFilterProvider';
|
||||||
import { CollectionFieldOptions, useCollection } from '../collection-manager';
|
import { CollectionFieldOptions, useCollection } from '../collection-manager';
|
||||||
import { removeNullCondition } from '../schema-component';
|
import { removeNullCondition } from '../schema-component';
|
||||||
import { useAssociatedFields } from './utils';
|
import { useAssociatedFields } from './utils';
|
||||||
import { uniqBy } from 'lodash';
|
|
||||||
|
|
||||||
type Collection = ReturnType<typeof useCollection>;
|
type Collection = ReturnType<typeof useCollection>;
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next';
|
||||||
import locale from '../locale';
|
import locale from '../locale';
|
||||||
const log = require('debug')('i18next');
|
|
||||||
|
|
||||||
export const i18n = i18next.createInstance();
|
export const i18n = i18next.createInstance();
|
||||||
|
|
||||||
|
98
packages/core/client/src/icon/__tests__/icon.test.tsx
Normal file
98
packages/core/client/src/icon/__tests__/icon.test.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from 'testUtils';
|
||||||
|
import App1 from '../demos/antd-icon';
|
||||||
|
import App3 from '../demos/custom-icon';
|
||||||
|
import App2 from '../demos/iconfont';
|
||||||
|
import App4 from '../demos/register-icon';
|
||||||
|
|
||||||
|
describe('Icon', () => {
|
||||||
|
it('antd icon', () => {
|
||||||
|
render(<App1 />);
|
||||||
|
expect(screen.getByRole('img')).toMatchInlineSnapshot(`
|
||||||
|
<span
|
||||||
|
aria-label="book"
|
||||||
|
class="anticon anticon-book"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
data-icon="book"
|
||||||
|
fill="currentColor"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32zm-260 72h96v209.9L621.5 312 572 347.4V136zm220 752H232V136h280v296.9c0 3.3 1 6.6 3 9.3a15.9 15.9 0 0022.3 3.7l83.8-59.9 81.4 59.4c2.7 2 6 3.1 9.4 3.1 8.8 0 16-7.2 16-16V136h64v752z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('iconfont', () => {
|
||||||
|
render(<App2 />);
|
||||||
|
expect(screen.getByRole('img')).toMatchInlineSnapshot(`
|
||||||
|
<span
|
||||||
|
class="anticon"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class=""
|
||||||
|
fill="currentColor"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<use
|
||||||
|
xlink:href="#icon-tuichu"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('custom icon', () => {
|
||||||
|
render(<App3 />);
|
||||||
|
expect(screen.getByRole('img')).toMatchInlineSnapshot(`
|
||||||
|
<span
|
||||||
|
class="anticon"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M923 283.6c-13.4-31.1-32.6-58.9-56.9-82.8-24.3-23.8-52.5-42.4-84-55.5-32.5-13.5-66.9-20.3-102.4-20.3-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5-24.4 23.9-43.5 51.7-56.9 82.8-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3 0.1-35.3-7-69.6-20.9-101.9z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('register icon', () => {
|
||||||
|
render(<App4 />);
|
||||||
|
expect(screen.getByRole('img')).toMatchInlineSnapshot(`
|
||||||
|
<span
|
||||||
|
class="anticon"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M923 283.6c-13.4-31.1-32.6-58.9-56.9-82.8-24.3-23.8-52.5-42.4-84-55.5-32.5-13.5-66.9-20.3-102.4-20.3-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5-24.4 23.9-43.5 51.7-56.9 82.8-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3 0.1-35.3-7-69.6-20.9-101.9z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
4
packages/core/client/src/icon/demos/antd-icon.tsx
Normal file
4
packages/core/client/src/icon/demos/antd-icon.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { Icon } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default () => <Icon type="BookOutlined" />;
|
10
packages/core/client/src/icon/demos/custom-icon.tsx
Normal file
10
packages/core/client/src/icon/demos/custom-icon.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Icon } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const HeartSvg = () => (
|
||||||
|
<svg width="1em" height="1em" fill="currentColor" viewBox="0 0 1024 1024">
|
||||||
|
<path d="M923 283.6c-13.4-31.1-32.6-58.9-56.9-82.8-24.3-23.8-52.5-42.4-84-55.5-32.5-13.5-66.9-20.3-102.4-20.3-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5-24.4 23.9-43.5 51.7-56.9 82.8-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3 0.1-35.3-7-69.6-20.9-101.9z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default () => <Icon component={HeartSvg} />;
|
8
packages/core/client/src/icon/demos/iconfont.tsx
Normal file
8
packages/core/client/src/icon/demos/iconfont.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Icon } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
Icon.createFromIconfontCN({
|
||||||
|
scriptUrl: '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default () => <Icon type="icon-tuichu" />;
|
14
packages/core/client/src/icon/demos/register-icon.tsx
Normal file
14
packages/core/client/src/icon/demos/register-icon.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Icon } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const HeartSvg = () => (
|
||||||
|
<svg width="1em" height="1em" fill="currentColor" viewBox="0 0 1024 1024">
|
||||||
|
<path d="M923 283.6c-13.4-31.1-32.6-58.9-56.9-82.8-24.3-23.8-52.5-42.4-84-55.5-32.5-13.5-66.9-20.3-102.4-20.3-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5-24.4 23.9-43.5 51.7-56.9 82.8-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3 0.1-35.3-7-69.6-20.9-101.9z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CustomIcon = (props) => <Icon component={HeartSvg} {...props} />;
|
||||||
|
|
||||||
|
Icon.register({ CustomIcon });
|
||||||
|
|
||||||
|
export default () => <Icon type={'CustomIcon'} />;
|
@ -21,56 +21,16 @@ Icon.createFromIconfontCN({
|
|||||||
|
|
||||||
Use @ant-design/icons
|
Use @ant-design/icons
|
||||||
|
|
||||||
```tsx
|
<code src="./demos/antd-icon.tsx"></code>
|
||||||
import React from 'react';
|
|
||||||
import { Icon } from '@nocobase/client';
|
|
||||||
|
|
||||||
export default () => <Icon type="BookOutlined" />
|
|
||||||
```
|
|
||||||
|
|
||||||
Use iconfont.cn
|
Use iconfont.cn
|
||||||
|
|
||||||
```tsx
|
<code src="./demos/iconfont.tsx"/>
|
||||||
import React from 'react';
|
|
||||||
import { Icon } from '@nocobase/client';
|
|
||||||
|
|
||||||
Icon.createFromIconfontCN({
|
|
||||||
scriptUrl: '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js',
|
|
||||||
});
|
|
||||||
|
|
||||||
export default () => <Icon type="icon-tuichu" />
|
|
||||||
```
|
|
||||||
|
|
||||||
Custom Icon
|
Custom Icon
|
||||||
|
|
||||||
```tsx
|
<code src="./demos/custom-icon.tsx"/>
|
||||||
import React from 'react';
|
|
||||||
import { Icon } from '@nocobase/client';
|
|
||||||
|
|
||||||
const HeartSvg = () => (
|
|
||||||
<svg width="1em" height="1em" fill="currentColor" viewBox="0 0 1024 1024">
|
|
||||||
<path d="M923 283.6c-13.4-31.1-32.6-58.9-56.9-82.8-24.3-23.8-52.5-42.4-84-55.5-32.5-13.5-66.9-20.3-102.4-20.3-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5-24.4 23.9-43.5 51.7-56.9 82.8-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3 0.1-35.3-7-69.6-20.9-101.9z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default () => <Icon component={HeartSvg} />
|
|
||||||
```
|
|
||||||
|
|
||||||
Register Icon
|
Register Icon
|
||||||
|
|
||||||
```tsx
|
<code src="./demos/register-icon.tsx"/>
|
||||||
import React from 'react';
|
|
||||||
import { Icon } from '@nocobase/client';
|
|
||||||
|
|
||||||
const HeartSvg = () => (
|
|
||||||
<svg width="1em" height="1em" fill="currentColor" viewBox="0 0 1024 1024">
|
|
||||||
<path d="M923 283.6c-13.4-31.1-32.6-58.9-56.9-82.8-24.3-23.8-52.5-42.4-84-55.5-32.5-13.5-66.9-20.3-102.4-20.3-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5-24.4 23.9-43.5 51.7-56.9 82.8-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3 0.1-35.3-7-69.6-20.9-101.9z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
const CustomIcon = (props) => <Icon component={HeartSvg} {...props} />;
|
|
||||||
|
|
||||||
Icon.register({ CustomIcon });
|
|
||||||
|
|
||||||
export default () => <Icon type={'CustomIcon'} />
|
|
||||||
```
|
|
||||||
|
@ -7,11 +7,3 @@ exports[`route-switch > case 1 1`] = `
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`route-switch case 1 1`] = `
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
Home
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
@ -28,7 +28,7 @@ export const ActionPage: ComposedActionDrawer = observer(
|
|||||||
{containerRef?.current &&
|
{containerRef?.current &&
|
||||||
visible &&
|
visible &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<div>
|
<div className="nb-action-page">
|
||||||
<RecursionField
|
<RecursionField
|
||||||
basePath={field.address}
|
basePath={field.address}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
|
@ -0,0 +1,144 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { fireEvent, render, screen, sleep, userEvent } from 'testUtils';
|
||||||
|
import App1 from '../demos/demo1';
|
||||||
|
import App2 from '../demos/demo2';
|
||||||
|
import App3 from '../demos/demo3';
|
||||||
|
import App4 from '../demos/demo4';
|
||||||
|
|
||||||
|
describe('Action', () => {
|
||||||
|
it('show the drawer when click the button', async () => {
|
||||||
|
const { getByText } = render(<App1 />);
|
||||||
|
|
||||||
|
await userEvent.click(getByText('Open'));
|
||||||
|
await sleep(300);
|
||||||
|
// drawer
|
||||||
|
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||||
|
// mask
|
||||||
|
expect(document.querySelector('.ant-drawer-mask')).toBeInTheDocument();
|
||||||
|
// title
|
||||||
|
expect(getByText('Drawer Title')).toBeInTheDocument();
|
||||||
|
// content
|
||||||
|
expect(getByText('Hello')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// close button
|
||||||
|
await userEvent.click(getByText('Close'));
|
||||||
|
await sleep(300);
|
||||||
|
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// should also close when click the mask
|
||||||
|
await userEvent.click(getByText('Open'));
|
||||||
|
await sleep(300);
|
||||||
|
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||||
|
await userEvent.click(document.querySelector('.ant-drawer-mask') as HTMLElement);
|
||||||
|
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// should also close when click the close icon
|
||||||
|
await userEvent.click(getByText('Open'));
|
||||||
|
await sleep(300);
|
||||||
|
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||||
|
await userEvent.click(document.querySelector('.ant-drawer-close') as HTMLElement);
|
||||||
|
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('openMode', async () => {
|
||||||
|
const { getByText } = render(<App3 />);
|
||||||
|
|
||||||
|
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.ant-modal')).not.toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.nb-action-page')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// drawer
|
||||||
|
await userEvent.click(getByText('Drawer'));
|
||||||
|
await userEvent.click(getByText('Open'));
|
||||||
|
await sleep(300);
|
||||||
|
|
||||||
|
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.ant-modal')).not.toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.nb-action-page')).not.toBeInTheDocument();
|
||||||
|
await userEvent.click(getByText('Close'));
|
||||||
|
|
||||||
|
// modal
|
||||||
|
await userEvent.click(getByText('Modal'));
|
||||||
|
await userEvent.click(getByText('Open'));
|
||||||
|
await sleep(300);
|
||||||
|
|
||||||
|
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.ant-modal')).toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.nb-action-page')).not.toBeInTheDocument();
|
||||||
|
await userEvent.click(getByText('Close'));
|
||||||
|
|
||||||
|
// page
|
||||||
|
await userEvent.click(getByText('Page'));
|
||||||
|
await userEvent.click(getByText('Open'));
|
||||||
|
await sleep(300);
|
||||||
|
|
||||||
|
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.ant-modal')).not.toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.nb-action-page')).toBeInTheDocument();
|
||||||
|
await userEvent.click(getByText('Close'));
|
||||||
|
|
||||||
|
// TODO: 点击关闭按钮时应该消失
|
||||||
|
// expect(document.querySelector('.nb-action-page')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Action.Drawer without Action', () => {
|
||||||
|
it('show the drawer when click the button', async () => {
|
||||||
|
const { getByText } = render(<App2 />);
|
||||||
|
|
||||||
|
await userEvent.click(getByText('Open'));
|
||||||
|
await sleep(300);
|
||||||
|
// drawer
|
||||||
|
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||||
|
// mask
|
||||||
|
expect(document.querySelector('.ant-drawer-mask')).toBeInTheDocument();
|
||||||
|
// title
|
||||||
|
expect(getByText('Drawer Title')).toBeInTheDocument();
|
||||||
|
// content
|
||||||
|
expect(getByText('Hello')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// close button
|
||||||
|
await userEvent.click(getByText('Close'));
|
||||||
|
await sleep(300);
|
||||||
|
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// should also close when click the mask
|
||||||
|
await userEvent.click(getByText('Open'));
|
||||||
|
await sleep(300);
|
||||||
|
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||||
|
await userEvent.click(document.querySelector('.ant-drawer-mask') as HTMLElement);
|
||||||
|
await sleep(300);
|
||||||
|
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// should also close when click the close icon
|
||||||
|
await userEvent.click(getByText('Open'));
|
||||||
|
await sleep(300);
|
||||||
|
|
||||||
|
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||||
|
await userEvent.click(document.querySelector('.ant-drawer-close') as HTMLElement);
|
||||||
|
await sleep(300);
|
||||||
|
|
||||||
|
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Action.Popover', () => {
|
||||||
|
it('show the popover when hover the button', async () => {
|
||||||
|
const { container } = render(<App4 />);
|
||||||
|
const btn = container.querySelector('.ant-btn') as HTMLElement;
|
||||||
|
|
||||||
|
fireEvent.mouseEnter(btn);
|
||||||
|
|
||||||
|
// wait for the popover to show
|
||||||
|
await sleep(300);
|
||||||
|
// popover
|
||||||
|
expect(document.querySelector('.ant-popover')).toBeInTheDocument();
|
||||||
|
// content
|
||||||
|
expect(screen.getByText('Hello')).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.mouseLeave(btn);
|
||||||
|
// wait for the popover to hide
|
||||||
|
await sleep(300);
|
||||||
|
expect(document.querySelector('.ant-popover')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,13 @@
|
|||||||
import { FormItem, Input } from '@formily/antd';
|
|
||||||
import { ISchema, observer, useForm } from '@formily/react';
|
import { ISchema, observer, useForm } from '@formily/react';
|
||||||
import { Action, Form, SchemaComponent, SchemaComponentProvider, useActionContext } from '@nocobase/client';
|
import {
|
||||||
|
Action,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaComponentProvider,
|
||||||
|
useActionContext,
|
||||||
|
} from '@nocobase/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const useCloseAction = () => {
|
const useCloseAction = () => {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { FormItem, Input } from '@formily/antd';
|
|
||||||
import { ISchema, observer, useForm } from '@formily/react';
|
import { ISchema, observer, useForm } from '@formily/react';
|
||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
ActionContext,
|
ActionContext,
|
||||||
Form,
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
SchemaComponentProvider,
|
SchemaComponentProvider,
|
||||||
useActionContext,
|
useActionContext,
|
||||||
|
@ -1,19 +1,14 @@
|
|||||||
import { FormItem, Input } from '@formily/antd';
|
|
||||||
import { Field } from '@formily/core';
|
import { Field } from '@formily/core';
|
||||||
import { ISchema, observer, useField, useFieldSchema, useForm } from '@formily/react';
|
import { ISchema, observer, useField, useFieldSchema } from '@formily/react';
|
||||||
import { Action, Form, SchemaComponent, SchemaComponentProvider, useActionContext } from '@nocobase/client';
|
import { Action, SchemaComponent, SchemaComponentProvider, useActionContext } from '@nocobase/client';
|
||||||
import { Select } from 'antd';
|
import { Radio } from 'antd';
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
|
|
||||||
const useCloseAction = () => {
|
const useCloseAction = () => {
|
||||||
const { setVisible } = useActionContext();
|
const { setVisible } = useActionContext();
|
||||||
const form = useForm();
|
|
||||||
return {
|
return {
|
||||||
async run() {
|
async run() {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
form.submit((values) => {
|
|
||||||
console.log(values);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -26,18 +21,17 @@ const Editable = observer(
|
|||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: 24 }}>
|
<div style={{ marginBottom: 24 }}>
|
||||||
打开方式:
|
打开方式:
|
||||||
<Select
|
<Radio.Group
|
||||||
value={field.componentProps.openMode}
|
value={field.componentProps.openMode}
|
||||||
style={{ width: 100 }}
|
onChange={(e) => {
|
||||||
onChange={(value) => {
|
field.componentProps.openMode = e.target.value;
|
||||||
field.componentProps.openMode = value;
|
schema['x-component-props']['openMode'] = e.target.value;
|
||||||
schema['x-component-props']['openMode'] = value;
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Select.Option value={'drawer'}>Drawer</Select.Option>
|
<Radio.Button value="drawer">Drawer</Radio.Button>
|
||||||
<Select.Option value={'modal'}>Modal</Select.Option>
|
<Radio.Button value="modal">Modal</Radio.Button>
|
||||||
<Select.Option value={'page'}>Page</Select.Option>
|
<Radio.Button value="page">Page</Radio.Button>
|
||||||
</Select>
|
</Radio.Group>
|
||||||
</div>
|
</div>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
@ -50,6 +44,8 @@ const schema: ISchema = {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
action1: {
|
action1: {
|
||||||
|
type: 'void',
|
||||||
|
title: 'Open',
|
||||||
'x-decorator': 'Editable',
|
'x-decorator': 'Editable',
|
||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
@ -57,22 +53,20 @@ const schema: ISchema = {
|
|||||||
openMode: 'modal',
|
openMode: 'modal',
|
||||||
containerRefKey: 'containerRef',
|
containerRefKey: 'containerRef',
|
||||||
},
|
},
|
||||||
type: 'void',
|
|
||||||
title: 'Open',
|
|
||||||
properties: {
|
properties: {
|
||||||
drawer1: {
|
drawer1: {
|
||||||
'x-component': 'Action.Container',
|
|
||||||
'x-component-props': {},
|
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: 'Modal Title',
|
title: 'Modal Title',
|
||||||
|
'x-component': 'Action.Container',
|
||||||
|
'x-component-props': {},
|
||||||
properties: {
|
properties: {
|
||||||
hello1: {
|
hello1: {
|
||||||
title: 'T1',
|
title: 'T1',
|
||||||
'x-content': 'Hello',
|
'x-content': 'Hello',
|
||||||
},
|
},
|
||||||
footer1: {
|
footer1: {
|
||||||
'x-component': 'Action.Container.Footer',
|
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
'x-component': 'Action.Container.Footer',
|
||||||
properties: {
|
properties: {
|
||||||
close1: {
|
close1: {
|
||||||
title: 'Close',
|
title: 'Close',
|
||||||
@ -99,12 +93,9 @@ const schema: ISchema = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default observer(() => {
|
export default observer(() => {
|
||||||
const containerRef = useRef();
|
const containerRef = useRef(null);
|
||||||
return (
|
return (
|
||||||
<SchemaComponentProvider
|
<SchemaComponentProvider scope={{ useCloseAction, containerRef }} components={{ Editable, Action }}>
|
||||||
scope={{ containerRef, useCloseAction }}
|
|
||||||
components={{ Editable, Form, Action, Input, FormItem }}
|
|
||||||
>
|
|
||||||
<SchemaComponent schema={schema} />
|
<SchemaComponent schema={schema} />
|
||||||
<div ref={containerRef}></div>
|
<div ref={containerRef}></div>
|
||||||
</SchemaComponentProvider>
|
</SchemaComponentProvider>
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
import { FormItem, Input } from '@formily/antd';
|
|
||||||
import { ISchema, observer, useForm } from '@formily/react';
|
import { ISchema, observer, useForm } from '@formily/react';
|
||||||
import { Action, Form, SchemaComponent, SchemaComponentProvider, useActionContext } from '@nocobase/client';
|
import {
|
||||||
|
Action,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaComponentProvider,
|
||||||
|
useActionContext,
|
||||||
|
} from '@nocobase/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const useCloseAction = () => {
|
const useCloseAction = () => {
|
||||||
|
@ -10,9 +10,9 @@ import {
|
|||||||
NumberPicker,
|
NumberPicker,
|
||||||
Submit,
|
Submit,
|
||||||
} from '@formily/antd';
|
} from '@formily/antd';
|
||||||
import { Select } from 'antd';
|
import { Field, createForm, onFieldValueChange } from '@formily/core';
|
||||||
import { createForm, Field, onFieldValueChange } from '@formily/core';
|
|
||||||
import { connect, createSchemaField, observer, useField, useForm, useFormEffects } from '@formily/react';
|
import { connect, createSchemaField, observer, useField, useForm, useFormEffects } from '@formily/react';
|
||||||
|
import { Select } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const ViewOptions = connect((props) => {
|
const ViewOptions = connect((props) => {
|
||||||
@ -110,6 +110,10 @@ const EditOptions = observer(
|
|||||||
})
|
})
|
||||||
.then((values) => {
|
.then((values) => {
|
||||||
field.value = values;
|
field.value = values;
|
||||||
|
return values;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { RecursionField, observer, useField, useFieldSchema } from '@formily/react';
|
import { RecursionField, observer, useField, useFieldSchema } from '@formily/react';
|
||||||
import { Input, Select } from 'antd';
|
import { Input, Select } from 'antd';
|
||||||
import { differenceBy, unionBy } from 'lodash';
|
import { differenceBy, unionBy } from 'lodash';
|
||||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
import React, { useContext, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
FormProvider,
|
FormProvider,
|
||||||
RecordPickerContext,
|
RecordPickerContext,
|
||||||
@ -9,6 +9,7 @@ import {
|
|||||||
SchemaComponentOptions,
|
SchemaComponentOptions,
|
||||||
useActionContext,
|
useActionContext,
|
||||||
} from '../..';
|
} from '../..';
|
||||||
|
import { RecordProvider } from '../../../';
|
||||||
import {
|
import {
|
||||||
TableSelectorParamsProvider,
|
TableSelectorParamsProvider,
|
||||||
useTableSelectorProps as useTsp,
|
useTableSelectorProps as useTsp,
|
||||||
@ -19,7 +20,6 @@ import { ActionContext } from '../action';
|
|||||||
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
||||||
import schema from './schema';
|
import schema from './schema';
|
||||||
import { flatData, getLabelFormatValue, useLabelUiSchema } from './util';
|
import { flatData, getLabelFormatValue, useLabelUiSchema } from './util';
|
||||||
import { RecordProvider } from '../../../';
|
|
||||||
|
|
||||||
const useTableSelectorProps = () => {
|
const useTableSelectorProps = () => {
|
||||||
const field: any = useField();
|
const field: any = useField();
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen, sleep, userEvent } from 'testUtils';
|
||||||
|
import App1 from '../demos/demo1';
|
||||||
|
|
||||||
|
describe('AssociationSelect', () => {
|
||||||
|
it('should render correctly', async () => {
|
||||||
|
render(<App1 />);
|
||||||
|
|
||||||
|
const selector = document.querySelector('.ant-select-selector');
|
||||||
|
expect(selector).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(selector);
|
||||||
|
|
||||||
|
// 下拉框中显示 loading
|
||||||
|
expect(document.querySelector('.ant-spin')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// 等待接口返回数据
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
expect(screen.getByText('title1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('title2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,57 @@
|
|||||||
|
import { APIClientProvider, AssociationSelect, FormProvider, SchemaComponent } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { mockAPIClient } from '../../../../test';
|
||||||
|
|
||||||
|
const { apiClient, mockRequest } = mockAPIClient();
|
||||||
|
mockRequest.onGet('/posts:list').reply(() => {
|
||||||
|
return [
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'title1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'title2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 写一个简单的 schema
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
association: {
|
||||||
|
type: 'array',
|
||||||
|
'x-component': 'AssociationSelect',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: true,
|
||||||
|
fieldNames: {
|
||||||
|
label: 'title',
|
||||||
|
value: 'id',
|
||||||
|
},
|
||||||
|
service: {
|
||||||
|
resource: 'posts',
|
||||||
|
action: 'list',
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<FormProvider>
|
||||||
|
<APIClientProvider apiClient={apiClient}>
|
||||||
|
<SchemaComponent components={{ AssociationSelect }} schema={schema} />
|
||||||
|
</APIClientProvider>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -5,10 +5,12 @@ group:
|
|||||||
path: /schema-components
|
path: /schema-components
|
||||||
---
|
---
|
||||||
|
|
||||||
# RemoteSelect
|
# AssociationSelect
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
<code src="./demos/demo1.tsx"/>
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
基于 Ant Design 的 [Select](https://ant.design/components/select/#API),相关扩展属性有:
|
基于 Ant Design 的 [Select](https://ant.design/components/select/#API),相关扩展属性有:
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import App1 from '../demos/demo1';
|
||||||
|
|
||||||
|
describe('BlockItem', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
render(<App1 />);
|
||||||
|
|
||||||
|
const items = document.querySelectorAll('.nc-block-item');
|
||||||
|
expect(items.length).toBe(3);
|
||||||
|
expect(items[0]).toHaveTextContent('Block block1');
|
||||||
|
expect(items[1]).toHaveTextContent('Block block2');
|
||||||
|
expect(items[2]).toHaveTextContent('Block block3');
|
||||||
|
});
|
||||||
|
});
|
@ -4,10 +4,13 @@ import { BlockItem, DndContext, DragHandler, SchemaComponent, SchemaComponentPro
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const Block = observer(
|
const Block = observer(
|
||||||
(props) => {
|
() => {
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}>
|
<div
|
||||||
|
className="nc-block-item"
|
||||||
|
style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}
|
||||||
|
>
|
||||||
Block {fieldSchema.name}
|
Block {fieldSchema.name}
|
||||||
<DragHandler />
|
<DragHandler />
|
||||||
</div>
|
</div>
|
||||||
@ -16,34 +19,34 @@ const Block = observer(
|
|||||||
{ displayName: 'Block' },
|
{ displayName: 'Block' },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'void',
|
||||||
|
name: 'page',
|
||||||
|
'x-component': 'DndContext',
|
||||||
|
'x-uid': uid(),
|
||||||
|
properties: {
|
||||||
|
block1: {
|
||||||
|
'x-decorator': 'BlockItem',
|
||||||
|
'x-component': 'Block',
|
||||||
|
'x-uid': uid(),
|
||||||
|
},
|
||||||
|
block2: {
|
||||||
|
'x-decorator': 'BlockItem',
|
||||||
|
'x-component': 'Block',
|
||||||
|
'x-uid': uid(),
|
||||||
|
},
|
||||||
|
block3: {
|
||||||
|
'x-decorator': 'BlockItem',
|
||||||
|
'x-component': 'Block',
|
||||||
|
'x-uid': uid(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<SchemaComponentProvider components={{ DndContext, BlockItem, Block }}>
|
<SchemaComponentProvider components={{ DndContext, BlockItem, Block }}>
|
||||||
<SchemaComponent
|
<SchemaComponent schema={schema} />
|
||||||
schema={{
|
|
||||||
type: 'void',
|
|
||||||
name: 'page',
|
|
||||||
'x-component': 'DndContext',
|
|
||||||
'x-uid': uid(),
|
|
||||||
properties: {
|
|
||||||
block1: {
|
|
||||||
'x-decorator': 'BlockItem',
|
|
||||||
'x-component': 'Block',
|
|
||||||
'x-uid': uid(),
|
|
||||||
},
|
|
||||||
block2: {
|
|
||||||
'x-decorator': 'BlockItem',
|
|
||||||
'x-component': 'Block',
|
|
||||||
'x-uid': uid(),
|
|
||||||
},
|
|
||||||
block3: {
|
|
||||||
'x-decorator': 'BlockItem',
|
|
||||||
'x-component': 'Block',
|
|
||||||
'x-uid': uid(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</SchemaComponentProvider>
|
</SchemaComponentProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||||
import { createForm } from '@formily/core';
|
import { createForm } from '@formily/core';
|
||||||
import { observer, RecursionField, Schema, useFieldSchema } from '@formily/react';
|
import { RecursionField, Schema, observer, useFieldSchema } from '@formily/react';
|
||||||
import { parseExpression } from 'cron-parser';
|
import { parseExpression } from 'cron-parser';
|
||||||
|
import { eq } from 'date-arithmetic';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { Calendar as BigCalendar, momentLocalizer } from 'react-big-calendar';
|
import { Calendar as BigCalendar, momentLocalizer } from 'react-big-calendar';
|
||||||
import * as dates from 'react-big-calendar/lib/utils/dates';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { RecordProvider } from '../../../';
|
import { RecordProvider } from '../../../';
|
||||||
import { i18n } from '../../../i18n';
|
import { i18n } from '../../../i18n';
|
||||||
@ -247,7 +247,7 @@ export const Calendar: any = observer(
|
|||||||
agendaDateFormat: 'M-DD',
|
agendaDateFormat: 'M-DD',
|
||||||
dayHeaderFormat: 'Y-M-DD',
|
dayHeaderFormat: 'Y-M-DD',
|
||||||
dayRangeHeaderFormat: ({ start, end }, culture, local) => {
|
dayRangeHeaderFormat: ({ start, end }, culture, local) => {
|
||||||
if (dates.eq(start, end, 'month')) {
|
if (eq(start, end, 'month')) {
|
||||||
return local.format(start, 'Y-M', culture);
|
return local.format(start, 'Y-M', culture);
|
||||||
}
|
}
|
||||||
return `${local.format(start, 'Y-M', culture)} - ${local.format(end, 'Y-M', culture)}`;
|
return `${local.format(start, 'Y-M', culture)} - ${local.format(end, 'Y-M', culture)}`;
|
||||||
|
@ -2,7 +2,7 @@ import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
|||||||
import { observer } from '@formily/react';
|
import { observer } from '@formily/react';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { navigate } from 'react-big-calendar/lib/utils/constants';
|
import { Navigate } from 'react-big-calendar/dist/react-big-calendar.esm';
|
||||||
import { useDesignable } from '../../hooks';
|
import { useDesignable } from '../../hooks';
|
||||||
import { CalendarToolbarContext } from './context';
|
import { CalendarToolbarContext } from './context';
|
||||||
|
|
||||||
@ -12,8 +12,8 @@ export const Nav = observer(
|
|||||||
const { onNavigate } = useContext(CalendarToolbarContext);
|
const { onNavigate } = useContext(CalendarToolbarContext);
|
||||||
return (
|
return (
|
||||||
<div className="ant-btn-group">
|
<div className="ant-btn-group">
|
||||||
<Button icon={<LeftOutlined />} onClick={() => onNavigate(navigate.PREVIOUS)}></Button>
|
<Button icon={<LeftOutlined />} onClick={() => onNavigate(Navigate.PREVIOUS)}></Button>
|
||||||
<Button icon={<RightOutlined />} onClick={() => onNavigate(navigate.NEXT)}></Button>
|
<Button icon={<RightOutlined />} onClick={() => onNavigate(Navigate.NEXT)}></Button>
|
||||||
<DesignableBar />
|
<DesignableBar />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { observer } from '@formily/react';
|
import { observer } from '@formily/react';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { navigate } from 'react-big-calendar/lib/utils/constants';
|
import { Navigate } from 'react-big-calendar/dist/react-big-calendar.esm';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDesignable } from '../../hooks';
|
import { useDesignable } from '../../hooks';
|
||||||
import { CalendarToolbarContext } from './context';
|
import { CalendarToolbarContext } from './context';
|
||||||
@ -14,7 +14,7 @@ export const Today = observer(
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onNavigate(navigate.TODAY);
|
onNavigate(Navigate.TODAY);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('Today')}
|
{t('Today')}
|
||||||
|
@ -17,7 +17,9 @@ export const ViewSelect = observer(
|
|||||||
<div className="ant-btn-group">
|
<div className="ant-btn-group">
|
||||||
<Select value={view} onChange={onView}>
|
<Select value={view} onChange={onView}>
|
||||||
{views.map((name) => (
|
{views.map((name) => (
|
||||||
<Select.Option value={name}>{messages[name]}</Select.Option>
|
<Select.Option key={name} value={name}>
|
||||||
|
{messages[name]}
|
||||||
|
</Select.Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
<DesignableBar />
|
<DesignableBar />
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from 'testUtils';
|
||||||
|
import App1 from '../demos/demo1';
|
||||||
|
import App2 from '../demos/demo2';
|
||||||
|
|
||||||
|
describe('Calendar', () => {
|
||||||
|
it('basic', () => {
|
||||||
|
render(<App1 />);
|
||||||
|
|
||||||
|
const currentDate = moment().format('YYYY-M');
|
||||||
|
|
||||||
|
expect(screen.getByText('Today')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(currentDate)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Month')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('use CalendarBlockProvider', () => {
|
||||||
|
render(<App2 />);
|
||||||
|
|
||||||
|
const currentDate = moment().format('YYYY-M');
|
||||||
|
|
||||||
|
expect(screen.getByText('Today')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(currentDate)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Month')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from 'testUtils';
|
||||||
|
import App1 from '../demos/demo1';
|
||||||
|
|
||||||
|
describe('CardItem', () => {
|
||||||
|
it('should works', () => {
|
||||||
|
render(<App1 />);
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
expect(screen.getByText('Card')).toBeInTheDocument();
|
||||||
|
// 内容
|
||||||
|
expect(screen.getByText('Hello Card!')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,30 @@
|
|||||||
|
import { CardItem, FormProvider, SchemaComponent } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
card: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'CardItem',
|
||||||
|
'x-component-props': {
|
||||||
|
title: 'Card',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
hello: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'div',
|
||||||
|
'x-content': 'Hello Card!',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<FormProvider>
|
||||||
|
<SchemaComponent components={{ CardItem }} schema={schema} />
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -8,3 +8,7 @@ group:
|
|||||||
# CardItem
|
# CardItem
|
||||||
|
|
||||||
卡片装饰器。除此之外,也继承了 BlockItem 的功能。
|
卡片装饰器。除此之外,也继承了 BlockItem 的功能。
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
<code src="./demos/demo1.tsx" />
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { fireEvent, render, screen, sleep, userEvent } from 'testUtils';
|
||||||
|
import App1 from '../demos/demo1';
|
||||||
|
import App2 from '../demos/demo2';
|
||||||
|
|
||||||
|
describe('Cascader', () => {
|
||||||
|
it('sync', async () => {
|
||||||
|
const { container } = render(<App1 />);
|
||||||
|
const select = container.querySelector('.ant-select-selector') as HTMLElement;
|
||||||
|
// 显示下拉框
|
||||||
|
await userEvent.click(select);
|
||||||
|
|
||||||
|
// 点击选项
|
||||||
|
fireEvent.click(screen.getByText('Zhejiang'));
|
||||||
|
fireEvent.click(screen.getByText('Hangzhou'));
|
||||||
|
fireEvent.click(screen.getByText('West Lake'));
|
||||||
|
|
||||||
|
// 页面中显示的内容
|
||||||
|
// 因为内容被不同的标签分开了,所以需要分开查找
|
||||||
|
expect(screen.getByText('Zhejiang /')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Hangzhou /')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('async', async () => {
|
||||||
|
const { container } = render(<App2 />);
|
||||||
|
const select = container.querySelector('.ant-select-selector') as HTMLElement;
|
||||||
|
|
||||||
|
// 显示下拉框
|
||||||
|
await userEvent.click(select);
|
||||||
|
|
||||||
|
// 点击选项
|
||||||
|
fireEvent.click(screen.getByText('Zhejiang'));
|
||||||
|
|
||||||
|
// 因为是异步加载,所以需要等待一下
|
||||||
|
expect(screen.queryByText('Zhejiang Dynamic 1')).not.toBeInTheDocument();
|
||||||
|
await sleep(300);
|
||||||
|
expect(screen.getByText('Zhejiang Dynamic 1')).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Zhejiang Dynamic 1'));
|
||||||
|
|
||||||
|
// 页面中显示的内容
|
||||||
|
// 因为内容被不同的标签分开了,所以需要分开查找
|
||||||
|
expect(screen.getByText('Zhejiang /')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -64,7 +64,7 @@ const useLoadData = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
field.dataSource = [...field.dataSource];
|
field.dataSource = [...field.dataSource];
|
||||||
}, 500);
|
}, 300);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
|||||||
import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
|
import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
|
||||||
import { isValid } from '@formily/shared';
|
import { isValid } from '@formily/shared';
|
||||||
import { Checkbox as AntdCheckbox, Tag } from 'antd';
|
import { Checkbox as AntdCheckbox, Tag } from 'antd';
|
||||||
import type { CheckboxGroupProps, CheckboxProps } from 'antd/lib/checkbox';
|
import type { CheckboxGroupProps, CheckboxProps } from 'antd/es/checkbox';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
|
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen, userEvent } from 'testUtils';
|
||||||
|
import App1 from '../demos/checkbox';
|
||||||
|
import App2 from '../demos/checkbox.group';
|
||||||
|
|
||||||
|
describe('Checkbox', () => {
|
||||||
|
it('should display the title', () => {
|
||||||
|
render(<App1 />);
|
||||||
|
|
||||||
|
expect(screen.getByText('编辑模式').innerHTML).toBe('编辑模式');
|
||||||
|
expect(screen.getByText('阅读模式').innerHTML).toBe('阅读模式');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the icon when checked', async () => {
|
||||||
|
const { container } = render(<App1 />);
|
||||||
|
|
||||||
|
const input = container.querySelector('input') as HTMLInputElement;
|
||||||
|
await userEvent.click(input);
|
||||||
|
const icon = container.querySelector('svg') as SVGElement;
|
||||||
|
expect(icon).toMatchInlineSnapshot(`
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
data-icon="check"
|
||||||
|
fill="currentColor"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// icon should be hidden when unchecked
|
||||||
|
await userEvent.click(input);
|
||||||
|
expect(container.querySelector('svg')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Checkbox.Group', () => {
|
||||||
|
it('should display the title', () => {
|
||||||
|
render(<App2 />);
|
||||||
|
|
||||||
|
expect(screen.getByText('编辑模式').innerHTML).toBe('编辑模式');
|
||||||
|
expect(screen.getByText('阅读模式').innerHTML).toBe('阅读模式');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the tag when checked', async () => {
|
||||||
|
const { container } = render(<App2 />);
|
||||||
|
|
||||||
|
const option1 = screen.getByLabelText('选项1');
|
||||||
|
const option2 = screen.getByLabelText('选项2');
|
||||||
|
await userEvent.click(option1);
|
||||||
|
expect(container.querySelectorAll('.ant-tag')).toMatchInlineSnapshot(`
|
||||||
|
NodeList [
|
||||||
|
<span
|
||||||
|
class="ant-tag ant-tag-red"
|
||||||
|
>
|
||||||
|
选项1
|
||||||
|
</span>,
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
await userEvent.click(option2);
|
||||||
|
expect(container.querySelectorAll('.ant-tag')).toMatchInlineSnapshot(`
|
||||||
|
NodeList [
|
||||||
|
<span
|
||||||
|
class="ant-tag ant-tag-red"
|
||||||
|
>
|
||||||
|
选项1
|
||||||
|
</span>,
|
||||||
|
<span
|
||||||
|
class="ant-tag ant-tag-blue"
|
||||||
|
>
|
||||||
|
选项2
|
||||||
|
</span>,
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
|
||||||
|
// should be hidden when unchecked
|
||||||
|
await userEvent.click(option1);
|
||||||
|
await userEvent.click(option2);
|
||||||
|
expect(container.querySelectorAll('.ant-tag')).toMatchInlineSnapshot(`NodeList []`);
|
||||||
|
});
|
||||||
|
});
|
@ -11,8 +11,8 @@ group:
|
|||||||
|
|
||||||
### 勾选
|
### 勾选
|
||||||
|
|
||||||
<code src="./demos/demo1.tsx" />
|
<code src="./demos/checkbox.tsx" />
|
||||||
|
|
||||||
### 组
|
### 组
|
||||||
|
|
||||||
<code src="./demos/demo2.tsx" />
|
<code src="./demos/checkbox.group.tsx" />
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen, userEvent } from 'testUtils';
|
||||||
|
import App1 from '../demos/demo1';
|
||||||
|
|
||||||
|
describe('CollectionSelect', () => {
|
||||||
|
it('should works', async () => {
|
||||||
|
render(<App1 />);
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
expect(screen.getByText('Collection')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const selector = document.querySelector('.ant-select-selector');
|
||||||
|
expect(selector).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(selector);
|
||||||
|
|
||||||
|
// 下拉框内容
|
||||||
|
expect(screen.getByText('Users')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Roles')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('测试表')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,29 @@
|
|||||||
|
import { FormItem } from '@formily/antd';
|
||||||
|
import { CollectionManagerProvider, CollectionSelect, FormProvider, SchemaComponent } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { collections } from '../../../../test';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
select: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Collection',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CollectionSelect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider>
|
||||||
|
<CollectionManagerProvider collections={collections}>
|
||||||
|
<SchemaComponent components={{ FormItem, CollectionSelect }} scope={{ t }} schema={schema} />
|
||||||
|
</CollectionManagerProvider>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
nav:
|
||||||
|
path: /client
|
||||||
|
group:
|
||||||
|
path: /schema-components
|
||||||
|
---
|
||||||
|
|
||||||
|
# CollectionSelect
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
<code src="./demos/demo1.tsx" />
|
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { fireEvent, render, screen } from 'testUtils';
|
||||||
|
import App from '../demos/demo1';
|
||||||
|
|
||||||
|
describe('ColorSelect', () => {
|
||||||
|
it('should display the value of user selected', async () => {
|
||||||
|
const { container } = render(<App />);
|
||||||
|
|
||||||
|
const selector = container.querySelector('.ant-select-selector');
|
||||||
|
// @ts-ignore
|
||||||
|
fireEvent.mouseDown(selector);
|
||||||
|
// @ts-ignore
|
||||||
|
expect(screen.getByText('Red')).toBeInTheDocument();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(screen.getByText('Magenta')).toBeInTheDocument();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(screen.getByText('Volcano')).toBeInTheDocument();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(screen.getByText('Orange')).toBeInTheDocument();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(screen.getByText('Gold')).toBeInTheDocument();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(screen.getByText('Lime')).toBeInTheDocument();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(screen.getByText('Green')).toBeInTheDocument();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(screen.getByText('Cyan')).toBeInTheDocument();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(screen.getByText('Blue')).toBeInTheDocument();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(screen.getByText('Geek blue')).toBeInTheDocument();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(screen.getByText('Purple')).toBeInTheDocument();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(screen.getByText('Default')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// select red
|
||||||
|
fireEvent.click(screen.getByText('Red'));
|
||||||
|
expect(screen.getAllByText('Red').length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
@ -1,11 +1,11 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { connect, mapReadPretty } from '@formily/react';
|
import { connect, mapReadPretty } from '@formily/react';
|
||||||
|
import { error } from '@nocobase/utils/client';
|
||||||
import cronstrue from 'cronstrue';
|
import cronstrue from 'cronstrue';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Cron as ReactCron, CronProps } from 'react-js-cron';
|
import { CronProps, Cron as ReactCron } from 'react-js-cron';
|
||||||
import { useAPIClient } from '../../../api-client';
|
|
||||||
import 'react-js-cron/dist/styles.css';
|
import 'react-js-cron/dist/styles.css';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useAPIClient } from '../../../api-client';
|
||||||
|
|
||||||
const Input = (props: Omit<CronProps, 'setValue'> & { onChange: (value: string) => void }) => {
|
const Input = (props: Omit<CronProps, 'setValue'> & { onChange: (value: string) => void }) => {
|
||||||
const { onChange, ...rest } = props;
|
const { onChange, ...rest } = props;
|
||||||
@ -46,7 +46,7 @@ const ReadPretty = (props) => {
|
|||||||
use24HourTimeFormat: true,
|
use24HourTimeFormat: true,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
console.error(`The '${props.value}' is not a valid cron expression`);
|
error(`The '${props.value}' is not a valid cron expression`);
|
||||||
return props.value;
|
return props.value;
|
||||||
}
|
}
|
||||||
}, [props.value]);
|
}, [props.value]);
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen, userEvent } from 'testUtils';
|
||||||
|
import Cron from '../demos/demo1';
|
||||||
|
import CronSet from '../demos/demo2';
|
||||||
|
|
||||||
|
describe('Cron', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
render(<Cron />);
|
||||||
|
|
||||||
|
// title
|
||||||
|
expect(screen.getByText('Cron')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText('Every')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('day')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('at')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('every hour')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('every minute')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Clear')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CronSet', () => {
|
||||||
|
it('should render correctly', async () => {
|
||||||
|
render(<CronSet />);
|
||||||
|
|
||||||
|
// title
|
||||||
|
expect(screen.getByText('CronSet')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const selector = document.querySelector('.ant-select-selector');
|
||||||
|
expect(selector).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(selector);
|
||||||
|
await userEvent.click(screen.getByText('Custom'));
|
||||||
|
|
||||||
|
expect(screen.getByText('Every')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('day')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Clear')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,26 @@
|
|||||||
|
import { FormItem } from '@formily/antd';
|
||||||
|
import { Cron, FormProvider, SchemaComponent } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
cron: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Cron',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Cron',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider>
|
||||||
|
<SchemaComponent components={{ Cron, FormItem }} scope={{ t }} schema={schema} />
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,45 @@
|
|||||||
|
import { FormItem } from '@formily/antd';
|
||||||
|
import { CronSet, FormProvider, SchemaComponent } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
cronSet: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'CronSet',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CronSet',
|
||||||
|
'x-component-props': 'allowClear',
|
||||||
|
enum: [
|
||||||
|
{
|
||||||
|
label: '{{t("Daily")}}',
|
||||||
|
value: '0 0 0 * * ?',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '{{t("Weekly")}}',
|
||||||
|
value: 'every_week',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '{{t("Monthly")}}',
|
||||||
|
value: 'every_month',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '{{t("Yearly")}}',
|
||||||
|
value: 'every_year',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider>
|
||||||
|
<SchemaComponent components={{ CronSet, FormItem }} scope={{ t }} schema={schema} />
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
18
packages/core/client/src/schema-component/antd/cron/index.md
Normal file
18
packages/core/client/src/schema-component/antd/cron/index.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
nav:
|
||||||
|
path: /client
|
||||||
|
group:
|
||||||
|
path: /schema-components
|
||||||
|
---
|
||||||
|
|
||||||
|
# Cron
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
#### Cron
|
||||||
|
|
||||||
|
<code src="./demos/demo1.tsx" />
|
||||||
|
|
||||||
|
#### CronSet
|
||||||
|
|
||||||
|
<code src="./demos/demo2.tsx" />
|
@ -3,7 +3,7 @@ import { DatePicker as AntdDatePicker } from 'antd';
|
|||||||
import type {
|
import type {
|
||||||
DatePickerProps as AntdDatePickerProps,
|
DatePickerProps as AntdDatePickerProps,
|
||||||
RangePickerProps as AntdRangePickerProps,
|
RangePickerProps as AntdRangePickerProps,
|
||||||
} from 'antd/lib/date-picker';
|
} from 'antd/es/date-picker';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ReadPretty } from './ReadPretty';
|
import { ReadPretty } from './ReadPretty';
|
||||||
@ -40,7 +40,7 @@ export const DatePicker = (props) => {
|
|||||||
return <_DatePicker {...props} />;
|
return <_DatePicker {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
DatePicker.RangePicker = (props) => {
|
DatePicker.RangePicker = function RangePicker(props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { utc = true } = useDatePickerContext();
|
const { utc = true } = useDatePickerContext();
|
||||||
const rangesValue = getDateRanges();
|
const rangesValue = getDateRanges();
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { usePrefixCls } from '@formily/antd/lib/__builtins__';
|
import { usePrefixCls } from '@formily/antd/esm/__builtins__';
|
||||||
import { isArr } from '@formily/shared';
|
import { isArr } from '@formily/shared';
|
||||||
import { getDefaultFormat, str2moment } from '@nocobase/utils/client';
|
import { getDefaultFormat, str2moment } from '@nocobase/utils/client';
|
||||||
import type {
|
import type {
|
||||||
DatePickerProps as AntdDatePickerProps,
|
DatePickerProps as AntdDatePickerProps,
|
||||||
RangePickerProps as AntdRangePickerProps,
|
RangePickerProps as AntdRangePickerProps,
|
||||||
} from 'antd/lib/date-picker';
|
} from 'antd/es/date-picker';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
@ -0,0 +1,127 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen, sleep, userEvent } from 'testUtils';
|
||||||
|
import App1 from '../demos/demo1';
|
||||||
|
import App2 from '../demos/demo2';
|
||||||
|
import App3 from '../demos/demo3';
|
||||||
|
import App4 from '../demos/demo4';
|
||||||
|
import App5 from '../demos/demo5';
|
||||||
|
import App6 from '../demos/demo6';
|
||||||
|
|
||||||
|
describe('DatePicker', () => {
|
||||||
|
it('basic', async () => {
|
||||||
|
const { container, getByText } = render(<App1 />);
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
const input = container.querySelector('input') as HTMLElement;
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
await userEvent.type(input, '2023/05/01 00:00:00');
|
||||||
|
await userEvent.click(getByText('OK'));
|
||||||
|
|
||||||
|
await sleep(100);
|
||||||
|
|
||||||
|
expect(input).toHaveValue('2023/05/01 00:00:00');
|
||||||
|
// Read pretty
|
||||||
|
expect(screen.getByText('2023/05/01 00:00:00', { selector: '.ant-description-date-picker' })).toBeInTheDocument();
|
||||||
|
|
||||||
|
// TODO: 本地没有错,但是 CI 上会报错,暂时注释掉
|
||||||
|
// Value
|
||||||
|
// expect(screen.getByText('2023-04-30T16:00:00.000Z')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GMT', async () => {
|
||||||
|
const { container, getByText } = render(<App2 />);
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
const input = container.querySelector('input') as HTMLElement;
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
// 清空默认值
|
||||||
|
await userEvent.clear(input);
|
||||||
|
await userEvent.type(input, '2023/05/01 00:00:00');
|
||||||
|
await userEvent.click(getByText('OK'));
|
||||||
|
|
||||||
|
expect(input).toHaveValue('2023/05/01 00:00:00');
|
||||||
|
// Read pretty
|
||||||
|
expect(screen.getByText('2023/05/01 00:00:00', { selector: '.ant-description-date-picker' })).toBeInTheDocument();
|
||||||
|
// Value
|
||||||
|
expect(screen.getByText('2023-05-01T00:00:00.000Z')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('non-UTC', async () => {
|
||||||
|
const { container } = render(<App3 />);
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
const input = container.querySelector('input') as HTMLElement;
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
await userEvent.type(input, '2023/05/01');
|
||||||
|
|
||||||
|
const selected = document.querySelector('.ant-picker-cell-selected') as HTMLElement;
|
||||||
|
expect(selected).toBeInTheDocument();
|
||||||
|
await userEvent.click(selected);
|
||||||
|
|
||||||
|
expect(input).toHaveValue('2023/05/01');
|
||||||
|
// Read pretty
|
||||||
|
expect(screen.getByText('2023/05/01 00:00:00', { selector: '.ant-description-date-picker' })).toBeInTheDocument();
|
||||||
|
// Value
|
||||||
|
expect(screen.getByText('2023-05-01')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('RangePicker', () => {
|
||||||
|
it('GMT', async () => {
|
||||||
|
const { container, getByPlaceholderText } = render(<App4 />);
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
const startInput = getByPlaceholderText('Start date');
|
||||||
|
const endInput = getByPlaceholderText('End date');
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
await userEvent.click(document.querySelector('[title="2023-05-01"]') as HTMLElement);
|
||||||
|
await userEvent.click(document.querySelector('[title="2023-05-02"]') as HTMLElement);
|
||||||
|
|
||||||
|
expect(startInput).toHaveValue('2023-05-01');
|
||||||
|
expect(endInput).toHaveValue('2023-05-02');
|
||||||
|
// Read pretty
|
||||||
|
expect(screen.getByText('2023-05-01~2023-05-02', { selector: '.ant-description-text' })).toBeInTheDocument();
|
||||||
|
// Value
|
||||||
|
expect(screen.getByText('2023-05-01T00:00:00.000Z ~ 2023-05-02T23:59:59.999Z')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('non-GMT', async () => {
|
||||||
|
const { container, getByPlaceholderText } = render(<App5 />);
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
const startInput = getByPlaceholderText('Start date');
|
||||||
|
const endInput = getByPlaceholderText('End date');
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
await userEvent.click(document.querySelector('[title="2023-05-01"]') as HTMLElement);
|
||||||
|
await userEvent.click(document.querySelector('[title="2023-05-02"]') as HTMLElement);
|
||||||
|
|
||||||
|
await sleep(100);
|
||||||
|
|
||||||
|
expect(startInput).toHaveValue('2023-05-01');
|
||||||
|
expect(endInput).toHaveValue('2023-05-02');
|
||||||
|
// Read pretty
|
||||||
|
expect(screen.getByText('2023-05-01~2023-05-02', { selector: '.ant-description-text' })).toBeInTheDocument();
|
||||||
|
|
||||||
|
// TODO: 本地没有错,但是 CI 上会报错,暂时注释掉
|
||||||
|
// Value
|
||||||
|
// expect(screen.getByText(/2023-04-30t16:00:00\.000z ~ 2023-05-02t15:59:59\.999z/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('non-UTC', async () => {
|
||||||
|
const { container, getByPlaceholderText } = render(<App6 />);
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
const startInput = getByPlaceholderText('Start date');
|
||||||
|
const endInput = getByPlaceholderText('End date');
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
await userEvent.click(document.querySelector('[title="2023-05-01"]') as HTMLElement);
|
||||||
|
await userEvent.click(document.querySelector('[title="2023-05-02"]') as HTMLElement);
|
||||||
|
|
||||||
|
expect(startInput).toHaveValue('2023-05-01');
|
||||||
|
expect(endInput).toHaveValue('2023-05-02');
|
||||||
|
// Read pretty
|
||||||
|
expect(screen.getByText('2023-05-01~2023-05-02', { selector: '.ant-description-text' })).toBeInTheDocument();
|
||||||
|
// Value
|
||||||
|
expect(screen.getByText('2023-05-01 ~ 2023-05-02')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* title: DatePicker.RangePicker
|
* title: DatePicker (GMT)
|
||||||
*/
|
*/
|
||||||
import { FormItem } from '@formily/antd';
|
import { FormItem } from '@formily/antd';
|
||||||
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
@ -12,36 +12,31 @@ const schema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
title: `Editable`,
|
title: `Editable`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'DatePicker.RangePicker',
|
'x-component': 'DatePicker',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: true,
|
||||||
gmt: true,
|
gmt: true,
|
||||||
},
|
},
|
||||||
'x-reactions': [
|
default: '2022-06-04T15:00:00.000Z',
|
||||||
{
|
'x-reactions': {
|
||||||
target: 'read1',
|
target: '*(read1,read2)',
|
||||||
fulfill: {
|
fulfill: {
|
||||||
state: {
|
state: {
|
||||||
value: '{{$self.value}}',
|
value: '{{$self.value}}',
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
target: 'read2',
|
|
||||||
fulfill: {
|
|
||||||
state: {
|
|
||||||
value: '{{$self.value && $self.value.join(" ~ ")}}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
read1: {
|
read1: {
|
||||||
type: 'boolean',
|
type: 'string',
|
||||||
title: `Read pretty`,
|
title: `Read pretty`,
|
||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'DatePicker.RangePicker',
|
'x-component': 'DatePicker',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: true,
|
||||||
gmt: true,
|
gmt: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* title: DatePicker (GMT)
|
* title: DatePicker
|
||||||
*/
|
*/
|
||||||
import { FormItem } from '@formily/antd';
|
import { FormItem } from '@formily/antd';
|
||||||
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
@ -15,10 +15,9 @@ const schema = {
|
|||||||
'x-component': 'DatePicker',
|
'x-component': 'DatePicker',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
dateFormat: 'YYYY/MM/DD',
|
dateFormat: 'YYYY/MM/DD',
|
||||||
showTime: true,
|
showTime: false,
|
||||||
gmt: true,
|
utc: false,
|
||||||
},
|
},
|
||||||
default: '2022-06-04T15:00:00.000Z',
|
|
||||||
'x-reactions': {
|
'x-reactions': {
|
||||||
target: '*(read1,read2)',
|
target: '*(read1,read2)',
|
||||||
fulfill: {
|
fulfill: {
|
||||||
@ -29,7 +28,7 @@ const schema = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
read1: {
|
read1: {
|
||||||
type: 'string',
|
type: 'boolean',
|
||||||
title: `Read pretty`,
|
title: `Read pretty`,
|
||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
@ -37,7 +36,6 @@ const schema = {
|
|||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
dateFormat: 'YYYY/MM/DD',
|
dateFormat: 'YYYY/MM/DD',
|
||||||
showTime: true,
|
showTime: true,
|
||||||
gmt: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
read2: {
|
read2: {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import { FormItem } from '@formily/antd';
|
import { FormItem } from '@formily/antd';
|
||||||
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import moment from 'moment';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
@ -14,7 +15,8 @@ const schema = {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'DatePicker.RangePicker',
|
'x-component': 'DatePicker.RangePicker',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
utc: false,
|
gmt: true,
|
||||||
|
defaultPickerValue: [moment('2023-05-01')],
|
||||||
},
|
},
|
||||||
'x-reactions': [
|
'x-reactions': [
|
||||||
{
|
{
|
||||||
@ -41,6 +43,10 @@ const schema = {
|
|||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'DatePicker.RangePicker',
|
'x-component': 'DatePicker.RangePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
gmt: true,
|
||||||
|
defaultPickerValue: [moment('2023-05-01')],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
read2: {
|
read2: {
|
||||||
type: 'string',
|
type: 'string',
|
@ -1,8 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* title: DatePicker
|
* title: DatePicker.RangePicker
|
||||||
*/
|
*/
|
||||||
import { FormItem } from '@formily/antd';
|
import { FormItem } from '@formily/antd';
|
||||||
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import moment from 'moment';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
@ -12,30 +13,38 @@ const schema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
title: `Editable`,
|
title: `Editable`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'DatePicker',
|
'x-component': 'DatePicker.RangePicker',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
dateFormat: 'YYYY/MM/DD',
|
gmt: false,
|
||||||
showTime: false,
|
defaultPickerValue: [moment('2023-05-01')],
|
||||||
utc: false,
|
|
||||||
},
|
},
|
||||||
'x-reactions': {
|
'x-reactions': [
|
||||||
target: '*(read1,read2)',
|
{
|
||||||
fulfill: {
|
target: 'read1',
|
||||||
state: {
|
fulfill: {
|
||||||
value: '{{$self.value}}',
|
state: {
|
||||||
|
value: '{{$self.value}}',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
|
target: 'read2',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value && $self.value.join(" ~ ")}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
read1: {
|
read1: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
title: `Read pretty`,
|
title: `Read pretty`,
|
||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'DatePicker',
|
'x-component': 'DatePicker.RangePicker',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
dateFormat: 'YYYY/MM/DD',
|
defaultPickerValue: [moment('2023-05-01')],
|
||||||
showTime: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
read2: {
|
read2: {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import { FormItem } from '@formily/antd';
|
import { FormItem } from '@formily/antd';
|
||||||
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import moment from 'moment';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
@ -14,7 +15,8 @@ const schema = {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'DatePicker.RangePicker',
|
'x-component': 'DatePicker.RangePicker',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
gmt: false,
|
utc: false,
|
||||||
|
defaultPickerValue: [moment('2023-05-01')],
|
||||||
},
|
},
|
||||||
'x-reactions': [
|
'x-reactions': [
|
||||||
{
|
{
|
||||||
@ -41,6 +43,9 @@ const schema = {
|
|||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'DatePicker.RangePicker',
|
'x-component': 'DatePicker.RangePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
defaultPickerValue: [moment('2023-05-01')],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
read2: {
|
read2: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -15,23 +15,23 @@ group:
|
|||||||
|
|
||||||
### DatePicker (GMT)
|
### DatePicker (GMT)
|
||||||
|
|
||||||
<code src="./demos/demo3.tsx" />
|
<code src="./demos/demo2.tsx" />
|
||||||
|
|
||||||
### DatePicker (non-UTC)
|
### DatePicker (non-UTC)
|
||||||
|
|
||||||
<code src="./demos/demo5.tsx" />
|
<code src="./demos/demo3.tsx" />
|
||||||
|
|
||||||
### RangePicker (GMT)
|
### RangePicker (GMT)
|
||||||
|
|
||||||
<code src="./demos/demo2.tsx" />
|
<code src="./demos/demo4.tsx" />
|
||||||
|
|
||||||
### RangePicker (non-GMT)
|
### RangePicker (non-GMT)
|
||||||
|
|
||||||
<code src="./demos/demo6.tsx" />
|
<code src="./demos/demo5.tsx" />
|
||||||
|
|
||||||
### RangePicker (non-UTC)
|
### RangePicker (non-UTC)
|
||||||
|
|
||||||
<code src="./demos/demo7.tsx" />
|
<code src="./demos/demo6.tsx" />
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from 'testUtils';
|
||||||
|
import App1 from '../demos/demo1';
|
||||||
|
|
||||||
|
describe('Details', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
render(<App1 />);
|
||||||
|
|
||||||
|
expect(screen.getByText('this is name')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('this is age')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,39 @@
|
|||||||
|
import { FormItem } from '@formily/antd';
|
||||||
|
import { Details, FormProvider, Input, SchemaComponent } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
details: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'Details',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Name',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
default: 'this is name',
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Age',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
default: 'this is age',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<FormProvider>
|
||||||
|
<SchemaComponent components={{ Details, FormItem, Input }} schema={schema} />
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
nav:
|
||||||
|
path: /client
|
||||||
|
group:
|
||||||
|
path: /schema-components
|
||||||
|
---
|
||||||
|
|
||||||
|
# Details
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
<code src="./demos/demo1.tsx" />
|
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from 'testUtils';
|
||||||
|
import App1 from '../demos/demo1';
|
||||||
|
|
||||||
|
describe('ErrorFallback', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
render(<App1 />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/render failed/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/this is likely a nocobase internals bug\. please open an issue at/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('link', { name: /feedback/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/try again/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/error: error message/i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// 底部复制按钮
|
||||||
|
expect(document.querySelector('.ant-typography-copy')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import { ErrorFallback } from '../ErrorFallback';
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
throw new Error('error message');
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
|
<App />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
nav:
|
||||||
|
path: /client
|
||||||
|
group:
|
||||||
|
path: /schema-components
|
||||||
|
---
|
||||||
|
|
||||||
|
# ErrorFallback
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
<code src="./demos/demo1.tsx" />
|
@ -1,15 +1,15 @@
|
|||||||
|
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useFieldSchema, useField, ISchema } from '@formily/react';
|
|
||||||
import { useDesignable } from '../..';
|
import { useDesignable } from '../..';
|
||||||
|
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||||
|
|
||||||
export const ExpandActionDesign = (props) => {
|
export const ExpandActionDesign = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
const { titleExpand, titleCollapse, iconExpand, iconCollapse } = fieldSchema['x-component-props'] || {}
|
const { titleExpand, titleCollapse, iconExpand, iconCollapse } = fieldSchema['x-component-props'] || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GeneralSchemaDesigner {...props} disableInitializer>
|
<GeneralSchemaDesigner {...props} disableInitializer>
|
||||||
@ -24,13 +24,13 @@ export const ExpandActionDesign = (props) => {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
title: `${t('Button title')} - ${t('Expand all')}`,
|
title: `${t('Button title')} - ${t('Expand all')}`,
|
||||||
default: titleExpand
|
default: titleExpand,
|
||||||
},
|
},
|
||||||
titleCollapse: {
|
titleCollapse: {
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
title: `${t('Button title')} - ${t('Collapse all')}`,
|
title: `${t('Button title')} - ${t('Collapse all')}`,
|
||||||
default: titleCollapse
|
default: titleCollapse,
|
||||||
},
|
},
|
||||||
iconExpand: {
|
iconExpand: {
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
@ -75,14 +75,14 @@ export const ExpandActionDesign = (props) => {
|
|||||||
iconExpand,
|
iconExpand,
|
||||||
iconCollapse,
|
iconCollapse,
|
||||||
type,
|
type,
|
||||||
danger: type === 'danger'
|
danger: type === 'danger',
|
||||||
};
|
};
|
||||||
dn.emit('patch', {
|
dn.emit('patch', {
|
||||||
schema: {
|
schema: {
|
||||||
'x-uid': fieldSchema['x-uid'],
|
'x-uid': fieldSchema['x-uid'],
|
||||||
'x-component-props': fieldSchema['x-component-props'],
|
'x-component-props': fieldSchema['x-component-props'],
|
||||||
title: fieldSchema.title,
|
title: fieldSchema.title,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
dn.refresh();
|
dn.refresh();
|
||||||
}}
|
}}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useFieldSchema } from '@formily/react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useTableBlockContext, useTableSelectorContext } from '../../../block-provider';
|
|
||||||
import { Button } from 'antd';
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTableBlockContext, useTableSelectorContext } from '../../../block-provider';
|
||||||
import { Icon } from '../../../icon';
|
import { Icon } from '../../../icon';
|
||||||
|
|
||||||
const actionDesignerCss = css`
|
const actionDesignerCss = css`
|
||||||
@ -46,10 +45,9 @@ const actionDesignerCss = css`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const ExpandAction = (props) => {
|
export const ExpandAction = (props) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const schema = useFieldSchema();
|
const schema = useFieldSchema();
|
||||||
const ctxSelector = useTableSelectorContext()
|
const ctxSelector = useTableSelectorContext();
|
||||||
const ctxBlock = useTableBlockContext()
|
const ctxBlock = useTableBlockContext();
|
||||||
const isTableSelector = schema.parent?.parent?.['x-decorator'] === 'TableSelectorProvider';
|
const isTableSelector = schema.parent?.parent?.['x-decorator'] === 'TableSelectorProvider';
|
||||||
const ctx = isTableSelector ? ctxSelector : ctxBlock;
|
const ctx = isTableSelector ? ctxSelector : ctxBlock;
|
||||||
const { titleExpand, titleCollapse, iconExpand, iconCollapse } = schema['x-component-props'] || {};
|
const { titleExpand, titleCollapse, iconExpand, iconCollapse } = schema['x-component-props'] || {};
|
||||||
@ -64,7 +62,7 @@ export const ExpandAction = (props) => {
|
|||||||
type={props.type}
|
type={props.type}
|
||||||
>
|
>
|
||||||
{props.children[1]}
|
{props.children[1]}
|
||||||
<span style={{ marginLeft: 10 }}>{ ctx?.expandFlag ? titleCollapse : titleExpand }</span>
|
<span style={{ marginLeft: 10 }}>{ctx?.expandFlag ? titleCollapse : titleExpand}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ExpandAction } from './Expand.Action'
|
import { ExpandAction } from './Expand.Action';
|
||||||
import { ExpandActionDesign } from './Expand.Action.Design'
|
import { ExpandActionDesign } from './Expand.Action.Design';
|
||||||
|
|
||||||
export const Expand: any = ExpandAction
|
export const Expand: any = ExpandAction;
|
||||||
|
|
||||||
Expand.Action = ExpandAction
|
Expand.Action = ExpandAction;
|
||||||
Expand.Action.Design = ExpandActionDesign
|
Expand.Action.Design = ExpandActionDesign;
|
||||||
|
@ -4,11 +4,11 @@ import React from 'react';
|
|||||||
import { useRequest } from '../../../api-client';
|
import { useRequest } from '../../../api-client';
|
||||||
import { useProps } from '../../hooks/useProps';
|
import { useProps } from '../../hooks/useProps';
|
||||||
import { DatePickerProvider } from '../date-picker';
|
import { DatePickerProvider } from '../date-picker';
|
||||||
import { FilterContext } from './context';
|
|
||||||
import { FilterActionDesigner } from './Filter.Action.Designer';
|
import { FilterActionDesigner } from './Filter.Action.Designer';
|
||||||
import { FilterAction } from './FilterAction';
|
import { FilterAction } from './FilterAction';
|
||||||
import { FilterGroup } from './FilterGroup';
|
import { FilterGroup } from './FilterGroup';
|
||||||
import { SaveDefaultValue } from './SaveDefaultValue';
|
import { SaveDefaultValue } from './SaveDefaultValue';
|
||||||
|
import { FilterContext } from './context';
|
||||||
|
|
||||||
const useDef = (options) => {
|
const useDef = (options) => {
|
||||||
const field = useField<ObjectFieldModel>();
|
const field = useField<ObjectFieldModel>();
|
||||||
|
@ -2,11 +2,11 @@ import { CloseCircleOutlined } from '@ant-design/icons';
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { observer } from '@formily/react';
|
import { observer } from '@formily/react';
|
||||||
import { Cascader, Select, Space } from 'antd';
|
import { Cascader, Select, Space } from 'antd';
|
||||||
import React, { useContext, useEffect } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCompile } from '../..';
|
import { useCompile } from '../..';
|
||||||
import { RemoveConditionContext } from './context';
|
|
||||||
import { DynamicComponent } from './DynamicComponent';
|
import { DynamicComponent } from './DynamicComponent';
|
||||||
|
import { RemoveConditionContext } from './context';
|
||||||
import { useValues } from './useValues';
|
import { useValues } from './useValues';
|
||||||
|
|
||||||
export const FilterItem = observer(
|
export const FilterItem = observer(
|
||||||
@ -16,7 +16,8 @@ export const FilterItem = observer(
|
|||||||
const remove = useContext(RemoveConditionContext);
|
const remove = useContext(RemoveConditionContext);
|
||||||
const { schema, fields, operators, dataIndex, operator, setDataIndex, setOperator, value, setValue } = useValues();
|
const { schema, fields, operators, dataIndex, operator, setDataIndex, setOperator, value, setValue } = useValues();
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: 8 }}>
|
// 添加 nc-filter-item 类名是为了帮助编写测试时更容易选中该元素
|
||||||
|
<div style={{ marginBottom: 8 }} className="nc-filter-item">
|
||||||
<Space>
|
<Space>
|
||||||
<Cascader
|
<Cascader
|
||||||
className={css`
|
className={css`
|
||||||
@ -46,14 +47,7 @@ export const FilterItem = observer(
|
|||||||
}}
|
}}
|
||||||
placeholder={t('Comparision')}
|
placeholder={t('Comparision')}
|
||||||
/>
|
/>
|
||||||
{!operator?.noValue &&
|
{!operator?.noValue ? <DynamicComponent value={value} schema={schema} onChange={setValue} /> : null}
|
||||||
React.createElement(DynamicComponent, {
|
|
||||||
value: value ? value : undefined,
|
|
||||||
schema,
|
|
||||||
onChange(value) {
|
|
||||||
setValue(value);
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
{!props.disabled && (
|
{!props.disabled && (
|
||||||
<a>
|
<a>
|
||||||
<CloseCircleOutlined onClick={() => remove()} style={{ color: '#bfbfbf' }} />
|
<CloseCircleOutlined onClick={() => remove()} style={{ color: '#bfbfbf' }} />
|
||||||
|
@ -0,0 +1,123 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen, userEvent, within } from 'testUtils';
|
||||||
|
import App2 from '../demos/demo2';
|
||||||
|
import App3 from '../demos/demo3';
|
||||||
|
import App4 from '../demos/demo4';
|
||||||
|
import App5 from '../demos/demo5';
|
||||||
|
import App6 from '../demos/demo6';
|
||||||
|
|
||||||
|
describe('Filter', () => {
|
||||||
|
it('Filter & Action', async () => {
|
||||||
|
render(<App3 />);
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText(/open/i));
|
||||||
|
const tooltip = screen.getByRole('tooltip');
|
||||||
|
expect(tooltip).toBeInTheDocument();
|
||||||
|
|
||||||
|
// 弹窗中显示的内容
|
||||||
|
expect(within(tooltip).getByText(/name/i)).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(/ne/i)).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(/tags \/ title/i)).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(/eq/i)).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(/^Add condition$/i)).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(/^Add condition group$/i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
const inputs = tooltip.querySelectorAll('.ant-input');
|
||||||
|
expect(inputs).toHaveLength(2);
|
||||||
|
// 输入框中的默认值
|
||||||
|
expect(inputs[0]).toHaveValue('aa');
|
||||||
|
expect(inputs[1]).toHaveValue('aaa');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('default value', () => {
|
||||||
|
render(<App2 />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/name/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/ne/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/tags \/ title/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/eq/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/^Add condition$/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/^Add condition group$/i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
const inputs = document.querySelectorAll('.ant-input');
|
||||||
|
expect(inputs).toHaveLength(2);
|
||||||
|
// 输入框中的默认值
|
||||||
|
expect(inputs[0]).toHaveValue('');
|
||||||
|
expect(inputs[1]).toHaveValue('aaa');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('custom dynamic component', async () => {
|
||||||
|
render(<App4 />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/name/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/ne/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/tags \/ title/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/eq/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/^Add condition$/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/^Add condition group$/i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
const selects = screen.getAllByText('默认');
|
||||||
|
// 自定义组件中的选择框
|
||||||
|
expect(selects[0]).toBeInTheDocument();
|
||||||
|
expect(selects[1]).toBeInTheDocument();
|
||||||
|
|
||||||
|
const inputs = document.querySelectorAll('.ant-input');
|
||||||
|
expect(inputs).toHaveLength(2);
|
||||||
|
// 输入框中的默认值
|
||||||
|
expect(inputs[0]).toHaveValue('aaa');
|
||||||
|
expect(inputs[1]).toHaveValue('bbb');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('FilterAction', async () => {
|
||||||
|
render(<App5 />);
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText(/filter/i));
|
||||||
|
const tooltip = screen.getByRole('tooltip');
|
||||||
|
expect(tooltip).toBeInTheDocument();
|
||||||
|
|
||||||
|
// 弹窗中显示的内容
|
||||||
|
expect(within(tooltip).getByText(/name/i)).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(/ne/i)).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(/tags \/ title/i)).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(/eq/i)).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(/^Add condition$/i)).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(/^Add condition group$/i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
const inputs = tooltip.querySelectorAll('.ant-input');
|
||||||
|
expect(inputs).toHaveLength(2);
|
||||||
|
// 输入框中的默认值
|
||||||
|
expect(inputs[0]).toHaveValue('');
|
||||||
|
expect(inputs[1]).toHaveValue('aaa');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dynamic options', async () => {
|
||||||
|
render(<App6 />);
|
||||||
|
|
||||||
|
const addBtn = screen.getByText(/^Add condition$/i);
|
||||||
|
const addGroupBtn = screen.getByText(/^Add condition group$/i);
|
||||||
|
|
||||||
|
expect(screen.getByText(/test1/i)).toBeInTheDocument();
|
||||||
|
expect(addBtn).toBeInTheDocument();
|
||||||
|
expect(addGroupBtn).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(addBtn);
|
||||||
|
const item = document.querySelector('.nc-filter-item');
|
||||||
|
const selector = item.querySelector('.ant-select-selector');
|
||||||
|
expect(item).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(selector);
|
||||||
|
// 选中 Title1
|
||||||
|
await userEvent.click(screen.getByText(/title1/i));
|
||||||
|
expect(screen.getByText(/title1/i, { selector: '.ant-select-selection-item' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/contains/i, { selector: '.ant-select-selection-item' })).toBeInTheDocument();
|
||||||
|
|
||||||
|
// 切换为 test2
|
||||||
|
await userEvent.click(screen.getByText(/test1/i));
|
||||||
|
await userEvent.click(screen.getByText(/test2/i, { selector: '.ant-select-item-option-content' }));
|
||||||
|
await userEvent.click(selector);
|
||||||
|
// 选中 Title2
|
||||||
|
await userEvent.click(screen.getByText(/title2/i));
|
||||||
|
expect(screen.getByText(/title2/i, { selector: '.ant-select-selection-item' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/contains/i, { selector: '.ant-select-selection-item' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -1,95 +1,93 @@
|
|||||||
import {
|
import { AntdSchemaComponentProvider, Filter, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
AntdSchemaComponentProvider,
|
|
||||||
SharedFilter,
|
|
||||||
Input,
|
|
||||||
SchemaComponent,
|
|
||||||
SchemaComponentProvider,
|
|
||||||
} from '@nocobase/client';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
title: 'Name',
|
||||||
|
operators: [
|
||||||
|
{ label: 'eq', value: '$eq' },
|
||||||
|
{ label: 'ne', value: '$ne' },
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Name',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'age',
|
||||||
|
title: 'Age',
|
||||||
|
operators: [
|
||||||
|
{ label: 'in', value: '$in' },
|
||||||
|
{ label: 'not', value: '$not' },
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Age',
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tags',
|
||||||
|
title: 'Tags',
|
||||||
|
schema: {
|
||||||
|
title: 'Tags',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'slug',
|
||||||
|
title: 'Slug',
|
||||||
|
operators: [
|
||||||
|
{ label: 'in', value: '$in' },
|
||||||
|
{ label: 'not', value: '$not' },
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
title: 'Slug',
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
title: 'Title',
|
||||||
|
operators: [
|
||||||
|
{ label: 'eq', value: '$eq' },
|
||||||
|
{ label: 'ne', value: '$ne' },
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
title: 'Title',
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultValue = {
|
||||||
|
$or: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
$ne: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'tags.title': {
|
||||||
|
$eq: 'aaa',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const schema: any = {
|
const schema: any = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
properties: {
|
properties: {
|
||||||
demo: {
|
demo: {
|
||||||
name: 'filter',
|
name: 'filter',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
enum: [
|
enum: options,
|
||||||
{
|
default: defaultValue,
|
||||||
name: 'name',
|
|
||||||
title: 'Name',
|
|
||||||
operators: [
|
|
||||||
{ label: 'eq', value: '$eq' },
|
|
||||||
{ label: 'ne', value: '$ne' },
|
|
||||||
],
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Name',
|
|
||||||
'x-component': 'Input',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'age',
|
|
||||||
title: 'Age',
|
|
||||||
operators: [
|
|
||||||
{ label: 'in', value: '$in' },
|
|
||||||
{ label: 'not', value: '$not' },
|
|
||||||
],
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Age',
|
|
||||||
'x-component': 'InputNumber',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tags',
|
|
||||||
title: 'Tags',
|
|
||||||
schema: {
|
|
||||||
title: 'Tags',
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'slug',
|
|
||||||
title: 'Slug',
|
|
||||||
operators: [
|
|
||||||
{ label: 'in', value: '$in' },
|
|
||||||
{ label: 'not', value: '$not' },
|
|
||||||
],
|
|
||||||
schema: {
|
|
||||||
title: 'Slug',
|
|
||||||
type: 'string',
|
|
||||||
'x-component': 'Input',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'title',
|
|
||||||
title: 'Title',
|
|
||||||
operators: [
|
|
||||||
{ label: 'eq', value: '$eq' },
|
|
||||||
{ label: 'ne', value: '$ne' },
|
|
||||||
],
|
|
||||||
schema: {
|
|
||||||
title: 'Title',
|
|
||||||
type: 'string',
|
|
||||||
'x-component': 'Input',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: {
|
|
||||||
$or: [
|
|
||||||
{
|
|
||||||
name: {
|
|
||||||
$ne: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tags.title': {
|
|
||||||
$eq: 'aaa',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'x-component': 'Filter',
|
'x-component': 'Filter',
|
||||||
'x-component-props': {},
|
'x-component-props': {},
|
||||||
},
|
},
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { ISchema, useForm } from '@formily/react';
|
import { ISchema, useForm } from '@formily/react';
|
||||||
import {
|
import {
|
||||||
AntdSchemaComponentProvider,
|
AntdSchemaComponentProvider,
|
||||||
SharedFilter,
|
Filter,
|
||||||
Input,
|
Input,
|
||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
SchemaComponentProvider,
|
SchemaComponentProvider,
|
||||||
useActionContext,
|
useActionContext
|
||||||
useRequest,
|
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
@ -74,6 +73,21 @@ const dataSource = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const defaultValue = {
|
||||||
|
$or: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
$ne: 'aa',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'tags.title': {
|
||||||
|
$eq: 'aaa',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const schema: ISchema = {
|
const schema: ISchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
@ -99,31 +113,10 @@ const schema: ISchema = {
|
|||||||
properties: {
|
properties: {
|
||||||
filter: {
|
filter: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
default: {
|
default: defaultValue,
|
||||||
$or: [
|
|
||||||
{
|
|
||||||
name: {
|
|
||||||
$ne: 'aa',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tags.title': {
|
|
||||||
$eq: 'aaa',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'x-component': 'Filter',
|
'x-component': 'Filter',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
useDataSource(options) {
|
options: dataSource,
|
||||||
return useRequest(
|
|
||||||
() =>
|
|
||||||
Promise.resolve({
|
|
||||||
data: dataSource,
|
|
||||||
}),
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
import {
|
import {
|
||||||
AntdSchemaComponentProvider,
|
AntdSchemaComponentProvider,
|
||||||
SharedFilter,
|
Filter,
|
||||||
Input,
|
Input,
|
||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
SchemaComponentProvider,
|
SchemaComponentProvider,
|
||||||
@ -78,12 +78,12 @@ const defaultValue = {
|
|||||||
$or: [
|
$or: [
|
||||||
{
|
{
|
||||||
name: {
|
name: {
|
||||||
$ne: '{{node1.field1}}',
|
$ne: 'aaa',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'tags.title': {
|
'tags.title': {
|
||||||
$eq: 'aaa',
|
$eq: 'bbb',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -1,88 +1,94 @@
|
|||||||
import { AntdSchemaComponentProvider, FilterAction, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
import { ISchema } from '@formily/json-schema';
|
||||||
|
import { Filter, FilterAction, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const schema: any = {
|
const options = [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
title: 'Name',
|
||||||
|
operators: [
|
||||||
|
{ label: 'eq', value: '$eq' },
|
||||||
|
{ label: 'ne', value: '$ne' },
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Name',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'age',
|
||||||
|
title: 'Age',
|
||||||
|
operators: [
|
||||||
|
{ label: 'in', value: '$in' },
|
||||||
|
{ label: 'not', value: '$not' },
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Age',
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tags',
|
||||||
|
title: 'Tags',
|
||||||
|
schema: {
|
||||||
|
title: 'Tags',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'slug',
|
||||||
|
title: 'Slug',
|
||||||
|
operators: [
|
||||||
|
{ label: 'in', value: '$in' },
|
||||||
|
{ label: 'not', value: '$not' },
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
title: 'Slug',
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
title: 'Title',
|
||||||
|
operators: [
|
||||||
|
{ label: 'eq', value: '$eq' },
|
||||||
|
{ label: 'ne', value: '$ne' },
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
title: 'Title',
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultValue = {
|
||||||
|
$or: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
$ne: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'tags.title': {
|
||||||
|
$eq: 'aaa',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const schema: ISchema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
properties: {
|
properties: {
|
||||||
demo: {
|
demo: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
enum: [
|
title: 'Filter',
|
||||||
{
|
enum: '{{ options }}',
|
||||||
name: 'name',
|
default: defaultValue,
|
||||||
title: 'Name',
|
|
||||||
operators: [
|
|
||||||
{ label: 'eq', value: '$eq' },
|
|
||||||
{ label: 'ne', value: '$ne' },
|
|
||||||
],
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Name',
|
|
||||||
'x-component': 'Input',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'age',
|
|
||||||
title: 'Age',
|
|
||||||
operators: [
|
|
||||||
{ label: 'in', value: '$in' },
|
|
||||||
{ label: 'not', value: '$not' },
|
|
||||||
],
|
|
||||||
schema: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Age',
|
|
||||||
'x-component': 'InputNumber',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tags',
|
|
||||||
title: 'Tags',
|
|
||||||
schema: {
|
|
||||||
title: 'Tags',
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'slug',
|
|
||||||
title: 'Slug',
|
|
||||||
operators: [
|
|
||||||
{ label: 'in', value: '$in' },
|
|
||||||
{ label: 'not', value: '$not' },
|
|
||||||
],
|
|
||||||
schema: {
|
|
||||||
title: 'Slug',
|
|
||||||
type: 'string',
|
|
||||||
'x-component': 'Input',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'title',
|
|
||||||
title: 'Title',
|
|
||||||
operators: [
|
|
||||||
{ label: 'eq', value: '$eq' },
|
|
||||||
{ label: 'ne', value: '$ne' },
|
|
||||||
],
|
|
||||||
schema: {
|
|
||||||
title: 'Title',
|
|
||||||
type: 'string',
|
|
||||||
'x-component': 'Input',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: {
|
|
||||||
$or: [
|
|
||||||
{
|
|
||||||
name: {
|
|
||||||
$ne: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tags.title': {
|
|
||||||
$eq: 'aaa',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'x-component': 'FilterAction',
|
'x-component': 'FilterAction',
|
||||||
'x-component-props': {},
|
'x-component-props': {},
|
||||||
},
|
},
|
||||||
@ -91,10 +97,8 @@ const schema: any = {
|
|||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return (
|
return (
|
||||||
<SchemaComponentProvider>
|
<SchemaComponentProvider components={{ FilterAction, Filter, Input }} scope={{ options }}>
|
||||||
<AntdSchemaComponentProvider>
|
<SchemaComponent schema={schema} />
|
||||||
<SchemaComponent components={{ FilterAction }} schema={schema} />
|
|
||||||
</AntdSchemaComponentProvider>
|
|
||||||
</SchemaComponentProvider>
|
</SchemaComponentProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@ import {
|
|||||||
AntdSchemaComponentProvider,
|
AntdSchemaComponentProvider,
|
||||||
CollectionManagerProvider,
|
CollectionManagerProvider,
|
||||||
CollectionProvider,
|
CollectionProvider,
|
||||||
SharedFilter,
|
Filter,
|
||||||
Input,
|
Input,
|
||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
SchemaComponentProvider,
|
SchemaComponentProvider,
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
export * from './Filter';
|
export * from './Filter';
|
||||||
|
export * from './FilterAction';
|
||||||
export * from './useFilterActionProps';
|
export * from './useFilterActionProps';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useField } from '@formily/react';
|
import { useField } from '@formily/react';
|
||||||
import { merge } from '@formily/shared';
|
import { merge } from '@formily/shared';
|
||||||
import flat from 'flat';
|
import flat, { unflatten } from 'flat';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { useContext, useEffect } from 'react';
|
import { useContext, useEffect } from 'react';
|
||||||
@ -31,7 +31,7 @@ export const useValues = () => {
|
|||||||
};
|
};
|
||||||
const value2data = () => {
|
const value2data = () => {
|
||||||
field.data = field.data || {};
|
field.data = field.data || {};
|
||||||
const values = flat(field.value || {});
|
const values: object = flat(field.value || {});
|
||||||
const path = Object.keys(values).shift() || '';
|
const path = Object.keys(values).shift() || '';
|
||||||
if (!path || !options) {
|
if (!path || !options) {
|
||||||
return;
|
return;
|
||||||
@ -46,7 +46,7 @@ export const useValues = () => {
|
|||||||
field.data.operators = operators;
|
field.data.operators = operators;
|
||||||
field.data.operator = operator;
|
field.data.operator = operator;
|
||||||
field.data.schema = merge(option?.schema, operator?.schema);
|
field.data.schema = merge(option?.schema, operator?.schema);
|
||||||
field.data.value = get(field.value, `${fieldPath}.$${operatorValue}`);
|
field.data.value = get(unflatten(field.value), `${fieldPath}.$${operatorValue}`);
|
||||||
};
|
};
|
||||||
useEffect(value2data, [field.path.entire]);
|
useEffect(value2data, [field.path.entire]);
|
||||||
return {
|
return {
|
||||||
|
@ -2,14 +2,14 @@ import { css, cx } from '@emotion/css';
|
|||||||
import { ArrayCollapse, ArrayItems, FormLayout, FormItem as Item } from '@formily/antd';
|
import { ArrayCollapse, ArrayItems, FormLayout, FormItem as Item } from '@formily/antd';
|
||||||
import { Field } from '@formily/core';
|
import { Field } from '@formily/core';
|
||||||
import { ISchema, Schema, observer, useField, useFieldSchema } from '@formily/react';
|
import { ISchema, Schema, observer, useField, useFieldSchema } from '@formily/react';
|
||||||
import { uid } from '@formily/shared';
|
|
||||||
import { Select } from 'antd';
|
import { Select } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import React, { useContext, useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ACLCollectionFieldProvider } from '../../../acl/ACLProvider';
|
import { ACLCollectionFieldProvider } from '../../../acl/ACLProvider';
|
||||||
import { BlockRequestContext, useFormBlockContext } from '../../../block-provider';
|
import { useBlockRequestContext } from '../../../block-provider/BlockProvider';
|
||||||
|
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
|
||||||
import {
|
import {
|
||||||
Collection,
|
Collection,
|
||||||
CollectionFieldOptions,
|
CollectionFieldOptions,
|
||||||
@ -40,25 +40,12 @@ const defaultInputStyle = css`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const divWrap = (schema: ISchema) => {
|
|
||||||
return {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'div',
|
|
||||||
'x-component-props': {
|
|
||||||
className: 'nb-block-wrap',
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
[schema.name || uid()]: schema,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FormItem: any = observer(
|
export const FormItem: any = observer(
|
||||||
(props: any) => {
|
(props: any) => {
|
||||||
useEnsureOperatorsValid();
|
useEnsureOperatorsValid();
|
||||||
|
|
||||||
const field = useField<Field>();
|
const field = useField<Field>();
|
||||||
const ctx = useContext(BlockRequestContext);
|
const ctx = useBlockRequestContext();
|
||||||
const schema = useFieldSchema();
|
const schema = useFieldSchema();
|
||||||
const variablesCtx = useVariablesCtx();
|
const variablesCtx = useVariablesCtx();
|
||||||
|
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from 'testUtils';
|
||||||
|
import App1 from '../demos/demo1';
|
||||||
|
|
||||||
|
describe('FormItem', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
render(<App1 />);
|
||||||
|
|
||||||
|
expect(screen.getByText('title')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,22 @@
|
|||||||
|
import { FormItem, FormProvider, Input, SchemaComponent } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'title',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<FormProvider>
|
||||||
|
<SchemaComponent components={{ FormItem, Input }} schema={schema} />
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -8,3 +8,7 @@ group:
|
|||||||
# FormItem
|
# FormItem
|
||||||
|
|
||||||
表单字段装饰器。除此之外,也继承了 BlockItem 的功能。
|
表单字段装饰器。除此之外,也继承了 BlockItem 的功能。
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
<code src="./demos/demo1.tsx"/>
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen, sleep, userEvent } from 'testUtils';
|
||||||
|
import App1 from '../demos/demo1';
|
||||||
|
import App2 from '../demos/demo2';
|
||||||
|
import App3 from '../demos/demo3';
|
||||||
|
|
||||||
|
describe('FormV2', () => {
|
||||||
|
it('basic', async () => {
|
||||||
|
render(<App1 />);
|
||||||
|
|
||||||
|
const input = document.querySelector('.ant-input') as HTMLInputElement;
|
||||||
|
const submit = screen.getByText('Submit');
|
||||||
|
|
||||||
|
expect(input).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Nickname')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.type(input, '李四');
|
||||||
|
await userEvent.click(submit);
|
||||||
|
|
||||||
|
// notification 的内容
|
||||||
|
expect(screen.getByText(/\{"nickname":"李四"\}/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initial values', async () => {
|
||||||
|
render(<App2 />);
|
||||||
|
|
||||||
|
await sleep(100);
|
||||||
|
|
||||||
|
const nicknameInput = document.querySelector('.nickname .ant-input') as HTMLInputElement;
|
||||||
|
const passwordInput = document.querySelector('.password .ant-input') as HTMLInputElement;
|
||||||
|
const submit = screen.getByText('Submit');
|
||||||
|
|
||||||
|
expect(submit).toBeInTheDocument();
|
||||||
|
expect(nicknameInput).toBeInTheDocument();
|
||||||
|
expect(passwordInput).toBeInTheDocument();
|
||||||
|
expect(nicknameInput).toHaveValue('张三');
|
||||||
|
expect(passwordInput).toHaveValue('123456');
|
||||||
|
expect(screen.getByText('Nickname')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Password')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('read pretty', async () => {
|
||||||
|
render(<App3 />);
|
||||||
|
|
||||||
|
await sleep(100);
|
||||||
|
|
||||||
|
expect(screen.getByText('Nickname')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Password')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('张三')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/\*\*\*\*\*\*\*\*/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
File diff suppressed because it is too large
Load Diff
@ -1,26 +1,43 @@
|
|||||||
import { ISchema, useForm } from '@formily/react';
|
import { ISchema, useForm } from '@formily/react';
|
||||||
import {
|
import {
|
||||||
AntdSchemaComponentProvider,
|
|
||||||
APIClient,
|
|
||||||
APIClientProvider,
|
APIClientProvider,
|
||||||
BlockSchemaComponentProvider,
|
Action,
|
||||||
|
CollectionField,
|
||||||
CollectionManagerProvider,
|
CollectionManagerProvider,
|
||||||
|
FormBlockProvider,
|
||||||
|
FormItem,
|
||||||
|
FormV2,
|
||||||
|
Input,
|
||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
SchemaComponentProvider,
|
SchemaComponentProvider,
|
||||||
useFormBlockContext,
|
useFormBlockContext,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
|
import { notification } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useFilterByTk } from '../../../../block-provider/BlockProvider';
|
||||||
|
import { mockAPIClient } from '../../../../test';
|
||||||
import collections from './collections';
|
import collections from './collections';
|
||||||
|
|
||||||
|
const { apiClient, mockRequest } = mockAPIClient();
|
||||||
|
|
||||||
|
mockRequest.onPost('/users:update').reply((params) => {
|
||||||
|
notification.success({
|
||||||
|
message: params.data,
|
||||||
|
});
|
||||||
|
return [200, JSON.parse(params.data)];
|
||||||
|
});
|
||||||
|
|
||||||
function useAction() {
|
function useAction() {
|
||||||
const ctx = useFormBlockContext();
|
const ctx = useFormBlockContext();
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
|
const filterByTk = useFilterByTk();
|
||||||
return {
|
return {
|
||||||
async run() {
|
async run() {
|
||||||
console.log('form.values', form.values);
|
console.log('form.values', form.values);
|
||||||
// await ctx.resource.create({
|
await ctx.resource.update({
|
||||||
// values: form.values,
|
filterByTk,
|
||||||
// });
|
values: form.values,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -47,7 +64,7 @@ const schema: ISchema = {
|
|||||||
'x-component': 'CollectionField',
|
'x-component': 'CollectionField',
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
title: '提交',
|
title: 'Submit',
|
||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
htmlType: 'submit',
|
htmlType: 'submit',
|
||||||
@ -62,22 +79,14 @@ const schema: ISchema = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiClient = new APIClient({
|
|
||||||
baseURL: 'http://localhost:3000/api',
|
|
||||||
});
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return (
|
return (
|
||||||
<APIClientProvider apiClient={apiClient}>
|
<APIClientProvider apiClient={apiClient}>
|
||||||
<SchemaComponentProvider>
|
<CollectionManagerProvider collections={collections}>
|
||||||
<CollectionManagerProvider collections={collections.data}>
|
<SchemaComponentProvider components={{ FormBlockProvider, FormV2, FormItem, CollectionField, Action, Input }}>
|
||||||
<AntdSchemaComponentProvider>
|
<SchemaComponent schema={schema} />
|
||||||
<BlockSchemaComponentProvider>
|
</SchemaComponentProvider>
|
||||||
<SchemaComponent schema={schema} />
|
</CollectionManagerProvider>
|
||||||
</BlockSchemaComponentProvider>
|
|
||||||
</AntdSchemaComponentProvider>
|
|
||||||
</CollectionManagerProvider>
|
|
||||||
</SchemaComponentProvider>
|
|
||||||
</APIClientProvider>
|
</APIClientProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user