Merge branch 'main' into T-557

This commit is contained in:
katherinehhh 2023-06-25 09:06:21 +08:00
commit a16be91b5b
138 changed files with 3542 additions and 2531 deletions

View File

@ -46,8 +46,8 @@ export default defineConfig({
github: 'https://github.com/nocobase/nocobase', github: 'https://github.com/nocobase/nocobase',
footer: 'nocobase | Copyright © 2022', footer: 'nocobase | Copyright © 2022',
localesEnhance: [ localesEnhance: [
{ id: 'zh-CN', switchPrefix: '中' }, { id: 'zh-CN', switchPrefix: '中', hostname: 'docs-cn.nocobase.com' },
{ id: 'en-US', switchPrefix: 'en' } { id: 'en-US', switchPrefix: 'en', hostname: 'docs.nocobase.com' }
], ],
}), }),
// mfsu: true, // 报错 // mfsu: true, // 报错

9
deploy-docs-cn.sh Executable file
View File

@ -0,0 +1,9 @@
cd docs/dist/zh-CN
echo "docs-cn.nocobase.com" >> CNAME
echo "" >> .nojekyll
git init
git remote add origin git@github.com:nocobase/docs-cn.nocobase.com.git
git branch -M main
git add .
git commit -m "first commit"
git push -f origin main

9
deploy-docs.sh Executable file
View File

@ -0,0 +1,9 @@
cd docs/dist/en-US
echo "docs.nocobase.com" >> CNAME
echo "" >> .nojekyll
git init
git remote add origin git@github.com:nocobase/docs.nocobase.com.git
git branch -M main
git add .
git commit -m "first commit"
git push -f origin main

View File

@ -285,23 +285,17 @@ const sidebar = {
}, },
], ],
}, },
'/api/cli',
'/api/actions',
'/api/sdk',
{ {
title: '@nocobase/cli', title: '@nocobase/cli',
path: '/api/cli', link: '/api/cli',
type: 'item',
}, },
{ {
title: '@nocobase/actions', title: '@nocobase/actions',
path: '/api/actions', link: '/api/actions',
type: 'item',
}, },
{ {
title: '@nocobase/sdk', title: '@nocobase/sdk',
path: '/api/sdk', link: '/api/sdk',
type: 'item',
}, },
], ],
}; };

View File

@ -1,4 +1,4 @@
# NocoBase CLI # @nocobase/cli
The NocoBase CLI is designed to help you develop, build, and deploy NocoBase applications. The NocoBase CLI is designed to help you develop, build, and deploy NocoBase applications.

View File

@ -17,11 +17,11 @@ git pull
v0.10 进行了依赖的重大升级,如果 v0.9 升级 v0.10,需要删掉以下目录之后再升级 v0.10 进行了依赖的重大升级,如果 v0.9 升级 v0.10,需要删掉以下目录之后再升级
```bash ```bash
# 删除 .umi 相关缓存 # Remove .umi cache
yarn rimraf -rf ./**/{.umi,.umi-production} yarn rimraf -rf "./**/{.umi,.umi-production}"
# 删除编译文件 # Delete compiled files
yarn rimraf -rf packages/*/*/{lib,esm,es,dist,node_modules} yarn rimraf -rf "./packages/*/*/{lib,esm,es,dist,node_modules}"
# 删除全部依赖 # Remove dependencies
yarn rimraf -rf node_modules yarn rimraf -rf node_modules
``` ```

View File

