mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-07 22:49:26 +08:00
Merge branch 'main' into T-557
This commit is contained in:
commit
a16be91b5b
@ -46,8 +46,8 @@ export default defineConfig({
|
||||
github: 'https://github.com/nocobase/nocobase',
|
||||
footer: 'nocobase | Copyright © 2022',
|
||||
localesEnhance: [
|
||||
{ id: 'zh-CN', switchPrefix: '中' },
|
||||
{ id: 'en-US', switchPrefix: 'en' }
|
||||
{ id: 'zh-CN', switchPrefix: '中', hostname: 'docs-cn.nocobase.com' },
|
||||
{ id: 'en-US', switchPrefix: 'en', hostname: 'docs.nocobase.com' }
|
||||
],
|
||||
}),
|
||||
// mfsu: true, // 报错
|
||||
|
9
deploy-docs-cn.sh
Executable file
9
deploy-docs-cn.sh
Executable 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
9
deploy-docs.sh
Executable 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
|
@ -285,23 +285,17 @@ const sidebar = {
|
||||
},
|
||||
],
|
||||
},
|
||||
'/api/cli',
|
||||
'/api/actions',
|
||||
'/api/sdk',
|
||||
{
|
||||
title: '@nocobase/cli',
|
||||
path: '/api/cli',
|
||||
type: 'item',
|
||||
link: '/api/cli',
|
||||
},
|
||||
{
|
||||
title: '@nocobase/actions',
|
||||
path: '/api/actions',
|
||||
type: 'item',
|
||||
link: '/api/actions',
|
||||
},
|
||||
{
|
||||
title: '@nocobase/sdk',
|
||||
path: '/api/sdk',
|
||||
type: 'item',
|
||||
link: '/api/sdk',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
# NocoBase CLI
|
||||
# @nocobase/cli
|
||||
|
||||
The NocoBase CLI is designed to help you develop, build, and deploy NocoBase applications.
|
||||
|
||||
|
@ -17,11 +17,11 @@ git pull
|
||||
v0.10 进行了依赖的重大升级,如果 v0.9 升级 v0.10,需要删掉以下目录之后再升级
|
||||
|
||||
```bash
|
||||
# 删除 .umi 相关缓存
|
||||
yarn rimraf -rf ./**/{.umi,.umi-production}
|
||||
# 删除编译文件
|
||||
yarn rimraf -rf packages/*/*/{lib,esm,es,dist,node_modules}
|
||||
# 删除全部依赖
|
||||
# Remove .umi cache
|
||||
yarn rimraf -rf "./**/{.umi,.umi-production}"
|
||||
# Delete compiled files
|
||||
yarn rimraf -rf "./packages/*/*/{lib,esm,es,dist,node_modules}"
|
||||
# Remove dependencies
|
||||
yarn rimraf -rf node_modules
|
||||
```
|
||||
|
||||
|
@ -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
|
||||
|
||||
```bash
|
||||
### Remove .umi-related cache
|
||||
# Remove .umi cache
|
||||
yarn rimraf -rf "./**/{.umi,.umi-production}"
|
||||
# Delete compiled files
|
||||
yarn rimraf -rf "./packages/*/*/{lib,esm,es,dist,node_modules}"
|
||||
|
@ -18,9 +18,9 @@ v0.10 进行了依赖的重大升级,如果 v0.9 升级 v0.10,需要删掉
|
||||
|
||||
```bash
|
||||
# 删除 .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
|
||||
```
|
||||
|
@ -57,13 +57,13 @@
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"auto-changelog": "^2.4.0",
|
||||
"dumi": "^2.2.0",
|
||||
"dumi-theme-nocobase": "^0.2.11",
|
||||
"dumi-theme-nocobase": "^0.2.12",
|
||||
"ghooks": "^2.0.4",
|
||||
"jsdom-worker": "^0.3.0",
|
||||
"prettier": "^2.2.1",
|
||||
"pretty-format": "^24.0.0",
|
||||
"pretty-quick": "^3.1.0",
|
||||
"vite": "^4.3.8",
|
||||
"vite": "^4.3.9",
|
||||
"vitest": "^0.32.0"
|
||||
},
|
||||
"volta": {
|
||||
|
@ -9,6 +9,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@umijs/utils": "3.5.20",
|
||||
"chalk": "^4.1.1",
|
||||
"commander": "^9.2.0",
|
||||
"dotenv": "^10.0.0",
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
const CommonMemo = React.memo((props) => {
|
||||
const MyProvider = React.memo((props) => {
|
||||
return <>{props.children}</>;
|
||||
});
|
||||
CommonMemo.displayName = 'CommonMemo';
|
||||
MyProvider.displayName = 'MyProvider';
|
||||
|
||||
export default CommonMemo;
|
||||
export default MyProvider;
|
||||
|
@ -7,12 +7,13 @@
|
||||
"typings": "es/index.d.ts",
|
||||
"dependencies": {
|
||||
"@antv/g2plot": "^2.4.18",
|
||||
"@ant-design/pro-layout": "^7.14.3",
|
||||
"@dnd-kit/core": "^5.0.1",
|
||||
"@dnd-kit/sortable": "^6.0.0",
|
||||
"@emotion/css": "^11.7.1",
|
||||
"@formily/antd": "2.2.24",
|
||||
"@formily/core": "2.2.24",
|
||||
"@formily/react": "2.2.24",
|
||||
"@formily/antd": "2.2.26",
|
||||
"@formily/core": "2.2.26",
|
||||
"@formily/react": "2.2.26",
|
||||
"@nocobase/evaluators": "0.10.0-alpha.2",
|
||||
"@nocobase/sdk": "0.10.0-alpha.2",
|
||||
"@nocobase/utils": "0.10.0-alpha.2",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Spin } from 'antd';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { useRequest, useAPIClient } from '../../api-client';
|
||||
import { useRoute } from '../../route-switch';
|
||||
import { useRequest } from '../../api-client';
|
||||
import { useAdminSchemaUid } from '../../hooks';
|
||||
|
||||
const MenuItemsContext = createContext(null);
|
||||
|
||||
@ -28,9 +28,9 @@ export const useMenuItems = () => {
|
||||
};
|
||||
|
||||
export const MenuItemsProvider = (props) => {
|
||||
const route = useRoute();
|
||||
const adminSchemaUid = useAdminSchemaUid();
|
||||
const options = {
|
||||
url: `uiSchemas:getProperties/${route.uiSchemaUid}`,
|
||||
url: `uiSchemas:getProperties/${adminSchemaUid}`,
|
||||
};
|
||||
const service = useRequest(options);
|
||||
if (service.loading) {
|
||||
|
@ -7,6 +7,8 @@ import { Link, NavLink } from 'react-router-dom';
|
||||
import { ACLProvider } from '../acl';
|
||||
import { AntdConfigProvider } from '../antd-config-provider';
|
||||
import { APIClient, APIClientProvider } from '../api-client';
|
||||
import { SigninPage, SignupPage } from '../auth';
|
||||
import { SigninPageExtensionProvider } from '../auth/SigninPageExtension';
|
||||
import { BlockSchemaComponentProvider } from '../block-provider';
|
||||
import { RemoteDocumentTitleProvider } from '../document-title';
|
||||
import { i18n } from '../i18n';
|
||||
@ -30,8 +32,6 @@ import { ErrorFallback } from '../schema-component/antd/error-fallback';
|
||||
import { SchemaInitializerProvider } from '../schema-initializer';
|
||||
import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates';
|
||||
import { SystemSettingsProvider } from '../system-settings';
|
||||
import { SigninPage, SignupPage } from '../auth';
|
||||
import { SigninPageExtensionProvider } from '../auth/SigninPageExtension';
|
||||
import { compose } from './compose';
|
||||
|
||||
export interface ApplicationOptions {
|
||||
@ -52,9 +52,7 @@ export type PluginCallback = () => Promise<any>;
|
||||
const App = React.memo((props: any) => {
|
||||
const C = compose(...props.providers)(() => {
|
||||
const routes = useRoutes();
|
||||
return (
|
||||
<RouteSwitch routes={routes} />
|
||||
);
|
||||
return <RouteSwitch routes={routes} />;
|
||||
});
|
||||
return <C />;
|
||||
});
|
||||
|
@ -1,19 +1,19 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useForm } from '@formily/react';
|
||||
import { Space, Tabs } from 'antd';
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
FunctionComponentElement,
|
||||
createContext,
|
||||
createElement,
|
||||
useCallback,
|
||||
useContext,
|
||||
createContext,
|
||||
FunctionComponent,
|
||||
createElement,
|
||||
useState,
|
||||
FunctionComponentElement,
|
||||
} from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAPIClient, useCurrentDocumentTitle, useRequest, useViewport } from '..';
|
||||
import { useSigninPageExtension } from './SigninPageExtension';
|
||||
import { useForm } from '@formily/react';
|
||||
|
||||
const SigninPageContext = createContext<{
|
||||
[authType: string]: {
|
||||
@ -132,13 +132,7 @@ export const SigninPage = () => {
|
||||
`}
|
||||
>
|
||||
{tabs.length > 1 ? (
|
||||
<Tabs>
|
||||
{tabs.map((tab) => (
|
||||
<Tabs.TabPane tab={tab.tabTitle} key={tab.name}>
|
||||
{tab.component}
|
||||
</Tabs.TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
<Tabs items={tabs.map((tab) => ({ label: tab.tabTitle, key: tab.name, children: tab.component }))} />
|
||||
) : tabs.length ? (
|
||||
<div>{tabs[0].component}</div>
|
||||
) : (
|
||||
|
@ -2,9 +2,9 @@ import { DownOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { ArrayTable } from '@formily/antd';
|
||||
import { ISchema, useField, useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Button, Dropdown, Menu } from 'antd';
|
||||
import { Button, Dropdown, MenuProps } from 'antd';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRequest } from '../../api-client';
|
||||
import { RecordProvider, useRecord } from '../../record-provider';
|
||||
@ -246,41 +246,41 @@ export const AddCollectionAction = (props) => {
|
||||
const [schema, setSchema] = useState({});
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
const collectionTemplates = templateOptions();
|
||||
const items = [];
|
||||
const collectionTemplates = useMemo(templateOptions, []);
|
||||
const items = useMemo(() => {
|
||||
const result = [];
|
||||
collectionTemplates.forEach((item) => {
|
||||
if (item.divider) {
|
||||
items.push({
|
||||
result.push({
|
||||
type: 'divider',
|
||||
});
|
||||
}
|
||||
items.push({ label: compile(item.title), key: item.name });
|
||||
result.push({ label: compile(item.title), key: item.name });
|
||||
});
|
||||
return result;
|
||||
}, [collectionTemplates]);
|
||||
const {
|
||||
state: { category },
|
||||
} = useResourceActionContext();
|
||||
return (
|
||||
<RecordProvider record={record}>
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Dropdown
|
||||
getPopupContainer={getContainer}
|
||||
trigger={trigger}
|
||||
align={align}
|
||||
overlay={
|
||||
<Menu
|
||||
style={{
|
||||
const menu = useMemo<MenuProps>(() => {
|
||||
return {
|
||||
style: {
|
||||
maxHeight: '60vh',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
onClick={(info) => {
|
||||
},
|
||||
onClick: (info) => {
|
||||
const schema = getSchema(getTemplate(info.key), category, compile);
|
||||
setSchema(schema);
|
||||
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 || (
|
||||
<Button icon={<PlusOutlined />} type={'primary'}>
|
||||
{t('Create collection')} <DownOutlined />
|
||||
|
@ -2,9 +2,9 @@ import { PlusOutlined } from '@ant-design/icons';
|
||||
import { ArrayTable } from '@formily/antd';
|
||||
import { useField, useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Button, Dropdown, Menu } from 'antd';
|
||||
import { Button, Dropdown, MenuProps } from 'antd';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRequest } from '../../api-client';
|
||||
import { RecordProvider, useRecord } from '../../record-provider';
|
||||
@ -12,7 +12,6 @@ import { ActionContextProvider, SchemaComponent, useActionContext, useCompile }
|
||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
||||
import { useCancelAction } from '../action-hooks';
|
||||
import { useCollectionManager } from '../hooks';
|
||||
import { useOptions } from '../hooks/useOptions';
|
||||
import { IField } from '../interfaces/types';
|
||||
import * as components from './components';
|
||||
import { getOptions } from './interfaces';
|
||||
@ -175,8 +174,7 @@ export const AddFieldAction = (props) => {
|
||||
const [schema, setSchema] = useState({});
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
const options = useOptions();
|
||||
const getFieldOptions = () => {
|
||||
const getFieldOptions = useCallback(() => {
|
||||
const { availableFieldInterfaces } = getTemplate(record.template) || {};
|
||||
const { exclude, include } = availableFieldInterfaces || {};
|
||||
const optionArr = [];
|
||||
@ -218,22 +216,39 @@ export const AddFieldAction = (props) => {
|
||||
}
|
||||
});
|
||||
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 }}>
|
||||
<Dropdown
|
||||
getPopupContainer={getContainer}
|
||||
trigger={trigger}
|
||||
align={align}
|
||||
overlay={
|
||||
<Menu
|
||||
style={{
|
||||
}),
|
||||
};
|
||||
});
|
||||
}, [getFieldOptions]);
|
||||
|
||||
const menu = useMemo<MenuProps>(() => {
|
||||
return {
|
||||
style: {
|
||||
maxHeight: '60vh',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
},
|
||||
onClick: (e) => {
|
||||
//@ts-ignore
|
||||
const targetScope = e.item.props['data-targetScope'];
|
||||
targetScope && setTargetScope(targetScope);
|
||||
@ -242,28 +257,16 @@ export const AddFieldAction = (props) => {
|
||||
setSchema(schema);
|
||||
setVisible(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{getFieldOptions().map((option) => {
|
||||
},
|
||||
items,
|
||||
};
|
||||
}, [getInterface, items, record]);
|
||||
|
||||
return (
|
||||
option.children.length > 0 && (
|
||||
<Menu.ItemGroup key={option.label} title={compile(option.label)}>
|
||||
{option.children
|
||||
.filter((child) => !['o2o', 'subTable', 'linkTo'].includes(child.name))
|
||||
.map((child) => {
|
||||
return (
|
||||
<Menu.Item key={child.name} data-targetScope={child.targetScope}>
|
||||
{compile(child.title)}
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu.ItemGroup>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
record.template !== 'view' && (
|
||||
<RecordProvider record={record}>
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Dropdown getPopupContainer={getContainer} trigger={trigger} align={align} menu={menu}>
|
||||
{children || (
|
||||
<Button icon={<PlusOutlined />} type={'primary'}>
|
||||
{t('Add field')}
|
||||
|
@ -2,9 +2,9 @@ import { PlusOutlined } from '@ant-design/icons';
|
||||
import { ArrayTable } from '@formily/antd';
|
||||
import { ISchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Button, Dropdown, Menu } from 'antd';
|
||||
import { Button, Dropdown, MenuProps } from 'antd';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRequest } from '../../api-client';
|
||||
import { RecordProvider } from '../../record-provider';
|
||||
@ -101,7 +101,8 @@ export const AddSubFieldAction = () => {
|
||||
const compile = useCompile();
|
||||
const options = useOptions();
|
||||
const { t } = useTranslation();
|
||||
const items = options.map((option) => {
|
||||
const items = useMemo(() => {
|
||||
return options.map((option) => {
|
||||
const children = option.children.map((child) => {
|
||||
return { label: compile(child.title), key: child.name };
|
||||
});
|
||||
@ -111,24 +112,25 @@ export const AddSubFieldAction = () => {
|
||||
children,
|
||||
};
|
||||
});
|
||||
return (
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu
|
||||
style={{
|
||||
}, [options]);
|
||||
const menu = useMemo<MenuProps>(() => {
|
||||
return {
|
||||
style: {
|
||||
maxHeight: '60vh',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
onClick={(info) => {
|
||||
},
|
||||
onClick: (info) => {
|
||||
const schema = getSchema(getInterface(info.key));
|
||||
setSchema(schema);
|
||||
setVisible(true);
|
||||
}}
|
||||
items={items}
|
||||
/>
|
||||
}
|
||||
>
|
||||
},
|
||||
items,
|
||||
};
|
||||
}, [items]);
|
||||
|
||||
return (
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Dropdown menu={menu}>
|
||||
<Button icon={<PlusOutlined />} type={'primary'}>
|
||||
{t('Add field')}
|
||||
</Button>
|
||||
|
@ -11,7 +11,8 @@ import {
|
||||
} from '@dnd-kit/core';
|
||||
import { RecursionField, observer } from '@formily/react';
|
||||
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 { useAPIClient } from '../../api-client';
|
||||
import { SchemaComponent, SchemaComponentOptions, useCompile } from '../../schema-component';
|
||||
@ -181,9 +182,13 @@ export const ConfigurationTabs = () => {
|
||||
value: item.id,
|
||||
}));
|
||||
};
|
||||
const menu = (item) => (
|
||||
<Menu>
|
||||
<Menu.Item key={'edit'}>
|
||||
|
||||
const menu = _.memoize((item) => {
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
key: 'edit',
|
||||
label: (
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
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>
|
||||
</Menu>
|
||||
);
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: compile("{{t('Delete category')}}"),
|
||||
onClick: () => remove(item.id),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<DndProvider>
|
||||
<Tabs
|
||||
@ -228,27 +238,24 @@ export const ConfigurationTabs = () => {
|
||||
type="editable-card"
|
||||
destroyInactiveTabPane={true}
|
||||
tabBarStyle={{ marginBottom: '0px' }}
|
||||
>
|
||||
{tabsItems.map((item) => {
|
||||
return (
|
||||
<Tabs.TabPane
|
||||
tab={
|
||||
items={tabsItems.map((item) => {
|
||||
return {
|
||||
label:
|
||||
item.id !== 'all' ? (
|
||||
<div data-no-dnd="true">
|
||||
<TabTitle item={item} />
|
||||
</div>
|
||||
) : (
|
||||
compile(item.name)
|
||||
)
|
||||
}
|
||||
key={item.id}
|
||||
closable={item.closable}
|
||||
closeIcon={
|
||||
<Dropdown overlay={menu(item)}>
|
||||
),
|
||||
key: item.id,
|
||||
closable: item.closable,
|
||||
closeIcon: (
|
||||
<Dropdown menu={menu(item)}>
|
||||
<MenuOutlined />
|
||||
</Dropdown>
|
||||
}
|
||||
>
|
||||
),
|
||||
children: (
|
||||
<Card bordered={false}>
|
||||
<SchemaComponentOptions
|
||||
components={{ CollectionFields }}
|
||||
@ -258,10 +265,10 @@ export const ConfigurationTabs = () => {
|
||||
<RecursionField name={key} schema={item.schema} onlyRenderProperties />
|
||||
</SchemaComponentOptions>
|
||||
</Card>
|
||||
</Tabs.TabPane>
|
||||
);
|
||||
),
|
||||
};
|
||||
})}
|
||||
</Tabs>
|
||||
/>
|
||||
</DndProvider>
|
||||
);
|
||||
};
|
||||
|
@ -82,7 +82,7 @@ const FormItemInitializer = (props) => {
|
||||
collection.fields.push(options);
|
||||
form.setValuesIn(name, uid());
|
||||
|
||||
const { values } = await FormDrawer('Add field', () => {
|
||||
await FormDrawer('Add field', () => {
|
||||
return (
|
||||
<CollectionManagerContext.Provider value={cm}>
|
||||
<AntdSchemaComponentProvider>
|
||||
|
@ -1,9 +1,11 @@
|
||||
import set from 'lodash/set';
|
||||
import { useMemo } from 'react';
|
||||
import { useCollectionManager } from './useCollectionManager';
|
||||
|
||||
export const useOptions = () => {
|
||||
const { interfaces } = useCollectionManager();
|
||||
|
||||
return useMemo(() => {
|
||||
const fields = {};
|
||||
|
||||
Object.keys(interfaces).forEach((type) => {
|
||||
@ -42,4 +44,5 @@ export const useOptions = () => {
|
||||
})
|
||||
.sort((a, b) => a.order - b.order),
|
||||
}));
|
||||
}, [interfaces]);
|
||||
};
|
||||
|
@ -24,7 +24,7 @@ export const integer: IField = {
|
||||
'x-component': 'InputNumber',
|
||||
'x-component-props': {
|
||||
stringMode: true,
|
||||
step: '0',
|
||||
step: '1',
|
||||
},
|
||||
'x-validator': 'integer',
|
||||
},
|
||||
|
@ -18,7 +18,7 @@ export const number: IField = {
|
||||
'x-component': 'InputNumber',
|
||||
'x-component-props': {
|
||||
stringMode: true,
|
||||
step: '0',
|
||||
step: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -32,9 +32,9 @@ export const number: IField = {
|
||||
title: '{{t("Precision")}}',
|
||||
'x-component': 'Select',
|
||||
'x-decorator': 'FormItem',
|
||||
default: '0',
|
||||
default: '1',
|
||||
enum: [
|
||||
{ value: '0', label: '1' },
|
||||
{ value: '1', label: '1' },
|
||||
{ value: '0.1', label: '1.0' },
|
||||
{ value: '0.01', label: '1.00' },
|
||||
{ value: '0.001', label: '1.000' },
|
||||
|
@ -63,7 +63,7 @@ export const percent: IField = {
|
||||
'x-component': 'Percent',
|
||||
'x-component-props': {
|
||||
stringMode: true,
|
||||
step: '0',
|
||||
step: '1',
|
||||
addonAfter: '%',
|
||||
},
|
||||
},
|
||||
@ -85,9 +85,9 @@ export const percent: IField = {
|
||||
title: '{{t("Precision")}}',
|
||||
'x-component': 'Select',
|
||||
'x-decorator': 'FormItem',
|
||||
default: '0',
|
||||
default: '1',
|
||||
enum: [
|
||||
{ value: '0', label: '1%' },
|
||||
{ value: '1', label: '1%' },
|
||||
{ value: '0.1', label: '1.0%' },
|
||||
{ value: '0.01', label: '1.00%' },
|
||||
{ value: '0.001', label: '1.000%' },
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Field, onFormSubmitValidateStart } from '@formily/core';
|
||||
import { useField, useFormEffects } from '@formily/react';
|
||||
import { Dropdown, Menu } from 'antd';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Dropdown, MenuProps } from 'antd';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function pasteHtml(html, selectPastedContent = false) {
|
||||
@ -71,11 +71,15 @@ export const Expression = (props) => {
|
||||
const inputRef = useRef<any>();
|
||||
const [changed, setChanged] = useState(false);
|
||||
|
||||
const onChange = (value) => {
|
||||
const onChange = useCallback(
|
||||
(value) => {
|
||||
setChanged(true);
|
||||
props.onChange(value);
|
||||
};
|
||||
},
|
||||
[props.onChange],
|
||||
);
|
||||
|
||||
const { numColumns, scope } = useMemo(() => {
|
||||
const numColumns = new Map<string, string>();
|
||||
const scope = {};
|
||||
fields
|
||||
@ -84,6 +88,10 @@ export const Expression = (props) => {
|
||||
numColumns.set(field.name, field.uiSchema.title);
|
||||
scope[field.name] = 1;
|
||||
});
|
||||
|
||||
return { numColumns, scope };
|
||||
}, [fields, supports]);
|
||||
|
||||
const keys = Array.from(numColumns.keys());
|
||||
const [html, setHtml] = useState(() => {
|
||||
const scope = {};
|
||||
@ -95,6 +103,7 @@ export const Expression = (props) => {
|
||||
}
|
||||
return renderExp(value || '', scope);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (changed) {
|
||||
return;
|
||||
@ -109,11 +118,13 @@ export const Expression = (props) => {
|
||||
const val = renderExp(value || '', scope);
|
||||
setHtml(val);
|
||||
}, [value]);
|
||||
const menu = (
|
||||
<Menu>
|
||||
{keys.length > 0 ? (
|
||||
keys.map((key) => (
|
||||
<Menu.Item disabled key={key}>
|
||||
|
||||
const menuItems = useMemo<MenuProps['items']>(() => {
|
||||
if (keys.length > 0) {
|
||||
return keys.map((key) => ({
|
||||
key,
|
||||
disabled: true,
|
||||
label: (
|
||||
<button
|
||||
onClick={async (args) => {
|
||||
(inputRef.current as any).focus();
|
||||
@ -123,20 +134,28 @@ export const Expression = (props) => {
|
||||
);
|
||||
const text = getValue(inputRef.current);
|
||||
onChange(text);
|
||||
console.log('onChange', text);
|
||||
}}
|
||||
>
|
||||
{numColumns.get(key)}
|
||||
</button>
|
||||
</Menu.Item>
|
||||
))
|
||||
) : (
|
||||
<Menu.Item disabled key={0}>
|
||||
{t('No available fields')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
),
|
||||
}));
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
key: 0,
|
||||
disabled: true,
|
||||
label: t('No available fields'),
|
||||
},
|
||||
];
|
||||
}
|
||||
}, [keys, numColumns, onChange]);
|
||||
|
||||
const menu = useMemo<MenuProps>(() => {
|
||||
return {
|
||||
items: menuItems,
|
||||
};
|
||||
}, [menuItems]);
|
||||
|
||||
useFormEffects(() => {
|
||||
onFormSubmitValidateStart(() => {
|
||||
@ -157,7 +176,7 @@ export const Expression = (props) => {
|
||||
return (
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
overlay={menu}
|
||||
menu={menu}
|
||||
overlayClassName={css`
|
||||
.ant-dropdown-menu-item {
|
||||
padding: 0;
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './useAdminSchemaUid';
|
||||
export * from './useViewport';
|
||||
|
6
packages/core/client/src/hooks/useAdminSchemaUid.ts
Normal file
6
packages/core/client/src/hooks/useAdminSchemaUid.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { useSystemSettings } from '../system-settings';
|
||||
|
||||
export const useAdminSchemaUid = () => {
|
||||
const ctx = useSystemSettings();
|
||||
return ctx?.data?.data?.options?.adminSchemaUid;
|
||||
};
|
102
packages/core/client/src/hooks/useMenuItem.tsx
Normal file
102
packages/core/client/src/hooks/useMenuItem.tsx
Normal 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 };
|
||||
};
|
@ -14,6 +14,7 @@ export * from './collection-manager';
|
||||
export * from './document-title';
|
||||
export * from './filter-provider';
|
||||
export * from './formula';
|
||||
export * from './hooks';
|
||||
export * from './i18n';
|
||||
export * from './icon';
|
||||
export * from './plugin-manager';
|
||||
@ -23,10 +24,9 @@ export * from './record-provider';
|
||||
export * from './route-switch';
|
||||
export * from './schema-component';
|
||||
export * from './schema-initializer';
|
||||
export * from './schema-items';
|
||||
export * from './schema-settings';
|
||||
export * from './schema-templates';
|
||||
export * from './schema-items';
|
||||
export * from './settings-form';
|
||||
export * from './system-settings';
|
||||
export * from './user';
|
||||
export * from './hooks';
|
||||
|
||||
|
@ -703,5 +703,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"
|
||||
"Create":"Create",
|
||||
"Current form": "Current form",
|
||||
"Current object":"Current object"
|
||||
};
|
||||
|
@ -614,5 +614,7 @@ export default {
|
||||
"First or create":"存在しない場合に追加",
|
||||
"Update or create":"存在しなければ新規、存在すれば更新",
|
||||
"Find by the following fields":"次のフィールドで検索",
|
||||
"Create":"新規のみ"
|
||||
"Create":"新規のみ" ,
|
||||
"Current form":"現在のフォーム",
|
||||
"Current object":"現在のオブジェクト"
|
||||
}
|
||||
|
@ -782,6 +782,8 @@ export default {
|
||||
"Update or create":"不存在时新增,存在时更新",
|
||||
"Find by the following fields":"通过以下字段查找",
|
||||
"Create":"仅新增",
|
||||
"Current form":"当前表单",
|
||||
"Current object":"当前对象",
|
||||
"Quick create": "快速创建",
|
||||
"Dropdown": "下拉菜单",
|
||||
"Pop-up": "弹窗",
|
||||
|
@ -1,9 +1,8 @@
|
||||
import React, { useEffect, useMemo, useState, useCallback, MouseEventHandler } from 'react';
|
||||
import { useAPIClient, useRequest } from '../api-client';
|
||||
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
Avatar,
|
||||
Card,
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Spin,
|
||||
@ -13,14 +12,15 @@ import {
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd';
|
||||
import { css } from '@emotion/css';
|
||||
import cls from 'classnames';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { useParseMarkdown } from '../schema-component/antd/markdown/util';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { IPluginData } from '.';
|
||||
import { useAPIClient, useRequest } from '../api-client';
|
||||
import { useParseMarkdown } from '../schema-component/antd/markdown/util';
|
||||
|
||||
interface PluginDocumentProps {
|
||||
path: string;
|
||||
@ -163,7 +163,7 @@ function PluginDetail(props: IPluginDetail) {
|
||||
destroyOnClose
|
||||
>
|
||||
{plugin?.description && <div className={'plugin-desc'}>{plugin?.description}</div>}
|
||||
<Tabs items={items}></Tabs>
|
||||
<Tabs items={items} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { ApiOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { Button, Dropdown, Menu, Tooltip } from 'antd';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { Button, Dropdown, MenuProps, Tooltip } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useContext, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useACLRoleContext } from '../acl/ACLProvider';
|
||||
import { ActionContextProvider, useCompile } from '../schema-component';
|
||||
import { getPluginsTabs, SettingsCenterContext } from './index';
|
||||
import { SettingsCenterContext, getPluginsTabs } from './index';
|
||||
|
||||
export const PluginManagerLink = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -23,7 +24,7 @@ export const PluginManagerLink = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const getBookmarkTabs = (data) => {
|
||||
const getBookmarkTabs = _.memoize((data) => {
|
||||
const bookmarkTabs = [];
|
||||
data.forEach((plugin) => {
|
||||
const tabs = plugin.tabs;
|
||||
@ -32,7 +33,7 @@ const getBookmarkTabs = (data) => {
|
||||
});
|
||||
});
|
||||
return bookmarkTabs;
|
||||
};
|
||||
});
|
||||
export const SettingsCenterDropdown = () => {
|
||||
const { snippets = [] } = useACLRoleContext();
|
||||
const [visible, setVisible] = useState(false);
|
||||
@ -42,11 +43,8 @@ export const SettingsCenterDropdown = () => {
|
||||
const itemData = useContext(SettingsCenterContext);
|
||||
const pluginsTabs = getPluginsTabs(itemData, snippets);
|
||||
const bookmarkTabs = getBookmarkTabs(pluginsTabs);
|
||||
return (
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Dropdown
|
||||
placement="bottom"
|
||||
menu={{
|
||||
const menu = useMemo<MenuProps>(() => {
|
||||
return {
|
||||
items: [
|
||||
...bookmarkTabs.map((tab) => ({
|
||||
key: `/admin/settings/${tab.path}`,
|
||||
@ -61,8 +59,12 @@ export const SettingsCenterDropdown = () => {
|
||||
onClick({ key }) {
|
||||
navigate(key);
|
||||
},
|
||||
}}
|
||||
>
|
||||
};
|
||||
}, [bookmarkTabs]);
|
||||
|
||||
return (
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Dropdown placement="bottom" menu={menu}>
|
||||
<Button
|
||||
icon={<SettingOutlined />}
|
||||
// title={t('All plugin settings')}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { PageHeader } from '@ant-design/pro-layout';
|
||||
import { css } from '@emotion/css';
|
||||
import { Layout, Menu, PageHeader, Result, Spin, Tabs } from 'antd';
|
||||
import { sortBy } from 'lodash';
|
||||
import { Layout, Menu, Result, Spin, Tabs } from 'antd';
|
||||
import _, { sortBy } from 'lodash';
|
||||
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router-dom';
|
||||
@ -126,6 +127,10 @@ const PluginList = (props) => {
|
||||
return snippets.includes('pm') ? (
|
||||
<div>
|
||||
<PageHeader
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
paddingBottom: 0,
|
||||
}}
|
||||
ghost={false}
|
||||
title={t('Plugin manager')}
|
||||
footer={
|
||||
@ -134,11 +139,21 @@ const PluginList = (props) => {
|
||||
onChange={(activeKey) => {
|
||||
navigate(`/admin/pm/list/${activeKey}`);
|
||||
}}
|
||||
>
|
||||
<Tabs.TabPane tab={t('Local')} key={'local'} />
|
||||
<Tabs.TabPane tab={t('Built-in')} key={'built-in'} />
|
||||
<Tabs.TabPane tab={t('Marketplace')} key={'marketplace'} />
|
||||
</Tabs>
|
||||
items={[
|
||||
{
|
||||
key: 'local',
|
||||
label: t('Local'),
|
||||
},
|
||||
{
|
||||
key: 'built-in',
|
||||
label: t('Built-in'),
|
||||
},
|
||||
{
|
||||
key: 'marketplace',
|
||||
label: t('Marketplace'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<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 tabsObj = items[plugin].tabs;
|
||||
const tabs = sortBy(
|
||||
@ -223,7 +238,7 @@ export const getPluginsTabs = (items, snippets) => {
|
||||
};
|
||||
});
|
||||
return sortBy(pluginsTabs, (o) => !o.isAllow);
|
||||
};
|
||||
});
|
||||
|
||||
const SettingsCenter = (props) => {
|
||||
const { snippets = [] } = useACLRoleContext();
|
||||
@ -296,6 +311,7 @@ const SettingsCenter = (props) => {
|
||||
<Layout.Content>
|
||||
{aclPluginTabCheck && (
|
||||
<PageHeader
|
||||
style={{ backgroundColor: 'white', paddingBottom: 0 }}
|
||||
ghost={false}
|
||||
title={compile(items[pluginName]?.title)}
|
||||
footer={
|
||||
@ -304,11 +320,16 @@ const SettingsCenter = (props) => {
|
||||
onChange={(activeKey) => {
|
||||
navigate(`/admin/settings/${pluginName}/${activeKey}`);
|
||||
}}
|
||||
>
|
||||
{plugin.tabs?.map((tab) => {
|
||||
return tab.isAllow && <Tabs.TabPane tab={compile(tab?.title)} key={tab.key} />;
|
||||
items={plugin.tabs?.map((tab) => {
|
||||
if (!tab.isAllow) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
label: compile(tab?.title),
|
||||
key: tab.key,
|
||||
};
|
||||
})}
|
||||
</Tabs>
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
@ -14,9 +14,9 @@ import {
|
||||
findByUid,
|
||||
findMenuItem,
|
||||
useACLRoleContext,
|
||||
useAdminSchemaUid,
|
||||
useDocumentTitle,
|
||||
useRequest,
|
||||
useRoute,
|
||||
useSystemSettings,
|
||||
} from '../../../';
|
||||
import { useCollectionManager } from '../../../collection-manager';
|
||||
@ -54,6 +54,7 @@ const useMenuProps = () => {
|
||||
defaultSelectedUid,
|
||||
};
|
||||
};
|
||||
|
||||
const MenuEditor = (props) => {
|
||||
const { setTitle } = useDocumentTitle();
|
||||
const navigate = useNavigate();
|
||||
@ -61,7 +62,6 @@ const MenuEditor = (props) => {
|
||||
const defaultSelectedUid = params.name;
|
||||
const { sideMenuRef } = props;
|
||||
const ctx = useACLRoleContext();
|
||||
const route = useRoute();
|
||||
const [current, setCurrent] = useState(null);
|
||||
const onSelect = ({ item }) => {
|
||||
const schema = item.props.schema;
|
||||
@ -70,12 +70,14 @@ const MenuEditor = (props) => {
|
||||
navigate(`/admin/${schema['x-uid']}`);
|
||||
};
|
||||
|
||||
const adminSchemaUid = useAdminSchemaUid();
|
||||
|
||||
const { data, loading } = useRequest(
|
||||
{
|
||||
url: `/uiSchemas:getJsonSchema/${route.uiSchemaUid}`,
|
||||
url: `/uiSchemas:getJsonSchema/${adminSchemaUid}`,
|
||||
},
|
||||
{
|
||||
refreshDeps: [route.uiSchemaUid],
|
||||
refreshDeps: [adminSchemaUid],
|
||||
onSuccess(data) {
|
||||
const schema = filterByACL(data?.data, ctx);
|
||||
// url 为 `/admin` 的情况
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { connect, ISchema, mapProps, useField, useFieldSchema } from '@formily/react';
|
||||
import { isValid, uid } from '@formily/shared';
|
||||
import { Tree as AntdTree, Menu } from 'antd';
|
||||
import { Tree as AntdTree } from 'antd';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -46,7 +46,11 @@ const MenuGroup = (props) => {
|
||||
) {
|
||||
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) => {
|
||||
@ -54,7 +58,7 @@ export const ActionDesigner = (props) => {
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { name } = useCollection();
|
||||
const { getChildrenCollections, getCollection, getCollectionField } = useCollectionManager();
|
||||
const { getChildrenCollections } = useCollectionManager();
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const isAction = useLinkageAction();
|
||||
|
@ -36,7 +36,7 @@ export const ActionDrawer: ComposedActionDrawer = observer(
|
||||
{...others}
|
||||
{...drawerProps}
|
||||
{...modalProps}
|
||||
style={{
|
||||
rootStyle={{
|
||||
...drawerProps?.style,
|
||||
...others?.style,
|
||||
}}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import { observer, RecursionField, useFieldSchema } from '@formily/react';
|
||||
import { RecursionField, observer, useFieldSchema } from '@formily/react';
|
||||
import { Space } from 'antd';
|
||||
import React, { CSSProperties, useContext } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
@ -46,7 +46,7 @@ export const DeleteEvent = observer(
|
||||
return createPortal(
|
||||
<Modal
|
||||
title={cron ? t('Delete events') : null}
|
||||
visible={visible}
|
||||
open={visible}
|
||||
onCancel={() => setVisible(false)}
|
||||
onOk={() => onOk()}
|
||||
confirmLoading={loading}
|
||||
|
@ -2,10 +2,9 @@ import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { ArrayField } from '@formily/core';
|
||||
import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
|
||||
import { toArr } from '@formily/shared';
|
||||
import { action } from '@formily/reactive';
|
||||
import { Cascader as AntdCascader, Space } from 'antd';
|
||||
import { isBoolean, omit } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { useRequest } from '../../../api-client';
|
||||
import { defaultFieldNames } from './defaultFieldNames';
|
||||
import { ReadPretty } from './ReadPretty';
|
||||
@ -64,7 +63,7 @@ export const Cascader = connect(
|
||||
);
|
||||
};
|
||||
const handelDropDownVisible = (value) => {
|
||||
if (value && !field.dataSource) {
|
||||
if (value && !field.dataSource.length) {
|
||||
run();
|
||||
}
|
||||
};
|
||||
|
@ -104,6 +104,7 @@ FormItem.Designer = function Designer() {
|
||||
const { getCollectionFields, getInterface, getCollectionJoinField, getCollection } = useCollectionManager();
|
||||
const { getField } = useCollection();
|
||||
const { form } = useFormBlockContext();
|
||||
const ctx = useBlockRequestContext();
|
||||
const field = useField<Field>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { t } = useTranslation();
|
||||
@ -111,7 +112,6 @@ FormItem.Designer = function Designer() {
|
||||
const compile = useCompile();
|
||||
const variablesCtx = useVariablesCtx();
|
||||
const IsShowMultipleSwitch = useIsShowMultipleSwitch();
|
||||
|
||||
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||
if (collectionField?.target) {
|
||||
targetField = getCollectionJoinField(
|
||||
@ -447,11 +447,16 @@ FormItem.Designer = function Designer() {
|
||||
properties: {
|
||||
filter: {
|
||||
default: defaultFilter,
|
||||
// title: '数据范围',
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'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);
|
||||
_.set(field.componentProps, 'service.params.filter', filter);
|
||||
fieldSchema['x-component-props'] = field.componentProps;
|
||||
field.componentProps = field.componentProps;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
@ -878,6 +882,11 @@ export function isFileCollection(collection: Collection) {
|
||||
return collection?.template === 'file';
|
||||
}
|
||||
|
||||
function extractFirstPart(path) {
|
||||
const firstDotIndex = path.indexOf('.');
|
||||
return firstDotIndex !== -1 ? path.slice(0, firstDotIndex) : path;
|
||||
}
|
||||
|
||||
FormItem.FilterFormDesigner = FilterFormDesigner;
|
||||
|
||||
export function getFieldDefaultValue(fieldSchema: ISchema, collectionField: CollectionFieldOptions) {
|
||||
|
@ -1,18 +1,17 @@
|
||||
import { useFieldSchema, useField, ISchema } from '@formily/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { ArrayItems } from '@formily/antd';
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import { Slider } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCollection, useCollectionFilterOptions, useSortFields } from '../../../collection-manager';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { SchemaComponentOptions } from '../../core';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
|
||||
import { SchemaComponentOptions } from '../../core';
|
||||
import { defaultColumnCount, gridSizes, pageSizeOptions, screenSizeMaps, screenSizeTitleMaps } from './options';
|
||||
Slider;
|
||||
|
||||
const columnCountMarks = [1, 2, 3, 4, 6, 8, 12, 24].reduce((obj, cur) => {
|
||||
obj[cur] = cur;
|
||||
|
@ -42,7 +42,5 @@
|
||||
}
|
||||
|
||||
html body {
|
||||
--adm-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB,
|
||||
Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji,
|
||||
Segoe UI Symbol;
|
||||
--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";
|
||||
}
|
||||
|
@ -1,18 +1,21 @@
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
FieldContext,
|
||||
observer,
|
||||
RecursionField,
|
||||
Schema,
|
||||
SchemaContext,
|
||||
SchemaExpressionScopeContext,
|
||||
useField,
|
||||
useFieldSchema,
|
||||
} from '@formily/react';
|
||||
import { Menu as AntdMenu } from 'antd';
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { error } from '@nocobase/utils/client';
|
||||
import { Menu as AntdMenu, MenuProps } from 'antd';
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createDesignable, DndContext, SortableItem, useDesignable, useDesigner } from '../..';
|
||||
import { Icon, useAPIClient, useSchemaInitializer } from '../../../';
|
||||
import { useCollectMenuItems, useMenuItem } from '../../../hooks/useMenuItem';
|
||||
import { useProps } from '../../hooks/useProps';
|
||||
import { MenuDesigner } from './Menu.Designer';
|
||||
import { findKeysByUid, findMenuItem } from './util';
|
||||
@ -115,7 +118,7 @@ const designerCss = css`
|
||||
}
|
||||
`;
|
||||
|
||||
const antdMenuClass = css`
|
||||
const headerMenuClass = css`
|
||||
.ant-menu-item:hover {
|
||||
> .ant-menu-title-content > div {
|
||||
.general-schema-designer {
|
||||
@ -125,171 +128,7 @@ const antdMenuClass = css`
|
||||
}
|
||||
`;
|
||||
|
||||
type ComposedMenu = React.FC<any> & {
|
||||
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`
|
||||
const sideMenuClass = css`
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
@ -320,11 +159,127 @@ export const Menu: ComposedMenu = observer(
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
<RecursionField schema={sideMenuSchema} onlyRenderProperties />
|
||||
{render({
|
||||
style: { margin: 8 },
|
||||
`;
|
||||
|
||||
const menuItemClass = css`
|
||||
:active {
|
||||
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) => {
|
||||
const dn = createDesignable({
|
||||
t,
|
||||
@ -335,11 +290,167 @@ export const Menu: ComposedMenu = observer(
|
||||
dn.loadAPIClientEvents();
|
||||
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>,
|
||||
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>
|
||||
</MenuItemDesignerContext.Provider>
|
||||
</DndContext>
|
||||
@ -350,116 +461,149 @@ export const Menu: ComposedMenu = observer(
|
||||
|
||||
Menu.Item = observer(
|
||||
(props) => {
|
||||
const { icon, ...others } = props;
|
||||
const { pushMenuItem } = useCollectMenuItems();
|
||||
const { icon, children, ...others } = props;
|
||||
const schema = useFieldSchema();
|
||||
const field = useField();
|
||||
const Designer = useContext(MenuItemDesignerContext);
|
||||
return (
|
||||
<AntdMenu.Item
|
||||
{...others}
|
||||
className={css`
|
||||
:active {
|
||||
background: inherit;
|
||||
}
|
||||
`}
|
||||
key={schema.name}
|
||||
eventKey={schema.name}
|
||||
schema={schema}
|
||||
>
|
||||
const item = useMemo(() => {
|
||||
return {
|
||||
...others,
|
||||
className: menuItemClass,
|
||||
key: schema.name,
|
||||
eventKey: schema.name,
|
||||
schema,
|
||||
label: (
|
||||
<SchemaContext.Provider value={schema}>
|
||||
<FieldContext.Provider value={field}>
|
||||
<SortableItem className={designerCss} removeParentsIfNoChildren={false}>
|
||||
<Icon type={icon} />
|
||||
<span
|
||||
className={css`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
`}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
>
|
||||
{field.title}
|
||||
</span>
|
||||
{Designer && <Designer />}
|
||||
</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' },
|
||||
);
|
||||
|
||||
Menu.URL = observer(
|
||||
(props) => {
|
||||
const { icon, ...others } = props;
|
||||
const { pushMenuItem } = useCollectMenuItems();
|
||||
const { icon, children, ...others } = props;
|
||||
const schema = useFieldSchema();
|
||||
const field = useField();
|
||||
const Designer = useContext(MenuItemDesignerContext);
|
||||
return (
|
||||
<AntdMenu.Item
|
||||
{...others}
|
||||
className={css`
|
||||
:active {
|
||||
background: inherit;
|
||||
|
||||
if (!pushMenuItem) {
|
||||
error('Menu.URL must be wrapped by GetMenuItemsContext.Provider');
|
||||
return null;
|
||||
}
|
||||
`}
|
||||
key={schema.name}
|
||||
eventKey={schema.name}
|
||||
schema={schema}
|
||||
onClick={() => {
|
||||
|
||||
const item = useMemo(() => {
|
||||
return {
|
||||
...others,
|
||||
className: menuItemClass,
|
||||
key: schema.name,
|
||||
eventKey: schema.name,
|
||||
schema,
|
||||
onClick: () => {
|
||||
window.open(props.href, '_blank');
|
||||
}}
|
||||
>
|
||||
},
|
||||
label: (
|
||||
<SchemaContext.Provider value={schema}>
|
||||
<FieldContext.Provider value={field}>
|
||||
<SortableItem className={designerCss} removeParentsIfNoChildren={false}>
|
||||
<Icon type={icon} />
|
||||
<span
|
||||
className={css`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
`}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
>
|
||||
{field.title}
|
||||
</span>
|
||||
{Designer && <Designer />}
|
||||
</SortableItem>
|
||||
</AntdMenu.Item>
|
||||
);
|
||||
</FieldContext.Provider>
|
||||
</SchemaContext.Provider>
|
||||
),
|
||||
};
|
||||
}, [field.title, icon, props.href, schema]);
|
||||
|
||||
pushMenuItem(item);
|
||||
return null;
|
||||
},
|
||||
{ displayName: 'MenuURL' },
|
||||
);
|
||||
|
||||
Menu.SubMenu = observer(
|
||||
(props) => {
|
||||
const { icon, ...others } = props;
|
||||
const { Component, getMenuItems } = useMenuItem();
|
||||
const { pushMenuItem } = useCollectMenuItems();
|
||||
const { icon, children, ...others } = props;
|
||||
const schema = useFieldSchema();
|
||||
const field = useField();
|
||||
const mode = useContext(MenuModeContext);
|
||||
const Designer = useContext(MenuItemDesignerContext);
|
||||
if (mode === 'mix') {
|
||||
return <Menu.Item {...props} />;
|
||||
}
|
||||
return (
|
||||
<AntdMenu.SubMenu
|
||||
{...others}
|
||||
className={css`
|
||||
:active {
|
||||
background: inherit;
|
||||
}
|
||||
`}
|
||||
key={schema.name}
|
||||
eventKey={schema.name}
|
||||
title={
|
||||
const submenu = useMemo(() => {
|
||||
return {
|
||||
...others,
|
||||
className: menuItemClass,
|
||||
key: schema.name,
|
||||
eventKey: schema.name,
|
||||
label: (
|
||||
<SchemaContext.Provider value={schema}>
|
||||
<FieldContext.Provider value={field}>
|
||||
<SortableItem className={subMenuDesignerCss} removeParentsIfNoChildren={false}>
|
||||
<Icon type={icon} />
|
||||
{field.title}
|
||||
{Designer && <Designer />}
|
||||
</SortableItem>
|
||||
</FieldContext.Provider>
|
||||
</SchemaContext.Provider>
|
||||
),
|
||||
children: getMenuItems(() => {
|
||||
return <RecursionField schema={schema} onlyRenderProperties />;
|
||||
}),
|
||||
};
|
||||
}, [field.title, icon, schema, children]);
|
||||
|
||||
if (!pushMenuItem) {
|
||||
error('Menu.SubMenu must be wrapped by GetMenuItemsContext.Provider');
|
||||
return null;
|
||||
}
|
||||
>
|
||||
<RecursionField schema={schema} onlyRenderProperties />
|
||||
</AntdMenu.SubMenu>
|
||||
);
|
||||
|
||||
if (mode === 'mix') {
|
||||
return <Menu.Item {...props} />;
|
||||
}
|
||||
|
||||
pushMenuItem(submenu);
|
||||
return <Component />;
|
||||
},
|
||||
{ displayName: 'Menu.SubMenu' },
|
||||
);
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout';
|
||||
import { css } from '@emotion/css';
|
||||
import { FormDialog, FormLayout } from '@formily/antd';
|
||||
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 React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
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) => {
|
||||
const { children, ...others } = props;
|
||||
const compile = useCompile();
|
||||
@ -156,6 +176,8 @@ export const Page = (props) => {
|
||||
const handleErrors = (error) => {
|
||||
console.error(error);
|
||||
};
|
||||
|
||||
const pageHeaderTitle = hidePageTitle ? undefined : fieldSchema.title || compile(title);
|
||||
return (
|
||||
<FilterBlockProvider>
|
||||
<div className={pageDesignerCss}>
|
||||
@ -167,19 +189,10 @@ export const Page = (props) => {
|
||||
>
|
||||
{!disablePageHeader && (
|
||||
<AntdPageHeader
|
||||
className={css`
|
||||
&.has-footer {
|
||||
padding-top: 12px;
|
||||
.ant-page-header-heading-left {
|
||||
/* margin: 0; */
|
||||
}
|
||||
.ant-page-header-footer {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
`}
|
||||
className={classNames(pageHeaderCss, pageHeaderTitle || enablePageTabs ? '' : height0)}
|
||||
ghost={false}
|
||||
title={hidePageTitle ? undefined : fieldSchema.title || compile(title)}
|
||||
// 如果标题为空的时候会导致 PageHeader 不渲染,所以这里设置一个空白字符,然后再设置高度为 0
|
||||
title={pageHeaderTitle || ' '}
|
||||
{...others}
|
||||
footer={
|
||||
enablePageTabs && (
|
||||
@ -247,11 +260,9 @@ export const Page = (props) => {
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
{fieldSchema.mapProperties((schema) => {
|
||||
return (
|
||||
<Tabs.TabPane
|
||||
tab={
|
||||
items={fieldSchema.mapProperties((schema) => {
|
||||
return {
|
||||
label: (
|
||||
<SortableItem
|
||||
id={schema.name as string}
|
||||
schema={schema}
|
||||
@ -261,12 +272,11 @@ export const Page = (props) => {
|
||||
<span>{schema.title || t('Unnamed')}</span>
|
||||
<PageTabDesigner schema={schema} />
|
||||
</SortableItem>
|
||||
}
|
||||
key={schema.name}
|
||||
/>
|
||||
);
|
||||
),
|
||||
key: schema.name as string,
|
||||
};
|
||||
})}
|
||||
</Tabs>
|
||||
/>
|
||||
</DndContext>
|
||||
)
|
||||
}
|
||||
|
@ -1,15 +1,18 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { connect, mapProps, mapReadPretty, useField, useFieldSchema } from '@formily/react';
|
||||
import { SelectProps, Tag, Empty, Divider } from 'antd';
|
||||
import { connect, mapProps, mapReadPretty, useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { Divider, SelectProps, Tag } from 'antd';
|
||||
import flat from 'flat';
|
||||
import { uniqBy } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ResourceActionOptions, useRequest } from '../../../api-client';
|
||||
import { useBlockRequestContext } from '../../../block-provider/BlockProvider';
|
||||
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
||||
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 { extractFilterfield, extractValuesByPattern, generatePattern, parseVariables } from './utils';
|
||||
const EMPTY = 'N/A';
|
||||
|
||||
export type RemoteSelectProps<P = any> = SelectProps<P, any> & {
|
||||
@ -39,10 +42,12 @@ const InternalRemoteSelect = connect(
|
||||
...others
|
||||
} = props;
|
||||
const [open, setOpen] = useState(false);
|
||||
const form = useForm();
|
||||
const firstRun = useRef(false);
|
||||
const fieldSchema = useFieldSchema();
|
||||
const isQuickAdd = fieldSchema['x-component-props']?.addMode === 'quickAdd';
|
||||
const field = useField();
|
||||
const ctx = useBlockRequestContext();
|
||||
const { getField } = useCollection();
|
||||
const searchData = useRef(null);
|
||||
const { getCollectionJoinField, getInterface } = useCollectionManager();
|
||||
@ -115,6 +120,49 @@ const InternalRemoteSelect = connect(
|
||||
},
|
||||
[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(
|
||||
{
|
||||
action: 'list',
|
||||
@ -122,9 +170,8 @@ const InternalRemoteSelect = connect(
|
||||
params: {
|
||||
pageSize: 200,
|
||||
...service?.params,
|
||||
// fields: [fieldNames.label, fieldNames.value, ...(service?.params?.fields || [])],
|
||||
// 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])) || [];
|
||||
return uniqBy(data?.data?.concat(valueOptions) || [], fieldNames.value);
|
||||
}, [data?.data, value]);
|
||||
|
||||
const onDropdownVisibleChange = (visible) => {
|
||||
setOpen(visible);
|
||||
searchData.current = null;
|
||||
if (firstRun.current && data?.data.length > 0) {
|
||||
return;
|
||||
}
|
||||
run();
|
||||
firstRun.current = true;
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
@ -3,8 +3,8 @@ import { useVariableOptions } from '../../../schema-settings/VariableInput/hooks
|
||||
import { Variable } from '../variable';
|
||||
|
||||
export function FilterDynamicComponent(props) {
|
||||
const { value, onChange, renderSchemaComponent } = props;
|
||||
const options = useVariableOptions();
|
||||
const { value, onChange, renderSchemaComponent, form, collectionField, ...other } = props;
|
||||
const options = useVariableOptions({ form, collectionField, ...other });
|
||||
|
||||
return (
|
||||
<Variable.Input value={value} onChange={onChange} scope={options}>
|
||||
|
@ -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: pagination2, onClickRow, ...others2 } = useProps?.() || {};
|
||||
const {
|
||||
@ -453,7 +454,7 @@ export const Table: any = observer((props: any) => {
|
||||
return dragSort
|
||||
? React.createElement(SortableContext, {
|
||||
items: field.value?.map?.(getRowKey) || [],
|
||||
children: children,
|
||||
children,
|
||||
})
|
||||
: React.createElement(React.Fragment, {
|
||||
children,
|
||||
@ -533,4 +534,6 @@ export const Table: any = observer((props: any) => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
},
|
||||
{ displayName: 'Table' },
|
||||
);
|
||||
|
@ -1,47 +1,51 @@
|
||||
import { css } from '@emotion/css';
|
||||
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 React, { useMemo } from 'react';
|
||||
import { Icon } from '../../../icon';
|
||||
import { useSchemaInitializer } from '../../../schema-initializer';
|
||||
import { DndContext, SortableItem } from '../../common';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { useDesigner } from '../../hooks/useDesigner';
|
||||
import { TabsContextProvider, useTabsContext } from './context';
|
||||
import { useTabsContext } from './context';
|
||||
import { TabsDesigner } from './Tabs.Designer';
|
||||
|
||||
export const Tabs: any = observer(
|
||||
(props: TabsProps) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
|
||||
const { designable } = useDesignable();
|
||||
const contextProps = useTabsContext();
|
||||
const { PaneRoot = React.Fragment as React.FC<any> } = contextProps;
|
||||
|
||||
const PaneProvider = useMemo(() => {
|
||||
if (contextProps.deep === false) {
|
||||
return TabsContextProvider;
|
||||
const items = useMemo(() => {
|
||||
const result = fieldSchema.mapProperties((schema, key: string) => {
|
||||
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 (
|
||||
<DndContext>
|
||||
<AntdTabs
|
||||
{...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>
|
||||
<AntdTabs {...contextProps} style={props.style} items={items} />
|
||||
</DndContext>
|
||||
);
|
||||
},
|
||||
|
@ -2,7 +2,7 @@ import { TabsProps } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
interface TabsContextProps extends TabsProps {
|
||||
deep?: boolean;
|
||||
PaneRoot?: React.FC<any>;
|
||||
}
|
||||
const TabsContext = React.createContext<TabsContextProps>({});
|
||||
|
||||
|
@ -18,7 +18,7 @@ type Composed = React.FC<UploadProps> & {
|
||||
|
||||
export const ReadPretty: Composed = () => null;
|
||||
|
||||
ReadPretty.File = (props: UploadProps) => {
|
||||
ReadPretty.File = function File(props: UploadProps) {
|
||||
const record = useRecord();
|
||||
const field = useField<Field>();
|
||||
const value = isString(field.value) ? record : field.value;
|
||||
@ -44,7 +44,7 @@ ReadPretty.File = (props: UploadProps) => {
|
||||
// }
|
||||
};
|
||||
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-info'}>
|
||||
<span className="ant-upload-span">
|
||||
@ -114,6 +114,7 @@ ReadPretty.File = (props: UploadProps) => {
|
||||
imageTitle={images[photoIndex]?.title}
|
||||
toolbarButtons={[
|
||||
<button
|
||||
key={'download'}
|
||||
style={{ fontSize: 22, background: 'none', lineHeight: 1 }}
|
||||
type="button"
|
||||
aria-label="Zoom in"
|
||||
@ -135,10 +136,10 @@ ReadPretty.File = (props: UploadProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
ReadPretty.Upload = (props) => {
|
||||
ReadPretty.Upload = function Upload(props) {
|
||||
const field = useField<Field>();
|
||||
return (field.value || []).map((item) => (
|
||||
<div>
|
||||
<div key={item.name}>
|
||||
{item.url ? (
|
||||
<a target={'_blank'} href={item.url} rel="noreferrer">
|
||||
{item.name}
|
||||
|
@ -65,7 +65,7 @@ export function VariableSelect(props) {
|
||||
}
|
||||
}
|
||||
}}
|
||||
dropdownClassName={css`
|
||||
popupClassName={css`
|
||||
.ant-cascader-menu {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
|
||||
import flat from 'flat';
|
||||
import _, { every, findIndex, isArray, some } from 'lodash';
|
||||
import moment from 'moment';
|
||||
@ -13,7 +14,6 @@ type VariablesCtx = {
|
||||
|
||||
export const useVariablesCtx = (): VariablesCtx => {
|
||||
const { data } = useCurrentUserContext() || {};
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
$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) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
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 React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { useCollectMenuItem, useMenuItem } from '../hooks/useMenuItem';
|
||||
import { Icon } from '../icon';
|
||||
import { SchemaComponent, useActionContext } from '../schema-component';
|
||||
import { useCompile, useDesignable } from '../schema-component/hooks';
|
||||
@ -14,10 +16,22 @@ import {
|
||||
SchemaInitializerItemProps,
|
||||
} from './types';
|
||||
|
||||
const overlayClassName = css`
|
||||
.ant-dropdown-menu-item-group-list {
|
||||
max-height: 40vh;
|
||||
overflow: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
const defaultWrap = (s: ISchema) => s;
|
||||
|
||||
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;
|
||||
|
||||
@ -41,27 +55,72 @@ SchemaInitializer.Button = observer(
|
||||
const compile = useCompile();
|
||||
const { insertAdjacent, findComponent, designable } = useDesignable();
|
||||
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) => {
|
||||
if (props.insert) {
|
||||
props.insert(wrap(schema));
|
||||
if (insert) {
|
||||
insert(wrap(schema));
|
||||
} else {
|
||||
insertAdjacent(insertPosition, wrap(schema), { onSuccess });
|
||||
}
|
||||
};
|
||||
|
||||
const renderItems = (items: any) => {
|
||||
return items
|
||||
.filter((v) => {
|
||||
.filter((v: any) => {
|
||||
return v && (v?.visible ? v.visible() : true);
|
||||
})
|
||||
?.map((item, indexA) => {
|
||||
?.map((item: any, indexA: number) => {
|
||||
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) {
|
||||
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 (
|
||||
Component && (
|
||||
<SchemaInitializerItemContext.Provider
|
||||
key={item.key}
|
||||
value={{
|
||||
@ -80,75 +139,61 @@ SchemaInitializer.Button = observer(
|
||||
insert={insertSchema}
|
||||
/>
|
||||
</SchemaInitializerItemContext.Provider>
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
if (item.type === 'itemGroup') {
|
||||
const label = compile(item.title);
|
||||
return (
|
||||
!!item.children?.length && (
|
||||
<Menu.ItemGroup key={item.key || `item-group-${indexA}`} title={compile(item.title)}>
|
||||
{renderItems(item.children)}
|
||||
</Menu.ItemGroup>
|
||||
)
|
||||
!!item.children?.length && {
|
||||
type: 'group',
|
||||
key: item.key || `item-group-${indexA}`,
|
||||
label,
|
||||
title: label,
|
||||
children: renderItems(item.children),
|
||||
}
|
||||
);
|
||||
}
|
||||
if (item.type === 'subMenu') {
|
||||
const label = compile(item.title);
|
||||
return (
|
||||
!!item.children?.length && (
|
||||
<Menu.SubMenu
|
||||
key={item.key || `item-group-${indexA}`}
|
||||
title={compile(item.title)}
|
||||
popupClassName={menuItemGroupCss}
|
||||
>
|
||||
{renderItems(item.children)}
|
||||
</Menu.SubMenu>
|
||||
)
|
||||
!!item.children?.length && {
|
||||
key: item.key || `item-group-${indexA}`,
|
||||
label,
|
||||
title: label,
|
||||
popupClassName: menuItemGroupCss,
|
||||
children: renderItems(item.children),
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const buttonDom = (
|
||||
<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>
|
||||
);
|
||||
if (!items.length) {
|
||||
return buttonDom;
|
||||
}
|
||||
const menu = <Menu style={{ maxHeight: '60vh', overflowY: 'auto' }}>{renderItems(items)}</Menu>;
|
||||
if (!designable && props.designable !== true) {
|
||||
return null;
|
||||
}
|
||||
clean();
|
||||
const menuItems = renderItems(items);
|
||||
|
||||
return (
|
||||
<SchemaInitializerButtonContext.Provider value={{ visible, setVisible }}>
|
||||
<SchemaInitializerButtonContext.Provider value={{ visible, setVisible, searchValue, setSearchValue }}>
|
||||
<CollectionComponent />
|
||||
<Dropdown
|
||||
className={classNames('nb-schema-initializer-button')}
|
||||
openClassName={`nb-schema-initializer-button-open`}
|
||||
overlayClassName={classNames(
|
||||
'nb-schema-initializer-button-overlay',
|
||||
css`
|
||||
.ant-dropdown-menu-item-group-list {
|
||||
max-height: 40vh;
|
||||
overflow: auto;
|
||||
}
|
||||
`,
|
||||
)}
|
||||
overlayClassName={classNames('nb-schema-initializer-button-overlay', overlayClassName)}
|
||||
open={visible}
|
||||
onOpenChange={(visible) => {
|
||||
setVisible(visible);
|
||||
onOpenChange={() => {
|
||||
// 如果不清空输入框的值,那么下次打开的时候会出现上次输入的值
|
||||
setSearchValue('');
|
||||
setShouldRender(false);
|
||||
setVisible(false);
|
||||
}}
|
||||
menu={{
|
||||
style: {
|
||||
maxHeight: '60vh',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
items: menuItems,
|
||||
}}
|
||||
{...dropdown}
|
||||
overlay={menu}
|
||||
>
|
||||
{component ? component : buttonDom}
|
||||
</Dropdown>
|
||||
@ -158,10 +203,17 @@ SchemaInitializer.Button = observer(
|
||||
{ displayName: 'SchemaInitializer.Button' },
|
||||
);
|
||||
|
||||
SchemaInitializer.Item = (props: SchemaInitializerItemProps) => {
|
||||
const { index, info } = useContext(SchemaInitializerItemContext);
|
||||
SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
|
||||
const { info } = useContext(SchemaInitializerItemContext);
|
||||
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) {
|
||||
const renderMenuItem = (items: SchemaInitializerItemOptions[]) => {
|
||||
if (!items?.length) {
|
||||
@ -169,77 +221,70 @@ SchemaInitializer.Item = (props: SchemaInitializerItemProps) => {
|
||||
}
|
||||
return items.map((item, indexA) => {
|
||||
if (item.type === 'divider') {
|
||||
return <Menu.Divider key={`divider-${indexA}`} />;
|
||||
return { type: 'divider', key: `divider-${indexA}` };
|
||||
}
|
||||
if (item.type === 'itemGroup') {
|
||||
return (
|
||||
<Menu.ItemGroup
|
||||
// @ts-ignore
|
||||
eventKey={item.key || `item-group-${indexA}`}
|
||||
key={item.key || `item-group-${indexA}`}
|
||||
title={compile(item.title)}
|
||||
className={menuItemGroupCss}
|
||||
>
|
||||
{renderMenuItem(item.children)}
|
||||
</Menu.ItemGroup>
|
||||
);
|
||||
const label = compile(item.title);
|
||||
return {
|
||||
type: 'group',
|
||||
key: item.key || `item-group-${indexA}`,
|
||||
label,
|
||||
title: label,
|
||||
className: menuItemGroupCss,
|
||||
children: renderMenuItem(item.children),
|
||||
} as MenuProps['items'][0];
|
||||
}
|
||||
if (item.type === 'subMenu') {
|
||||
return (
|
||||
<Menu.SubMenu
|
||||
// @ts-ignore
|
||||
eventKey={item.key || `sub-menu-${indexA}`}
|
||||
key={item.key || `sub-menu-${indexA}`}
|
||||
title={compile(item.title)}
|
||||
>
|
||||
{renderMenuItem(item.children)}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
const label = compile(item.title);
|
||||
return {
|
||||
key: item.key || `sub-menu-${indexA}`,
|
||||
label,
|
||||
title: label,
|
||||
children: renderMenuItem(item.children),
|
||||
};
|
||||
}
|
||||
return (
|
||||
<Menu.Item
|
||||
eventKey={item.key}
|
||||
key={item.key}
|
||||
onClick={(info) => {
|
||||
const label = compile(item.title);
|
||||
return {
|
||||
key: item.key || `${info.key}-${item.title}-${indexA}`,
|
||||
label,
|
||||
title: label,
|
||||
onClick: (info) => {
|
||||
item?.clearKeywords?.();
|
||||
if (item.onClick) {
|
||||
item.onClick({ ...info, item });
|
||||
} else {
|
||||
onClick({ ...info, item });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{compile(item.title)}
|
||||
</Menu.Item>
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Menu.SubMenu
|
||||
// @ts-ignore
|
||||
eventKey={eventKey ? `${eventKey}-${index}` : info.key}
|
||||
key={info.key}
|
||||
title={compile(children)}
|
||||
icon={typeof icon === 'string' ? <Icon type={icon as string} /> : icon}
|
||||
>
|
||||
{renderMenuItem(items)}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
|
||||
const item = {
|
||||
key: info.key,
|
||||
label: isString(children) ? compile(children) : children,
|
||||
icon: typeof icon === 'string' ? <Icon type={icon as string} /> : icon,
|
||||
children: renderMenuItem(items),
|
||||
};
|
||||
|
||||
collectMenuItem(item);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Menu.Item
|
||||
// {...others}
|
||||
key={info.key}
|
||||
eventKey={eventKey ? `${eventKey}-${index}` : info.key}
|
||||
icon={typeof icon === 'string' ? <Icon type={icon as string} /> : icon}
|
||||
onClick={(opts) => {
|
||||
|
||||
const label = isString(children) ? compile(children) : children;
|
||||
const item = {
|
||||
key: info.key,
|
||||
label,
|
||||
title: label,
|
||||
icon: typeof icon === 'string' ? <Icon type={icon as string} /> : icon,
|
||||
onClick: (opts) => {
|
||||
info?.clearKeywords?.();
|
||||
onClick({ ...opts, item: info });
|
||||
}}
|
||||
>
|
||||
{compile(children)}
|
||||
</Menu.Item>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
collectMenuItem(item);
|
||||
return null;
|
||||
};
|
||||
|
||||
SchemaInitializer.itemWrap = (component?: SchemaInitializerItemComponent) => {
|
||||
@ -253,11 +298,13 @@ interface SchemaInitializerActionModalProps {
|
||||
onSubmit?: (values: any) => void;
|
||||
buttonText?: any;
|
||||
}
|
||||
SchemaInitializer.ActionModal = (props: SchemaInitializerActionModalProps) => {
|
||||
SchemaInitializer.ActionModal = function ActionModal(props: SchemaInitializerActionModalProps) {
|
||||
const { title, schema, buttonText, onCancel, onSubmit } = props;
|
||||
|
||||
const useCancelAction = useCallback(() => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const form = useForm();
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const ctx = useActionContext();
|
||||
return {
|
||||
async run() {
|
||||
@ -269,7 +316,9 @@ SchemaInitializer.ActionModal = (props: SchemaInitializerActionModalProps) => {
|
||||
}, [onCancel]);
|
||||
|
||||
const useSubmitAction = useCallback(() => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const form = useForm();
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const ctx = useActionContext();
|
||||
return {
|
||||
async run() {
|
||||
|
@ -1,31 +1,28 @@
|
||||
import { Divider, Input } from 'antd';
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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 { collections } = useCollectionManager();
|
||||
const [value, setValue] = useState<string>(outValue);
|
||||
|
||||
// 之所以要增加个内部的 value 是为了防止用户输入过快时造成卡顿的问题
|
||||
useEffect(() => {
|
||||
setValue(outValue);
|
||||
}, [outValue]);
|
||||
|
||||
return (
|
||||
<div style={{ width: 210 }}>
|
||||
<Input
|
||||
autoFocus
|
||||
allowClear
|
||||
style={{ padding: '0 4px 6px' }}
|
||||
bordered={false}
|
||||
placeholder={t('Search and select collection')}
|
||||
value={value}
|
||||
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);
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { MenuOutlined } from '@ant-design/icons';
|
||||
import { ISchema, useFieldSchema } from '@formily/react';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SchemaInitializer, SchemaSettings } from '../..';
|
||||
import { useAPIClient } from '../../api-client';
|
||||
import { useCollection } from '../../collection-manager';
|
||||
import { createDesignable, useDesignable } from '../../schema-component';
|
||||
import _ from 'lodash';
|
||||
|
||||
export const Resizable = (props) => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -32,7 +32,7 @@ export const TableActionInitializers = {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
},
|
||||
visible: () => {
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'view' && collection.template !== 'file';
|
||||
},
|
||||
@ -45,7 +45,7 @@ export const TableActionInitializers = {
|
||||
'x-align': 'right',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
},
|
||||
visible: () => {
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return (collection as any).template !== 'view';
|
||||
},
|
||||
@ -65,7 +65,7 @@ export const TableActionInitializers = {
|
||||
schema: {
|
||||
'x-align': 'right',
|
||||
},
|
||||
visible: () => {
|
||||
visible: function useVisible() {
|
||||
const schema = useFieldSchema();
|
||||
const collection = useCollection();
|
||||
const { treeTable } = schema?.parent?.['x-decorator-props'] || {};
|
||||
@ -76,7 +76,7 @@ export const TableActionInitializers = {
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
visible: () => {
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return (collection as any).template !== 'view';
|
||||
},
|
||||
@ -157,7 +157,7 @@ export const TableActionInitializers = {
|
||||
},
|
||||
},
|
||||
],
|
||||
visible: () => {
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return (collection as any).template !== 'view';
|
||||
},
|
||||
|
@ -8,12 +8,13 @@ import {
|
||||
useInheritsTableColumnInitializerFields,
|
||||
} from '../utils';
|
||||
import { useCompile } from '../../schema-component';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
|
||||
// 表格列配置
|
||||
export const TableColumnInitializers = (props: any) => {
|
||||
const { items = [], action = true } = props;
|
||||
const { t } = useTranslation();
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const associatedFields = useAssociatedTableColumnInitializerFields();
|
||||
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(
|
||||
{
|
||||
type: 'divider',
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { DownOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { RecursionField, observer, useField, useFieldSchema } from '@formily/react';
|
||||
import { Button, Dropdown, Menu } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Dropdown, MenuProps } from 'antd';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useDesignable } from '../../';
|
||||
import { useACLRolesCheck, useRecordPkValue } from '../../acl/ACLProvider';
|
||||
import { CollectionProvider, useCollection, useCollectionManager } from '../../collection-manager';
|
||||
@ -131,7 +131,8 @@ export const CreateAction = observer(
|
||||
const componentType = field.componentProps.type || 'primary';
|
||||
const { getChildrenCollections } = useCollectionManager();
|
||||
const totalChildCollections = getChildrenCollections(collection.name);
|
||||
const inheritsCollections = enableChildren
|
||||
const inheritsCollections = useMemo(() => {
|
||||
return enableChildren
|
||||
.map((k) => {
|
||||
if (!k) {
|
||||
return;
|
||||
@ -148,27 +149,26 @@ export const CreateAction = observer(
|
||||
.filter((v) => {
|
||||
return v && actionAclCheck(`${v.name}:create`);
|
||||
});
|
||||
}, [enableChildren, totalChildCollections]);
|
||||
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
|
||||
const values = useRecord();
|
||||
const compile = useCompile();
|
||||
const { designable } = useDesignable();
|
||||
const icon = props.icon || <PlusOutlined />;
|
||||
const menu = (
|
||||
<Menu>
|
||||
{inheritsCollections.map((option) => {
|
||||
return (
|
||||
<Menu.Item
|
||||
key={option.name}
|
||||
onClick={(info) => {
|
||||
onClick?.(option.name);
|
||||
}}
|
||||
>
|
||||
{compile(option.title)}
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
);
|
||||
const menuItems = useMemo<MenuProps['items']>(() => {
|
||||
return inheritsCollections.map((option) => ({
|
||||
key: option.name,
|
||||
label: compile(option.title),
|
||||
onClick: () => onClick?.(option.name),
|
||||
}));
|
||||
}, [inheritsCollections, onClick]);
|
||||
|
||||
const menu = useMemo<MenuProps>(() => {
|
||||
return {
|
||||
items: menuItems,
|
||||
};
|
||||
}, [menuItems]);
|
||||
|
||||
useEffect(() => {
|
||||
field.linkageProperty = {};
|
||||
linkageRules
|
||||
@ -190,7 +190,7 @@ export const CreateAction = observer(
|
||||
leftButton,
|
||||
React.cloneElement(rightButton as React.ReactElement<any, string>, { loading: false }),
|
||||
]}
|
||||
overlay={menu}
|
||||
menu={menu}
|
||||
onClick={(info) => {
|
||||
onClick?.(collection.name);
|
||||
}}
|
||||
@ -199,7 +199,7 @@ export const CreateAction = observer(
|
||||
{props.children}
|
||||
</Dropdown.Button>
|
||||
) : (
|
||||
<Dropdown overlay={menu}>
|
||||
<Dropdown menu={menu}>
|
||||
{
|
||||
<Button icon={icon} type={componentType}>
|
||||
{props.children} <DownOutlined />
|
||||
|
@ -89,6 +89,7 @@ export enum AssignedFieldValueType {
|
||||
export const AssignedField = (props: any) => {
|
||||
const { t } = useTranslation();
|
||||
const compile = useCompile();
|
||||
const collection = useCollection();
|
||||
const field = useField<Field>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const isDynamicValue =
|
||||
@ -110,7 +111,7 @@ export const AssignedField = (props: any) => {
|
||||
const [options, setOptions] = useState<any[]>([]);
|
||||
const { getField } = useCollection();
|
||||
const collectionField = getField(fieldSchema.name);
|
||||
const fields = useCollectionFilterOptions(collectionField?.collectionName);
|
||||
const fields = useCollectionFilterOptions(collection?.name);
|
||||
const userFields = useCollectionFilterOptions('users');
|
||||
const dateTimeFields = ['createdAt', 'datetime', 'time', 'updatedAt'];
|
||||
useEffect(() => {
|
||||
|
@ -1,14 +1,13 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { FormDialog, FormLayout } from '@formily/antd';
|
||||
import { FormOutlined } from '@ant-design/icons';
|
||||
import { FormDialog, FormLayout } from '@formily/antd';
|
||||
import { SchemaOptionsContext } from '@formily/react';
|
||||
import React, { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useCollection, useCollectionManager } from '../../collection-manager';
|
||||
import { SchemaComponent, SchemaComponentOptions, useCompile } from '../../schema-component';
|
||||
import { useCollectionManager } from '../../collection-manager';
|
||||
import { SchemaComponent, SchemaComponentOptions } from '../../schema-component';
|
||||
import { createCalendarBlockSchema } from '../utils';
|
||||
import { DataBlockInitializer } from './DataBlockInitializer';
|
||||
import { CascaderProps } from 'antd';
|
||||
|
||||
export const CalendarBlockInitializer = (props) => {
|
||||
const { insert } = props;
|
||||
|
@ -3,8 +3,6 @@ import React from 'react';
|
||||
|
||||
import { SchemaInitializer } from '..';
|
||||
import { useCurrentSchema } from '../utils';
|
||||
import { useBlockRequestContext } from '../../block-provider';
|
||||
import { useCollection } from '../../collection-manager';
|
||||
|
||||
export const InitializerWithSwitch = (props) => {
|
||||
const { type, schema, item, insert, remove: passInRemove } = props;
|
||||
@ -14,6 +12,7 @@ export const InitializerWithSwitch = (props) => {
|
||||
item.find,
|
||||
passInRemove ?? item.remove,
|
||||
);
|
||||
|
||||
return (
|
||||
<SchemaInitializer.SwitchItem
|
||||
checked={exists}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
|
||||
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 { BlockRequestContext, SchemaInitializerItemOptions } from '../';
|
||||
import { BlockRequestContext, SchemaInitializerButtonContext, SchemaInitializerItemOptions } from '../';
|
||||
import { FieldOptions, useCollection, useCollectionManager } from '../collection-manager';
|
||||
import { isAssocField } from '../filter-provider/utils';
|
||||
import { useActionContext, useDesignable } from '../schema-component';
|
||||
@ -91,6 +93,7 @@ export const useTableColumnInitializerFields = () => {
|
||||
const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable';
|
||||
const form = useForm();
|
||||
const isReadPretty = isSubTable ? form.readPretty : true;
|
||||
|
||||
return currentFields
|
||||
.filter(
|
||||
(field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey && !field?.treeChildren,
|
||||
@ -201,6 +204,10 @@ export const useAssociatedTableColumnInitializerFields = () => {
|
||||
export const useInheritsTableColumnInitializerFields = () => {
|
||||
const { name } = useCollection();
|
||||
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);
|
||||
return inherits?.map((v) => {
|
||||
const fields = getParentCollectionFields(v, name);
|
||||
@ -212,12 +219,30 @@ export const useInheritsTableColumnInitializerFields = () => {
|
||||
})
|
||||
.map((k) => {
|
||||
const interfaceConfig = getInterface(k.interface);
|
||||
const isFileCollection = k?.target && getCollection(k?.target)?.template === 'file';
|
||||
const schema = {
|
||||
name: `${k.name}`,
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
'x-read-pretty': isReadPretty || k.uiSchema?.['x-read-pretty'],
|
||||
'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 {
|
||||
type: 'item',
|
||||
@ -807,24 +832,28 @@ export const useCollectionDataSourceItems = (componentName) => {
|
||||
const { t } = useTranslation();
|
||||
const { collections, getCollectionFields } = useCollectionManager();
|
||||
const { getTemplatesByCollection } = useSchemaTemplateManager();
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [value, onChange] = useState(null);
|
||||
const { searchValue, setSearchValue } = useContext(SchemaInitializerButtonContext);
|
||||
// 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 = () => {
|
||||
setSelected([]);
|
||||
onChange(null);
|
||||
setSearchValue('');
|
||||
};
|
||||
return [
|
||||
{
|
||||
key: 'tableBlock',
|
||||
type: 'itemGroup',
|
||||
title: React.createElement(SelectCollection, {
|
||||
value,
|
||||
value: searchValue,
|
||||
onChange,
|
||||
setSelected,
|
||||
}),
|
||||
children: collections
|
||||
?.filter((item) => {
|
||||
const b = !value || selected.includes(item.name);
|
||||
if (item.inherit) {
|
||||
return false;
|
||||
}
|
||||
@ -836,7 +865,12 @@ export const useCollectionDataSourceItems = (componentName) => {
|
||||
} else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) {
|
||||
return false;
|
||||
} 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) => {
|
||||
|
@ -11,8 +11,8 @@ import {
|
||||
CascaderProps,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Menu,
|
||||
MenuItemProps,
|
||||
MenuProps,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
@ -46,6 +46,7 @@ import {
|
||||
} from '..';
|
||||
import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks';
|
||||
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 { useSchemaTemplateManager } from '../schema-templates';
|
||||
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
|
||||
@ -53,7 +54,6 @@ import { FormDataTemplates } from './DataTemplates';
|
||||
import { EnableChildCollections } from './EnableChildCollections';
|
||||
import { FormLinkageRules } from './LinkageRules';
|
||||
import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks';
|
||||
import { MenuDividerProps } from 'antd/lib/menu';
|
||||
|
||||
interface SchemaSettingsProps {
|
||||
title?: any;
|
||||
@ -117,17 +117,7 @@ export const SchemaSettingsProvider: React.FC<SchemaSettingsProviderProps> = (pr
|
||||
);
|
||||
};
|
||||
|
||||
export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNested = (props) => {
|
||||
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(
|
||||
const overlayClassName = classNames(
|
||||
'nb-schema-initializer-button-overlay',
|
||||
css`
|
||||
.ant-dropdown-menu-item-group-list {
|
||||
@ -135,19 +125,52 @@ export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNeste
|
||||
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}
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
|
||||
if (dn) {
|
||||
return (
|
||||
<SchemaSettingsProvider visible={visible} setVisible={setVisible} dn={dn} {...others}>
|
||||
{DropdownMenu}
|
||||
{dropdownMenu()}
|
||||
</SchemaSettingsProvider>
|
||||
);
|
||||
}
|
||||
return DropdownMenu;
|
||||
return dropdownMenu();
|
||||
};
|
||||
|
||||
SchemaSettings.Template = function Template(props) {
|
||||
@ -388,35 +411,70 @@ SchemaSettings.FormItemTemplate = function FormItemTemplate(props) {
|
||||
};
|
||||
|
||||
SchemaSettings.Item = function Item(props) {
|
||||
const { pushMenuItem } = useCollectMenuItems();
|
||||
const { collectMenuItem } = useCollectMenuItem();
|
||||
const { eventKey } = props;
|
||||
const key = useMemo(() => uid(), []);
|
||||
return (
|
||||
<Menu.Item
|
||||
key={key}
|
||||
eventKey={(eventKey as any) || key}
|
||||
{...props}
|
||||
onClick={(info) => {
|
||||
const item = {
|
||||
..._.omit(props, ['children']),
|
||||
key,
|
||||
eventKey: (eventKey as any) || key,
|
||||
onClick: (info) => {
|
||||
info.domEvent.preventDefault();
|
||||
info.domEvent.stopPropagation();
|
||||
props?.onClick?.(info);
|
||||
}}
|
||||
style={{ minWidth: 120 }}
|
||||
>
|
||||
{props.children || props.title}
|
||||
</Menu.Item>
|
||||
);
|
||||
},
|
||||
style: { minWidth: 120 },
|
||||
label: props.children || props.title,
|
||||
title: props.title,
|
||||
} as MenuProps['items'][0];
|
||||
|
||||
pushMenuItem?.(item);
|
||||
collectMenuItem?.(item);
|
||||
return null;
|
||||
};
|
||||
|
||||
SchemaSettings.ItemGroup = (props) => {
|
||||
return <Menu.ItemGroup {...props} />;
|
||||
SchemaSettings.ItemGroup = function 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) => {
|
||||
return <Menu.SubMenu {...props} />;
|
||||
SchemaSettings.SubMenu = function 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) => {
|
||||
return <Menu.Divider {...props} />;
|
||||
SchemaSettings.Divider = function Divider() {
|
||||
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) {
|
||||
@ -470,6 +528,7 @@ SchemaSettings.ConnectDataBlocks = function ConnectDataBlocks(props: {
|
||||
const collection = useCollection();
|
||||
const { inProvider } = useFilterBlock();
|
||||
const dataBlocks = useSupportedBlocks(type);
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { targets = [], uid } = findFilterTargets(fieldSchema);
|
||||
const compile = useCompile();
|
||||
|
||||
@ -578,11 +637,13 @@ SchemaSettings.ConnectDataBlocks = function ConnectDataBlocks(props: {
|
||||
{Content.length ? (
|
||||
Content
|
||||
) : (
|
||||
<SchemaSettings.Item>
|
||||
<Empty
|
||||
style={{ width: 160, padding: '0 1em' }}
|
||||
description={emptyDescription}
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
</SchemaSettings.Item>
|
||||
)}
|
||||
</SchemaSettings.SubMenu>
|
||||
);
|
||||
@ -755,7 +816,7 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => {
|
||||
title={compile(title)}
|
||||
{...others}
|
||||
destroyOnClose
|
||||
visible={visible}
|
||||
open={visible}
|
||||
onCancel={cancelHandler}
|
||||
footer={
|
||||
<Space>
|
||||
@ -1105,7 +1166,6 @@ SchemaSettings.EnableChildCollections = function EnableChildCollectionsItem(prop
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Enable child collections')}
|
||||
components={{ ArrayItems, FormLayout }}
|
||||
width={600}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
|
@ -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;
|
||||
};
|
@ -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]);
|
||||
};
|
@ -2,13 +2,20 @@ import { useMemo } from 'react';
|
||||
import { useValues } from '../../../schema-component/antd/filter/useValues';
|
||||
import { useDateVariable } from './useDateVariable';
|
||||
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 userVariable = useUserVariable({ maxDepth: 3, 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 [];
|
||||
|
||||
|
@ -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 { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useAPIClient, useRequest, useSchemaTemplateManager } from '..';
|
||||
@ -75,6 +76,7 @@ export const BlockTemplateDetails = () => {
|
||||
return (
|
||||
<div>
|
||||
<AntdPageHeader
|
||||
style={{ backgroundColor: 'white' }}
|
||||
onBack={() => {
|
||||
navigate('/admin/plugins/block-templates');
|
||||
}}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PageHeader as AntdPageHeader } from 'antd';
|
||||
import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CollectionManagerProvider } from '../collection-manager';
|
||||
@ -10,7 +10,7 @@ export const BlockTemplatePage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<AntdPageHeader ghost={false} title={t('Block templates')} />
|
||||
<AntdPageHeader style={{ backgroundColor: 'white' }} ghost={false} title={t('Block templates')} />
|
||||
<div style={{ margin: 'var(--nb-spacing)' }}>
|
||||
<CollectionManagerProvider collections={[uiSchemaTemplatesCollection]}>
|
||||
<SchemaComponent schema={uiSchemaTemplatesSchema} />
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
---
|
||||
group:
|
||||
title: Client
|
||||
order: 1
|
||||
---
|
||||
|
||||
# SettingsForm
|
||||
|
||||
<code src="./demos/demo1.tsx"></code>
|
@ -1 +0,0 @@
|
||||
export * from './SettingsForm';
|
@ -1,11 +1,10 @@
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Menu } from 'antd';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { MenuProps } from 'antd';
|
||||
import React, { useContext, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActionContextProvider, SchemaComponent, useActionContext } from '../';
|
||||
import { ActionContextProvider, DropdownVisibleContext, SchemaComponent, useActionContext } from '../';
|
||||
import { useAPIClient } from '../api-client';
|
||||
import { DropdownVisibleContext } from './CurrentUser';
|
||||
|
||||
const useCloseAction = () => {
|
||||
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 { t } = useTranslation();
|
||||
const ctx = useContext(DropdownVisibleContext);
|
||||
return (
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Menu.Item
|
||||
key="password"
|
||||
eventKey={'ChangePassword'}
|
||||
onClick={() => {
|
||||
ctx?.setVisible?.(false);
|
||||
|
||||
return useMemo<MenuProps['items'][0]>(() => {
|
||||
return {
|
||||
key: 'password',
|
||||
eventKey: 'ChangePassword',
|
||||
onClick: () => {
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
ctx?.setVisible(false);
|
||||
},
|
||||
label: (
|
||||
<>
|
||||
{t('Change password')}
|
||||
</Menu.Item>
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<SchemaComponent scope={{ useCloseAction, useSaveCurrentUserValues }} schema={schema} />
|
||||
</div>
|
||||
</ActionContextProvider>
|
||||
);
|
||||
</>
|
||||
),
|
||||
};
|
||||
}, [visible]);
|
||||
};
|
||||
|
@ -1,22 +1,25 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Dropdown, Menu, Modal } from 'antd';
|
||||
import React, { createContext, useState } from 'react';
|
||||
import { error } from '@nocobase/utils/client';
|
||||
import { Dropdown, Menu, MenuProps, Modal } from 'antd';
|
||||
import React, { createContext, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useACLRoleContext, useAPIClient, useCurrentUserContext } from '..';
|
||||
import { useCurrentAppInfo } from '../appInfo/CurrentAppInfoProvider';
|
||||
import { ChangePassword } from './ChangePassword';
|
||||
import { EditProfile } from './EditProfile';
|
||||
import { LanguageSettings } from './LanguageSettings';
|
||||
import { SwitchRole } from './SwitchRole';
|
||||
import { ThemeSettings } from './ThemeSettings';
|
||||
const ApplicationVersion = () => {
|
||||
import { useChangePassword } from './ChangePassword';
|
||||
import { useEditProfile } from './EditProfile';
|
||||
import { useLanguageSettings } from './LanguageSettings';
|
||||
import { useSwitchRole } from './SwitchRole';
|
||||
import { useThemeSettings } from './ThemeSettings';
|
||||
const useApplicationVersion = () => {
|
||||
const data = useCurrentAppInfo();
|
||||
return (
|
||||
<Menu.Item key="version" disabled>
|
||||
Version {data?.data?.version}
|
||||
</Menu.Item>
|
||||
);
|
||||
return useMemo(() => {
|
||||
return {
|
||||
key: 'version',
|
||||
disabled: true,
|
||||
label: `Version ${data?.data?.version}`,
|
||||
};
|
||||
}, [data?.data?.version]);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -32,7 +35,7 @@ export const SettingsMenu: React.FC<{
|
||||
const api = useAPIClient();
|
||||
const { t } = useTranslation();
|
||||
const silenceApi = useAPIClient();
|
||||
const check = async () => {
|
||||
const check = useCallback(async () => {
|
||||
return await new Promise((resolve) => {
|
||||
const heartbeat = setInterval(() => {
|
||||
silenceApi
|
||||
@ -47,42 +50,44 @@ export const SettingsMenu: React.FC<{
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.catch(() => {
|
||||
// ignore
|
||||
.catch((err) => {
|
||||
error(err);
|
||||
});
|
||||
}, 3000);
|
||||
});
|
||||
}, [silenceApi]);
|
||||
const divider = useMemo<MenuProps['items'][0]>(() => {
|
||||
return {
|
||||
type: 'divider',
|
||||
};
|
||||
return (
|
||||
<Menu>
|
||||
<ApplicationVersion />
|
||||
<Menu.Divider />
|
||||
<EditProfile />
|
||||
<ChangePassword />
|
||||
<Menu.Divider />
|
||||
<SwitchRole />
|
||||
<LanguageSettings />
|
||||
<ThemeSettings />
|
||||
<Menu.Divider />
|
||||
{appAllowed && (
|
||||
<>
|
||||
<Menu.Item
|
||||
key="cache"
|
||||
onClick={async () => {
|
||||
}, []);
|
||||
const appVersion = useApplicationVersion();
|
||||
const editProfile = useEditProfile();
|
||||
const changePassword = useChangePassword();
|
||||
const switchRole = useSwitchRole();
|
||||
const languageSettings = useLanguageSettings();
|
||||
const themeSettings = useThemeSettings();
|
||||
const controlApp = useMemo<MenuProps['items']>(() => {
|
||||
if (!appAllowed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'cache',
|
||||
label: t('Clear cache'),
|
||||
onClick: async () => {
|
||||
await api.resource('app').clearCache();
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
{t('Clear cache')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="reboot"
|
||||
onClick={async () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'reboot',
|
||||
label: t('Reboot application'),
|
||||
onClick: async () => {
|
||||
Modal.confirm({
|
||||
title: t('Reboot application'),
|
||||
content: t(
|
||||
'The will interrupt service, it may take a few seconds to restart. Are you sure to continue?',
|
||||
),
|
||||
content: t('The will interrupt service, it may take a few seconds to restart. Are you sure to continue?'),
|
||||
okText: t('Reboot'),
|
||||
okButtonProps: {
|
||||
danger: true,
|
||||
@ -93,29 +98,52 @@ export const SettingsMenu: React.FC<{
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Reboot application')}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
)}
|
||||
<Menu.Item
|
||||
key="signout"
|
||||
onClick={async () => {
|
||||
},
|
||||
},
|
||||
divider,
|
||||
];
|
||||
}, [appAllowed, check]);
|
||||
const items = useMemo<MenuProps['items']>(() => {
|
||||
return [
|
||||
appVersion,
|
||||
divider,
|
||||
editProfile,
|
||||
changePassword,
|
||||
divider,
|
||||
switchRole,
|
||||
languageSettings,
|
||||
themeSettings,
|
||||
divider,
|
||||
...controlApp,
|
||||
{
|
||||
key: 'signout',
|
||||
label: t('Sign out'),
|
||||
onClick: async () => {
|
||||
await api.auth.signOut();
|
||||
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 CurrentUser = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { data } = useCurrentUserContext();
|
||||
|
||||
return (
|
||||
<div style={{ display: 'inline-flex', verticalAlign: 'top' }}>
|
||||
<DropdownVisibleContext.Provider value={{ visible, setVisible }}>
|
||||
@ -124,7 +152,9 @@ export const CurrentUser = () => {
|
||||
onOpenChange={(visible) => {
|
||||
setVisible(visible);
|
||||
}}
|
||||
overlay={<SettingsMenu />}
|
||||
dropdownRender={() => {
|
||||
return <SettingsMenu />;
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={css`
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Menu } from 'antd';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { MenuProps } from 'antd';
|
||||
import React, { useContext, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActionContextProvider,
|
||||
@ -114,23 +114,32 @@ const schema: ISchema = {
|
||||
},
|
||||
};
|
||||
|
||||
export const EditProfile = () => {
|
||||
export const useEditProfile = () => {
|
||||
const ctx = useContext(DropdownVisibleContext);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const ctx = useContext(DropdownVisibleContext);
|
||||
return (
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Menu.Item
|
||||
key="profile"
|
||||
eventKey={'EditProfile'}
|
||||
onClick={() => {
|
||||
|
||||
return useMemo<MenuProps['items'][0]>(() => {
|
||||
return {
|
||||
key: 'profile',
|
||||
eventKey: 'EditProfile',
|
||||
onClick: () => {
|
||||
setVisible(true);
|
||||
ctx?.setVisible(false);
|
||||
}}
|
||||
>
|
||||
},
|
||||
label: (
|
||||
<>
|
||||
{t('Edit profile')}
|
||||
</Menu.Item>
|
||||
<SchemaComponent scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }} schema={schema} />
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<SchemaComponent
|
||||
scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }}
|
||||
schema={schema}
|
||||
/>
|
||||
</div>
|
||||
</ActionContextProvider>
|
||||
);
|
||||
</>
|
||||
),
|
||||
};
|
||||
}, [visible]);
|
||||
};
|
||||
|
@ -1,35 +1,29 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Menu, Select } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { MenuProps, Select } from 'antd';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAPIClient, useCurrentUserContext, useSystemSettings } from '..';
|
||||
import { useAPIClient, useSystemSettings } from '..';
|
||||
import locale from '../locale';
|
||||
|
||||
export const LanguageSettings = () => {
|
||||
export const useLanguageSettings = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const api = useAPIClient();
|
||||
const ctx = useCurrentUserContext();
|
||||
const { data } = useSystemSettings();
|
||||
const enabledLanguages: string[] = data?.data?.enabledLanguages || [];
|
||||
if (enabledLanguages.length < 2) {
|
||||
return null;
|
||||
}
|
||||
// console.log('data', data?.data?.enabledLanguages);
|
||||
return (
|
||||
<Menu.Item
|
||||
key="language"
|
||||
eventKey={'LanguageSettings'}
|
||||
onClick={() => {
|
||||
const result = useMemo<MenuProps['items'][0]>(() => {
|
||||
return {
|
||||
key: 'language',
|
||||
eventKey: 'LanguageSettings',
|
||||
onClick: () => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
},
|
||||
label: (
|
||||
<div
|
||||
className={css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{t('Language')}{' '}
|
||||
<Select
|
||||
@ -60,6 +54,13 @@ export const LanguageSettings = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
);
|
||||
),
|
||||
};
|
||||
}, [enabledLanguages, i18n, open]);
|
||||
|
||||
if (enabledLanguages.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import { Space, Tabs } from 'antd';
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { SchemaComponent, useAPIClient, useCurrentDocumentTitle, useSystemSettings } from '..';
|
||||
@ -142,11 +142,18 @@ export const SigninPage = (props: SigninPageProps) => {
|
||||
`}
|
||||
>
|
||||
{smsAuthEnabled ? (
|
||||
<Tabs defaultActiveKey="password">
|
||||
<Tabs.TabPane tab={t('Sign in via account')} key="password">
|
||||
<SchemaComponent scope={{ usePasswordSignIn }} schema={schema || passwordForm} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={t('Sign in via phone')} key="phone">
|
||||
<Tabs
|
||||
defaultActiveKey="password"
|
||||
items={[
|
||||
{
|
||||
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
|
||||
schema={phoneForm}
|
||||
scope={{ usePhoneSignIn, ...scope }}
|
||||
@ -155,8 +162,10 @@ export const SigninPage = (props: SigninPageProps) => {
|
||||
...components,
|
||||
}}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<SchemaComponent
|
||||
components={{ ...components }}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Menu, Select } from 'antd';
|
||||
import React from 'react';
|
||||
import { MenuProps, Select } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useACLRoleContext } from '../acl';
|
||||
import { useAPIClient } from '../api-client';
|
||||
@ -26,21 +25,21 @@ const useCurrentRoles = () => {
|
||||
return compile(options);
|
||||
};
|
||||
|
||||
export const SwitchRole = () => {
|
||||
export const useSwitchRole = () => {
|
||||
const api = useAPIClient();
|
||||
const roles = useCurrentRoles();
|
||||
const { t } = useTranslation();
|
||||
if (roles.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Menu.Item key="role" eventKey={'SwitchRole'}>
|
||||
const result = useMemo<MenuProps['items'][0]>(() => {
|
||||
return {
|
||||
key: 'role',
|
||||
eventKey: 'SwitchRole',
|
||||
label: (
|
||||
<div
|
||||
className={css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{t('Switch role')}{' '}
|
||||
<Select
|
||||
@ -60,6 +59,13 @@ export const SwitchRole = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
);
|
||||
),
|
||||
};
|
||||
}, [api, history, roles]);
|
||||
|
||||
if (roles.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
@ -1,22 +1,25 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Menu, Select } from 'antd';
|
||||
import React from 'react';
|
||||
import { MenuProps, Select } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAPIClient } from '../api-client';
|
||||
import { useCurrentUserContext } from './CurrentUserProvider';
|
||||
|
||||
export const ThemeSettings = () => {
|
||||
export const useThemeSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const ctx = useCurrentUserContext();
|
||||
const api = useAPIClient();
|
||||
return (
|
||||
<Menu.Item key="theme" eventKey={'theme'}>
|
||||
|
||||
return useMemo<MenuProps['items'][0]>(() => {
|
||||
return {
|
||||
key: 'theme',
|
||||
eventKey: 'theme',
|
||||
label: (
|
||||
<div
|
||||
className={css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{t('Theme')}{' '}
|
||||
<Select
|
||||
@ -42,6 +45,7 @@ export const ThemeSettings = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
);
|
||||
),
|
||||
};
|
||||
}, [ctx.data.data.id, ctx.data.data.systemSettings]);
|
||||
};
|
||||
|
@ -4,7 +4,7 @@
|
||||
"main": "src/index.js",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@umijs/utils": "^3.5.20",
|
||||
"@umijs/utils": "3.5.20",
|
||||
"axios": "^0.26.1",
|
||||
"chalk": "^4.1.1",
|
||||
"commander": "^9.2.0",
|
||||
|
@ -398,6 +398,7 @@ describe('repository.update', () => {
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
{ type: 'belongsTo', name: 'user' },
|
||||
],
|
||||
});
|
||||
Comment = db.collection({
|
||||
@ -411,7 +412,7 @@ describe('repository.update', () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('update1', async () => {
|
||||
it('update with filterByTk and with associations', async () => {
|
||||
const user = await User.model.create<any>({
|
||||
name: 'user1',
|
||||
});
|
||||
@ -454,7 +455,7 @@ describe('repository.update', () => {
|
||||
expect(updated2.posts.length).toBe(2);
|
||||
});
|
||||
|
||||
it('update2', async () => {
|
||||
it('update with filterByTk', async () => {
|
||||
const user = await User.model.create<any>({
|
||||
name: 'user1',
|
||||
});
|
||||
@ -463,6 +464,9 @@ describe('repository.update', () => {
|
||||
name: 'user2',
|
||||
});
|
||||
|
||||
const hook = jest.fn();
|
||||
db.on('users.afterUpdate', hook);
|
||||
|
||||
await User.repository.update({
|
||||
filterByTk: user.id,
|
||||
values: {
|
||||
@ -470,6 +474,8 @@ describe('repository.update', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(hook).toBeCalledTimes(1);
|
||||
|
||||
const updated = await User.model.findByPk(user.id);
|
||||
|
||||
expect(updated.get('name')).toEqual('user11');
|
||||
@ -477,6 +483,119 @@ describe('repository.update', () => {
|
||||
const u2 = await User.model.findByPk(user2.id);
|
||||
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', () => {
|
||||
|
@ -370,6 +370,7 @@ describe('One2One Association', () => {
|
||||
uid: 1,
|
||||
name: '123',
|
||||
},
|
||||
userId: 1,
|
||||
};
|
||||
|
||||
const guard = new UpdateGuard();
|
||||
@ -381,6 +382,7 @@ describe('One2One Association', () => {
|
||||
user: {
|
||||
uid: 1,
|
||||
},
|
||||
userId: 1,
|
||||
});
|
||||
|
||||
guard.setAssociationKeysToBeUpdate(['user']);
|
||||
|
@ -609,6 +609,45 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
|
||||
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({
|
||||
...queryOptions,
|
||||
transaction,
|
||||
|
@ -93,6 +93,8 @@ export class UpdateGuard {
|
||||
Object.keys(associationsValues).forEach((association) => {
|
||||
let associationValues = associationsValues[association];
|
||||
|
||||
const associationObj = associations[association];
|
||||
|
||||
const filterAssociationToBeUpdate = (value) => {
|
||||
if (value === null) {
|
||||
return value;
|
||||
@ -104,8 +106,6 @@ export class UpdateGuard {
|
||||
return value;
|
||||
}
|
||||
|
||||
const associationObj = associations[association];
|
||||
|
||||
const associationKeyName =
|
||||
associationObj.associationType == 'BelongsTo' || associationObj.associationType == 'HasOne'
|
||||
? (<any>associationObj).targetKey
|
||||
@ -143,6 +143,16 @@ export class UpdateGuard {
|
||||
|
||||
// set association values to sanitized value
|
||||
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) {
|
||||
|
@ -8,7 +8,8 @@ import {
|
||||
SchemaInitializerButtonContext,
|
||||
useAPIClient,
|
||||
} 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 { lang } from './locale';
|
||||
import { getQueryTypeSchema } from './settings/queryTypes';
|
||||
@ -21,20 +22,13 @@ export interface ChartQueryMetadata {
|
||||
}
|
||||
|
||||
export const ChartQueryBlockInitializer = (props) => {
|
||||
const defaultItems: any = [
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: lang('Select query data'),
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
const { templateWrap, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props;
|
||||
const { setVisible } = useContext(SchemaInitializerButtonContext);
|
||||
const [items, setItems] = useState(defaultItems);
|
||||
const apiClient = useAPIClient();
|
||||
const ctx = useChartQueryMetadataContext();
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const onAddQuery = (info) => {
|
||||
const onAddQuery = useCallback(
|
||||
(info) => {
|
||||
FormDialog(
|
||||
{
|
||||
sql: lang('Add SQL query'),
|
||||
@ -72,33 +66,52 @@ export const ChartQueryBlockInitializer = (props) => {
|
||||
})
|
||||
.then(async (values) => {
|
||||
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 item = items.find((item) => item.id === data?.data?.id);
|
||||
onCreateBlockSchema({ item });
|
||||
}
|
||||
setVisible(false);
|
||||
} catch (error) {}
|
||||
} catch (err) {
|
||||
error(err);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
useEffect(() => {
|
||||
.catch((err) => {
|
||||
error(err);
|
||||
});
|
||||
},
|
||||
[apiClient, ctx, onCreateBlockSchema, options.components, options.scope, setVisible],
|
||||
);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const defaultItems: any = [
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: lang('Select query data'),
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
const chartQueryMetadata = ctx.data;
|
||||
if (chartQueryMetadata && Array.isArray(chartQueryMetadata)) {
|
||||
setItems(
|
||||
[
|
||||
const item1 =
|
||||
chartQueryMetadata.length > 0
|
||||
? {
|
||||
type: 'itemGroup',
|
||||
title: '{{t("Select chart query", {ns: "charts"})}}',
|
||||
children: chartQueryMetadata,
|
||||
}
|
||||
: null,
|
||||
: null;
|
||||
const item2 =
|
||||
chartQueryMetadata.length > 0
|
||||
? {
|
||||
type: 'divider',
|
||||
}
|
||||
: null,
|
||||
,
|
||||
: null;
|
||||
|
||||
return [
|
||||
item1,
|
||||
item2,
|
||||
{
|
||||
type: 'subMenu',
|
||||
title: lang('Add chart query'),
|
||||
@ -118,10 +131,12 @@ export const ChartQueryBlockInitializer = (props) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
].filter(Boolean),
|
||||
);
|
||||
].filter(Boolean);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return defaultItems;
|
||||
}, [ctx.data, onAddQuery]);
|
||||
|
||||
return (
|
||||
<SchemaInitializer.Item
|
||||
icon={<TableOutlined />}
|
||||
|
@ -10,11 +10,11 @@ import {
|
||||
useResourceActionContext,
|
||||
useResourceContext,
|
||||
} from '@nocobase/client';
|
||||
import { Button, Dropdown, Menu } from 'antd';
|
||||
import { Button, Dropdown, MenuProps } from 'antd';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useChartQueryMetadataContext } from '../ChartQueryMetadataProvider';
|
||||
import { getQueryTypeSchema } from './queryTypes';
|
||||
import { lang } from '../locale';
|
||||
import { getQueryTypeSchema } from './queryTypes';
|
||||
|
||||
const useCreateAction = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
@ -112,25 +112,40 @@ export const AddNewQuery = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [schema, setSchema] = useState({});
|
||||
const form = useMemo(() => createForm(), []);
|
||||
const menu = (
|
||||
<Menu
|
||||
onClick={(info) => {
|
||||
|
||||
const menu = useMemo<MenuProps>(() => {
|
||||
return {
|
||||
onClick: (info) => {
|
||||
setVisible(true);
|
||||
form.setValues({ type: info.key });
|
||||
setSchema(getSchema({ type: info.key }, { form, isNewRecord: true }));
|
||||
}}
|
||||
>
|
||||
<Menu.Item key={'json'}>JSON</Menu.Item>
|
||||
<Menu.Item key={'sql'}>SQL</Menu.Item>
|
||||
<Menu.Item disabled key={'api'}>
|
||||
API
|
||||
</Menu.Item>
|
||||
<Menu.Item disabled>Collection</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
},
|
||||
items: [
|
||||
{
|
||||
key: 'json',
|
||||
label: 'JSON',
|
||||
},
|
||||
{
|
||||
key: 'sql',
|
||||
label: 'SQL',
|
||||
},
|
||||
{
|
||||
key: 'api',
|
||||
label: 'API',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'collection',
|
||||
label: 'Collection',
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Dropdown overlay={menu}>
|
||||
<Dropdown menu={menu}>
|
||||
<Button icon={<PlusOutlined />} type={'primary'}>
|
||||
{lang('Add query')} <DownOutlined />
|
||||
</Button>
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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() {
|
||||
this.db.addMigrations({
|
||||
namespace: 'client',
|
||||
directory: resolve(__dirname, './migrations'),
|
||||
context: {
|
||||
plugin: this,
|
||||
},
|
||||
});
|
||||
this.app.acl.allow('app', 'getLang');
|
||||
this.app.acl.allow('app', 'getInfo');
|
||||
this.app.acl.allow('app', 'getPlugins');
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@formily/json-schema": "2.2.24",
|
||||
"@formily/json-schema": "2.2.26",
|
||||
"@nocobase/server": "0.10.0-alpha.2"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -12,19 +12,19 @@ import { css, cx } from '@emotion/css';
|
||||
import { SchemaOptionsContext } from '@formily/react';
|
||||
import {
|
||||
APIClientProvider,
|
||||
collection,
|
||||
CollectionCategroriesContext,
|
||||
CollectionCategroriesProvider,
|
||||
CollectionManagerContext,
|
||||
CollectionManagerProvider,
|
||||
CurrentAppInfoContext,
|
||||
SchemaComponent,
|
||||
SchemaComponentOptions,
|
||||
Select,
|
||||
collection,
|
||||
useAPIClient,
|
||||
useCollectionManager,
|
||||
useCompile,
|
||||
useCurrentAppInfo,
|
||||
CollectionCategroriesProvider,
|
||||
CollectionCategroriesContext,
|
||||
} from '@nocobase/client';
|
||||
import { useFullscreen } from 'ahooks';
|
||||
import { Button, Input, Layout, Menu, Popover, Switch, Tooltip } from 'antd';
|
||||
@ -42,6 +42,7 @@ import {
|
||||
getDiffEdge,
|
||||
getDiffNode,
|
||||
getInheritCollections,
|
||||
getPopupContainer,
|
||||
useGCMTranslation,
|
||||
} from './utils';
|
||||
|
||||
@ -232,10 +233,6 @@ function getEdges(edges) {
|
||||
});
|
||||
}
|
||||
|
||||
const getPopupContainer = () => {
|
||||
return document.getElementById('graph_container');
|
||||
};
|
||||
|
||||
const CollapsedContext = createContext<any>({});
|
||||
const formatNodeData = () => {
|
||||
const layoutNodes = [];
|
||||
@ -1021,14 +1018,7 @@ export const GraphDrawPage = React.memo(() => {
|
||||
<div className={cx(collectionListClass)}>
|
||||
<SchemaComponent
|
||||
components={{
|
||||
Select: (props) => (
|
||||
<Select
|
||||
{...props}
|
||||
getPopupContainer={() => {
|
||||
return document.getElementById('graph_container');
|
||||
}}
|
||||
/>
|
||||
),
|
||||
Select: (props) => <Select {...props} getPopupContainer={getPopupContainer} />,
|
||||
AddCollectionAction,
|
||||
}}
|
||||
schema={{
|
||||
@ -1100,7 +1090,7 @@ export const GraphDrawPage = React.memo(() => {
|
||||
},
|
||||
collectionList: {
|
||||
type: 'void',
|
||||
'x-component': () => {
|
||||
'x-component': function Com() {
|
||||
const { handleSearchCollection, collectionList } = useContext(CollapsedContext);
|
||||
const [selectedKeys, setSelectKey] = useState([]);
|
||||
const content = (
|
||||
@ -1121,13 +1111,13 @@ export const GraphDrawPage = React.memo(() => {
|
||||
}
|
||||
`}
|
||||
style={{ maxHeight: '70vh', overflowY: 'auto', border: 'none' }}
|
||||
>
|
||||
<Menu.Divider />
|
||||
{collectionList.map((v) => {
|
||||
return (
|
||||
<Menu.Item
|
||||
key={v.name}
|
||||
onClick={(e: any) => {
|
||||
items={[
|
||||
{ type: 'divider' },
|
||||
...collectionList.map((v) => {
|
||||
return {
|
||||
key: v.name,
|
||||
label: compile(v.title),
|
||||
onClick: (e: any) => {
|
||||
if (e.key !== selectedKeys[0]) {
|
||||
setSelectKey([e.key]);
|
||||
handleFiterCollections(e.key);
|
||||
@ -1136,13 +1126,11 @@ export const GraphDrawPage = React.memo(() => {
|
||||
handleFiterCollections(false);
|
||||
setSelectKey([]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>{compile(v.title)}</span>
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
},
|
||||
};
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
@ -1226,24 +1214,22 @@ export const GraphDrawPage = React.memo(() => {
|
||||
}
|
||||
`}
|
||||
style={{ maxHeight: '70vh', overflowY: 'auto', border: 'none' }}
|
||||
>
|
||||
<Menu.Divider />
|
||||
{menuItems.map((v) => {
|
||||
return (
|
||||
<Menu.Item
|
||||
key={v.key}
|
||||
onClick={(e: any) => {
|
||||
items={[
|
||||
{ type: 'divider' },
|
||||
...menuItems.map((v) => {
|
||||
return {
|
||||
key: v.key,
|
||||
label: t(v.label),
|
||||
onClick: (e: any) => {
|
||||
targetGraph.connectionType = v.key;
|
||||
const { filterConfig } = targetGraph;
|
||||
filterConfig && handleFiterCollections(filterConfig.key);
|
||||
handleSetRelationshipType(v.key);
|
||||
}}
|
||||
>
|
||||
<span>{t(v.label)}</span>
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
},
|
||||
};
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
@ -1303,25 +1289,23 @@ export const GraphDrawPage = React.memo(() => {
|
||||
}
|
||||
`}
|
||||
style={{ maxHeight: '70vh', overflowY: 'auto', border: 'none' }}
|
||||
>
|
||||
<Menu.Divider />
|
||||
{menuItems.map((v) => {
|
||||
return (
|
||||
<Menu.Item
|
||||
key={v.key}
|
||||
onClick={(e: any) => {
|
||||
items={[
|
||||
{ type: 'divider' },
|
||||
...menuItems.map((v) => {
|
||||
return {
|
||||
key: v.key,
|
||||
label: t(v.label),
|
||||
onClick: (e: any) => {
|
||||
targetGraph.direction = v.key;
|
||||
const { filterConfig } = targetGraph;
|
||||
if (filterConfig) {
|
||||
handleFiterCollections(filterConfig.key);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>{t(v.label)}</span>
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
},
|
||||
};
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { AddCollection } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { Button } from 'antd';
|
||||
import React from 'react';
|
||||
import { useCancelAction } from '../action-hooks';
|
||||
import { getPopupContainer } from '../utils';
|
||||
|
||||
export const AddCollectionAction = ({ item: record }) => {
|
||||
return (
|
||||
@ -17,7 +18,7 @@ export const AddCollectionAction = ({ item: record }) => {
|
||||
scope={{
|
||||
useCancelAction,
|
||||
}}
|
||||
getContainer={() => document.getElementById('graph_container')}
|
||||
getContainer={getPopupContainer}
|
||||
>
|
||||
<Button type="primary">
|
||||
<PlusOutlined />
|
||||
|
@ -2,6 +2,7 @@ import { PlusOutlined } from '@ant-design/icons';
|
||||
import { AddFieldAction as AddCollectionFieldAction } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useCancelAction, useCreateAction } from '../action-hooks';
|
||||
import { getPopupContainer } from '../utils';
|
||||
|
||||
const useCreateCollectionField = (record) => {
|
||||
const title = record.collectionName;
|
||||
@ -27,7 +28,7 @@ export const AddFieldAction = ({ item: record }) => {
|
||||
useCancelAction,
|
||||
useCreateCollectionField: () => useCreateCollectionField(record),
|
||||
}}
|
||||
getContainer={() => document.getElementById('graph_container')}
|
||||
getContainer={getPopupContainer}
|
||||
>
|
||||
<PlusOutlined className="btn-add" id="graph_btn_add_field" />
|
||||
</AddCollectionFieldAction>
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { EditCollection } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { useCancelAction, useUpdateCollectionActionAndRefreshCM } from '../action-hooks';
|
||||
import { getPopupContainer } from '../utils';
|
||||
|
||||
export const EditCollectionAction = ({ item: record }) => {
|
||||
return (
|
||||
@ -13,7 +14,7 @@ export const EditCollectionAction = ({ item: record }) => {
|
||||
useUpdateCollectionActionAndRefreshCM,
|
||||
createOnly: false,
|
||||
}}
|
||||
getContainer={() => document.getElementById('graph_container')}
|
||||
getContainer={getPopupContainer}
|
||||
>
|
||||
<EditOutlined
|
||||
className={css`
|
||||
|
@ -2,6 +2,7 @@ import { EditOutlined } from '@ant-design/icons';
|
||||
import { EditFieldAction as EditCollectionFieldAction } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useCancelAction, useUpdateFieldAction } from '../action-hooks';
|
||||
import { getPopupContainer } from '../utils';
|
||||
|
||||
const useUpdateCollectionField = (record) => {
|
||||
const collectionName = record.collectionName;
|
||||
@ -21,7 +22,7 @@ export const EditFieldAction = ({ item: record }) => {
|
||||
useCancelAction,
|
||||
useUpdateCollectionField: () => useUpdateCollectionField(record),
|
||||
}}
|
||||
getContainer={() => document.getElementById('graph_container')}
|
||||
getContainer={getPopupContainer}
|
||||
>
|
||||
<EditOutlined className="btn-edit" />
|
||||
</EditCollectionFieldAction>
|
||||
|
@ -5,7 +5,7 @@ import { uid } from '@formily/shared';
|
||||
import {
|
||||
Action,
|
||||
Checkbox,
|
||||
collection,
|
||||
CollectionCategroriesContext,
|
||||
CollectionField,
|
||||
CollectionProvider,
|
||||
Form,
|
||||
@ -19,15 +19,15 @@ import {
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
Select,
|
||||
collection,
|
||||
useCollectionManager,
|
||||
useCompile,
|
||||
useCurrentAppInfo,
|
||||
useRecord,
|
||||
CollectionCategroriesContext,
|
||||
} from '@nocobase/client';
|
||||
import { Badge, Dropdown, Popover, Tag } from 'antd';
|
||||
import { groupBy } from 'lodash';
|
||||
import React, { useRef, useState, useContext } from 'react';
|
||||
import React, { useContext, useRef, useState } from 'react';
|
||||
import {
|
||||
useAsyncDataSource,
|
||||
useCancelAction,
|
||||
@ -37,7 +37,7 @@ import {
|
||||
useValuesFromRecord,
|
||||
} from '../action-hooks';
|
||||
import { collectiionPopoverClass, entityContainer, headClass, tableBtnClass, tableNameClass } from '../style';
|
||||
import { useGCMTranslation } from '../utils';
|
||||
import { getPopupContainer, useGCMTranslation } from '../utils';
|
||||
import { AddFieldAction } from './AddFieldAction';
|
||||
import { CollectionNodeProvder } from './CollectionNodeProvder';
|
||||
import { EditCollectionAction } from './EditCollectionAction';
|
||||
@ -161,9 +161,7 @@ const Entity: React.FC<{
|
||||
|
||||
confirm: {
|
||||
title: "{{t('Delete record')}}",
|
||||
getContainer: () => {
|
||||
return document.getElementById('graph_container');
|
||||
},
|
||||
getContainer: getPopupContainer,
|
||||
collectionConten: "{{t('Are you sure you want to delete it?')}}",
|
||||
},
|
||||
useAction: () => useDestroyActionAndRefreshCM({ name, id }),
|
||||
@ -233,6 +231,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
|
||||
return 'orange';
|
||||
}
|
||||
};
|
||||
|
||||
const OperationButton = ({ property }) => {
|
||||
const isInheritField = !(property.collectionName !== name);
|
||||
return (
|
||||
@ -244,14 +243,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
|
||||
Input,
|
||||
Form,
|
||||
ResourceActionProvider,
|
||||
Select: (props) => (
|
||||
<Select
|
||||
{...props}
|
||||
getPopupContainer={() => {
|
||||
return document.getElementById('graph_container');
|
||||
}}
|
||||
/>
|
||||
),
|
||||
Select: (props) => <Select {...props} getPopupContainer={getPopupContainer} />,
|
||||
Checkbox,
|
||||
Radio,
|
||||
InputNumber,
|
||||
@ -334,9 +326,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
|
||||
`,
|
||||
confirm: {
|
||||
title: "{{t('Delete record')}}",
|
||||
getContainer: () => {
|
||||
return document.getElementById('graph_container');
|
||||
},
|
||||
getContainer: getPopupContainer,
|
||||
collectionConten: "{{t('Are you sure you want to delete it?')}}",
|
||||
},
|
||||
useAction: () =>
|
||||
@ -409,9 +399,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
|
||||
property.uiSchema && (
|
||||
<Popover
|
||||
content={CollectionConten(property)}
|
||||
getPopupContainer={() => {
|
||||
return document.getElementById('graph_container');
|
||||
}}
|
||||
getPopupContainer={getPopupContainer}
|
||||
mouseLeaveDelay={0}
|
||||
zIndex={100}
|
||||
title={
|
||||
@ -452,9 +440,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
|
||||
property.uiSchema && (
|
||||
<Popover
|
||||
content={CollectionConten(property)}
|
||||
getPopupContainer={() => {
|
||||
return document.getElementById('graph_container');
|
||||
}}
|
||||
getPopupContainer={getPopupContainer}
|
||||
mouseLeaveDelay={0}
|
||||
zIndex={100}
|
||||
title={
|
||||
|
@ -2,6 +2,7 @@ import { CopyOutlined } from '@ant-design/icons';
|
||||
import { OverridingFieldAction as OverridingCollectionFieldAction } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useCancelAction, useCreateAction } from '../action-hooks';
|
||||
import { getPopupContainer } from '../utils';
|
||||
|
||||
const useOverridingCollectionField = (record) => {
|
||||
const collectionName = record.targetCollection;
|
||||
@ -21,7 +22,7 @@ export const OverrideFieldAction = ({ item: record }) => {
|
||||
useCancelAction,
|
||||
useOverridingCollectionField: () => useOverridingCollectionField(record),
|
||||
}}
|
||||
getContainer={() => document.getElementById('graph_container')}
|
||||
getContainer={getPopupContainer}
|
||||
>
|
||||
<CopyOutlined className="btn-override" />
|
||||
</OverridingCollectionFieldAction>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user