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(() => {
const result = [];
collectionTemplates.forEach((item) => { collectionTemplates.forEach((item) => {
if (item.divider) { if (item.divider) {
items.push({ result.push({
type: 'divider', 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();
return ( const menu = useMemo<MenuProps>(() => {
<RecordProvider record={record}> return {
<ActionContextProvider value={{ visible, setVisible }}> style: {
<Dropdown
getPopupContainer={getContainer}
trigger={trigger}
align={align}
overlay={
<Menu
style={{
maxHeight: '60vh', maxHeight: '60vh',
overflow: 'auto', overflow: 'auto',
}} },
onClick={(info) => { onClick: (info) => {
const schema = getSchema(getTemplate(info.key), category, compile); const schema = getSchema(getTemplate(info.key), category, compile);
setSchema(schema); setSchema(schema);
setVisible(true); setVisible(true);
}} },
items={items} items,
/> };
} }, [category, items]);
>
return (
<RecordProvider record={record}>
<ActionContextProvider value={{ visible, setVisible }}>
<Dropdown getPopupContainer={getContainer} trigger={trigger} align={align} menu={menu}>
{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,22 +216,39 @@ 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,
}; };
return ( }),
record.template !== 'view' && ( };
<RecordProvider record={record}> });
<ActionContextProvider value={{ visible, setVisible }}> }, [getFieldOptions]);
<Dropdown
getPopupContainer={getContainer} const menu = useMemo<MenuProps>(() => {
trigger={trigger} return {
align={align} style: {
overlay={
<Menu
style={{
maxHeight: '60vh', maxHeight: '60vh',
overflow: 'auto', overflow: 'auto',
}} },
onClick={(e) => { onClick: (e) => {
//@ts-ignore //@ts-ignore
const targetScope = e.item.props['data-targetScope']; const targetScope = e.item.props['data-targetScope'];
targetScope && setTargetScope(targetScope); targetScope && setTargetScope(targetScope);
@ -242,28 +257,16 @@ export const AddFieldAction = (props) => {
setSchema(schema); setSchema(schema);
setVisible(true); setVisible(true);
} }
}} },
> items,
{getFieldOptions().map((option) => { };
}, [getInterface, items, record]);
return ( return (
option.children.length > 0 && ( record.template !== 'view' && (
<Menu.ItemGroup key={option.label} title={compile(option.label)}> <RecordProvider record={record}>
{option.children <ActionContextProvider value={{ visible, setVisible }}>
.filter((child) => !['o2o', 'subTable', 'linkTo'].includes(child.name)) <Dropdown getPopupContainer={getContainer} trigger={trigger} align={align} menu={menu}>
.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,7 +101,8 @@ 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(() => {
return options.map((option) => {
const children = option.children.map((child) => { const children = option.children.map((child) => {
return { label: compile(child.title), key: child.name }; return { label: compile(child.title), key: child.name };
}); });
@ -111,24 +112,25 @@ export const AddSubFieldAction = () => {
children, children,
}; };
}); });
return ( }, [options]);
<ActionContextProvider value={{ visible, setVisible }}> const menu = useMemo<MenuProps>(() => {
<Dropdown return {
overlay={ style: {
<Menu
style={{
maxHeight: '60vh', maxHeight: '60vh',
overflow: 'auto', overflow: 'auto',
}} },
onClick={(info) => { onClick: (info) => {
const schema = getSchema(getInterface(info.key)); const schema = getSchema(getInterface(info.key));
setSchema(schema); setSchema(schema);
setVisible(true); setVisible(true);
}} },
items={items} items,
/> };
} }, [items]);
>
return (
<ActionContextProvider value={{ visible, setVisible }}>
<Dropdown menu={menu}>
<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,9 +182,13 @@ export const ConfigurationTabs = () => {
value: item.id, value: item.id,
})); }));
}; };
const menu = (item) => (
<Menu> const menu = _.memoize((item) => {
<Menu.Item key={'edit'}> return {
items: [
{
key: 'edit',
label: (
<SchemaComponent <SchemaComponent
schema={{ schema={{
type: 'void', type: 'void',
@ -197,12 +202,17 @@ export const ConfigurationTabs = () => {
}, },
}} }}
/> />
</Menu.Item> ),
<Menu.Item key="delete" onClick={() => remove(item.id)}> },
{compile("{{t('Delete category')}}")} {
</Menu.Item> key: 'delete',
</Menu> label: compile("{{t('Delete category')}}"),
); onClick: () => remove(item.id),
},
],
};
});
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
tab={
item.id !== 'all' ? ( item.id !== 'all' ? (
<div data-no-dnd="true"> <div data-no-dnd="true">
<TabTitle item={item} /> <TabTitle item={item} />
</div> </div>
) : ( ) : (
compile(item.name) compile(item.name)
) ),
} key: item.id,
key={item.id} closable: item.closable,
closable={item.closable} closeIcon: (
closeIcon={ <Dropdown menu={menu(item)}>
<Dropdown overlay={menu(item)}>
<MenuOutlined /> <MenuOutlined />
</Dropdown> </Dropdown>
} ),
> children: (
<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,9 +1,11 @@
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();
return useMemo(() => {
const fields = {}; const fields = {};
Object.keys(interfaces).forEach((type) => { Object.keys(interfaces).forEach((type) => {
@ -42,4 +44,5 @@ export const useOptions = () => {
}) })
.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,11 +71,15 @@ 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(
(value) => {
setChanged(true); setChanged(true);
props.onChange(value); props.onChange(value);
}; },
[props.onChange],
);
const { numColumns, scope } = useMemo(() => {
const numColumns = new Map<string, string>(); const numColumns = new Map<string, string>();
const scope = {}; const scope = {};
fields fields
@ -84,6 +88,10 @@ export const Expression = (props) => {
numColumns.set(field.name, field.uiSchema.title); numColumns.set(field.name, field.uiSchema.title);
scope[field.name] = 1; scope[field.name] = 1;
}); });
return { numColumns, scope };
}, [fields, supports]);
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,11 +118,13 @@ 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,
disabled: true,
label: (
<button <button
onClick={async (args) => { onClick={async (args) => {
(inputRef.current as any).focus(); (inputRef.current as any).focus();
@ -123,20 +134,28 @@ export const Expression = (props) => {
); );
const text = getValue(inputRef.current); const text = getValue(inputRef.current);
onChange(text); onChange(text);
console.log('onChange', text);
}} }}
> >
{numColumns.get(key)} {numColumns.get(key)}
</button> </button>
</Menu.Item> ),
)) }));
) : ( } else {
<Menu.Item disabled key={0}> return [
{t('No available fields')} {
</Menu.Item> key: 0,
)} disabled: true,
</Menu> 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,11 +43,8 @@ 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);
return ( const menu = useMemo<MenuProps>(() => {
<ActionContextProvider value={{ visible, setVisible }}> return {
<Dropdown
placement="bottom"
menu={{
items: [ items: [
...bookmarkTabs.map((tab) => ({ ...bookmarkTabs.map((tab) => ({
key: `/admin/settings/${tab.path}`, key: `/admin/settings/${tab.path}`,
@ -61,8 +59,12 @@ export const SettingsCenterDropdown = () => {
onClick({ key }) { onClick({ key }) {
navigate(key); navigate(key);
}, },
}} };
> }, [bookmarkTabs]);
return (
<ActionContextProvider value={{ visible, setVisible }}>
<Dropdown placement="bottom" menu={menu}>
<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,171 +128,7 @@ const antdMenuClass = css`
} }
`; `;
type ComposedMenu = React.FC<any> & { const sideMenuClass = css`
Item?: React.FC<any>;
URL?: React.FC<any>;
SubMenu?: React.FC<any>;
Designer?: React.FC<any>;
};
const MenuModeContext = createContext(null);
MenuModeContext.displayName = 'MenuModeContext';
const useSideMenuRef = () => {
const schema = useFieldSchema();
const scope = useContext(SchemaExpressionScopeContext);
const scopeKey = schema?.['x-component-props']?.['sideMenuRefScopeKey'];
if (!scopeKey) {
return;
}
return scope[scopeKey];
};
const MenuItemDesignerContext = createContext(null);
MenuItemDesignerContext.displayName = 'MenuItemDesignerContext';
export const Menu: ComposedMenu = observer(
(props) => {
const {
onSelect,
mode,
selectedUid,
defaultSelectedUid,
sideMenuRefScopeKey,
defaultSelectedKeys: dSelectedKeys,
defaultOpenKeys: dOpenKeys,
...others
} = useProps(props);
const { t } = useTranslation();
const Designer = useDesigner();
const schema = useFieldSchema();
const { refresh } = useDesignable();
const api = useAPIClient();
const { render } = useSchemaInitializer(schema['x-initializer']);
const sideMenuRef = useSideMenuRef();
const [selectedKeys, setSelectedKeys] = useState<string[]>();
const [defaultSelectedKeys, setDefaultSelectedKeys] = useState(() => {
if (dSelectedKeys) {
return dSelectedKeys;
}
if (defaultSelectedUid) {
return findKeysByUid(schema, defaultSelectedUid);
}
return [];
});
const [loading, setLoading] = useState(false);
const [defaultOpenKeys, setDefaultOpenKeys] = useState(() => {
if (['inline', 'mix'].includes(mode)) {
return dOpenKeys || defaultSelectedKeys;
}
return dOpenKeys;
});
const [sideMenuSchema, setSideMenuSchema] = useState<Schema>(() => {
const key = defaultSelectedKeys?.[0] || null;
if (mode === 'mix' && key) {
const s = schema.properties?.[key];
if (s['x-component'] === 'Menu.SubMenu') {
return s;
}
}
return null;
});
useEffect(() => {
if (!selectedUid) {
setSelectedKeys(undefined);
return;
}
const keys = findKeysByUid(schema, selectedUid);
setSelectedKeys(keys);
if (['inline', 'mix'].includes(mode)) {
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]);
useEffect(() => {
if (['inline', 'mix'].includes(mode)) {
setDefaultOpenKeys(defaultSelectedKeys);
}
}, [defaultSelectedKeys]);
const { designable } = useDesignable();
return (
<DndContext>
<MenuItemDesignerContext.Provider value={Designer}>
<MenuModeContext.Provider value={mode}>
<AntdMenu
{...others}
className={antdMenuClass}
onSelect={(info: any) => {
const s = schema.properties[info.key];
if (mode === 'mix') {
setSideMenuSchema(s);
if (s['x-component'] !== 'Menu.SubMenu') {
onSelect && onSelect(info);
} else {
const menuItemSchema = findMenuItem(s);
if (!menuItemSchema) {
return;
}
// 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}
>
{designable && (
<AntdMenu.Item disabled key="x-designer-button" style={{ padding: '0 8px', order: 9999 }}>
{render({ style: { background: 'none' } })}
</AntdMenu.Item>
)}
{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%; height: 100%;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
@ -320,11 +159,127 @@ export const Menu: ComposedMenu = observer(
} }
} }
} }
`} `;
>
<RecursionField schema={sideMenuSchema} onlyRenderProperties /> const menuItemClass = css`
{render({ :active {
style: { margin: 8 }, background: inherit;
}
`;
type ComposedMenu = React.FC<any> & {
Item?: React.FC<any>;
URL?: React.FC<any>;
SubMenu?: 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) => { insert: (s) => {
const dn = createDesignable({ const dn = createDesignable({
t, t,
@ -335,11 +290,167 @@ export const Menu: ComposedMenu = observer(
dn.loadAPIClientEvents(); dn.loadAPIClientEvents();
dn.insertAdjacent('beforeEnd', s); dn.insertAdjacent('beforeEnd', s);
}, },
})} }),
</AntdMenu> 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>, </MenuModeContext.Provider>,
sideMenuRef.current.firstChild, sideMenuRef.current.firstChild,
)} )
);
};
const MenuModeContext = createContext(null);
MenuModeContext.displayName = 'MenuModeContext';
const useSideMenuRef = () => {
const schema = useFieldSchema();
const scope = useContext(SchemaExpressionScopeContext);
const scopeKey = schema?.['x-component-props']?.['sideMenuRefScopeKey'];
if (!scopeKey) {
return;
}
return scope[scopeKey];
};
const MenuItemDesignerContext = createContext(null);
MenuItemDesignerContext.displayName = 'MenuItemDesignerContext';
export const Menu: ComposedMenu = observer(
(props) => {
const {
onSelect,
mode,
selectedUid,
defaultSelectedUid,
sideMenuRefScopeKey,
defaultSelectedKeys: dSelectedKeys,
defaultOpenKeys: dOpenKeys,
children,
...others
} = useProps(props);
const { t } = useTranslation();
const Designer = useDesigner();
const schema = useFieldSchema();
const { refresh } = useDesignable();
const api = useAPIClient();
const { render } = useSchemaInitializer(schema['x-initializer']);
const sideMenuRef = useSideMenuRef();
const [selectedKeys, setSelectedKeys] = useState<string[]>();
const [defaultSelectedKeys, setDefaultSelectedKeys] = useState(() => {
if (dSelectedKeys) {
return dSelectedKeys;
}
if (defaultSelectedUid) {
return findKeysByUid(schema, defaultSelectedUid);
}
return [];
});
const [loading, setLoading] = useState(false);
const [defaultOpenKeys, setDefaultOpenKeys] = useState(() => {
if (['inline', 'mix'].includes(mode)) {
return dOpenKeys || defaultSelectedKeys;
}
return dOpenKeys;
});
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) {
const s = schema.properties?.[key];
if (s['x-component'] === 'Menu.SubMenu') {
return s;
}
}
return null;
}, [defaultSelectedKeys, mode, schema, selectedUid]);
useEffect(() => {
if (!selectedUid) {
setSelectedKeys(undefined);
return;
}
const keys = findKeysByUid(schema, selectedUid);
setSelectedKeys(keys);
if (['inline', 'mix'].includes(mode)) {
setDefaultOpenKeys(dOpenKeys || keys);
}
}, [selectedUid]);
useEffect(() => {
if (['inline', 'mix'].includes(mode)) {
setDefaultOpenKeys(defaultSelectedKeys);
}
}, [defaultSelectedKeys]);
const { designable } = useDesignable();
return (
<DndContext>
<MenuItemDesignerContext.Provider value={Designer}>
<MenuModeContext.Provider value={mode}>
<HeaderMenu
others={others}
schema={schema}
mode={mode}
onSelect={onSelect}
setLoading={setLoading}
setDefaultSelectedKeys={setDefaultSelectedKeys}
defaultSelectedKeys={defaultSelectedKeys}
defaultOpenKeys={defaultOpenKeys}
selectedKeys={selectedKeys}
designable={designable}
render={render}
>
{children}
</HeaderMenu>
<SideMenu
loading={loading}
mode={mode}
sideMenuSchema={sideMenuSchema}
sideMenuRef={sideMenuRef}
defaultOpenKeys={defaultOpenKeys}
defaultSelectedKeys={defaultSelectedKeys}
onSelect={onSelect}
render={render}
t={t}
api={api}
refresh={refresh}
designable={designable}
/>
</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}> <SortableItem className={designerCss} removeParentsIfNoChildren={false}>
<Icon type={icon} /> <Icon type={icon} />
<span <span
className={css` style={{
overflow: hidden; overflow: 'hidden',
text-overflow: ellipsis; textOverflow: 'ellipsis',
display: inline-block; display: 'inline-block',
width: 100%; width: '100%',
vertical-align: middle; verticalAlign: 'middle',
`} }}
> >
{field.title} {field.title}
</span> </span>
{Designer && <Designer />} {Designer && <Designer />}
</SortableItem> </SortableItem>
</AntdMenu.Item> </FieldContext.Provider>
); </SchemaContext.Provider>
),
};
}, [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;
} }
`}
key={schema.name} const item = useMemo(() => {
eventKey={schema.name} return {
schema={schema} ...others,
onClick={() => { className: menuItemClass,
key: schema.name,
eventKey: schema.name,
schema,
onClick: () => {
window.open(props.href, '_blank'); window.open(props.href, '_blank');
}} },
> label: (
<SchemaContext.Provider value={schema}>
<FieldContext.Provider value={field}>
<SortableItem className={designerCss} removeParentsIfNoChildren={false}> <SortableItem className={designerCss} removeParentsIfNoChildren={false}>
<Icon type={icon} /> <Icon type={icon} />
<span <span
className={css` style={{
overflow: hidden; overflow: 'hidden',
text-overflow: ellipsis; textOverflow: 'ellipsis',
display: inline-block; display: 'inline-block',
width: 100%; width: '100%',
vertical-align: middle; verticalAlign: 'middle',
`} }}
> >
{field.title} {field.title}
</span> </span>
{Designer && <Designer />} {Designer && <Designer />}
</SortableItem> </SortableItem>
</AntdMenu.Item> </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);
if (mode === 'mix') { const submenu = useMemo(() => {
return <Menu.Item {...props} />; return {
} ...others,
return ( className: menuItemClass,
<AntdMenu.SubMenu key: schema.name,
{...others} eventKey: schema.name,
className={css` label: (
:active { <SchemaContext.Provider value={schema}>
background: inherit; <FieldContext.Provider value={field}>
}
`}
key={schema.name}
eventKey={schema.name}
title={
<SortableItem className={subMenuDesignerCss} removeParentsIfNoChildren={false}> <SortableItem className={subMenuDesignerCss} removeParentsIfNoChildren={false}>
<Icon type={icon} /> <Icon type={icon} />
{field.title} {field.title}
{Designer && <Designer />} {Designer && <Designer />}
</SortableItem> </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;
} }
>
<RecursionField schema={schema} onlyRenderProperties /> if (mode === 'mix') {
</AntdMenu.SubMenu> return <Menu.Item {...props} />;
); }
pushMenuItem(submenu);
return <Component />;
}, },
{ 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,11 +260,9 @@ export const Page = (props) => {
</Button> </Button>
) )
} }
> items={fieldSchema.mapProperties((schema) => {
{fieldSchema.mapProperties((schema) => { return {
return ( label: (
<Tabs.TabPane
tab={
<SortableItem <SortableItem
id={schema.name as string} id={schema.name as string}
schema={schema} schema={schema}
@ -261,12 +272,11 @@ export const Page = (props) => {
<span>{schema.title || t('Unnamed')}</span> <span>{schema.title || t('Unnamed')}</span>
<PageTabDesigner schema={schema} /> <PageTabDesigner schema={schema} />
</SortableItem> </SortableItem>
} ),
key={schema.name} key: schema.name as string,
/> };
);
})} })}
</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,7 +193,8 @@ const useValidator = (validator: (value: any) => string) => {
}, []); }, []);
}; };
export const Table: any = observer((props: any) => { export const Table: any = observer(
(props: any) => {
const { pagination: pagination1, useProps, onChange, ...others1 } = props; const { pagination: pagination1, useProps, onChange, ...others1 } = props;
const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {}; const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {};
const { const {
@ -453,7 +454,7 @@ export const Table: any = observer((props: any) => {
return dragSort return dragSort
? React.createElement(SortableContext, { ? React.createElement(SortableContext, {
items: field.value?.map?.(getRowKey) || [], items: field.value?.map?.(getRowKey) || [],
children: children, children,
}) })
: React.createElement(React.Fragment, { : React.createElement(React.Fragment, {
children, children,
@ -533,4 +534,6 @@ export const Table: any = observer((props: any) => {
)} )}
</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) {
error(`SchemaInitializer: component "${item.component}" not found`);
return null;
}
if (!item.key) {
item.key = `${item.title}-${indexA}`;
}
return getMenuItem(() => {
return ( return (
Component && (
<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,
onClick: (info) => {
item?.clearKeywords?.(); item?.clearKeywords?.();
if (item.onClick) { if (item.onClick) {
item.onClick({ ...info, item }); item.onClick({ ...info, item });
} else { } else {
onClick({ ...info, item }); 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,
onClick: (opts) => {
info?.clearKeywords?.(); info?.clearKeywords?.();
onClick({ ...opts, item: info }); 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,7 +131,8 @@ 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(() => {
return enableChildren
.map((k) => { .map((k) => {
if (!k) { if (!k) {
return; return;
@ -148,27 +149,26 @@ export const CreateAction = observer(
.filter((v) => { .filter((v) => {
return v && actionAclCheck(`${v.name}:create`); 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,17 +117,7 @@ export const SchemaSettingsProvider: React.FC<SchemaSettingsProviderProps> = (pr
); );
}; };
export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNested = (props) => { const overlayClassName = classNames(
const { title, dn, ...others } = props;
const [visible, setVisible] = useState(false);
const DropdownMenu = (
<Dropdown
open={visible}
onOpenChange={(visible) => {
setVisible(visible);
}}
overlay={<Menu>{props.children as any}</Menu>}
overlayClassName={classNames(
'nb-schema-initializer-button-overlay', 'nb-schema-initializer-button-overlay',
css` css`
.ant-dropdown-menu-item-group-list { .ant-dropdown-menu-item-group-list {
@ -135,19 +125,52 @@ export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNeste
overflow: auto; overflow: auto;
} }
`, `,
)} );
export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNested = (props) => {
const { title, dn, ...others } = props;
const [visible, setVisible] = useState(false);
const { Component, getMenuItems } = useMenuItem();
const [shouldRender, setShouldRender] = useState(false);
if (!shouldRender) {
return (
<div
onMouseEnter={() => {
setShouldRender(true);
setVisible(true);
}}
>
{typeof title === 'string' ? <span>{title}</span> : title}
</div>
);
}
const dropdownMenu = () => (
<>
<Component />
<Dropdown
open={visible}
onOpenChange={() => {
setShouldRender(false);
setVisible(false);
}}
menu={{ items: getMenuItems(() => props.children) }}
overlayClassName={overlayClassName}
> >
{typeof title === 'string' ? <span>{title}</span> : title} {typeof title === 'string' ? <span>{title}</span> : title}
</Dropdown> </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,
{props.children || props.title} title: props.title,
</Menu.Item> } as MenuProps['items'][0];
);
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
) : ( ) : (
<SchemaSettings.Item>
<Empty <Empty
style={{ width: 160, padding: '0 1em' }} style={{ width: 160, padding: '0 1em' }}
description={emptyDescription} description={emptyDescription}
image={Empty.PRESENTED_IMAGE_SIMPLE} 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={() => {
ctx?.setVisible?.(false);
setVisible(true); setVisible(true);
}} ctx?.setVisible(false);
> },
label: (
<>
{t('Change password')} {t('Change password')}
</Menu.Item> <ActionContextProvider value={{ visible, setVisible }}>
<div onClick={(e) => e.stopPropagation()}>
<SchemaComponent scope={{ useCloseAction, useSaveCurrentUserValues }} schema={schema} /> <SchemaComponent scope={{ useCloseAction, useSaveCurrentUserValues }} schema={schema} />
</div>
</ActionContextProvider> </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,42 +50,44 @@ export const SettingsMenu: React.FC<{
} }
return res; return res;
}) })
.catch(() => { .catch((err) => {
// ignore error(err);
}); });
}, 3000); }, 3000);
}); });
}, [silenceApi]);
const divider = useMemo<MenuProps['items'][0]>(() => {
return {
type: 'divider',
}; };
return ( }, []);
<Menu> const appVersion = useApplicationVersion();
<ApplicationVersion /> const editProfile = useEditProfile();
<Menu.Divider /> const changePassword = useChangePassword();
<EditProfile /> const switchRole = useSwitchRole();
<ChangePassword /> const languageSettings = useLanguageSettings();
<Menu.Divider /> const themeSettings = useThemeSettings();
<SwitchRole /> const controlApp = useMemo<MenuProps['items']>(() => {
<LanguageSettings /> if (!appAllowed) {
<ThemeSettings /> return [];
<Menu.Divider /> }
{appAllowed && (
<> return [
<Menu.Item {
key="cache" key: 'cache',
onClick={async () => { label: t('Clear cache'),
onClick: async () => {
await api.resource('app').clearCache(); await api.resource('app').clearCache();
window.location.reload(); window.location.reload();
}} },
> },
{t('Clear cache')} {
</Menu.Item> key: 'reboot',
<Menu.Item label: t('Reboot application'),
key="reboot" onClick: async () => {
onClick={async () => {
Modal.confirm({ Modal.confirm({
title: t('Reboot application'), title: t('Reboot application'),
content: t( content: t('The will interrupt service, it may take a few seconds to restart. Are you sure to continue?'),
'The will interrupt service, it may take a few seconds to restart. Are you sure to continue?',
),
okText: t('Reboot'), okText: t('Reboot'),
okButtonProps: { okButtonProps: {
danger: true, danger: true,
@ -93,29 +98,52 @@ export const SettingsMenu: React.FC<{
window.location.reload(); window.location.reload();
}, },
}); });
}} },
> },
{t('Reboot application')} divider,
</Menu.Item> ];
<Menu.Divider /> }, [appAllowed, check]);
</> const items = useMemo<MenuProps['items']>(() => {
)} return [
<Menu.Item appVersion,
key="signout" divider,
onClick={async () => { editProfile,
changePassword,
divider,
switchRole,
languageSettings,
themeSettings,
divider,
...controlApp,
{
key: 'signout',
label: t('Sign out'),
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()}>
<SchemaComponent
scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }}
schema={schema}
/>
</div>
</ActionContextProvider> </ActionContextProvider>
); </>
),
};
}, [visible]);
}; };

View File

@ -1,35 +1,29 @@
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 || [];
if (enabledLanguages.length < 2) { const result = useMemo<MenuProps['items'][0]>(() => {
return null; return {
} key: 'language',
// console.log('data', data?.data?.enabledLanguages); eventKey: 'LanguageSettings',
return ( onClick: () => {
<Menu.Item
key="language"
eventKey={'LanguageSettings'}
onClick={() => {
setOpen(true); setOpen(true);
}} },
> label: (
<div <div
className={css` style={{
display: flex; display: 'flex',
align-items: center; alignItems: 'center',
justify-content: space-between; justifyContent: 'space-between',
`} }}
> >
{t('Language')}{' '} {t('Language')}{' '}
<Select <Select
@ -60,6 +54,13 @@ export const LanguageSettings = () => {
}} }}
/> />
</div> </div>
</Menu.Item> ),
); };
}, [enabledLanguages, i18n, open]);
if (enabledLanguages.length < 2) {
return null;
}
return result;
}; };

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,11 +142,18 @@ 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'),
key: 'password',
children: <SchemaComponent scope={{ usePasswordSignIn }} schema={schema || passwordForm} />,
},
{
label: t('Sign in via phone'),
key: 'phone',
children: (
<SchemaComponent <SchemaComponent
schema={phoneForm} schema={phoneForm}
scope={{ usePhoneSignIn, ...scope }} scope={{ usePhoneSignIn, ...scope }}
@ -155,8 +162,10 @@ export const SigninPage = (props: SigninPageProps) => {
...components, ...components,
}} }}
/> />
</Tabs.TabPane> ),
</Tabs> },
]}
/>
) : ( ) : (
<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,21 +25,21 @@ 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();
if (roles.length <= 1) { const result = useMemo<MenuProps['items'][0]>(() => {
return null; return {
} key: 'role',
return ( eventKey: 'SwitchRole',
<Menu.Item key="role" eventKey={'SwitchRole'}> label: (
<div <div
className={css` style={{
display: flex; display: 'flex',
align-items: center; alignItems: 'center',
justify-content: space-between; justifyContent: 'space-between',
`} }}
> >
{t('Switch role')}{' '} {t('Switch role')}{' '}
<Select <Select
@ -60,6 +59,13 @@ export const SwitchRole = () => {
}} }}
/> />
</div> </div>
</Menu.Item> ),
); };
}, [api, history, roles]);
if (roles.length <= 1) {
return null;
}
return result;
}; };

View File

@ -1,22 +1,25 @@
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]>(() => {
return {
key: 'theme',
eventKey: 'theme',
label: (
<div <div
className={css` style={{
display: flex; display: 'flex',
align-items: center; alignItems: 'center',
justify-content: space-between; justifyContent: 'space-between',
`} }}
> >
{t('Theme')}{' '} {t('Theme')}{' '}
<Select <Select
@ -42,6 +45,7 @@ export const ThemeSettings = () => {
}} }}
/> />
</div> </div>
</Menu.Item> ),
); };
}, [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,20 +22,13 @@ 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(
(info) => {
FormDialog( FormDialog(
{ {
sql: lang('Add SQL query'), sql: lang('Add SQL query'),
@ -72,33 +66,52 @@ export const ChartQueryBlockInitializer = (props) => {
}) })
.then(async (values) => { .then(async (values) => {
try { try {
const { data } = await apiClient.resource('chartsQueries')?.create?.({ values }); if (apiClient.resource('chartsQueries')?.create) {
const { data } = await apiClient.resource('chartsQueries').create({ values });
const items = (await ctx.refresh()) as any; const items = (await ctx.refresh()) as any;
const item = items.find((item) => item.id === data?.data?.id); const item = items.find((item) => item.id === data?.data?.id);
onCreateBlockSchema({ item }); onCreateBlockSchema({ item });
}
setVisible(false); setVisible(false);
} catch (error) {} } catch (err) {
error(err);
}
}) })
.catch(() => {}); .catch((err) => {
}; error(err);
useEffect(() => { });
},
[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 [
item1,
item2,
{ {
type: 'subMenu', type: 'subMenu',
title: lang('Add chart query'), title: lang('Add chart query'),
@ -118,10 +131,12 @@ export const ChartQueryBlockInitializer = (props) => {
}, },
], ],
}, },
].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