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",
|
||||
"name": "Debug Server",
|
||||
"runtimeArgs": [
|
||||
"-r", "dotenv/config",
|
||||
"-r", "tsconfig-paths/register",
|
||||
"-r", "ts-node/register"
|
||||
"-r",
|
||||
"dotenv/config",
|
||||
"-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": [
|
||||
"<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",
|
||||
"request": "launch",
|
||||
|
@ -1 +0,0 @@
|
||||
module.exports = {};
|
@ -15,7 +15,6 @@ module.exports = {
|
||||
...pathsToModuleNameMapper(compilerOptions.paths, {
|
||||
prefix: '<rootDir>/',
|
||||
}),
|
||||
'\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js',
|
||||
},
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
|
@ -19,6 +19,7 @@
|
||||
"build": "nocobase build",
|
||||
"test": "nocobase test",
|
||||
"test:client": "vitest",
|
||||
"tc": "yarn test:client",
|
||||
"doc": "nocobase doc",
|
||||
"postinstall": "nocobase postinstall",
|
||||
"lint": "eslint .",
|
||||
@ -43,10 +44,12 @@
|
||||
"@commitlint/config-conventional": "^16.0.0",
|
||||
"@commitlint/prompt-cli": "^16.1.0",
|
||||
"@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",
|
||||
"auto-changelog": "^2.4.0",
|
||||
"ghooks": "^2.0.4",
|
||||
"jsdom-worker": "^0.3.0",
|
||||
"prettier": "^2.2.1",
|
||||
"pretty-format": "^24.0.0",
|
||||
"pretty-quick": "^3.1.0",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { createCache, createDefaultCacheConfig } from '@nocobase/cache';
|
||||
|
||||
export function sleep(ms: number) {
|
||||
export function sleep(ms?: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
|
@ -29,20 +29,6 @@ const getRouteUrl = (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) => {
|
||||
const route = getRouteUrl(props.children.props);
|
||||
const { setDesignable } = useDesignable();
|
||||
@ -180,11 +166,11 @@ export const ACLCollectionProvider = (props) => {
|
||||
const { allowAll, parseAction } = useACLRoleContext();
|
||||
const schema = useFieldSchema();
|
||||
if (allowAll) {
|
||||
return <>{props.children}</>;
|
||||
return props.children;
|
||||
}
|
||||
const actionPath = schema['x-acl-action'];
|
||||
if (!actionPath) {
|
||||
return <>{props.children}</>;
|
||||
return props.children;
|
||||
}
|
||||
const params = parseAction(actionPath, { schema });
|
||||
if (!params) {
|
||||
@ -288,5 +274,3 @@ export const ACLMenuItemProvider = (props) => {
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ACLProvider;
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 React from 'react';
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { isPlainObject } from '@nocobase/utils/client';
|
||||
import deepmerge from 'deepmerge';
|
||||
import uniq from 'lodash/uniq';
|
||||
|
||||
@ -10,15 +11,6 @@ export interface MergeStrategies {
|
||||
[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[] {
|
||||
return Object.getOwnPropertySymbols
|
||||
? Object.getOwnPropertySymbols(target).filter((symbol) => target.propertyIsEnumerable(symbol))
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { merge } from '@formily/shared';
|
||||
import { useSetState } from 'ahooks';
|
||||
import { default as useReq } from 'ahooks/lib/useRequest';
|
||||
import { Options } from 'ahooks/lib/useRequest/src/types';
|
||||
import { default as useReq } from 'ahooks/es/useRequest';
|
||||
import { Options } from 'ahooks/es/useRequest/src/types';
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { useContext } from 'react';
|
||||
|
@ -75,79 +75,3 @@ exports[`compose > case 5 1`] = `
|
||||
</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 { useRequest } from '../api-client';
|
||||
|
||||
|
@ -2,13 +2,12 @@ import { css } from '@emotion/css';
|
||||
import { Field } from '@formily/core';
|
||||
import { RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import { useRequest } from 'ahooks';
|
||||
import merge from 'deepmerge';
|
||||
import { Col, Row } from 'antd';
|
||||
import merge from 'deepmerge';
|
||||
import template from 'lodash/template';
|
||||
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
ACLCollectionProvider,
|
||||
TableFieldResource,
|
||||
WithoutTableFieldResource,
|
||||
useAPIClient,
|
||||
@ -16,11 +15,11 @@ import {
|
||||
useDesignable,
|
||||
useRecord,
|
||||
} from '../';
|
||||
import { ACLCollectionProvider } from '../acl/ACLProvider';
|
||||
import { CollectionProvider, useCollection, useCollectionManager } from '../collection-manager';
|
||||
import { FilterBlockRecord } from '../filter-provider/FilterProvider';
|
||||
import { useRecordIndex } from '../record-provider';
|
||||
import { SharedFilterProvider } from './SharedFilterProvider';
|
||||
import _ from 'lodash';
|
||||
|
||||
export const BlockResourceContext = createContext(null);
|
||||
export const BlockAssociationContext = createContext(null);
|
||||
@ -161,7 +160,7 @@ export const MaybeCollectionProvider = (props) => {
|
||||
<ACLCollectionProvider>{props.children}</ACLCollectionProvider>
|
||||
</CollectionProvider>
|
||||
) : (
|
||||
<>{props.children}</>
|
||||
props.children
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useField } from '@formily/react';
|
||||
import React, { forwardRef, useState } from 'react';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
import Column from './Column';
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { createForm, Field } from '@formily/core';
|
||||
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 { AsyncDataProvider, useAsyncData, useRequest } from '../..';
|
||||
import { useAttach } from '@formily/react/lib/hooks/useAttach';
|
||||
import { TableProps } from 'antd';
|
||||
import { CollectionFieldsTableArray } from './CollectionFieldsTableArray';
|
||||
|
||||
type TableVoidProps = TableProps<any> & {
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { useCollectionManager } from '.';
|
||||
import { CollectionProvider, useRecord } from '..';
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { defaultProps, recordPickerSelector, recordPickerViewer } from './properties';
|
||||
import { defaultProps } from './properties';
|
||||
import { IField } from './types';
|
||||
|
||||
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 = {
|
||||
type: 'void',
|
||||
title: '{{ t("View record") }}',
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import { uniqBy } from 'lodash';
|
||||
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 { CollectionFieldOptions, useCollection } from '../collection-manager';
|
||||
import { removeNullCondition } from '../schema-component';
|
||||
import { useAssociatedFields } from './utils';
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
type Collection = ReturnType<typeof useCollection>;
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import i18next from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import locale from '../locale';
|
||||
const log = require('debug')('i18next');
|
||||
|
||||
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
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Icon } from '@nocobase/client';
|
||||
|
||||
export default () => <Icon type="BookOutlined" />
|
||||
```
|
||||
<code src="./demos/antd-icon.tsx"></code>
|
||||
|
||||
Use iconfont.cn
|
||||
|
||||
```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" />
|
||||
```
|
||||
<code src="./demos/iconfont.tsx"/>
|
||||
|
||||
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} />
|
||||
```
|
||||
<code src="./demos/custom-icon.tsx"/>
|
||||
|
||||
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'} />
|
||||
```
|
||||
<code src="./demos/register-icon.tsx"/>
|
||||
|
@ -7,11 +7,3 @@ exports[`route-switch > case 1 1`] = `
|
||||
</h1>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`route-switch case 1 1`] = `
|
||||
<div>
|
||||
<h1>
|
||||
Home
|
||||
</h1>
|
||||
</div>
|
||||
`;
|
||||
|
@ -28,7 +28,7 @@ export const ActionPage: ComposedActionDrawer = observer(
|
||||
{containerRef?.current &&
|
||||
visible &&
|
||||
createPortal(
|
||||
<div>
|
||||
<div className="nb-action-page">
|
||||
<RecursionField
|
||||
basePath={field.address}
|
||||
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 { Action, Form, SchemaComponent, SchemaComponentProvider, useActionContext } from '@nocobase/client';
|
||||
import {
|
||||
Action,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
useActionContext,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
const useCloseAction = () => {
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { FormItem, Input } from '@formily/antd';
|
||||
import { ISchema, observer, useForm } from '@formily/react';
|
||||
import {
|
||||
Action,
|
||||
ActionContext,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
useActionContext,
|
||||
|
@ -1,19 +1,14 @@
|
||||
import { FormItem, Input } from '@formily/antd';
|
||||
import { Field } from '@formily/core';
|
||||
import { ISchema, observer, useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { Action, Form, SchemaComponent, SchemaComponentProvider, useActionContext } from '@nocobase/client';
|
||||
import { Select } from 'antd';
|
||||
import { ISchema, observer, useField, useFieldSchema } from '@formily/react';
|
||||
import { Action, SchemaComponent, SchemaComponentProvider, useActionContext } from '@nocobase/client';
|
||||
import { Radio } from 'antd';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
const useCloseAction = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
const form = useForm();
|
||||
return {
|
||||
async run() {
|
||||
setVisible(false);
|
||||
form.submit((values) => {
|
||||
console.log(values);
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -26,18 +21,17 @@ const Editable = observer(
|
||||
<div>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
打开方式:
|
||||
<Select
|
||||
<Radio.Group
|
||||
value={field.componentProps.openMode}
|
||||
style={{ width: 100 }}
|
||||
onChange={(value) => {
|
||||
field.componentProps.openMode = value;
|
||||
schema['x-component-props']['openMode'] = value;
|
||||
onChange={(e) => {
|
||||
field.componentProps.openMode = e.target.value;
|
||||
schema['x-component-props']['openMode'] = e.target.value;
|
||||
}}
|
||||
>
|
||||
<Select.Option value={'drawer'}>Drawer</Select.Option>
|
||||
<Select.Option value={'modal'}>Modal</Select.Option>
|
||||
<Select.Option value={'page'}>Page</Select.Option>
|
||||
</Select>
|
||||
<Radio.Button value="drawer">Drawer</Radio.Button>
|
||||
<Radio.Button value="modal">Modal</Radio.Button>
|
||||
<Radio.Button value="page">Page</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
@ -50,6 +44,8 @@ const schema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action1: {
|
||||
type: 'void',
|
||||
title: 'Open',
|
||||
'x-decorator': 'Editable',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
@ -57,22 +53,20 @@ const schema: ISchema = {
|
||||
openMode: 'modal',
|
||||
containerRefKey: 'containerRef',
|
||||
},
|
||||
type: 'void',
|
||||
title: 'Open',
|
||||
properties: {
|
||||
drawer1: {
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {},
|
||||
type: 'void',
|
||||
title: 'Modal Title',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
hello1: {
|
||||
title: 'T1',
|
||||
'x-content': 'Hello',
|
||||
},
|
||||
footer1: {
|
||||
'x-component': 'Action.Container.Footer',
|
||||
type: 'void',
|
||||
'x-component': 'Action.Container.Footer',
|
||||
properties: {
|
||||
close1: {
|
||||
title: 'Close',
|
||||
@ -99,12 +93,9 @@ const schema: ISchema = {
|
||||
};
|
||||
|
||||
export default observer(() => {
|
||||
const containerRef = useRef();
|
||||
const containerRef = useRef(null);
|
||||
return (
|
||||
<SchemaComponentProvider
|
||||
scope={{ containerRef, useCloseAction }}
|
||||
components={{ Editable, Form, Action, Input, FormItem }}
|
||||
>
|
||||
<SchemaComponentProvider scope={{ useCloseAction, containerRef }} components={{ Editable, Action }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
<div ref={containerRef}></div>
|
||||
</SchemaComponentProvider>
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { FormItem, Input } from '@formily/antd';
|
||||
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';
|
||||
|
||||
const useCloseAction = () => {
|
||||
|
@ -10,9 +10,9 @@ import {
|
||||
NumberPicker,
|
||||
Submit,
|
||||
} from '@formily/antd';
|
||||
import { Select } from 'antd';
|
||||
import { createForm, Field, onFieldValueChange } from '@formily/core';
|
||||
import { Field, createForm, onFieldValueChange } from '@formily/core';
|
||||
import { connect, createSchemaField, observer, useField, useForm, useFormEffects } from '@formily/react';
|
||||
import { Select } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const ViewOptions = connect((props) => {
|
||||
@ -110,6 +110,10 @@ const EditOptions = observer(
|
||||
})
|
||||
.then((values) => {
|
||||
field.value = values;
|
||||
return values;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { RecursionField, observer, useField, useFieldSchema } from '@formily/react';
|
||||
import { Input, Select } from 'antd';
|
||||
import { differenceBy, unionBy } from 'lodash';
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useContext, useMemo, useState } from 'react';
|
||||
import {
|
||||
FormProvider,
|
||||
RecordPickerContext,
|
||||
@ -9,6 +9,7 @@ import {
|
||||
SchemaComponentOptions,
|
||||
useActionContext,
|
||||
} from '../..';
|
||||
import { RecordProvider } from '../../../';
|
||||
import {
|
||||
TableSelectorParamsProvider,
|
||||
useTableSelectorProps as useTsp,
|
||||
@ -19,7 +20,6 @@ import { ActionContext } from '../action';
|
||||
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
||||
import schema from './schema';
|
||||
import { flatData, getLabelFormatValue, useLabelUiSchema } from './util';
|
||||
import { RecordProvider } from '../../../';
|
||||
|
||||
const useTableSelectorProps = () => {
|
||||
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
|
||||
---
|
||||
|
||||
# RemoteSelect
|
||||
# AssociationSelect
|
||||
|
||||
## Examples
|
||||
|
||||
<code src="./demos/demo1.tsx"/>
|
||||
|
||||
## 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';
|
||||
|
||||
const Block = observer(
|
||||
(props) => {
|
||||
() => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
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}
|
||||
<DragHandler />
|
||||
</div>
|
||||
@ -16,34 +19,34 @@ const Block = observer(
|
||||
{ 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() {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ DndContext, BlockItem, Block }}>
|
||||
<SchemaComponent
|
||||
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(),
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||
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 { eq } from 'date-arithmetic';
|
||||
import get from 'lodash/get';
|
||||
import moment from 'moment';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
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 { RecordProvider } from '../../../';
|
||||
import { i18n } from '../../../i18n';
|
||||
@ -247,7 +247,7 @@ export const Calendar: any = observer(
|
||||
agendaDateFormat: 'M-DD',
|
||||
dayHeaderFormat: 'Y-M-DD',
|
||||
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)} - ${local.format(end, 'Y-M', culture)}`;
|
||||
|
@ -2,7 +2,7 @@ import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { observer } from '@formily/react';
|
||||
import { Button } from 'antd';
|
||||
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 { CalendarToolbarContext } from './context';
|
||||
|
||||
@ -12,8 +12,8 @@ export const Nav = observer(
|
||||
const { onNavigate } = useContext(CalendarToolbarContext);
|
||||
return (
|
||||
<div className="ant-btn-group">
|
||||
<Button icon={<LeftOutlined />} onClick={() => onNavigate(navigate.PREVIOUS)}></Button>
|
||||
<Button icon={<RightOutlined />} onClick={() => onNavigate(navigate.NEXT)}></Button>
|
||||
<Button icon={<LeftOutlined />} onClick={() => onNavigate(Navigate.PREVIOUS)}></Button>
|
||||
<Button icon={<RightOutlined />} onClick={() => onNavigate(Navigate.NEXT)}></Button>
|
||||
<DesignableBar />
|
||||
</div>
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { observer } from '@formily/react';
|
||||
import { Button } from 'antd';
|
||||
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 { useDesignable } from '../../hooks';
|
||||
import { CalendarToolbarContext } from './context';
|
||||
@ -14,7 +14,7 @@ export const Today = observer(
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
onNavigate(navigate.TODAY);
|
||||
onNavigate(Navigate.TODAY);
|
||||
}}
|
||||
>
|
||||
{t('Today')}
|
||||
|
@ -17,7 +17,9 @@ export const ViewSelect = observer(
|
||||
<div className="ant-btn-group">
|
||||
<Select value={view} onChange={onView}>
|
||||
{views.map((name) => (
|
||||
<Select.Option value={name}>{messages[name]}</Select.Option>
|
||||
<Select.Option key={name} value={name}>
|
||||
{messages[name]}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<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>
|
||||
);
|
||||
};
|
@ -7,4 +7,8 @@ group:
|
||||
|
||||
# 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];
|
||||
}, 500);
|
||||
}, 300);
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
|
||||
import { isValid } from '@formily/shared';
|
||||
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 React from 'react';
|
||||
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 { connect, mapReadPretty } from '@formily/react';
|
||||
import { error } from '@nocobase/utils/client';
|
||||
import cronstrue from 'cronstrue';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Cron as ReactCron, CronProps } from 'react-js-cron';
|
||||
import { useAPIClient } from '../../../api-client';
|
||||
import { CronProps, Cron as ReactCron } from 'react-js-cron';
|
||||
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 { onChange, ...rest } = props;
|
||||
@ -46,7 +46,7 @@ const ReadPretty = (props) => {
|
||||
use24HourTimeFormat: true,
|
||||
});
|
||||
} 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;
|
||||
}
|
||||
}, [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 {
|
||||
DatePickerProps as AntdDatePickerProps,
|
||||
RangePickerProps as AntdRangePickerProps,
|
||||
} from 'antd/lib/date-picker';
|
||||
} from 'antd/es/date-picker';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReadPretty } from './ReadPretty';
|
||||
@ -40,7 +40,7 @@ export const DatePicker = (props) => {
|
||||
return <_DatePicker {...props} />;
|
||||
};
|
||||
|
||||
DatePicker.RangePicker = (props) => {
|
||||
DatePicker.RangePicker = function RangePicker(props) {
|
||||
const { t } = useTranslation();
|
||||
const { utc = true } = useDatePickerContext();
|
||||
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 { getDefaultFormat, str2moment } from '@nocobase/utils/client';
|
||||
import type {
|
||||
DatePickerProps as AntdDatePickerProps,
|
||||
RangePickerProps as AntdRangePickerProps,
|
||||
} from 'antd/lib/date-picker';
|
||||
} from 'antd/es/date-picker';
|
||||
import cls from 'classnames';
|
||||
import moment from 'moment';
|
||||
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 { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
@ -12,36 +12,31 @@ const schema = {
|
||||
type: 'boolean',
|
||||
title: `Editable`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
dateFormat: 'YYYY/MM/DD',
|
||||
showTime: true,
|
||||
gmt: true,
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
target: 'read1',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value}}',
|
||||
},
|
||||
default: '2022-06-04T15:00:00.000Z',
|
||||
'x-reactions': {
|
||||
target: '*(read1,read2)',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'read2',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value && $self.value.join(" ~ ")}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
read1: {
|
||||
type: 'boolean',
|
||||
type: 'string',
|
||||
title: `Read pretty`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
dateFormat: 'YYYY/MM/DD',
|
||||
showTime: true,
|
||||
gmt: true,
|
||||
},
|
||||
},
|
||||
|
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* title: DatePicker (GMT)
|
||||
* title: DatePicker
|
||||
*/
|
||||
import { FormItem } from '@formily/antd';
|
||||
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
@ -15,10 +15,9 @@ const schema = {
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
dateFormat: 'YYYY/MM/DD',
|
||||
showTime: true,
|
||||
gmt: true,
|
||||
showTime: false,
|
||||
utc: false,
|
||||
},
|
||||
default: '2022-06-04T15:00:00.000Z',
|
||||
'x-reactions': {
|
||||
target: '*(read1,read2)',
|
||||
fulfill: {
|
||||
@ -29,7 +28,7 @@ const schema = {
|
||||
},
|
||||
},
|
||||
read1: {
|
||||
type: 'string',
|
||||
type: 'boolean',
|
||||
title: `Read pretty`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
@ -37,7 +36,6 @@ const schema = {
|
||||
'x-component-props': {
|
||||
dateFormat: 'YYYY/MM/DD',
|
||||
showTime: true,
|
||||
gmt: true,
|
||||
},
|
||||
},
|
||||
read2: {
|
||||
|
@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { FormItem } from '@formily/antd';
|
||||
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
|
||||
const schema = {
|
||||
@ -14,7 +15,8 @@ const schema = {
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
'x-component-props': {
|
||||
utc: false,
|
||||
gmt: true,
|
||||
defaultPickerValue: [moment('2023-05-01')],
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
@ -41,6 +43,10 @@ const schema = {
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
'x-component-props': {
|
||||
gmt: true,
|
||||
defaultPickerValue: [moment('2023-05-01')],
|
||||
},
|
||||
},
|
||||
read2: {
|
||||
type: 'string',
|
@ -1,8 +1,9 @@
|
||||
/**
|
||||
* title: DatePicker
|
||||
* title: DatePicker.RangePicker
|
||||
*/
|
||||
import { FormItem } from '@formily/antd';
|
||||
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
|
||||
const schema = {
|
||||
@ -12,30 +13,38 @@ const schema = {
|
||||
type: 'boolean',
|
||||
title: `Editable`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
'x-component-props': {
|
||||
dateFormat: 'YYYY/MM/DD',
|
||||
showTime: false,
|
||||
utc: false,
|
||||
gmt: false,
|
||||
defaultPickerValue: [moment('2023-05-01')],
|
||||
},
|
||||
'x-reactions': {
|
||||
target: '*(read1,read2)',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value}}',
|
||||
'x-reactions': [
|
||||
{
|
||||
target: 'read1',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'read2',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value && $self.value.join(" ~ ")}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
read1: {
|
||||
type: 'boolean',
|
||||
title: `Read pretty`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
'x-component-props': {
|
||||
dateFormat: 'YYYY/MM/DD',
|
||||
showTime: true,
|
||||
defaultPickerValue: [moment('2023-05-01')],
|
||||
},
|
||||
},
|
||||
read2: {
|
||||
|
@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { FormItem } from '@formily/antd';
|
||||
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
|
||||
const schema = {
|
||||
@ -14,7 +15,8 @@ const schema = {
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
'x-component-props': {
|
||||
gmt: false,
|
||||
utc: false,
|
||||
defaultPickerValue: [moment('2023-05-01')],
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
@ -41,6 +43,9 @@ const schema = {
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
'x-component-props': {
|
||||
defaultPickerValue: [moment('2023-05-01')],
|
||||
},
|
||||
},
|
||||
read2: {
|
||||
type: 'string',
|
||||
|
@ -15,23 +15,23 @@ group:
|
||||
|
||||
### DatePicker (GMT)
|
||||
|
||||
<code src="./demos/demo3.tsx" />
|
||||
<code src="./demos/demo2.tsx" />
|
||||
|
||||
### DatePicker (non-UTC)
|
||||
|
||||
<code src="./demos/demo5.tsx" />
|
||||
<code src="./demos/demo3.tsx" />
|
||||
|
||||
### RangePicker (GMT)
|
||||
|
||||
<code src="./demos/demo2.tsx" />
|
||||
<code src="./demos/demo4.tsx" />
|
||||
|
||||
### RangePicker (non-GMT)
|
||||
|
||||
<code src="./demos/demo6.tsx" />
|
||||
<code src="./demos/demo5.tsx" />
|
||||
|
||||
### RangePicker (non-UTC)
|
||||
|
||||
<code src="./demos/demo7.tsx" />
|
||||
<code src="./demos/demo6.tsx" />
|
||||
|
||||
## 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 { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFieldSchema, useField, ISchema } from '@formily/react';
|
||||
import { useDesignable } from '../..';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
|
||||
export const ExpandActionDesign = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const field = useField();
|
||||
const { dn } = useDesignable();
|
||||
const { titleExpand, titleCollapse, iconExpand, iconCollapse } = fieldSchema['x-component-props'] || {}
|
||||
const { titleExpand, titleCollapse, iconExpand, iconCollapse } = fieldSchema['x-component-props'] || {};
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner {...props} disableInitializer>
|
||||
@ -24,13 +24,13 @@ export const ExpandActionDesign = (props) => {
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
title: `${t('Button title')} - ${t('Expand all')}`,
|
||||
default: titleExpand
|
||||
default: titleExpand,
|
||||
},
|
||||
titleCollapse: {
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
title: `${t('Button title')} - ${t('Collapse all')}`,
|
||||
default: titleCollapse
|
||||
default: titleCollapse,
|
||||
},
|
||||
iconExpand: {
|
||||
'x-decorator': 'FormItem',
|
||||
@ -75,14 +75,14 @@ export const ExpandActionDesign = (props) => {
|
||||
iconExpand,
|
||||
iconCollapse,
|
||||
type,
|
||||
danger: type === 'danger'
|
||||
danger: type === 'danger',
|
||||
};
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
'x-component-props': fieldSchema['x-component-props'],
|
||||
title: fieldSchema.title,
|
||||
}
|
||||
},
|
||||
});
|
||||
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 { useFieldSchema } from '@formily/react';
|
||||
import { Button } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTableBlockContext, useTableSelectorContext } from '../../../block-provider';
|
||||
import { Icon } from '../../../icon';
|
||||
|
||||
const actionDesignerCss = css`
|
||||
@ -46,10 +45,9 @@ const actionDesignerCss = css`
|
||||
`;
|
||||
|
||||
export const ExpandAction = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const schema = useFieldSchema();
|
||||
const ctxSelector = useTableSelectorContext()
|
||||
const ctxBlock = useTableBlockContext()
|
||||
const ctxSelector = useTableSelectorContext();
|
||||
const ctxBlock = useTableBlockContext();
|
||||
const isTableSelector = schema.parent?.parent?.['x-decorator'] === 'TableSelectorProvider';
|
||||
const ctx = isTableSelector ? ctxSelector : ctxBlock;
|
||||
const { titleExpand, titleCollapse, iconExpand, iconCollapse } = schema['x-component-props'] || {};
|
||||
@ -64,7 +62,7 @@ export const ExpandAction = (props) => {
|
||||
type={props.type}
|
||||
>
|
||||
{props.children[1]}
|
||||
<span style={{ marginLeft: 10 }}>{ ctx?.expandFlag ? titleCollapse : titleExpand }</span>
|
||||
<span style={{ marginLeft: 10 }}>{ctx?.expandFlag ? titleCollapse : titleExpand}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ExpandAction } from './Expand.Action'
|
||||
import { ExpandActionDesign } from './Expand.Action.Design'
|
||||
import { ExpandAction } from './Expand.Action';
|
||||
import { ExpandActionDesign } from './Expand.Action.Design';
|
||||
|
||||
export const Expand: any = ExpandAction
|
||||
export const Expand: any = ExpandAction;
|
||||
|
||||
Expand.Action = ExpandAction
|
||||
Expand.Action.Design = ExpandActionDesign
|
||||
Expand.Action = ExpandAction;
|
||||
Expand.Action.Design = ExpandActionDesign;
|
||||
|
@ -4,11 +4,11 @@ import React from 'react';
|
||||
import { useRequest } from '../../../api-client';
|
||||
import { useProps } from '../../hooks/useProps';
|
||||
import { DatePickerProvider } from '../date-picker';
|
||||
import { FilterContext } from './context';
|
||||
import { FilterActionDesigner } from './Filter.Action.Designer';
|
||||
import { FilterAction } from './FilterAction';
|
||||
import { FilterGroup } from './FilterGroup';
|
||||
import { SaveDefaultValue } from './SaveDefaultValue';
|
||||
import { FilterContext } from './context';
|
||||
|
||||
const useDef = (options) => {
|
||||
const field = useField<ObjectFieldModel>();
|
||||
|
@ -2,11 +2,11 @@ import { CloseCircleOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { observer } from '@formily/react';
|
||||
import { Cascader, Select, Space } from 'antd';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCompile } from '../..';
|
||||
import { RemoveConditionContext } from './context';
|
||||
import { DynamicComponent } from './DynamicComponent';
|
||||
import { RemoveConditionContext } from './context';
|
||||
import { useValues } from './useValues';
|
||||
|
||||
export const FilterItem = observer(
|
||||
@ -16,7 +16,8 @@ export const FilterItem = observer(
|
||||
const remove = useContext(RemoveConditionContext);
|
||||
const { schema, fields, operators, dataIndex, operator, setDataIndex, setOperator, value, setValue } = useValues();
|
||||
return (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
// 添加 nc-filter-item 类名是为了帮助编写测试时更容易选中该元素
|
||||
<div style={{ marginBottom: 8 }} className="nc-filter-item">
|
||||
<Space>
|
||||
<Cascader
|
||||
className={css`
|
||||
@ -46,14 +47,7 @@ export const FilterItem = observer(
|
||||
}}
|
||||
placeholder={t('Comparision')}
|
||||
/>
|
||||
{!operator?.noValue &&
|
||||
React.createElement(DynamicComponent, {
|
||||
value: value ? value : undefined,
|
||||
schema,
|
||||
onChange(value) {
|
||||
setValue(value);
|
||||
},
|
||||
})}
|
||||
{!operator?.noValue ? <DynamicComponent value={value} schema={schema} onChange={setValue} /> : null}
|
||||
{!props.disabled && (
|
||||
<a>
|
||||
<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 {
|
||||
AntdSchemaComponentProvider,
|
||||
SharedFilter,
|
||||
Input,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
} from '@nocobase/client';
|
||||
import { AntdSchemaComponentProvider, Filter, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
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 = {
|
||||
type: 'void',
|
||||
properties: {
|
||||
demo: {
|
||||
name: 'filter',
|
||||
type: 'object',
|
||||
enum: [
|
||||
{
|
||||
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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
enum: options,
|
||||
default: defaultValue,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {},
|
||||
},
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import {
|
||||
AntdSchemaComponentProvider,
|
||||
SharedFilter,
|
||||
Filter,
|
||||
Input,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
useActionContext,
|
||||
useRequest,
|
||||
useActionContext
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
@ -74,6 +73,21 @@ const dataSource = [
|
||||
},
|
||||
];
|
||||
|
||||
const defaultValue = {
|
||||
$or: [
|
||||
{
|
||||
name: {
|
||||
$ne: 'aa',
|
||||
},
|
||||
},
|
||||
{
|
||||
'tags.title': {
|
||||
$eq: 'aaa',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@ -99,31 +113,10 @@ const schema: ISchema = {
|
||||
properties: {
|
||||
filter: {
|
||||
type: 'object',
|
||||
default: {
|
||||
$or: [
|
||||
{
|
||||
name: {
|
||||
$ne: 'aa',
|
||||
},
|
||||
},
|
||||
{
|
||||
'tags.title': {
|
||||
$eq: 'aaa',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
default: defaultValue,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {
|
||||
useDataSource(options) {
|
||||
return useRequest(
|
||||
() =>
|
||||
Promise.resolve({
|
||||
data: dataSource,
|
||||
}),
|
||||
options,
|
||||
);
|
||||
},
|
||||
options: dataSource,
|
||||
},
|
||||
},
|
||||
footer: {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import {
|
||||
AntdSchemaComponentProvider,
|
||||
SharedFilter,
|
||||
Filter,
|
||||
Input,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
@ -78,12 +78,12 @@ const defaultValue = {
|
||||
$or: [
|
||||
{
|
||||
name: {
|
||||
$ne: '{{node1.field1}}',
|
||||
$ne: 'aaa',
|
||||
},
|
||||
},
|
||||
{
|
||||
'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';
|
||||
|
||||
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',
|
||||
properties: {
|
||||
demo: {
|
||||
type: 'object',
|
||||
enum: [
|
||||
{
|
||||
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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
title: 'Filter',
|
||||
enum: '{{ options }}',
|
||||
default: defaultValue,
|
||||
'x-component': 'FilterAction',
|
||||
'x-component-props': {},
|
||||
},
|
||||
@ -91,10 +97,8 @@ const schema: any = {
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<SchemaComponentProvider>
|
||||
<AntdSchemaComponentProvider>
|
||||
<SchemaComponent components={{ FilterAction }} schema={schema} />
|
||||
</AntdSchemaComponentProvider>
|
||||
<SchemaComponentProvider components={{ FilterAction, Filter, Input }} scope={{ options }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import {
|
||||
AntdSchemaComponentProvider,
|
||||
CollectionManagerProvider,
|
||||
CollectionProvider,
|
||||
SharedFilter,
|
||||
Filter,
|
||||
Input,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
|
@ -1,2 +1,4 @@
|
||||
export * from './Filter';
|
||||
export * from './FilterAction';
|
||||
export * from './useFilterActionProps';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useField } from '@formily/react';
|
||||
import { merge } from '@formily/shared';
|
||||
import flat from 'flat';
|
||||
import flat, { unflatten } from 'flat';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import get from 'lodash/get';
|
||||
import { useContext, useEffect } from 'react';
|
||||
@ -31,7 +31,7 @@ export const useValues = () => {
|
||||
};
|
||||
const value2data = () => {
|
||||
field.data = field.data || {};
|
||||
const values = flat(field.value || {});
|
||||
const values: object = flat(field.value || {});
|
||||
const path = Object.keys(values).shift() || '';
|
||||
if (!path || !options) {
|
||||
return;
|
||||
@ -46,7 +46,7 @@ export const useValues = () => {
|
||||
field.data.operators = operators;
|
||||
field.data.operator = operator;
|
||||
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]);
|
||||
return {
|
||||
|
@ -2,14 +2,14 @@ import { css, cx } from '@emotion/css';
|
||||
import { ArrayCollapse, ArrayItems, FormLayout, FormItem as Item } from '@formily/antd';
|
||||
import { Field } from '@formily/core';
|
||||
import { ISchema, Schema, observer, useField, useFieldSchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Select } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ACLCollectionFieldProvider } from '../../../acl/ACLProvider';
|
||||
import { BlockRequestContext, useFormBlockContext } from '../../../block-provider';
|
||||
import { useBlockRequestContext } from '../../../block-provider/BlockProvider';
|
||||
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
|
||||
import {
|
||||
Collection,
|
||||
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(
|
||||
(props: any) => {
|
||||
useEnsureOperatorsValid();
|
||||
|
||||
const field = useField<Field>();
|
||||
const ctx = useContext(BlockRequestContext);
|
||||
const ctx = useBlockRequestContext();
|
||||
const schema = useFieldSchema();
|
||||
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
|
||||
|
||||
表单字段装饰器。除此之外,也继承了 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 {
|
||||
AntdSchemaComponentProvider,
|
||||
APIClient,
|
||||
APIClientProvider,
|
||||
BlockSchemaComponentProvider,
|
||||
Action,
|
||||
CollectionField,
|
||||
CollectionManagerProvider,
|
||||
FormBlockProvider,
|
||||
FormItem,
|
||||
FormV2,
|
||||
Input,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
useFormBlockContext,
|
||||
} from '@nocobase/client';
|
||||
import { notification } from 'antd';
|
||||
import React from 'react';
|
||||
import { useFilterByTk } from '../../../../block-provider/BlockProvider';
|
||||
import { mockAPIClient } from '../../../../test';
|
||||
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() {
|
||||
const ctx = useFormBlockContext();
|
||||
const form = useForm();
|
||||
const filterByTk = useFilterByTk();
|
||||
return {
|
||||
async run() {
|
||||
console.log('form.values', form.values);
|
||||
// await ctx.resource.create({
|
||||
// values: form.values,
|
||||
// });
|
||||
await ctx.resource.update({
|
||||
filterByTk,
|
||||
values: form.values,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -47,7 +64,7 @@ const schema: ISchema = {
|
||||
'x-component': 'CollectionField',
|
||||
},
|
||||
button: {
|
||||
title: '提交',
|
||||
title: 'Submit',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
htmlType: 'submit',
|
||||
@ -62,22 +79,14 @@ const schema: ISchema = {
|
||||
},
|
||||
};
|
||||
|
||||
const apiClient = new APIClient({
|
||||
baseURL: 'http://localhost:3000/api',
|
||||
});
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
<SchemaComponentProvider>
|
||||
<CollectionManagerProvider collections={collections.data}>
|
||||
<AntdSchemaComponentProvider>
|
||||
<BlockSchemaComponentProvider>
|
||||
<SchemaComponent schema={schema} />
|
||||
</BlockSchemaComponentProvider>
|
||||
</AntdSchemaComponentProvider>
|
||||
</CollectionManagerProvider>
|
||||
</SchemaComponentProvider>
|
||||
<CollectionManagerProvider collections={collections}>
|
||||
<SchemaComponentProvider components={{ FormBlockProvider, FormV2, FormItem, CollectionField, Action, Input }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</CollectionManagerProvider>
|
||||
</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