refactor: fix warning of antd 4.x (#1998)

* refactor: fix warning by codemod

* refactor: fix warning of Dropdown

* perf: use memo

* refactor: resolve SchemaInitializer

* refactor: fix lint

* refactor: remove SettingsForm

* refactor: resolve SchemaInitializer

* refactor: fix lint

* refactor: move useMenuItem to root dir

* chore: fix conflicts

* refactor: resolve SchemaSetting

* refactor: fix lint

* test: fix failed

* chore: upgrade Vite

* fix: fix style

* refactor: fix lint

* refactor: extract component

* refactor: resovle Menu

* refactor: resolve Tabs

* refactor(getPopupContainer): should return the unique div

* refactor(Drawer): change style to rootStyle and className to rootClassName

* chore: update yarn.lock

* fix: fix T-432

* fix: fix T-338

* fix: fix T-490

* fix: collection fields

* fix: fix style

* fix: fix T-500

* fix: fix SettingMenu error (close T-516)

* fix: fix tanslation of Map (T-506)

* style: fix style (T-508)

* fix: fix schemaSetting switch of mobile (T-517)

* fix: fix T-518

* fix: fix T-524

* fix: fix T-507

* perf: optimize SchemaInitializer.Button

* perf: optimize SchemaSettings

* fix: fix serch of SchemaInitializer (T-547)

* chore: change delay

* fix: fix button style (T-548)

* fix: fix scroll bar

* fix: update yarn.lock

* fix: fix build error

* fix: should update sideMenu when change it

* fix: fix build error

* chore: mouseEnterDelay

* fix: fix group menu can not selected
This commit is contained in:
被雨水过滤的空气-Rairn 2023-06-22 19:51:16 +08:00 committed by GitHub
parent 5fc5428d03
commit 6eed9ac2bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 2331 additions and 2146 deletions

View File

@ -63,7 +63,7 @@
"prettier": "^2.2.1", "prettier": "^2.2.1",
"pretty-format": "^24.0.0", "pretty-format": "^24.0.0",
"pretty-quick": "^3.1.0", "pretty-quick": "^3.1.0",
"vite": "^4.3.8", "vite": "^4.3.9",
"vitest": "^0.32.0" "vitest": "^0.32.0"
}, },
"volta": { "volta": {

View File

@ -7,6 +7,7 @@
"typings": "es/index.d.ts", "typings": "es/index.d.ts",
"dependencies": { "dependencies": {
"@antv/g2plot": "^2.4.18", "@antv/g2plot": "^2.4.18",
"@ant-design/pro-layout": "^7.14.3",
"@dnd-kit/core": "^5.0.1", "@dnd-kit/core": "^5.0.1",
"@dnd-kit/sortable": "^6.0.0", "@dnd-kit/sortable": "^6.0.0",
"@emotion/css": "^11.7.1", "@emotion/css": "^11.7.1",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,15 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { TabPaneProps, Tabs as AntdTabs, TabsProps } from 'antd'; import { Tabs as AntdTabs, TabPaneProps, TabsProps } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React, { useMemo } from 'react';
import { Icon } from '../../../icon'; import { Icon } from '../../../icon';
import { useSchemaInitializer } from '../../../schema-initializer'; import { useSchemaInitializer } from '../../../schema-initializer';
import { DndContext, SortableItem } from '../../common'; import { DndContext, SortableItem } from '../../common';
import { useDesignable } from '../../hooks';
import { useDesigner } from '../../hooks/useDesigner'; import { useDesigner } from '../../hooks/useDesigner';
import { useTabsContext } from './context'; import { useTabsContext } from './context';
import { TabsDesigner } from './Tabs.Designer'; import { TabsDesigner } from './Tabs.Designer';
import { useDesignable } from '../../hooks';
export const Tabs: any = observer( export const Tabs: any = observer(
(props: TabsProps) => { (props: TabsProps) => {
@ -19,20 +19,33 @@ export const Tabs: any = observer(
const contextProps = useTabsContext(); const contextProps = useTabsContext();
const { PaneRoot = React.Fragment as React.FC<any> } = contextProps; const { PaneRoot = React.Fragment as React.FC<any> } = contextProps;
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 result;
}, [fieldSchema.mapProperties((s, key) => key).join()]);
return ( return (
<DndContext> <DndContext>
<AntdTabs {...contextProps} style={props.style}> <AntdTabs {...contextProps} style={props.style} items={items} />
{fieldSchema.mapProperties((schema, key) => {
return (
<AntdTabs.TabPane tab={<RecursionField name={key} schema={schema} onlyRenderSelf />} key={key}>
<PaneRoot active={key === contextProps.activeKey}>
<RecursionField name={key} schema={schema} onlyRenderProperties />
</PaneRoot>
</AntdTabs.TabPane>
);
})}
{designable && <AntdTabs.TabPane tab={render()} />}
</AntdTabs>
</DndContext> </DndContext>
); );
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,10 @@
import { ISchema, Schema, useFieldSchema, useForm } from '@formily/react'; import { ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import React, { useContext, useMemo, useState } from 'react'; import { error } from '@nocobase/utils/client';
import _ from 'lodash';
import React, { useCallback, useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { BlockRequestContext, SchemaInitializerItemOptions } from '../'; import { BlockRequestContext, SchemaInitializerButtonContext, SchemaInitializerItemOptions } from '../';
import { FieldOptions, useCollection, useCollectionManager } from '../collection-manager'; import { FieldOptions, useCollection, useCollectionManager } from '../collection-manager';
import { isAssocField } from '../filter-provider/utils'; import { isAssocField } from '../filter-provider/utils';
import { useActionContext, useDesignable } from '../schema-component'; import { useActionContext, useDesignable } from '../schema-component';
@ -807,24 +809,28 @@ export const useCollectionDataSourceItems = (componentName) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { collections, getCollectionFields } = useCollectionManager(); const { collections, getCollectionFields } = useCollectionManager();
const { getTemplatesByCollection } = useSchemaTemplateManager(); const { getTemplatesByCollection } = useSchemaTemplateManager();
const [selected, setSelected] = useState([]); const { searchValue, setSearchValue } = useContext(SchemaInitializerButtonContext);
const [value, onChange] = useState(null); // eslint-disable-next-line react-hooks/exhaustive-deps
const onChange = useCallback(_.debounce(setSearchValue, 300), [setSearchValue]);
if (!setSearchValue) {
error('useCollectionDataSourceItems: please use in SchemaInitializerButtonContext and provide setSearchValue');
return [];
}
const clearKeywords = () => { const clearKeywords = () => {
setSelected([]); setSearchValue('');
onChange(null);
}; };
return [ return [
{ {
key: 'tableBlock', key: 'tableBlock',
type: 'itemGroup', type: 'itemGroup',
title: React.createElement(SelectCollection, { title: React.createElement(SelectCollection, {
value, value: searchValue,
onChange, onChange,
setSelected,
}), }),
children: collections children: collections
?.filter((item) => { ?.filter((item) => {
const b = !value || selected.includes(item.name);
if (item.inherit) { if (item.inherit) {
return false; return false;
} }
@ -836,7 +842,12 @@ export const useCollectionDataSourceItems = (componentName) => {
} else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) { } else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) {
return false; return false;
} else { } else {
return b && !(item?.isThrough && item?.autoCreate); if (!item.title) {
return false;
}
return (
item.title.toUpperCase().includes(searchValue.toUpperCase()) && !(item?.isThrough && item?.autoCreate)
);
} }
}) })
?.map((item, index) => { ?.map((item, index) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,11 @@
import { EyeOutlined } from '@ant-design/icons'; import { EyeOutlined } from '@ant-design/icons';
import { ViewFieldAction as ViewCollectionFieldAction } from '@nocobase/client'; import { ViewFieldAction as ViewCollectionFieldAction } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { getPopupContainer } from '../utils';
export const ViewFieldAction = ({ item: record }) => { export const ViewFieldAction = ({ item: record }) => {
return ( return (
<ViewCollectionFieldAction item={{ ...record }} getContainer={() => document.getElementById('graph_container')}> <ViewCollectionFieldAction item={{ ...record }} getContainer={getPopupContainer}>
<EyeOutlined className="btn-view" /> <EyeOutlined className="btn-view" />
</ViewCollectionFieldAction> </ViewCollectionFieldAction>
); );

View File

@ -532,3 +532,15 @@ export const getDiffEdge = (newEdges, oldEdges) => {
} }
return edges; return edges;
}; };
let graphContainer;
/**
* getPopupContainer divReact 18 concurrent
* https://ant.design/docs/react/migration-v5-cn#%E5%8D%87%E7%BA%A7%E5%87%86%E5%A4%87
*/
export const getPopupContainer = () => {
if (graphContainer) {
return graphContainer;
}
return (graphContainer = document.getElementById('graph_container'));
};

View File

@ -1,11 +1,9 @@
import { FormOutlined } from '@ant-design/icons'; import { FormOutlined } from '@ant-design/icons';
import { SchemaInitializer } from '@nocobase/client'; import { SchemaInitializer } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
export const IframeBlockInitializer = (props) => { export const IframeBlockInitializer = (props) => {
const { insert } = props; const { insert } = props;
const { t } = useTranslation();
return ( return (
<SchemaInitializer.Item <SchemaInitializer.Item
{...props} {...props}

View File

@ -18,7 +18,7 @@ export const ImportInitializerProvider = (props: any) => {
skipScopeCheck: true, skipScopeCheck: true,
}, },
}, },
visible: () => { visible: function useVisible() {
const collection = useCollection(); const collection = useCollection();
return collection.template !== 'view' && collection.template !== 'file'; return collection.template !== 'view' && collection.template !== 'file';
}, },

View File

@ -1,6 +1,6 @@
import { useAPIClient, useCompile } from '@nocobase/client'; import { useAPIClient, useCompile } from '@nocobase/client';
import { useBoolean } from 'ahooks'; import { useBoolean } from 'ahooks';
import { Button, Card, Form, Input, message, Tabs } from 'antd'; import { Button, Card, Form, Input, Tabs, message } from 'antd';
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import { MapTypes } from '../constants'; import { MapTypes } from '../constants';
import { MapConfigurationResourceKey, getSSKey, useMapConfiguration } from '../hooks'; import { MapConfigurationResourceKey, getSSKey, useMapConfiguration } from '../hooks';
@ -76,15 +76,16 @@ const Configuration = () => {
const compile = useCompile(); const compile = useCompile();
return ( return (
<Card bordered> <Card bordered>
<Tabs type="card"> <Tabs
{tabList.map((tab) => { type="card"
return ( items={tabList.map((tab) => {
<Tabs.TabPane key={tab.value} tab={compile(tab.label)}> return {
<tab.component type={tab.value} /> key: tab.value,
</Tabs.TabPane> label: compile(tab.label),
); children: <tab.component type={tab.value} />,
};
})} })}
</Tabs> />
</Card> </Card>
); );
}; };

View File

@ -1,13 +1,13 @@
import { SchemaSettings, useDesignable } from '@nocobase/client'; import { MenuOutlined } from '@ant-design/icons';
import React from 'react';
import { generateNTemplate, useTranslation } from '../../../../locale';
import { Schema, useField, useFieldSchema } from '@formily/react'; import { Schema, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { useNavigate } from 'react-router-dom'; import { SchemaSettings, useDesignable } from '@nocobase/client';
import { findSchema } from '../../helpers';
import { Button } from 'antd'; import { Button } from 'antd';
import { MenuOutlined } from '@ant-design/icons'; import React from 'react';
import { useNavigate } from 'react-router-dom';
import { generateNTemplate, useTranslation } from '../../../../locale';
import { PageSchema } from '../../common'; import { PageSchema } from '../../common';
import { findSchema } from '../../helpers';
export const ContainerDesigner = () => { export const ContainerDesigner = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -32,6 +32,7 @@ export const ContainerDesigner = () => {
style={{ style={{
borderColor: 'rgb(241, 139, 98)', borderColor: 'rgb(241, 139, 98)',
color: 'rgb(241, 139, 98)', color: 'rgb(241, 139, 98)',
width: '100%',
}} }}
icon={<MenuOutlined />} icon={<MenuOutlined />}
type="dashed" type="dashed"

View File

@ -1,11 +1,11 @@
import { GeneralSchemaDesigner, SchemaSettings, useDesignable } from '@nocobase/client'; import { MenuOutlined } from '@ant-design/icons';
import { useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared';
import { SchemaSettings, useDesignable } from '@nocobase/client';
import { Button } from 'antd';
import React from 'react'; import React from 'react';
import { generateNTemplate, useTranslation } from '../../../../locale'; import { generateNTemplate, useTranslation } from '../../../../locale';
import { useField, useFieldSchema } from '@formily/react';
import { findGridSchema } from '../../helpers'; import { findGridSchema } from '../../helpers';
import { uid } from '@formily/shared';
import { Button } from 'antd';
import { MenuOutlined } from '@ant-design/icons';
export const PageDesigner = (props) => { export const PageDesigner = (props) => {
const { showBack } = props; const { showBack } = props;
@ -28,6 +28,7 @@ export const PageDesigner = (props) => {
style={{ style={{
borderColor: 'rgb(241, 139, 98)', borderColor: 'rgb(241, 139, 98)',
color: 'rgb(241, 139, 98)', color: 'rgb(241, 139, 98)',
width: '100%',
}} }}
icon={<MenuOutlined />} icon={<MenuOutlined />}
type="dashed" type="dashed"

View File

@ -1,7 +1,7 @@
import React from 'react';
import { SettingsMenu, SortableItem, useDesigner } from '@nocobase/client';
import { SettingsDesigner } from './Settings.Designer';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { SettingsMenu, SortableItem, useDesigner } from '@nocobase/client';
import React from 'react';
import { SettingsDesigner } from './Settings.Designer';
export const InternalSettings = () => { export const InternalSettings = () => {
const Designer = useDesigner(); const Designer = useDesigner();
return ( return (

View File

@ -5,7 +5,7 @@ import {
SettingsCenterProvider, SettingsCenterProvider,
useRequest, useRequest,
} from '@nocobase/client'; } from '@nocobase/client';
import { Button, Dropdown, Menu } from 'antd'; import { Button, Dropdown } from 'antd';
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { AppManager } from './AppManager'; import { AppManager } from './AppManager';

View File

@ -1,12 +1,12 @@
import React from 'react';
import { cx } from '@emotion/css';
import { Dropdown, Menu, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { cx } from '@emotion/css';
import { useAPIClient, useCompile } from '@nocobase/client'; import { useAPIClient, useCompile } from '@nocobase/client';
import { Button, Dropdown, MenuProps } from 'antd';
import React, { useCallback, useMemo } from 'react';
import { useFlowContext } from './FlowContext'; import { useFlowContext } from './FlowContext';
import { NAMESPACE } from './locale';
import { Instruction, instructions } from './nodes'; import { Instruction, instructions } from './nodes';
import { addButtonClass } from './style'; import { addButtonClass } from './style';
import { NAMESPACE } from './locale';
interface AddButtonProps { interface AddButtonProps {
upstream; upstream;
@ -17,72 +17,82 @@ export function AddButton({ upstream, branchIndex = null }: AddButtonProps) {
const compile = useCompile(); const compile = useCompile();
const api = useAPIClient(); const api = useAPIClient();
const { workflow, refresh } = useFlowContext() ?? {}; const { workflow, refresh } = useFlowContext() ?? {};
const instructionList = Array.from(instructions.getValues()) as Instruction[];
const groups = useMemo(() => {
return [
{ key: 'control', label: `{{t("Control", { ns: "${NAMESPACE}" })}}` },
{ key: 'collection', label: `{{t("Collection operations", { ns: "${NAMESPACE}" })}}` },
{ key: 'manual', label: `{{t("Manual", { ns: "${NAMESPACE}" })}}` },
{ key: 'extended', label: `{{t("Extended types", { ns: "${NAMESPACE}" })}}` },
]
.filter((group) => instructionList.filter((item) => item.group === group.key).length)
.map((group) => {
const groupInstructions = instructionList.filter((item) => item.group === group.key);
return {
...group,
type: 'group',
children: groupInstructions.map((item) => ({
key: item.type,
label: item.title,
type: item.options ? 'subMenu' : null,
children: item.options
? item.options.map((option) => ({
key: option.key,
label: option.label,
}))
: null,
})),
};
});
}, [instructionList]);
const resource = useMemo(() => {
if (!workflow) {
return null;
}
return api.resource('workflows.nodes', workflow.id);
}, [workflow?.id]);
const onCreate = useCallback(
async ({ keyPath }) => {
const type = keyPath.pop();
const config = {};
const [optionKey] = keyPath;
const instruction = instructions.get(type);
if (optionKey) {
const { value } = instruction.options?.find((item) => item.key === optionKey) ?? {};
Object.assign(config, value);
}
if (resource) {
await resource.create({
values: {
type,
upstreamId: upstream?.id ?? null,
branchIndex,
title: compile(instruction.title),
config,
},
});
refresh();
}
},
[branchIndex, resource?.create, upstream?.id],
);
const menu = useMemo<MenuProps>(() => {
return {
onClick: (ev) => onCreate(ev),
items: compile(groups),
};
}, [groups, onCreate]);
if (!workflow) { if (!workflow) {
return null; return null;
} }
const resource = api.resource('workflows.nodes', workflow.id);
async function onCreate({ keyPath }) {
const type = keyPath.pop();
const config = {};
const [optionKey] = keyPath;
const instruction = instructions.get(type);
if (optionKey) {
const { value } = instruction.options?.find((item) => item.key === optionKey) ?? {};
Object.assign(config, value);
}
const {
data: { data: node },
} = await resource.create!({
values: {
type,
upstreamId: upstream?.id ?? null,
branchIndex,
title: compile(instruction.title),
config,
},
});
refresh();
}
const instructionList = Array.from(instructions.getValues()) as Instruction[];
const groups = [
{ key: 'control', label: `{{t("Control", { ns: "${NAMESPACE}" })}}` },
{ key: 'collection', label: `{{t("Collection operations", { ns: "${NAMESPACE}" })}}` },
{ key: 'manual', label: `{{t("Manual", { ns: "${NAMESPACE}" })}}` },
{ key: 'extended', label: `{{t("Extended types", { ns: "${NAMESPACE}" })}}` },
]
.filter((group) => instructionList.filter((item) => item.group === group.key).length)
.map((group) => {
const groupInstructions = instructionList.filter((item) => item.group === group.key);
return {
...group,
type: 'group',
children: groupInstructions.map((item) => ({
key: item.type,
label: item.title,
type: item.options ? 'subMenu' : null,
children: item.options
? item.options.map((option) => ({
key: option.key,
label: option.label,
}))
: null,
})),
};
});
return ( return (
<div className={cx(addButtonClass)}> <div className={cx(addButtonClass)}>
<Dropdown <Dropdown trigger={['click']} menu={menu} disabled={workflow.executed}>
trigger={['click']}
overlay={<Menu onClick={(ev) => onCreate(ev)} items={compile(groups)} />}
disabled={workflow.executed}
>
<Button shape="circle" icon={<PlusOutlined />} /> <Button shape="circle" icon={<PlusOutlined />} />
</Dropdown> </Dropdown>
</div> </div>

View File

@ -5,7 +5,7 @@ import {
SchemaComponent, SchemaComponent,
SchemaComponentOptions, SchemaComponentOptions,
SettingsCenterProvider, SettingsCenterProvider,
useCollectionDataSource useCollectionDataSource,
} from '@nocobase/client'; } from '@nocobase/client';
import { Card } from 'antd'; import { Card } from 'antd';
import React, { useContext } from 'react'; import React, { useContext } from 'react';

View File

@ -1,9 +1,9 @@
import React, { useCallback } from 'react'; import { CloseCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { observer, useForm, useField } from '@formily/react';
import { Input, Button, Dropdown, Menu, Form } from 'antd';
import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { observer, useField, useForm } from '@formily/react';
import { Button, Dropdown, Form, Input, MenuProps } from 'antd';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
CollectionField, CollectionField,
@ -53,8 +53,23 @@ const CollectionFieldSet = observer(
const collectionFields = useCollectionUIFields(collectionName); const collectionFields = useCollectionUIFields(collectionName);
const fields = filter ? collectionFields.filter(filter.bind(config)) : collectionFields; const fields = filter ? collectionFields.filter(filter.bind(config)) : collectionFields;
const unassignedFields = fields.filter((field) => !value || !(field.name in value)); const unassignedFields = useMemo(() => fields.filter((field) => !value || !(field.name in value)), [fields, value]);
const mergedDisabled = disabled || form.disabled; const mergedDisabled = disabled || form.disabled;
const menu = useMemo<MenuProps>(() => {
return {
onClick: ({ key }) => {
onChange({ ...value, [key]: null });
},
style: {
maxHeight: 300,
overflowY: 'auto',
},
items: unassignedFields.map((field) => ({
key: field.name,
label: compile(field.uiSchema?.title ?? field.name),
})),
};
}, [onChange, unassignedFields, value]);
return ( return (
<fieldset <fieldset
@ -127,21 +142,7 @@ const CollectionFieldSet = observer(
); );
})} })}
{unassignedFields.length ? ( {unassignedFields.length ? (
<Dropdown <Dropdown menu={menu}>
overlay={
<Menu
items={unassignedFields.map((field) => ({
key: field.name,
label: compile(field.uiSchema?.title ?? field.name),
}))}
onClick={({ key }) => onChange({ ...value, [key]: null })}
className={css`
max-height: 300px;
overflow-y: auto;
`}
/>
}
>
<Button icon={<PlusOutlined />}>{t('Add field')}</Button> <Button icon={<PlusOutlined />}>{t('Add field')}</Button>
</Dropdown> </Dropdown>
) : null} ) : null}

151
yarn.lock
View File

@ -163,10 +163,10 @@
classnames "^2.2.6" classnames "^2.2.6"
rc-util "^5.9.4" rc-util "^5.9.4"
"@ant-design/icons@^5.1.0": "@ant-design/icons@^5.0.0", "@ant-design/icons@^5.1.0":
version "5.1.2" version "5.1.4"
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-5.1.2.tgz#6dc30c5caff0670f11b6391f0839ed41847c72e6" resolved "https://registry.npmmirror.com/@ant-design/icons/-/icons-5.1.4.tgz#614e29e26d092c2c1c1a2acbc0d84434d8d1474e"
integrity sha512-sSorFv+4e5nkSq9vD7UHG2IQYUmR8jCO49+z4vn3MUeiM1G1qxCqh7JiR5pexVY0hZywHes/uxiNflAQZCWZ8Q== integrity sha512-YHKL7Jx3bM12OxvtiYDon04BsBT/6LGitYEqar3GljzWaAyMOAD8i/uF1Rsi5Us/YNdWWXBGSvZV2OZWMpJlcA==
dependencies: dependencies:
"@ant-design/colors" "^7.0.0" "@ant-design/colors" "^7.0.0"
"@ant-design/icons-svg" "^4.2.1" "@ant-design/icons-svg" "^4.2.1"
@ -185,6 +185,52 @@
classnames "^2.2.6" classnames "^2.2.6"
rc-util "^5.31.1" rc-util "^5.31.1"
"@ant-design/pro-layout@^7.14.3":
version "7.14.3"
resolved "https://registry.npmmirror.com/@ant-design/pro-layout/-/pro-layout-7.14.3.tgz#01063883a334060c41b72ec084c3e83200673952"
integrity sha512-FcKj23f/laod2hfSSYfBbkjb9r74lZLgan8dylI+tngQnZKQc1o1a1f9If7CggkrhEdgDyIkIyFl9Ii/tfDa+A==
dependencies:
"@ant-design/icons" "^5.0.0"
"@ant-design/pro-provider" "^2.10.2"
"@ant-design/pro-utils" "^2.11.3"
"@babel/runtime" "^7.18.0"
"@umijs/route-utils" "^4.0.0"
"@umijs/use-params" "^1.0.9"
classnames "^2.3.2"
lodash.merge "^4.6.2"
omit.js "^2.0.2"
path-to-regexp "2.4.0"
rc-resize-observer "^1.1.0"
rc-util "^5.0.6"
swr "^2.0.0"
use-json-comparison "^1.0.3"
use-media-antd-query "^1.1.0"
warning "^4.0.3"
"@ant-design/pro-provider@^2.10.2":
version "2.10.2"
resolved "https://registry.npmmirror.com/@ant-design/pro-provider/-/pro-provider-2.10.2.tgz#3338afa88f35fe06da8f3573e7cf31c323cad456"
integrity sha512-I0wfnP/fJUgGrl/9P5haMtITg8oHe2NOm83QKFVjqQWGYnA2V4O5rVqD+vdWveuaJ6rtCW8mEoeelfHF6nOnxw==
dependencies:
"@ant-design/cssinjs" "^1.9.1"
"@babel/runtime" "^7.18.0"
"@ctrl/tinycolor" "^3.4.0"
rc-util "^5.0.1"
swr "^2.0.0"
"@ant-design/pro-utils@^2.11.3":
version "2.11.3"
resolved "https://registry.npmmirror.com/@ant-design/pro-utils/-/pro-utils-2.11.3.tgz#847da90763f4157bc5e273bf0aa018af13ec28ae"
integrity sha512-Sd2oHzhQlSJ0svaD3Ih3v2VeXPnYWwJ05ul+ek8i9GWAGnFWD0yDTawGj7pDZwCQwJYaLpAzPHhfNvq3Led0/w==
dependencies:
"@ant-design/icons" "^5.0.0"
"@ant-design/pro-provider" "^2.10.2"
"@babel/runtime" "^7.18.0"
classnames "^2.3.2"
dayjs "^1.11.4"
rc-util "^5.0.6"
swr "^2.0.0"
"@ant-design/react-slick@~0.29.1": "@ant-design/react-slick@~0.29.1":
version "0.29.2" version "0.29.2"
resolved "https://registry.yarnpkg.com/@ant-design/react-slick/-/react-slick-0.29.2.tgz#53e6a7920ea3562eebb304c15a7fc2d7e619d29c" resolved "https://registry.yarnpkg.com/@ant-design/react-slick/-/react-slick-0.29.2.tgz#53e6a7920ea3562eebb304c15a7fc2d7e619d29c"
@ -3394,7 +3440,7 @@
"@emotion/hash@^0.8.0": "@emotion/hash@^0.8.0":
version "0.8.0" version "0.8.0"
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" resolved "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
"@emotion/hash@^0.9.1": "@emotion/hash@^0.9.1":
@ -3442,16 +3488,15 @@
"@emotion/sheet@^1.2.2": "@emotion/sheet@^1.2.2":
version "1.2.2" version "1.2.2"
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec"
integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==
"@emotion/stylis@^0.8.4": "@emotion/stylis@^0.8.4":
version "0.8.5" version "0.8.5"
resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" resolved "https://registry.npmmirror.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04"
integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==
"@emotion/unitless@^0.7.4", "@emotion/unitless@^0.7.5": "@emotion/unitless@^0.7.4", "@emotion/unitless@^0.7.5":
version "0.7.5" version "0.7.5"
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" resolved "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
"@emotion/unitless@^0.8.1": "@emotion/unitless@^0.8.1":
@ -7376,6 +7421,11 @@
react-helmet-async "1.3.0" react-helmet-async "1.3.0"
react-router-dom "6.3.0" react-router-dom "6.3.0"
"@umijs/route-utils@^4.0.0":
version "4.0.1"
resolved "https://registry.npmmirror.com/@umijs/route-utils/-/route-utils-4.0.1.tgz#156df5b3f2328059722d3ee7dd8f65e18c3cde8b"
integrity sha512-+1ixf1BTOLuH+ORb4x8vYMPeIt38n9q0fJDwhv9nSxrV46mxbLF0nmELIo9CKQB2gHfuC4+hww6xejJ6VYnBHQ==
"@umijs/server@4.0.69": "@umijs/server@4.0.69":
version "4.0.69" version "4.0.69"
resolved "https://registry.yarnpkg.com/@umijs/server/-/server-4.0.69.tgz#6819612ee294ae432a37b7c673ea39e39a6aa7ef" resolved "https://registry.yarnpkg.com/@umijs/server/-/server-4.0.69.tgz#6819612ee294ae432a37b7c673ea39e39a6aa7ef"
@ -7406,6 +7456,11 @@
resolved "https://registry.yarnpkg.com/@umijs/ui/-/ui-3.0.1.tgz#64ae7ef36bf9374823f7361a7a844876d96c9e06" resolved "https://registry.yarnpkg.com/@umijs/ui/-/ui-3.0.1.tgz#64ae7ef36bf9374823f7361a7a844876d96c9e06"
integrity sha512-zcz37AJH0xt/6XVVbyO/hmsK9Hq4vH23HZ4KYVi5A8rbM9KeJkJigTS7ELOdArawZhVNGe+h3a5Oixs4a2QsWw== integrity sha512-zcz37AJH0xt/6XVVbyO/hmsK9Hq4vH23HZ4KYVi5A8rbM9KeJkJigTS7ELOdArawZhVNGe+h3a5Oixs4a2QsWw==
"@umijs/use-params@^1.0.9":
version "1.0.9"
resolved "https://registry.npmmirror.com/@umijs/use-params/-/use-params-1.0.9.tgz#0ae4a87f4922d8e8e3fb4495b0f8f4de9ca38c52"
integrity sha512-QlN0RJSBVQBwLRNxbxjQ5qzqYIGn+K7USppMoIOVlf7fxXHsnQZ2bEsa6Pm74bt6DVQxpUE8HqvdStn6Y9FV1w==
"@umijs/utils@3.5.39", "@umijs/utils@^3.5.20": "@umijs/utils@3.5.39", "@umijs/utils@^3.5.20":
version "3.5.39" version "3.5.39"
resolved "https://registry.yarnpkg.com/@umijs/utils/-/utils-3.5.39.tgz#86a57e262bfbdbc356cdcacaf415b11b1a450e12" resolved "https://registry.yarnpkg.com/@umijs/utils/-/utils-3.5.39.tgz#86a57e262bfbdbc356cdcacaf415b11b1a450e12"
@ -11113,13 +11168,15 @@ dateformat@^3.0.0:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
dayjs@1.x, dayjs@^1.11.1, dayjs@^1.11.7, dayjs@^1.9.1, dayjs@~1.11.5: dayjs@1.x, dayjs@^1.11.1, dayjs@^1.11.4, dayjs@^1.11.7, dayjs@^1.9.1, dayjs@~1.11.5:
version "1.11.7" version "1.11.8"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.8.tgz#4282f139c8c19dd6d0c7bd571e30c2d0ba7698ea"
integrity sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ==
dayjs@~1.8.24: dayjs@~1.8.24:
version "1.8.36" version "1.8.36"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.36.tgz#be36e248467afabf8f5a86bae0de0cdceecced50" resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.8.36.tgz#be36e248467afabf8f5a86bae0de0cdceecced50"
integrity sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw==
debounce-fn@^4.0.0: debounce-fn@^4.0.0:
version "4.0.0" version "4.0.0"
@ -19239,6 +19296,11 @@ omit-deep@0.3.0:
is-plain-object "^2.0.1" is-plain-object "^2.0.1"
unset-value "^0.1.1" unset-value "^0.1.1"
omit.js@^2.0.2:
version "2.0.2"
resolved "https://registry.npmmirror.com/omit.js/-/omit.js-2.0.2.tgz#dd9b8436fab947a5f3ff214cb2538631e313ec2f"
integrity sha512-hJmu9D+bNB40YpL9jYebQl4lsTW6yEHRTroJzNLqQJYHm7c+NQnJGfZmIWh8S3q3KoaxV1aLhV6B3+0N0/kyJg==
on-exit-leak-free@^0.2.0: on-exit-leak-free@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz#b39c9e3bf7690d890f4861558b0d7b90a442d209" resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz#b39c9e3bf7690d890f4861558b0d7b90a442d209"
@ -19874,6 +19936,11 @@ path-to-regexp@2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45"
path-to-regexp@2.4.0:
version "2.4.0"
resolved "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-2.4.0.tgz#35ce7f333d5616f1c1e1bfe266c3aba2e5b2e704"
integrity sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==
path-to-regexp@^6.1.0: path-to-regexp@^6.1.0:
version "6.2.1" version "6.2.1"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5"
@ -21883,17 +21950,17 @@ rc-upload@~4.3.0:
classnames "^2.2.5" classnames "^2.2.5"
rc-util "^5.2.0" rc-util "^5.2.0"
rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.15.0, rc-util@^5.16.0, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.19.2, rc-util@^5.2.0, rc-util@^5.2.1, rc-util@^5.20.1, rc-util@^5.21.0, rc-util@^5.21.2, rc-util@^5.22.5, rc-util@^5.23.0, rc-util@^5.24.4, rc-util@^5.26.0, rc-util@^5.27.0, rc-util@^5.4.0, rc-util@^5.6.1, rc-util@^5.8.0, rc-util@^5.9.4: rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.15.0, rc-util@^5.16.0, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.19.2, rc-util@^5.2.0, rc-util@^5.2.1, rc-util@^5.20.1, rc-util@^5.21.0, rc-util@^5.21.2, rc-util@^5.22.5, rc-util@^5.23.0, rc-util@^5.24.4, rc-util@^5.25.2, rc-util@^5.26.0, rc-util@^5.27.0, rc-util@^5.31.1, rc-util@^5.4.0, rc-util@^5.6.1, rc-util@^5.8.0, rc-util@^5.9.4:
version "5.29.3" version "5.33.0"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.29.3.tgz#dc02b7b2103468e9fdf14e0daa58584f47898e37" resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.33.0.tgz#8e673700e467b24c722014f2fe2f2f77aa6c2c07"
dependencies: dependencies:
"@babel/runtime" "^7.18.3" "@babel/runtime" "^7.18.3"
react-is "^16.12.0" react-is "^16.12.0"
rc-util@^5.21.5, rc-util@^5.25.2, rc-util@^5.27.1, rc-util@^5.28.0, rc-util@^5.31.1, rc-util@^5.33.0: rc-util@^5.21.5, rc-util@^5.27.1, rc-util@^5.28.0, rc-util@^5.33.0:
version "5.33.0" version "5.33.1"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.33.0.tgz#8e673700e467b24c722014f2fe2f2f77aa6c2c07" resolved "https://registry.npmmirror.com/rc-util/-/rc-util-5.33.1.tgz#96e5814400e04b819bace502b6ca40a67f3be37f"
integrity sha512-mq2NkEAnHklq4fgU/JqjiE0PS8+8u33gEWw2bDUNDPck3OroPpSgw/8oEyuFrvPgaZEmt9BgQdh59JfQt2cU+w== integrity sha512-oMs2OIV/2lUCF8nllevzLccneyxAzdSOaHSs5y91qOLdqaLbIMsuL49C6/DhF/WKMqiAKEKGdVk2F1sB5HQe9A==
dependencies: dependencies:
"@babel/runtime" "^7.18.3" "@babel/runtime" "^7.18.3"
react-is "^16.12.0" react-is "^16.12.0"
@ -21906,16 +21973,7 @@ rc-util@^5.30.0:
"@babel/runtime" "^7.18.3" "@babel/runtime" "^7.18.3"
react-is "^16.12.0" react-is "^16.12.0"
rc-virtual-list@^3.2.0: rc-virtual-list@^3.2.0, rc-virtual-list@^3.4.8, rc-virtual-list@^3.5.2:
version "3.4.13"
resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.4.13.tgz#20acc934b263abcf7b7c161f50ef82281b2f7e8d"
dependencies:
"@babel/runtime" "^7.20.0"
classnames "^2.2.6"
rc-resize-observer "^1.0.0"
rc-util "^5.15.0"
rc-virtual-list@^3.4.8, rc-virtual-list@^3.5.2:
version "3.5.2" version "3.5.2"
resolved "https://registry.npmmirror.com/rc-virtual-list/-/rc-virtual-list-3.5.2.tgz#5e1028869bae900eacbae6788d4eca7210736006" resolved "https://registry.npmmirror.com/rc-virtual-list/-/rc-virtual-list-3.5.2.tgz#5e1028869bae900eacbae6788d4eca7210736006"
integrity sha512-sE2G9hTPjVmatQni8OP2Kx33+Oth6DMKm67OblBBmgMBJDJQOOFpSGH7KZ6Pm85rrI2IGxDRXZCr0QhYOH2pfQ== integrity sha512-sE2G9hTPjVmatQni8OP2Kx33+Oth6DMKm67OblBBmgMBJDJQOOFpSGH7KZ6Pm85rrI2IGxDRXZCr0QhYOH2pfQ==
@ -24256,25 +24314,21 @@ stylehacks@^4.0.0:
stylelint-config-recommended@^7.0.0: stylelint-config-recommended@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz#7497372ae83ab7a6fffc18d7d7b424c6480ae15e" resolved "https://registry.npmmirror.com/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz#7497372ae83ab7a6fffc18d7d7b424c6480ae15e"
integrity sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q== integrity sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q==
stylelint-config-standard@25.0.0: stylelint-config-standard@25.0.0:
version "25.0.0" version "25.0.0"
resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-25.0.0.tgz#2c916984e6655d40d6e8748b19baa8603b680bff" resolved "https://registry.npmmirror.com/stylelint-config-standard/-/stylelint-config-standard-25.0.0.tgz#2c916984e6655d40d6e8748b19baa8603b680bff"
integrity sha512-21HnP3VSpaT1wFjFvv9VjvOGDtAviv47uTp3uFmzcN+3Lt+RYRv6oAplLaV51Kf792JSxJ6svCJh/G18E9VnCA== integrity sha512-21HnP3VSpaT1wFjFvv9VjvOGDtAviv47uTp3uFmzcN+3Lt+RYRv6oAplLaV51Kf792JSxJ6svCJh/G18E9VnCA==
dependencies: dependencies:
stylelint-config-recommended "^7.0.0" stylelint-config-recommended "^7.0.0"
stylis@4.2.0, stylis@^4.0.13: stylis@4.2.0, stylis@^4.0.13, stylis@^4.1.2:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" resolved "https://registry.npmmirror.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51"
integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==
stylis@^4.1.2:
version "4.1.3"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7"
superagent@^8.0.5: superagent@^8.0.5:
version "8.0.9" version "8.0.9"
resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.9.tgz#2c6fda6fadb40516515f93e9098c0eb1602e0535" resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.9.tgz#2c6fda6fadb40516515f93e9098c0eb1602e0535"
@ -24409,6 +24463,13 @@ svgson@^4.1.0:
omit-deep "0.3.0" omit-deep "0.3.0"
xml-reader "2.4.3" xml-reader "2.4.3"
swr@^2.0.0:
version "2.1.5"
resolved "https://registry.npmmirror.com/swr/-/swr-2.1.5.tgz#688effa719c03f6d35c66decbb0f8e79c7190399"
integrity sha512-/OhfZMcEpuz77KavXST5q6XE9nrOBOVcBLWjMT+oAE/kQHyE3PASrevXCtQDZ8aamntOfFkbVJp7Il9tNBQWrw==
dependencies:
use-sync-external-store "^1.2.0"
symbol-tree@^3.2.2, symbol-tree@^3.2.4: symbol-tree@^3.2.2, symbol-tree@^3.2.4:
version "3.2.4" version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@ -25641,6 +25702,16 @@ use-isomorphic-layout-effect@^1.1.1:
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
use-json-comparison@^1.0.3:
version "1.0.6"
resolved "https://registry.npmmirror.com/use-json-comparison/-/use-json-comparison-1.0.6.tgz#a012bbc258ce745db1f56745dc653f575226cb21"
integrity sha512-xPadt5yMRbEmVfOSGFSMqjjICrq7nLbfSH3rYIXsrtcuFX7PmbYDN/ku8ObBn3v8o/yZelO1OxUS5+5TI3+fUw==
use-media-antd-query@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/use-media-antd-query/-/use-media-antd-query-1.1.0.tgz#f083ad7e292c1c0261b6bbfaac0edc3e0920d85d"
integrity sha512-B6kKZwNV4R+l4Rl11sWO7HqOay9alzs1Vp1b4YJqjz33YxbltBCZtt/yxXxkXN9rc1S7OeEL/GbwC30Wmqhw6Q==
use-memo-one@^1.1.1: use-memo-one@^1.1.1:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99"
@ -25889,7 +25960,7 @@ vite-node@0.32.0:
vite@4.3.1: vite@4.3.1:
version "4.3.1" version "4.3.1"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.3.1.tgz#9badb1377f995632cdcf05f32103414db6fbb95a" resolved "https://registry.npmmirror.com/vite/-/vite-4.3.1.tgz#9badb1377f995632cdcf05f32103414db6fbb95a"
integrity sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg== integrity sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==
dependencies: dependencies:
esbuild "^0.17.5" esbuild "^0.17.5"
@ -25898,9 +25969,9 @@ vite@4.3.1:
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.2"
"vite@^3.0.0 || ^4.0.0", vite@^4.3.8: "vite@^3.0.0 || ^4.0.0", vite@^4.3.9:
version "4.3.9" version "4.3.9"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.3.9.tgz#db896200c0b1aa13b37cdc35c9e99ee2fdd5f96d" resolved "https://registry.npmmirror.com/vite/-/vite-4.3.9.tgz#db896200c0b1aa13b37cdc35c9e99ee2fdd5f96d"
integrity sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg== integrity sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==
dependencies: dependencies:
esbuild "^0.17.5" esbuild "^0.17.5"