mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
Merge branch 'main' into T-1155
This commit is contained in:
commit
99c1e3c879
20
CHANGELOG.md
20
CHANGELOG.md
@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
## [v0.11.1-alpha.4](https://github.com/nocobase/nocobase/compare/v0.11.1-alpha.3...v0.11.1-alpha.4) - 2023-07-29
|
||||
|
||||
### Merged
|
||||
|
||||
- refactor(plugin-workflow): allow system values to be assigned in create and update node [`#2345`](https://github.com/nocobase/nocobase/pull/2345)
|
||||
- chore(database): merge fields arguments by path [`#2331`](https://github.com/nocobase/nocobase/pull/2331)
|
||||
- fix(theme-editor): avoid error [`#2340`](https://github.com/nocobase/nocobase/pull/2340)
|
||||
- refactor: upgrade @testing-library/react to 14.x [`#2339`](https://github.com/nocobase/nocobase/pull/2339)
|
||||
- test: view collection as through model [`#2336`](https://github.com/nocobase/nocobase/pull/2336)
|
||||
- fix: sub-form record provider data failed to matching [`#2337`](https://github.com/nocobase/nocobase/pull/2337)
|
||||
- fix(bi): issue of formatting relation field & reference link of line chart [`#2332`](https://github.com/nocobase/nocobase/pull/2332)
|
||||
- chore: tsx [`#2329`](https://github.com/nocobase/nocobase/pull/2329)
|
||||
- chore: upgrade jest [`#2323`](https://github.com/nocobase/nocobase/pull/2323)
|
||||
|
||||
### Commits
|
||||
|
||||
- chore(versions): 😊 publish v0.11.1-alpha.4 [`b93f28a`](https://github.com/nocobase/nocobase/commit/b93f28a952fef20e99570ca6f19b3bf8192db465)
|
||||
- fix: yarn run test [`d956c90`](https://github.com/nocobase/nocobase/commit/d956c90e91e303ae02e54f71498b92481eab0399)
|
||||
- chore: update changelog [`54f2405`](https://github.com/nocobase/nocobase/commit/54f240539c5cf82d31c689bf409bcb5656ded496)
|
||||
|
||||
## [v0.11.1-alpha.3](https://github.com/nocobase/nocobase/compare/v0.11.1-alpha.2...v0.11.1-alpha.3) - 2023-07-26
|
||||
|
||||
### Merged
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"npmClientArgs": ["--ignore-engines"],
|
||||
|
@ -58,8 +58,8 @@
|
||||
"@commitlint/cli": "^16.1.0",
|
||||
"@commitlint/config-conventional": "^16.0.0",
|
||||
"@commitlint/prompt-cli": "^16.1.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
|
@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@nocobase/app-client",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"license": "AGPL-3.0",
|
||||
"devDependencies": {
|
||||
"@nocobase/client": "0.11.1-alpha.3"
|
||||
"@nocobase/client": "0.11.1-alpha.5"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@nocobase/app-server",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/preset-nocobase": "0.11.1-alpha.3"
|
||||
"@nocobase/preset-nocobase": "0.11.1-alpha.5"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@nocobase/acl",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/resourcer": "0.11.1-alpha.3",
|
||||
"@nocobase/utils": "0.11.1-alpha.3",
|
||||
"@nocobase/resourcer": "0.11.1-alpha.5",
|
||||
"@nocobase/utils": "0.11.1-alpha.5",
|
||||
"minimatch": "^5.1.1"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@nocobase/actions",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/cache": "0.11.1-alpha.3",
|
||||
"@nocobase/database": "0.11.1-alpha.3",
|
||||
"@nocobase/resourcer": "0.11.1-alpha.3"
|
||||
"@nocobase/cache": "0.11.1-alpha.5",
|
||||
"@nocobase/database": "0.11.1-alpha.5",
|
||||
"@nocobase/resourcer": "0.11.1-alpha.5"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@nocobase/auth",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/actions": "0.11.1-alpha.3",
|
||||
"@nocobase/database": "0.11.1-alpha.3",
|
||||
"@nocobase/resourcer": "0.11.1-alpha.3",
|
||||
"@nocobase/utils": "0.11.1-alpha.3"
|
||||
"@nocobase/actions": "0.11.1-alpha.5",
|
||||
"@nocobase/database": "0.11.1-alpha.5",
|
||||
"@nocobase/resourcer": "0.11.1-alpha.5",
|
||||
"@nocobase/utils": "0.11.1-alpha.5"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/build",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"description": "Library build tool based on rollup.",
|
||||
"main": "lib/index.js",
|
||||
"bin": {
|
||||
|
2
packages/core/cache/package.json
vendored
2
packages/core/cache/package.json
vendored
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/cache",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/cli",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./src/index.js",
|
||||
@ -22,7 +22,7 @@
|
||||
"tsx": "^3.12.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nocobase/devtools": "0.11.1-alpha.3"
|
||||
"@nocobase/devtools": "0.11.1-alpha.5"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/client",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"license": "Apache-2.0",
|
||||
"main": "lib",
|
||||
"module": "es/index.js",
|
||||
@ -12,12 +12,19 @@
|
||||
"@dnd-kit/core": "^5.0.1",
|
||||
"@dnd-kit/sortable": "^6.0.0",
|
||||
"@emotion/css": "^11.7.1",
|
||||
"@formily/antd-v5": "^1.1.0-beta.4",
|
||||
"@formily/core": "2.2.26",
|
||||
"@formily/react": "2.2.26",
|
||||
"@nocobase/evaluators": "0.11.1-alpha.3",
|
||||
"@nocobase/sdk": "0.11.1-alpha.3",
|
||||
"@nocobase/utils": "0.11.1-alpha.3",
|
||||
"@formily/antd-v5": "^1.1.0",
|
||||
"@formily/core": "^2.2.27",
|
||||
"@formily/grid": "^2.2.27",
|
||||
"@formily/json-schema": "^2.2.27",
|
||||
"@formily/path": "^2.2.27",
|
||||
"@formily/react": "^2.2.27",
|
||||
"@formily/reactive": "^2.2.27",
|
||||
"@formily/reactive-react": "^2.2.27",
|
||||
"@formily/shared": "^2.2.27",
|
||||
"@formily/validator": "^2.2.27",
|
||||
"@nocobase/evaluators": "0.11.1-alpha.5",
|
||||
"@nocobase/sdk": "0.11.1-alpha.5",
|
||||
"@nocobase/utils": "0.11.1-alpha.5",
|
||||
"ahooks": "^3.7.2",
|
||||
"antd": "^5.6.4",
|
||||
"antd-style": "^3.3.0",
|
||||
@ -55,7 +62,7 @@
|
||||
"react-is": ">=18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/markdown-it": "12.2.3",
|
||||
"@types/markdown-it-highlightjs": "3.3.1",
|
||||
"@types/react-big-calendar": "^1.6.4",
|
||||
|
@ -5,7 +5,15 @@ import { useRequest } from '../api-client';
|
||||
export const CurrentAppInfoContext = createContext(null);
|
||||
|
||||
export const useCurrentAppInfo = () => {
|
||||
return useContext(CurrentAppInfoContext);
|
||||
return useContext<{
|
||||
data: {
|
||||
database: {
|
||||
dialect: string;
|
||||
};
|
||||
lang: string;
|
||||
version: string;
|
||||
};
|
||||
}>(CurrentAppInfoContext);
|
||||
};
|
||||
export const CurrentAppInfoProvider = (props) => {
|
||||
const result = useRequest({
|
||||
|
@ -360,6 +360,9 @@ export const useParamsFromRecord = () => {
|
||||
const obj = {
|
||||
filterByTk: filterByTk,
|
||||
};
|
||||
if (record.__collection) {
|
||||
obj['targetCollection'] = record.__collection;
|
||||
}
|
||||
if (!filterByTk) {
|
||||
obj['filter'] = filter;
|
||||
}
|
||||
|
@ -1100,8 +1100,7 @@ export const useAssociationFilterBlockProps = () => {
|
||||
labelKey,
|
||||
};
|
||||
};
|
||||
|
||||
function getAssociationPath(str) {
|
||||
export function getAssociationPath(str) {
|
||||
const lastIndex = str.lastIndexOf('.');
|
||||
if (lastIndex !== -1) {
|
||||
return str.substring(0, lastIndex);
|
||||
|
@ -9,10 +9,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useRequest } from '../../api-client';
|
||||
import { RecordProvider, useRecord } from '../../record-provider';
|
||||
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
|
||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
||||
import { useCancelAction } from '../action-hooks';
|
||||
import { useCollectionManager } from '../hooks';
|
||||
import useDialect from '../hooks/useDialect';
|
||||
import { IField } from '../interfaces/types';
|
||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
||||
import * as components from './components';
|
||||
import { getOptions } from './interfaces';
|
||||
|
||||
@ -176,6 +177,8 @@ export const AddFieldAction = (props) => {
|
||||
const [schema, setSchema] = useState({});
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
const { isDialect } = useDialect();
|
||||
|
||||
const currentCollections = useMemo(() => {
|
||||
return collections.map((v) => {
|
||||
return {
|
||||
@ -298,6 +301,8 @@ export const AddFieldAction = (props) => {
|
||||
showReverseFieldConfig: true,
|
||||
targetScope,
|
||||
collections: currentCollections,
|
||||
isDialect,
|
||||
disabledJSONB: false,
|
||||
...scope,
|
||||
}}
|
||||
/>
|
||||
|
@ -8,10 +8,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useAPIClient, useRequest } from '../../api-client';
|
||||
import { RecordProvider, useRecord } from '../../record-provider';
|
||||
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
|
||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
||||
import { useCancelAction, useUpdateAction } from '../action-hooks';
|
||||
import { useCollectionManager } from '../hooks';
|
||||
import useDialect from '../hooks/useDialect';
|
||||
import { IField } from '../interfaces/types';
|
||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
||||
import * as components from './components';
|
||||
|
||||
const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => {
|
||||
@ -144,6 +145,8 @@ export const EditFieldAction = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const compile = useCompile();
|
||||
const [data, setData] = useState<any>({});
|
||||
const { isDialect } = useDialect();
|
||||
|
||||
const currentCollections = useMemo(() => {
|
||||
return collections.map((v) => {
|
||||
return {
|
||||
@ -194,6 +197,8 @@ export const EditFieldAction = (props) => {
|
||||
useCancelAction,
|
||||
showReverseFieldConfig: !data?.reverseField,
|
||||
collections: currentCollections,
|
||||
isDialect,
|
||||
disabledJSONB: true,
|
||||
...scope,
|
||||
}}
|
||||
/>
|
||||
|
@ -0,0 +1,16 @@
|
||||
import { useCurrentAppInfo } from '../../appInfo';
|
||||
|
||||
const useDialect = () => {
|
||||
const {
|
||||
data: { database },
|
||||
} = useCurrentAppInfo();
|
||||
|
||||
const isDialect = (dialect: string) => database?.dialect === dialect;
|
||||
|
||||
return {
|
||||
isDialect,
|
||||
dialect: database?.dialect,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDialect;
|
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from 'testUtils';
|
||||
import { CurrentAppInfoContext } from '../../../appInfo';
|
||||
import { Checkbox } from '../../../schema-component/antd/checkbox';
|
||||
import { Input } from '../../../schema-component/antd/input';
|
||||
import { SchemaComponent } from '../../../schema-component/core/SchemaComponent';
|
||||
import { SchemaComponentProvider } from '../../../schema-component/core/SchemaComponentProvider';
|
||||
import { json } from '../json';
|
||||
|
||||
const Component = () => {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Input, Checkbox }}>
|
||||
<SchemaComponent schema={json} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: 需要先修复测试中的路径问题:即某些引用路径返回的模块是 undefined
|
||||
describe('JSON', () => {
|
||||
it('should show JSONB when dialect is postgres', async () => {
|
||||
render(<Component />, {
|
||||
wrapper: ({ children }) => (
|
||||
<CurrentAppInfoContext.Provider
|
||||
value={{
|
||||
data: {
|
||||
database: {
|
||||
dialect: 'postgres',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CurrentAppInfoContext.Provider>
|
||||
),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('JSONB')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,26 @@
|
||||
import { defaultProps, operators } from './properties';
|
||||
import { IField } from './types';
|
||||
|
||||
export const color: IField = {
|
||||
name: 'color',
|
||||
type: 'object',
|
||||
group: 'basic',
|
||||
order: 10,
|
||||
title: '{{t("Color")}}',
|
||||
default: {
|
||||
type: 'string',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'ColorPicker',
|
||||
default: '#1677FF',
|
||||
},
|
||||
},
|
||||
availableTypes: ['string'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
},
|
||||
filterable: {
|
||||
operators: operators.string,
|
||||
},
|
||||
};
|
@ -2,6 +2,7 @@ export * from './checkbox';
|
||||
export * from './checkboxGroup';
|
||||
export * from './chinaRegion';
|
||||
export * from './collection';
|
||||
export * from './color';
|
||||
export * from './createdAt';
|
||||
export * from './createdBy';
|
||||
export * from './datetime';
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { FormItem, FormLayout } from '@formily/antd-v5';
|
||||
import { registerValidateRules } from '@formily/core';
|
||||
import React from 'react';
|
||||
import { defaultProps } from './properties';
|
||||
import { IField } from './types';
|
||||
|
||||
@ -43,6 +45,19 @@ export const json: IField = {
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
jsonb: {
|
||||
type: 'boolean',
|
||||
title: 'JSONB',
|
||||
// 不直接用 `FormItem` 的原因是为了想要设置 `FormLayout` 的 `layout` 属性为 `horizontal` (默认就是 horizontal)
|
||||
'x-decorator': ({ children }) => (
|
||||
<FormLayout>
|
||||
<FormItem>{children}</FormItem>
|
||||
</FormLayout>
|
||||
),
|
||||
'x-component': 'Checkbox',
|
||||
'x-hidden': `{{ !isDialect('postgres') }}`,
|
||||
'x-disabled': `{{ disabledJSONB }}`,
|
||||
},
|
||||
},
|
||||
filterable: {},
|
||||
};
|
@ -710,5 +710,10 @@ export default {
|
||||
"Allow add new, update and delete actions":"Allow add new, update and delete actions",
|
||||
"Date display format":"Date display format",
|
||||
"Assign data scope for the template":"Assign data scope for the template",
|
||||
"Table selected records":"Table selected records"
|
||||
"Table selected records":"Table selected records",
|
||||
"Tag":"Tag",
|
||||
"Tag color field":"Tag color field",
|
||||
"Sync successfully":"Sync successfully",
|
||||
"Sync from form fields":"Sync from form fields",
|
||||
"Select all":"Select all"
|
||||
};
|
||||
|
@ -621,4 +621,9 @@ export default {
|
||||
"Allow add new, update and delete actions":"削除変更操作の許可",
|
||||
"Date display format":"日付表示形式",
|
||||
"Assign data scope for the template":"テンプレートのデータ範囲の指定",
|
||||
"Tag":"タブ",
|
||||
"Tag color field":"ラベルの色フィールド",
|
||||
"Sync successfully":"同期成功",
|
||||
"Sync from form fields":"フォームフィールドの同期",
|
||||
"Select all":"すべて選択"
|
||||
}
|
||||
|
@ -795,5 +795,10 @@ export default {
|
||||
"Allow add new, update and delete actions":"允许增删改操作",
|
||||
"Date display format":"日期显示格式",
|
||||
"Assign data scope for the template":"为模板指定数据范围",
|
||||
"Table selected records":"表格中选中的记录"
|
||||
"Table selected records":"表格中选中的记录",
|
||||
"Tag":"标签",
|
||||
"Tag color field":"标签颜色字段",
|
||||
"Sync successfully":"同步成功",
|
||||
"Sync from form fields":"同步表单字段",
|
||||
"Select all":"全选"
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { connect, ISchema, mapProps, useField, useFieldSchema } from '@formily/react';
|
||||
import { connect, ISchema, mapProps, useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { isValid, uid } from '@formily/shared';
|
||||
import { Tree as AntdTree } from 'antd';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDesignable } from '../..';
|
||||
import { useCollection, useCollectionManager } from '../../../collection-manager';
|
||||
@ -12,15 +12,21 @@ import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks
|
||||
import { useLinkageAction } from './hooks';
|
||||
import { requestSettingsSchema } from './utils';
|
||||
import { useRecord } from '../../../record-provider';
|
||||
import { useSyncFromForm } from '../../../schema-settings/DataTemplates/utils';
|
||||
|
||||
const Tree = connect(
|
||||
AntdTree,
|
||||
mapProps((props, field: any) => {
|
||||
const [checkedKeys, setCheckedKeys] = useState(props.defaultCheckedKeys || []);
|
||||
const onCheck = (checkedKeys) => {
|
||||
setCheckedKeys(checkedKeys);
|
||||
field.value = checkedKeys;
|
||||
};
|
||||
field.onCheck = onCheck;
|
||||
return {
|
||||
...props,
|
||||
onCheck: (checkedKeys) => {
|
||||
field.value = checkedKeys;
|
||||
},
|
||||
checkedKeys,
|
||||
onCheck,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@ -218,6 +224,28 @@ function SaveMode() {
|
||||
);
|
||||
}
|
||||
|
||||
const findFormBlock = (schema) => {
|
||||
const formSchema = schema.reduceProperties((_, s) => {
|
||||
if (s['x-decorator'] === 'FormBlockProvider') {
|
||||
return s;
|
||||
} else {
|
||||
return findFormBlock(s);
|
||||
}
|
||||
}, null);
|
||||
return formSchema;
|
||||
};
|
||||
|
||||
const getAllkeys = (data, result) => {
|
||||
for (let i = 0; i < data?.length; i++) {
|
||||
const { children, ...rest } = data[i];
|
||||
result.push(rest.key);
|
||||
if (children) {
|
||||
getAllkeys(children, result);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
function DuplicationMode() {
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
@ -227,7 +255,27 @@ function DuplicationMode() {
|
||||
const { collectionList, getEnableFieldTree, getOnLoadData, getOnCheck } = useCollectionState(name);
|
||||
const duplicateValues = cloneDeep(fieldSchema['x-component-props'].duplicateFields || []);
|
||||
const record = useRecord();
|
||||
|
||||
const syncCallBack = useCallback((treeData, selectFields, form) => {
|
||||
form.query('duplicateFields').take((f) => {
|
||||
f.componentProps.treeData = treeData;
|
||||
f.componentProps.defaultCheckedKeys = selectFields;
|
||||
f.setInitialValue(selectFields);
|
||||
f?.onCheck(selectFields);
|
||||
form.setValues({ ...form.values, treeData });
|
||||
});
|
||||
}, []);
|
||||
const useSelectAllFields = (form) => {
|
||||
return {
|
||||
async run() {
|
||||
form.query('duplicateFields').take((f) => {
|
||||
const selectFields = getAllkeys(f.componentProps.treeData, []);
|
||||
f.componentProps.defaultCheckedKeys = selectFields;
|
||||
f.setInitialValue(selectFields);
|
||||
f?.onCheck(selectFields);
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
return (
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Duplicate mode')}
|
||||
@ -238,6 +286,7 @@ function DuplicationMode() {
|
||||
currentCollection: record?.__collection || name,
|
||||
getOnLoadData,
|
||||
getOnCheck,
|
||||
treeData: fieldSchema['x-component-props']?.treeData,
|
||||
}}
|
||||
schema={
|
||||
{
|
||||
@ -278,11 +327,60 @@ function DuplicationMode() {
|
||||
},
|
||||
],
|
||||
},
|
||||
syncFromForm: {
|
||||
type: 'void',
|
||||
title: '{{ t("Sync from form fields") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
style: { float: 'right', position: 'relative', zIndex: 1200 },
|
||||
useAction: () => {
|
||||
const formSchema = useMemo(() => findFormBlock(fieldSchema), [fieldSchema]);
|
||||
return useSyncFromForm(
|
||||
formSchema,
|
||||
fieldSchema['x-component-props']?.duplicateCollection || record?.__collection || name,
|
||||
syncCallBack,
|
||||
);
|
||||
},
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.duplicateMode'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: `{{ $deps[0]!=="quickDulicate" }}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
selectAll: {
|
||||
type: 'void',
|
||||
title: '{{ t("Select all") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.duplicateMode'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: `{{ $deps[0]==="quickDulicate" }}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
style: { float: 'right', position: 'relative', zIndex: 1200 },
|
||||
useAction: () => {
|
||||
const from = useForm();
|
||||
return useSelectAllFields(from);
|
||||
},
|
||||
},
|
||||
},
|
||||
duplicateFields: {
|
||||
type: 'array',
|
||||
title: '{{ t("Data fields") }}',
|
||||
required: true,
|
||||
default: duplicateValues,
|
||||
description: t('Only the selected fields will be used as the initialization data for the form'),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': Tree,
|
||||
@ -310,7 +408,7 @@ function DuplicationMode() {
|
||||
state: {
|
||||
disabled: '{{ !$deps[0] }}',
|
||||
componentProps: {
|
||||
treeData: '{{ getEnableFieldTree($deps[0], $self) }}',
|
||||
treeData: '{{ getEnableFieldTree($deps[0], $self,treeData) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -320,7 +418,7 @@ function DuplicationMode() {
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ duplicateMode, collection, duplicateFields }) => {
|
||||
onSubmit={({ duplicateMode, collection, duplicateFields, treeData }) => {
|
||||
const fields = Array.isArray(duplicateFields) ? duplicateFields : duplicateFields.checked || [];
|
||||
field.componentProps.duplicateMode = duplicateMode;
|
||||
field.componentProps.duplicateFields = fields;
|
||||
@ -328,6 +426,7 @@ function DuplicationMode() {
|
||||
fieldSchema['x-component-props'].duplicateMode = duplicateMode;
|
||||
fieldSchema['x-component-props'].duplicateFields = fields;
|
||||
fieldSchema['x-component-props'].duplicateCollection = collection;
|
||||
fieldSchema['x-component-props'].treeData = treeData;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
@ -580,7 +679,7 @@ export const ActionDesigner = (props) => {
|
||||
const { name } = useCollection();
|
||||
const { getChildrenCollections } = useCollectionManager();
|
||||
const isAction = useLinkageAction();
|
||||
const isPopupAction = ['create', 'update', 'view', 'customize:popup', 'duplicate','customize:create'].includes(
|
||||
const isPopupAction = ['create', 'update', 'view', 'customize:popup', 'duplicate', 'customize:create'].includes(
|
||||
fieldSchema['x-action'] || '',
|
||||
);
|
||||
const isUpdateModePopupAction = ['customize:bulkUpdate', 'customize:bulkEdit'].includes(fieldSchema['x-action']);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { lodash } from '@nocobase/utils';
|
||||
import { lodash } from '@nocobase/utils/client';
|
||||
import { App, Button, Popover } from 'antd';
|
||||
import classnames from 'classnames';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen, sleep, userEvent } from 'testUtils';
|
||||
import { fireEvent, render, screen, userEvent, waitFor } from 'testUtils';
|
||||
import App1 from '../demos/demo1';
|
||||
import App2 from '../demos/demo2';
|
||||
import App3 from '../demos/demo3';
|
||||
@ -10,9 +10,10 @@ describe('Action', () => {
|
||||
const { getByText } = render(<App1 />);
|
||||
|
||||
await userEvent.click(getByText('Open'));
|
||||
await sleep(300);
|
||||
// drawer
|
||||
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
// drawer
|
||||
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||
});
|
||||
// mask
|
||||
expect(document.querySelector('.ant-drawer-mask')).toBeInTheDocument();
|
||||
// title
|
||||
@ -22,22 +23,29 @@ describe('Action', () => {
|
||||
|
||||
// close button
|
||||
await userEvent.click(getByText('Close'));
|
||||
await sleep(300);
|
||||
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
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 waitFor(() => {
|
||||
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||
});
|
||||
await userEvent.click(document.querySelector('.ant-drawer-mask') as HTMLElement);
|
||||
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
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 waitFor(() => {
|
||||
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||
});
|
||||
await userEvent.click(document.querySelector('.ant-drawer-close') as HTMLElement);
|
||||
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('openMode', async () => {
|
||||
@ -50,31 +58,36 @@ describe('Action', () => {
|
||||
// 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 waitFor(() => {
|
||||
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 waitFor(() => {
|
||||
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 waitFor(() => {
|
||||
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: 点击关闭按钮时应该消失
|
||||
@ -87,38 +100,45 @@ describe('Action.Drawer without Action', () => {
|
||||
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();
|
||||
await waitFor(() => {
|
||||
// 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();
|
||||
await waitFor(() => {
|
||||
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 waitFor(() => {
|
||||
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();
|
||||
await waitFor(() => {
|
||||
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 waitFor(() => {
|
||||
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();
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -129,16 +149,16 @@ describe('Action.Popover', () => {
|
||||
|
||||
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();
|
||||
await waitFor(() => {
|
||||
// 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();
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.ant-popover')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,123 @@
|
||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import { toArr } from '@formily/shared';
|
||||
import flat from 'flat';
|
||||
import React, { Fragment, useRef, useState } from 'react';
|
||||
import { useDesignable } from '../../';
|
||||
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
|
||||
import { CollectionProvider } from '../../../collection-manager';
|
||||
import { RecordProvider, useRecord } from '../../../record-provider';
|
||||
import { FormProvider } from '../../core';
|
||||
import { useCompile } from '../../hooks';
|
||||
import { ActionContextProvider, useActionContext } from '../action';
|
||||
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
|
||||
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
||||
import schema from './schema';
|
||||
import { getTabFormatValue, useLabelUiSchema } from './util';
|
||||
|
||||
interface IEllipsisWithTooltipRef {
|
||||
setPopoverVisible: (boolean) => void;
|
||||
}
|
||||
|
||||
const toValue = (value, placeholder) => {
|
||||
if (value === null || value === undefined) {
|
||||
return placeholder;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
export const ReadPrettyInternalTag: React.FC = observer(
|
||||
(props: any) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const recordCtx = useRecord();
|
||||
const { enableLink, tagColorField } = fieldSchema['x-component-props'];
|
||||
// value 做了转换,但 props.value 和原来 useField().value 的值不一致
|
||||
const field = useField();
|
||||
const fieldNames = useFieldNames(props);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const insertViewer = useInsertSchema('Viewer');
|
||||
const { options: collectionField } = useAssociationFieldContext();
|
||||
const [record, setRecord] = useState({});
|
||||
const compile = useCompile();
|
||||
const { designable } = useDesignable();
|
||||
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
|
||||
const { snapshot } = useActionContext();
|
||||
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
|
||||
const tagColor = flat(recordCtx)[`${fieldSchema.name}.${tagColorField}`];
|
||||
const renderRecords = () =>
|
||||
toArr(props.value).map((record, index, arr) => {
|
||||
const val = toValue(compile(record?.[fieldNames?.label || 'label']), 'N/A');
|
||||
const text = getTabFormatValue(compile(labelUiSchema), val, tagColor);
|
||||
return (
|
||||
<Fragment key={`${record.id}_${index}`}>
|
||||
<span>
|
||||
{snapshot ? (
|
||||
text
|
||||
) : enableLink !== false ? (
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (designable) {
|
||||
insertViewer(schema.Viewer);
|
||||
}
|
||||
setVisible(true);
|
||||
setRecord(record);
|
||||
ellipsisWithTooltipRef?.current?.setPopoverVisible(false);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</span>
|
||||
{index < arr.length - 1 ? <span style={{ marginRight: 4, color: '#aaa' }}>,</span> : null}
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
const renderWithoutTableFieldResourceProvider = () => (
|
||||
<WithoutTableFieldResource.Provider value={true}>
|
||||
<FormProvider>
|
||||
<RecursionField
|
||||
schema={fieldSchema}
|
||||
onlyRenderProperties
|
||||
basePath={field.address}
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] === 'AssociationField.Viewer';
|
||||
}}
|
||||
/>
|
||||
</FormProvider>
|
||||
</WithoutTableFieldResource.Provider>
|
||||
);
|
||||
|
||||
const renderRecordProvider = () => {
|
||||
const collectionFieldNames = fieldSchema?.['x-collection-field']?.split('.');
|
||||
|
||||
return collectionFieldNames && collectionFieldNames.length > 2 ? (
|
||||
<RecordProvider record={recordCtx[collectionFieldNames[1]]}>
|
||||
<RecordProvider record={record}>{renderWithoutTableFieldResourceProvider()}</RecordProvider>
|
||||
</RecordProvider>
|
||||
) : (
|
||||
<RecordProvider record={record}>{renderWithoutTableFieldResourceProvider()}</RecordProvider>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BlockAssociationContext.Provider value={`${collectionField?.collectionName}.${collectionField?.name}`}>
|
||||
<CollectionProvider name={collectionField?.target ?? collectionField?.targetCollection}>
|
||||
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
|
||||
{renderRecords()}
|
||||
</EllipsisWithTooltip>
|
||||
<ActionContextProvider
|
||||
value={{ visible, setVisible, openMode: 'drawer', snapshot: collectionField?.interface === 'snapshot' }}
|
||||
>
|
||||
{renderRecordProvider()}
|
||||
</ActionContextProvider>
|
||||
</CollectionProvider>
|
||||
</BlockAssociationContext.Provider>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ displayName: 'ReadPrettyInternalTag' },
|
||||
);
|
@ -1,11 +1,12 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useField, observer } from '@formily/react';
|
||||
import { observer } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { AssociationFieldProvider } from './AssociationFieldProvider';
|
||||
import { InternalNester } from './InternalNester';
|
||||
import { ReadPrettyInternalViewer } from './InternalViewer';
|
||||
import { InternalSubTable } from './InternalSubTable';
|
||||
import { FileManageReadPretty } from './FileManager';
|
||||
import { useAssociationFieldContext } from './hooks';
|
||||
import { InternalNester } from './InternalNester';
|
||||
import { InternalSubTable } from './InternalSubTable';
|
||||
import { ReadPrettyInternalTag } from './InternalTag';
|
||||
import { ReadPrettyInternalViewer } from './InternalViewer';
|
||||
|
||||
const ReadPrettyAssociationField = observer(
|
||||
(props: any) => {
|
||||
@ -14,6 +15,7 @@ const ReadPrettyAssociationField = observer(
|
||||
return (
|
||||
<>
|
||||
{['Select', 'Picker'].includes(currentMode) && <ReadPrettyInternalViewer {...props} />}
|
||||
{currentMode === 'Tag' && <ReadPrettyInternalTag {...props} />}
|
||||
{currentMode === 'Nester' && <InternalNester {...props} />}
|
||||
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
|
||||
{currentMode === 'FileManager' && <FileManageReadPretty {...props} />}
|
||||
|
@ -48,6 +48,28 @@ export const getLabelFormatValue = (labelUiSchema: ISchema, value: any, isTag =
|
||||
}
|
||||
};
|
||||
|
||||
export const getTabFormatValue = (labelUiSchema: ISchema, value: any, tagColor): any => {
|
||||
const options = labelUiSchema?.enum;
|
||||
if (Array.isArray(options) && value) {
|
||||
const values = toArr(value).map((val) => {
|
||||
const opt: any = options.find((option: any) => option.value === val);
|
||||
return React.createElement(Tag, { color: tagColor||opt?.color }, opt?.label);
|
||||
});
|
||||
return values;
|
||||
}
|
||||
switch (labelUiSchema?.['x-component']) {
|
||||
case 'DatePicker':
|
||||
return React.createElement(
|
||||
Tag,
|
||||
{ color: tagColor },
|
||||
getDatePickerLabels({ ...labelUiSchema?.['x-component-props'], value }),
|
||||
);
|
||||
|
||||
default:
|
||||
return React.createElement(Tag, { color: tagColor }, value);
|
||||
}
|
||||
};
|
||||
|
||||
export function flatData(data) {
|
||||
const newArr = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen, sleep, userEvent } from 'testUtils';
|
||||
import { fireEvent, render, screen, userEvent, waitFor } from 'testUtils';
|
||||
import App1 from '../demos/demo1';
|
||||
import App2 from '../demos/demo2';
|
||||
|
||||
@ -33,8 +33,9 @@ describe('Cascader', () => {
|
||||
|
||||
// 因为是异步加载,所以需要等待一下
|
||||
expect(screen.queryByText('Zhejiang Dynamic 1')).not.toBeInTheDocument();
|
||||
await sleep(300);
|
||||
expect(screen.getByText('Zhejiang Dynamic 1')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Zhejiang Dynamic 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Zhejiang Dynamic 1'));
|
||||
|
||||
|
@ -0,0 +1,68 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__';
|
||||
import { connect, mapProps, mapReadPretty } from '@formily/react';
|
||||
import { ColorPicker as AntdColorPicker } from 'antd';
|
||||
import cls from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
export const ColorPicker = connect(
|
||||
(props) => {
|
||||
const { value, onChange, ...others } = props;
|
||||
return (
|
||||
<AntdColorPicker
|
||||
value={value}
|
||||
trigger="hover"
|
||||
{...others}
|
||||
destroyTooltipOnHide
|
||||
getPopupContainer={(current) => current}
|
||||
presets={[
|
||||
{
|
||||
label: 'Recommended',
|
||||
colors: [
|
||||
'#8BBB11',
|
||||
'#52C41A',
|
||||
'#13A8A8',
|
||||
'#1677FF',
|
||||
'#F5222D',
|
||||
'#FADB14',
|
||||
'#FA8C164D',
|
||||
'#FADB144D',
|
||||
'#52C41A4D',
|
||||
'#1677FF4D',
|
||||
'#2F54EB4D',
|
||||
'#722ED14D',
|
||||
'#EB2F964D',
|
||||
],
|
||||
},
|
||||
]}
|
||||
onChange={(color) => onChange(color.toHexString())}
|
||||
/>
|
||||
);
|
||||
},
|
||||
mapProps((props, field) => {
|
||||
return {
|
||||
...props,
|
||||
};
|
||||
}),
|
||||
mapReadPretty((props) => {
|
||||
const prefixCls = usePrefixCls('description-color-picker', props);
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
prefixCls,
|
||||
css`
|
||||
display: inline-flex;
|
||||
.ant-color-picker-trigger-disabled {
|
||||
cursor: default;
|
||||
}
|
||||
`,
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<AntdColorPicker disabled value={props.value} size="small" {...props} />
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
export default ColorPicker;
|
@ -0,0 +1,24 @@
|
||||
import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__';
|
||||
import type { ColorPickerProps as AntdColorPickerProps } from 'antd/es/color-picker';
|
||||
import cls from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
type Composed = {
|
||||
ColorPicker: React.FC<AntdColorPickerProps>;
|
||||
};
|
||||
|
||||
export const ReadPretty: Composed = () => null;
|
||||
|
||||
ReadPretty.ColorPicker = function ColorPicker(props: any) {
|
||||
const prefixCls = usePrefixCls('description-color-picker', props);
|
||||
|
||||
if (!props.value) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cls(prefixCls, props.className)}>
|
||||
<ColorPicker showText disabled value={props.value} size='small'/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* title: ColorPicker
|
||||
*/
|
||||
import { FormItem } from '@formily/antd-v5';
|
||||
import { ColorPicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
input: {
|
||||
type: 'boolean',
|
||||
title: `Editable`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ColorPicker',
|
||||
'x-reactions': {
|
||||
target: '*(read1,read2)',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
read1: {
|
||||
type: 'boolean',
|
||||
title: `Read pretty`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ColorPicker',
|
||||
},
|
||||
read2: {
|
||||
type: 'string',
|
||||
title: `Value`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Input, ColorPicker, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
---
|
||||
group:
|
||||
title: Schema Components
|
||||
order: 3
|
||||
---
|
||||
|
||||
# ColorPicker
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
<code src="./demos/demo1.tsx"></code>
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1 @@
|
||||
export * from './ColorPicker';
|
@ -0,0 +1,158 @@
|
||||
import { dayjs, getDefaultFormat, str2moment, toGmt, toLocal } from '@nocobase/utils/client';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
const toStringByPicker = (value, picker, timezone: 'gmt' | 'local') => {
|
||||
if (!dayjs.isDayjs(value)) return value;
|
||||
if (timezone === 'local') {
|
||||
const offset = new Date().getTimezoneOffset();
|
||||
return dayjs(toStringByPicker(value, picker, 'gmt')).add(offset, 'minutes').toISOString();
|
||||
}
|
||||
|
||||
if (picker === 'year') {
|
||||
return value.format('YYYY') + '-01-01T00:00:00.000Z';
|
||||
}
|
||||
if (picker === 'month') {
|
||||
return value.format('YYYY-MM') + '-01T00:00:00.000Z';
|
||||
}
|
||||
if (picker === 'quarter') {
|
||||
return value.startOf('quarter').format('YYYY-MM') + '-01T00:00:00.000Z';
|
||||
}
|
||||
if (picker === 'week') {
|
||||
return value.startOf('week').add(1, 'day').format('YYYY-MM-DD') + 'T00:00:00.000Z';
|
||||
}
|
||||
return value.format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z';
|
||||
};
|
||||
|
||||
const toGmtByPicker = (value: Dayjs, picker?: any) => {
|
||||
if (!value || !dayjs.isDayjs(value)) {
|
||||
return value;
|
||||
}
|
||||
return toStringByPicker(value, picker, 'gmt');
|
||||
};
|
||||
|
||||
const toLocalByPicker = (value: Dayjs, picker?: any) => {
|
||||
if (!value || !dayjs.isDayjs(value)) {
|
||||
return value;
|
||||
}
|
||||
return toStringByPicker(value, picker, 'local');
|
||||
};
|
||||
|
||||
export interface Moment2strOptions {
|
||||
showTime?: boolean;
|
||||
gmt?: boolean;
|
||||
utc?: boolean;
|
||||
picker?: 'year' | 'month' | 'week' | 'quarter';
|
||||
}
|
||||
|
||||
export const moment2str = (value?: Dayjs | null, options: Moment2strOptions = {}) => {
|
||||
const { showTime, gmt, picker, utc = true } = options;
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
if (!utc) {
|
||||
const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
|
||||
return value.format(format);
|
||||
}
|
||||
if (showTime) {
|
||||
return gmt ? toGmt(value) : toLocal(value);
|
||||
}
|
||||
if (typeof gmt === 'boolean') {
|
||||
return gmt ? toGmtByPicker(value, picker) : toLocalByPicker(value, picker);
|
||||
}
|
||||
return toGmtByPicker(value, picker);
|
||||
};
|
||||
|
||||
export const mapDatePicker = function () {
|
||||
return (props: any) => {
|
||||
const format = getDefaultFormat(props) as any;
|
||||
const onChange = props.onChange;
|
||||
|
||||
return {
|
||||
...props,
|
||||
format: format,
|
||||
value: str2moment(props.value, props),
|
||||
onChange: (value: Dayjs | null) => {
|
||||
if (onChange) {
|
||||
if (!props.showTime && value) {
|
||||
value = value.startOf('day');
|
||||
}
|
||||
onChange(moment2str(value, props));
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const mapRangePicker = function () {
|
||||
return (props: any) => {
|
||||
const format = getDefaultFormat(props) as any;
|
||||
const onChange = props.onChange;
|
||||
|
||||
return {
|
||||
...props,
|
||||
format: format,
|
||||
value: str2moment(props.value, props),
|
||||
onChange: (value: Dayjs[]) => {
|
||||
if (onChange) {
|
||||
onChange(
|
||||
value
|
||||
? [moment2str(getRangeStart(value[0], props), props), moment2str(getRangeEnd(value[1], props), props)]
|
||||
: [],
|
||||
);
|
||||
}
|
||||
},
|
||||
} as any;
|
||||
};
|
||||
};
|
||||
|
||||
function getRangeStart(value: Dayjs, options: Moment2strOptions) {
|
||||
const { showTime } = options;
|
||||
if (showTime) {
|
||||
return value;
|
||||
}
|
||||
return value.startOf('day');
|
||||
}
|
||||
|
||||
function getRangeEnd(value: Dayjs, options: Moment2strOptions) {
|
||||
const { showTime } = options;
|
||||
if (showTime) {
|
||||
return value;
|
||||
}
|
||||
return value.endOf('day');
|
||||
}
|
||||
|
||||
const getStart = (offset: any, unit: any) => {
|
||||
return dayjs()
|
||||
.add(offset, unit === 'isoWeek' ? 'week' : unit)
|
||||
.startOf(unit);
|
||||
};
|
||||
|
||||
const getEnd = (offset: any, unit: any) => {
|
||||
return dayjs()
|
||||
.add(offset, unit === 'isoWeek' ? 'week' : unit)
|
||||
.endOf(unit);
|
||||
};
|
||||
|
||||
export const getDateRanges = () => {
|
||||
return {
|
||||
today: () => [getStart(0, 'day'), getEnd(0, 'day')],
|
||||
lastWeek: () => [getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')],
|
||||
thisWeek: () => [getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')],
|
||||
nextWeek: () => [getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')],
|
||||
lastMonth: () => [getStart(-1, 'month'), getEnd(-1, 'month')],
|
||||
thisMonth: () => [getStart(0, 'month'), getEnd(0, 'month')],
|
||||
nextMonth: () => [getStart(1, 'month'), getEnd(1, 'month')],
|
||||
lastQuarter: () => [getStart(-1, 'quarter'), getEnd(-1, 'quarter')],
|
||||
thisQuarter: () => [getStart(0, 'quarter'), getEnd(0, 'quarter')],
|
||||
nextQuarter: () => [getStart(1, 'quarter'), getEnd(1, 'quarter')],
|
||||
lastYear: () => [getStart(-1, 'year'), getEnd(-1, 'year')],
|
||||
thisYear: () => [getStart(0, 'year'), getEnd(0, 'year')],
|
||||
nextYear: () => [getStart(1, 'year'), getEnd(1, 'year')],
|
||||
last7Days: () => [getStart(-6, 'days'), getEnd(0, 'days')],
|
||||
next7Days: () => [getStart(1, 'day'), getEnd(7, 'days')],
|
||||
last30Days: () => [getStart(-29, 'days'), getEnd(0, 'days')],
|
||||
next30Days: () => [getStart(1, 'day'), getEnd(30, 'days')],
|
||||
last90Days: () => [getStart(-89, 'days'), getEnd(0, 'days')],
|
||||
next90Days: () => [getStart(1, 'day'), getEnd(90, 'days')],
|
||||
};
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { render, screen, userEvent, waitFor } from 'testUtils';
|
||||
import { render, screen, sleep, userEvent, waitFor } from 'testUtils';
|
||||
import App1 from '../demos/demo1';
|
||||
import App2 from '../demos/demo2';
|
||||
import App3 from '../demos/demo3';
|
||||
@ -13,6 +13,9 @@ import App9 from '../demos/demo9';
|
||||
describe('DatePicker', () => {
|
||||
it('basic', async () => {
|
||||
const { container, getByText } = render(<App1 />);
|
||||
|
||||
await sleep();
|
||||
|
||||
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||
const input = container.querySelector('input') as HTMLElement;
|
||||
|
||||
@ -35,6 +38,9 @@ describe('DatePicker', () => {
|
||||
|
||||
it('GMT', async () => {
|
||||
const { container, getByText } = render(<App2 />);
|
||||
|
||||
await sleep();
|
||||
|
||||
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||
const input = container.querySelector('input') as HTMLElement;
|
||||
|
||||
@ -53,6 +59,9 @@ describe('DatePicker', () => {
|
||||
|
||||
it('non-UTC', async () => {
|
||||
const { container } = render(<App3 />);
|
||||
|
||||
await sleep();
|
||||
|
||||
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||
const input = container.querySelector('input') as HTMLElement;
|
||||
|
||||
@ -74,6 +83,9 @@ describe('DatePicker', () => {
|
||||
describe('RangePicker', () => {
|
||||
it('GMT', async () => {
|
||||
const { container, getByPlaceholderText } = render(<App4 />);
|
||||
|
||||
await sleep();
|
||||
|
||||
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||
const startInput = getByPlaceholderText('Start date');
|
||||
const endInput = getByPlaceholderText('End date');
|
||||
@ -92,6 +104,9 @@ describe('RangePicker', () => {
|
||||
|
||||
it('non-GMT', async () => {
|
||||
const { container, getByPlaceholderText } = render(<App5 />);
|
||||
|
||||
await sleep();
|
||||
|
||||
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||
const startInput = getByPlaceholderText('Start date');
|
||||
const endInput = getByPlaceholderText('End date');
|
||||
@ -115,6 +130,9 @@ describe('RangePicker', () => {
|
||||
|
||||
it('non-UTC', async () => {
|
||||
const { container, getByPlaceholderText } = render(<App6 />);
|
||||
|
||||
await sleep();
|
||||
|
||||
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||
const startInput = getByPlaceholderText('Start date');
|
||||
const endInput = getByPlaceholderText('End date');
|
||||
@ -133,6 +151,9 @@ describe('RangePicker', () => {
|
||||
|
||||
it('showTime=false,gmt=true,utc=true', async () => {
|
||||
const { container } = render(<App7 />);
|
||||
|
||||
await sleep();
|
||||
|
||||
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||
const input = container.querySelector('input') as HTMLElement;
|
||||
|
||||
@ -152,6 +173,9 @@ describe('RangePicker', () => {
|
||||
|
||||
it('showTime=false,gmt=false,utc=true', async () => {
|
||||
const { container } = render(<App8 />);
|
||||
|
||||
await sleep();
|
||||
|
||||
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||
const input = container.querySelector('input') as HTMLElement;
|
||||
|
||||
@ -175,6 +199,9 @@ describe('RangePicker', () => {
|
||||
it('showTime=false,gmt=true,utc=true & not input', async () => {
|
||||
const currentDateString = new Date().toISOString().split('T')[0];
|
||||
const { container } = render(<App9 />);
|
||||
|
||||
await sleep();
|
||||
|
||||
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||
|
||||
await userEvent.click(picker);
|
||||
|
@ -7,8 +7,7 @@ import App5 from '../demos/demo5';
|
||||
import App6 from '../demos/demo6';
|
||||
|
||||
describe('Filter', () => {
|
||||
// TODO: 等 @Testing-Library 升级到 14.x
|
||||
it.skip('Filter & Action', async () => {
|
||||
it('Filter & Action', async () => {
|
||||
render(<App3 />);
|
||||
|
||||
await waitFor(
|
||||
|
@ -28,6 +28,7 @@ import { BlockItem } from '../block-item';
|
||||
import { removeNullCondition } from '../filter';
|
||||
import { HTMLEncode } from '../input/shared';
|
||||
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
|
||||
import { useColorFields } from '../table-v2/Table.Column.Designer';
|
||||
import { FilterFormDesigner } from './FormItem.FilterFormDesigner';
|
||||
import { useEnsureOperatorsValid } from './SchemaSettingOptions';
|
||||
|
||||
@ -183,6 +184,7 @@ FormItem.Designer = function Designer() {
|
||||
value: field?.name,
|
||||
label: compile(field?.uiSchema?.title) || field?.name,
|
||||
}));
|
||||
const colorFieldOptions = useColorFields(collectionField?.target ?? collectionField?.targetCollection);
|
||||
|
||||
let readOnlyMode = 'editable';
|
||||
if (fieldSchema['x-disabled'] === true) {
|
||||
@ -529,10 +531,6 @@ FormItem.Designer = function Designer() {
|
||||
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||
field.componentProps = field.componentProps || {};
|
||||
field.componentProps.mode = mode;
|
||||
// if (mode === 'Nester') {
|
||||
// const initValue = ['hasMany', 'belongsToMany'].includes(collectionField?.type) ? [{}] : {};
|
||||
// field.value = field.value || initValue;
|
||||
// }
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
@ -811,6 +809,29 @@ FormItem.Designer = function Designer() {
|
||||
/>
|
||||
)}
|
||||
{isDateField && <SchemaSettings.DataFormat fieldSchema={fieldSchema} />}
|
||||
|
||||
{isAssociationField && ['Tag'].includes(fieldMode) && (
|
||||
<SchemaSettings.SelectItem
|
||||
key="title-field"
|
||||
title={t('Tag color field')}
|
||||
options={colorFieldOptions}
|
||||
value={field?.componentProps?.tagColorField}
|
||||
onChange={(tagColorField) => {
|
||||
const schema = {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
};
|
||||
|
||||
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||
fieldSchema['x-component-props']['tagColorField'] = tagColorField;
|
||||
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||
field.componentProps.tagColorField = tagColorField;
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{collectionField && <SchemaSettings.Divider />}
|
||||
<SchemaSettings.Remove
|
||||
key="remove"
|
||||
|
@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from 'testUtils';
|
||||
import { render, screen, waitFor } from 'testUtils';
|
||||
import App1 from '../demos/demo1';
|
||||
|
||||
describe('FormItem', () => {
|
||||
it('should render correctly', () => {
|
||||
it('should render correctly', async () => {
|
||||
render(<App1 />);
|
||||
|
||||
expect(screen.getByText('title')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('title')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,18 @@
|
||||
import { FormItem, FormProvider, Input, SchemaComponent } from '@nocobase/client';
|
||||
import {
|
||||
APIClientProvider,
|
||||
CurrentUserProvider,
|
||||
FormItem,
|
||||
FormProvider,
|
||||
Input,
|
||||
SchemaComponent,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { mockAPIClient } from '../../../../test';
|
||||
|
||||
const { apiClient, mockRequest } = mockAPIClient();
|
||||
mockRequest.onGet('/auth:check').reply(() => {
|
||||
return [200, { data: {} }];
|
||||
});
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
@ -15,8 +28,12 @@ const schema = {
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<FormProvider>
|
||||
<SchemaComponent components={{ FormItem, Input }} schema={schema} />
|
||||
</FormProvider>
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
<CurrentUserProvider>
|
||||
<FormProvider>
|
||||
<SchemaComponent components={{ FormItem, Input }} schema={schema} />
|
||||
</FormProvider>
|
||||
</CurrentUserProvider>
|
||||
</APIClientProvider>
|
||||
);
|
||||
};
|
||||
|
@ -163,7 +163,7 @@ export const Templates = ({ style = {}, form }) => {
|
||||
{targetTemplate !== 'none' && (
|
||||
<RemoteSelect
|
||||
style={{ width: 220 }}
|
||||
fieldNames={{ label: template.titleField, value: 'id' }}
|
||||
fieldNames={{ label: template?.titleField, value: 'id' }}
|
||||
target={template?.collection}
|
||||
value={targetTemplateData}
|
||||
objectValue
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { render, screen, sleep, userEvent, waitFor } from 'testUtils';
|
||||
import { render, screen, userEvent, waitFor } from 'testUtils';
|
||||
import App1 from '../demos/demo1';
|
||||
import App2 from '../demos/demo2';
|
||||
import App3 from '../demos/demo3';
|
||||
@ -8,19 +8,21 @@ 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();
|
||||
let input, submit;
|
||||
await waitFor(() => {
|
||||
input = document.querySelector('.ant-input') as HTMLInputElement;
|
||||
submit = screen.getByText('Submit');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(screen.queryByText('Nickname')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.type(input, '李四');
|
||||
await userEvent.click(submit);
|
||||
|
||||
await sleep(100);
|
||||
|
||||
// notification 的内容
|
||||
expect(screen.getByText(/\{"nickname":"李四"\}/i)).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
// notification 的内容
|
||||
expect(screen.getByText(/\{"nickname":"李四"\}/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('initial values', async () => {
|
||||
@ -41,8 +43,7 @@ describe('FormV2', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: 等 @Testing-Library 升级到 14.x
|
||||
it.skip('read pretty', async () => {
|
||||
it('read pretty', async () => {
|
||||
render(<App3 />);
|
||||
|
||||
await waitFor(() => {
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
Action,
|
||||
CollectionField,
|
||||
CollectionManagerProvider,
|
||||
CurrentUserProvider,
|
||||
FormBlockProvider,
|
||||
FormItem,
|
||||
FormV2,
|
||||
@ -26,6 +27,9 @@ mockRequest.onPost('/users:update').reply((params) => {
|
||||
});
|
||||
return [200, JSON.parse(params.data)];
|
||||
});
|
||||
mockRequest.onGet('/auth:check').reply(() => {
|
||||
return [200, { data: {} }];
|
||||
});
|
||||
|
||||
function useAction() {
|
||||
const ctx = useFormBlockContext();
|
||||
@ -82,11 +86,13 @@ const schema: ISchema = {
|
||||
export default () => {
|
||||
return (
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
<CollectionManagerProvider collections={collections}>
|
||||
<SchemaComponentProvider components={{ FormBlockProvider, FormV2, FormItem, CollectionField, Action, Input }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</CollectionManagerProvider>
|
||||
<CurrentUserProvider>
|
||||
<CollectionManagerProvider collections={collections}>
|
||||
<SchemaComponentProvider components={{ FormBlockProvider, FormV2, FormItem, CollectionField, Action, Input }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</CollectionManagerProvider>
|
||||
</CurrentUserProvider>
|
||||
</APIClientProvider>
|
||||
);
|
||||
};
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
BlockSchemaComponentProvider,
|
||||
CollectionField,
|
||||
CollectionManagerProvider,
|
||||
CurrentUserProvider,
|
||||
FormBlockProvider,
|
||||
FormItem,
|
||||
FormV2,
|
||||
@ -36,6 +37,9 @@ mockRequest.onPost('/users:update').reply((params) => {
|
||||
});
|
||||
return [200, JSON.parse(params.data)];
|
||||
});
|
||||
mockRequest.onGet('/auth:check').reply(() => {
|
||||
return [200, { data: {} }];
|
||||
});
|
||||
|
||||
const useAction = () => {
|
||||
const ctx = useFormBlockContext();
|
||||
@ -107,15 +111,17 @@ const schema: ISchema = {
|
||||
export default () => {
|
||||
return (
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
<CollectionManagerProvider collections={collections}>
|
||||
<SchemaComponentProvider
|
||||
components={{ FormBlockProvider, FormItem, CollectionField, Input, Action, FormV2, Password }}
|
||||
>
|
||||
<BlockSchemaComponentProvider>
|
||||
<SchemaComponent schema={schema} />
|
||||
</BlockSchemaComponentProvider>
|
||||
</SchemaComponentProvider>
|
||||
</CollectionManagerProvider>
|
||||
<CurrentUserProvider>
|
||||
<CollectionManagerProvider collections={collections}>
|
||||
<SchemaComponentProvider
|
||||
components={{ FormBlockProvider, FormItem, CollectionField, Input, Action, FormV2, Password }}
|
||||
>
|
||||
<BlockSchemaComponentProvider>
|
||||
<SchemaComponent schema={schema} />
|
||||
</BlockSchemaComponentProvider>
|
||||
</SchemaComponentProvider>
|
||||
</CollectionManagerProvider>
|
||||
</CurrentUserProvider>
|
||||
</APIClientProvider>
|
||||
);
|
||||
};
|
||||
|
@ -5,9 +5,11 @@ import {
|
||||
BlockSchemaComponentProvider,
|
||||
CollectionField,
|
||||
CollectionManagerProvider,
|
||||
CurrentUserProvider,
|
||||
FormBlockProvider,
|
||||
FormItem,
|
||||
FormV2,
|
||||
Grid,
|
||||
Input,
|
||||
Password,
|
||||
SchemaComponent,
|
||||
@ -26,6 +28,9 @@ mockRequest.onGet('/users:get').reply(200, {
|
||||
password: '123456',
|
||||
},
|
||||
});
|
||||
mockRequest.onGet('/auth:check').reply(() => {
|
||||
return [200, { data: {} }];
|
||||
});
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
@ -93,15 +98,17 @@ const schema: ISchema = {
|
||||
export default () => {
|
||||
return (
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
<CollectionManagerProvider collections={collections}>
|
||||
<SchemaComponentProvider
|
||||
components={{ FormBlockProvider, FormItem, CollectionField, Input, Action, FormV2, Password }}
|
||||
>
|
||||
<BlockSchemaComponentProvider>
|
||||
<SchemaComponent schema={schema} />
|
||||
</BlockSchemaComponentProvider>
|
||||
</SchemaComponentProvider>
|
||||
</CollectionManagerProvider>
|
||||
<CurrentUserProvider>
|
||||
<CollectionManagerProvider collections={collections}>
|
||||
<SchemaComponentProvider
|
||||
components={{ FormBlockProvider, FormItem, CollectionField, Input, Action, FormV2, Password, Grid }}
|
||||
>
|
||||
<BlockSchemaComponentProvider>
|
||||
<SchemaComponent schema={schema} />
|
||||
</BlockSchemaComponentProvider>
|
||||
</SchemaComponentProvider>
|
||||
</CollectionManagerProvider>
|
||||
</CurrentUserProvider>
|
||||
</APIClientProvider>
|
||||
);
|
||||
};
|
||||
|
@ -38,9 +38,12 @@ describe('Form', () => {
|
||||
|
||||
expect(submit).toBeInTheDocument();
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue('aaa');
|
||||
expect(screen.getByText('T1')).toBeInTheDocument();
|
||||
expect(screen.getByText(/\{ "field1": "aaa" \}/i)).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(input).toHaveValue('aaa');
|
||||
expect(screen.getByText('T1')).toBeInTheDocument();
|
||||
expect(screen.getByText(/\{ "field1": "aaa" \}/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.type(input, '123');
|
||||
expect(screen.getByText(/\{ "field1": "aaa123" \}/i)).toBeInTheDocument();
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { render, sleep } from 'testUtils';
|
||||
import { render, waitFor } from 'testUtils';
|
||||
import App1 from '../demos/demo1';
|
||||
|
||||
// jsdom does not support canvas, so we need to skip this test
|
||||
@ -7,9 +7,9 @@ describe.skip('G2Plot', () => {
|
||||
it('basic', async () => {
|
||||
render(<App1 />);
|
||||
|
||||
await sleep(100);
|
||||
|
||||
const g2plot = document.querySelector('.g2plot') as HTMLDivElement;
|
||||
expect(g2plot).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
const g2plot = document.querySelector('.g2plot') as HTMLDivElement;
|
||||
expect(g2plot).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from 'testUtils';
|
||||
import { render, screen, waitFor } from 'testUtils';
|
||||
import App1 from '../demos/demo1';
|
||||
import App2 from '../demos/demo2';
|
||||
import App3 from '../demos/demo3';
|
||||
@ -14,12 +14,13 @@ describe('Grid', () => {
|
||||
expect(screen.getByText('Block 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('input', () => {
|
||||
it('input', async () => {
|
||||
render(<App2 />);
|
||||
|
||||
const inputs = document.querySelectorAll('.ant-input');
|
||||
|
||||
expect(inputs.length).toBe(3);
|
||||
await waitFor(() => {
|
||||
const inputs = document.querySelectorAll('.ant-input');
|
||||
expect(inputs.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('initializer', () => {
|
||||
|
@ -1,8 +1,24 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Form, FormItem, Grid, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import {
|
||||
APIClientProvider,
|
||||
CurrentUserProvider,
|
||||
Form,
|
||||
FormItem,
|
||||
Grid,
|
||||
Input,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
import { mockAPIClient } from '../../../../test';
|
||||
|
||||
const { apiClient, mockRequest } = mockAPIClient();
|
||||
mockRequest.onGet('/auth:check').reply(() => {
|
||||
return [200, { data: {} }];
|
||||
});
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'void',
|
||||
name: 'grid1',
|
||||
@ -54,8 +70,12 @@ const schema: ISchema = {
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Form, Grid, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
<CurrentUserProvider>
|
||||
<SchemaComponentProvider components={{ Form, Grid, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</CurrentUserProvider>
|
||||
</APIClientProvider>
|
||||
);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ export * from './card-item';
|
||||
export * from './cascader';
|
||||
export * from './checkbox';
|
||||
export * from './collection-select';
|
||||
export * from './color-picker';
|
||||
export * from './color-select';
|
||||
export * from './cron';
|
||||
export * from './date-picker';
|
||||
@ -49,4 +50,5 @@ export * from './time-picker';
|
||||
export * from './tree-select';
|
||||
export * from './upload';
|
||||
export * from './variable';
|
||||
|
||||
import './index.less';
|
||||
|
@ -2,8 +2,7 @@ import React from 'react';
|
||||
import { render, screen, waitFor } from 'testUtils';
|
||||
import App1 from '../demos/demo1';
|
||||
|
||||
// TODO: 等 @Testing-Library 升级到 14.x
|
||||
describe.skip('Kanban', () => {
|
||||
describe('Kanban', () => {
|
||||
it('should render correctly', async () => {
|
||||
render(<App1 />);
|
||||
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
APIClientProvider,
|
||||
BlockSchemaComponentProvider,
|
||||
CollectionManagerProvider,
|
||||
CurrentUserProvider,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
} from '@nocobase/client';
|
||||
@ -19,6 +20,9 @@ mockRequest.onGet('/t_j6omof6tza8:list').reply(async (config) => {
|
||||
await sleep(200);
|
||||
return [200, data];
|
||||
});
|
||||
mockRequest.onGet('/auth:check').reply(() => {
|
||||
return [200, { data: {} }];
|
||||
});
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
@ -69,15 +73,17 @@ const schema: ISchema = {
|
||||
export default () => {
|
||||
return (
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
<SchemaComponentProvider>
|
||||
<CollectionManagerProvider collections={collections}>
|
||||
<AntdSchemaComponentProvider>
|
||||
<BlockSchemaComponentProvider>
|
||||
<SchemaComponent schema={schema} />
|
||||
</BlockSchemaComponentProvider>
|
||||
</AntdSchemaComponentProvider>
|
||||
</CollectionManagerProvider>
|
||||
</SchemaComponentProvider>
|
||||
<CurrentUserProvider>
|
||||
<SchemaComponentProvider>
|
||||
<CollectionManagerProvider collections={collections}>
|
||||
<AntdSchemaComponentProvider>
|
||||
<BlockSchemaComponentProvider>
|
||||
<SchemaComponent schema={schema} />
|
||||
</BlockSchemaComponentProvider>
|
||||
</AntdSchemaComponentProvider>
|
||||
</CollectionManagerProvider>
|
||||
</SchemaComponentProvider>
|
||||
</CurrentUserProvider>
|
||||
</APIClientProvider>
|
||||
);
|
||||
};
|
||||
|
@ -1,26 +1,32 @@
|
||||
import React from 'react';
|
||||
import { render, screen, sleep, userEvent, within } from 'testUtils';
|
||||
import { render, screen, userEvent, waitFor, within } from 'testUtils';
|
||||
import App1 from '../demos/demo1';
|
||||
|
||||
describe('RecordPicker', () => {
|
||||
it('should show selected options', async () => {
|
||||
render(<App1 />);
|
||||
|
||||
const selector = document.querySelector('.ant-select-selector') as HTMLElement;
|
||||
expect(selector).toBeInTheDocument();
|
||||
let selector;
|
||||
await waitFor(() => {
|
||||
selector = document.querySelector('.ant-select-selector') as HTMLElement;
|
||||
expect(selector).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(selector);
|
||||
await sleep(100);
|
||||
await waitFor(() => {
|
||||
// 弹窗标题
|
||||
expect(screen.queryByText(/select record/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// 弹窗标题
|
||||
expect(screen.getByText(/select record/i)).toBeInTheDocument();
|
||||
const checkboxes = document.querySelectorAll('.ant-checkbox');
|
||||
|
||||
// 第 3 个选项的内容是: “软件开发”
|
||||
await userEvent.click(checkboxes[2]);
|
||||
await userEvent.click(screen.getByText(/submit/i));
|
||||
|
||||
expect(within(selector).getByText(/软件开发/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/软件开发/i, { selector: '.test-record-picker-read-pretty-item' })).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(within(selector).queryByText(/软件开发/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/软件开发/i, { selector: '.test-record-picker-read-pretty-item' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
BlockItem,
|
||||
CollectionField,
|
||||
CollectionManagerProvider,
|
||||
CurrentUserProvider,
|
||||
FormItem,
|
||||
Input,
|
||||
RecordPicker,
|
||||
@ -23,6 +24,9 @@ import data from './mockData';
|
||||
|
||||
const { apiClient, mockRequest } = mockAPIClient();
|
||||
|
||||
mockRequest.onGet('/auth:check').reply(() => {
|
||||
return [200, { data: {} }];
|
||||
});
|
||||
mockRequest.onGet('/tt_bd_range:list').reply(({ params }) => {
|
||||
// 已选中的 id
|
||||
const ids = JSON.parse(params.filter).$and?.[0]?.['id.$ne'] || [];
|
||||
@ -172,11 +176,13 @@ export default () => {
|
||||
|
||||
return (
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
<CollectionManagerProvider collections={mainCollections}>
|
||||
<SchemaComponentProvider components={components}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</CollectionManagerProvider>
|
||||
<CurrentUserProvider>
|
||||
<CollectionManagerProvider collections={mainCollections}>
|
||||
<SchemaComponentProvider components={components}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</CollectionManagerProvider>
|
||||
</CurrentUserProvider>
|
||||
</APIClientProvider>
|
||||
);
|
||||
};
|
||||
|
@ -27,6 +27,22 @@ const useLabelFields = (collectionName?: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useColorFields = (collectionName?: any) => {
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
if (!collectionName) {
|
||||
return [];
|
||||
}
|
||||
const targetFields = getCollectionFields(collectionName);
|
||||
return targetFields
|
||||
?.filter?.((field) => field?.interface === 'color')
|
||||
?.map?.((field) => {
|
||||
return {
|
||||
value: field.name,
|
||||
label: compile(field?.uiSchema?.title || field.name),
|
||||
};
|
||||
});
|
||||
};
|
||||
export const TableColumnDesigner = (props) => {
|
||||
const { uiSchema, fieldSchema, collectionField } = props;
|
||||
const { getInterface, getCollection } = useCollectionManager();
|
||||
@ -37,6 +53,7 @@ export const TableColumnDesigner = (props) => {
|
||||
const fieldNames =
|
||||
fieldSchema?.['x-component-props']?.['fieldNames'] || uiSchema?.['x-component-props']?.['fieldNames'];
|
||||
const options = useLabelFields(collectionField?.target ?? collectionField?.targetCollection);
|
||||
const colorFieldOptions = useColorFields(collectionField?.target ?? collectionField?.targetCollection);
|
||||
const intefaceCfg = getInterface(collectionField?.interface);
|
||||
const targetCollection = getCollection(collectionField?.target);
|
||||
const isFileField = isFileCollection(targetCollection);
|
||||
@ -45,6 +62,7 @@ export const TableColumnDesigner = (props) => {
|
||||
const defaultFilter = fieldSchema?.['x-component-props']?.service?.params?.filter || {};
|
||||
const dataSource = useCollectionFilterOptions(collectionField?.target);
|
||||
const isDateField = ['datetime', 'createdAt', 'updatedAt'].includes(collectionField?.interface);
|
||||
const fieldMode = fieldSchema?.['x-component-props']?.['mode'] || 'Select';
|
||||
let readOnlyMode = 'editable';
|
||||
if (fieldSchema['x-disabled'] === true) {
|
||||
readOnlyMode = 'readonly';
|
||||
@ -232,6 +250,55 @@ export const TableColumnDesigner = (props) => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{readOnlyMode === 'read-pretty' &&
|
||||
['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho', 'snapshot'].includes(collectionField?.interface) && (
|
||||
<SchemaSettings.SelectItem
|
||||
key="field-mode"
|
||||
title={t('Field component')}
|
||||
options={[
|
||||
{ label: t('Title'), value: 'Select' },
|
||||
{ label: t('Tag'), value: 'Tag' },
|
||||
]}
|
||||
value={fieldMode}
|
||||
onChange={(mode) => {
|
||||
const schema = {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
};
|
||||
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||
fieldSchema['x-component-props']['mode'] = mode;
|
||||
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||
field.componentProps = field.componentProps || {};
|
||||
field.componentProps.mode = mode;
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{['Tag'].includes(fieldMode) && (
|
||||
<SchemaSettings.SelectItem
|
||||
key="title-field"
|
||||
title={t('Tag color field')}
|
||||
options={colorFieldOptions}
|
||||
value={fieldSchema?.['x-component-props']?.tagColorField}
|
||||
onChange={(tagColorField) => {
|
||||
const schema = {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
};
|
||||
|
||||
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||
fieldSchema['x-component-props']['tagColorField'] = tagColorField;
|
||||
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||
field.componentProps.tagColorField = tagColorField;
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isSubTableColumn && !field.readPretty && !uiSchema?.['x-read-pretty'] && (
|
||||
<SchemaSettings.SwitchItem
|
||||
|
@ -297,7 +297,6 @@ export const Table: any = observer(
|
||||
console.warn('move cancel');
|
||||
return;
|
||||
}
|
||||
|
||||
const fromIndex = e.active?.data.current?.sortable?.index;
|
||||
const toIndex = e.over?.data.current?.sortable?.index;
|
||||
const from = field.value[fromIndex];
|
||||
@ -324,6 +323,11 @@ export const Table: any = observer(
|
||||
.nb-read-pretty-input-number {
|
||||
text-align: right;
|
||||
}
|
||||
.ant-color-picker-trigger{
|
||||
position:absolute;
|
||||
top:50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
`,
|
||||
)}
|
||||
/>
|
||||
|
@ -14,18 +14,18 @@ type VariablesCtx = {
|
||||
};
|
||||
|
||||
export const useVariablesCtx = (): VariablesCtx => {
|
||||
const { data } = useCurrentUserContext() || {};
|
||||
const currentUser = useCurrentUserContext();
|
||||
const { field, service, rowKey } = useTableBlockContext();
|
||||
const contextData = service?.data?.data?.filter((v) => (field?.data?.selectedRowKeys || [])?.includes(v[rowKey]));
|
||||
return useMemo(() => {
|
||||
return {
|
||||
$user: data?.data || {},
|
||||
$user: currentUser?.data?.data || {},
|
||||
$date: {
|
||||
now: () => dayjs().toISOString(),
|
||||
},
|
||||
$context: contextData,
|
||||
};
|
||||
}, [data]);
|
||||
}, [contextData, currentUser?.data?.data]);
|
||||
};
|
||||
|
||||
export const isVariable = (str: unknown) => {
|
||||
|
@ -24,6 +24,7 @@ export const useFieldModeOptions = () => {
|
||||
? [
|
||||
{ label: t('Title'), value: 'Select' },
|
||||
{ label: t('File manager'), value: 'FileManager' },
|
||||
{ label: t('Tag'), value: 'Tag' },
|
||||
]
|
||||
: [
|
||||
{ label: t('Select'), value: 'Select' },
|
||||
@ -37,6 +38,7 @@ export const useFieldModeOptions = () => {
|
||||
return isReadPretty
|
||||
? [
|
||||
{ label: t('Title'), value: 'Select' },
|
||||
{ label: t('Tag'), value: 'Tag' },
|
||||
{ label: t('Sub-table'), value: 'SubTable' },
|
||||
{ label: t('Sub-details'), value: 'Nester' },
|
||||
]
|
||||
@ -50,6 +52,7 @@ export const useFieldModeOptions = () => {
|
||||
return isReadPretty
|
||||
? [
|
||||
{ label: t('Title'), value: 'Select' },
|
||||
{ label: t('Tag'), value: 'Tag' },
|
||||
{ label: t('Sub-details'), value: 'Nester' },
|
||||
{ label: t('Sub-table'), value: 'SubTable' },
|
||||
]
|
||||
@ -64,6 +67,7 @@ export const useFieldModeOptions = () => {
|
||||
return isReadPretty
|
||||
? [
|
||||
{ label: t('Title'), value: 'Select' },
|
||||
{ label: t('Tag'), value: 'Tag' },
|
||||
{ label: t('Sub-details'), value: 'Nester' },
|
||||
]
|
||||
: [
|
||||
@ -76,6 +80,7 @@ export const useFieldModeOptions = () => {
|
||||
return isReadPretty
|
||||
? [
|
||||
{ label: t('Title'), value: 'Select' },
|
||||
{ label: t('Tag'), value: 'Tag' },
|
||||
{ label: t('Sub-details'), value: 'Nester' },
|
||||
]
|
||||
: [
|
||||
|
@ -13,6 +13,7 @@ import { AsDefaultTemplate } from './components/AsDefaultTemplate';
|
||||
import { ArrayCollapse } from './components/DataTemplateTitle';
|
||||
import { getSelectedIdFilter } from './components/Designer';
|
||||
import { useCollectionState } from './hooks/useCollectionState';
|
||||
import { useSyncFromForm } from './utils';
|
||||
|
||||
const Tree = connect(
|
||||
AntdTree,
|
||||
@ -48,7 +49,6 @@ export const FormDataTemplates = observer(
|
||||
} = useCollectionState(collectionName);
|
||||
const { getCollection, getCollectionField } = useCollectionManager();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 不要在后面的数组中依赖 defaultValues,否则会因为 defaultValues 的变化导致 activeData 响应性丢失
|
||||
const activeData = useMemo<ITemplate>(
|
||||
() =>
|
||||
@ -61,7 +61,6 @@ export const FormDataTemplates = observer(
|
||||
),
|
||||
[],
|
||||
);
|
||||
console.log(activeData);
|
||||
const getTargetField = (collectionName: string) => {
|
||||
const collection = getCollection(collectionName);
|
||||
return getCollectionField(
|
||||
@ -170,6 +169,16 @@ export const FormDataTemplates = observer(
|
||||
required: true,
|
||||
'x-reactions': '{{useTitleFieldDataSource}}',
|
||||
},
|
||||
syncFromForm: {
|
||||
type: 'void',
|
||||
title: '{{ t("Sync from form fields") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
style: { float: 'right', position: 'relative', zIndex: 1200 },
|
||||
useAction: () => useSyncFromForm(formSchema),
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
type: 'array',
|
||||
title: '{{ t("Data fields") }}',
|
||||
|
@ -48,10 +48,9 @@ const DataTemplateTitle = observer<{ index: number; item: any }>((props) => {
|
||||
export interface IArrayCollapseProps extends CollapseProps {
|
||||
defaultOpenPanelCount?: number;
|
||||
}
|
||||
type ComposedArrayCollapse =
|
||||
| React.FC<React.PropsWithChildren<IArrayCollapseProps>> & {
|
||||
CollapsePanel?: React.FC<React.PropsWithChildren<CollapsePanelProps>>;
|
||||
};
|
||||
type ComposedArrayCollapse = React.FC<React.PropsWithChildren<IArrayCollapseProps>> & {
|
||||
CollapsePanel?: React.FC<React.PropsWithChildren<CollapsePanelProps>>;
|
||||
};
|
||||
|
||||
const isAdditionComponent = (schema: ISchema) => {
|
||||
return schema['x-component']?.indexOf?.('Addition') > -1;
|
||||
@ -218,6 +217,9 @@ export const ArrayCollapse: ComposedArrayCollapse = observer(
|
||||
onAdd={(index) => {
|
||||
setActiveKeys(insertActiveKeys(activeKeys, index));
|
||||
}}
|
||||
onRemove={() => {
|
||||
field.initialValue = field.value;
|
||||
}}
|
||||
>
|
||||
{renderEmpty()}
|
||||
{renderItems()}
|
||||
|
@ -1,32 +1,34 @@
|
||||
import { ArrayField } from '@formily/core';
|
||||
import { useField } from '@formily/react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useCollectionManager } from '../../../collection-manager';
|
||||
import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields';
|
||||
import { useCompile } from '../../../schema-component';
|
||||
import { TreeNode } from '../TreeLabel';
|
||||
|
||||
// 过滤掉系统字段
|
||||
export const systemKeys = [
|
||||
// 'id',
|
||||
'sort',
|
||||
'createdById',
|
||||
'createdBy',
|
||||
'createdAt',
|
||||
'updatedById',
|
||||
'updatedBy',
|
||||
'updatedAt',
|
||||
'password',
|
||||
'sequence',
|
||||
];
|
||||
export const useCollectionState = (currentCollectionName: string) => {
|
||||
const { getCollectionFields, getAllCollectionsInheritChain, getCollection, getInterface } = useCollectionManager();
|
||||
const [collectionList] = useState(getCollectionList);
|
||||
const compile = useCompile();
|
||||
const templateField: any = useField();
|
||||
|
||||
function getCollectionList() {
|
||||
const collections = getAllCollectionsInheritChain(currentCollectionName);
|
||||
return collections.map((name) => ({ label: getCollection(name)?.title, value: name }));
|
||||
}
|
||||
|
||||
// 过滤掉系统字段
|
||||
const systemKeys = [
|
||||
// 'id',
|
||||
'sort',
|
||||
'createdById',
|
||||
'createdBy',
|
||||
'createdAt',
|
||||
'updatedById',
|
||||
'updatedBy',
|
||||
'updatedAt',
|
||||
];
|
||||
|
||||
/**
|
||||
* maxDepth: 从 0 开始,0 表示一层,1 表示两层,以此类推
|
||||
*/
|
||||
@ -115,12 +117,25 @@ export const useCollectionState = (currentCollectionName: string) => {
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
const parseTreeData = (data) => {
|
||||
return data.map((v) => {
|
||||
return {
|
||||
...v,
|
||||
title: React.createElement(TreeNode, { ...v, type: v.type }),
|
||||
children: v.children ? parseTreeData(v.children) : null,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const getEnableFieldTree = useCallback((collectionName: string) => {
|
||||
const getEnableFieldTree = useCallback((collectionName: string, field, treeData?) => {
|
||||
const index = field.index;
|
||||
const targetTemplate = templateField.initialValue?.items?.[index];
|
||||
if (!collectionName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (targetTemplate?.treeData || treeData) {
|
||||
return parseTreeData(treeData || targetTemplate.treeData);
|
||||
}
|
||||
try {
|
||||
return traverseFields(collectionName, { exclude: ['id', ...systemKeys], maxDepth: 1 });
|
||||
} catch (error) {
|
||||
@ -242,9 +257,8 @@ function findNode(treeData, item) {
|
||||
}
|
||||
|
||||
function loadChildren({ node, traverseAssociations, traverseFields, systemKeys, fields }) {
|
||||
const activeNode = findNode(fields.componentProps.treeData, node);
|
||||
const activeNode = findNode(fields.dataSource || fields.componentProps.treeData, node);
|
||||
let children = [];
|
||||
|
||||
// 多对多和多对一只展示关系字段
|
||||
if (['belongsTo', 'belongsToMany'].includes(node.field.type)) {
|
||||
children = traverseAssociations(node.field.target, {
|
||||
|
232
packages/core/client/src/schema-settings/DataTemplates/utils.tsx
Normal file
232
packages/core/client/src/schema-settings/DataTemplates/utils.tsx
Normal file
@ -0,0 +1,232 @@
|
||||
import { ArrayBase } from '@formily/antd-v5';
|
||||
import { useForm } from '@formily/react';
|
||||
import { message } from 'antd';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getAssociationPath } from '../../block-provider/hooks';
|
||||
import { useCollectionManager } from '../../collection-manager';
|
||||
import { useCompile } from '../../schema-component';
|
||||
import { TreeNode } from './TreeLabel';
|
||||
import { systemKeys } from './hooks/useCollectionState';
|
||||
import LRUCache from 'lru-cache';
|
||||
|
||||
export const useSyncFromForm = (fieldSchema, collection?, callBack?) => {
|
||||
const { getCollectionJoinField, getCollectionFields } = useCollectionManager();
|
||||
const array = ArrayBase.useArray();
|
||||
const index = ArrayBase.useIndex();
|
||||
const record = ArrayBase.useRecord();
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
const from = useForm();
|
||||
|
||||
const traverseFields = ((cache) => {
|
||||
return (collectionName, { exclude = [], depth = 0, maxDepth, prefix = '', disabled = false }, formData) => {
|
||||
const cacheKey = `${collectionName}-${exclude.join(',')}-${depth}-${maxDepth}-${prefix}`;
|
||||
const cachedResult = cache.get(cacheKey);
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
if (depth > maxDepth) {
|
||||
return [];
|
||||
}
|
||||
const result = getCollectionFields(collectionName)
|
||||
.map((field) => {
|
||||
if (exclude.includes(field.name)) {
|
||||
return;
|
||||
}
|
||||
if (!field.interface) {
|
||||
return;
|
||||
}
|
||||
if (['sort', 'password', 'sequence'].includes(field.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = {
|
||||
type: 'duplicate',
|
||||
tag: compile(field.uiSchema?.title) || field.name,
|
||||
};
|
||||
const option = {
|
||||
...node,
|
||||
title: React.createElement(TreeNode, node),
|
||||
key: prefix ? `${prefix}.${field.name}` : field.name,
|
||||
isLeaf: true,
|
||||
field,
|
||||
disabled,
|
||||
};
|
||||
const tatgetFormField = formData.find((v) => v.name === field.name);
|
||||
if (
|
||||
['belongsTo', 'belongsToMany'].includes(field.type) &&
|
||||
(!tatgetFormField || ['Select', 'Picker'].includes(tatgetFormField?.fieldMode))
|
||||
) {
|
||||
node['type'] = 'reference';
|
||||
option['type'] = 'reference';
|
||||
option['title'] = React.createElement(TreeNode, { ...node, type: 'reference' });
|
||||
option.isLeaf = false;
|
||||
option['children'] = traverseAssociations(field.target, {
|
||||
depth: depth + 1,
|
||||
maxDepth,
|
||||
prefix: option.key,
|
||||
exclude: systemKeys,
|
||||
});
|
||||
} else if (
|
||||
['hasOne', 'hasMany'].includes(field.type) ||
|
||||
['Nester', 'SubTable'].includes(tatgetFormField?.fieldMode)
|
||||
) {
|
||||
let childrenDisabled = false;
|
||||
if (
|
||||
['hasOne', 'hasMany'].includes(field.type) &&
|
||||
['Select', 'Picker'].includes(tatgetFormField?.fieldMode)
|
||||
) {
|
||||
childrenDisabled = true;
|
||||
}
|
||||
option.disabled = true;
|
||||
option.isLeaf = false;
|
||||
option['children'] = traverseFields(
|
||||
field.target,
|
||||
{
|
||||
depth: depth + 1,
|
||||
maxDepth,
|
||||
prefix: option.key,
|
||||
exclude: ['id', ...systemKeys],
|
||||
disabled: childrenDisabled,
|
||||
},
|
||||
formData,
|
||||
);
|
||||
}
|
||||
return option;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
cache.set(cacheKey, result);
|
||||
return result;
|
||||
};
|
||||
})(
|
||||
new LRUCache<string, any>({ max: 100 }),
|
||||
);
|
||||
|
||||
const traverseAssociations = ((cache) => {
|
||||
return (collectionName, { prefix, maxDepth, depth = 0, exclude = [] }) => {
|
||||
const cacheKey = `${collectionName}-${exclude.join(',')}-${depth}-${maxDepth}-${prefix}`;
|
||||
const cachedResult = cache.get(cacheKey);
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
if (depth > maxDepth) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = getCollectionFields(collectionName)
|
||||
.map((field) => {
|
||||
if (!field.target || !field.interface) {
|
||||
return;
|
||||
}
|
||||
if (exclude.includes(field.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const option = {
|
||||
type: 'preloading',
|
||||
tag: compile(field.uiSchema?.title) || field.name,
|
||||
};
|
||||
const value = prefix ? `${prefix}.${field.name}` : field.name;
|
||||
return {
|
||||
type: 'preloading',
|
||||
tag: compile(field.uiSchema?.title) || field.name,
|
||||
title: React.createElement(TreeNode, option),
|
||||
key: value,
|
||||
isLeaf: false,
|
||||
field,
|
||||
children: traverseAssociations(field.target, {
|
||||
prefix: value,
|
||||
depth: depth + 1,
|
||||
maxDepth,
|
||||
exclude,
|
||||
}),
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
cache.set(cacheKey, result);
|
||||
return result;
|
||||
};
|
||||
})(
|
||||
new LRUCache<string, any>({ max: 100 }),
|
||||
);
|
||||
const getEnableFieldTree = useCallback((collectionName: string, formData) => {
|
||||
if (!collectionName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return traverseFields(collectionName, { exclude: ['id', ...systemKeys], maxDepth: 1, disabled: false }, formData);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
async run() {
|
||||
const formData = new Set([]);
|
||||
const selectFields = new Set([]);
|
||||
const getAssociationAppends = (schema, str) => {
|
||||
schema.reduceProperties((pre, s) => {
|
||||
const prefix = pre || str;
|
||||
const collectionfield = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field']);
|
||||
const isAssociationSubfield = s.name.includes('.');
|
||||
const isAssociationField =
|
||||
collectionfield && ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(collectionfield.type);
|
||||
const fieldPath = !isAssociationField && isAssociationSubfield ? getAssociationPath(s.name) : s.name;
|
||||
const path = prefix === '' || !prefix ? fieldPath : prefix + '.' + fieldPath;
|
||||
if (
|
||||
collectionfield &&
|
||||
!(
|
||||
['hasOne', 'hasMany'].includes(collectionfield.type) ||
|
||||
['SubForm', 'Nester'].includes(s['x-component-props']?.mode)
|
||||
)
|
||||
) {
|
||||
selectFields.add(path);
|
||||
}
|
||||
if (collectionfield && (isAssociationField || isAssociationSubfield) && s['x-component'] !== 'TableField') {
|
||||
formData.add({ name: path, fieldMode: s['x-component-props']['mode'] || 'Select' });
|
||||
if (['Nester', 'SubTable'].includes(s['x-component-props']?.mode)) {
|
||||
const bufPrefix = prefix && prefix !== '' ? prefix + '.' + s.name : s.name;
|
||||
getAssociationAppends(s, bufPrefix);
|
||||
}
|
||||
} else if (
|
||||
![
|
||||
'ActionBar',
|
||||
'Action',
|
||||
'Action.Link',
|
||||
'Action.Modal',
|
||||
'Selector',
|
||||
'Viewer',
|
||||
'AddNewer',
|
||||
'AssociationField.Selector',
|
||||
'AssociationField.AddNewer',
|
||||
'TableField',
|
||||
].includes(s['x-component'])
|
||||
) {
|
||||
getAssociationAppends(s, str);
|
||||
}
|
||||
}, str);
|
||||
};
|
||||
getAssociationAppends(fieldSchema, '');
|
||||
const treeData = getEnableFieldTree(record?.collection || collection, [...formData]);
|
||||
if (callBack) {
|
||||
callBack(treeData, [...selectFields], from);
|
||||
} else {
|
||||
array?.field.form.query(`fieldReaction.items.${index}.layout.fields`).take((f: any) => {
|
||||
f.componentProps.treeData = [];
|
||||
setTimeout(() => (f.componentProps.treeData = treeData));
|
||||
});
|
||||
array?.field.value.splice(index, 1, {
|
||||
...array?.field?.value[index],
|
||||
fields: [...selectFields],
|
||||
treeData: treeData,
|
||||
});
|
||||
}
|
||||
message.success(t('Sync successfully'));
|
||||
},
|
||||
};
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { ArrayCollapse, ArrayItems, FormItem, FormLayout, Input } from '@formily/antd-v5';
|
||||
import { Field, GeneralField, createForm } from '@formily/core';
|
||||
import { createForm, Field, GeneralField } from '@formily/core';
|
||||
import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { error } from '@nocobase/utils/client';
|
||||
@ -21,32 +21,32 @@ import {
|
||||
} from 'antd';
|
||||
import _, { cloneDeep } from 'lodash';
|
||||
import React, {
|
||||
ReactNode,
|
||||
createContext,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
// @ts-ignore
|
||||
useTransition as useReactTransition,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
APIClientProvider,
|
||||
ActionContextProvider,
|
||||
APIClientProvider,
|
||||
CollectionFieldOptions,
|
||||
CollectionManagerContext,
|
||||
CollectionProvider,
|
||||
createDesignable,
|
||||
Designable,
|
||||
findFormBlock,
|
||||
FormDialog,
|
||||
FormProvider,
|
||||
RemoteSchemaComponent,
|
||||
SchemaComponent,
|
||||
SchemaComponentContext,
|
||||
SchemaComponentOptions,
|
||||
createDesignable,
|
||||
findFormBlock,
|
||||
useAPIClient,
|
||||
useBlockRequestContext,
|
||||
useCollection,
|
||||
@ -1146,7 +1146,6 @@ SchemaSettings.DataTemplates = function DataTemplates(props) {
|
||||
const { t } = useTranslation();
|
||||
const formSchema = findFormBlock(fieldSchema) || fieldSchema;
|
||||
const { templateData } = useDataTemplates();
|
||||
|
||||
const schema = useMemo(
|
||||
() => ({
|
||||
type: 'object',
|
||||
@ -1171,7 +1170,6 @@ SchemaSettings.DataTemplates = function DataTemplates(props) {
|
||||
);
|
||||
const onSubmit = useCallback((v) => {
|
||||
const data = { ...(formSchema['x-data-templates'] || {}), ...v.fieldReaction };
|
||||
|
||||
// 当 Tree 组件开启 checkStrictly 属性时,会导致 checkedKeys 的值是一个对象,而不是数组,所以这里需要转换一下以支持旧版本
|
||||
data.items.forEach((item) => {
|
||||
item.fields = Array.isArray(item.fields) ? item.fields : item.fields.checked;
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-nocobase-app",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"main": "src/index.js",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@nocobase/database",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"description": "",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@nocobase/logger": "0.11.1-alpha.3",
|
||||
"@nocobase/utils": "0.11.1-alpha.3",
|
||||
"@nocobase/logger": "0.11.1-alpha.5",
|
||||
"@nocobase/utils": "0.11.1-alpha.5",
|
||||
"async-mutex": "^0.3.2",
|
||||
"cron-parser": "4.4.0",
|
||||
"dayjs": "^1.11.8",
|
||||
|
@ -112,80 +112,114 @@ describe('option parser', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('option parser with fields option', async () => {
|
||||
let options: any = {
|
||||
fields: ['id', 'posts'],
|
||||
};
|
||||
// 转换为 attributes: ['id'], include: [{association: 'posts'}]
|
||||
let parser = new OptionsParser(options, {
|
||||
collection: User,
|
||||
describe('options parser with fields option', () => {
|
||||
it('should handle field and association', () => {
|
||||
const options: any = {
|
||||
fields: ['id', 'posts'],
|
||||
};
|
||||
|
||||
// 转换为 attributes: ['id'], include: [{association: 'posts'}]
|
||||
const parser = new OptionsParser(options, {
|
||||
collection: User,
|
||||
});
|
||||
|
||||
const params = parser.toSequelizeParams();
|
||||
|
||||
console.log(params);
|
||||
expect(params['attributes']).toContain('id');
|
||||
expect(params['include'][0]['association']).toEqual('posts');
|
||||
});
|
||||
let params = parser.toSequelizeParams();
|
||||
|
||||
expect(params['attributes']).toContain('id');
|
||||
expect(params['include'][0]['association']).toEqual('posts');
|
||||
it('should handle field with association', () => {
|
||||
const options = {
|
||||
appends: ['posts'],
|
||||
};
|
||||
|
||||
// only appends
|
||||
options = {
|
||||
appends: ['posts'],
|
||||
};
|
||||
const parser = new OptionsParser(options, {
|
||||
collection: User,
|
||||
});
|
||||
const params = parser.toSequelizeParams();
|
||||
|
||||
parser = new OptionsParser(options, {
|
||||
collection: User,
|
||||
expect(params['attributes']['include']).toEqual([]);
|
||||
expect(params['include'][0]['association']).toEqual('posts');
|
||||
});
|
||||
params = parser.toSequelizeParams();
|
||||
expect(params['attributes']['include']).toEqual([]);
|
||||
expect(params['include'][0]['association']).toEqual('posts');
|
||||
|
||||
// fields with association field
|
||||
options = {
|
||||
fields: ['id', 'posts.title'],
|
||||
};
|
||||
it('should handle field with association field', () => {
|
||||
// fields with association field
|
||||
const options = {
|
||||
fields: ['id', 'posts.title'],
|
||||
};
|
||||
|
||||
parser = new OptionsParser(options, {
|
||||
collection: User,
|
||||
const parser = new OptionsParser(options, {
|
||||
collection: User,
|
||||
});
|
||||
|
||||
const params = parser.toSequelizeParams();
|
||||
expect(params['attributes']).toContain('id');
|
||||
expect(params['include'][0]['association']).toEqual('posts');
|
||||
expect(params['include'][0]['attributes']).toContain('title');
|
||||
});
|
||||
params = parser.toSequelizeParams();
|
||||
expect(params['attributes']).toContain('id');
|
||||
expect(params['include'][0]['association']).toEqual('posts');
|
||||
expect(params['include'][0]['attributes']).toContain('title');
|
||||
|
||||
// fields with nested field
|
||||
options = {
|
||||
fields: ['id', 'posts', 'posts.comments.content'],
|
||||
};
|
||||
it('should handle nested fields option', () => {
|
||||
const options = {
|
||||
fields: ['posts', 'posts.title'],
|
||||
};
|
||||
|
||||
parser = new OptionsParser(options, {
|
||||
collection: User,
|
||||
const parser = new OptionsParser(options, {
|
||||
collection: User,
|
||||
});
|
||||
|
||||
const params = parser.toSequelizeParams();
|
||||
const postAssociationParams = params['include'][0];
|
||||
expect(postAssociationParams['attributes']).toEqual({ include: [] });
|
||||
});
|
||||
params = parser.toSequelizeParams();
|
||||
expect(params['attributes']).toContain('id');
|
||||
expect(params['include'][0]['association']).toEqual('posts');
|
||||
expect(params['include'][0]['attributes']).toEqual({ include: [] });
|
||||
expect(params['include'][0]['include'][0]['association']).toEqual('comments');
|
||||
|
||||
// fields with expect
|
||||
options = {
|
||||
except: ['id'],
|
||||
};
|
||||
parser = new OptionsParser(options, {
|
||||
collection: User,
|
||||
it('should handle fields with association & association field', () => {
|
||||
// fields with nested field
|
||||
const options = {
|
||||
fields: ['id', 'posts', 'posts.comments.content'],
|
||||
};
|
||||
|
||||
const parser = new OptionsParser(options, {
|
||||
collection: User,
|
||||
});
|
||||
|
||||
const params = parser.toSequelizeParams();
|
||||
const postAssociationParams = params['include'][0];
|
||||
|
||||
expect(params['attributes']).toContain('id');
|
||||
expect(postAssociationParams['association']).toEqual('posts');
|
||||
expect(postAssociationParams['attributes']).toEqual({ include: [] });
|
||||
expect(postAssociationParams['include'][0]['association']).toEqual('comments');
|
||||
});
|
||||
params = parser.toSequelizeParams();
|
||||
expect(params['attributes']['exclude']).toContain('id');
|
||||
|
||||
// expect with association
|
||||
options = {
|
||||
fields: ['posts'],
|
||||
except: ['posts.id'],
|
||||
};
|
||||
it('should handle except option', () => {
|
||||
// fields with expect
|
||||
const options = {
|
||||
except: ['id'],
|
||||
};
|
||||
const parser = new OptionsParser(options, {
|
||||
collection: User,
|
||||
});
|
||||
|
||||
parser = new OptionsParser(options, {
|
||||
collection: User,
|
||||
const params = parser.toSequelizeParams();
|
||||
expect(params['attributes']['exclude']).toContain('id');
|
||||
});
|
||||
params = parser.toSequelizeParams();
|
||||
|
||||
expect(params['include'][0]['attributes']['exclude']).toContain('id');
|
||||
it('should handle fields with except option', () => {
|
||||
// expect with association
|
||||
const options = {
|
||||
fields: ['posts'],
|
||||
except: ['posts.id'],
|
||||
};
|
||||
|
||||
const parser = new OptionsParser(options, {
|
||||
collection: User,
|
||||
});
|
||||
const params = parser.toSequelizeParams();
|
||||
|
||||
expect(params['include'][0]['attributes']['exclude']).toContain('id');
|
||||
});
|
||||
});
|
||||
|
||||
test('option parser with multiple association', () => {
|
||||
|
@ -378,23 +378,33 @@ describe('repository find', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should only output filed in fields args', async () => {
|
||||
const resp = await User.model.findOne({
|
||||
attributes: [],
|
||||
include: [
|
||||
{
|
||||
association: 'profile',
|
||||
attributes: ['salary'],
|
||||
},
|
||||
],
|
||||
describe('find with fields', () => {
|
||||
it('should only output filed in fields args', async () => {
|
||||
const users = await User.repository.find({
|
||||
fields: ['profile.salary'],
|
||||
});
|
||||
|
||||
const firstUser = users[0].toJSON();
|
||||
expect(Object.keys(firstUser)).toEqual(['profile']);
|
||||
expect(Object.keys(firstUser.profile)).toEqual(['salary']);
|
||||
});
|
||||
|
||||
const users = await User.repository.find({
|
||||
fields: ['profile', 'profile.salary', 'profile.id'],
|
||||
});
|
||||
it('should output all fields when field has relation field', async () => {
|
||||
const users = await User.repository.find({
|
||||
fields: ['profile.salary', 'profile'],
|
||||
});
|
||||
|
||||
const firstUser = users[0].toJSON();
|
||||
expect(Object.keys(firstUser)).toEqual(['profile']);
|
||||
const firstUser = users[0].toJSON();
|
||||
expect(Object.keys(firstUser)).toEqual(['profile']);
|
||||
expect(Object.keys(firstUser.profile)).toEqual([
|
||||
'id',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'salary',
|
||||
'userId',
|
||||
'description',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('append with associations', async () => {
|
||||
|
@ -18,131 +18,155 @@ pgOnly()('', () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('should skip on delete on view collection', async () => {
|
||||
const Order = db.collection({
|
||||
name: 'orders',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'orderItems',
|
||||
foreignKey: 'order_id',
|
||||
target: 'orderItems',
|
||||
},
|
||||
],
|
||||
});
|
||||
describe('view as through table', () => {
|
||||
let Order;
|
||||
let OrderItem;
|
||||
let Item;
|
||||
let OrderItemView;
|
||||
|
||||
const OrderItem = db.collection({
|
||||
name: 'orderItems',
|
||||
timestamps: false,
|
||||
fields: [
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'count',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'item',
|
||||
target: 'items',
|
||||
foreignKey: 'item_id',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'order',
|
||||
target: 'orders',
|
||||
foreignKey: 'order_id',
|
||||
onDelete: 'NO ACTION',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const Item = db.collection({
|
||||
name: 'items',
|
||||
fields: [{ name: 'name', type: 'string' }],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
const viewName = 'order_item_view';
|
||||
|
||||
const dropViewSQL = `DROP VIEW IF EXISTS ${viewName}`;
|
||||
await db.sequelize.query(dropViewSQL);
|
||||
|
||||
const viewSQL = `CREATE VIEW ${viewName} as SELECT orders.*, items.name as item_name FROM ${OrderItem.quotedTableName()} as orders INNER JOIN ${Item.quotedTableName()} as items ON orders.item_id = items.id`;
|
||||
|
||||
await db.sequelize.query(viewSQL);
|
||||
|
||||
const OrderItemView = db.collection({
|
||||
name: viewName,
|
||||
view: true,
|
||||
schema: db.inDialect('postgres') ? 'public' : undefined,
|
||||
fields: [
|
||||
{
|
||||
type: 'bigInt',
|
||||
name: 'order_id',
|
||||
},
|
||||
{
|
||||
type: 'bigInt',
|
||||
name: 'item_id',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
Order.setField('items', {
|
||||
type: 'belongsToMany',
|
||||
target: 'orderItems',
|
||||
through: viewName,
|
||||
foreignKey: 'order_id',
|
||||
otherKey: 'item_id',
|
||||
sourceKey: 'id',
|
||||
targetKey: 'id',
|
||||
onDelete: 'CASCADE',
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
const order1 = await db.getRepository('orders').create({
|
||||
values: {
|
||||
name: 'order1',
|
||||
orderItems: [
|
||||
beforeEach(async () => {
|
||||
Order = db.collection({
|
||||
name: 'orders',
|
||||
fields: [
|
||||
{
|
||||
count: 1,
|
||||
item: {
|
||||
name: 'item1',
|
||||
},
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
count: 2,
|
||||
item: {
|
||||
name: 'item2',
|
||||
},
|
||||
type: 'hasMany',
|
||||
name: 'orderItems',
|
||||
foreignKey: 'order_id',
|
||||
target: 'orderItems',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const item1 = await db.getRepository('items').findOne({
|
||||
filter: {
|
||||
name: 'item1',
|
||||
},
|
||||
});
|
||||
|
||||
let error;
|
||||
try {
|
||||
await db.getRepository('orders').destroy({
|
||||
filterByTk: order1.get('id'),
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
OrderItem = db.collection({
|
||||
name: 'orderItems',
|
||||
timestamps: false,
|
||||
fields: [
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'count',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'item',
|
||||
target: 'items',
|
||||
foreignKey: 'item_id',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'order',
|
||||
target: 'orders',
|
||||
foreignKey: 'order_id',
|
||||
onDelete: 'NO ACTION',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Item = db.collection({
|
||||
name: 'items',
|
||||
fields: [{ name: 'name', type: 'string' }],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
const viewName = 'order_item_view';
|
||||
|
||||
const dropViewSQL = `DROP VIEW IF EXISTS ${viewName}`;
|
||||
await db.sequelize.query(dropViewSQL);
|
||||
|
||||
const viewSQL = `CREATE VIEW ${viewName} as SELECT order_item.order_id as order_id, order_item.item_id as item_id, items.name as item_name FROM ${OrderItem.quotedTableName()} as order_item INNER JOIN ${Item.quotedTableName()} as items ON order_item.item_id = items.id`;
|
||||
|
||||
await db.sequelize.query(viewSQL);
|
||||
|
||||
OrderItemView = db.collection({
|
||||
name: viewName,
|
||||
view: true,
|
||||
schema: db.inDialect('postgres') ? 'public' : undefined,
|
||||
fields: [
|
||||
{
|
||||
type: 'bigInt',
|
||||
name: 'order_id',
|
||||
},
|
||||
{
|
||||
type: 'bigInt',
|
||||
name: 'item_id',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
Order.setField('items', {
|
||||
type: 'belongsToMany',
|
||||
target: 'items',
|
||||
through: viewName,
|
||||
foreignKey: 'order_id',
|
||||
otherKey: 'item_id',
|
||||
sourceKey: 'id',
|
||||
targetKey: 'id',
|
||||
onDelete: 'CASCADE',
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
await db.getRepository('orders').create({
|
||||
values: {
|
||||
name: 'order1',
|
||||
orderItems: [
|
||||
{
|
||||
count: 1,
|
||||
item: {
|
||||
name: 'item1',
|
||||
},
|
||||
},
|
||||
{
|
||||
count: 2,
|
||||
item: {
|
||||
name: 'item2',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip on delete on view collection', async () => {
|
||||
const order1 = await db.getRepository('orders').findOne({});
|
||||
|
||||
const item1 = await db.getRepository('items').findOne({
|
||||
filter: {
|
||||
name: 'item1',
|
||||
},
|
||||
});
|
||||
|
||||
let error;
|
||||
try {
|
||||
await db.getRepository('orders').destroy({
|
||||
filterByTk: order1.get('id'),
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should filter by view collection as through table', async () => {
|
||||
const orders = await db.getRepository('orders').find({
|
||||
appends: ['items'],
|
||||
filter: {
|
||||
items: {
|
||||
name: 'not exists',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(orders).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should update view collection', async () => {
|
||||
@ -196,7 +220,7 @@ pgOnly()('', () => {
|
||||
});
|
||||
|
||||
// create INSTEAD OF INSERT trigger
|
||||
await db.sequelize.query(`
|
||||
await db.sequelize.query(`
|
||||
CREATE OR REPLACE FUNCTION insert_users_with_group() RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
new_group_id BIGINT;
|
||||
|
@ -3,6 +3,11 @@ import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
export class JsonField extends Field {
|
||||
get dataType() {
|
||||
const dialect = this.context.database.sequelize.getDialect();
|
||||
const { jsonb } = this.options;
|
||||
if (dialect === 'postgres' && jsonb) {
|
||||
return DataTypes.JSONB;
|
||||
}
|
||||
return DataTypes.JSON;
|
||||
}
|
||||
}
|
||||
|
@ -240,6 +240,9 @@ export class OptionsParser {
|
||||
protected parseAppends(appends: Appends, filterParams: any) {
|
||||
if (!appends) return filterParams;
|
||||
|
||||
// sort appends by path length
|
||||
appends = lodash.sortBy(appends, (append) => append.split('.').length);
|
||||
|
||||
/**
|
||||
* set include params
|
||||
* @param model
|
||||
@ -287,6 +290,25 @@ export class OptionsParser {
|
||||
// if include from filter, remove fromFilter attribute
|
||||
if (existIncludeIndex != -1) {
|
||||
delete queryParams['include'][existIncludeIndex]['fromFilter'];
|
||||
|
||||
// set include attributes to all attributes
|
||||
if (
|
||||
Array.isArray(queryParams['include'][existIncludeIndex]['attributes']) &&
|
||||
queryParams['include'][existIncludeIndex]['attributes'].length == 0
|
||||
) {
|
||||
queryParams['include'][existIncludeIndex]['attributes'] = {
|
||||
include: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
lastLevel &&
|
||||
existIncludeIndex != -1 &&
|
||||
lodash.get(queryParams, ['include', existIncludeIndex, 'attributes', 'include'])?.length == 0
|
||||
) {
|
||||
// if append is last level and association exists, ignore it
|
||||
return;
|
||||
}
|
||||
|
||||
// if association not exist, create it
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@nocobase/devtools",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./src/index.js",
|
||||
"dependencies": {
|
||||
"@nocobase/build": "0.11.1-alpha.3",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@nocobase/build": "0.11.1-alpha.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/koa": "^2.13.4",
|
||||
"@types/koa-bodyparser": "^4.3.4",
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@nocobase/evaluators",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"description": "",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@formulajs/formulajs": "4.2.0",
|
||||
"@nocobase/utils": "0.11.1-alpha.3",
|
||||
"@nocobase/utils": "0.11.1-alpha.5",
|
||||
"mathjs": "^10.6.0"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/logger",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"description": "nocobase logging library",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@nocobase/resourcer",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"description": "",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@nocobase/utils": "0.11.1-alpha.3",
|
||||
"@nocobase/utils": "0.11.1-alpha.5",
|
||||
"deepmerge": "^4.2.2",
|
||||
"koa-compose": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/sdk",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"license": "Apache-2.0",
|
||||
"main": "lib",
|
||||
"module": "es/index.js",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/server",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"main": "lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
@ -8,13 +8,13 @@
|
||||
"@hapi/topo": "^6.0.0",
|
||||
"@koa/cors": "^3.1.0",
|
||||
"@koa/router": "^9.4.0",
|
||||
"@nocobase/acl": "0.11.1-alpha.3",
|
||||
"@nocobase/actions": "0.11.1-alpha.3",
|
||||
"@nocobase/auth": "0.11.1-alpha.3",
|
||||
"@nocobase/database": "0.11.1-alpha.3",
|
||||
"@nocobase/logger": "0.11.1-alpha.3",
|
||||
"@nocobase/resourcer": "0.11.1-alpha.3",
|
||||
"@nocobase/utils": "0.11.1-alpha.3",
|
||||
"@nocobase/acl": "0.11.1-alpha.5",
|
||||
"@nocobase/actions": "0.11.1-alpha.5",
|
||||
"@nocobase/auth": "0.11.1-alpha.5",
|
||||
"@nocobase/database": "0.11.1-alpha.5",
|
||||
"@nocobase/logger": "0.11.1-alpha.5",
|
||||
"@nocobase/resourcer": "0.11.1-alpha.5",
|
||||
"@nocobase/utils": "0.11.1-alpha.5",
|
||||
"chalk": "^4.1.1",
|
||||
"commander": "^9.2.0",
|
||||
"dayjs": "^1.11.8",
|
||||
|
@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@nocobase/test",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"main": "lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@nocobase/server": "0.11.1-alpha.3",
|
||||
"@nocobase/server": "0.11.1-alpha.5",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"mockjs": "^1.1.0",
|
||||
"mysql2": "^2.3.3",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/utils",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"main": "lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"displayName.zh-CN": "权限控制",
|
||||
"description": "A simple access control based on roles, resources and actions",
|
||||
"description.zh-CN": "基于角色、资源和操作的权限控制。",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/server/index.js",
|
||||
"files": [
|
||||
@ -19,13 +19,13 @@
|
||||
"client.d.ts"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@nocobase/acl": "0.11.1-alpha.3",
|
||||
"@nocobase/actions": "0.11.1-alpha.3",
|
||||
"@nocobase/client": "0.11.1-alpha.3",
|
||||
"@nocobase/database": "0.11.1-alpha.3",
|
||||
"@nocobase/server": "0.11.1-alpha.3",
|
||||
"@nocobase/test": "0.11.1-alpha.3",
|
||||
"@nocobase/utils": "0.11.1-alpha.3",
|
||||
"@nocobase/acl": "0.11.1-alpha.5",
|
||||
"@nocobase/actions": "0.11.1-alpha.5",
|
||||
"@nocobase/client": "0.11.1-alpha.5",
|
||||
"@nocobase/database": "0.11.1-alpha.5",
|
||||
"@nocobase/server": "0.11.1-alpha.5",
|
||||
"@nocobase/test": "0.11.1-alpha.5",
|
||||
"@nocobase/utils": "0.11.1-alpha.5",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"react": "^18.2.0",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"displayName.zh-CN": "API keys",
|
||||
"description": "Allow users to use API key to access NocoBase's api",
|
||||
"description.zh-CN": "允许用户使用 API key 访问 NocoBase 的 api",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/server/index.js",
|
||||
"files": [
|
||||
@ -19,15 +19,15 @@
|
||||
"client.d.ts"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@formily/react": "2.2.26",
|
||||
"@formily/shared": "2.2.26",
|
||||
"@nocobase/actions": "0.11.1-alpha.3",
|
||||
"@nocobase/client": "0.11.1-alpha.3",
|
||||
"@nocobase/database": "0.11.1-alpha.3",
|
||||
"@nocobase/resourcer": "0.11.1-alpha.3",
|
||||
"@nocobase/server": "0.11.1-alpha.3",
|
||||
"@nocobase/test": "0.11.1-alpha.3",
|
||||
"@nocobase/utils": "0.11.1-alpha.3",
|
||||
"@formily/react": "^2.2.27",
|
||||
"@formily/shared": "^2.2.27",
|
||||
"@nocobase/actions": "0.11.1-alpha.5",
|
||||
"@nocobase/client": "0.11.1-alpha.5",
|
||||
"@nocobase/database": "0.11.1-alpha.5",
|
||||
"@nocobase/resourcer": "0.11.1-alpha.5",
|
||||
"@nocobase/server": "0.11.1-alpha.5",
|
||||
"@nocobase/test": "0.11.1-alpha.5",
|
||||
"@nocobase/utils": "0.11.1-alpha.5",
|
||||
"antd": "^5.6.4",
|
||||
"dayjs": "^1.11.8",
|
||||
"i18next": "^22.4.9",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-audit-logs",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"displayName": "audit-logs",
|
||||
"displayName.zh-CN": "审计日志",
|
||||
"description": "audit logs plugin",
|
||||
@ -20,13 +20,13 @@
|
||||
"license": "AGPL-3.0",
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^5.1.4",
|
||||
"@formily/antd-v5": "1.1.0-beta.4",
|
||||
"@formily/react": "2.2.26",
|
||||
"@formily/shared": "2.2.26",
|
||||
"@nocobase/client": "0.11.1-alpha.3",
|
||||
"@nocobase/database": "0.11.1-alpha.3",
|
||||
"@nocobase/server": "0.11.1-alpha.3",
|
||||
"@nocobase/test": "0.11.1-alpha.3",
|
||||
"@formily/antd-v5": "^1.1.0",
|
||||
"@formily/react": "^2.2.27",
|
||||
"@formily/shared": "^2.2.27",
|
||||
"@nocobase/client": "0.11.1-alpha.5",
|
||||
"@nocobase/database": "0.11.1-alpha.5",
|
||||
"@nocobase/server": "0.11.1-alpha.5",
|
||||
"@nocobase/test": "0.11.1-alpha.5",
|
||||
"react": "^18.2.0",
|
||||
"react-i18next": "^11.15.1"
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-auth",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"main": "./lib/server/index.js",
|
||||
"files": [
|
||||
"lib",
|
||||
@ -15,20 +15,20 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^5.1.4",
|
||||
"@formily/react": "2.2.26",
|
||||
"@formily/shared": "2.2.26",
|
||||
"@nocobase/auth": "0.11.1-alpha.3",
|
||||
"@nocobase/test": "0.11.1-alpha.3",
|
||||
"@formily/react": "^2.2.27",
|
||||
"@formily/shared": "^2.2.27",
|
||||
"@nocobase/auth": "0.11.1-alpha.5",
|
||||
"@nocobase/test": "0.11.1-alpha.5",
|
||||
"@types/cron": "^2.0.1",
|
||||
"antd": "^5.6.4",
|
||||
"react": "^18.2.0",
|
||||
"react-i18next": "^11.15.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nocobase/actions": "0.11.1-alpha.3",
|
||||
"@nocobase/client": "0.11.1-alpha.3",
|
||||
"@nocobase/database": "0.11.1-alpha.3",
|
||||
"@nocobase/server": "0.11.1-alpha.3",
|
||||
"@nocobase/actions": "0.11.1-alpha.5",
|
||||
"@nocobase/client": "0.11.1-alpha.5",
|
||||
"@nocobase/database": "0.11.1-alpha.5",
|
||||
"@nocobase/server": "0.11.1-alpha.5",
|
||||
"cron": "^2.3.1"
|
||||
},
|
||||
"displayName": "Authentication",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"displayName.zh-CN": "图表",
|
||||
"description": "Out-of-the-box, feature-rich chart plugins.",
|
||||
"description.zh-CN": "开箱即用、丰富的报表。",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"main": "./lib/server/index.js",
|
||||
"files": [
|
||||
"lib",
|
||||
@ -22,15 +22,15 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^5.1.4",
|
||||
"@formily/antd-v5": "1.1.0-beta.4",
|
||||
"@formily/core": "2.2.26",
|
||||
"@formily/react": "2.2.26",
|
||||
"@formily/shared": "2.2.26",
|
||||
"@nocobase/client": "0.11.1-alpha.3",
|
||||
"@nocobase/database": "0.11.1-alpha.3",
|
||||
"@nocobase/server": "0.11.1-alpha.3",
|
||||
"@nocobase/test": "0.11.1-alpha.3",
|
||||
"@nocobase/utils": "0.11.1-alpha.3",
|
||||
"@formily/antd-v5": "^1.1.0",
|
||||
"@formily/core": "^2.2.27",
|
||||
"@formily/react": "^2.2.27",
|
||||
"@formily/shared": "^2.2.27",
|
||||
"@nocobase/client": "0.11.1-alpha.5",
|
||||
"@nocobase/database": "0.11.1-alpha.5",
|
||||
"@nocobase/server": "0.11.1-alpha.5",
|
||||
"@nocobase/test": "0.11.1-alpha.5",
|
||||
"@nocobase/utils": "0.11.1-alpha.5",
|
||||
"antd": "^5.6.4",
|
||||
"react": "^18.2.0",
|
||||
"react-i18next": "^11.15.1",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-china-region",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"displayName": "china-region",
|
||||
"displayName.zh-CN": "中国行政区",
|
||||
"description": "Chinese Administrative Division Plugin, including all administrative regions of China.",
|
||||
@ -22,12 +22,12 @@
|
||||
"china-division": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formily/core": "2.2.26",
|
||||
"@formily/react": "2.2.26",
|
||||
"@nocobase/client": "0.11.1-alpha.3",
|
||||
"@nocobase/database": "0.11.1-alpha.3",
|
||||
"@nocobase/server": "0.11.1-alpha.3",
|
||||
"@nocobase/test": "0.11.1-alpha.3"
|
||||
"@formily/core": "^2.2.27",
|
||||
"@formily/react": "^2.2.27",
|
||||
"@nocobase/client": "0.11.1-alpha.5",
|
||||
"@nocobase/database": "0.11.1-alpha.5",
|
||||
"@nocobase/server": "0.11.1-alpha.5",
|
||||
"@nocobase/test": "0.11.1-alpha.5"
|
||||
},
|
||||
"gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644"
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
"displayName.zh-CN": "客户端",
|
||||
"description": "client",
|
||||
"description.zh-CN": "客户端。",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"main": "./lib/server/index.js",
|
||||
"files": [
|
||||
"lib",
|
||||
@ -25,11 +25,11 @@
|
||||
"koa-static": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nocobase/client": "0.11.1-alpha.3",
|
||||
"@nocobase/database": "0.11.1-alpha.3",
|
||||
"@nocobase/server": "0.11.1-alpha.3",
|
||||
"@nocobase/test": "0.11.1-alpha.3",
|
||||
"@nocobase/utils": "0.11.1-alpha.3"
|
||||
"@nocobase/client": "0.11.1-alpha.5",
|
||||
"@nocobase/database": "0.11.1-alpha.5",
|
||||
"@nocobase/server": "0.11.1-alpha.5",
|
||||
"@nocobase/test": "0.11.1-alpha.5",
|
||||
"@nocobase/utils": "0.11.1-alpha.5"
|
||||
},
|
||||
"gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644"
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ export default class extends Migration {
|
||||
async up() {
|
||||
const systemSettings = this.db.getRepository('systemSettings');
|
||||
let instance: Model = await systemSettings.findOne();
|
||||
if (instance?.options?.adminSchemaUid) {
|
||||
return;
|
||||
}
|
||||
const uiRoutes = this.db.getRepository('uiRoutes');
|
||||
const routes = await uiRoutes.find();
|
||||
for (const route of routes) {
|
||||
|
@ -4,7 +4,7 @@
|
||||
"displayName.zh-CN": "数据库管理",
|
||||
"description": " database management plugin designed to simplify the process of managing and operating databases. It seamlessly integrates with various relational database systems such as MySQL and PostgreSQL, and provides an intuitive user interface for performing various database tasks.",
|
||||
"description.zh-CN": "可以与多种关系型数据库系统(如MySQL、PostgreSQL)无缝集成,并提供直观的用户界面来执行各种数据库任务。",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"main": "./lib/server/index.js",
|
||||
"files": [
|
||||
"lib",
|
||||
@ -24,12 +24,12 @@
|
||||
"toposort": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nocobase/client": "0.11.1-alpha.3",
|
||||
"@nocobase/database": "0.11.1-alpha.3",
|
||||
"@nocobase/plugin-error-handler": "0.11.1-alpha.3",
|
||||
"@nocobase/server": "0.11.1-alpha.3",
|
||||
"@nocobase/test": "0.11.1-alpha.3",
|
||||
"@nocobase/utils": "0.11.1-alpha.3"
|
||||
"@nocobase/client": "0.11.1-alpha.5",
|
||||
"@nocobase/database": "0.11.1-alpha.5",
|
||||
"@nocobase/plugin-error-handler": "0.11.1-alpha.5",
|
||||
"@nocobase/server": "0.11.1-alpha.5",
|
||||
"@nocobase/test": "0.11.1-alpha.5",
|
||||
"@nocobase/utils": "0.11.1-alpha.5"
|
||||
},
|
||||
"gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644"
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-data-visualization",
|
||||
"version": "0.11.1-alpha.3",
|
||||
"version": "0.11.1-alpha.5",
|
||||
"displayName": "Data Visualization",
|
||||
"displayName.zh-CN": "数据可视化",
|
||||
"description": "Provides business intelligence and data visualization features",
|
||||
@ -13,18 +13,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^5.1.4",
|
||||
"@formily/antd-v5": "1.1.0-beta.4",
|
||||
"@formily/core": "2.2.26",
|
||||
"@formily/react": "2.2.26",
|
||||
"@formily/shared": "2.2.26",
|
||||
"@nocobase/actions": "0.11.1-alpha.3",
|
||||
"@nocobase/cache": "0.11.1-alpha.3",
|
||||
"@nocobase/client": "0.11.1-alpha.3",
|
||||
"@nocobase/database": "0.11.1-alpha.3",
|
||||
"@nocobase/server": "0.11.1-alpha.3",
|
||||
"@nocobase/test": "0.11.1-alpha.3",
|
||||
"@nocobase/utils": "0.11.1-alpha.3",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@formily/antd-v5": "^1.1.0",
|
||||
"@formily/core": "^2.2.27",
|
||||
"@formily/react": "^2.2.27",
|
||||
"@formily/shared": "^2.2.27",
|
||||
"@nocobase/actions": "0.11.1-alpha.5",
|
||||
"@nocobase/cache": "0.11.1-alpha.5",
|
||||
"@nocobase/client": "0.11.1-alpha.5",
|
||||
"@nocobase/database": "0.11.1-alpha.5",
|
||||
"@nocobase/server": "0.11.1-alpha.5",
|
||||
"@nocobase/test": "0.11.1-alpha.5",
|
||||
"@nocobase/utils": "0.11.1-alpha.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"antd": "^5.6.4",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
|
@ -5,7 +5,7 @@ import { Button, Card } from 'antd';
|
||||
import cls from 'classnames';
|
||||
import React, { useContext } from 'react';
|
||||
import { useChartsTranslation } from './locale';
|
||||
import { ChartLibraryContext, useToggleChartLibrary } from './renderer';
|
||||
import { ChartLibraryContext, useToggleChartLibrary } from './chart/library';
|
||||
|
||||
export const Settings = () => {
|
||||
const { t } = useChartsTranslation();
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Chart } from '../chart/chart';
|
||||
import { FieldOption } from '../hooks';
|
||||
import { infer } from '../renderer';
|
||||
const chart = new Chart('test', 'Test', null);
|
||||
|
||||
describe('library', () => {
|
||||
describe('auto infer', () => {
|
||||
@ -37,7 +38,7 @@ describe('library', () => {
|
||||
] as FieldOption[];
|
||||
|
||||
test('1 measure, 1 dimension', () => {
|
||||
const { xField, yField } = infer(fields, {
|
||||
const { xField, yField } = chart.infer(fields, {
|
||||
measures: [{ field: ['price'] }],
|
||||
dimensions: [{ field: ['title'] }],
|
||||
});
|
||||
@ -46,7 +47,7 @@ describe('library', () => {
|
||||
});
|
||||
|
||||
test('1 measure, 2 dimensions with date', () => {
|
||||
const { xField, yField, seriesField } = infer(fields, {
|
||||
const { xField, yField, seriesField } = chart.infer(fields, {
|
||||
measures: [{ field: ['price'] }],
|
||||
dimensions: [{ field: ['title'] }, { field: ['createdAt'] }],
|
||||
});
|
||||
@ -56,7 +57,7 @@ describe('library', () => {
|
||||
});
|
||||
|
||||
test('1 measure, 2 dimensions without date', () => {
|
||||
const { xField, yField, seriesField } = infer(fields, {
|
||||
const { xField, yField, seriesField } = chart.infer(fields, {
|
||||
measures: [{ field: ['price'] }],
|
||||
dimensions: [{ field: ['title'] }, { field: ['name'] }],
|
||||
});
|
||||
@ -66,7 +67,7 @@ describe('library', () => {
|
||||
});
|
||||
|
||||
test('2 measures, 1 dimension', () => {
|
||||
const { xField, yField, yFields } = infer(fields, {
|
||||
const { xField, yField, yFields } = chart.infer(fields, {
|
||||
measures: [{ field: ['price'] }, { field: ['count'] }],
|
||||
dimensions: [{ field: ['title'] }],
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as client from '@nocobase/client';
|
||||
// import { renderHook } from '@testing-library/react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { vi } from 'vitest';
|
||||
import formatters from '../block/formatters';
|
||||
import transformers from '../block/transformers';
|
||||
@ -13,8 +13,7 @@ import {
|
||||
useTransformers,
|
||||
} from '../hooks';
|
||||
|
||||
// TODO: 为了暂时解决很多 Warning 的问题把 `@testing-library/react` 降级到了 12.x 所以不支持 `renderHook`,等到再次升级到 14.x 时再把 skip 去掉
|
||||
describe.skip('hooks', () => {
|
||||
describe('hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(client, 'useCollectionManager').mockReturnValue({
|
||||
getCollectionFields: (name: string) =>
|
||||
|
@ -17,6 +17,7 @@ import React, { createContext, useContext, useMemo, useRef } from 'react';
|
||||
import {
|
||||
useChartFields,
|
||||
useCollectionOptions,
|
||||
useData,
|
||||
useFieldTypes,
|
||||
useFieldsWithAssociation,
|
||||
useFormatters,
|
||||
@ -25,9 +26,10 @@ import {
|
||||
useTransformers,
|
||||
} from '../hooks';
|
||||
import { useChartsTranslation } from '../locale';
|
||||
import { ChartRenderer, ChartRendererContext, useChartTypes, useCharts, useDefaultChartType } from '../renderer';
|
||||
import { createRendererSchema, getField, getSelectedFields, processData } from '../utils';
|
||||
import { ChartRenderer, ChartRendererContext } from '../renderer';
|
||||
import { createRendererSchema, getField, getSelectedFields } from '../utils';
|
||||
import { getConfigSchema, querySchema, transformSchema } from './schemas/configure';
|
||||
import { useChartTypes, useCharts, useDefaultChartType } from '../chart/library';
|
||||
const { Paragraph, Text } = Typography;
|
||||
|
||||
export type ChartConfigCurrent = {
|
||||
@ -97,7 +99,7 @@ export const ChartConfigure: React.FC<{
|
||||
}
|
||||
const query = form.values.query;
|
||||
const selectedFields = getSelectedFields(fields, query);
|
||||
const { general, advanced } = init(selectedFields, query);
|
||||
const { general, advanced } = chart.init(selectedFields, query);
|
||||
if (general || overwrite) {
|
||||
form.values.config.general = general;
|
||||
}
|
||||
@ -363,7 +365,7 @@ ChartConfigure.Config = function Config() {
|
||||
const charts = useCharts();
|
||||
const getChartFields = useChartFields(fields);
|
||||
const getReference = (chartType: string) => {
|
||||
const reference = charts[chartType]?.reference;
|
||||
const reference = charts[chartType]?.getReference?.();
|
||||
if (!reference) return '';
|
||||
const { title, link } = reference;
|
||||
return (
|
||||
@ -409,11 +411,10 @@ ChartConfigure.Transform = function Transform() {
|
||||
};
|
||||
|
||||
ChartConfigure.Data = function Data() {
|
||||
const { t } = useChartsTranslation();
|
||||
const { current } = useContext(ChartConfigContext);
|
||||
const { service } = useContext(ChartRendererContext);
|
||||
const { current } = useContext(ChartConfigContext);
|
||||
const fields = useFieldsWithAssociation();
|
||||
const data = processData(fields, service?.data || current?.data || [], { t });
|
||||
const data = useData(current?.data);
|
||||
const error = service?.error;
|
||||
return !error ? (
|
||||
<div
|
||||
|
@ -0,0 +1,10 @@
|
||||
import { Chart } from '../chart';
|
||||
|
||||
export class AntdChart extends Chart {
|
||||
getReference() {
|
||||
return {
|
||||
title: this.title,
|
||||
link: `https://ant.design/components/${this.name}`,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import { Statistic } from './statistic';
|
||||
import { Table } from './table';
|
||||
|
||||
export default [new Statistic(), new Table()];
|
@ -0,0 +1,64 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import { AntdChart } from './antd';
|
||||
import { Statistic as AntdStatistic } from 'antd';
|
||||
import { lang } from '../../locale';
|
||||
import { FieldOption } from '../../hooks';
|
||||
import { QueryProps } from '../../renderer';
|
||||
import { RenderProps } from '../chart';
|
||||
|
||||
export class Statistic extends AntdChart {
|
||||
schema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
field: {
|
||||
title: lang('Field'),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-reactions': '{{ useChartFields }}',
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
title: lang('Title'),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super('statistic', 'Statistic', AntdStatistic);
|
||||
}
|
||||
|
||||
init(
|
||||
fields: FieldOption[],
|
||||
{
|
||||
measures,
|
||||
dimensions,
|
||||
}: {
|
||||
measures?: QueryProps['measures'];
|
||||
dimensions?: QueryProps['dimensions'];
|
||||
},
|
||||
) {
|
||||
const { yField } = this.infer(fields, { measures, dimensions });
|
||||
return {
|
||||
general: {
|
||||
field: yField?.value,
|
||||
title: yField?.label,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getProps({ data, fieldProps, general, advanced }: RenderProps) {
|
||||
const record = data[0] || {};
|
||||
const field = general?.field;
|
||||
const props = fieldProps[field];
|
||||
return {
|
||||
value: record[field],
|
||||
formatter: props?.transformer,
|
||||
...general,
|
||||
...advanced,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import { RenderProps } from '../chart';
|
||||
import { AntdChart } from './antd';
|
||||
import { Table as AntdTable } from 'antd';
|
||||
|
||||
export class Table extends AntdChart {
|
||||
constructor() {
|
||||
super('table', 'Table', AntdTable);
|
||||
}
|
||||
|
||||
getProps({ data, fieldProps, general, advanced }: RenderProps) {
|
||||
const columns = data.length
|
||||
? Object.keys(data[0]).map((item) => ({
|
||||
title: fieldProps[item]?.label || item,
|
||||
dataIndex: item,
|
||||
key: item,
|
||||
}))
|
||||
: [];
|
||||
const dataSource = data.map((item: any) => {
|
||||
Object.keys(item).map((key: string) => {
|
||||
const props = fieldProps[key];
|
||||
if (props?.transformer) {
|
||||
item[key] = props.transformer(item[key]);
|
||||
}
|
||||
});
|
||||
return item;
|
||||
});
|
||||
const pageSize = advanced?.pagination?.pageSize || 10;
|
||||
return {
|
||||
bordered: true,
|
||||
size: 'middle',
|
||||
pagination:
|
||||
dataSource.length < pageSize
|
||||
? false
|
||||
: {
|
||||
pageSize,
|
||||
},
|
||||
dataSource,
|
||||
columns,
|
||||
...general,
|
||||
...advanced,
|
||||
};
|
||||
}
|
||||
}
|
134
packages/plugins/data-visualization/src/client/chart/chart.ts
Normal file
134
packages/plugins/data-visualization/src/client/chart/chart.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import { FieldOption } from '../hooks';
|
||||
import { QueryProps } from '../renderer';
|
||||
import { parseField } from '../utils';
|
||||
import { ISchema } from '@formily/react';
|
||||
|
||||
export type RenderProps = {
|
||||
data: any[];
|
||||
general: any;
|
||||
advanced: any;
|
||||
fieldProps: {
|
||||
[field: string]: FieldOption & {
|
||||
transformer: (val: any) => string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export interface ChartType {
|
||||
name: string;
|
||||
title: string;
|
||||
component: React.FC<any>;
|
||||
schema: ISchema;
|
||||
infer: (
|
||||
fields: FieldOption[],
|
||||
{
|
||||
measures,
|
||||
dimensions,
|
||||
}: {
|
||||
measures?: QueryProps['measures'];
|
||||
dimensions?: QueryProps['dimensions'];
|
||||
},
|
||||
) => {
|
||||
xField: FieldOption;
|
||||
yField: FieldOption;
|
||||
seriesField: FieldOption;
|
||||
yFields: FieldOption[];
|
||||
};
|
||||
init?: (
|
||||
fields: FieldOption[],
|
||||
query: {
|
||||
measures?: QueryProps['measures'];
|
||||
dimensions?: QueryProps['dimensions'];
|
||||
},
|
||||
) => {
|
||||
general?: any;
|
||||
advanced?: any;
|
||||
};
|
||||
/**
|
||||
* getProps
|
||||
* Accept the information that the chart component needs to render,
|
||||
* process it and return the props of the chart component.
|
||||
*/
|
||||
getProps: (props: RenderProps) => any;
|
||||
getReference?: () => {
|
||||
title: string;
|
||||
link: string;
|
||||
};
|
||||
render: (props: RenderProps) => React.FC<any>;
|
||||
}
|
||||
|
||||
export class Chart implements ChartType {
|
||||
name: string;
|
||||
title: string;
|
||||
component: React.FC<any>;
|
||||
schema = {};
|
||||
|
||||
constructor(name: string, title: string, component: React.FC<any>) {
|
||||
this.name = name;
|
||||
this.title = title;
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
infer(
|
||||
fields: FieldOption[],
|
||||
{
|
||||
measures,
|
||||
dimensions,
|
||||
}: {
|
||||
measures?: QueryProps['measures'];
|
||||
dimensions?: QueryProps['dimensions'];
|
||||
},
|
||||
) {
|
||||
let xField: FieldOption;
|
||||
let yField: FieldOption;
|
||||
let seriesField: FieldOption;
|
||||
let yFields: FieldOption[];
|
||||
const getField = (fields: FieldOption[], selected: { field: string | string[]; alias?: string }) => {
|
||||
if (selected.alias) {
|
||||
return fields.find((f) => f.value === selected.alias);
|
||||
}
|
||||
const { alias } = parseField(selected.field);
|
||||
return fields.find((f) => f.value === alias);
|
||||
};
|
||||
if (measures?.length) {
|
||||
yField = getField(fields, measures[0]);
|
||||
yFields = measures.map((m) => getField(fields, m));
|
||||
}
|
||||
if (dimensions) {
|
||||
if (dimensions.length === 1) {
|
||||
xField = getField(fields, dimensions[0]);
|
||||
} else if (dimensions.length > 1) {
|
||||
// If there is a time field, it is used as the x-axis field by default.
|
||||
let xIndex: number;
|
||||
dimensions.forEach((d, i) => {
|
||||
const field = getField(fields, d);
|
||||
if (['date', 'time', 'datetime'].includes(field?.type)) {
|
||||
xField = field;
|
||||
xIndex = i;
|
||||
}
|
||||
});
|
||||
if (xIndex) {
|
||||
// If there is a time field, the other field is used as the series field by default.
|
||||
const index = xIndex === 0 ? 1 : 0;
|
||||
seriesField = getField(fields, dimensions[index]);
|
||||
} else {
|
||||
xField = getField(fields, dimensions[0]);
|
||||
seriesField = getField(fields, dimensions[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { xField, yField, seriesField, yFields };
|
||||
}
|
||||
|
||||
getProps(props: RenderProps) {
|
||||
return props;
|
||||
}
|
||||
|
||||
render({ data, general, advanced, fieldProps }: RenderProps) {
|
||||
return () =>
|
||||
React.createElement(this.component, {
|
||||
...this.getProps({ data, general, advanced, fieldProps }),
|
||||
});
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user