@ -44,7 +44,7 @@ No change, upgrade reference [Upgrading for Docker compose](/welcome/getting-sta
v0.10 has a major upgrade of dependencies, so to prevent errors when upgrading the source code, you need to delete the following directories before upgrading v0.10 has a major upgrade of dependencies, so to prevent errors when upgrading the source code, you need to delete the following directories before upgrading
```bash ```bash
### Remove .umi-related cache # Remove .umi cache
yarn rimraf -rf "./**/{.umi,.umi-production}" yarn rimraf -rf "./**/{.umi,.umi-production}"
# Delete compiled files # Delete compiled files
yarn rimraf -rf "./packages/*/*/{lib,esm,es,dist,node_modules}" yarn rimraf -rf "./packages/*/*/{lib,esm,es,dist,node_modules}"

View File

@ -18,9 +18,9 @@ v0.10 进行了依赖的重大升级,如果 v0.9 升级 v0.10,需要删掉
```bash ```bash
# 删除 .umi 相关缓存 # 删除 .umi 相关缓存
yarn rimraf -rf ./**/{.umi,.umi-production} yarn rimraf -rf "./**/{.umi,.umi-production}"
# 删除编译文件 # 删除编译文件
yarn rimraf -rf packages/*/*/{lib,esm,es,dist,node_modules} yarn rimraf -rf "./packages/*/*/{lib,esm,es,dist,node_modules}"
# 删除全部依赖 # 删除全部依赖
yarn rimraf -rf node_modules yarn rimraf -rf node_modules
``` ```

View File

@ -57,13 +57,13 @@
"@vitejs/plugin-react": "^4.0.0", "@vitejs/plugin-react": "^4.0.0",
"auto-changelog": "^2.4.0", "auto-changelog": "^2.4.0",
"dumi": "^2.2.0", "dumi": "^2.2.0",
"dumi-theme-nocobase": "^0.2.11", "dumi-theme-nocobase": "^0.2.12",
"ghooks": "^2.0.4", "ghooks": "^2.0.4",
"jsdom-worker": "^0.3.0", "jsdom-worker": "^0.3.0",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"pretty-format": "^24.0.0", "pretty-format": "^24.0.0",
"pretty-quick": "^3.1.0", "pretty-quick": "^3.1.0",
"vite": "^4.3.8", "vite": "^4.3.9",
"vitest": "^0.32.0" "vitest": "^0.32.0"
}, },
"volta": { "volta": {

View File

@ -9,6 +9,7 @@
}, },
"dependencies": { "dependencies": {
"@types/fs-extra": "^11.0.1", "@types/fs-extra": "^11.0.1",
"@umijs/utils": "3.5.20",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"commander": "^9.2.0", "commander": "^9.2.0",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
const CommonMemo = React.memo((props) => { const MyProvider = React.memo((props) => {
return <>{props.children}</>; return <>{props.children}</>;
}); });
CommonMemo.displayName = 'CommonMemo'; MyProvider.displayName = 'MyProvider';
export default CommonMemo; export default MyProvider;

View File

@ -7,12 +7,13 @@
"typings": "es/index.d.ts", "typings": "es/index.d.ts",
"dependencies": { "dependencies": {
"@antv/g2plot": "^2.4.18", "@antv/g2plot": "^2.4.18",
"@ant-design/pro-layout": "^7.14.3",
"@dnd-kit/core": "^5.0.1", "@dnd-kit/core": "^5.0.1",
"@dnd-kit/sortable": "^6.0.0", "@dnd-kit/sortable": "^6.0.0",
"@emotion/css": "^11.7.1", "@emotion/css": "^11.7.1",
"@formily/antd": "2.2.24", "@formily/antd": "2.2.26",
"@formily/core": "2.2.24", "@formily/core": "2.2.26",
"@formily/react": "2.2.24", "@formily/react": "2.2.26",
"@nocobase/evaluators": "0.10.0-alpha.2", "@nocobase/evaluators": "0.10.0-alpha.2",
"@nocobase/sdk": "0.10.0-alpha.2", "@nocobase/sdk": "0.10.0-alpha.2",
"@nocobase/utils": "0.10.0-alpha.2", "@nocobase/utils": "0.10.0-alpha.2",

View File

@ -1,7 +1,7 @@
import { Spin } from 'antd'; import { Spin } from 'antd';
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import { useRequest, useAPIClient } from '../../api-client'; import { useRequest } from '../../api-client';
import { useRoute } from '../../route-switch'; import { useAdminSchemaUid } from '../../hooks';
const MenuItemsContext = createContext(null); const MenuItemsContext = createContext(null);
@ -28,9 +28,9 @@ export const useMenuItems = () => {
}; };
export const MenuItemsProvider = (props) => { export const MenuItemsProvider = (props) => {
const route = useRoute(); const adminSchemaUid = useAdminSchemaUid();
const options = { const options = {
url: `uiSchemas:getProperties/${route.uiSchemaUid}`, url: `uiSchemas:getProperties/${adminSchemaUid}`,
}; };
const service = useRequest(options); const service = useRequest(options);
if (service.loading) { if (service.loading) {

View File

@ -7,6 +7,8 @@ import { Link, NavLink } from 'react-router-dom';
import { ACLProvider } from '../acl'; import { ACLProvider } from '../acl';
import { AntdConfigProvider } from '../antd-config-provider'; import { AntdConfigProvider } from '../antd-config-provider';
import { APIClient, APIClientProvider } from '../api-client'; import { APIClient, APIClientProvider } from '../api-client';
import { SigninPage, SignupPage } from '../auth';
import { SigninPageExtensionProvider } from '../auth/SigninPageExtension';
import { BlockSchemaComponentProvider } from '../block-provider'; import { BlockSchemaComponentProvider } from '../block-provider';
import { RemoteDocumentTitleProvider } from '../document-title'; import { RemoteDocumentTitleProvider } from '../document-title';
import { i18n } from '../i18n'; import { i18n } from '../i18n';
@ -30,8 +32,6 @@ import { ErrorFallback } from '../schema-component/antd/error-fallback';
import { SchemaInitializerProvider } from '../schema-initializer'; import { SchemaInitializerProvider } from '../schema-initializer';
import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates'; import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates';
import { SystemSettingsProvider } from '../system-settings'; import { SystemSettingsProvider } from '../system-settings';
import { SigninPage, SignupPage } from '../auth';
import { SigninPageExtensionProvider } from '../auth/SigninPageExtension';
import { compose } from './compose'; import { compose } from './compose';
export interface ApplicationOptions { export interface ApplicationOptions {
@ -52,9 +52,7 @@ export type PluginCallback = () => Promise<any>;
const App = React.memo((props: any) => { const App = React.memo((props: any) => {
const C = compose(...props.providers)(() => { const C = compose(...props.providers)(() => {
const routes = useRoutes(); const routes = useRoutes();
return ( return <RouteSwitch routes={routes} />;
<RouteSwitch routes={routes} />
);
}); });
return <C />; return <C />;
}); });

View File

@ -1,19 +1,19 @@
import { css } from '@emotion/css';
import { useForm } from '@formily/react';
import { Space, Tabs } from 'antd'; import { Space, Tabs } from 'antd';
import React, { import React, {
FunctionComponent,
FunctionComponentElement,
createContext,
createElement,
useCallback, useCallback,
useContext, useContext,
createContext,
FunctionComponent,
createElement,
useState, useState,
FunctionComponentElement,
} from 'react'; } from 'react';
import { css } from '@emotion/css';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAPIClient, useCurrentDocumentTitle, useRequest, useViewport } from '..'; import { useAPIClient, useCurrentDocumentTitle, useRequest, useViewport } from '..';
import { useSigninPageExtension } from './SigninPageExtension'; import { useSigninPageExtension } from './SigninPageExtension';
import { useForm } from '@formily/react';
const SigninPageContext = createContext<{ const SigninPageContext = createContext<{
[authType: string]: { [authType: string]: {
@ -132,13 +132,7 @@ export const SigninPage = () => {
`} `}
> >
{tabs.length > 1 ? ( {tabs.length > 1 ? (
<Tabs> <Tabs items={tabs.map((tab) => ({ label: tab.tabTitle, key: tab.name, children: tab.component }))} />
{tabs.map((tab) => (
<Tabs.TabPane tab={tab.tabTitle} key={tab.name}>
{tab.component}
</Tabs.TabPane>
))}
</Tabs>
) : tabs.length ? ( ) : tabs.length ? (
<div>{tabs[0].component}</div> <div>{tabs[0].component}</div>
) : ( ) : (

View File

@ -2,9 +2,9 @@ import { DownOutlined, PlusOutlined } from '@ant-design/icons';
import { ArrayTable } from '@formily/antd'; import { ArrayTable } from '@formily/antd';
import { ISchema, useField, useForm } from '@formily/react'; import { ISchema, useField, useForm } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { Button, Dropdown, Menu } from 'antd'; import { Button, Dropdown, MenuProps } from 'antd';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import React, { useState } from 'react'; import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRequest } from '../../api-client'; import { useRequest } from '../../api-client';
import { RecordProvider, useRecord } from '../../record-provider'; import { RecordProvider, useRecord } from '../../record-provider';
@ -246,41 +246,41 @@ export const AddCollectionAction = (props) => {
const [schema, setSchema] = useState({}); const [schema, setSchema] = useState({});
const compile = useCompile(); const compile = useCompile();
const { t } = useTranslation(); const { t } = useTranslation();
const collectionTemplates = templateOptions(); const collectionTemplates = useMemo(templateOptions, []);
const items = []; const items = useMemo(() => {
collectionTemplates.forEach((item) => { const result = [];
if (item.divider) { collectionTemplates.forEach((item) => {
items.push({ if (item.divider) {
type: 'divider', result.push({
}); type: 'divider',
} });
items.push({ label: compile(item.title), key: item.name }); }
}); result.push({ label: compile(item.title), key: item.name });
});
return result;
}, [collectionTemplates]);
const { const {
state: { category }, state: { category },
} = useResourceActionContext(); } = useResourceActionContext();
const menu = useMemo<MenuProps>(() => {
return {
style: {
maxHeight: '60vh',
overflow: 'auto',
},
onClick: (info) => {
const schema = getSchema(getTemplate(info.key), category, compile);
setSchema(schema);
setVisible(true);
},
items,
};
}, [category, items]);
return ( return (
<RecordProvider record={record}> <RecordProvider record={record}>
<ActionContextProvider value={{ visible, setVisible }}> <ActionContextProvider value={{ visible, setVisible }}>
<Dropdown <Dropdown getPopupContainer={getContainer} trigger={trigger} align={align} menu={menu}>
getPopupContainer={getContainer}
trigger={trigger}
align={align}
overlay={
<Menu
style={{
maxHeight: '60vh',
overflow: 'auto',
}}
onClick={(info) => {
const schema = getSchema(getTemplate(info.key), category, compile);
setSchema(schema);
setVisible(true);
}}
items={items}
/>
}
>
{children || ( {children || (
<Button icon={<PlusOutlined />} type={'primary'}> <Button icon={<PlusOutlined />} type={'primary'}>
{t('Create collection')} <DownOutlined /> {t('Create collection')} <DownOutlined />

View File

@ -2,9 +2,9 @@ import { PlusOutlined } from '@ant-design/icons';
import { ArrayTable } from '@formily/antd'; import { ArrayTable } from '@formily/antd';
import { useField, useForm } from '@formily/react'; import { useField, useForm } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { Button, Dropdown, Menu } from 'antd'; import { Button, Dropdown, MenuProps } from 'antd';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import React, { useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRequest } from '../../api-client'; import { useRequest } from '../../api-client';
import { RecordProvider, useRecord } from '../../record-provider'; import { RecordProvider, useRecord } from '../../record-provider';
@ -12,7 +12,6 @@ import { ActionContextProvider, SchemaComponent, useActionContext, useCompile }
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import { useCancelAction } from '../action-hooks'; import { useCancelAction } from '../action-hooks';
import { useCollectionManager } from '../hooks'; import { useCollectionManager } from '../hooks';
import { useOptions } from '../hooks/useOptions';
import { IField } from '../interfaces/types'; import { IField } from '../interfaces/types';
import * as components from './components'; import * as components from './components';
import { getOptions } from './interfaces'; import { getOptions } from './interfaces';
@ -175,8 +174,7 @@ export const AddFieldAction = (props) => {
const [schema, setSchema] = useState({}); const [schema, setSchema] = useState({});
const compile = useCompile(); const compile = useCompile();
const { t } = useTranslation(); const { t } = useTranslation();
const options = useOptions(); const getFieldOptions = useCallback(() => {
const getFieldOptions = () => {
const { availableFieldInterfaces } = getTemplate(record.template) || {}; const { availableFieldInterfaces } = getTemplate(record.template) || {};
const { exclude, include } = availableFieldInterfaces || {}; const { exclude, include } = availableFieldInterfaces || {};
const optionArr = []; const optionArr = [];
@ -218,52 +216,57 @@ export const AddFieldAction = (props) => {
} }
}); });
return optionArr; return optionArr;
}; }, [getTemplate, record]);
const items = useMemo<MenuProps['items']>(() => {
return getFieldOptions().map((option) => {
if (option.children.length === 0) {
return null;
}
return {
type: 'group',
label: compile(option.label),
title: compile(option.label),
key: option.label,
children: option.children
.filter((child) => !['o2o', 'subTable', 'linkTo'].includes(child.name))
.map((child) => {
return {
label: compile(child.title),
title: compile(child.title),
key: child.name,
dataTargetScope: child.targetScope,
};
}),
};
});
}, [getFieldOptions]);
const menu = useMemo<MenuProps>(() => {
return {
style: {
maxHeight: '60vh',
overflow: 'auto',
},
onClick: (e) => {
//@ts-ignore
const targetScope = e.item.props['data-targetScope'];
targetScope && setTargetScope(targetScope);
const schema = getSchema(getInterface(e.key), record, compile);
if (schema) {
setSchema(schema);
setVisible(true);
}
},
items,
};
}, [getInterface, items, record]);
return ( return (
record.template !== 'view' && ( record.template !== 'view' && (
<RecordProvider record={record}> <RecordProvider record={record}>
<ActionContextProvider value={{ visible, setVisible }}> <ActionContextProvider value={{ visible, setVisible }}>
<Dropdown <Dropdown getPopupContainer={getContainer} trigger={trigger} align={align} menu={menu}>
getPopupContainer={getContainer}
trigger={trigger}
align={align}
overlay={
<Menu
style={{
maxHeight: '60vh',
overflow: 'auto',
}}
onClick={(e) => {
//@ts-ignore
const targetScope = e.item.props['data-targetScope'];
targetScope && setTargetScope(targetScope);
const schema = getSchema(getInterface(e.key), record, compile);
if (schema) {
setSchema(schema);
setVisible(true);
}
}}
>
{getFieldOptions().map((option) => {
return (
option.children.length > 0 && (
<Menu.ItemGroup key={option.label} title={compile(option.label)}>
{option.children
.filter((child) => !['o2o', 'subTable', 'linkTo'].includes(child.name))
.map((child) => {
return (
<Menu.Item key={child.name} data-targetScope={child.targetScope}>
{compile(child.title)}
</Menu.Item>
);
})}
</Menu.ItemGroup>
)
);
})}
</Menu>
}
>
{children || ( {children || (
<Button icon={<PlusOutlined />} type={'primary'}> <Button icon={<PlusOutlined />} type={'primary'}>
{t('Add field')} {t('Add field')}

View File

@ -2,9 +2,9 @@ import { PlusOutlined } from '@ant-design/icons';
import { ArrayTable } from '@formily/antd'; import { ArrayTable } from '@formily/antd';
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { Button, Dropdown, Menu } from 'antd'; import { Button, Dropdown, MenuProps } from 'antd';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import React, { useState } from 'react'; import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRequest } from '../../api-client'; import { useRequest } from '../../api-client';
import { RecordProvider } from '../../record-provider'; import { RecordProvider } from '../../record-provider';
@ -101,34 +101,36 @@ export const AddSubFieldAction = () => {
const compile = useCompile(); const compile = useCompile();
const options = useOptions(); const options = useOptions();
const { t } = useTranslation(); const { t } = useTranslation();
const items = options.map((option) => { const items = useMemo(() => {
const children = option.children.map((child) => { return options.map((option) => {
return { label: compile(child.title), key: child.name }; const children = option.children.map((child) => {
return { label: compile(child.title), key: child.name };
});
return {
label: compile(option.label),
key: option.key,
children,
};
}); });
}, [options]);
const menu = useMemo<MenuProps>(() => {
return { return {
label: compile(option.label), style: {
key: option.key, maxHeight: '60vh',
children, overflow: 'auto',
},
onClick: (info) => {
const schema = getSchema(getInterface(info.key));
setSchema(schema);
setVisible(true);
},
items,
}; };
}); }, [items]);
return ( return (
<ActionContextProvider value={{ visible, setVisible }}> <ActionContextProvider value={{ visible, setVisible }}>
<Dropdown <Dropdown menu={menu}>
overlay={
<Menu
style={{
maxHeight: '60vh',
overflow: 'auto',
}}
onClick={(info) => {
const schema = getSchema(getInterface(info.key));
setSchema(schema);
setVisible(true);
}}
items={items}
/>
}
>
<Button icon={<PlusOutlined />} type={'primary'}> <Button icon={<PlusOutlined />} type={'primary'}>
{t('Add field')} {t('Add field')}
</Button> </Button>

View File

@ -11,7 +11,8 @@ import {
} from '@dnd-kit/core'; } from '@dnd-kit/core';
import { RecursionField, observer } from '@formily/react'; import { RecursionField, observer } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { Badge, Card, Dropdown, Menu, Modal, Tabs } from 'antd'; import { Badge, Card, Dropdown, Modal, Tabs } from 'antd';
import _ from 'lodash';
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { useAPIClient } from '../../api-client'; import { useAPIClient } from '../../api-client';
import { SchemaComponent, SchemaComponentOptions, useCompile } from '../../schema-component'; import { SchemaComponent, SchemaComponentOptions, useCompile } from '../../schema-component';
@ -181,28 +182,37 @@ export const ConfigurationTabs = () => {
value: item.id, value: item.id,
})); }));
}; };
const menu = (item) => (
<Menu> const menu = _.memoize((item) => {
<Menu.Item key={'edit'}> return {
<SchemaComponent items: [
schema={{ {
type: 'void', key: 'edit',
properties: { label: (
[uid()]: { <SchemaComponent
'x-component': 'EditCategory', schema={{
'x-component-props': { type: 'void',
item: item, properties: {
[uid()]: {
'x-component': 'EditCategory',
'x-component-props': {
item: item,
},
},
}, },
}, }}
}, />
}} ),
/> },
</Menu.Item> {
<Menu.Item key="delete" onClick={() => remove(item.id)}> key: 'delete',
{compile("{{t('Delete category')}}")} label: compile("{{t('Delete category')}}"),
</Menu.Item> onClick: () => remove(item.id),
</Menu> },
); ],
};
});
return ( return (
<DndProvider> <DndProvider>
<Tabs <Tabs
@ -228,27 +238,24 @@ export const ConfigurationTabs = () => {
type="editable-card" type="editable-card"
destroyInactiveTabPane={true} destroyInactiveTabPane={true}
tabBarStyle={{ marginBottom: '0px' }} tabBarStyle={{ marginBottom: '0px' }}
> items={tabsItems.map((item) => {
{tabsItems.map((item) => { return {
return ( label:
<Tabs.TabPane item.id !== 'all' ? (
tab={ <div data-no-dnd="true">
item.id !== 'all' ? ( <TabTitle item={item} />
<div data-no-dnd="true"> </div>
<TabTitle item={item} /> ) : (
</div> compile(item.name)
) : ( ),
compile(item.name) key: item.id,
) closable: item.closable,
} closeIcon: (
key={item.id} <Dropdown menu={menu(item)}>
closable={item.closable} <MenuOutlined />
closeIcon={ </Dropdown>
<Dropdown overlay={menu(item)}> ),
<MenuOutlined /> children: (
</Dropdown>
}
>
<Card bordered={false}> <Card bordered={false}>
<SchemaComponentOptions <SchemaComponentOptions
components={{ CollectionFields }} components={{ CollectionFields }}
@ -258,10 +265,10 @@ export const ConfigurationTabs = () => {
<RecursionField name={key} schema={item.schema} onlyRenderProperties /> <RecursionField name={key} schema={item.schema} onlyRenderProperties />
</SchemaComponentOptions> </SchemaComponentOptions>
</Card> </Card>
</Tabs.TabPane> ),
); };
})} })}
</Tabs> />
</DndProvider> </DndProvider>
); );
}; };

View File

@ -82,7 +82,7 @@ const FormItemInitializer = (props) => {
collection.fields.push(options); collection.fields.push(options);
form.setValuesIn(name, uid()); form.setValuesIn(name, uid());
const { values } = await FormDrawer('Add field', () => { await FormDrawer('Add field', () => {
return ( return (
<CollectionManagerContext.Provider value={cm}> <CollectionManagerContext.Provider value={cm}>
<AntdSchemaComponentProvider> <AntdSchemaComponentProvider>

View File

@ -1,45 +1,48 @@
import set from 'lodash/set'; import set from 'lodash/set';
import { useMemo } from 'react';
import { useCollectionManager } from './useCollectionManager'; import { useCollectionManager } from './useCollectionManager';
export const useOptions = () => { export const useOptions = () => {
const { interfaces } = useCollectionManager(); const { interfaces } = useCollectionManager();
const fields = {}; return useMemo(() => {
const fields = {};
Object.keys(interfaces).forEach((type) => { Object.keys(interfaces).forEach((type) => {
const schema = interfaces[type]; const schema = interfaces[type];
registerField(schema.group || 'others', type, { order: 0, ...schema }); registerField(schema.group || 'others', type, { order: 0, ...schema });
}); });
function registerField(group: string, type: string, schema) { function registerField(group: string, type: string, schema) {
fields[group] = fields[group] || {}; fields[group] = fields[group] || {};
set(fields, [group, type], schema); set(fields, [group, type], schema);
} }
const groupLabels = { const groupLabels = {
basic: '{{t("Basic")}}', basic: '{{t("Basic")}}',
choices: '{{t("Choices")}}', choices: '{{t("Choices")}}',
media: '{{t("Media")}}', media: '{{t("Media")}}',
datetime: '{{t("Date & Time")}}', datetime: '{{t("Date & Time")}}',
relation: '{{t("Relation")}}', relation: '{{t("Relation")}}',
advanced: '{{t("Advanced type")}}', advanced: '{{t("Advanced type")}}',
systemInfo: '{{t("System info")}}', systemInfo: '{{t("System info")}}',
others: '{{t("Others")}}', others: '{{t("Others")}}',
}; };
return Object.keys(groupLabels).map((groupName) => ({ return Object.keys(groupLabels).map((groupName) => ({
label: groupLabels[groupName], label: groupLabels[groupName],
key: groupName, key: groupName,
children: Object.keys(fields[groupName] || {}) children: Object.keys(fields[groupName] || {})
.map((type) => { .map((type) => {
const field = fields[groupName][type]; const field = fields[groupName][type];
return { return {
value: type, value: type,
label: field.title, label: field.title,
name: type, name: type,
...fields[groupName][type], ...fields[groupName][type],
}; };
}) })
.sort((a, b) => a.order - b.order), .sort((a, b) => a.order - b.order),
})); }));
}, [interfaces]);
}; };

View File

@ -24,7 +24,7 @@ export const integer: IField = {
'x-component': 'InputNumber', 'x-component': 'InputNumber',
'x-component-props': { 'x-component-props': {
stringMode: true, stringMode: true,
step: '0', step: '1',
}, },
'x-validator': 'integer', 'x-validator': 'integer',
}, },

View File

@ -18,7 +18,7 @@ export const number: IField = {
'x-component': 'InputNumber', 'x-component': 'InputNumber',
'x-component-props': { 'x-component-props': {
stringMode: true, stringMode: true,
step: '0', step: '1',
}, },
}, },
}, },
@ -32,9 +32,9 @@ export const number: IField = {
title: '{{t("Precision")}}', title: '{{t("Precision")}}',
'x-component': 'Select', 'x-component': 'Select',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
default: '0', default: '1',
enum: [ enum: [
{ value: '0', label: '1' }, { value: '1', label: '1' },
{ value: '0.1', label: '1.0' }, { value: '0.1', label: '1.0' },
{ value: '0.01', label: '1.00' }, { value: '0.01', label: '1.00' },
{ value: '0.001', label: '1.000' }, { value: '0.001', label: '1.000' },

View File

@ -63,7 +63,7 @@ export const percent: IField = {
'x-component': 'Percent', 'x-component': 'Percent',
'x-component-props': { 'x-component-props': {
stringMode: true, stringMode: true,
step: '0', step: '1',
addonAfter: '%', addonAfter: '%',
}, },
}, },
@ -85,9 +85,9 @@ export const percent: IField = {
title: '{{t("Precision")}}', title: '{{t("Precision")}}',
'x-component': 'Select', 'x-component': 'Select',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
default: '0', default: '1',
enum: [ enum: [
{ value: '0', label: '1%' }, { value: '1', label: '1%' },
{ value: '0.1', label: '1.0%' }, { value: '0.1', label: '1.0%' },
{ value: '0.01', label: '1.00%' }, { value: '0.01', label: '1.00%' },
{ value: '0.001', label: '1.000%' }, { value: '0.001', label: '1.000%' },

View File

@ -1,8 +1,8 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Field, onFormSubmitValidateStart } from '@formily/core'; import { Field, onFormSubmitValidateStart } from '@formily/core';
import { useField, useFormEffects } from '@formily/react'; import { useField, useFormEffects } from '@formily/react';
import { Dropdown, Menu } from 'antd'; import { Dropdown, MenuProps } from 'antd';
import React, { useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
function pasteHtml(html, selectPastedContent = false) { function pasteHtml(html, selectPastedContent = false) {
@ -71,19 +71,27 @@ export const Expression = (props) => {
const inputRef = useRef<any>(); const inputRef = useRef<any>();
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
const onChange = (value) => { const onChange = useCallback(
setChanged(true); (value) => {
props.onChange(value); setChanged(true);
}; props.onChange(value);
},
[props.onChange],
);
const { numColumns, scope } = useMemo(() => {
const numColumns = new Map<string, string>();
const scope = {};
fields
.filter((field) => supports.includes(field.interface))
.forEach((field) => {
numColumns.set(field.name, field.uiSchema.title);
scope[field.name] = 1;
});
return { numColumns, scope };
}, [fields, supports]);
const numColumns = new Map<string, string>();
const scope = {};
fields
.filter((field) => supports.includes(field.interface))
.forEach((field) => {
numColumns.set(field.name, field.uiSchema.title);
scope[field.name] = 1;
});
const keys = Array.from(numColumns.keys()); const keys = Array.from(numColumns.keys());
const [html, setHtml] = useState(() => { const [html, setHtml] = useState(() => {
const scope = {}; const scope = {};
@ -95,6 +103,7 @@ export const Expression = (props) => {
} }
return renderExp(value || '', scope); return renderExp(value || '', scope);
}); });
useEffect(() => { useEffect(() => {
if (changed) { if (changed) {
return; return;
@ -109,34 +118,44 @@ export const Expression = (props) => {
const val = renderExp(value || '', scope); const val = renderExp(value || '', scope);
setHtml(val); setHtml(val);
}, [value]); }, [value]);
const menu = (
<Menu> const menuItems = useMemo<MenuProps['items']>(() => {
{keys.length > 0 ? ( if (keys.length > 0) {
keys.map((key) => ( return keys.map((key) => ({
<Menu.Item disabled key={key}> key,
<button disabled: true,
onClick={async (args) => { label: (
(inputRef.current as any).focus(); <button
const val = numColumns.get(key); onClick={async (args) => {
pasteHtml( (inputRef.current as any).focus();
` <span class="ant-tag" style="margin: 0 3px;" contentEditable="false" data-key="${key}">${val}</span> `, const val = numColumns.get(key);
); pasteHtml(
const text = getValue(inputRef.current); ` <span class="ant-tag" style="margin: 0 3px;" contentEditable="false" data-key="${key}">${val}</span> `,
onChange(text); );
console.log('onChange', text); const text = getValue(inputRef.current);
}} onChange(text);
> }}
{numColumns.get(key)} >
</button> {numColumns.get(key)}
</Menu.Item> </button>
)) ),
) : ( }));
<Menu.Item disabled key={0}> } else {
{t('No available fields')} return [
</Menu.Item> {
)} key: 0,
</Menu> disabled: true,
); label: t('No available fields'),
},
];
}
}, [keys, numColumns, onChange]);
const menu = useMemo<MenuProps>(() => {
return {
items: menuItems,
};
}, [menuItems]);
useFormEffects(() => { useFormEffects(() => {
onFormSubmitValidateStart(() => { onFormSubmitValidateStart(() => {
@ -157,7 +176,7 @@ export const Expression = (props) => {
return ( return (
<Dropdown <Dropdown
trigger={['click']} trigger={['click']}
overlay={menu} menu={menu}
overlayClassName={css` overlayClassName={css`
.ant-dropdown-menu-item { .ant-dropdown-menu-item {
padding: 0; padding: 0;

View File

@ -1 +1,2 @@
export * from './useAdminSchemaUid';
export * from './useViewport'; export * from './useViewport';

View File

@ -0,0 +1,6 @@
import { useSystemSettings } from '../system-settings';
export const useAdminSchemaUid = () => {
const ctx = useSystemSettings();
return ctx?.data?.data?.options?.adminSchemaUid;
};

View File

@ -0,0 +1,102 @@
import { MenuProps } from 'antd';
import React, { ReactNode, createContext, useCallback, useContext, useRef } from 'react';
type Item = MenuProps['items'][0] & {
/** 在清空数组时,如果该字段为 true 则保留该选项 */
notdelete?: boolean;
/** 用于给列表排序 */
order?: number;
};
export const GetMenuItemContext = createContext<{ collectMenuItem?(item: Item): void; onChange?: () => void }>(null);
export const GetMenuItemsContext = createContext<{ pushMenuItem?(item: Item): void }>(null);
/**
* SchemaInitializer.Item
* @returns
*/
export const useCollectMenuItem = () => {
return useContext(GetMenuItemContext) || {};
};
export const useCollectMenuItems = () => {
return useContext(GetMenuItemsContext) || {};
};
/**
* antd 4.x 5.x SchemaInitializer.Item Menu items
* @returns
*/
export const useMenuItem = () => {
const list = useRef<any[]>([]);
const renderItems = useRef<() => JSX.Element>(null);
const shouldRerender = useRef(false);
const Component = useCallback(() => {
if (!shouldRerender.current) {
return null;
}
shouldRerender.current = false;
if (renderItems.current) {
return renderItems.current();
}
return (
<>
{list.current.map((Com, index) => (
<Com key={index} />
))}
</>
);
}, []);
const getMenuItems = useCallback((Com: () => ReactNode): Item[] => {
const items: Item[] = [];
const pushMenuItem = (item: Item) => {
items.push(item);
items.sort((a, b) => (a.order || 0) - (b.order || 0));
};
shouldRerender.current = true;
renderItems.current = () => {
const notDeleteItems = items.filter((item) => item.notdelete);
items.length = 0;
items.push(...notDeleteItems);
return (
<GetMenuItemsContext.Provider
value={{
pushMenuItem,
}}
>
{Com()}
</GetMenuItemsContext.Provider>
);
};
return items;
}, []);
const getMenuItem = useCallback((Com: () => JSX.Element): Item => {
const item = {} as Item;
const collectMenuItem = (menuItem: Item) => {
Object.assign(item, menuItem);
};
shouldRerender.current = true;
list.current.push(() => {
return <GetMenuItemContext.Provider value={{ collectMenuItem }}>{Com()}</GetMenuItemContext.Provider>;
});
return item;
}, []);
// 防止 list 有重复元素
const clean = useCallback(() => {
list.current = [];
}, []);
return { Component, getMenuItems, getMenuItem, clean };
};

View File

@ -14,6 +14,7 @@ export * from './collection-manager';
export * from './document-title'; export * from './document-title';
export * from './filter-provider'; export * from './filter-provider';
export * from './formula'; export * from './formula';
export * from './hooks';
export * from './i18n'; export * from './i18n';
export * from './icon'; export * from './icon';
export * from './plugin-manager'; export * from './plugin-manager';
@ -23,10 +24,9 @@ export * from './record-provider';
export * from './route-switch'; export * from './route-switch';
export * from './schema-component'; export * from './schema-component';
export * from './schema-initializer'; export * from './schema-initializer';
export * from './schema-items';
export * from './schema-settings'; export * from './schema-settings';
export * from './schema-templates'; export * from './schema-templates';
export * from './schema-items';
export * from './settings-form';
export * from './system-settings'; export * from './system-settings';
export * from './user'; export * from './user';
export * from './hooks';

View File

@ -703,5 +703,7 @@ export default {
"First or create":"First or create", "First or create":"First or create",
"Update or create":"Update or create", "Update or create":"Update or create",
"Find by the following fields":"Find by the following fields", "Find by the following fields":"Find by the following fields",
"Create":"Create" "Create":"Create",
"Current form": "Current form",
"Current object":"Current object"
}; };

View File

@ -614,5 +614,7 @@ export default {
"First or create":"存在しない場合に追加", "First or create":"存在しない場合に追加",
"Update or create":"存在しなければ新規、存在すれば更新", "Update or create":"存在しなければ新規、存在すれば更新",
"Find by the following fields":"次のフィールドで検索", "Find by the following fields":"次のフィールドで検索",
"Create":"新規のみ" "Create":"新規のみ" ,
"Current form":"現在のフォーム",
"Current object":"現在のオブジェクト"
} }

View File

@ -782,6 +782,8 @@ export default {
"Update or create":"不存在时新增,存在时更新", "Update or create":"不存在时新增,存在时更新",
"Find by the following fields":"通过以下字段查找", "Find by the following fields":"通过以下字段查找",
"Create":"仅新增", "Create":"仅新增",
"Current form":"当前表单",
"Current object":"当前对象",
"Quick create": "快速创建", "Quick create": "快速创建",
"Dropdown": "下拉菜单", "Dropdown": "下拉菜单",
"Pop-up": "弹窗", "Pop-up": "弹窗",

View File

@ -1,9 +1,8 @@
import React, { useEffect, useMemo, useState, useCallback, MouseEventHandler } from 'react'; import { DeleteOutlined, SettingOutlined } from '@ant-design/icons';
import { useAPIClient, useRequest } from '../api-client'; import { css } from '@emotion/css';
import { import {
Avatar, Avatar,
Card, Card,
message,
Modal, Modal,
Popconfirm, Popconfirm,
Spin, Spin,
@ -13,14 +12,15 @@ import {
Tag, Tag,
Tooltip, Tooltip,
Typography, Typography,
message,
} from 'antd'; } from 'antd';
import { css } from '@emotion/css';
import cls from 'classnames'; import cls from 'classnames';
import { useNavigate } from 'react-router-dom'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom';
import { useParseMarkdown } from '../schema-component/antd/markdown/util';
import type { IPluginData } from '.'; import type { IPluginData } from '.';
import { useAPIClient, useRequest } from '../api-client';
import { useParseMarkdown } from '../schema-component/antd/markdown/util';
interface PluginDocumentProps { interface PluginDocumentProps {
path: string; path: string;
@ -163,7 +163,7 @@ function PluginDetail(props: IPluginDetail) {
destroyOnClose destroyOnClose
> >
{plugin?.description && <div className={'plugin-desc'}>{plugin?.description}</div>} {plugin?.description && <div className={'plugin-desc'}>{plugin?.description}</div>}
<Tabs items={items}></Tabs> <Tabs items={items} />
</Modal> </Modal>
); );
} }

View File

@ -1,11 +1,12 @@
import { ApiOutlined, SettingOutlined } from '@ant-design/icons'; import { ApiOutlined, SettingOutlined } from '@ant-design/icons';
import { Button, Dropdown, Menu, Tooltip } from 'antd'; import { Button, Dropdown, MenuProps, Tooltip } from 'antd';
import React, { useContext, useState } from 'react'; import _ from 'lodash';
import React, { useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useACLRoleContext } from '../acl/ACLProvider'; import { useACLRoleContext } from '../acl/ACLProvider';
import { ActionContextProvider, useCompile } from '../schema-component'; import { ActionContextProvider, useCompile } from '../schema-component';
import { getPluginsTabs, SettingsCenterContext } from './index'; import { SettingsCenterContext, getPluginsTabs } from './index';
export const PluginManagerLink = () => { export const PluginManagerLink = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -23,7 +24,7 @@ export const PluginManagerLink = () => {
); );
}; };
const getBookmarkTabs = (data) => { const getBookmarkTabs = _.memoize((data) => {
const bookmarkTabs = []; const bookmarkTabs = [];
data.forEach((plugin) => { data.forEach((plugin) => {
const tabs = plugin.tabs; const tabs = plugin.tabs;
@ -32,7 +33,7 @@ const getBookmarkTabs = (data) => {
}); });
}); });
return bookmarkTabs; return bookmarkTabs;
}; });
export const SettingsCenterDropdown = () => { export const SettingsCenterDropdown = () => {
const { snippets = [] } = useACLRoleContext(); const { snippets = [] } = useACLRoleContext();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
@ -42,27 +43,28 @@ export const SettingsCenterDropdown = () => {
const itemData = useContext(SettingsCenterContext); const itemData = useContext(SettingsCenterContext);
const pluginsTabs = getPluginsTabs(itemData, snippets); const pluginsTabs = getPluginsTabs(itemData, snippets);
const bookmarkTabs = getBookmarkTabs(pluginsTabs); const bookmarkTabs = getBookmarkTabs(pluginsTabs);
const menu = useMemo<MenuProps>(() => {
return {
items: [
...bookmarkTabs.map((tab) => ({
key: `/admin/settings/${tab.path}`,
label: compile(tab.title),
})),
{ type: 'divider' },
{
key: '/admin/settings',
label: t('All plugin settings'),
},
],
onClick({ key }) {
navigate(key);
},
};
}, [bookmarkTabs]);
return ( return (
<ActionContextProvider value={{ visible, setVisible }}> <ActionContextProvider value={{ visible, setVisible }}>
<Dropdown <Dropdown placement="bottom" menu={menu}>
placement="bottom"
menu={{
items: [
...bookmarkTabs.map((tab) => ({
key: `/admin/settings/${tab.path}`,
label: compile(tab.title),
})),
{ type: 'divider' },
{
key: '/admin/settings',
label: t('All plugin settings'),
},
],
onClick({ key }) {
navigate(key);
},
}}
>
<Button <Button
icon={<SettingOutlined />} icon={<SettingOutlined />}
// title={t('All plugin settings')} // title={t('All plugin settings')}

View File

@ -1,6 +1,7 @@
import { PageHeader } from '@ant-design/pro-layout';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Layout, Menu, PageHeader, Result, Spin, Tabs } from 'antd'; import { Layout, Menu, Result, Spin, Tabs } from 'antd';
import { sortBy } from 'lodash'; import _, { sortBy } from 'lodash';
import React, { createContext, useContext, useEffect, useMemo } from 'react'; import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Navigate, useNavigate, useParams } from 'react-router-dom'; import { Navigate, useNavigate, useParams } from 'react-router-dom';
@ -126,6 +127,10 @@ const PluginList = (props) => {
return snippets.includes('pm') ? ( return snippets.includes('pm') ? (
<div> <div>
<PageHeader <PageHeader
style={{
backgroundColor: 'white',
paddingBottom: 0,
}}
ghost={false} ghost={false}
title={t('Plugin manager')} title={t('Plugin manager')}
footer={ footer={
@ -134,11 +139,21 @@ const PluginList = (props) => {
onChange={(activeKey) => { onChange={(activeKey) => {
navigate(`/admin/pm/list/${activeKey}`); navigate(`/admin/pm/list/${activeKey}`);
}} }}
> items={[
<Tabs.TabPane tab={t('Local')} key={'local'} /> {
<Tabs.TabPane tab={t('Built-in')} key={'built-in'} /> key: 'local',
<Tabs.TabPane tab={t('Marketplace')} key={'marketplace'} /> label: t('Local'),
</Tabs> },
{
key: 'built-in',
label: t('Built-in'),
},
{
key: 'marketplace',
label: t('Marketplace'),
},
]}
/>
} }
/> />
<div className={'m24'} style={{ margin: 24, display: 'flex', flexFlow: 'row wrap' }}> <div className={'m24'} style={{ margin: 24, display: 'flex', flexFlow: 'row wrap' }}>
@ -202,7 +217,7 @@ const settings = {
}, },
}; };
export const getPluginsTabs = (items, snippets) => { export const getPluginsTabs = _.memoize((items, snippets) => {
const pluginsTabs = Object.keys(items).map((plugin) => { const pluginsTabs = Object.keys(items).map((plugin) => {
const tabsObj = items[plugin].tabs; const tabsObj = items[plugin].tabs;
const tabs = sortBy( const tabs = sortBy(
@ -223,7 +238,7 @@ export const getPluginsTabs = (items, snippets) => {
}; };
}); });
return sortBy(pluginsTabs, (o) => !o.isAllow); return sortBy(pluginsTabs, (o) => !o.isAllow);
}; });
const SettingsCenter = (props) => { const SettingsCenter = (props) => {
const { snippets = [] } = useACLRoleContext(); const { snippets = [] } = useACLRoleContext();
@ -296,6 +311,7 @@ const SettingsCenter = (props) => {
<Layout.Content> <Layout.Content>
{aclPluginTabCheck && ( {aclPluginTabCheck && (
<PageHeader <PageHeader
style={{ backgroundColor: 'white', paddingBottom: 0 }}
ghost={false} ghost={false}
title={compile(items[pluginName]?.title)} title={compile(items[pluginName]?.title)}
footer={ footer={
@ -304,11 +320,16 @@ const SettingsCenter = (props) => {
onChange={(activeKey) => { onChange={(activeKey) => {
navigate(`/admin/settings/${pluginName}/${activeKey}`); navigate(`/admin/settings/${pluginName}/${activeKey}`);
}} }}
> items={plugin.tabs?.map((tab) => {
{plugin.tabs?.map((tab) => { if (!tab.isAllow) {
return tab.isAllow && <Tabs.TabPane tab={compile(tab?.title)} key={tab.key} />; return null;
}
return {
label: compile(tab?.title),
key: tab.key,
};
})} })}
</Tabs> />
} }
/> />
)} )}

View File

@ -14,9 +14,9 @@ import {
findByUid, findByUid,
findMenuItem, findMenuItem,
useACLRoleContext, useACLRoleContext,
useAdminSchemaUid,
useDocumentTitle, useDocumentTitle,
useRequest, useRequest,
useRoute,
useSystemSettings, useSystemSettings,
} from '../../../'; } from '../../../';
import { useCollectionManager } from '../../../collection-manager'; import { useCollectionManager } from '../../../collection-manager';
@ -54,6 +54,7 @@ const useMenuProps = () => {
defaultSelectedUid, defaultSelectedUid,
}; };
}; };
const MenuEditor = (props) => { const MenuEditor = (props) => {
const { setTitle } = useDocumentTitle(); const { setTitle } = useDocumentTitle();
const navigate = useNavigate(); const navigate = useNavigate();
@ -61,7 +62,6 @@ const MenuEditor = (props) => {
const defaultSelectedUid = params.name; const defaultSelectedUid = params.name;
const { sideMenuRef } = props; const { sideMenuRef } = props;
const ctx = useACLRoleContext(); const ctx = useACLRoleContext();
const route = useRoute();
const [current, setCurrent] = useState(null); const [current, setCurrent] = useState(null);
const onSelect = ({ item }) => { const onSelect = ({ item }) => {
const schema = item.props.schema; const schema = item.props.schema;
@ -70,12 +70,14 @@ const MenuEditor = (props) => {
navigate(`/admin/${schema['x-uid']}`); navigate(`/admin/${schema['x-uid']}`);
}; };
const adminSchemaUid = useAdminSchemaUid();
const { data, loading } = useRequest( const { data, loading } = useRequest(
{ {
url: `/uiSchemas:getJsonSchema/${route.uiSchemaUid}`, url: `/uiSchemas:getJsonSchema/${adminSchemaUid}`,
}, },
{ {
refreshDeps: [route.uiSchemaUid], refreshDeps: [adminSchemaUid],
onSuccess(data) { onSuccess(data) {
const schema = filterByACL(data?.data, ctx); const schema = filterByACL(data?.data, ctx);
// url 为 `/admin` 的情况 // url 为 `/admin` 的情况

View File

@ -1,6 +1,6 @@
import { connect, ISchema, mapProps, useField, useFieldSchema } from '@formily/react'; import { connect, ISchema, mapProps, useField, useFieldSchema } from '@formily/react';
import { isValid, uid } from '@formily/shared'; import { isValid, uid } from '@formily/shared';
import { Tree as AntdTree, Menu } from 'antd'; import { Tree as AntdTree } from 'antd';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -46,7 +46,11 @@ const MenuGroup = (props) => {
) { ) {
return <>{props.children}</>; return <>{props.children}</>;
} }
return <Menu.ItemGroup title={`${t('Customize')} > ${actionTitles[actionType]}`}>{props.children}</Menu.ItemGroup>; return (
<SchemaSettings.ItemGroup title={`${t('Customize')} > ${actionTitles[actionType]}`}>
{props.children}
</SchemaSettings.ItemGroup>
);
}; };
export const ActionDesigner = (props) => { export const ActionDesigner = (props) => {
@ -54,7 +58,7 @@ export const ActionDesigner = (props) => {
const field = useField(); const field = useField();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { name } = useCollection(); const { name } = useCollection();
const { getChildrenCollections, getCollection, getCollectionField } = useCollectionManager(); const { getChildrenCollections } = useCollectionManager();
const { dn } = useDesignable(); const { dn } = useDesignable();
const { t } = useTranslation(); const { t } = useTranslation();
const isAction = useLinkageAction(); const isAction = useLinkageAction();

View File

@ -36,7 +36,7 @@ export const ActionDrawer: ComposedActionDrawer = observer(
{...others} {...others}
{...drawerProps} {...drawerProps}
{...modalProps} {...modalProps}
style={{ rootStyle={{
...drawerProps?.style, ...drawerProps?.style,
...others?.style, ...others?.style,
}} }}

View File

@ -1,5 +1,5 @@
import { cx } from '@emotion/css'; import { cx } from '@emotion/css';
import { observer, RecursionField, useFieldSchema } from '@formily/react'; import { RecursionField, observer, useFieldSchema } from '@formily/react';
import { Space } from 'antd'; import { Space } from 'antd';
import React, { CSSProperties, useContext } from 'react'; import React, { CSSProperties, useContext } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';

View File

@ -46,7 +46,7 @@ export const DeleteEvent = observer(
return createPortal( return createPortal(
<Modal <Modal
title={cron ? t('Delete events') : null} title={cron ? t('Delete events') : null}
visible={visible} open={visible}
onCancel={() => setVisible(false)} onCancel={() => setVisible(false)}
onOk={() => onOk()} onOk={() => onOk()}
confirmLoading={loading} confirmLoading={loading}

View File

@ -2,10 +2,9 @@ import { LoadingOutlined } from '@ant-design/icons';
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { connect, mapProps, mapReadPretty, useField } from '@formily/react'; import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
import { toArr } from '@formily/shared'; import { toArr } from '@formily/shared';
import { action } from '@formily/reactive';
import { Cascader as AntdCascader, Space } from 'antd'; import { Cascader as AntdCascader, Space } from 'antd';
import { isBoolean, omit } from 'lodash'; import { isBoolean, omit } from 'lodash';
import React, { useState } from 'react'; import React from 'react';
import { useRequest } from '../../../api-client'; import { useRequest } from '../../../api-client';
import { defaultFieldNames } from './defaultFieldNames'; import { defaultFieldNames } from './defaultFieldNames';
import { ReadPretty } from './ReadPretty'; import { ReadPretty } from './ReadPretty';
@ -64,7 +63,7 @@ export const Cascader = connect(
); );
}; };
const handelDropDownVisible = (value) => { const handelDropDownVisible = (value) => {
if (value && !field.dataSource) { if (value && !field.dataSource.length) {
run(); run();
} }
}; };

View File

@ -104,6 +104,7 @@ FormItem.Designer = function Designer() {
const { getCollectionFields, getInterface, getCollectionJoinField, getCollection } = useCollectionManager(); const { getCollectionFields, getInterface, getCollectionJoinField, getCollection } = useCollectionManager();
const { getField } = useCollection(); const { getField } = useCollection();
const { form } = useFormBlockContext(); const { form } = useFormBlockContext();
const ctx = useBlockRequestContext();
const field = useField<Field>(); const field = useField<Field>();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { t } = useTranslation(); const { t } = useTranslation();
@ -111,7 +112,6 @@ FormItem.Designer = function Designer() {
const compile = useCompile(); const compile = useCompile();
const variablesCtx = useVariablesCtx(); const variablesCtx = useVariablesCtx();
const IsShowMultipleSwitch = useIsShowMultipleSwitch(); const IsShowMultipleSwitch = useIsShowMultipleSwitch();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
if (collectionField?.target) { if (collectionField?.target) {
targetField = getCollectionJoinField( targetField = getCollectionJoinField(
@ -447,11 +447,16 @@ FormItem.Designer = function Designer() {
properties: { properties: {
filter: { filter: {
default: defaultFilter, default: defaultFilter,
// title: '数据范围',
enum: dataSource, enum: dataSource,
'x-component': 'Filter', 'x-component': 'Filter',
'x-component-props': { 'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }), dynamicComponent: (props) =>
FilterDynamicComponent({
...props,
form,
collectionField,
rootCollection: ctx.props.collection || ctx.props.resource,
}),
}, },
}, },
}, },
@ -461,7 +466,6 @@ FormItem.Designer = function Designer() {
filter = removeNullCondition(filter); filter = removeNullCondition(filter);
_.set(field.componentProps, 'service.params.filter', filter); _.set(field.componentProps, 'service.params.filter', filter);
fieldSchema['x-component-props'] = field.componentProps; fieldSchema['x-component-props'] = field.componentProps;
field.componentProps = field.componentProps;
dn.emit('patch', { dn.emit('patch', {
schema: { schema: {
['x-uid']: fieldSchema['x-uid'], ['x-uid']: fieldSchema['x-uid'],
@ -878,6 +882,11 @@ export function isFileCollection(collection: Collection) {
return collection?.template === 'file'; return collection?.template === 'file';
} }
function extractFirstPart(path) {
const firstDotIndex = path.indexOf('.');
return firstDotIndex !== -1 ? path.slice(0, firstDotIndex) : path;
}
FormItem.FilterFormDesigner = FilterFormDesigner; FormItem.FilterFormDesigner = FilterFormDesigner;
export function getFieldDefaultValue(fieldSchema: ISchema, collectionField: CollectionFieldOptions) { export function getFieldDefaultValue(fieldSchema: ISchema, collectionField: CollectionFieldOptions) {

View File

@ -1,18 +1,17 @@
import { useFieldSchema, useField, ISchema } from '@formily/react';
import React, { useMemo } from 'react';
import { ArrayItems } from '@formily/antd'; import { ArrayItems } from '@formily/antd';
import { ISchema, useField, useFieldSchema } from '@formily/react';
import { Slider } from 'antd'; import { Slider } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCollection, useCollectionFilterOptions, useSortFields } from '../../../collection-manager'; import { useCollection, useCollectionFilterOptions, useSortFields } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
import { SchemaComponentOptions } from '../../core';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../hooks';
import { removeNullCondition } from '../filter'; import { removeNullCondition } from '../filter';
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent'; import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
import { SchemaComponentOptions } from '../../core';
import { defaultColumnCount, gridSizes, pageSizeOptions, screenSizeMaps, screenSizeTitleMaps } from './options'; import { defaultColumnCount, gridSizes, pageSizeOptions, screenSizeMaps, screenSizeTitleMaps } from './options';
Slider;
const columnCountMarks = [1, 2, 3, 4, 6, 8, 12, 24].reduce((obj, cur) => { const columnCountMarks = [1, 2, 3, 4, 6, 8, 12, 24].reduce((obj, cur) => {
obj[cur] = cur; obj[cur] = cur;

View File

@ -42,7 +42,5 @@
} }
html body { html body {
--adm-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, --adm-font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji,
Segoe UI Symbol;
} }

View File

@ -1,18 +1,21 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { import {
FieldContext,
observer, observer,
RecursionField, RecursionField,
Schema, SchemaContext,
SchemaExpressionScopeContext, SchemaExpressionScopeContext,
useField, useField,
useFieldSchema, useFieldSchema,
} from '@formily/react'; } from '@formily/react';
import { Menu as AntdMenu } from 'antd'; import { error } from '@nocobase/utils/client';
import React, { createContext, useContext, useEffect, useState } from 'react'; import { Menu as AntdMenu, MenuProps } from 'antd';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { createDesignable, DndContext, SortableItem, useDesignable, useDesigner } from '../..'; import { createDesignable, DndContext, SortableItem, useDesignable, useDesigner } from '../..';
import { Icon, useAPIClient, useSchemaInitializer } from '../../../'; import { Icon, useAPIClient, useSchemaInitializer } from '../../../';
import { useCollectMenuItems, useMenuItem } from '../../../hooks/useMenuItem';
import { useProps } from '../../hooks/useProps'; import { useProps } from '../../hooks/useProps';
import { MenuDesigner } from './Menu.Designer'; import { MenuDesigner } from './Menu.Designer';
import { findKeysByUid, findMenuItem } from './util'; import { findKeysByUid, findMenuItem } from './util';
@ -115,7 +118,7 @@ const designerCss = css`
} }
`; `;
const antdMenuClass = css` const headerMenuClass = css`
.ant-menu-item:hover { .ant-menu-item:hover {
> .ant-menu-title-content > div { > .ant-menu-title-content > div {
.general-schema-designer { .general-schema-designer {
@ -125,6 +128,45 @@ const antdMenuClass = css`
} }
`; `;
const sideMenuClass = css`
height: 100%;
overflow-y: auto;
overflow-x: hidden;
.ant-menu-item {
> .ant-menu-title-content {
margin-left: -24px;
margin-right: -16px;
padding: 0 16px 0 24px;
> div {
> .general-schema-designer {
right: 6px !important;
}
}
}
}
.ant-menu-submenu-title {
.ant-menu-title-content {
margin-left: -24px;
margin-right: -34px;
padding: 0 34px 0 24px;
> div {
> .general-schema-designer {
right: 6px !important;
}
> span.anticon {
margin-right: 10px;
}
}
}
}
`;
const menuItemClass = css`
:active {
background: inherit;
}
`;
type ComposedMenu = React.FC<any> & { type ComposedMenu = React.FC<any> & {
Item?: React.FC<any>; Item?: React.FC<any>;
URL?: React.FC<any>; URL?: React.FC<any>;
@ -132,6 +174,158 @@ type ComposedMenu = React.FC<any> & {
Designer?: React.FC<any>; Designer?: React.FC<any>;
}; };
const HeaderMenu = ({
others,
schema,
mode,
onSelect,
setLoading,
setDefaultSelectedKeys,
defaultSelectedKeys,
defaultOpenKeys,
selectedKeys,
designable,
render,
children,
}) => {
const { Component, getMenuItems } = useMenuItem();
const items = useMemo(() => {
const designerBtn = {
key: 'x-designer-button',
disabled: true,
style: { padding: '0 8px', order: 9999 },
label: render({ style: { background: 'none' } }),
notdelete: true,
};
const result = getMenuItems(() => {
return children;
});
if (designable) {
result.push(designerBtn);
}
return result;
}, [children, designable]);
return (
<>
<Component />
<AntdMenu
{...others}
className={headerMenuClass}
onSelect={(info: any) => {
const s = schema.properties[info.key];
if (mode === 'mix') {
if (s['x-component'] !== 'Menu.SubMenu') {
onSelect && onSelect(info);
} else {
const menuItemSchema = findMenuItem(s);
if (!menuItemSchema) {
return onSelect && onSelect(info);
}
// TODO
setLoading(true);
const keys = findKeysByUid(schema, menuItemSchema['x-uid']);
setDefaultSelectedKeys(keys);
setTimeout(() => {
setLoading(false);
}, 100);
onSelect &&
onSelect({
key: menuItemSchema.name,
item: {
props: {
schema: menuItemSchema,
},
},
});
}
} else {
onSelect && onSelect(info);
}
}}
mode={mode === 'mix' ? 'horizontal' : mode}
defaultOpenKeys={defaultOpenKeys}
defaultSelectedKeys={defaultSelectedKeys}
selectedKeys={selectedKeys}
items={items}
/>
</>
);
};
const SideMenu = ({
loading,
mode,
sideMenuSchema,
sideMenuRef,
defaultOpenKeys,
defaultSelectedKeys,
onSelect,
render,
t,
api,
refresh,
designable,
}) => {
const { Component, getMenuItems } = useMenuItem();
const items = useMemo(() => {
const result = getMenuItems(() => {
return <RecursionField schema={sideMenuSchema} onlyRenderProperties />;
});
if (designable) {
result.push({
key: 'x-designer-button',
disabled: true,
label: render({
insert: (s) => {
const dn = createDesignable({
t,
api,
refresh,
current: sideMenuSchema,
});
dn.loadAPIClientEvents();
dn.insertAdjacent('beforeEnd', s);
},
}),
order: 1,
notdelete: true,
});
}
return result;
}, [render, sideMenuSchema, designable, loading]);
if (loading) {
return null;
}
return (
mode === 'mix' &&
sideMenuSchema?.['x-component'] === 'Menu.SubMenu' &&
sideMenuRef?.current?.firstChild &&
createPortal(
<MenuModeContext.Provider value={'inline'}>
<Component />
<AntdMenu
mode={'inline'}
defaultOpenKeys={defaultOpenKeys}
defaultSelectedKeys={defaultSelectedKeys}
onSelect={(info) => {
onSelect && onSelect(info);
}}
className={sideMenuClass}
items={items as MenuProps['items']}
/>
</MenuModeContext.Provider>,
sideMenuRef.current.firstChild,
)
);
};
const MenuModeContext = createContext(null); const MenuModeContext = createContext(null);
MenuModeContext.displayName = 'MenuModeContext'; MenuModeContext.displayName = 'MenuModeContext';
@ -159,6 +353,7 @@ export const Menu: ComposedMenu = observer(
sideMenuRefScopeKey, sideMenuRefScopeKey,
defaultSelectedKeys: dSelectedKeys, defaultSelectedKeys: dSelectedKeys,
defaultOpenKeys: dOpenKeys, defaultOpenKeys: dOpenKeys,
children,
...others ...others
} = useProps(props); } = useProps(props);
const { t } = useTranslation(); const { t } = useTranslation();
@ -185,8 +380,17 @@ export const Menu: ComposedMenu = observer(
} }
return dOpenKeys; return dOpenKeys;
}); });
const [sideMenuSchema, setSideMenuSchema] = useState<Schema>(() => {
const key = defaultSelectedKeys?.[0] || null; const sideMenuSchema = useMemo(() => {
let key;
if (selectedUid) {
const keys = findKeysByUid(schema, selectedUid);
key = keys?.[0] || null;
} else {
key = defaultSelectedKeys?.[0] || null;
}
if (mode === 'mix' && key) { if (mode === 'mix' && key) {
const s = schema.properties?.[key]; const s = schema.properties?.[key];
if (s['x-component'] === 'Menu.SubMenu') { if (s['x-component'] === 'Menu.SubMenu') {
@ -194,7 +398,8 @@ export const Menu: ComposedMenu = observer(
} }
} }
return null; return null;
}); }, [defaultSelectedKeys, mode, schema, selectedUid]);
useEffect(() => { useEffect(() => {
if (!selectedUid) { if (!selectedUid) {
setSelectedKeys(undefined); setSelectedKeys(undefined);
@ -206,17 +411,6 @@ export const Menu: ComposedMenu = observer(
if (['inline', 'mix'].includes(mode)) { if (['inline', 'mix'].includes(mode)) {
setDefaultOpenKeys(dOpenKeys || keys); setDefaultOpenKeys(dOpenKeys || keys);
} }
const key = keys?.[0] || null;
if (mode === 'mix') {
if (key) {
const s = schema.properties?.[key];
if (s['x-component'] === 'Menu.SubMenu') {
setSideMenuSchema(s);
}
} else {
setSideMenuSchema(null);
}
}
}, [selectedUid]); }, [selectedUid]);
useEffect(() => { useEffect(() => {
if (['inline', 'mix'].includes(mode)) { if (['inline', 'mix'].includes(mode)) {
@ -228,118 +422,35 @@ export const Menu: ComposedMenu = observer(
<DndContext> <DndContext>
<MenuItemDesignerContext.Provider value={Designer}> <MenuItemDesignerContext.Provider value={Designer}>
<MenuModeContext.Provider value={mode}> <MenuModeContext.Provider value={mode}>
<AntdMenu <HeaderMenu
{...others} others={others}
className={antdMenuClass} schema={schema}
onSelect={(info: any) => { mode={mode}
const s = schema.properties[info.key]; onSelect={onSelect}
if (mode === 'mix') { setLoading={setLoading}
setSideMenuSchema(s); setDefaultSelectedKeys={setDefaultSelectedKeys}
if (s['x-component'] !== 'Menu.SubMenu') { defaultSelectedKeys={defaultSelectedKeys}
onSelect && onSelect(info); defaultOpenKeys={defaultOpenKeys}
} else { selectedKeys={selectedKeys}
const menuItemSchema = findMenuItem(s); designable={designable}
if (!menuItemSchema) { render={render}
return; >
} {children}
// TODO </HeaderMenu>
setLoading(true); <SideMenu
const keys = findKeysByUid(schema, menuItemSchema['x-uid']); loading={loading}
setDefaultSelectedKeys(keys); mode={mode}
setTimeout(() => { sideMenuSchema={sideMenuSchema}
setLoading(false); sideMenuRef={sideMenuRef}
}, 100);
onSelect &&
onSelect({
key: menuItemSchema.name,
item: {
props: {
schema: menuItemSchema,
},
},
});
}
} else {
onSelect && onSelect(info);
}
}}
mode={mode === 'mix' ? 'horizontal' : mode}
defaultOpenKeys={defaultOpenKeys} defaultOpenKeys={defaultOpenKeys}
defaultSelectedKeys={defaultSelectedKeys} defaultSelectedKeys={defaultSelectedKeys}
selectedKeys={selectedKeys} onSelect={onSelect}
> render={render}
{designable && ( t={t}
<AntdMenu.Item disabled key="x-designer-button" style={{ padding: '0 8px', order: 9999 }}> api={api}
{render({ style: { background: 'none' } })} refresh={refresh}
</AntdMenu.Item> designable={designable}
)} />
{props.children}
</AntdMenu>
{loading
? null
: mode === 'mix' &&
sideMenuSchema?.['x-component'] === 'Menu.SubMenu' &&
sideMenuRef?.current?.firstChild &&
createPortal(
<MenuModeContext.Provider value={'inline'}>
<AntdMenu
mode={'inline'}
defaultOpenKeys={defaultOpenKeys}
defaultSelectedKeys={defaultSelectedKeys}
onSelect={(info) => {
onSelect && onSelect(info);
}}
className={css`
height: 100%;
overflow-y: auto;
overflow-x: hidden;
.ant-menu-item {
> .ant-menu-title-content {
margin-left: -24px;
margin-right: -16px;
padding: 0 16px 0 24px;
> div {
> .general-schema-designer {
right: 6px !important;
}
}
}
}
.ant-menu-submenu-title {
.ant-menu-title-content {
margin-left: -24px;
margin-right: -34px;
padding: 0 34px 0 24px;
> div {
> .general-schema-designer {
right: 6px !important;
}
> span.anticon {
margin-right: 10px;
}
}
}
}
`}
>
<RecursionField schema={sideMenuSchema} onlyRenderProperties />
{render({
style: { margin: 8 },
insert: (s) => {
const dn = createDesignable({
t,
api,
refresh,
current: sideMenuSchema,
});
dn.loadAPIClientEvents();
dn.insertAdjacent('beforeEnd', s);
},
})}
</AntdMenu>
</MenuModeContext.Provider>,
sideMenuRef.current.firstChild,
)}
</MenuModeContext.Provider> </MenuModeContext.Provider>
</MenuItemDesignerContext.Provider> </MenuItemDesignerContext.Provider>
</DndContext> </DndContext>
@ -350,116 +461,149 @@ export const Menu: ComposedMenu = observer(
Menu.Item = observer( Menu.Item = observer(
(props) => { (props) => {
const { icon, ...others } = props; const { pushMenuItem } = useCollectMenuItems();
const { icon, children, ...others } = props;
const schema = useFieldSchema(); const schema = useFieldSchema();
const field = useField(); const field = useField();
const Designer = useContext(MenuItemDesignerContext); const Designer = useContext(MenuItemDesignerContext);
return ( const item = useMemo(() => {
<AntdMenu.Item return {
{...others} ...others,
className={css` className: menuItemClass,
:active { key: schema.name,
background: inherit; eventKey: schema.name,
} schema,
`} label: (
key={schema.name} <SchemaContext.Provider value={schema}>
eventKey={schema.name} <FieldContext.Provider value={field}>
schema={schema} <SortableItem className={designerCss} removeParentsIfNoChildren={false}>
> <Icon type={icon} />
<SortableItem className={designerCss} removeParentsIfNoChildren={false}> <span
<Icon type={icon} /> style={{
<span overflow: 'hidden',
className={css` textOverflow: 'ellipsis',
overflow: hidden; display: 'inline-block',
text-overflow: ellipsis; width: '100%',
display: inline-block; verticalAlign: 'middle',
width: 100%; }}
vertical-align: middle; >
`} {field.title}
> </span>
{field.title} {Designer && <Designer />}
</span> </SortableItem>
{Designer && <Designer />} </FieldContext.Provider>
</SortableItem> </SchemaContext.Provider>
</AntdMenu.Item> ),
); };
}, [field.title, icon, schema]);
if (!pushMenuItem) {
error('Menu.Item must be wrapped by GetMenuItemsContext.Provider');
return null;
}
pushMenuItem(item);
return null;
}, },
{ displayName: 'Menu.Item' }, { displayName: 'Menu.Item' },
); );
Menu.URL = observer( Menu.URL = observer(
(props) => { (props) => {
const { icon, ...others } = props; const { pushMenuItem } = useCollectMenuItems();
const { icon, children, ...others } = props;
const schema = useFieldSchema(); const schema = useFieldSchema();
const field = useField(); const field = useField();
const Designer = useContext(MenuItemDesignerContext); const Designer = useContext(MenuItemDesignerContext);
return (
<AntdMenu.Item if (!pushMenuItem) {
{...others} error('Menu.URL must be wrapped by GetMenuItemsContext.Provider');
className={css` return null;
:active { }
background: inherit;
} const item = useMemo(() => {
`} return {
key={schema.name} ...others,
eventKey={schema.name} className: menuItemClass,
schema={schema} key: schema.name,
onClick={() => { eventKey: schema.name,
schema,
onClick: () => {
window.open(props.href, '_blank'); window.open(props.href, '_blank');
}} },
> label: (
<SortableItem className={designerCss} removeParentsIfNoChildren={false}> <SchemaContext.Provider value={schema}>
<Icon type={icon} /> <FieldContext.Provider value={field}>
<span <SortableItem className={designerCss} removeParentsIfNoChildren={false}>
className={css` <Icon type={icon} />
overflow: hidden; <span
text-overflow: ellipsis; style={{
display: inline-block; overflow: 'hidden',
width: 100%; textOverflow: 'ellipsis',
vertical-align: middle; display: 'inline-block',
`} width: '100%',
> verticalAlign: 'middle',
{field.title} }}
</span> >
{Designer && <Designer />} {field.title}
</SortableItem> </span>
</AntdMenu.Item> {Designer && <Designer />}
); </SortableItem>
</FieldContext.Provider>
</SchemaContext.Provider>
),
};
}, [field.title, icon, props.href, schema]);
pushMenuItem(item);
return null;
}, },
{ displayName: 'MenuURL' }, { displayName: 'MenuURL' },
); );
Menu.SubMenu = observer( Menu.SubMenu = observer(
(props) => { (props) => {
const { icon, ...others } = props; const { Component, getMenuItems } = useMenuItem();
const { pushMenuItem } = useCollectMenuItems();
const { icon, children, ...others } = props;
const schema = useFieldSchema(); const schema = useFieldSchema();
const field = useField(); const field = useField();
const mode = useContext(MenuModeContext); const mode = useContext(MenuModeContext);
const Designer = useContext(MenuItemDesignerContext); const Designer = useContext(MenuItemDesignerContext);
const submenu = useMemo(() => {
return {
...others,
className: menuItemClass,
key: schema.name,
eventKey: schema.name,
label: (
<SchemaContext.Provider value={schema}>
<FieldContext.Provider value={field}>
<SortableItem className={subMenuDesignerCss} removeParentsIfNoChildren={false}>
<Icon type={icon} />
{field.title}
{Designer && <Designer />}
</SortableItem>
</FieldContext.Provider>
</SchemaContext.Provider>
),
children: getMenuItems(() => {
return <RecursionField schema={schema} onlyRenderProperties />;
}),
};
}, [field.title, icon, schema, children]);
if (!pushMenuItem) {
error('Menu.SubMenu must be wrapped by GetMenuItemsContext.Provider');
return null;
}
if (mode === 'mix') { if (mode === 'mix') {
return <Menu.Item {...props} />; return <Menu.Item {...props} />;
} }
return (
<AntdMenu.SubMenu pushMenuItem(submenu);
{...others} return <Component />;
className={css`
:active {
background: inherit;
}
`}
key={schema.name}
eventKey={schema.name}
title={
<SortableItem className={subMenuDesignerCss} removeParentsIfNoChildren={false}>
<Icon type={icon} />
{field.title}
{Designer && <Designer />}
</SortableItem>
}
>
<RecursionField schema={schema} onlyRenderProperties />
</AntdMenu.SubMenu>
);
}, },
{ displayName: 'Menu.SubMenu' }, { displayName: 'Menu.SubMenu' },
); );

View File

@ -1,8 +1,9 @@
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { FormDialog, FormLayout } from '@formily/antd'; import { FormDialog, FormLayout } from '@formily/antd';
import { Schema, SchemaOptionsContext, useFieldSchema } from '@formily/react'; import { Schema, SchemaOptionsContext, useFieldSchema } from '@formily/react';
import { Button, PageHeader as AntdPageHeader, Spin, Tabs } from 'antd'; import { Button, Spin, Tabs } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useContext, useEffect, useMemo, useState } from 'react'; import React, { useContext, useEffect, useMemo, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
@ -119,6 +120,25 @@ const pageWithFixedBlockCss = classNames([
`, `,
]); ]);
const pageHeaderCss = css`
background-color: white;
&.ant-page-header-has-footer {
padding-top: 12px;
padding-bottom: 0;
.ant-page-header-heading-left {
/* margin: 0; */
}
.ant-page-header-footer {
margin-top: 0;
}
}
`;
const height0 = css`
font-size: 0;
height: 0;
`;
export const Page = (props) => { export const Page = (props) => {
const { children, ...others } = props; const { children, ...others } = props;
const compile = useCompile(); const compile = useCompile();
@ -156,6 +176,8 @@ export const Page = (props) => {
const handleErrors = (error) => { const handleErrors = (error) => {
console.error(error); console.error(error);
}; };
const pageHeaderTitle = hidePageTitle ? undefined : fieldSchema.title || compile(title);
return ( return (
<FilterBlockProvider> <FilterBlockProvider>
<div className={pageDesignerCss}> <div className={pageDesignerCss}>
@ -167,19 +189,10 @@ export const Page = (props) => {
> >
{!disablePageHeader && ( {!disablePageHeader && (
<AntdPageHeader <AntdPageHeader
className={css` className={classNames(pageHeaderCss, pageHeaderTitle || enablePageTabs ? '' : height0)}
&.has-footer {
padding-top: 12px;
.ant-page-header-heading-left {
/* margin: 0; */
}
.ant-page-header-footer {
margin-top: 0;
}
}
`}
ghost={false} ghost={false}
title={hidePageTitle ? undefined : fieldSchema.title || compile(title)} // 如果标题为空的时候会导致 PageHeader 不渲染,所以这里设置一个空白字符,然后再设置高度为 0
title={pageHeaderTitle || ' '}
{...others} {...others}
footer={ footer={
enablePageTabs && ( enablePageTabs && (
@ -247,26 +260,23 @@ export const Page = (props) => {
</Button> </Button>
) )
} }
> items={fieldSchema.mapProperties((schema) => {
{fieldSchema.mapProperties((schema) => { return {
return ( label: (
<Tabs.TabPane <SortableItem
tab={ id={schema.name as string}
<SortableItem schema={schema}
id={schema.name as string} className={classNames('nb-action-link', designerCss, props.className)}
schema={schema} >
className={classNames('nb-action-link', designerCss, props.className)} {schema['x-icon'] && <Icon style={{ marginRight: 8 }} type={schema['x-icon']} />}
> <span>{schema.title || t('Unnamed')}</span>
{schema['x-icon'] && <Icon style={{ marginRight: 8 }} type={schema['x-icon']} />} <PageTabDesigner schema={schema} />
<span>{schema.title || t('Unnamed')}</span> </SortableItem>
<PageTabDesigner schema={schema} /> ),
</SortableItem> key: schema.name as string,
} };
key={schema.name}
/>
);
})} })}
</Tabs> />
</DndContext> </DndContext>
) )
} }

View File

@ -1,15 +1,18 @@
import { LoadingOutlined } from '@ant-design/icons'; import { LoadingOutlined } from '@ant-design/icons';
import { connect, mapProps, mapReadPretty, useField, useFieldSchema } from '@formily/react'; import { connect, mapProps, mapReadPretty, useField, useFieldSchema, useForm } from '@formily/react';
import { SelectProps, Tag, Empty, Divider } from 'antd'; import { Divider, SelectProps, Tag } from 'antd';
import flat from 'flat';
import { uniqBy } from 'lodash'; import { uniqBy } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ResourceActionOptions, useRequest } from '../../../api-client'; import { ResourceActionOptions, useRequest } from '../../../api-client';
import { useBlockRequestContext } from '../../../block-provider/BlockProvider';
import { mergeFilter } from '../../../block-provider/SharedFilterProvider'; import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useCollection, useCollectionManager } from '../../../collection-manager';
import { Select, defaultFieldNames } from '../select'; import { getInnermostKeyAndValue } from '../../common/utils/uitls';
import { defaultFieldNames, Select } from '../select';
import { ReadPretty } from './ReadPretty'; import { ReadPretty } from './ReadPretty';
import { extractFilterfield, extractValuesByPattern, generatePattern, parseVariables } from './utils';
const EMPTY = 'N/A'; const EMPTY = 'N/A';
export type RemoteSelectProps<P = any> = SelectProps<P, any> & { export type RemoteSelectProps<P = any> = SelectProps<P, any> & {
@ -39,10 +42,12 @@ const InternalRemoteSelect = connect(
...others ...others
} = props; } = props;
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const form = useForm();
const firstRun = useRef(false); const firstRun = useRef(false);
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const isQuickAdd = fieldSchema['x-component-props']?.addMode === 'quickAdd'; const isQuickAdd = fieldSchema['x-component-props']?.addMode === 'quickAdd';
const field = useField(); const field = useField();
const ctx = useBlockRequestContext();
const { getField } = useCollection(); const { getField } = useCollection();
const searchData = useRef(null); const searchData = useRef(null);
const { getCollectionJoinField, getInterface } = useCollectionManager(); const { getCollectionJoinField, getInterface } = useCollectionManager();
@ -115,6 +120,49 @@ const InternalRemoteSelect = connect(
}, },
[targetField?.uiSchema, fieldNames], [targetField?.uiSchema, fieldNames],
); );
const parseFilter = (rules) => {
if (!rules) {
return undefined;
}
const type = Object.keys(rules)[0] || '$and';
const conditions = rules[type];
const results = [];
conditions?.forEach((c) => {
const jsonlogic = getInnermostKeyAndValue(c);
const regex = /{{(.*?)}}/;
const matches = jsonlogic.value?.match?.(regex);
if (!matches || (!matches[1].includes('$form') && !matches[1].includes('$iteration'))) {
results.push(c);
return;
}
const associationfield = extractFilterfield(matches[1]);
const filterCollectionField = getCollectionJoinField(`${ctx.props.collection}.${associationfield}`);
if (['o2m', 'm2m'].includes(filterCollectionField?.interface)) {
// 对多子表单
const pattern = generatePattern(matches?.[1], associationfield);
const parseValue: any = extractValuesByPattern(flat(form.values), pattern);
const filters = parseValue.map((v) => {
return JSON.parse(JSON.stringify(c).replace(jsonlogic.value, v));
});
results.push({ $or: filters });
} else {
const variablesCtx = { $form: form.values, $iteration: form.values };
let str = matches?.[1];
if (str.includes('$iteration')) {
const path = field.path.segments.concat([]);
path.pop();
str = str.replace('$iteration.', `$iteration.${path.join('.')}.`);
}
const parseValue = parseVariables(str, variablesCtx);
const filterObj = JSON.parse(
JSON.stringify(c).replace(jsonlogic.value, str.endsWith('id') ? parseValue ?? 0 : parseValue),
);
results.push(filterObj);
}
});
return { [type]: results };
};
const { data, run, loading } = useRequest( const { data, run, loading } = useRequest(
{ {
action: 'list', action: 'list',
@ -122,9 +170,8 @@ const InternalRemoteSelect = connect(
params: { params: {
pageSize: 200, pageSize: 200,
...service?.params, ...service?.params,
// fields: [fieldNames.label, fieldNames.value, ...(service?.params?.fields || [])],
// search needs // search needs
filter: mergeFilter([field.componentProps?.service?.params?.filter || service?.params?.filter]), filter: mergeFilter([parseFilter(field.componentProps?.service?.params?.filter) || service?.params?.filter]),
}, },
}, },
{ {
@ -185,12 +232,10 @@ const InternalRemoteSelect = connect(
const valueOptions = (value != null && (Array.isArray(value) ? value : [value])) || []; const valueOptions = (value != null && (Array.isArray(value) ? value : [value])) || [];
return uniqBy(data?.data?.concat(valueOptions) || [], fieldNames.value); return uniqBy(data?.data?.concat(valueOptions) || [], fieldNames.value);
}, [data?.data, value]); }, [data?.data, value]);
const onDropdownVisibleChange = (visible) => { const onDropdownVisibleChange = (visible) => {
setOpen(visible); setOpen(visible);
searchData.current = null; searchData.current = null;
if (firstRun.current && data?.data.length > 0) {
return;
}
run(); run();
firstRun.current = true; firstRun.current = true;
}; };

View File

@ -0,0 +1,35 @@
import { get, isFunction } from 'lodash';
export const parseVariables = (str: string, ctx) => {
if (str) {
const result = get(ctx, str);
return isFunction(result) ? result() : result;
} else {
return str;
}
};
export function extractFilterfield(str) {
const match = str.match(/^\$form\.([^.[\]]+)/);
if (match) {
return match[1];
}
return null;
}
export function extractValuesByPattern(obj, pattern) {
const regexPattern = new RegExp(pattern.replace(/\*/g, '\\d+'));
const result = [];
for (const key in obj) {
if (regexPattern.test(key)) {
const value = obj[key];
result.push(value);
}
}
return result;
}
export function generatePattern(str, fieldName) {
const result = str.replace(`$form.${fieldName}.`, `${fieldName}.*.`);
return result;
}

View File

@ -3,8 +3,8 @@ import { useVariableOptions } from '../../../schema-settings/VariableInput/hooks
import { Variable } from '../variable'; import { Variable } from '../variable';
export function FilterDynamicComponent(props) { export function FilterDynamicComponent(props) {
const { value, onChange, renderSchemaComponent } = props; const { value, onChange, renderSchemaComponent, form, collectionField, ...other } = props;
const options = useVariableOptions(); const options = useVariableOptions({ form, collectionField, ...other });
return ( return (
<Variable.Input value={value} onChange={onChange} scope={options}> <Variable.Input value={value} onChange={onChange} scope={options}>

View File

@ -193,344 +193,347 @@ const useValidator = (validator: (value: any) => string) => {
}, []); }, []);
}; };
export const Table: any = observer((props: any) => { export const Table: any = observer(
const { pagination: pagination1, useProps, onChange, ...others1 } = props; (props: any) => {
const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {}; const { pagination: pagination1, useProps, onChange, ...others1 } = props;
const { const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {};
dragSort = false, const {
showIndex = true, dragSort = false,
onRowSelectionChange, showIndex = true,
onChange: onTableChange, onRowSelectionChange,
rowSelection, onChange: onTableChange,
rowKey, rowSelection,
required, rowKey,
onExpand, required,
...others onExpand,
} = { ...others1, ...others2 } as any; ...others
const field = useArrayField(others); } = { ...others1, ...others2 } as any;
const columns = useTableColumns(others); const field = useArrayField(others);
const schema = useFieldSchema(); const columns = useTableColumns(others);
const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider'; const schema = useFieldSchema();
const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext(); const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider';
const { expandFlag } = ctx; const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext();
const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {})); const { expandFlag } = ctx;
const paginationProps = usePaginationProps(pagination1, pagination2); const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {}));
// const requiredValidator = field.required || required; const paginationProps = usePaginationProps(pagination1, pagination2);
const { treeTable } = schema?.parent?.['x-decorator-props'] || {}; // const requiredValidator = field.required || required;
const [expandedKeys, setExpandesKeys] = useState([]); const { treeTable } = schema?.parent?.['x-decorator-props'] || {};
const [allIncludesChildren, setAllIncludesChildren] = useState([]); const [expandedKeys, setExpandesKeys] = useState([]);
const [selectedRowKeys, setSelectedRowKeys] = useState<any[]>(field?.data?.selectedRowKeys || []); const [allIncludesChildren, setAllIncludesChildren] = useState([]);
const [selectedRow, setSelectedRow] = useState([]); const [selectedRowKeys, setSelectedRowKeys] = useState<any[]>(field?.data?.selectedRowKeys || []);
const dataSource = field?.value?.slice?.()?.filter?.(Boolean) || []; const [selectedRow, setSelectedRow] = useState([]);
const isRowSelect = rowSelection?.type !== 'none'; const dataSource = field?.value?.slice?.()?.filter?.(Boolean) || [];
const isRowSelect = rowSelection?.type !== 'none';
let onRow = null, let onRow = null,
highlightRow = ''; highlightRow = '';
if (onClickRow) { if (onClickRow) {
onRow = (record) => { onRow = (record) => {
return { return {
onClick: () => onClickRow(record, setSelectedRow, selectedRow), onClick: () => onClickRow(record, setSelectedRow, selectedRow),
};
}; };
}; highlightRow = css`
highlightRow = css` & > td {
& > td { background-color: #caedff !important;
background-color: #caedff !important; }
} &:hover > td {
&:hover > td { background-color: #caedff !important;
background-color: #caedff !important; }
} `;
`;
}
// useEffect(() => {
// field.setValidator((value) => {
// if (requiredValidator) {
// return Array.isArray(value) && value.length > 0 ? null : 'The field value is required';
// }
// return;
// });
// }, [requiredValidator]);
useEffect(() => {
if (treeTable !== false) {
const keys = getIdsWithChildren(field.value?.slice?.());
setAllIncludesChildren(keys);
} }
}, [field.value]);
useEffect(() => {
if (expandFlag) {
setExpandesKeys(allIncludesChildren);
} else {
setExpandesKeys([]);
}
}, [expandFlag, allIncludesChildren]);
const components = useMemo(() => { // useEffect(() => {
return { // field.setValidator((value) => {
header: { // if (requiredValidator) {
wrapper: (props) => { // return Array.isArray(value) && value.length > 0 ? null : 'The field value is required';
return ( // }
<DndContext> // return;
<thead {...props} /> // });
</DndContext> // }, [requiredValidator]);
);
useEffect(() => {
if (treeTable !== false) {
const keys = getIdsWithChildren(field.value?.slice?.());
setAllIncludesChildren(keys);
}
}, [field.value]);
useEffect(() => {
if (expandFlag) {
setExpandesKeys(allIncludesChildren);
} else {
setExpandesKeys([]);
}
}, [expandFlag, allIncludesChildren]);
const components = useMemo(() => {
return {
header: {
wrapper: (props) => {
return (
<DndContext>
<thead {...props} />
</DndContext>
);
},
cell: (props) => {
return (
<th
{...props}
className={cls(
props.className,
css`
max-width: 300px;
white-space: nowrap;
&:hover .general-schema-designer {
display: block;
}
`,
)}
/>
);
},
}, },
cell: (props) => { body: {
return ( wrapper: (props) => {
<th return (
<DndContext
onDragEnd={(e) => {
if (!e.active || !e.over) {
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];
const to = field.value[toIndex];
field.move(fromIndex, toIndex);
onRowDragEnd({ fromIndex, toIndex, from, to });
}}
>
<tbody {...props} />
</DndContext>
);
},
row: (props) => {
return <SortableRow {...props}></SortableRow>;
},
cell: (props) => (
<td
{...props} {...props}
className={cls( className={classNames(
props.className, props.className,
css` css`
max-width: 300px; max-width: 300px;
white-space: nowrap; white-space: nowrap;
&:hover .general-schema-designer { .nb-read-pretty-input-number {
display: block; text-align: right;
} }
`, `,
)} )}
/> />
); ),
}, },
}, };
body: { }, [field, onRowDragEnd, dragSort]);
wrapper: (props) => {
return (
<DndContext
onDragEnd={(e) => {
if (!e.active || !e.over) {
console.warn('move cancel');
return;
}
const fromIndex = e.active?.data.current?.sortable?.index; const defaultRowKey = (record: any) => {
const toIndex = e.over?.data.current?.sortable?.index; return field.value?.indexOf?.(record);
const from = field.value[fromIndex];
const to = field.value[toIndex];
field.move(fromIndex, toIndex);
onRowDragEnd({ fromIndex, toIndex, from, to });
}}
>
<tbody {...props} />
</DndContext>
);
},
row: (props) => {
return <SortableRow {...props}></SortableRow>;
},
cell: (props) => (
<td
{...props}
className={classNames(
props.className,
css`
max-width: 300px;
white-space: nowrap;
.nb-read-pretty-input-number {
text-align: right;
}
`,
)}
/>
),
},
}; };
}, [field, onRowDragEnd, dragSort]);
const defaultRowKey = (record: any) => { const getRowKey = (record: any) => {
return field.value?.indexOf?.(record); if (typeof rowKey === 'string') {
}; return record[rowKey]?.toString();
} else {
return (rowKey ?? defaultRowKey)(record)?.toString();
}
};
const getRowKey = (record: any) => { const restProps = {
if (typeof rowKey === 'string') { rowSelection: rowSelection
return record[rowKey]?.toString(); ? {
} else { type: 'checkbox',
return (rowKey ?? defaultRowKey)(record)?.toString(); selectedRowKeys: selectedRowKeys,
} onChange(selectedRowKeys: any[], selectedRows: any[]) {
}; field.data = field.data || {};
field.data.selectedRowKeys = selectedRowKeys;
const restProps = { setSelectedRowKeys(selectedRowKeys);
rowSelection: rowSelection onRowSelectionChange?.(selectedRowKeys, selectedRows);
? { },
type: 'checkbox', renderCell: (checked, record, index, originNode) => {
selectedRowKeys: selectedRowKeys, if (!dragSort && !showIndex) {
onChange(selectedRowKeys: any[], selectedRows: any[]) { return originNode;
field.data = field.data || {}; }
field.data.selectedRowKeys = selectedRowKeys; const current = props?.pagination?.current;
setSelectedRowKeys(selectedRowKeys); const pageSize = props?.pagination?.pageSize || 20;
onRowSelectionChange?.(selectedRowKeys, selectedRows); if (current) {
}, index = index + (current - 1) * pageSize + 1;
renderCell: (checked, record, index, originNode) => { } else {
if (!dragSort && !showIndex) { index = index + 1;
return originNode; }
} if (record.__index) {
const current = props?.pagination?.current; index = extractIndex(record.__index);
const pageSize = props?.pagination?.pageSize || 20; }
if (current) { return (
index = index + (current - 1) * pageSize + 1;
} else {
index = index + 1;
}
if (record.__index) {
index = extractIndex(record.__index);
}
return (
<div
className={classNames(
checked ? 'checked' : null,
css`
position: relative;
display: flex;
float: left;
align-items: center;
justify-content: space-evenly;
padding-right: 8px;
.nb-table-index {
opacity: 0;
}
&:not(.checked) {
.nb-table-index {
opacity: 1;
}
}
`,
{
[css`
&:hover {
.nb-table-index {
opacity: 0;
}
.nb-origin-node {
display: block;
}
}
`]: isRowSelect,
},
)}
>
<div <div
className={classNames( className={classNames(
checked ? 'checked' : null, checked ? 'checked' : null,
css` css`
position: relative; position: relative;
display: flex; display: flex;
float: left;
align-items: center; align-items: center;
justify-content: space-evenly; justify-content: space-evenly;
padding-right: 8px;
.nb-table-index {
opacity: 0;
}
&:not(.checked) {
.nb-table-index {
opacity: 1;
}
}
`, `,
{
[css`
&:hover {
.nb-table-index {
opacity: 0;
}
.nb-origin-node {
display: block;
}
}
`]: isRowSelect,
},
)} )}
> >
{dragSort && <SortHandle id={getRowKey(record)} />}
{showIndex && <TableIndex index={index} />}
</div>
{isRowSelect && (
<div <div
className={classNames( className={classNames(
'nb-origin-node',
checked ? 'checked' : null, checked ? 'checked' : null,
css` css`
position: absolute; position: relative;
right: 50%; display: flex;
transform: translateX(50%); align-items: center;
&:not(.checked) { justify-content: space-evenly;
display: none;
}
`, `,
)} )}
> >
{originNode} {dragSort && <SortHandle id={getRowKey(record)} />}
{showIndex && <TableIndex index={index} />}
</div> </div>
)} {isRowSelect && (
</div> <div
); className={classNames(
}, 'nb-origin-node',
...rowSelection, checked ? 'checked' : null,
} css`
: undefined, position: absolute;
}; right: 50%;
const SortableWrapper = useCallback<React.FC>( transform: translateX(50%);
({ children }) => { &:not(.checked) {
return dragSort display: none;
? React.createElement(SortableContext, { }
items: field.value?.map?.(getRowKey) || [], `,
children: children, )}
}) >
: React.createElement(React.Fragment, { {originNode}
children, </div>
}); )}
}, </div>
[field, dragSort], );
); },
const fieldSchema = useFieldSchema(); ...rowSelection,
const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock; }
: undefined,
};
const SortableWrapper = useCallback<React.FC>(
({ children }) => {
return dragSort
? React.createElement(SortableContext, {
items: field.value?.map?.(getRowKey) || [],
children,
})
: React.createElement(React.Fragment, {
children,
});
},
[field, dragSort],
);
const fieldSchema = useFieldSchema();
const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock;
const { height: tableHeight, tableSizeRefCallback } = useTableSize(); const { height: tableHeight, tableSizeRefCallback } = useTableSize();
const scroll = useMemo(() => { const scroll = useMemo(() => {
return fixedBlock return fixedBlock
? { ? {
x: 'max-content', x: 'max-content',
y: tableHeight, y: tableHeight,
} }
: { : {
x: 'max-content', x: 'max-content',
}; };
}, [fixedBlock, tableHeight]); }, [fixedBlock, tableHeight]);
return ( return (
<div <div
className={css` className={css`
height: 100%;
overflow: hidden;
.ant-table-wrapper {
height: 100%; height: 100%;
.ant-spin-nested-loading { overflow: hidden;
.ant-table-wrapper {
height: 100%; height: 100%;
.ant-spin-container { .ant-spin-nested-loading {
height: 100%; height: 100%;
display: flex; .ant-spin-container {
flex-direction: column; height: 100%;
display: flex;
flex-direction: column;
}
} }
} }
} .ant-table {
.ant-table { overflow-x: auto;
overflow-x: auto; overflow-y: hidden;
overflow-y: hidden; }
} `}
`} >
> <SortableWrapper>
<SortableWrapper> <AntdTable
<AntdTable ref={tableSizeRefCallback}
ref={tableSizeRefCallback} rowKey={rowKey ?? defaultRowKey}
rowKey={rowKey ?? defaultRowKey} dataSource={dataSource}
dataSource={dataSource} {...others}
{...others} {...restProps}
{...restProps} pagination={paginationProps}
pagination={paginationProps} components={components}
components={components} onChange={(pagination, filters, sorter, extra) => {
onChange={(pagination, filters, sorter, extra) => { onTableChange?.(pagination, filters, sorter, extra);
onTableChange?.(pagination, filters, sorter, extra); }}
}} onRow={onRow}
onRow={onRow} rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')}
rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')} tableLayout={'auto'}
tableLayout={'auto'} scroll={scroll}
scroll={scroll} columns={columns}
columns={columns} expandable={{
expandable={{ onExpand: (flag, record) => {
onExpand: (flag, record) => { const newKeys = flag ? [...expandedKeys, record.id] : expandedKeys.filter((i) => record.id !== i);
const newKeys = flag ? [...expandedKeys, record.id] : expandedKeys.filter((i) => record.id !== i); setExpandesKeys(newKeys);
setExpandesKeys(newKeys); onExpand?.(flag, record);
onExpand?.(flag, record); },
}, expandedRowKeys: expandedKeys,
expandedRowKeys: expandedKeys, }}
}} />
/> </SortableWrapper>
</SortableWrapper> {field.errors.length > 0 && (
{field.errors.length > 0 && ( <div className="ant-formily-item-error-help ant-formily-item-help ant-formily-item-help-enter ant-formily-item-help-enter-active">
<div className="ant-formily-item-error-help ant-formily-item-help ant-formily-item-help-enter ant-formily-item-help-enter-active"> {field.errors.map((error) => {
{field.errors.map((error) => { return error.messages.map((message) => <div>{message}</div>);
return error.messages.map((message) => <div>{message}</div>); })}
})} </div>
</div> )}
)} </div>
</div> );
); },
}); { displayName: 'Table' },
);

View File

@ -1,47 +1,51 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { TabPaneProps, Tabs as AntdTabs, TabsProps } from 'antd'; import { Tabs as AntdTabs, TabPaneProps, TabsProps } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Icon } from '../../../icon'; import { Icon } from '../../../icon';
import { useSchemaInitializer } from '../../../schema-initializer'; import { useSchemaInitializer } from '../../../schema-initializer';
import { DndContext, SortableItem } from '../../common'; import { DndContext, SortableItem } from '../../common';
import { useDesignable } from '../../hooks';
import { useDesigner } from '../../hooks/useDesigner'; import { useDesigner } from '../../hooks/useDesigner';
import { TabsContextProvider, useTabsContext } from './context'; import { useTabsContext } from './context';
import { TabsDesigner } from './Tabs.Designer'; import { TabsDesigner } from './Tabs.Designer';
export const Tabs: any = observer( export const Tabs: any = observer(
(props: TabsProps) => { (props: TabsProps) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializer(fieldSchema['x-initializer']); const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
const { designable } = useDesignable();
const contextProps = useTabsContext(); const contextProps = useTabsContext();
const { PaneRoot = React.Fragment as React.FC<any> } = contextProps;
const PaneProvider = useMemo(() => { const items = useMemo(() => {
if (contextProps.deep === false) { const result = fieldSchema.mapProperties((schema, key: string) => {
return TabsContextProvider; return {
key,
label: <RecursionField name={key} schema={schema} onlyRenderSelf />,
children: (
<PaneRoot active={key === contextProps.activeKey}>
<RecursionField name={key} schema={schema} onlyRenderProperties />
</PaneRoot>
),
};
});
if (designable) {
result.push({
key: 'designer',
label: render(),
children: null,
});
} }
return React.Fragment;
}, [contextProps.deep]); return result;
}, [fieldSchema.mapProperties((s, key) => key).join()]);
return ( return (
<DndContext> <DndContext>
<AntdTabs <AntdTabs {...contextProps} style={props.style} items={items} />
{...contextProps}
style={props.style}
tabBarExtraContent={{
right: render(),
}}
>
{fieldSchema.mapProperties((schema, key) => {
return (
<AntdTabs.TabPane tab={<RecursionField name={key} schema={schema} onlyRenderSelf />} key={key}>
<PaneProvider>
<RecursionField name={key} schema={schema} onlyRenderProperties />
</PaneProvider>
</AntdTabs.TabPane>
);
})}
</AntdTabs>
</DndContext> </DndContext>
); );
}, },

View File

@ -2,7 +2,7 @@ import { TabsProps } from 'antd';
import React from 'react'; import React from 'react';
interface TabsContextProps extends TabsProps { interface TabsContextProps extends TabsProps {
deep?: boolean; PaneRoot?: React.FC<any>;
} }
const TabsContext = React.createContext<TabsContextProps>({}); const TabsContext = React.createContext<TabsContextProps>({});

View File

@ -18,7 +18,7 @@ type Composed = React.FC<UploadProps> & {
export const ReadPretty: Composed = () => null; export const ReadPretty: Composed = () => null;
ReadPretty.File = (props: UploadProps) => { ReadPretty.File = function File(props: UploadProps) {
const record = useRecord(); const record = useRecord();
const field = useField<Field>(); const field = useField<Field>();
const value = isString(field.value) ? record : field.value; const value = isString(field.value) ? record : field.value;
@ -44,7 +44,7 @@ ReadPretty.File = (props: UploadProps) => {
// } // }
}; };
return ( return (
<div className={'ant-upload-list-picture-card-container'}> <div key={file.name} className={'ant-upload-list-picture-card-container'}>
<div className="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-picture-card"> <div className="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-picture-card">
<div className={'ant-upload-list-item-info'}> <div className={'ant-upload-list-item-info'}>
<span className="ant-upload-span"> <span className="ant-upload-span">
@ -114,6 +114,7 @@ ReadPretty.File = (props: UploadProps) => {
imageTitle={images[photoIndex]?.title} imageTitle={images[photoIndex]?.title}
toolbarButtons={[ toolbarButtons={[
<button <button
key={'download'}
style={{ fontSize: 22, background: 'none', lineHeight: 1 }} style={{ fontSize: 22, background: 'none', lineHeight: 1 }}
type="button" type="button"
aria-label="Zoom in" aria-label="Zoom in"
@ -135,10 +136,10 @@ ReadPretty.File = (props: UploadProps) => {
); );
}; };
ReadPretty.Upload = (props) => { ReadPretty.Upload = function Upload(props) {
const field = useField<Field>(); const field = useField<Field>();
return (field.value || []).map((item) => ( return (field.value || []).map((item) => (
<div> <div key={item.name}>
{item.url ? ( {item.url ? (
<a target={'_blank'} href={item.url} rel="noreferrer"> <a target={'_blank'} href={item.url} rel="noreferrer">
{item.name} {item.name}

View File

@ -65,7 +65,7 @@ export function VariableSelect(props) {
} }
} }
}} }}
dropdownClassName={css` popupClassName={css`
.ant-cascader-menu { .ant-cascader-menu {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@ -1,3 +1,4 @@
import flat from 'flat'; import flat from 'flat';
import _, { every, findIndex, isArray, some } from 'lodash'; import _, { every, findIndex, isArray, some } from 'lodash';
import moment from 'moment'; import moment from 'moment';
@ -13,7 +14,6 @@ type VariablesCtx = {
export const useVariablesCtx = (): VariablesCtx => { export const useVariablesCtx = (): VariablesCtx => {
const { data } = useCurrentUserContext() || {}; const { data } = useCurrentUserContext() || {};
return useMemo(() => { return useMemo(() => {
return { return {
$user: data?.data || {}, $user: data?.data || {},
@ -44,7 +44,7 @@ export const parseVariables = (str: string, ctx: VariablesCtx) => {
} }
}; };
function getInnermostKeyAndValue(obj) { export function getInnermostKeyAndValue(obj) {
if (typeof obj !== 'object' || obj === null) { if (typeof obj !== 'object' || obj === null) {
return null; return null;
} }

View File

@ -1,8 +1,10 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { ISchema, observer, useForm } from '@formily/react'; import { ISchema, observer, useForm } from '@formily/react';
import { Button, Dropdown, Menu, Switch } from 'antd'; import { error, isString } from '@nocobase/utils/client';
import { Button, Dropdown, MenuProps, Switch } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { useCollectMenuItem, useMenuItem } from '../hooks/useMenuItem';
import { Icon } from '../icon'; import { Icon } from '../icon';
import { SchemaComponent, useActionContext } from '../schema-component'; import { SchemaComponent, useActionContext } from '../schema-component';
import { useCompile, useDesignable } from '../schema-component/hooks'; import { useCompile, useDesignable } from '../schema-component/hooks';
@ -14,10 +16,22 @@ import {
SchemaInitializerItemProps, SchemaInitializerItemProps,
} from './types'; } from './types';
const overlayClassName = css`
.ant-dropdown-menu-item-group-list {
max-height: 40vh;
overflow: auto;
}
`;
const defaultWrap = (s: ISchema) => s; const defaultWrap = (s: ISchema) => s;
export const SchemaInitializerItemContext = createContext(null); export const SchemaInitializerItemContext = createContext(null);
export const SchemaInitializerButtonContext = createContext<any>({}); export const SchemaInitializerButtonContext = createContext<{
visible?: boolean;
setVisible?: (v: boolean) => void;
searchValue?: string;
setSearchValue?: (v: string) => void;
}>({});
export const SchemaInitializer = () => null; export const SchemaInitializer = () => null;
@ -41,27 +55,72 @@ SchemaInitializer.Button = observer(
const compile = useCompile(); const compile = useCompile();
const { insertAdjacent, findComponent, designable } = useDesignable(); const { insertAdjacent, findComponent, designable } = useDesignable();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { Component: CollectionComponent, getMenuItem, clean } = useMenuItem();
const [shouldRender, setShouldRender] = useState(false);
const [searchValue, setSearchValue] = useState('');
if (!designable && props.designable !== true) {
return null;
}
const buttonDom = (
<div
style={{ display: 'inline-block' }}
onMouseEnter={() => {
setShouldRender(true);
setVisible(true);
}}
>
{component ? (
component
) : (
<Button
type={'dashed'}
style={{
borderColor: '#f18b62',
color: '#f18b62',
...style,
}}
{...others}
icon={typeof icon === 'string' ? <Icon type={icon as string} /> : icon}
>
{compile(props.children || props.title)}
</Button>
)}
</div>
);
if (!shouldRender || !items.length) {
return buttonDom;
}
const insertSchema = (schema) => { const insertSchema = (schema) => {
if (props.insert) { if (insert) {
props.insert(wrap(schema)); insert(wrap(schema));
} else { } else {
insertAdjacent(insertPosition, wrap(schema), { onSuccess }); insertAdjacent(insertPosition, wrap(schema), { onSuccess });
} }
}; };
const renderItems = (items: any) => { const renderItems = (items: any) => {
return items return items
.filter((v) => { .filter((v: any) => {
return v && (v?.visible ? v.visible() : true); return v && (v?.visible ? v.visible() : true);
}) })
?.map((item, indexA) => { ?.map((item: any, indexA: number) => {
if (item.type === 'divider') { if (item.type === 'divider') {
return <Menu.Divider key={item.key || `item-${indexA}`} />; return { type: 'divider', key: item.key || `item-${indexA}` };
} }
if (item.type === 'item' && item.component) { if (item.type === 'item' && item.component) {
const Component = findComponent(item.component); const Component = findComponent(item.component);
item.key = `${item.key || item.title}-${indexA}`; if (!Component) {
return ( error(`SchemaInitializer: component "${item.component}" not found`);
Component && ( return null;
}
if (!item.key) {
item.key = `${item.title}-${indexA}`;
}
return getMenuItem(() => {
return (
<SchemaInitializerItemContext.Provider <SchemaInitializerItemContext.Provider
key={item.key} key={item.key}
value={{ value={{
@ -80,75 +139,61 @@ SchemaInitializer.Button = observer(
insert={insertSchema} insert={insertSchema}
/> />
</SchemaInitializerItemContext.Provider> </SchemaInitializerItemContext.Provider>
) );
); });
} }
if (item.type === 'itemGroup') { if (item.type === 'itemGroup') {
const label = compile(item.title);
return ( return (
!!item.children?.length && ( !!item.children?.length && {
<Menu.ItemGroup key={item.key || `item-group-${indexA}`} title={compile(item.title)}> type: 'group',
{renderItems(item.children)} key: item.key || `item-group-${indexA}`,
</Menu.ItemGroup> label,
) title: label,
children: renderItems(item.children),
}
); );
} }
if (item.type === 'subMenu') { if (item.type === 'subMenu') {
const label = compile(item.title);
return ( return (
!!item.children?.length && ( !!item.children?.length && {
<Menu.SubMenu key: item.key || `item-group-${indexA}`,
key={item.key || `item-group-${indexA}`} label,
title={compile(item.title)} title: label,
popupClassName={menuItemGroupCss} popupClassName: menuItemGroupCss,
> children: renderItems(item.children),
{renderItems(item.children)} }
</Menu.SubMenu>
)
); );
} }
}); });
}; };
const buttonDom = ( clean();
<Button const menuItems = renderItems(items);
type={'dashed'}
style={{
borderColor: '#f18b62',
color: '#f18b62',
...style,
}}
{...others}
icon={typeof icon === 'string' ? <Icon type={icon as string} /> : icon}
>
{compile(props.children || props.title)}
</Button>
);
if (!items.length) {
return buttonDom;
}
const menu = <Menu style={{ maxHeight: '60vh', overflowY: 'auto' }}>{renderItems(items)}</Menu>;
if (!designable && props.designable !== true) {
return null;
}
return ( return (
<SchemaInitializerButtonContext.Provider value={{ visible, setVisible }}> <SchemaInitializerButtonContext.Provider value={{ visible, setVisible, searchValue, setSearchValue }}>
<CollectionComponent />
<Dropdown <Dropdown
className={classNames('nb-schema-initializer-button')} className={classNames('nb-schema-initializer-button')}
openClassName={`nb-schema-initializer-button-open`} openClassName={`nb-schema-initializer-button-open`}
overlayClassName={classNames( overlayClassName={classNames('nb-schema-initializer-button-overlay', overlayClassName)}
'nb-schema-initializer-button-overlay',
css`
.ant-dropdown-menu-item-group-list {
max-height: 40vh;
overflow: auto;
}
`,
)}
open={visible} open={visible}
onOpenChange={(visible) => { onOpenChange={() => {
setVisible(visible); // 如果不清空输入框的值,那么下次打开的时候会出现上次输入的值
setSearchValue('');
setShouldRender(false);
setVisible(false);
}}
menu={{
style: {
maxHeight: '60vh',
overflowY: 'auto',
},
items: menuItems,
}} }}
{...dropdown} {...dropdown}
overlay={menu}
> >
{component ? component : buttonDom} {component ? component : buttonDom}
</Dropdown> </Dropdown>
@ -158,10 +203,17 @@ SchemaInitializer.Button = observer(
{ displayName: 'SchemaInitializer.Button' }, { displayName: 'SchemaInitializer.Button' },
); );
SchemaInitializer.Item = (props: SchemaInitializerItemProps) => { SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
const { index, info } = useContext(SchemaInitializerItemContext); const { info } = useContext(SchemaInitializerItemContext);
const compile = useCompile(); const compile = useCompile();
const { eventKey, items = [], children = info?.title, icon, onClick, ...others } = props; const { items = [], children = info?.title, icon, onClick } = props;
const { collectMenuItem } = useCollectMenuItem();
if (!collectMenuItem) {
error('SchemaInitializer.Item: collectMenuItem is undefined, please check the context');
return null;
}
if (items?.length > 0) { if (items?.length > 0) {
const renderMenuItem = (items: SchemaInitializerItemOptions[]) => { const renderMenuItem = (items: SchemaInitializerItemOptions[]) => {
if (!items?.length) { if (!items?.length) {
@ -169,77 +221,70 @@ SchemaInitializer.Item = (props: SchemaInitializerItemProps) => {
} }
return items.map((item, indexA) => { return items.map((item, indexA) => {
if (item.type === 'divider') { if (item.type === 'divider') {
return <Menu.Divider key={`divider-${indexA}`} />; return { type: 'divider', key: `divider-${indexA}` };
} }
if (item.type === 'itemGroup') { if (item.type === 'itemGroup') {
return ( const label = compile(item.title);
<Menu.ItemGroup return {
// @ts-ignore type: 'group',
eventKey={item.key || `item-group-${indexA}`} key: item.key || `item-group-${indexA}`,
key={item.key || `item-group-${indexA}`} label,
title={compile(item.title)} title: label,
className={menuItemGroupCss} className: menuItemGroupCss,
> children: renderMenuItem(item.children),
{renderMenuItem(item.children)} } as MenuProps['items'][0];
</Menu.ItemGroup>
);
} }
if (item.type === 'subMenu') { if (item.type === 'subMenu') {
return ( const label = compile(item.title);
<Menu.SubMenu return {
// @ts-ignore key: item.key || `sub-menu-${indexA}`,
eventKey={item.key || `sub-menu-${indexA}`} label,
key={item.key || `sub-menu-${indexA}`} title: label,
title={compile(item.title)} children: renderMenuItem(item.children),
> };
{renderMenuItem(item.children)}
</Menu.SubMenu>
);
} }
return ( const label = compile(item.title);
<Menu.Item return {
eventKey={item.key} key: item.key || `${info.key}-${item.title}-${indexA}`,
key={item.key} label,
onClick={(info) => { title: label,
item?.clearKeywords?.(); onClick: (info) => {
if (item.onClick) { item?.clearKeywords?.();
item.onClick({ ...info, item }); if (item.onClick) {
} else { item.onClick({ ...info, item });
onClick({ ...info, item }); } else {
} onClick({ ...info, item });
}} }
> },
{compile(item.title)} };
</Menu.Item>
);
}); });
}; };
return (
<Menu.SubMenu const item = {
// @ts-ignore key: info.key,
eventKey={eventKey ? `${eventKey}-${index}` : info.key} label: isString(children) ? compile(children) : children,
key={info.key} icon: typeof icon === 'string' ? <Icon type={icon as string} /> : icon,
title={compile(children)} children: renderMenuItem(items),
icon={typeof icon === 'string' ? <Icon type={icon as string} /> : icon} };
>
{renderMenuItem(items)} collectMenuItem(item);
</Menu.SubMenu> return null;
);
} }
return (
<Menu.Item const label = isString(children) ? compile(children) : children;
// {...others} const item = {
key={info.key} key: info.key,
eventKey={eventKey ? `${eventKey}-${index}` : info.key} label,
icon={typeof icon === 'string' ? <Icon type={icon as string} /> : icon} title: label,
onClick={(opts) => { icon: typeof icon === 'string' ? <Icon type={icon as string} /> : icon,
info?.clearKeywords?.(); onClick: (opts) => {
onClick({ ...opts, item: info }); info?.clearKeywords?.();
}} onClick({ ...opts, item: info });
> },
{compile(children)} };
</Menu.Item>
); collectMenuItem(item);
return null;
}; };
SchemaInitializer.itemWrap = (component?: SchemaInitializerItemComponent) => { SchemaInitializer.itemWrap = (component?: SchemaInitializerItemComponent) => {
@ -253,11 +298,13 @@ interface SchemaInitializerActionModalProps {
onSubmit?: (values: any) => void; onSubmit?: (values: any) => void;
buttonText?: any; buttonText?: any;
} }
SchemaInitializer.ActionModal = (props: SchemaInitializerActionModalProps) => { SchemaInitializer.ActionModal = function ActionModal(props: SchemaInitializerActionModalProps) {
const { title, schema, buttonText, onCancel, onSubmit } = props; const { title, schema, buttonText, onCancel, onSubmit } = props;
const useCancelAction = useCallback(() => { const useCancelAction = useCallback(() => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const form = useForm(); const form = useForm();
// eslint-disable-next-line react-hooks/rules-of-hooks
const ctx = useActionContext(); const ctx = useActionContext();
return { return {
async run() { async run() {
@ -269,7 +316,9 @@ SchemaInitializer.ActionModal = (props: SchemaInitializerActionModalProps) => {
}, [onCancel]); }, [onCancel]);
const useSubmitAction = useCallback(() => { const useSubmitAction = useCallback(() => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const form = useForm(); const form = useForm();
// eslint-disable-next-line react-hooks/rules-of-hooks
const ctx = useActionContext(); const ctx = useActionContext();
return { return {
async run() { async run() {

View File

@ -1,31 +1,28 @@
import { Divider, Input } from 'antd'; import { Divider, Input } from 'antd';
import React from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCollectionManager } from '../collection-manager';
export const SelectCollection = ({ value, onChange, setSelected }) => { export const SelectCollection = ({ value: outValue, onChange }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { collections } = useCollectionManager(); const [value, setValue] = useState<string>(outValue);
// 之所以要增加个内部的 value 是为了防止用户输入过快时造成卡顿的问题
useEffect(() => {
setValue(outValue);
}, [outValue]);
return ( return (
<div style={{ width: 210 }}> <div style={{ width: 210 }}>
<Input <Input
autoFocus
allowClear allowClear
style={{ padding: '0 4px 6px' }} style={{ padding: '0 4px 6px' }}
bordered={false} bordered={false}
placeholder={t('Search and select collection')} placeholder={t('Search and select collection')}
value={value} value={value}
onChange={(e) => { onChange={(e) => {
const names = collections
.filter((collection) => {
if (!collection.title) {
return;
}
return collection.title.toUpperCase().includes(e.target.value.toUpperCase());
})
.map((item) => item.name);
setSelected(names);
onChange(e.target.value); onChange(e.target.value);
setValue(e.target.value);
}} }}
/> />
<Divider style={{ margin: 0 }} /> <Divider style={{ margin: 0 }} />

View File

@ -1,12 +1,12 @@
import { MenuOutlined } from '@ant-design/icons'; import { MenuOutlined } from '@ant-design/icons';
import { ISchema, useFieldSchema } from '@formily/react'; import { ISchema, useFieldSchema } from '@formily/react';
import _ from 'lodash';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SchemaInitializer, SchemaSettings } from '../..'; import { SchemaInitializer, SchemaSettings } from '../..';
import { useAPIClient } from '../../api-client'; import { useAPIClient } from '../../api-client';
import { useCollection } from '../../collection-manager'; import { useCollection } from '../../collection-manager';
import { createDesignable, useDesignable } from '../../schema-component'; import { createDesignable, useDesignable } from '../../schema-component';
import _ from 'lodash';
export const Resizable = (props) => { export const Resizable = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -32,7 +32,7 @@ export const TableActionInitializers = {
skipScopeCheck: true, skipScopeCheck: true,
}, },
}, },
visible: () => { visible: function useVisible() {
const collection = useCollection(); const collection = useCollection();
return collection.template !== 'view' && collection.template !== 'file'; return collection.template !== 'view' && collection.template !== 'file';
}, },
@ -45,7 +45,7 @@ export const TableActionInitializers = {
'x-align': 'right', 'x-align': 'right',
'x-decorator': 'ACLActionProvider', 'x-decorator': 'ACLActionProvider',
}, },
visible: () => { visible: function useVisible() {
const collection = useCollection(); const collection = useCollection();
return (collection as any).template !== 'view'; return (collection as any).template !== 'view';
}, },
@ -65,7 +65,7 @@ export const TableActionInitializers = {
schema: { schema: {
'x-align': 'right', 'x-align': 'right',
}, },
visible: () => { visible: function useVisible() {
const schema = useFieldSchema(); const schema = useFieldSchema();
const collection = useCollection(); const collection = useCollection();
const { treeTable } = schema?.parent?.['x-decorator-props'] || {}; const { treeTable } = schema?.parent?.['x-decorator-props'] || {};
@ -76,7 +76,7 @@ export const TableActionInitializers = {
}, },
{ {
type: 'divider', type: 'divider',
visible: () => { visible: function useVisible() {
const collection = useCollection(); const collection = useCollection();
return (collection as any).template !== 'view'; return (collection as any).template !== 'view';
}, },
@ -157,7 +157,7 @@ export const TableActionInitializers = {
}, },
}, },
], ],
visible: () => { visible: function useVisible() {
const collection = useCollection(); const collection = useCollection();
return (collection as any).template !== 'view'; return (collection as any).template !== 'view';
}, },

View File

@ -8,12 +8,13 @@ import {
useInheritsTableColumnInitializerFields, useInheritsTableColumnInitializerFields,
} from '../utils'; } from '../utils';
import { useCompile } from '../../schema-component'; import { useCompile } from '../../schema-component';
import { useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
// 表格列配置 // 表格列配置
export const TableColumnInitializers = (props: any) => { export const TableColumnInitializers = (props: any) => {
const { items = [], action = true } = props; const { items = [], action = true } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const field = useField();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const associatedFields = useAssociatedTableColumnInitializerFields(); const associatedFields = useAssociatedTableColumnInitializerFields();
const inheritFields = useInheritsTableColumnInitializerFields(); const inheritFields = useInheritsTableColumnInitializerFields();
@ -41,7 +42,7 @@ export const TableColumnInitializers = (props: any) => {
); );
}); });
} }
if (associatedFields?.length > 0 && !isSubTable) { if (associatedFields?.length > 0 && field.readPretty) {
fieldItems.push( fieldItems.push(
{ {
type: 'divider', type: 'divider',

View File

@ -1,8 +1,8 @@
import { DownOutlined, PlusOutlined } from '@ant-design/icons'; import { DownOutlined, PlusOutlined } from '@ant-design/icons';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { RecursionField, observer, useField, useFieldSchema } from '@formily/react'; import { RecursionField, observer, useField, useFieldSchema } from '@formily/react';
import { Button, Dropdown, Menu } from 'antd'; import { Button, Dropdown, MenuProps } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { useDesignable } from '../../'; import { useDesignable } from '../../';
import { useACLRolesCheck, useRecordPkValue } from '../../acl/ACLProvider'; import { useACLRolesCheck, useRecordPkValue } from '../../acl/ACLProvider';
import { CollectionProvider, useCollection, useCollectionManager } from '../../collection-manager'; import { CollectionProvider, useCollection, useCollectionManager } from '../../collection-manager';
@ -131,44 +131,44 @@ export const CreateAction = observer(
const componentType = field.componentProps.type || 'primary'; const componentType = field.componentProps.type || 'primary';
const { getChildrenCollections } = useCollectionManager(); const { getChildrenCollections } = useCollectionManager();
const totalChildCollections = getChildrenCollections(collection.name); const totalChildCollections = getChildrenCollections(collection.name);
const inheritsCollections = enableChildren const inheritsCollections = useMemo(() => {
.map((k) => { return enableChildren
if (!k) { .map((k) => {
return; if (!k) {
} return;
const childCollection = totalChildCollections.find((j) => j.name === k.collection); }
if (!childCollection) { const childCollection = totalChildCollections.find((j) => j.name === k.collection);
return; if (!childCollection) {
} return;
return { }
...childCollection, return {
title: k.title || childCollection.title, ...childCollection,
}; title: k.title || childCollection.title,
}) };
.filter((v) => { })
return v && actionAclCheck(`${v.name}:create`); .filter((v) => {
}); return v && actionAclCheck(`${v.name}:create`);
});
}, [enableChildren, totalChildCollections]);
const linkageRules = fieldSchema?.['x-linkage-rules'] || []; const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
const values = useRecord(); const values = useRecord();
const compile = useCompile(); const compile = useCompile();
const { designable } = useDesignable(); const { designable } = useDesignable();
const icon = props.icon || <PlusOutlined />; const icon = props.icon || <PlusOutlined />;
const menu = ( const menuItems = useMemo<MenuProps['items']>(() => {
<Menu> return inheritsCollections.map((option) => ({
{inheritsCollections.map((option) => { key: option.name,
return ( label: compile(option.title),
<Menu.Item onClick: () => onClick?.(option.name),
key={option.name} }));
onClick={(info) => { }, [inheritsCollections, onClick]);
onClick?.(option.name);
}} const menu = useMemo<MenuProps>(() => {
> return {
{compile(option.title)} items: menuItems,
</Menu.Item> };
); }, [menuItems]);
})}
</Menu>
);
useEffect(() => { useEffect(() => {
field.linkageProperty = {}; field.linkageProperty = {};
linkageRules linkageRules
@ -190,7 +190,7 @@ export const CreateAction = observer(
leftButton, leftButton,
React.cloneElement(rightButton as React.ReactElement<any, string>, { loading: false }), React.cloneElement(rightButton as React.ReactElement<any, string>, { loading: false }),
]} ]}
overlay={menu} menu={menu}
onClick={(info) => { onClick={(info) => {
onClick?.(collection.name); onClick?.(collection.name);
}} }}
@ -199,7 +199,7 @@ export const CreateAction = observer(
{props.children} {props.children}
</Dropdown.Button> </Dropdown.Button>
) : ( ) : (
<Dropdown overlay={menu}> <Dropdown menu={menu}>
{ {
<Button icon={icon} type={componentType}> <Button icon={icon} type={componentType}>
{props.children} <DownOutlined /> {props.children} <DownOutlined />

View File

@ -89,6 +89,7 @@ export enum AssignedFieldValueType {
export const AssignedField = (props: any) => { export const AssignedField = (props: any) => {
const { t } = useTranslation(); const { t } = useTranslation();
const compile = useCompile(); const compile = useCompile();
const collection = useCollection();
const field = useField<Field>(); const field = useField<Field>();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const isDynamicValue = const isDynamicValue =
@ -110,7 +111,7 @@ export const AssignedField = (props: any) => {
const [options, setOptions] = useState<any[]>([]); const [options, setOptions] = useState<any[]>([]);
const { getField } = useCollection(); const { getField } = useCollection();
const collectionField = getField(fieldSchema.name); const collectionField = getField(fieldSchema.name);
const fields = useCollectionFilterOptions(collectionField?.collectionName); const fields = useCollectionFilterOptions(collection?.name);
const userFields = useCollectionFilterOptions('users'); const userFields = useCollectionFilterOptions('users');
const dateTimeFields = ['createdAt', 'datetime', 'time', 'updatedAt']; const dateTimeFields = ['createdAt', 'datetime', 'time', 'updatedAt'];
useEffect(() => { useEffect(() => {

View File

@ -1,14 +1,13 @@
import React, { useContext } from 'react';
import { FormDialog, FormLayout } from '@formily/antd';
import { FormOutlined } from '@ant-design/icons'; import { FormOutlined } from '@ant-design/icons';
import { FormDialog, FormLayout } from '@formily/antd';
import { SchemaOptionsContext } from '@formily/react'; import { SchemaOptionsContext } from '@formily/react';
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCollection, useCollectionManager } from '../../collection-manager'; import { useCollectionManager } from '../../collection-manager';
import { SchemaComponent, SchemaComponentOptions, useCompile } from '../../schema-component'; import { SchemaComponent, SchemaComponentOptions } from '../../schema-component';
import { createCalendarBlockSchema } from '../utils'; import { createCalendarBlockSchema } from '../utils';
import { DataBlockInitializer } from './DataBlockInitializer'; import { DataBlockInitializer } from './DataBlockInitializer';
import { CascaderProps } from 'antd';
export const CalendarBlockInitializer = (props) => { export const CalendarBlockInitializer = (props) => {
const { insert } = props; const { insert } = props;

View File

@ -3,8 +3,6 @@ import React from 'react';
import { SchemaInitializer } from '..'; import { SchemaInitializer } from '..';
import { useCurrentSchema } from '../utils'; import { useCurrentSchema } from '../utils';
import { useBlockRequestContext } from '../../block-provider';
import { useCollection } from '../../collection-manager';
export const InitializerWithSwitch = (props) => { export const InitializerWithSwitch = (props) => {
const { type, schema, item, insert, remove: passInRemove } = props; const { type, schema, item, insert, remove: passInRemove } = props;
@ -14,6 +12,7 @@ export const InitializerWithSwitch = (props) => {
item.find, item.find,
passInRemove ?? item.remove, passInRemove ?? item.remove,
); );
return ( return (
<SchemaInitializer.SwitchItem <SchemaInitializer.SwitchItem
checked={exists} checked={exists}

View File

@ -1,8 +1,10 @@
import { ISchema, Schema, useFieldSchema, useForm } from '@formily/react'; import { ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import React, { useContext, useMemo, useState } from 'react'; import { error } from '@nocobase/utils/client';
import _ from 'lodash';
import React, { useCallback, useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { BlockRequestContext, SchemaInitializerItemOptions } from '../'; import { BlockRequestContext, SchemaInitializerButtonContext, SchemaInitializerItemOptions } from '../';
import { FieldOptions, useCollection, useCollectionManager } from '../collection-manager'; import { FieldOptions, useCollection, useCollectionManager } from '../collection-manager';
import { isAssocField } from '../filter-provider/utils'; import { isAssocField } from '../filter-provider/utils';
import { useActionContext, useDesignable } from '../schema-component'; import { useActionContext, useDesignable } from '../schema-component';
@ -91,6 +93,7 @@ export const useTableColumnInitializerFields = () => {
const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable'; const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable';
const form = useForm(); const form = useForm();
const isReadPretty = isSubTable ? form.readPretty : true; const isReadPretty = isSubTable ? form.readPretty : true;
return currentFields return currentFields
.filter( .filter(
(field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey && !field?.treeChildren, (field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey && !field?.treeChildren,
@ -201,6 +204,10 @@ export const useAssociatedTableColumnInitializerFields = () => {
export const useInheritsTableColumnInitializerFields = () => { export const useInheritsTableColumnInitializerFields = () => {
const { name } = useCollection(); const { name } = useCollection();
const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = useCollectionManager(); const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = useCollectionManager();
const fieldSchema = useFieldSchema();
const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable';
const form = useForm();
const isReadPretty = isSubTable ? form.readPretty : true;
const inherits = getInheritCollections(name); const inherits = getInheritCollections(name);
return inherits?.map((v) => { return inherits?.map((v) => {
const fields = getParentCollectionFields(v, name); const fields = getParentCollectionFields(v, name);
@ -212,12 +219,30 @@ export const useInheritsTableColumnInitializerFields = () => {
}) })
.map((k) => { .map((k) => {
const interfaceConfig = getInterface(k.interface); const interfaceConfig = getInterface(k.interface);
const isFileCollection = k?.target && getCollection(k?.target)?.template === 'file';
const schema = { const schema = {
name: `${k.name}`, name: `${k.name}`,
'x-component': 'CollectionField', 'x-component': 'CollectionField',
'x-read-pretty': true, 'x-read-pretty': isReadPretty || k.uiSchema?.['x-read-pretty'],
'x-collection-field': `${name}.${k.name}`, 'x-collection-field': `${name}.${k.name}`,
'x-component-props': {}, 'x-component-props': isFileCollection
? {
fieldNames: {
label: 'preview',
value: 'id',
},
}
: {},
'x-decorator': isSubTable
? quickEditField.includes(k.interface) || isFileCollection
? 'QuickEdit'
: 'FormItem'
: null,
'x-decorator-props': {
labelStyle: {
display: 'none',
},
},
}; };
return { return {
type: 'item', type: 'item',
@ -807,24 +832,28 @@ export const useCollectionDataSourceItems = (componentName) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { collections, getCollectionFields } = useCollectionManager(); const { collections, getCollectionFields } = useCollectionManager();
const { getTemplatesByCollection } = useSchemaTemplateManager(); const { getTemplatesByCollection } = useSchemaTemplateManager();
const [selected, setSelected] = useState([]); const { searchValue, setSearchValue } = useContext(SchemaInitializerButtonContext);
const [value, onChange] = useState(null); // eslint-disable-next-line react-hooks/exhaustive-deps
const onChange = useCallback(_.debounce(setSearchValue, 300), [setSearchValue]);
if (!setSearchValue) {
error('useCollectionDataSourceItems: please use in SchemaInitializerButtonContext and provide setSearchValue');
return [];
}
const clearKeywords = () => { const clearKeywords = () => {
setSelected([]); setSearchValue('');
onChange(null);
}; };
return [ return [
{ {
key: 'tableBlock', key: 'tableBlock',
type: 'itemGroup', type: 'itemGroup',
title: React.createElement(SelectCollection, { title: React.createElement(SelectCollection, {
value, value: searchValue,
onChange, onChange,
setSelected,
}), }),
children: collections children: collections
?.filter((item) => { ?.filter((item) => {
const b = !value || selected.includes(item.name);
if (item.inherit) { if (item.inherit) {
return false; return false;
} }
@ -836,7 +865,12 @@ export const useCollectionDataSourceItems = (componentName) => {
} else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) { } else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) {
return false; return false;
} else { } else {
return b && !(item?.isThrough && item?.autoCreate); if (!item.title) {
return false;
}
return (
item.title.toUpperCase().includes(searchValue.toUpperCase()) && !(item?.isThrough && item?.autoCreate)
);
} }
}) })
?.map((item, index) => { ?.map((item, index) => {

View File

@ -11,8 +11,8 @@ import {
CascaderProps, CascaderProps,
Dropdown, Dropdown,
Empty, Empty,
Menu,
MenuItemProps, MenuItemProps,
MenuProps,
Modal, Modal,
Select, Select,
Space, Space,
@ -46,6 +46,7 @@ import {
} from '..'; } from '..';
import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks'; import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks';
import { FilterBlockType, isSameCollection, useSupportedBlocks } from '../filter-provider/utils'; import { FilterBlockType, isSameCollection, useSupportedBlocks } from '../filter-provider/utils';
import { useCollectMenuItem, useCollectMenuItems, useMenuItem } from '../hooks/useMenuItem';
import { getTargetKey } from '../schema-component/antd/association-filter/utilts'; import { getTargetKey } from '../schema-component/antd/association-filter/utilts';
import { useSchemaTemplateManager } from '../schema-templates'; import { useSchemaTemplateManager } from '../schema-templates';
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
@ -53,7 +54,6 @@ import { FormDataTemplates } from './DataTemplates';
import { EnableChildCollections } from './EnableChildCollections'; import { EnableChildCollections } from './EnableChildCollections';
import { FormLinkageRules } from './LinkageRules'; import { FormLinkageRules } from './LinkageRules';
import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks'; import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks';
import { MenuDividerProps } from 'antd/lib/menu';
interface SchemaSettingsProps { interface SchemaSettingsProps {
title?: any; title?: any;
@ -117,37 +117,60 @@ export const SchemaSettingsProvider: React.FC<SchemaSettingsProviderProps> = (pr
); );
}; };
const overlayClassName = classNames(
'nb-schema-initializer-button-overlay',
css`
.ant-dropdown-menu-item-group-list {
max-height: 40vh;
overflow: auto;
}
`,
);
export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNested = (props) => { export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNested = (props) => {
const { title, dn, ...others } = props; const { title, dn, ...others } = props;
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const DropdownMenu = ( const { Component, getMenuItems } = useMenuItem();
<Dropdown const [shouldRender, setShouldRender] = useState(false);
open={visible}
onOpenChange={(visible) => { if (!shouldRender) {
setVisible(visible); return (
}} <div
overlay={<Menu>{props.children as any}</Menu>} onMouseEnter={() => {
overlayClassName={classNames( setShouldRender(true);
'nb-schema-initializer-button-overlay', setVisible(true);
css` }}
.ant-dropdown-menu-item-group-list { >
max-height: 40vh; {typeof title === 'string' ? <span>{title}</span> : title}
overflow: auto; </div>
} );
`, }
)}
> const dropdownMenu = () => (
{typeof title === 'string' ? <span>{title}</span> : title} <>
</Dropdown> <Component />
<Dropdown
open={visible}
onOpenChange={() => {
setShouldRender(false);
setVisible(false);
}}
menu={{ items: getMenuItems(() => props.children) }}
overlayClassName={overlayClassName}
>
{typeof title === 'string' ? <span>{title}</span> : title}
</Dropdown>
</>
); );
if (dn) { if (dn) {
return ( return (
<SchemaSettingsProvider visible={visible} setVisible={setVisible} dn={dn} {...others}> <SchemaSettingsProvider visible={visible} setVisible={setVisible} dn={dn} {...others}>
{DropdownMenu} {dropdownMenu()}
</SchemaSettingsProvider> </SchemaSettingsProvider>
); );
} }
return DropdownMenu; return dropdownMenu();
}; };
SchemaSettings.Template = function Template(props) { SchemaSettings.Template = function Template(props) {
@ -388,35 +411,70 @@ SchemaSettings.FormItemTemplate = function FormItemTemplate(props) {
}; };
SchemaSettings.Item = function Item(props) { SchemaSettings.Item = function Item(props) {
const { pushMenuItem } = useCollectMenuItems();
const { collectMenuItem } = useCollectMenuItem();
const { eventKey } = props; const { eventKey } = props;
const key = useMemo(() => uid(), []); const key = useMemo(() => uid(), []);
return ( const item = {
<Menu.Item ..._.omit(props, ['children']),
key={key} key,
eventKey={(eventKey as any) || key} eventKey: (eventKey as any) || key,
{...props} onClick: (info) => {
onClick={(info) => { info.domEvent.preventDefault();
info.domEvent.preventDefault(); info.domEvent.stopPropagation();
info.domEvent.stopPropagation(); props?.onClick?.(info);
props?.onClick?.(info); },
}} style: { minWidth: 120 },
style={{ minWidth: 120 }} label: props.children || props.title,
> title: props.title,
{props.children || props.title} } as MenuProps['items'][0];
</Menu.Item>
); pushMenuItem?.(item);
collectMenuItem?.(item);
return null;
}; };
SchemaSettings.ItemGroup = (props) => { SchemaSettings.ItemGroup = function ItemGroup(props) {
return <Menu.ItemGroup {...props} />; const { Component, getMenuItems } = useMenuItem();
const { pushMenuItem } = useCollectMenuItems();
const key = useMemo(() => uid(), []);
const item = {
key,
type: 'group',
title: props.title,
label: props.title,
children: getMenuItems(() => props.children),
} as MenuProps['items'][0];
pushMenuItem(item);
return <Component />;
}; };
SchemaSettings.SubMenu = (props) => { SchemaSettings.SubMenu = function SubMenu(props) {
return <Menu.SubMenu {...props} />; const { Component, getMenuItems } = useMenuItem();
const { pushMenuItem } = useCollectMenuItems();
const key = useMemo(() => uid(), []);
const item = {
key,
label: props.title,
title: props.title,
children: getMenuItems(() => props.children),
} as MenuProps['items'][0];
pushMenuItem(item);
return <Component />;
}; };
SchemaSettings.Divider = (props: MenuDividerProps) => { SchemaSettings.Divider = function Divider() {
return <Menu.Divider {...props} />; const { pushMenuItem } = useCollectMenuItems();
const key = useMemo(() => uid(), []);
const item = {
key,
type: 'divider',
} as MenuProps['items'][0];
pushMenuItem(item);
return null;
}; };
SchemaSettings.Remove = function Remove(props: any) { SchemaSettings.Remove = function Remove(props: any) {
@ -470,6 +528,7 @@ SchemaSettings.ConnectDataBlocks = function ConnectDataBlocks(props: {
const collection = useCollection(); const collection = useCollection();
const { inProvider } = useFilterBlock(); const { inProvider } = useFilterBlock();
const dataBlocks = useSupportedBlocks(type); const dataBlocks = useSupportedBlocks(type);
// eslint-disable-next-line prefer-const
let { targets = [], uid } = findFilterTargets(fieldSchema); let { targets = [], uid } = findFilterTargets(fieldSchema);
const compile = useCompile(); const compile = useCompile();
@ -578,11 +637,13 @@ SchemaSettings.ConnectDataBlocks = function ConnectDataBlocks(props: {
{Content.length ? ( {Content.length ? (
Content Content
) : ( ) : (
<Empty <SchemaSettings.Item>
style={{ width: 160, padding: '0 1em' }} <Empty
description={emptyDescription} style={{ width: 160, padding: '0 1em' }}
image={Empty.PRESENTED_IMAGE_SIMPLE} description={emptyDescription}
/> image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</SchemaSettings.Item>
)} )}
</SchemaSettings.SubMenu> </SchemaSettings.SubMenu>
); );
@ -755,7 +816,7 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => {
title={compile(title)} title={compile(title)}
{...others} {...others}
destroyOnClose destroyOnClose
visible={visible} open={visible}
onCancel={cancelHandler} onCancel={cancelHandler}
footer={ footer={
<Space> <Space>
@ -1105,7 +1166,6 @@ SchemaSettings.EnableChildCollections = function EnableChildCollectionsItem(prop
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
title={t('Enable child collections')} title={t('Enable child collections')}
components={{ ArrayItems, FormLayout }} components={{ ArrayItems, FormLayout }}
width={600}
schema={ schema={
{ {
type: 'object', type: 'object',

View File

@ -0,0 +1,118 @@
import { useMemo } from 'react';
import { useCompile, useGetFilterOptions } from '../../../schema-component';
import { Schema } from '@formily/react';
import { FieldOption, Option } from '../type';
interface GetOptionsParams {
depth: number;
operator?: string;
maxDepth: number;
count?: number;
loadChildren?: (option: Option) => Promise<void>;
getFilterOptions?: (collectionName: string) => any[];
compile: (value: string) => any;
}
const getChildren = (
options: FieldOption[],
{ depth, maxDepth, loadChildren, compile }: GetOptionsParams,
): Option[] => {
const result = options
.map((option): Option => {
if (!option.target) {
return {
key: option.name,
value: option.name,
label: compile(option.title),
depth,
};
}
if (depth >= maxDepth) {
return null;
}
return {
key: option.name,
value: option.name,
label: compile(option.title),
children: [],
isLeaf: false,
field: option,
depth,
loadChildren,
};
})
.filter(Boolean);
return result;
};
export const useFormVariable = ({
blockForm,
rootCollection,
operator,
schema,
level,
}: {
blockForm?: any;
rootCollection: string;
operator?: any;
schema: Schema;
level?: number;
}) => {
const compile = useCompile();
const getFilterOptions = useGetFilterOptions();
const loadChildren = (option: any): Promise<void> => {
if (!option.field?.target) {
return new Promise((resolve) => {
resolve(void 0);
});
}
const collectionName = option.field.target;
const fields = getFilterOptions(collectionName);
const allowFields =
option.depth === 0
? fields.filter((field) => {
return Object.keys(blockForm.fields).some((name) => name.includes(`.${field.name}`));
})
: fields;
return new Promise((resolve) => {
setTimeout(() => {
const children =
getChildren(allowFields, {
depth: option.depth + 1,
maxDepth: 4,
loadChildren,
compile,
}) || [];
if (children.length === 0) {
option.disabled = true;
resolve();
return;
}
option.children = children;
resolve();
// 延迟 5 毫秒,防止阻塞主线程,导致 UI 卡顿
}, 5);
});
};
const result = useMemo(() => {
return (
blockForm && {
label: `{{t("Current form")}}`,
value: '$form',
key: '$form',
children: [],
isLeaf: false,
field: {
target: rootCollection,
},
depth: 0,
loadChildren,
}
);
}, [rootCollection]);
return result;
};

View File

@ -0,0 +1,94 @@
import { useMemo } from 'react';
import { useCollectionManager } from '../../../collection-manager';
import { useCompile, useGetFilterOptions } from '../../../schema-component';
interface GetOptionsParams {
schema: any;
operator?: string;
maxDepth: number;
count?: number;
getFilterOptions: (collectionName: string) => any[];
}
const getChildren = (options: any[], { schema, operator, maxDepth, count = 1, getFilterOptions }: GetOptionsParams) => {
if (count > maxDepth) {
return [];
}
const result = options.map((option) => {
if ((option.type !== 'belongsTo' && option.type !== 'hasOne') || !option.target) {
return {
key: option.name,
value: option.name,
label: option.title,
// TODO: 现在是通过组件的名称来过滤能够被选择的选项,这样的坏处是不够精确,后续可以优化
// disabled: schema?.['x-component'] !== option.schema?.['x-component'],
disabled: false,
};
}
const children =
getChildren(getFilterOptions(option.target), {
schema,
operator,
maxDepth,
count: count + 1,
getFilterOptions,
}) || [];
return {
key: option.name,
value: option.name,
label: option.title,
children,
disabled: children.every((child) => child.disabled),
};
});
return result;
};
export const useIterationVariable = ({
blockForm,
collectionField,
operator,
schema,
level,
rootCollection,
}: {
blockForm?: any;
collectionField: any;
operator?: any;
schema: any;
level?: number;
rootCollection?: string;
}) => {
const compile = useCompile();
const getFilterOptions = useGetFilterOptions();
const fields = getFilterOptions(collectionField?.collectionName);
const children = useMemo(() => {
const allowFields = fields.filter((field) => {
return Object.keys(blockForm.fields).some((name) => name.includes(field.name));
});
return (
getChildren(allowFields, {
schema,
operator,
maxDepth: level || 3,
getFilterOptions,
}) || []
);
}, [operator, schema, blockForm]);
return useMemo(() => {
return rootCollection !== collectionField?.collectionName && children.length > 0
? compile({
label: `{{t("Current object")}}`,
value: '$iteration',
key: '$iteration',
disabled: children.every((option) => option.disabled),
children: children,
})
: null;
}, [children]);
};

View File

@ -2,13 +2,20 @@ import { useMemo } from 'react';
import { useValues } from '../../../schema-component/antd/filter/useValues'; import { useValues } from '../../../schema-component/antd/filter/useValues';
import { useDateVariable } from './useDateVariable'; import { useDateVariable } from './useDateVariable';
import { useUserVariable } from './useUserVariable'; import { useUserVariable } from './useUserVariable';
import { useFormVariable } from './useFormVariable';
import { useIterationVariable } from './useIterationVariable';
export const useVariableOptions = () => { export const useVariableOptions = ({ form, collectionField, rootCollection }) => {
const { operator, schema } = useValues(); const { operator, schema } = useValues();
const userVariable = useUserVariable({ maxDepth: 3, schema }); const userVariable = useUserVariable({ maxDepth: 3, schema });
const dateVariable = useDateVariable({ operator, schema }); const dateVariable = useDateVariable({ operator, schema });
const formVariabele = useFormVariable({ blockForm: form, rootCollection, schema });
const iterationVariabele = useIterationVariable({ blockForm: form, collectionField, schema, rootCollection });
const result = useMemo(() => [userVariable, dateVariable], [dateVariable, userVariable]); const result = useMemo(
() => [userVariable, dateVariable, formVariabele, iterationVariabele].filter(Boolean),
[dateVariable, userVariable, formVariabele, iterationVariabele],
);
if (!operator || !schema) return []; if (!operator || !schema) return [];

View File

@ -1,4 +1,5 @@
import { PageHeader as AntdPageHeader, Input, Spin } from 'antd'; import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout';
import { Input, Spin } from 'antd';
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useAPIClient, useRequest, useSchemaTemplateManager } from '..'; import { useAPIClient, useRequest, useSchemaTemplateManager } from '..';
@ -75,6 +76,7 @@ export const BlockTemplateDetails = () => {
return ( return (
<div> <div>
<AntdPageHeader <AntdPageHeader
style={{ backgroundColor: 'white' }}
onBack={() => { onBack={() => {
navigate('/admin/plugins/block-templates'); navigate('/admin/plugins/block-templates');
}} }}

View File

@ -1,4 +1,4 @@
import { PageHeader as AntdPageHeader } from 'antd'; import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CollectionManagerProvider } from '../collection-manager'; import { CollectionManagerProvider } from '../collection-manager';
@ -10,7 +10,7 @@ export const BlockTemplatePage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div> <div>
<AntdPageHeader ghost={false} title={t('Block templates')} /> <AntdPageHeader style={{ backgroundColor: 'white' }} ghost={false} title={t('Block templates')} />
<div style={{ margin: 'var(--nb-spacing)' }}> <div style={{ margin: 'var(--nb-spacing)' }}>
<CollectionManagerProvider collections={[uiSchemaTemplatesCollection]}> <CollectionManagerProvider collections={[uiSchemaTemplatesCollection]}>
<SchemaComponent schema={uiSchemaTemplatesSchema} /> <SchemaComponent schema={uiSchemaTemplatesSchema} />

View File

@ -1,278 +0,0 @@
import { FormButtonGroup, FormDialog, FormDrawer, FormItem, FormLayout, Reset, Submit } from '@formily/antd';
import { createForm, Field, ObjectField, onFormValuesChange } from '@formily/core';
import {
FieldContext,
FormContext,
observer,
RecursionField,
Schema,
SchemaOptionsContext,
useField,
useFieldSchema,
useForm,
} from '@formily/react';
import { Dropdown, Menu, Modal, Select, Switch } from 'antd';
import React, { createContext, useContext, useMemo, useState } from 'react';
import { SchemaComponentOptions, useAttach, useDesignable } from '..';
export interface SettingsFormContextProps {
field?: Field;
fieldSchema?: Schema;
dropdownVisible?: boolean;
setDropdownVisible?: (v: boolean) => void;
dn?: any;
}
export const SettingsFormContext = createContext<SettingsFormContextProps>(null);
export const useSettingsFormContext = () => {
return useContext(SettingsFormContext);
};
export const SettingsForm: any = observer(
(props: any) => {
const dn = useDesignable();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const [dropdownVisible, setDropdownVisible] = useState(false);
const settingsFormSchema = useMemo(() => new Schema(props.schema), []);
const form = useMemo(
() =>
createForm({
initialValues: fieldSchema.toJSON(),
effects(form) {
onFormValuesChange((form) => {
dn.patch(form.values);
console.log('form.values', form.values);
});
},
}),
[],
);
const f = useAttach(form.createVoidField({ ...field.props, basePath: '' }));
return (
<SettingsFormContext.Provider value={{ dn, field, fieldSchema, dropdownVisible, setDropdownVisible }}>
<SchemaComponentOptions components={{ SettingsForm }}>
<FieldContext.Provider value={null}>
<FormContext.Provider value={form}>
<FieldContext.Provider value={f}>
<Dropdown
open={dropdownVisible}
onOpenChange={(visible) => setDropdownVisible(visible)}
overlayStyle={{ width: 200 }}
overlay={
<Menu>
{settingsFormSchema.mapProperties((s, key) => {
return <RecursionField name={key} schema={s} />;
})}
</Menu>
}
>
<a></a>
</Dropdown>
</FieldContext.Provider>
</FormContext.Provider>
</FieldContext.Provider>
</SchemaComponentOptions>
</SettingsFormContext.Provider>
);
},
{ displayName: 'SettingsForm' },
);
SettingsForm.Divider = () => {
return <Menu.Divider />;
};
SettingsForm.Remove = (props) => {
const field = useField();
const { dn, setDropdownVisible } = useSettingsFormContext();
return (
<Menu.Item
onClick={() => {
setDropdownVisible(false);
Modal.confirm({
title: 'Are you sure delete this task?',
content: 'Some descriptions',
okText: 'Yes',
okType: 'danger',
cancelText: 'No',
...props.confirm,
onOk() {
dn.remove();
console.log('OK');
},
onCancel() {
console.log('Cancel');
},
});
}}
>
{field.title}
</Menu.Item>
);
};
SettingsForm.Switch = observer(
() => {
const field = useField<Field>();
return (
<Menu.Item
onClick={() => {
field.value = !field.value;
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
{field.title} <Switch checked={!!field.value} />
</div>
</Menu.Item>
);
},
{ displayName: 'SettingsForm' },
);
SettingsForm.Select = observer(
(props) => {
const field = useField<Field>();
const [open, setOpen] = useState(false);
return (
<Menu.Item onClick={() => !open && setOpen(true)}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
{field.title}
<Select
open={open}
onDropdownVisibleChange={(open) => setOpen(open)}
onSelect={() => {
setOpen(false);
}}
onChange={(value) => {
field.value = value;
}}
value={field.value}
options={field.dataSource}
style={{ width: '60%' }}
size={'small'}
bordered={false}
/>
</div>
</Menu.Item>
);
},
{ displayName: 'SettingsForm' },
);
SettingsForm.Modal = () => {
const form = useForm();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const options = useContext(SchemaOptionsContext);
const { setDropdownVisible } = useSettingsFormContext();
return (
<Menu.Item
style={{ width: 200 }}
onClick={async () => {
setDropdownVisible(false);
const values = await FormDialog('Title', () => {
return (
<SchemaComponentOptions scope={options.scope} components={{ ...options.components, FormItem }}>
<FormLayout layout={'vertical'}>
<RecursionField schema={fieldSchema} onlyRenderProperties />
</FormLayout>
</SchemaComponentOptions>
);
}).open({
initialValues: fieldSchema.type !== 'void' ? field.value : form.values,
});
if (fieldSchema.type !== 'void') {
form.setValues(
{
[fieldSchema.name]: values,
},
'deepMerge',
);
} else {
form.setValues(values);
}
}}
>
{field.title}
</Menu.Item>
);
};
SettingsForm.Drawer = () => {
const form = useForm();
const field = useField<ObjectField>();
const fieldSchema = useFieldSchema();
const options = useContext(SchemaOptionsContext);
const { setDropdownVisible } = useSettingsFormContext();
return (
<Menu.Item
style={{ width: 200 }}
onClick={async () => {
setDropdownVisible(false);
const values = await FormDrawer('Popup form', () => {
return (
<SchemaComponentOptions scope={options.scope} components={{ ...options.components, FormItem }}>
<FormLayout layout={'vertical'}>
<RecursionField schema={fieldSchema} onlyRenderProperties />
<FormDrawer.Footer>
<FormButtonGroup align="right">
<Reset>Reset</Reset>
<Submit
onSubmit={() => {
return new Promise((resolve) => {
setTimeout(resolve, 1000);
});
}}
>
Submit
</Submit>
</FormButtonGroup>
</FormDrawer.Footer>
</FormLayout>
</SchemaComponentOptions>
);
}).open({
initialValues: fieldSchema.type !== 'void' ? field.value : form.values,
});
if (fieldSchema.type !== 'void') {
form.setValues(
{
[fieldSchema.name]: values,
},
'deepMerge',
);
} else {
form.setValues(values);
}
}}
>
{field.title}
</Menu.Item>
);
};
SettingsForm.SubMenu = () => {
const field = useField();
const fieldSchema = useFieldSchema();
return (
<Menu.SubMenu title={field.title}>
{fieldSchema.mapProperties((schema, key) => {
return <RecursionField name={key} schema={schema} />;
})}
</Menu.SubMenu>
);
};
SettingsForm.ItemGroup = () => {
const field = useField();
const fieldSchema = useFieldSchema();
return (
<Menu.ItemGroup title={field.title}>
{fieldSchema.mapProperties((schema, key) => {
return <RecursionField name={key} schema={schema} />;
})}
</Menu.ItemGroup>
);
};

View File

@ -1,136 +0,0 @@
import { ISchema, observer, useFieldSchema } from '@formily/react';
import { AntdSchemaComponentProvider, SchemaComponent, SchemaComponentProvider, SettingsForm } from '@nocobase/client';
import React from 'react';
const schema: ISchema = {
type: 'object',
properties: {
'x-component-props.switch': {
title: 'Switch',
'x-component': 'SettingsForm.Switch',
},
'x-component-props.select': {
title: 'Select',
'x-component': 'SettingsForm.Select',
enum: [
{ label: 'Option1', value: 'option1' },
{ label: 'Option2', value: 'option2' },
{ label: 'Option3', value: 'option3' },
],
},
modal: {
type: 'void',
title: 'Open Modal',
'x-component': 'SettingsForm.Modal',
'x-component-props': {},
properties: {
'x-component-props.title': {
title: '标题',
'x-component': 'Input',
'x-decorator': 'FormItem',
},
},
},
drawer: {
type: 'void',
title: 'Open Drawer',
'x-component': 'SettingsForm.Drawer',
properties: {
'x-component-props.title': {
title: '标题',
'x-component': 'Input',
'x-decorator': 'FormItem',
},
},
},
group: {
type: 'void',
title: 'ItemGroup',
'x-component': 'SettingsForm.ItemGroup',
properties: {
'x-component-props': {
type: 'object',
title: 'Open Modal',
'x-component': 'SettingsForm.Modal',
properties: {
title: {
title: '标题',
'x-component': 'Input',
'x-decorator': 'FormItem',
},
},
},
},
},
submenu: {
type: 'void',
title: 'SubMenu',
'x-component': 'SettingsForm.SubMenu',
properties: {
'x-component-props': {
type: 'object',
title: 'Open Modal',
'x-component': 'SettingsForm.Modal',
properties: {
title: {
title: '标题',
'x-component': 'Input',
'x-decorator': 'FormItem',
},
},
},
},
},
divider: {
'x-component': 'SettingsForm.Divider',
},
remove: {
title: 'Delete',
'x-component': 'SettingsForm.Remove',
'x-component-props': {
confirm: {
title: 'Are you sure delete this task?',
content: 'Some descriptions',
},
},
},
},
};
const Hello = observer(
(props: any) => {
const fieldSchema = useFieldSchema();
return (
<div>
<pre>{JSON.stringify(props, null, 2)}</pre>
<pre>{JSON.stringify(fieldSchema.toJSON(), null, 2)}</pre>
<SettingsForm schema={schema} />
</div>
);
},
{ displayName: 'Hello' },
);
export default () => {
return (
<SchemaComponentProvider components={{ Hello }}>
<AntdSchemaComponentProvider>
<SchemaComponent
schema={{
type: 'object',
properties: {
hello: {
'x-component': 'Hello',
'x-component-props': {
title: 'abc',
switch: true,
select: 'option1',
},
},
},
}}
/>
</AntdSchemaComponentProvider>
</SchemaComponentProvider>
);
};

View File

@ -1,9 +0,0 @@
---
group:
title: Client
order: 1
---
# SettingsForm
<code src="./demos/demo1.tsx"></code>

View File

@ -1 +0,0 @@
export * from './SettingsForm';

View File

@ -1,11 +1,10 @@
import { ISchema, useForm } from '@formily/react'; import { ISchema, useForm } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { Menu } from 'antd'; import { MenuProps } from 'antd';
import React, { useContext, useState } from 'react'; import React, { useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ActionContextProvider, SchemaComponent, useActionContext } from '../'; import { ActionContextProvider, DropdownVisibleContext, SchemaComponent, useActionContext } from '../';
import { useAPIClient } from '../api-client'; import { useAPIClient } from '../api-client';
import { DropdownVisibleContext } from './CurrentUser';
const useCloseAction = () => { const useCloseAction = () => {
const { setVisible } = useActionContext(); const { setVisible } = useActionContext();
@ -114,23 +113,29 @@ const schema: ISchema = {
}, },
}; };
export const ChangePassword = () => { export const useChangePassword = () => {
const ctx = useContext(DropdownVisibleContext);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const ctx = useContext(DropdownVisibleContext);
return ( return useMemo<MenuProps['items'][0]>(() => {
<ActionContextProvider value={{ visible, setVisible }}> return {
<Menu.Item key: 'password',
key="password" eventKey: 'ChangePassword',
eventKey={'ChangePassword'} onClick: () => {
onClick={() => { setVisible(true);
ctx?.setVisible?.(false); ctx?.setVisible(false);
setVisible(true); },
}} label: (
> <>
{t('Change password')} {t('Change password')}
</Menu.Item> <ActionContextProvider value={{ visible, setVisible }}>
<SchemaComponent scope={{ useCloseAction, useSaveCurrentUserValues }} schema={schema} /> <div onClick={(e) => e.stopPropagation()}>
</ActionContextProvider> <SchemaComponent scope={{ useCloseAction, useSaveCurrentUserValues }} schema={schema} />
); </div>
</ActionContextProvider>
</>
),
};
}, [visible]);
}; };

View File

@ -1,22 +1,25 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Dropdown, Menu, Modal } from 'antd'; import { error } from '@nocobase/utils/client';
import React, { createContext, useState } from 'react'; import { Dropdown, Menu, MenuProps, Modal } from 'antd';
import React, { createContext, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useACLRoleContext, useAPIClient, useCurrentUserContext } from '..'; import { useACLRoleContext, useAPIClient, useCurrentUserContext } from '..';
import { useCurrentAppInfo } from '../appInfo/CurrentAppInfoProvider'; import { useCurrentAppInfo } from '../appInfo/CurrentAppInfoProvider';
import { ChangePassword } from './ChangePassword'; import { useChangePassword } from './ChangePassword';
import { EditProfile } from './EditProfile'; import { useEditProfile } from './EditProfile';
import { LanguageSettings } from './LanguageSettings'; import { useLanguageSettings } from './LanguageSettings';
import { SwitchRole } from './SwitchRole'; import { useSwitchRole } from './SwitchRole';
import { ThemeSettings } from './ThemeSettings'; import { useThemeSettings } from './ThemeSettings';
const ApplicationVersion = () => { const useApplicationVersion = () => {
const data = useCurrentAppInfo(); const data = useCurrentAppInfo();
return ( return useMemo(() => {
<Menu.Item key="version" disabled> return {
Version {data?.data?.version} key: 'version',
</Menu.Item> disabled: true,
); label: `Version ${data?.data?.version}`,
};
}, [data?.data?.version]);
}; };
/** /**
@ -32,7 +35,7 @@ export const SettingsMenu: React.FC<{
const api = useAPIClient(); const api = useAPIClient();
const { t } = useTranslation(); const { t } = useTranslation();
const silenceApi = useAPIClient(); const silenceApi = useAPIClient();
const check = async () => { const check = useCallback(async () => {
return await new Promise((resolve) => { return await new Promise((resolve) => {
const heartbeat = setInterval(() => { const heartbeat = setInterval(() => {
silenceApi silenceApi
@ -47,75 +50,100 @@ export const SettingsMenu: React.FC<{
} }
return res; return res;
}) })
.catch(() => { .catch((err) => {
// ignore error(err);
}); });
}, 3000); }, 3000);
}); });
}; }, [silenceApi]);
return ( const divider = useMemo<MenuProps['items'][0]>(() => {
<Menu> return {
<ApplicationVersion /> type: 'divider',
<Menu.Divider /> };
<EditProfile /> }, []);
<ChangePassword /> const appVersion = useApplicationVersion();
<Menu.Divider /> const editProfile = useEditProfile();
<SwitchRole /> const changePassword = useChangePassword();
<LanguageSettings /> const switchRole = useSwitchRole();
<ThemeSettings /> const languageSettings = useLanguageSettings();
<Menu.Divider /> const themeSettings = useThemeSettings();
{appAllowed && ( const controlApp = useMemo<MenuProps['items']>(() => {
<> if (!appAllowed) {
<Menu.Item return [];
key="cache" }
onClick={async () => {
await api.resource('app').clearCache(); return [
{
key: 'cache',
label: t('Clear cache'),
onClick: async () => {
await api.resource('app').clearCache();
window.location.reload();
},
},
{
key: 'reboot',
label: t('Reboot application'),
onClick: async () => {
Modal.confirm({
title: t('Reboot application'),
content: t('The will interrupt service, it may take a few seconds to restart. Are you sure to continue?'),
okText: t('Reboot'),
okButtonProps: {
danger: true,
},
onOk: async () => {
await api.resource('app').reboot();
await check();
window.location.reload(); window.location.reload();
}} },
> });
{t('Clear cache')} },
</Menu.Item> },
<Menu.Item divider,
key="reboot" ];
onClick={async () => { }, [appAllowed, check]);
Modal.confirm({ const items = useMemo<MenuProps['items']>(() => {
title: t('Reboot application'), return [
content: t( appVersion,
'The will interrupt service, it may take a few seconds to restart. Are you sure to continue?', divider,
), editProfile,
okText: t('Reboot'), changePassword,
okButtonProps: { divider,
danger: true, switchRole,
}, languageSettings,
onOk: async () => { themeSettings,
await api.resource('app').reboot(); divider,
await check(); ...controlApp,
window.location.reload(); {
}, key: 'signout',
}); label: t('Sign out'),
}} onClick: async () => {
>
{t('Reboot application')}
</Menu.Item>
<Menu.Divider />
</>
)}
<Menu.Item
key="signout"
onClick={async () => {
await api.auth.signOut(); await api.auth.signOut();
navigate(`/signin?redirect=${encodeURIComponent(redirectUrl)}`); navigate(`/signin?redirect=${encodeURIComponent(redirectUrl)}`);
}} },
> },
{t('Sign out')} ];
</Menu.Item> }, [
</Menu> appVersion,
); changePassword,
controlApp,
divider,
editProfile,
history,
languageSettings,
switchRole,
themeSettings,
]);
return <Menu items={items} />;
}; };
export const DropdownVisibleContext = createContext(null); export const DropdownVisibleContext = createContext(null);
export const CurrentUser = () => { export const CurrentUser = () => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { data } = useCurrentUserContext(); const { data } = useCurrentUserContext();
return ( return (
<div style={{ display: 'inline-flex', verticalAlign: 'top' }}> <div style={{ display: 'inline-flex', verticalAlign: 'top' }}>
<DropdownVisibleContext.Provider value={{ visible, setVisible }}> <DropdownVisibleContext.Provider value={{ visible, setVisible }}>
@ -124,7 +152,9 @@ export const CurrentUser = () => {
onOpenChange={(visible) => { onOpenChange={(visible) => {
setVisible(visible); setVisible(visible);
}} }}
overlay={<SettingsMenu />} dropdownRender={() => {
return <SettingsMenu />;
}}
> >
<span <span
className={css` className={css`

View File

@ -1,7 +1,7 @@
import { ISchema, useForm } from '@formily/react'; import { ISchema, useForm } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { Menu } from 'antd'; import { MenuProps } from 'antd';
import React, { useContext, useState } from 'react'; import React, { useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
ActionContextProvider, ActionContextProvider,
@ -114,23 +114,32 @@ const schema: ISchema = {
}, },
}; };
export const EditProfile = () => { export const useEditProfile = () => {
const ctx = useContext(DropdownVisibleContext);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const ctx = useContext(DropdownVisibleContext);
return ( return useMemo<MenuProps['items'][0]>(() => {
<ActionContextProvider value={{ visible, setVisible }}> return {
<Menu.Item key: 'profile',
key="profile" eventKey: 'EditProfile',
eventKey={'EditProfile'} onClick: () => {
onClick={() => { setVisible(true);
setVisible(true); ctx?.setVisible(false);
ctx?.setVisible(false); },
}} label: (
> <>
{t('Edit profile')} {t('Edit profile')}
</Menu.Item> <ActionContextProvider value={{ visible, setVisible }}>
<SchemaComponent scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }} schema={schema} /> <div onClick={(e) => e.stopPropagation()}>
</ActionContextProvider> <SchemaComponent
); scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }}
schema={schema}
/>
</div>
</ActionContextProvider>
</>
),
};
}, [visible]);
}; };

View File

@ -1,65 +1,66 @@
import { css } from '@emotion/css'; import { MenuProps, Select } from 'antd';
import { Menu, Select } from 'antd'; import React, { useMemo, useState } from 'react';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAPIClient, useCurrentUserContext, useSystemSettings } from '..'; import { useAPIClient, useSystemSettings } from '..';
import locale from '../locale'; import locale from '../locale';
export const LanguageSettings = () => { export const useLanguageSettings = () => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const api = useAPIClient(); const api = useAPIClient();
const ctx = useCurrentUserContext();
const { data } = useSystemSettings(); const { data } = useSystemSettings();
const enabledLanguages: string[] = data?.data?.enabledLanguages || []; const enabledLanguages: string[] = data?.data?.enabledLanguages || [];
const result = useMemo<MenuProps['items'][0]>(() => {
return {
key: 'language',
eventKey: 'LanguageSettings',
onClick: () => {
setOpen(true);
},
label: (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
{t('Language')}{' '}
<Select
style={{ minWidth: 100 }}
bordered={false}
open={open}
onDropdownVisibleChange={(open) => {
setOpen(open);
}}
options={Object.keys(locale)
.filter((lang) => enabledLanguages.includes(lang))
.map((lang) => {
return {
label: locale[lang].label,
value: lang,
};
})}
value={i18n.language}
onChange={async (lang) => {
await api.resource('users').updateProfile({
values: {
appLang: lang,
},
});
api.auth.setLocale(lang);
await i18n.changeLanguage(lang);
window.location.reload();
}}
/>
</div>
),
};
}, [enabledLanguages, i18n, open]);
if (enabledLanguages.length < 2) { if (enabledLanguages.length < 2) {
return null; return null;
} }
// console.log('data', data?.data?.enabledLanguages);
return ( return result;
<Menu.Item
key="language"
eventKey={'LanguageSettings'}
onClick={() => {
setOpen(true);
}}
>
<div
className={css`
display: flex;
align-items: center;
justify-content: space-between;
`}
>
{t('Language')}{' '}
<Select
style={{ minWidth: 100 }}
bordered={false}
open={open}
onDropdownVisibleChange={(open) => {
setOpen(open);
}}
options={Object.keys(locale)
.filter((lang) => enabledLanguages.includes(lang))
.map((lang) => {
return {
label: locale[lang].label,
value: lang,
};
})}
value={i18n.language}
onChange={async (lang) => {
await api.resource('users').updateProfile({
values: {
appLang: lang,
},
});
api.auth.setLocale(lang);
await i18n.changeLanguage(lang);
window.location.reload();
}}
/>
</div>
</Menu.Item>
);
}; };

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { ISchema, useForm } from '@formily/react'; import { ISchema, useForm } from '@formily/react';
import { Space, Tabs } from 'antd'; import { Space, Tabs } from 'antd';
import React, { useCallback, useContext } from 'react'; import React, { useCallback } from 'react';
import { css } from '@emotion/css';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { SchemaComponent, useAPIClient, useCurrentDocumentTitle, useSystemSettings } from '..'; import { SchemaComponent, useAPIClient, useCurrentDocumentTitle, useSystemSettings } from '..';
@ -142,21 +142,30 @@ export const SigninPage = (props: SigninPageProps) => {
`} `}
> >
{smsAuthEnabled ? ( {smsAuthEnabled ? (
<Tabs defaultActiveKey="password"> <Tabs
<Tabs.TabPane tab={t('Sign in via account')} key="password"> defaultActiveKey="password"
<SchemaComponent scope={{ usePasswordSignIn }} schema={schema || passwordForm} /> items={[
</Tabs.TabPane> {
<Tabs.TabPane tab={t('Sign in via phone')} key="phone"> label: t('Sign in via account'),
<SchemaComponent key: 'password',
schema={phoneForm} children: <SchemaComponent scope={{ usePasswordSignIn }} schema={schema || passwordForm} />,
scope={{ usePhoneSignIn, ...scope }} },
components={{ {
VerificationCode, label: t('Sign in via phone'),
...components, key: 'phone',
}} children: (
/> <SchemaComponent
</Tabs.TabPane> schema={phoneForm}
</Tabs> scope={{ usePhoneSignIn, ...scope }}
components={{
VerificationCode,
...components,
}}
/>
),
},
]}
/>
) : ( ) : (
<SchemaComponent <SchemaComponent
components={{ ...components }} components={{ ...components }}

View File

@ -1,6 +1,5 @@
import { css } from '@emotion/css'; import { MenuProps, Select } from 'antd';
import { Menu, Select } from 'antd'; import React, { useMemo } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useACLRoleContext } from '../acl'; import { useACLRoleContext } from '../acl';
import { useAPIClient } from '../api-client'; import { useAPIClient } from '../api-client';
@ -26,40 +25,47 @@ const useCurrentRoles = () => {
return compile(options); return compile(options);
}; };
export const SwitchRole = () => { export const useSwitchRole = () => {
const api = useAPIClient(); const api = useAPIClient();
const roles = useCurrentRoles(); const roles = useCurrentRoles();
const { t } = useTranslation(); const { t } = useTranslation();
const result = useMemo<MenuProps['items'][0]>(() => {
return {
key: 'role',
eventKey: 'SwitchRole',
label: (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
{t('Switch role')}{' '}
<Select
style={{ minWidth: 100 }}
bordered={false}
fieldNames={{
label: 'title',
value: 'name',
}}
options={roles}
value={api.auth.role}
onChange={async (roleName) => {
api.auth.setRole(roleName);
await api.resource('users').setDefaultRole({ values: { roleName } });
location.reload();
window.location.reload();
}}
/>
</div>
),
};
}, [api, history, roles]);
if (roles.length <= 1) { if (roles.length <= 1) {
return null; return null;
} }
return (
<Menu.Item key="role" eventKey={'SwitchRole'}> return result;
<div
className={css`
display: flex;
align-items: center;
justify-content: space-between;
`}
>
{t('Switch role')}{' '}
<Select
style={{ minWidth: 100 }}
bordered={false}
fieldNames={{
label: 'title',
value: 'name',
}}
options={roles}
value={api.auth.role}
onChange={async (roleName) => {
api.auth.setRole(roleName);
await api.resource('users').setDefaultRole({ values: { roleName } });
location.reload();
window.location.reload();
}}
/>
</div>
</Menu.Item>
);
}; };

View File

@ -1,47 +1,51 @@
import { css } from '@emotion/css'; import { MenuProps, Select } from 'antd';
import { Menu, Select } from 'antd'; import React, { useMemo } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAPIClient } from '../api-client'; import { useAPIClient } from '../api-client';
import { useCurrentUserContext } from './CurrentUserProvider'; import { useCurrentUserContext } from './CurrentUserProvider';
export const ThemeSettings = () => { export const useThemeSettings = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const ctx = useCurrentUserContext(); const ctx = useCurrentUserContext();
const api = useAPIClient(); const api = useAPIClient();
return (
<Menu.Item key="theme" eventKey={'theme'}> return useMemo<MenuProps['items'][0]>(() => {
<div return {
className={css` key: 'theme',
display: flex; eventKey: 'theme',
align-items: center; label: (
justify-content: space-between; <div
`} style={{
> display: 'flex',
{t('Theme')}{' '} alignItems: 'center',
<Select justifyContent: 'space-between',
style={{ minWidth: 100 }}
bordered={false}
defaultValue={localStorage.getItem('NOCOBASE_THEME')}
options={[
{ label: t('Default theme'), value: 'default' },
{ label: t('Compact theme'), value: 'compact' },
]}
onChange={async (value) => {
await api.resource('users').update({
filterByTk: ctx.data.data.id,
values: {
systemSettings: {
...ctx.data.data.systemSettings,
theme: value,
},
},
});
localStorage.setItem('NOCOBASE_THEME', value);
window.location.reload();
}} }}
/> >
</div> {t('Theme')}{' '}
</Menu.Item> <Select
); style={{ minWidth: 100 }}
bordered={false}
defaultValue={localStorage.getItem('NOCOBASE_THEME')}
options={[
{ label: t('Default theme'), value: 'default' },
{ label: t('Compact theme'), value: 'compact' },
]}
onChange={async (value) => {
await api.resource('users').update({
filterByTk: ctx.data.data.id,
values: {
systemSettings: {
...ctx.data.data.systemSettings,
theme: value,
},
},
});
localStorage.setItem('NOCOBASE_THEME', value);
window.location.reload();
}}
/>
</div>
),
};
}, [ctx.data.data.id, ctx.data.data.systemSettings]);
}; };

View File

@ -4,7 +4,7 @@
"main": "src/index.js", "main": "src/index.js",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@umijs/utils": "^3.5.20", "@umijs/utils": "3.5.20",
"axios": "^0.26.1", "axios": "^0.26.1",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"commander": "^9.2.0", "commander": "^9.2.0",

View File

@ -398,6 +398,7 @@ describe('repository.update', () => {
fields: [ fields: [
{ type: 'string', name: 'name' }, { type: 'string', name: 'name' },
{ type: 'hasMany', name: 'comments' }, { type: 'hasMany', name: 'comments' },
{ type: 'belongsTo', name: 'user' },
], ],
}); });
Comment = db.collection({ Comment = db.collection({
@ -411,7 +412,7 @@ describe('repository.update', () => {
await db.close(); await db.close();
}); });
it('update1', async () => { it('update with filterByTk and with associations', async () => {
const user = await User.model.create<any>({ const user = await User.model.create<any>({
name: 'user1', name: 'user1',
}); });
@ -454,7 +455,7 @@ describe('repository.update', () => {
expect(updated2.posts.length).toBe(2); expect(updated2.posts.length).toBe(2);
}); });
it('update2', async () => { it('update with filterByTk', async () => {
const user = await User.model.create<any>({ const user = await User.model.create<any>({
name: 'user1', name: 'user1',
}); });
@ -463,6 +464,9 @@ describe('repository.update', () => {
name: 'user2', name: 'user2',
}); });
const hook = jest.fn();
db.on('users.afterUpdate', hook);
await User.repository.update({ await User.repository.update({
filterByTk: user.id, filterByTk: user.id,
values: { values: {
@ -470,6 +474,8 @@ describe('repository.update', () => {
}, },
}); });
expect(hook).toBeCalledTimes(1);
const updated = await User.model.findByPk(user.id); const updated = await User.model.findByPk(user.id);
expect(updated.get('name')).toEqual('user11'); expect(updated.get('name')).toEqual('user11');
@ -477,6 +483,119 @@ describe('repository.update', () => {
const u2 = await User.model.findByPk(user2.id); const u2 = await User.model.findByPk(user2.id);
expect(u2.get('name')).toEqual('user2'); expect(u2.get('name')).toEqual('user2');
}); });
it('update with filter one by one when individualHooks is not set', async () => {
const u1 = await User.repository.create({ values: { name: 'u1' } });
const p1 = await Post.repository.create({ values: { name: 'p1', userId: u1.id } });
const p2 = await Post.repository.create({ values: { name: 'p2', userId: u1.id } });
const p3 = await Post.repository.create({ values: { name: 'p3' } });
const hook = jest.fn();
db.on('posts.afterUpdate', hook);
await Post.repository.update({
filter: {
userId: u1.id,
},
values: {
name: 'pp',
},
});
const postsAfterUpdated = await Post.repository.find({ order: [['id', 'ASC']] });
expect(postsAfterUpdated[0].name).toBe('pp');
expect(postsAfterUpdated[1].name).toBe('pp');
expect(postsAfterUpdated[2].name).toBe('p3');
expect(hook).toBeCalledTimes(2);
});
it('update in batch when individualHooks is false', async () => {
const u1 = await User.repository.create({ values: { name: 'u1' } });
const p1 = await Post.repository.create({ values: { name: 'p1', userId: u1.id } });
const p2 = await Post.repository.create({ values: { name: 'p2', userId: u1.id } });
const p3 = await Post.repository.create({ values: { name: 'p3' } });
const hook = jest.fn();
db.on('posts.afterUpdate', hook);
await Post.repository.update({
filter: {
userId: u1.id,
},
values: {
name: 'pp',
},
individualHooks: false,
});
const postsAfterUpdated = await Post.repository.find({ order: [['id', 'ASC']] });
expect(postsAfterUpdated[0].name).toBe('pp');
expect(postsAfterUpdated[1].name).toBe('pp');
expect(postsAfterUpdated[2].name).toBe('p3');
expect(hook).toBeCalledTimes(0);
});
it('update in batch with belongsTo field as foreignKey', async () => {
const u1 = await User.repository.create({ values: { name: 'u1' } });
const u2 = await User.repository.create({ values: { name: 'u2' } });
const p1 = await Post.repository.create({ values: { name: 'p1', userId: u1.id } });
const p2 = await Post.repository.create({ values: { name: 'p2', userId: u1.id } });
const r1 = await Post.repository.update({
filter: {
name: p1.name,
},
values: {
user: u2.id,
},
individualHooks: false,
});
expect(r1).toEqual(1);
const p1Updated = await Post.repository.findOne({
filterByTk: p1.id,
});
expect(p1Updated.userId).toBe(u2.id);
const r2 = await Post.repository.update({
filter: {
id: p2.id,
},
values: {
user: null,
},
individualHooks: false,
});
expect(r2).toEqual(1);
const p2Updated = await Post.repository.findOne({
filterByTk: p2.id,
});
expect(p2Updated.userId).toBe(null);
const r3 = await Post.repository.update({
filter: {
id: p1.id,
},
values: {
user: { id: u1.id },
},
individualHooks: false,
});
expect(r3).toEqual(1);
const p1Updated2 = await Post.repository.findOne({
filterByTk: p1.id,
});
expect(p1Updated2.userId).toBe(u1.id);
});
}); });
describe('repository.destroy', () => { describe('repository.destroy', () => {

View File

@ -370,6 +370,7 @@ describe('One2One Association', () => {
uid: 1, uid: 1,
name: '123', name: '123',
}, },
userId: 1,
}; };
const guard = new UpdateGuard(); const guard = new UpdateGuard();
@ -381,6 +382,7 @@ describe('One2One Association', () => {
user: { user: {
uid: 1, uid: 1,
}, },
userId: 1,
}); });
guard.setAssociationKeysToBeUpdate(['user']); guard.setAssociationKeysToBeUpdate(['user']);

View File

@ -609,6 +609,45 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
const queryOptions = this.buildQueryOptions(options); const queryOptions = this.buildQueryOptions(options);
// NOTE:
// 1. better to be moved to separated API like bulkUpdate/updateMany
// 2. strictly `false` comparing for compatibility of legacy api invoking
if (options.individualHooks === false) {
const { model: Model } = this.collection;
// @ts-ignore
const primaryKeyField = Model.primaryKeyField || Model.primaryKeyAttribute;
// NOTE:
// 1. find ids first for reusing `queryOptions` logic
// 2. estimation memory usage will be N * M bytes (N = rows, M = model object memory)
// 3. would be more efficient up to 100000 ~ 1000000 rows
const rows = await Model.findAll({
...queryOptions,
attributes: [primaryKeyField],
group: `${Model.name}.${primaryKeyField}`,
include: queryOptions.include.filter((include) => {
return (
Object.keys(include.where || {}).length > 0 ||
JSON.stringify(queryOptions?.filter)?.includes(include.association)
);
}),
transaction,
});
const [result] = await Model.update(values, {
where: {
[primaryKeyField]: rows.map((row) => row.get(primaryKeyField)),
},
fields: options.fields,
hooks: options.hooks,
validate: options.validate,
sideEffects: options.sideEffects,
limit: options.limit,
silent: options.silent,
transaction,
});
// TODO: not support association fields except belongsTo
return result;
}
const instances = await this.find({ const instances = await this.find({
...queryOptions, ...queryOptions,
transaction, transaction,

View File

@ -93,6 +93,8 @@ export class UpdateGuard {
Object.keys(associationsValues).forEach((association) => { Object.keys(associationsValues).forEach((association) => {
let associationValues = associationsValues[association]; let associationValues = associationsValues[association];
const associationObj = associations[association];
const filterAssociationToBeUpdate = (value) => { const filterAssociationToBeUpdate = (value) => {
if (value === null) { if (value === null) {
return value; return value;
@ -104,8 +106,6 @@ export class UpdateGuard {
return value; return value;
} }
const associationObj = associations[association];
const associationKeyName = const associationKeyName =
associationObj.associationType == 'BelongsTo' || associationObj.associationType == 'HasOne' associationObj.associationType == 'BelongsTo' || associationObj.associationType == 'HasOne'
? (<any>associationObj).targetKey ? (<any>associationObj).targetKey
@ -143,6 +143,16 @@ export class UpdateGuard {
// set association values to sanitized value // set association values to sanitized value
values[association] = associationValues; values[association] = associationValues;
if (associationObj.associationType === 'BelongsTo') {
if (typeof associationValues === 'object' && associationValues !== null) {
if (associationValues[(associationObj as any).targetKey] != null) {
values[(associationObj as any).foreignKey] = associationValues[(associationObj as any).targetKey];
}
} else {
values[(associationObj as any).foreignKey] = associationValues;
}
}
}); });
if (values instanceof Model) { if (values instanceof Model) {

View File

@ -8,7 +8,8 @@ import {
SchemaInitializerButtonContext, SchemaInitializerButtonContext,
useAPIClient, useAPIClient,
} from '@nocobase/client'; } from '@nocobase/client';
import React, { useContext, useEffect, useState } from 'react'; import { error } from '@nocobase/utils/client';
import React, { useCallback, useContext, useMemo } from 'react';
import { useChartQueryMetadataContext } from './ChartQueryMetadataProvider'; import { useChartQueryMetadataContext } from './ChartQueryMetadataProvider';
import { lang } from './locale'; import { lang } from './locale';
import { getQueryTypeSchema } from './settings/queryTypes'; import { getQueryTypeSchema } from './settings/queryTypes';
@ -21,107 +22,121 @@ export interface ChartQueryMetadata {
} }
export const ChartQueryBlockInitializer = (props) => { export const ChartQueryBlockInitializer = (props) => {
const defaultItems: any = [
{
type: 'itemGroup',
title: lang('Select query data'),
children: [],
},
];
const { templateWrap, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; const { templateWrap, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props;
const { setVisible } = useContext(SchemaInitializerButtonContext); const { setVisible } = useContext(SchemaInitializerButtonContext);
const [items, setItems] = useState(defaultItems);
const apiClient = useAPIClient(); const apiClient = useAPIClient();
const ctx = useChartQueryMetadataContext(); const ctx = useChartQueryMetadataContext();
const options = useContext(SchemaOptionsContext); const options = useContext(SchemaOptionsContext);
const onAddQuery = (info) => { const onAddQuery = useCallback(
FormDialog( (info) => {
{ FormDialog(
sql: lang('Add SQL query'), {
json: lang('Add JSON query'), sql: lang('Add SQL query'),
}[info.key], json: lang('Add JSON query'),
() => { }[info.key],
return ( () => {
<div> return (
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}> <div>
<FormLayout layout={'vertical'}> <SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
<SchemaComponent <FormLayout layout={'vertical'}>
schema={{ <SchemaComponent
type: 'object', schema={{
properties: { type: 'object',
title: { properties: {
title: lang('Title'), title: {
required: true, title: lang('Title'),
'x-component': 'Input', required: true,
'x-decorator': 'FormItem', 'x-component': 'Input',
'x-decorator': 'FormItem',
},
options: getQueryTypeSchema(info.key),
}, },
options: getQueryTypeSchema(info.key), }}
}, />
}} </FormLayout>
/> </SchemaComponentOptions>
</FormLayout> </div>
</SchemaComponentOptions> );
</div>
);
},
)
.open({
initialValues: {
type: info.key,
}, },
}) )
.then(async (values) => { .open({
try { initialValues: {
const { data } = await apiClient.resource('chartsQueries')?.create?.({ values }); type: info.key,
const items = (await ctx.refresh()) as any; },
const item = items.find((item) => item.id === data?.data?.id); })
onCreateBlockSchema({ item }); .then(async (values) => {
setVisible(false); try {
} catch (error) {} if (apiClient.resource('chartsQueries')?.create) {
}) const { data } = await apiClient.resource('chartsQueries').create({ values });
.catch(() => {}); const items = (await ctx.refresh()) as any;
}; const item = items.find((item) => item.id === data?.data?.id);
useEffect(() => { onCreateBlockSchema({ item });
}
setVisible(false);
} catch (err) {
error(err);
}
})
.catch((err) => {
error(err);
});
},
[apiClient, ctx, onCreateBlockSchema, options.components, options.scope, setVisible],
);
const items = useMemo(() => {
const defaultItems: any = [
{
type: 'itemGroup',
title: lang('Select query data'),
children: [],
},
];
const chartQueryMetadata = ctx.data; const chartQueryMetadata = ctx.data;
if (chartQueryMetadata && Array.isArray(chartQueryMetadata)) { if (chartQueryMetadata && Array.isArray(chartQueryMetadata)) {
setItems( const item1 =
[ chartQueryMetadata.length > 0
chartQueryMetadata.length > 0 ? {
? { type: 'itemGroup',
type: 'itemGroup', title: '{{t("Select chart query", {ns: "charts"})}}',
title: '{{t("Select chart query", {ns: "charts"})}}', children: chartQueryMetadata,
children: chartQueryMetadata, }
} : null;
: null, const item2 =
chartQueryMetadata.length > 0 chartQueryMetadata.length > 0
? { ? {
type: 'divider', type: 'divider',
} }
: null, : null;
,
{ return [
type: 'subMenu', item1,
title: lang('Add chart query'), item2,
// component: AddChartQuery, {
children: [ type: 'subMenu',
{ title: lang('Add chart query'),
key: 'sql', // component: AddChartQuery,
type: 'item', children: [
title: 'SQL', {
onClick: onAddQuery, key: 'sql',
}, type: 'item',
{ title: 'SQL',
key: 'json', onClick: onAddQuery,
type: 'item', },
title: 'JSON', {
onClick: onAddQuery, key: 'json',
}, type: 'item',
], title: 'JSON',
}, onClick: onAddQuery,
].filter(Boolean), },
); ],
},
].filter(Boolean);
} }
}, []);
return defaultItems;
}, [ctx.data, onAddQuery]);
return ( return (
<SchemaInitializer.Item <SchemaInitializer.Item
icon={<TableOutlined />} icon={<TableOutlined />}

View File

@ -10,11 +10,11 @@ import {
useResourceActionContext, useResourceActionContext,
useResourceContext, useResourceContext,
} from '@nocobase/client'; } from '@nocobase/client';
import { Button, Dropdown, Menu } from 'antd'; import { Button, Dropdown, MenuProps } from 'antd';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { useChartQueryMetadataContext } from '../ChartQueryMetadataProvider'; import { useChartQueryMetadataContext } from '../ChartQueryMetadataProvider';
import { getQueryTypeSchema } from './queryTypes';
import { lang } from '../locale'; import { lang } from '../locale';
import { getQueryTypeSchema } from './queryTypes';
const useCreateAction = () => { const useCreateAction = () => {
const { setVisible } = useActionContext(); const { setVisible } = useActionContext();
@ -112,25 +112,40 @@ export const AddNewQuery = () => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [schema, setSchema] = useState({}); const [schema, setSchema] = useState({});
const form = useMemo(() => createForm(), []); const form = useMemo(() => createForm(), []);
const menu = (
<Menu const menu = useMemo<MenuProps>(() => {
onClick={(info) => { return {
onClick: (info) => {
setVisible(true); setVisible(true);
form.setValues({ type: info.key }); form.setValues({ type: info.key });
setSchema(getSchema({ type: info.key }, { form, isNewRecord: true })); setSchema(getSchema({ type: info.key }, { form, isNewRecord: true }));
}} },
> items: [
<Menu.Item key={'json'}>JSON</Menu.Item> {
<Menu.Item key={'sql'}>SQL</Menu.Item> key: 'json',
<Menu.Item disabled key={'api'}> label: 'JSON',
API },
</Menu.Item> {
<Menu.Item disabled>Collection</Menu.Item> key: 'sql',
</Menu> label: 'SQL',
); },
{
key: 'api',
label: 'API',
disabled: true,
},
{
key: 'collection',
label: 'Collection',
disabled: true,
},
],
};
}, [form]);
return ( return (
<ActionContextProvider value={{ visible, setVisible }}> <ActionContextProvider value={{ visible, setVisible }}>
<Dropdown overlay={menu}> <Dropdown menu={menu}>
<Button icon={<PlusOutlined />} type={'primary'}> <Button icon={<PlusOutlined />} type={'primary'}>
{lang('Add query')} <DownOutlined /> {lang('Add query')} <DownOutlined />
</Button> </Button>

View File

@ -0,0 +1,26 @@
import { Model } from '@nocobase/database';
import { Migration } from '@nocobase/server';
export default class extends Migration {
async up() {
const systemSettings = this.db.getRepository('systemSettings');
let instance: Model = await systemSettings.findOne();
const uiRoutes = this.db.getRepository('uiRoutes');
const routes = await uiRoutes.find();
for (const route of routes) {
if (route.uiSchemaUid && route?.options?.component === 'AdminLayout') {
const options = instance.options || {};
options['adminSchemaUid'] = route.uiSchemaUid;
instance.set('options', options);
instance.changed('options', true);
await instance.save();
return;
}
}
instance = await systemSettings.findOne();
if (!instance.get('options')?.mobileSchemaUid) {
throw new Error('adminSchemaUid invalid');
}
this.app.log.info('systemSettings.options', instance.toJSON());
}
}

View File

@ -87,9 +87,38 @@ export class ClientPlugin extends Plugin {
// //
} }
}); });
this.db.on('systemSettings.beforeCreate', async (instance, { transaction }) => {
const uiSchemas = this.db.getRepository<any>('uiSchemas');
const schema = await uiSchemas.insert(
{
type: 'void',
'x-component': 'Menu',
'x-designer': 'Menu.Designer',
'x-initializer': 'MenuItemInitializers',
'x-component-props': {
mode: 'mix',
theme: 'dark',
// defaultSelectedUid: 'u8',
onSelect: '{{ onSelect }}',
sideMenuRefScopeKey: 'sideMenuRef',
},
properties: {},
},
{ transaction },
);
instance.set('options.adminSchemaUid', schema['x-uid']);
});
} }
async load() { async load() {
this.db.addMigrations({
namespace: 'client',
directory: resolve(__dirname, './migrations'),
context: {
plugin: this,
},
});
this.app.acl.allow('app', 'getLang'); this.app.acl.allow('app', 'getLang');
this.app.acl.allow('app', 'getInfo'); this.app.acl.allow('app', 'getInfo');
this.app.acl.allow('app', 'getPlugins'); this.app.acl.allow('app', 'getPlugins');

View File

@ -0,0 +1,44 @@
import { Collection } from '@nocobase/database';
import { Migration } from '@nocobase/server';
import { FieldModel } from '../models';
export default class extends Migration {
async up() {
const transaction = await this.db.sequelize.transaction();
const migrateFieldsSchema = async (collection: Collection) => {
this.app.log.info(`Start to migrate ${collection.name} collection's ui schema`);
const fieldRecords: Array<FieldModel> = await collection.repository.find({
transaction,
filter: {
type: ['bigInt', 'float', 'double'],
},
});
this.app.log.info(`Total ${fieldRecords.length} fields need to be migrated`);
for (const fieldRecord of fieldRecords) {
const uiSchema = fieldRecord.get('uiSchema');
if (uiSchema?.['x-component-props']?.step !== '0') {
continue;
}
uiSchema['x-component-props']['step'] = '1';
fieldRecord.set('uiSchema', uiSchema);
await fieldRecord.save({
transaction,
});
console.log(`changed: ${fieldRecord.get('collectionName')}.${fieldRecord.get('name')}`);
}
};
try {
await migrateFieldsSchema(this.db.getCollection('fields'));
await transaction.commit();
} catch (error) {
await transaction.rollback();
this.app.log.error(error);
throw error;
}
}
}

View File

@ -9,7 +9,7 @@
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@formily/json-schema": "2.2.24", "@formily/json-schema": "2.2.26",
"@nocobase/server": "0.10.0-alpha.2" "@nocobase/server": "0.10.0-alpha.2"
}, },
"repository": { "repository": {

View File

@ -12,19 +12,19 @@ import { css, cx } from '@emotion/css';
import { SchemaOptionsContext } from '@formily/react'; import { SchemaOptionsContext } from '@formily/react';
import { import {
APIClientProvider, APIClientProvider,
collection, CollectionCategroriesContext,
CollectionCategroriesProvider,
CollectionManagerContext, CollectionManagerContext,
CollectionManagerProvider, CollectionManagerProvider,
CurrentAppInfoContext, CurrentAppInfoContext,
SchemaComponent, SchemaComponent,
SchemaComponentOptions, SchemaComponentOptions,
Select, Select,
collection,
useAPIClient, useAPIClient,
useCollectionManager, useCollectionManager,
useCompile, useCompile,
useCurrentAppInfo, useCurrentAppInfo,
CollectionCategroriesProvider,
CollectionCategroriesContext,
} from '@nocobase/client'; } from '@nocobase/client';
import { useFullscreen } from 'ahooks'; import { useFullscreen } from 'ahooks';
import { Button, Input, Layout, Menu, Popover, Switch, Tooltip } from 'antd'; import { Button, Input, Layout, Menu, Popover, Switch, Tooltip } from 'antd';
@ -42,6 +42,7 @@ import {
getDiffEdge, getDiffEdge,
getDiffNode, getDiffNode,
getInheritCollections, getInheritCollections,
getPopupContainer,
useGCMTranslation, useGCMTranslation,
} from './utils'; } from './utils';
@ -232,10 +233,6 @@ function getEdges(edges) {
}); });
} }
const getPopupContainer = () => {
return document.getElementById('graph_container');
};
const CollapsedContext = createContext<any>({}); const CollapsedContext = createContext<any>({});
const formatNodeData = () => { const formatNodeData = () => {
const layoutNodes = []; const layoutNodes = [];
@ -1021,14 +1018,7 @@ export const GraphDrawPage = React.memo(() => {
<div className={cx(collectionListClass)}> <div className={cx(collectionListClass)}>
<SchemaComponent <SchemaComponent
components={{ components={{
Select: (props) => ( Select: (props) => <Select {...props} getPopupContainer={getPopupContainer} />,
<Select
{...props}
getPopupContainer={() => {
return document.getElementById('graph_container');
}}
/>
),
AddCollectionAction, AddCollectionAction,
}} }}
schema={{ schema={{
@ -1100,7 +1090,7 @@ export const GraphDrawPage = React.memo(() => {
}, },
collectionList: { collectionList: {
type: 'void', type: 'void',
'x-component': () => { 'x-component': function Com() {
const { handleSearchCollection, collectionList } = useContext(CollapsedContext); const { handleSearchCollection, collectionList } = useContext(CollapsedContext);
const [selectedKeys, setSelectKey] = useState([]); const [selectedKeys, setSelectKey] = useState([]);
const content = ( const content = (
@ -1121,13 +1111,13 @@ export const GraphDrawPage = React.memo(() => {
} }
`} `}
style={{ maxHeight: '70vh', overflowY: 'auto', border: 'none' }} style={{ maxHeight: '70vh', overflowY: 'auto', border: 'none' }}
> items={[
<Menu.Divider /> { type: 'divider' },
{collectionList.map((v) => { ...collectionList.map((v) => {
return ( return {
<Menu.Item key: v.name,
key={v.name} label: compile(v.title),
onClick={(e: any) => { onClick: (e: any) => {
if (e.key !== selectedKeys[0]) { if (e.key !== selectedKeys[0]) {
setSelectKey([e.key]); setSelectKey([e.key]);
handleFiterCollections(e.key); handleFiterCollections(e.key);
@ -1136,13 +1126,11 @@ export const GraphDrawPage = React.memo(() => {
handleFiterCollections(false); handleFiterCollections(false);
setSelectKey([]); setSelectKey([]);
} }
}} },
> };
<span>{compile(v.title)}</span> }),
</Menu.Item> ]}
); />
})}
</Menu>
</div> </div>
); );
return ( return (
@ -1226,24 +1214,22 @@ export const GraphDrawPage = React.memo(() => {
} }
`} `}
style={{ maxHeight: '70vh', overflowY: 'auto', border: 'none' }} style={{ maxHeight: '70vh', overflowY: 'auto', border: 'none' }}
> items={[
<Menu.Divider /> { type: 'divider' },
{menuItems.map((v) => { ...menuItems.map((v) => {
return ( return {
<Menu.Item key: v.key,
key={v.key} label: t(v.label),
onClick={(e: any) => { onClick: (e: any) => {
targetGraph.connectionType = v.key; targetGraph.connectionType = v.key;
const { filterConfig } = targetGraph; const { filterConfig } = targetGraph;
filterConfig && handleFiterCollections(filterConfig.key); filterConfig && handleFiterCollections(filterConfig.key);
handleSetRelationshipType(v.key); handleSetRelationshipType(v.key);
}} },
> };
<span>{t(v.label)}</span> }),
</Menu.Item> ]}
); />
})}
</Menu>
</div> </div>
); );
return ( return (
@ -1303,25 +1289,23 @@ export const GraphDrawPage = React.memo(() => {
} }
`} `}
style={{ maxHeight: '70vh', overflowY: 'auto', border: 'none' }} style={{ maxHeight: '70vh', overflowY: 'auto', border: 'none' }}
> items={[
<Menu.Divider /> { type: 'divider' },
{menuItems.map((v) => { ...menuItems.map((v) => {
return ( return {
<Menu.Item key: v.key,
key={v.key} label: t(v.label),
onClick={(e: any) => { onClick: (e: any) => {
targetGraph.direction = v.key; targetGraph.direction = v.key;
const { filterConfig } = targetGraph; const { filterConfig } = targetGraph;
if (filterConfig) { if (filterConfig) {
handleFiterCollections(filterConfig.key); handleFiterCollections(filterConfig.key);
} }
}} },
> };
<span>{t(v.label)}</span> }),
</Menu.Item> ]}
); />
})}
</Menu>
</div> </div>
); );
return ( return (

View File

@ -1,8 +1,9 @@
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { AddCollection } from '@nocobase/client'; import { AddCollection } from '@nocobase/client';
import React from 'react';
import { Button } from 'antd'; import { Button } from 'antd';
import React from 'react';
import { useCancelAction } from '../action-hooks'; import { useCancelAction } from '../action-hooks';
import { getPopupContainer } from '../utils';
export const AddCollectionAction = ({ item: record }) => { export const AddCollectionAction = ({ item: record }) => {
return ( return (
@ -17,7 +18,7 @@ export const AddCollectionAction = ({ item: record }) => {
scope={{ scope={{
useCancelAction, useCancelAction,
}} }}
getContainer={() => document.getElementById('graph_container')} getContainer={getPopupContainer}
> >
<Button type="primary"> <Button type="primary">
<PlusOutlined /> <PlusOutlined />

View File

@ -2,6 +2,7 @@ import { PlusOutlined } from '@ant-design/icons';
import { AddFieldAction as AddCollectionFieldAction } from '@nocobase/client'; import { AddFieldAction as AddCollectionFieldAction } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useCancelAction, useCreateAction } from '../action-hooks'; import { useCancelAction, useCreateAction } from '../action-hooks';
import { getPopupContainer } from '../utils';
const useCreateCollectionField = (record) => { const useCreateCollectionField = (record) => {
const title = record.collectionName; const title = record.collectionName;
@ -27,7 +28,7 @@ export const AddFieldAction = ({ item: record }) => {
useCancelAction, useCancelAction,
useCreateCollectionField: () => useCreateCollectionField(record), useCreateCollectionField: () => useCreateCollectionField(record),
}} }}
getContainer={() => document.getElementById('graph_container')} getContainer={getPopupContainer}
> >
<PlusOutlined className="btn-add" id="graph_btn_add_field" /> <PlusOutlined className="btn-add" id="graph_btn_add_field" />
</AddCollectionFieldAction> </AddCollectionFieldAction>

View File

@ -1,8 +1,9 @@
import { EditOutlined } from '@ant-design/icons'; import { EditOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import { EditCollection } from '@nocobase/client'; import { EditCollection } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { css } from '@emotion/css';
import { useCancelAction, useUpdateCollectionActionAndRefreshCM } from '../action-hooks'; import { useCancelAction, useUpdateCollectionActionAndRefreshCM } from '../action-hooks';
import { getPopupContainer } from '../utils';
export const EditCollectionAction = ({ item: record }) => { export const EditCollectionAction = ({ item: record }) => {
return ( return (
@ -13,7 +14,7 @@ export const EditCollectionAction = ({ item: record }) => {
useUpdateCollectionActionAndRefreshCM, useUpdateCollectionActionAndRefreshCM,
createOnly: false, createOnly: false,
}} }}
getContainer={() => document.getElementById('graph_container')} getContainer={getPopupContainer}
> >
<EditOutlined <EditOutlined
className={css` className={css`

View File

@ -2,6 +2,7 @@ import { EditOutlined } from '@ant-design/icons';
import { EditFieldAction as EditCollectionFieldAction } from '@nocobase/client'; import { EditFieldAction as EditCollectionFieldAction } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useCancelAction, useUpdateFieldAction } from '../action-hooks'; import { useCancelAction, useUpdateFieldAction } from '../action-hooks';
import { getPopupContainer } from '../utils';
const useUpdateCollectionField = (record) => { const useUpdateCollectionField = (record) => {
const collectionName = record.collectionName; const collectionName = record.collectionName;
@ -21,7 +22,7 @@ export const EditFieldAction = ({ item: record }) => {
useCancelAction, useCancelAction,
useUpdateCollectionField: () => useUpdateCollectionField(record), useUpdateCollectionField: () => useUpdateCollectionField(record),
}} }}
getContainer={() => document.getElementById('graph_container')} getContainer={getPopupContainer}
> >
<EditOutlined className="btn-edit" /> <EditOutlined className="btn-edit" />
</EditCollectionFieldAction> </EditCollectionFieldAction>

View File

@ -5,7 +5,7 @@ import { uid } from '@formily/shared';
import { import {
Action, Action,
Checkbox, Checkbox,
collection, CollectionCategroriesContext,
CollectionField, CollectionField,
CollectionProvider, CollectionProvider,
Form, Form,
@ -19,15 +19,15 @@ import {
SchemaComponent, SchemaComponent,
SchemaComponentProvider, SchemaComponentProvider,
Select, Select,
collection,
useCollectionManager, useCollectionManager,
useCompile, useCompile,
useCurrentAppInfo, useCurrentAppInfo,
useRecord, useRecord,
CollectionCategroriesContext,
} from '@nocobase/client'; } from '@nocobase/client';
import { Badge, Dropdown, Popover, Tag } from 'antd'; import { Badge, Dropdown, Popover, Tag } from 'antd';
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
import React, { useRef, useState, useContext } from 'react'; import React, { useContext, useRef, useState } from 'react';
import { import {
useAsyncDataSource, useAsyncDataSource,
useCancelAction, useCancelAction,
@ -37,7 +37,7 @@ import {
useValuesFromRecord, useValuesFromRecord,
} from '../action-hooks'; } from '../action-hooks';
import { collectiionPopoverClass, entityContainer, headClass, tableBtnClass, tableNameClass } from '../style'; import { collectiionPopoverClass, entityContainer, headClass, tableBtnClass, tableNameClass } from '../style';
import { useGCMTranslation } from '../utils'; import { getPopupContainer, useGCMTranslation } from '../utils';
import { AddFieldAction } from './AddFieldAction'; import { AddFieldAction } from './AddFieldAction';
import { CollectionNodeProvder } from './CollectionNodeProvder'; import { CollectionNodeProvder } from './CollectionNodeProvder';
import { EditCollectionAction } from './EditCollectionAction'; import { EditCollectionAction } from './EditCollectionAction';
@ -161,9 +161,7 @@ const Entity: React.FC<{
confirm: { confirm: {
title: "{{t('Delete record')}}", title: "{{t('Delete record')}}",
getContainer: () => { getContainer: getPopupContainer,
return document.getElementById('graph_container');
},
collectionConten: "{{t('Are you sure you want to delete it?')}}", collectionConten: "{{t('Are you sure you want to delete it?')}}",
}, },
useAction: () => useDestroyActionAndRefreshCM({ name, id }), useAction: () => useDestroyActionAndRefreshCM({ name, id }),
@ -233,6 +231,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
return 'orange'; return 'orange';
} }
}; };
const OperationButton = ({ property }) => { const OperationButton = ({ property }) => {
const isInheritField = !(property.collectionName !== name); const isInheritField = !(property.collectionName !== name);
return ( return (
@ -244,14 +243,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
Input, Input,
Form, Form,
ResourceActionProvider, ResourceActionProvider,
Select: (props) => ( Select: (props) => <Select {...props} getPopupContainer={getPopupContainer} />,
<Select
{...props}
getPopupContainer={() => {
return document.getElementById('graph_container');
}}
/>
),
Checkbox, Checkbox,
Radio, Radio,
InputNumber, InputNumber,
@ -334,9 +326,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
`, `,
confirm: { confirm: {
title: "{{t('Delete record')}}", title: "{{t('Delete record')}}",
getContainer: () => { getContainer: getPopupContainer,
return document.getElementById('graph_container');
},
collectionConten: "{{t('Are you sure you want to delete it?')}}", collectionConten: "{{t('Are you sure you want to delete it?')}}",
}, },
useAction: () => useAction: () =>
@ -409,9 +399,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
property.uiSchema && ( property.uiSchema && (
<Popover <Popover
content={CollectionConten(property)} content={CollectionConten(property)}
getPopupContainer={() => { getPopupContainer={getPopupContainer}
return document.getElementById('graph_container');
}}
mouseLeaveDelay={0} mouseLeaveDelay={0}
zIndex={100} zIndex={100}
title={ title={
@ -452,9 +440,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
property.uiSchema && ( property.uiSchema && (
<Popover <Popover
content={CollectionConten(property)} content={CollectionConten(property)}
getPopupContainer={() => { getPopupContainer={getPopupContainer}
return document.getElementById('graph_container');
}}
mouseLeaveDelay={0} mouseLeaveDelay={0}
zIndex={100} zIndex={100}
title={ title={

View File

@ -2,6 +2,7 @@ import { CopyOutlined } from '@ant-design/icons';
import { OverridingFieldAction as OverridingCollectionFieldAction } from '@nocobase/client'; import { OverridingFieldAction as OverridingCollectionFieldAction } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useCancelAction, useCreateAction } from '../action-hooks'; import { useCancelAction, useCreateAction } from '../action-hooks';
import { getPopupContainer } from '../utils';
const useOverridingCollectionField = (record) => { const useOverridingCollectionField = (record) => {
const collectionName = record.targetCollection; const collectionName = record.targetCollection;
@ -21,7 +22,7 @@ export const OverrideFieldAction = ({ item: record }) => {
useCancelAction, useCancelAction,
useOverridingCollectionField: () => useOverridingCollectionField(record), useOverridingCollectionField: () => useOverridingCollectionField(record),
}} }}
getContainer={() => document.getElementById('graph_container')} getContainer={getPopupContainer}
> >
<CopyOutlined className="btn-override" /> <CopyOutlined className="btn-override" />
</OverridingCollectionFieldAction> </OverridingCollectionFieldAction>

Some files were not shown because too many files have changed in this diff Show More