perf: improve the UX of SchemaInitializer (#2666)
* perf: improve the UX of SchemaInitializer * fix: fix error of Charts block * fix: fix fields * fix: fix search * chore: avoid crash * chore: fix build * chore: avoid crash * refactor: rename SelectCollection to SearchCollections * refactor: increased code versatility for improved reusability * fix: fix Add chart * perf: workflow * refactor: remove useless code * fix: fix block template * fix: should clean search value when creating a block
This commit is contained in:
parent
8db9fda61b
commit
ff16f59908
@ -33,7 +33,7 @@ export const useMenuItem = () => {
|
|||||||
const renderItems = useRef<() => JSX.Element>(null);
|
const renderItems = useRef<() => JSX.Element>(null);
|
||||||
const shouldRerender = useRef(false);
|
const shouldRerender = useRef(false);
|
||||||
|
|
||||||
const Component = useCallback(() => {
|
const Component = useCallback(({ limitCount }) => {
|
||||||
if (!shouldRerender.current) {
|
if (!shouldRerender.current) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -43,6 +43,16 @@ export const useMenuItem = () => {
|
|||||||
return renderItems.current();
|
return renderItems.current();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (limitCount && list.current.length > limitCount) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{list.current.slice(0, limitCount).map((Com, index) => (
|
||||||
|
<Com key={index} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{list.current.map((Com, index) => (
|
{list.current.map((Com, index) => (
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { ISchema, observer, useForm } from '@formily/react';
|
import { ISchema, observer, useForm } from '@formily/react';
|
||||||
import { error, isString } from '@nocobase/utils/client';
|
import { error, isString } from '@nocobase/utils/client';
|
||||||
import { Button, Dropdown, MenuProps, Switch } from 'antd';
|
import { Button, Dropdown, Empty, Menu, MenuProps, Spin, Switch } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import React, { createContext, useCallback, useContext, useMemo, useRef, useState, useTransition } from 'react';
|
import { isEmpty } from 'lodash';
|
||||||
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useCollectMenuItem, useMenuItem } from '../hooks/useMenuItem';
|
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';
|
||||||
|
import { SearchCollections } from './SearchCollections';
|
||||||
import { useStyles } from './style';
|
import { useStyles } from './style';
|
||||||
import {
|
import {
|
||||||
SchemaInitializerButtonProps,
|
SchemaInitializerButtonProps,
|
||||||
@ -22,12 +24,212 @@ export const SchemaInitializerItemContext = createContext(null);
|
|||||||
export const SchemaInitializerButtonContext = createContext<{
|
export const SchemaInitializerButtonContext = createContext<{
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
setVisible?: (v: boolean) => void;
|
setVisible?: (v: boolean) => void;
|
||||||
searchValue?: string;
|
|
||||||
setSearchValue?: (v: string) => void;
|
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
export const SchemaInitializer = () => null;
|
export const SchemaInitializer = () => null;
|
||||||
|
|
||||||
|
const CollectionSearch = ({
|
||||||
|
onChange: _onChange,
|
||||||
|
clearValueRef,
|
||||||
|
}: {
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
clearValueRef?: React.MutableRefObject<() => void>;
|
||||||
|
}) => {
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
const onChange = useCallback(
|
||||||
|
(value) => {
|
||||||
|
setSearchValue(value);
|
||||||
|
_onChange(value);
|
||||||
|
},
|
||||||
|
[_onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (clearValueRef) {
|
||||||
|
clearValueRef.current = () => {
|
||||||
|
setSearchValue('');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SearchCollections value={searchValue} onChange={onChange} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LoadingItem = ({ loadMore }) => {
|
||||||
|
const spinRef = React.useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let root = spinRef.current;
|
||||||
|
|
||||||
|
while (root) {
|
||||||
|
if (root.classList?.contains('ant-dropdown-menu')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
root = root.parentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const item of entries) {
|
||||||
|
if (item.isIntersecting) {
|
||||||
|
return loadMore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(spinRef.current);
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [loadMore]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={spinRef}>
|
||||||
|
<Spin size="small" style={{ width: '100%' }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清除所有的 searchValue
|
||||||
|
const clearSearchValue = (items: any[]) => {
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (item._clearSearchValueRef?.current) {
|
||||||
|
item._clearSearchValueRef.current();
|
||||||
|
}
|
||||||
|
if (item.children?.length) {
|
||||||
|
clearSearchValue(item.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当存在 loadChildren 方法时,且 children 为空时,会触发懒加载
|
||||||
|
*
|
||||||
|
* 1. 如果存在 loadChildren 方法,则需要懒加载 children
|
||||||
|
* 2. 一开始先注入一个 loading 组件,当 loading 显示的时候,触发 loadChildren 方法
|
||||||
|
* 3. 每次在 loading 后显示的数目增加 minStep,直到显示完所有 children
|
||||||
|
* 4. 重置 item.label 为一个搜索框,用于筛选列表
|
||||||
|
* 5. 每次组件刷新的时候,会重复上面的步骤
|
||||||
|
* @param param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const lazyLoadChildren = ({
|
||||||
|
items,
|
||||||
|
minStep = 30,
|
||||||
|
beforeLoading,
|
||||||
|
afterLoading,
|
||||||
|
}: {
|
||||||
|
items: any[];
|
||||||
|
minStep?: number;
|
||||||
|
beforeLoading?: () => void;
|
||||||
|
afterLoading?: ({ currentCount }) => void;
|
||||||
|
}) => {
|
||||||
|
if (isEmpty(items)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addLoading = (item: any, searchValue: string) => {
|
||||||
|
if (isEmpty(item.children)) {
|
||||||
|
item.children = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
item.children.push({
|
||||||
|
key: `${item.key}-loading`,
|
||||||
|
label: (
|
||||||
|
<LoadingItem
|
||||||
|
loadMore={() => {
|
||||||
|
beforeLoading?.();
|
||||||
|
item._allChildren = item.loadChildren({ searchValue });
|
||||||
|
item._count += minStep;
|
||||||
|
item.children = item._allChildren?.slice(0, item._count);
|
||||||
|
if (item.children?.length < item._allChildren?.length) {
|
||||||
|
addLoading(item, searchValue);
|
||||||
|
}
|
||||||
|
afterLoading?.({ currentCount: item._count });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (!item) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.loadChildren && isEmpty(item.children)) {
|
||||||
|
item._count = 0;
|
||||||
|
item._clearSearchValueRef = {};
|
||||||
|
item.label = (
|
||||||
|
<CollectionSearch
|
||||||
|
clearValueRef={item._clearSearchValueRef}
|
||||||
|
onChange={(value) => {
|
||||||
|
item._count = minStep;
|
||||||
|
beforeLoading?.();
|
||||||
|
item._allChildren = item.loadChildren({ searchValue: value });
|
||||||
|
|
||||||
|
if (isEmpty(item._allChildren)) {
|
||||||
|
item.children = [
|
||||||
|
{
|
||||||
|
key: 'empty',
|
||||||
|
label: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
item.children = item._allChildren?.slice(0, item._count);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.children?.length < item._allChildren?.length) {
|
||||||
|
addLoading(item, value);
|
||||||
|
}
|
||||||
|
afterLoading?.({ currentCount: item._count });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 通过 loading 加载新数据
|
||||||
|
addLoading(item, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
lazyLoadChildren({
|
||||||
|
items: item.children,
|
||||||
|
minStep,
|
||||||
|
beforeLoading,
|
||||||
|
afterLoading,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const MenuWithLazyLoadChildren = ({ items: _items, style, clean, component: Component }) => {
|
||||||
|
const [items, setItems] = useState(_items);
|
||||||
|
const currentCountRef = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setItems(_items);
|
||||||
|
}, [_items]);
|
||||||
|
|
||||||
|
lazyLoadChildren({
|
||||||
|
items,
|
||||||
|
beforeLoading: () => {
|
||||||
|
clean();
|
||||||
|
},
|
||||||
|
afterLoading: ({ currentCount }) => {
|
||||||
|
currentCountRef.current = currentCount;
|
||||||
|
setItems([...items]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* 用于收集 menu item */}
|
||||||
|
<Component limitCount={currentCountRef.current} />
|
||||||
|
<Menu style={style} items={items} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
SchemaInitializer.Button = observer(
|
SchemaInitializer.Button = observer(
|
||||||
(props: SchemaInitializerButtonProps) => {
|
(props: SchemaInitializerButtonProps) => {
|
||||||
const {
|
const {
|
||||||
@ -46,26 +248,19 @@ 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 { Component: CollectComponent, getMenuItem, clean } = useMenuItem();
|
||||||
const [searchValue, setSearchValue] = useState('');
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
const menuItems = useRef([]);
|
const menuItems = useRef([]);
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
|
|
||||||
const changeMenu = (v: boolean) => {
|
const changeMenu = (v: boolean) => {
|
||||||
// 这里是为了防止当鼠标快速滑过时,终止菜单的渲染,防止卡顿
|
setVisible(v);
|
||||||
startTransition(() => {
|
|
||||||
setVisible(v);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!designable && props.designable !== true) {
|
if (!designable && props.designable !== true) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttonDom = component ? (
|
const buttonDom = component || (
|
||||||
component
|
|
||||||
) : (
|
|
||||||
<Button
|
<Button
|
||||||
type={'dashed'}
|
type={'dashed'}
|
||||||
style={{
|
style={{
|
||||||
@ -131,27 +326,30 @@ SchemaInitializer.Button = observer(
|
|||||||
}
|
}
|
||||||
if (item.type === 'itemGroup') {
|
if (item.type === 'itemGroup') {
|
||||||
const label = isString(item.title) ? compile(item.title) : item.title;
|
const label = isString(item.title) ? compile(item.title) : item.title;
|
||||||
return (
|
return {
|
||||||
!!item.children?.length && {
|
type: 'group',
|
||||||
type: 'group',
|
key: item.key || `item-group-${indexA}`,
|
||||||
key: item.key || `item-group-${indexA}`,
|
label,
|
||||||
label,
|
title: label,
|
||||||
title: label,
|
style: item.style,
|
||||||
children: renderItems(item.children),
|
loadChildren: isEmpty(item.children)
|
||||||
}
|
? ({ searchValue } = { searchValue: '' }) => renderItems(item.loadChildren?.({ searchValue }) || [])
|
||||||
);
|
: null,
|
||||||
|
children: isEmpty(item.children) ? [] : renderItems(item.children),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (item.type === 'subMenu') {
|
if (item.type === 'subMenu') {
|
||||||
const label = compile(item.title);
|
const label = compile(item.title);
|
||||||
return (
|
return {
|
||||||
!!item.children?.length && {
|
key: item.key || `item-group-${indexA}`,
|
||||||
key: item.key || `item-group-${indexA}`,
|
label,
|
||||||
label,
|
title: label,
|
||||||
title: label,
|
popupClassName: styles.nbMenuItemSubMenu,
|
||||||
popupClassName: styles.nbMenuItemSubMenu,
|
loadChildren: isEmpty(item.children)
|
||||||
children: renderItems(item.children),
|
? ({ searchValue } = { searchValue: '' }) => renderItems(item.loadChildren?.({ searchValue }) || [])
|
||||||
}
|
: null,
|
||||||
);
|
children: isEmpty(item.children) ? [] : renderItems(item.children),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -161,28 +359,38 @@ SchemaInitializer.Button = observer(
|
|||||||
menuItems.current = renderItems(items);
|
menuItems.current = renderItems(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dropdownRender = () => (
|
||||||
|
<MenuWithLazyLoadChildren
|
||||||
|
style={{
|
||||||
|
maxHeight: '50vh',
|
||||||
|
overflowY: 'auto',
|
||||||
|
}}
|
||||||
|
items={menuItems.current}
|
||||||
|
clean={clean}
|
||||||
|
component={CollectComponent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible === false) {
|
||||||
|
clearSearchValue(menuItems.current);
|
||||||
|
}
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SchemaInitializerButtonContext.Provider value={{ visible, setVisible, searchValue, setSearchValue }}>
|
<SchemaInitializerButtonContext.Provider value={{ visible, setVisible }}>
|
||||||
{visible ? <CollectionComponent /> : null}
|
{visible ? <CollectComponent /> : null}
|
||||||
<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`}
|
||||||
open={visible}
|
open={visible}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
// 如果不清空输入框的值,那么下次打开的时候会出现上次输入的值
|
|
||||||
setSearchValue('');
|
|
||||||
changeMenu(open);
|
changeMenu(open);
|
||||||
}}
|
}}
|
||||||
menu={{
|
dropdownRender={dropdownRender}
|
||||||
style: {
|
|
||||||
maxHeight: '50vh',
|
|
||||||
overflowY: 'auto',
|
|
||||||
},
|
|
||||||
items: menuItems.current,
|
|
||||||
}}
|
|
||||||
{...dropdown}
|
{...dropdown}
|
||||||
>
|
>
|
||||||
{component ? component : buttonDom}
|
{component || buttonDom}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</SchemaInitializerButtonContext.Provider>
|
</SchemaInitializerButtonContext.Provider>
|
||||||
);
|
);
|
||||||
@ -197,15 +405,14 @@ SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
|
|||||||
const { items = [], children = info?.title, icon, onClick } = props;
|
const { items = [], children = info?.title, icon, onClick } = props;
|
||||||
const { collectMenuItem } = useCollectMenuItem();
|
const { collectMenuItem } = useCollectMenuItem();
|
||||||
|
|
||||||
if (!collectMenuItem) {
|
if (process.env.NODE_ENV !== 'production' && !collectMenuItem) {
|
||||||
error('SchemaInitializer.Item: collectMenuItem is undefined, please check the context');
|
throw new Error('SchemaInitializer.Item: collectMenuItem is undefined, please check the context');
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items?.length > 0) {
|
if (items?.length > 0) {
|
||||||
const renderMenuItem = (items: SchemaInitializerItemOptions[], parentKey: string) => {
|
const renderMenuItem = (items: SchemaInitializerItemOptions[], parentKey: string) => {
|
||||||
if (!items?.length) {
|
if (!items?.length) {
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
return items.map((item, indexA) => {
|
return items.map((item, indexA) => {
|
||||||
if (item.type === 'divider') {
|
if (item.type === 'divider') {
|
||||||
@ -220,8 +427,12 @@ SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
|
|||||||
label,
|
label,
|
||||||
title: label,
|
title: label,
|
||||||
className: styles.nbMenuItemGroup,
|
className: styles.nbMenuItemGroup,
|
||||||
children: renderMenuItem(item.children, key),
|
loadChildren: isEmpty(item.children)
|
||||||
} as MenuProps['items'][0];
|
? ({ searchValue } = { searchValue: '' }) =>
|
||||||
|
renderMenuItem(item.loadChildren?.({ searchValue }) || [], key)
|
||||||
|
: null,
|
||||||
|
children: isEmpty(item.children) ? [] : renderMenuItem(item.children, key),
|
||||||
|
} as MenuProps['items'][0] & { loadChildren?: ({ searchValue }?: { searchValue: string }) => any[] };
|
||||||
}
|
}
|
||||||
if (item.type === 'subMenu') {
|
if (item.type === 'subMenu') {
|
||||||
const label = compile(item.title);
|
const label = compile(item.title);
|
||||||
@ -230,8 +441,12 @@ SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
|
|||||||
key,
|
key,
|
||||||
label,
|
label,
|
||||||
title: label,
|
title: label,
|
||||||
children: renderMenuItem(item.children, key),
|
loadChildren: isEmpty(item.children)
|
||||||
};
|
? ({ searchValue } = { searchValue: '' }) =>
|
||||||
|
renderMenuItem(item.loadChildren?.({ searchValue }) || [], key)
|
||||||
|
: null,
|
||||||
|
children: isEmpty(item.children) ? [] : renderMenuItem(item.children, key),
|
||||||
|
} as MenuProps['items'][0] & { loadChildren?: ({ searchValue }?: { searchValue: string }) => any[] };
|
||||||
}
|
}
|
||||||
const label = compile(item.title);
|
const label = compile(item.title);
|
||||||
return {
|
return {
|
||||||
@ -239,7 +454,6 @@ SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
|
|||||||
label,
|
label,
|
||||||
title: label,
|
title: label,
|
||||||
onClick: (info) => {
|
onClick: (info) => {
|
||||||
item?.clearKeywords?.();
|
|
||||||
if (item.onClick) {
|
if (item.onClick) {
|
||||||
item.onClick({ ...info, item });
|
item.onClick({ ...info, item });
|
||||||
} else {
|
} else {
|
||||||
@ -268,7 +482,6 @@ SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
|
|||||||
title: label,
|
title: label,
|
||||||
icon: typeof icon === 'string' ? <Icon type={icon as string} /> : icon,
|
icon: typeof icon === 'string' ? <Icon type={icon as string} /> : icon,
|
||||||
onClick: (opts) => {
|
onClick: (opts) => {
|
||||||
info?.clearKeywords?.();
|
|
||||||
onClick({ ...opts, item: info });
|
onClick({ ...opts, item: info });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@ import { Divider, Input } from 'antd';
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export const SelectCollection = ({ value: outValue, onChange }) => {
|
export const SearchCollections = ({ value: outValue, onChange }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [value, setValue] = useState<string>(outValue);
|
const [value, setValue] = useState<string>(outValue);
|
||||||
const inputRef = React.useRef(null);
|
const inputRef = React.useRef(null);
|
@ -22,11 +22,13 @@ interface ItemCommonOptions {
|
|||||||
interface ItemGroupOptions extends ItemCommonOptions {
|
interface ItemGroupOptions extends ItemCommonOptions {
|
||||||
type: 'itemGroup';
|
type: 'itemGroup';
|
||||||
children?: SchemaInitializerItemOptions[];
|
children?: SchemaInitializerItemOptions[];
|
||||||
|
loadChildren?: ({ searchValue }?: { searchValue: string }) => SchemaInitializerItemOptions[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SubMenuOptions extends ItemCommonOptions {
|
interface SubMenuOptions extends ItemCommonOptions {
|
||||||
type: 'subMenu';
|
type: 'subMenu';
|
||||||
children?: SchemaInitializerItemOptions[];
|
children?: SchemaInitializerItemOptions[];
|
||||||
|
loadChildren?: ({ searchValue }?: { searchValue: string }) => SchemaInitializerItemOptions[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type BreakFn = (s: ISchema) => boolean;
|
type BreakFn = (s: ISchema) => boolean;
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
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 { error } from '@nocobase/utils/client';
|
import { useContext, useMemo } from 'react';
|
||||||
import _ from 'lodash';
|
|
||||||
import React, { useCallback, useContext, useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { BlockRequestContext, SchemaInitializerButtonContext, SchemaInitializerItemOptions } from '../';
|
import { BlockRequestContext, SchemaInitializerItemOptions } from '..';
|
||||||
import { FieldOptions, useCollection, useCollectionManager } from '../collection-manager';
|
import { CollectionFieldOptions, 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';
|
||||||
import { useSchemaTemplateManager } from '../schema-templates';
|
import { useSchemaTemplateManager } from '../schema-templates';
|
||||||
import { SelectCollection } from './SelectCollection';
|
|
||||||
|
|
||||||
export const itemsMerge = (items1) => {
|
export const itemsMerge = (items1) => {
|
||||||
return items1;
|
return items1;
|
||||||
@ -834,120 +831,23 @@ 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 { searchValue, setSearchValue } = useContext(SchemaInitializerButtonContext);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
const onChange = useCallback(_.debounce(setSearchValue, 300), [setSearchValue]);
|
|
||||||
|
|
||||||
if (!setSearchValue) {
|
|
||||||
error('useCollectionDataSourceItems: please use in SchemaInitializerButtonContext and provide setSearchValue');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearKeywords = () => {
|
|
||||||
setSearchValue('');
|
|
||||||
};
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: 'tableBlock',
|
key: 'tableBlock',
|
||||||
type: 'itemGroup',
|
type: 'itemGroup',
|
||||||
title: React.createElement(SelectCollection, {
|
title: null,
|
||||||
value: searchValue,
|
children: [],
|
||||||
onChange,
|
loadChildren: ({ searchValue } = { searchValue: '' }) => {
|
||||||
}),
|
return getChildren({
|
||||||
children: collections
|
collections,
|
||||||
?.filter((item) => {
|
getCollectionFields,
|
||||||
if (item.inherit) {
|
componentName,
|
||||||
return false;
|
searchValue,
|
||||||
}
|
getTemplatesByCollection,
|
||||||
const fields = getCollectionFields(item.name);
|
t,
|
||||||
if (item.autoGenId === false && !fields.find((v) => v.primaryKey)) {
|
});
|
||||||
return false;
|
},
|
||||||
} else if (
|
|
||||||
['Kanban', 'FormItem'].includes(componentName) &&
|
|
||||||
((item.template === 'view' && !item.writableView) || item.template === 'sql')
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
} else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
if (!item.title) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
item.title.toUpperCase().includes(searchValue.toUpperCase()) && !(item?.isThrough && item?.autoCreate)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
?.map((item, index) => {
|
|
||||||
const templates = getTemplatesByCollection(item.name).filter((template) => {
|
|
||||||
return (
|
|
||||||
componentName &&
|
|
||||||
template.componentName === componentName &&
|
|
||||||
(!template.resourceName || template.resourceName === item.name)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (!templates.length) {
|
|
||||||
return {
|
|
||||||
type: 'item',
|
|
||||||
name: item.name,
|
|
||||||
title: item.title,
|
|
||||||
clearKeywords,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
key: `${componentName}_table_subMenu_${index}`,
|
|
||||||
type: 'subMenu',
|
|
||||||
name: `${item.name}_${index}`,
|
|
||||||
title: item.title,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'item',
|
|
||||||
name: item.name,
|
|
||||||
title: t('Blank block'),
|
|
||||||
clearKeywords,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'divider',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: `${componentName}_table_subMenu_${index}_copy`,
|
|
||||||
type: 'subMenu',
|
|
||||||
name: 'copy',
|
|
||||||
title: t('Duplicate template'),
|
|
||||||
children: templates.map((template) => {
|
|
||||||
const templateName =
|
|
||||||
template?.componentName === 'FormItem' ? `${template?.name} ${t('(Fields only)')}` : template?.name;
|
|
||||||
return {
|
|
||||||
type: 'item',
|
|
||||||
mode: 'copy',
|
|
||||||
name: item.name,
|
|
||||||
template,
|
|
||||||
clearKeywords,
|
|
||||||
title: templateName || t('Untitled'),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: `${componentName}_table_subMenu_${index}_ref`,
|
|
||||||
type: 'subMenu',
|
|
||||||
name: 'ref',
|
|
||||||
title: t('Reference template'),
|
|
||||||
children: templates.map((template) => {
|
|
||||||
const templateName =
|
|
||||||
template?.componentName === 'FormItem' ? `${template?.name} ${t('(Fields only)')}` : template?.name;
|
|
||||||
return {
|
|
||||||
type: 'item',
|
|
||||||
mode: 'reference',
|
|
||||||
clearKeywords,
|
|
||||||
name: item.name,
|
|
||||||
template,
|
|
||||||
title: templateName || t('Untitled'),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@ -1878,3 +1778,108 @@ export const createKanbanBlockSchema = (options) => {
|
|||||||
};
|
};
|
||||||
return schema;
|
return schema;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getChildren = ({
|
||||||
|
collections,
|
||||||
|
getCollectionFields,
|
||||||
|
componentName,
|
||||||
|
searchValue,
|
||||||
|
getTemplatesByCollection,
|
||||||
|
t,
|
||||||
|
}: {
|
||||||
|
collections: any[];
|
||||||
|
getCollectionFields: (name: any) => CollectionFieldOptions[];
|
||||||
|
componentName: any;
|
||||||
|
searchValue: string;
|
||||||
|
getTemplatesByCollection: (collectionName: string, resourceName?: string) => any;
|
||||||
|
t;
|
||||||
|
}) => {
|
||||||
|
return collections
|
||||||
|
?.filter((item) => {
|
||||||
|
if (item.inherit) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const fields = getCollectionFields(item.name);
|
||||||
|
if (item.autoGenId === false && !fields.find((v) => v.primaryKey)) {
|
||||||
|
return false;
|
||||||
|
} else if (
|
||||||
|
['Kanban', 'FormItem'].includes(componentName) &&
|
||||||
|
((item.template === 'view' && !item.writableView) || item.template === 'sql')
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
} else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
if (!item.title) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return item.title.toUpperCase().includes(searchValue.toUpperCase()) && !(item?.isThrough && item?.autoCreate);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
?.map((item, index) => {
|
||||||
|
const templates = getTemplatesByCollection(item.name).filter((template) => {
|
||||||
|
return (
|
||||||
|
componentName &&
|
||||||
|
template.componentName === componentName &&
|
||||||
|
(!template.resourceName || template.resourceName === item.name)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (!templates.length) {
|
||||||
|
return {
|
||||||
|
type: 'item',
|
||||||
|
name: item.name,
|
||||||
|
title: item.title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: `${componentName}_table_subMenu_${index}`,
|
||||||
|
type: 'subMenu',
|
||||||
|
name: `${item.name}_${index}`,
|
||||||
|
title: item.title,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
name: item.name,
|
||||||
|
title: t('Blank block'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: `${componentName}_table_subMenu_${index}_copy`,
|
||||||
|
type: 'subMenu',
|
||||||
|
name: 'copy',
|
||||||
|
title: t('Duplicate template'),
|
||||||
|
children: templates.map((template) => {
|
||||||
|
const templateName =
|
||||||
|
template?.componentName === 'FormItem' ? `${template?.name} ${t('(Fields only)')}` : template?.name;
|
||||||
|
return {
|
||||||
|
type: 'item',
|
||||||
|
mode: 'copy',
|
||||||
|
name: item.name,
|
||||||
|
template,
|
||||||
|
title: templateName || t('Untitled'),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: `${componentName}_table_subMenu_${index}_ref`,
|
||||||
|
type: 'subMenu',
|
||||||
|
name: 'ref',
|
||||||
|
title: t('Reference template'),
|
||||||
|
children: templates.map((template) => {
|
||||||
|
const templateName =
|
||||||
|
template?.componentName === 'FormItem' ? `${template?.name} ${t('(Fields only)')}` : template?.name;
|
||||||
|
return {
|
||||||
|
type: 'item',
|
||||||
|
mode: 'reference',
|
||||||
|
name: item.name,
|
||||||
|
template,
|
||||||
|
title: templateName || t('Untitled'),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -151,7 +151,7 @@ export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNeste
|
|||||||
const [isPending, startTransition] = useReactTransition();
|
const [isPending, startTransition] = useReactTransition();
|
||||||
|
|
||||||
const changeMenu = (v: boolean) => {
|
const changeMenu = (v: boolean) => {
|
||||||
// 这里是为了防止当鼠标快速滑过时,终止菜单的渲染,防止卡顿
|
// 当鼠标快速滑过时,终止菜单的渲染,防止卡顿
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
setVisible(v);
|
setVisible(v);
|
||||||
});
|
});
|
||||||
|
@ -45,6 +45,7 @@ const ChartsProvider = React.memo((props) => {
|
|||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const items = useContext<any>(SchemaInitializerContext);
|
const items = useContext<any>(SchemaInitializerContext);
|
||||||
const children = items.BlockInitializers.items[0].children;
|
const children = items.BlockInitializers.items[0].children;
|
||||||
|
|
||||||
if (children) {
|
if (children) {
|
||||||
const hasChartItem = children.some((child) => child?.component === 'ChartBlockInitializer');
|
const hasChartItem = children.some((child) => child?.component === 'ChartBlockInitializer');
|
||||||
if (!hasChartItem) {
|
if (!hasChartItem) {
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
import { SchemaInitializerButtonContext, useDesignable } from '@nocobase/client';
|
import { SchemaInitializerButtonContext, useDesignable } from '@nocobase/client';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { ChartRendererProvider } from '../renderer';
|
|
||||||
import { ChartConfigContext, ChartConfigCurrent, ChartConfigure } from '../configure/ChartConfigure';
|
import { ChartConfigContext, ChartConfigCurrent, ChartConfigure } from '../configure/ChartConfigure';
|
||||||
|
import { ChartRendererProvider } from '../renderer';
|
||||||
|
|
||||||
export const ChartV2Block: React.FC = (props) => {
|
export const ChartV2Block: React.FC = (props) => {
|
||||||
const { insertAdjacent } = useDesignable();
|
const { insertAdjacent } = useDesignable();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [current, setCurrent] = useState<ChartConfigCurrent>({} as any);
|
const [current, setCurrent] = useState<ChartConfigCurrent>({} as any);
|
||||||
const [initialVisible, setInitialVisible] = useState(false);
|
const [initialVisible, setInitialVisible] = useState(false);
|
||||||
const [searchValue, setSearchValue] = useState('');
|
|
||||||
return (
|
return (
|
||||||
<SchemaInitializerButtonContext.Provider
|
<SchemaInitializerButtonContext.Provider value={{ visible: initialVisible, setVisible: setInitialVisible }}>
|
||||||
value={{ visible: initialVisible, setVisible: setInitialVisible, searchValue, setSearchValue }}
|
|
||||||
>
|
|
||||||
<ChartConfigContext.Provider value={{ visible, setVisible, current, setCurrent }}>
|
<ChartConfigContext.Provider value={{ visible, setVisible, current, setCurrent }}>
|
||||||
{props.children}
|
{props.children}
|
||||||
<ChartRendererProvider {...current.field?.decoratorProps}>
|
<ChartRendererProvider {...current.field?.decoratorProps}>
|
||||||
|
@ -3,8 +3,8 @@ import { ISchema } from '@formily/react';
|
|||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { SchemaInitializer, useACLRoleContext, useCollectionDataSourceItems } from '@nocobase/client';
|
import { SchemaInitializer, useACLRoleContext, useCollectionDataSourceItems } from '@nocobase/client';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useChartsTranslation } from '../locale';
|
|
||||||
import { ChartConfigContext } from '../configure/ChartConfigure';
|
import { ChartConfigContext } from '../configure/ChartConfigure';
|
||||||
|
import { useChartsTranslation } from '../locale';
|
||||||
|
|
||||||
const itemWrap = SchemaInitializer.itemWrap;
|
const itemWrap = SchemaInitializer.itemWrap;
|
||||||
const ConfigureButton = itemWrap((props) => {
|
const ConfigureButton = itemWrap((props) => {
|
||||||
@ -24,23 +24,28 @@ export const ChartInitializers = () => {
|
|||||||
const { t } = useChartsTranslation();
|
const { t } = useChartsTranslation();
|
||||||
const collections = useCollectionDataSourceItems('Chart');
|
const collections = useCollectionDataSourceItems('Chart');
|
||||||
const { allowAll, parseAction } = useACLRoleContext();
|
const { allowAll, parseAction } = useACLRoleContext();
|
||||||
const children = collections[0].children
|
|
||||||
.filter((item) => {
|
if (collections[0].loadChildren) {
|
||||||
if (allowAll) {
|
const originalLoadChildren = collections[0].loadChildren;
|
||||||
return true;
|
collections[0].loadChildren = ({ searchValue }) => {
|
||||||
}
|
const children = originalLoadChildren({ searchValue });
|
||||||
const params = parseAction(`${item.name}:list`);
|
const result = children
|
||||||
return params;
|
.filter((item) => {
|
||||||
})
|
if (allowAll) {
|
||||||
.map((item) => ({
|
return true;
|
||||||
...item,
|
}
|
||||||
component: ConfigureButton,
|
const params = parseAction(`${item.name}:list`);
|
||||||
}));
|
return params;
|
||||||
if (!children.length) {
|
})
|
||||||
// Leave a blank item to show the filter component
|
.map((item) => ({
|
||||||
children.push({} as any);
|
...item,
|
||||||
|
component: ConfigureButton,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
collections[0].children = children;
|
|
||||||
return (
|
return (
|
||||||
<SchemaInitializer.Button
|
<SchemaInitializer.Button
|
||||||
icon={'PlusOutlined'}
|
icon={'PlusOutlined'}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { cx, css, useAPIClient, useCompile } from '@nocobase/client';
|
import { css, useAPIClient, useCompile } from '@nocobase/client';
|
||||||
import { Button, Dropdown, MenuProps } from 'antd';
|
import { Button, Dropdown, MenuProps } from 'antd';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useFlowContext } from './FlowContext';
|
import { useFlowContext } from './FlowContext';
|
||||||
@ -98,7 +98,7 @@ export function AddButton({ upstream, branchIndex = null }: AddButtonProps) {
|
|||||||
menu={menu}
|
menu={menu}
|
||||||
disabled={workflow.executed}
|
disabled={workflow.executed}
|
||||||
overlayClassName={css`
|
overlayClassName={css`
|
||||||
.ant-dropdown-menu-root{
|
.ant-dropdown-menu-root {
|
||||||
max-height: 30em;
|
max-height: 30em;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
import { FormLayout } from '@formily/antd-v5';
|
||||||
import { createForm } from '@formily/core';
|
import { createForm } from '@formily/core';
|
||||||
import { FormProvider, ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
|
import { FormProvider, ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
|
||||||
import { FormLayout } from '@formily/antd-v5';
|
|
||||||
import { Alert, Button, Modal, Space } from 'antd';
|
import { Alert, Button, Modal, Space } from 'antd';
|
||||||
|
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -18,22 +18,23 @@ import {
|
|||||||
SchemaSettings,
|
SchemaSettings,
|
||||||
VariableScopeProvider,
|
VariableScopeProvider,
|
||||||
gridRowColWrap,
|
gridRowColWrap,
|
||||||
|
useCollectionManager,
|
||||||
useCompile,
|
useCompile,
|
||||||
useFormBlockContext,
|
useFormBlockContext,
|
||||||
useSchemaOptionsContext,
|
useSchemaOptionsContext,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { Registry, lodash } from '@nocobase/utils/client';
|
import { Registry, lodash } from '@nocobase/utils/client';
|
||||||
import { instructions, useAvailableUpstreams, useNodeContext } from '..';
|
import { instructions, useAvailableUpstreams, useNodeContext } from '..';
|
||||||
import { JOB_STATUS } from '../../constants';
|
|
||||||
import { useFlowContext } from '../../FlowContext';
|
import { useFlowContext } from '../../FlowContext';
|
||||||
|
import { JOB_STATUS } from '../../constants';
|
||||||
import { NAMESPACE, lang } from '../../locale';
|
import { NAMESPACE, lang } from '../../locale';
|
||||||
import { useTrigger } from '../../triggers';
|
import { useTrigger } from '../../triggers';
|
||||||
|
import { useWorkflowVariableOptions } from '../../variable';
|
||||||
import { DetailsBlockProvider } from './DetailsBlockProvider';
|
import { DetailsBlockProvider } from './DetailsBlockProvider';
|
||||||
import { FormBlockProvider } from './FormBlockProvider';
|
import { FormBlockProvider } from './FormBlockProvider';
|
||||||
import createRecordForm from './forms/create';
|
import createRecordForm from './forms/create';
|
||||||
import customRecordForm from './forms/custom';
|
import customRecordForm from './forms/custom';
|
||||||
import updateRecordForm from './forms/update';
|
import updateRecordForm from './forms/update';
|
||||||
import { useWorkflowVariableOptions } from '../../variable';
|
|
||||||
|
|
||||||
type ValueOf<T> = T[keyof T];
|
type ValueOf<T> = T[keyof T];
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ export type FormType = {
|
|||||||
export type ManualFormType = {
|
export type ManualFormType = {
|
||||||
title: string;
|
title: string;
|
||||||
config: {
|
config: {
|
||||||
useInitializer: () => SchemaInitializerItemOptions;
|
useInitializer: ({ collections }?: { collections: any[] }) => SchemaInitializerItemOptions;
|
||||||
initializers?: {
|
initializers?: {
|
||||||
[key: string]: React.FC;
|
[key: string]: React.FC;
|
||||||
};
|
};
|
||||||
@ -108,6 +109,7 @@ function SimpleDesigner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AddBlockButton(props: any) {
|
function AddBlockButton(props: any) {
|
||||||
|
const { collections } = useCollectionManager();
|
||||||
const current = useNodeContext();
|
const current = useNodeContext();
|
||||||
const nodes = useAvailableUpstreams(current);
|
const nodes = useAvailableUpstreams(current);
|
||||||
const triggerInitializers = [useTriggerInitializers()].filter(Boolean);
|
const triggerInitializers = [useTriggerInitializers()].filter(Boolean);
|
||||||
@ -146,7 +148,7 @@ function AddBlockButton(props: any) {
|
|||||||
title: '{{t("Form")}}',
|
title: '{{t("Form")}}',
|
||||||
children: Array.from(manualFormTypes.getValues()).map((item: ManualFormType) => {
|
children: Array.from(manualFormTypes.getValues()).map((item: ManualFormType) => {
|
||||||
const { useInitializer: getInitializer } = item.config;
|
const { useInitializer: getInitializer } = item.config;
|
||||||
return getInitializer();
|
return getInitializer({ collections });
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -171,31 +173,32 @@ function AssignedFieldValues() {
|
|||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const scope = useWorkflowVariableOptions({ fieldNames: { label: 'title', value: 'name' } });
|
const scope = useWorkflowVariableOptions({ fieldNames: { label: 'title', value: 'name' } });
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [initialSchema, setInitialSchema] = useState(fieldSchema?.['x-action-settings']?.assignedValues?.schema ?? {
|
const [initialSchema, setInitialSchema] = useState(
|
||||||
type: 'void',
|
fieldSchema?.['x-action-settings']?.assignedValues?.schema ?? {
|
||||||
'x-component': 'Grid',
|
type: 'void',
|
||||||
'x-initializer': 'CustomFormItemInitializers',
|
'x-component': 'Grid',
|
||||||
properties: {},
|
'x-initializer': 'CustomFormItemInitializers',
|
||||||
});
|
properties: {},
|
||||||
|
},
|
||||||
|
);
|
||||||
const [schema, setSchema] = useState<Schema>(null);
|
const [schema, setSchema] = useState<Schema>(null);
|
||||||
const { components } = useSchemaOptionsContext();
|
const { components } = useSchemaOptionsContext();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSchema(new Schema({
|
setSchema(
|
||||||
properties: {
|
new Schema({
|
||||||
grid: initialSchema
|
properties: {
|
||||||
},
|
grid: initialSchema,
|
||||||
}));
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
}, [initialSchema]);
|
}, [initialSchema]);
|
||||||
const form = useMemo(
|
const form = useMemo(() => {
|
||||||
() => {
|
const initialValues = fieldSchema?.['x-action-settings']?.assignedValues?.values;
|
||||||
const initialValues = fieldSchema?.['x-action-settings']?.assignedValues?.values;
|
return createForm({
|
||||||
return createForm({
|
initialValues: lodash.cloneDeep(initialValues),
|
||||||
initialValues: lodash.cloneDeep(initialValues),
|
values: lodash.cloneDeep(initialValues),
|
||||||
values: lodash.cloneDeep(initialValues),
|
});
|
||||||
});
|
}, []);
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const title = t('Assign field values');
|
const title = t('Assign field values');
|
||||||
|
|
||||||
@ -220,9 +223,7 @@ function AssignedFieldValues() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SchemaSettings.Item onClick={() => setOpen(true)}>
|
<SchemaSettings.Item onClick={() => setOpen(true)}>{title}</SchemaSettings.Item>
|
||||||
{title}
|
|
||||||
</SchemaSettings.Item>
|
|
||||||
<Modal
|
<Modal
|
||||||
width={'50%'}
|
width={'50%'}
|
||||||
title={title}
|
title={title}
|
||||||
@ -231,14 +232,18 @@ function AssignedFieldValues() {
|
|||||||
footer={
|
footer={
|
||||||
<Space>
|
<Space>
|
||||||
<Button onClick={onCancel}>{t('Cancel')}</Button>
|
<Button onClick={onCancel}>{t('Cancel')}</Button>
|
||||||
<Button type="primary" onClick={onSubmit}>{t('Submit')}</Button>
|
<Button type="primary" onClick={onSubmit}>
|
||||||
|
{t('Submit')}
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<VariableScopeProvider scope={scope}>
|
<VariableScopeProvider scope={scope}>
|
||||||
<FormProvider form={form}>
|
<FormProvider form={form}>
|
||||||
<FormLayout layout={'vertical'}>
|
<FormLayout layout={'vertical'}>
|
||||||
<Alert message={lang('Values preset in this form will override user submitted ones when continue or reject.')} />
|
<Alert
|
||||||
|
message={lang('Values preset in this form will override user submitted ones when continue or reject.')}
|
||||||
|
/>
|
||||||
<br />
|
<br />
|
||||||
{open && schema && (
|
{open && schema && (
|
||||||
<SchemaComponentContext.Provider
|
<SchemaComponentContext.Provider
|
||||||
@ -246,7 +251,7 @@ function AssignedFieldValues() {
|
|||||||
...ctx,
|
...ctx,
|
||||||
refresh() {
|
refresh() {
|
||||||
setInitialSchema(lodash.get(schema.toJSON(), 'properties.grid'));
|
setInitialSchema(lodash.get(schema.toJSON(), 'properties.grid'));
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SchemaComponent schema={schema} components={components} />
|
<SchemaComponent schema={schema} components={components} />
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useFieldSchema } from '@formily/react';
|
|
||||||
|
|
||||||
import { GeneralSchemaDesigner, SchemaSettings, useCollection, useCollectionManager } from '@nocobase/client';
|
import { GeneralSchemaDesigner, SchemaSettings, useCollection } from '@nocobase/client';
|
||||||
|
|
||||||
import { NAMESPACE } from '../../../locale';
|
import { NAMESPACE } from '../../../locale';
|
||||||
import { findSchema } from '../utils';
|
|
||||||
import { ManualFormType } from '../SchemaConfig';
|
|
||||||
import { FormBlockInitializer } from '../FormBlockInitializer';
|
import { FormBlockInitializer } from '../FormBlockInitializer';
|
||||||
|
import { ManualFormType } from '../SchemaConfig';
|
||||||
|
import { findSchema } from '../utils';
|
||||||
|
|
||||||
function CreateFormDesigner() {
|
function CreateFormDesigner() {
|
||||||
const { name, title } = useCollection();
|
const { name, title } = useCollection();
|
||||||
@ -30,26 +29,37 @@ function CreateFormDesigner() {
|
|||||||
export default {
|
export default {
|
||||||
title: `{{t("Create record form", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Create record form", { ns: "${NAMESPACE}" })}}`,
|
||||||
config: {
|
config: {
|
||||||
useInitializer() {
|
useInitializer({ collections }) {
|
||||||
const { collections } = useCollectionManager();
|
|
||||||
return {
|
return {
|
||||||
key: 'createRecordForm',
|
key: 'createRecordForm',
|
||||||
type: 'subMenu',
|
type: 'subMenu',
|
||||||
title: `{{t("Create record form", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Create record form", { ns: "${NAMESPACE}" })}}`,
|
||||||
children: collections
|
children: [
|
||||||
.filter((item) => !item.hidden)
|
{
|
||||||
.map((item) => ({
|
key: 'createRecordForm-child',
|
||||||
key: `createForm-${item.name}`,
|
type: 'itemGroup',
|
||||||
type: 'item',
|
style: {
|
||||||
title: item.title,
|
maxHeight: '48vh',
|
||||||
schema: {
|
overflowY: 'auto',
|
||||||
collection: item.name,
|
|
||||||
title: `{{t("Create record", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
formType: 'create',
|
|
||||||
'x-designer': 'CreateFormDesigner',
|
|
||||||
},
|
},
|
||||||
component: FormBlockInitializer,
|
loadChildren: ({ searchValue }) => {
|
||||||
})),
|
return collections
|
||||||
|
.filter((item) => !item.hidden && item.title.includes(searchValue))
|
||||||
|
.map((item) => ({
|
||||||
|
key: `createRecordForm-child-${item.name}`,
|
||||||
|
type: 'item',
|
||||||
|
title: item.title,
|
||||||
|
schema: {
|
||||||
|
collection: item.name,
|
||||||
|
title: `{{t("Create record", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
formType: 'create',
|
||||||
|
'x-designer': 'CreateFormDesigner',
|
||||||
|
},
|
||||||
|
component: FormBlockInitializer,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
initializers: {
|
initializers: {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useFieldSchema } from '@formily/react';
|
import { useFieldSchema } from '@formily/react';
|
||||||
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -7,15 +7,14 @@ import {
|
|||||||
SchemaSettings,
|
SchemaSettings,
|
||||||
useCollection,
|
useCollection,
|
||||||
useCollectionFilterOptions,
|
useCollectionFilterOptions,
|
||||||
useCollectionManager,
|
|
||||||
useDesignable,
|
useDesignable,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
|
|
||||||
import { NAMESPACE } from '../../../locale';
|
|
||||||
import { findSchema } from '../utils';
|
|
||||||
import { ManualFormType } from '../SchemaConfig';
|
|
||||||
import { FilterDynamicComponent } from '../../../components/FilterDynamicComponent';
|
import { FilterDynamicComponent } from '../../../components/FilterDynamicComponent';
|
||||||
|
import { NAMESPACE } from '../../../locale';
|
||||||
import { FormBlockInitializer } from '../FormBlockInitializer';
|
import { FormBlockInitializer } from '../FormBlockInitializer';
|
||||||
|
import { ManualFormType } from '../SchemaConfig';
|
||||||
|
import { findSchema } from '../utils';
|
||||||
|
|
||||||
function UpdateFormDesigner() {
|
function UpdateFormDesigner() {
|
||||||
const { name, title } = useCollection();
|
const { name, title } = useCollection();
|
||||||
@ -71,26 +70,37 @@ function UpdateFormDesigner() {
|
|||||||
export default {
|
export default {
|
||||||
title: `{{t("Update record form", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Update record form", { ns: "${NAMESPACE}" })}}`,
|
||||||
config: {
|
config: {
|
||||||
useInitializer() {
|
useInitializer({ collections }) {
|
||||||
const { collections } = useCollectionManager();
|
|
||||||
return {
|
return {
|
||||||
key: 'updateRecordForm',
|
key: 'updateRecordForm',
|
||||||
type: 'subMenu',
|
type: 'subMenu',
|
||||||
title: `{{t("Update record form", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Update record form", { ns: "${NAMESPACE}" })}}`,
|
||||||
children: collections
|
children: [
|
||||||
.filter((item) => !item.hidden)
|
{
|
||||||
.map((item) => ({
|
key: 'updateRecordForm-child',
|
||||||
key: `updateForm-${item.name}`,
|
type: 'itemGroup',
|
||||||
type: 'item',
|
style: {
|
||||||
title: item.title,
|
maxHeight: '48vh',
|
||||||
schema: {
|
overflowY: 'auto',
|
||||||
collection: item.name,
|
|
||||||
title: `{{t("Update record", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
formType: 'update',
|
|
||||||
'x-designer': 'UpdateFormDesigner',
|
|
||||||
},
|
},
|
||||||
component: FormBlockInitializer,
|
loadChildren: ({ searchValue }) => {
|
||||||
})),
|
return collections
|
||||||
|
.filter((item) => !item.hidden && item.title.includes(searchValue))
|
||||||
|
.map((item) => ({
|
||||||
|
key: `updateRecordForm-child-${item.name}`,
|
||||||
|
type: 'item',
|
||||||
|
title: item.title,
|
||||||
|
schema: {
|
||||||
|
collection: item.name,
|
||||||
|
title: `{{t("Update record", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
formType: 'update',
|
||||||
|
'x-designer': 'UpdateFormDesigner',
|
||||||
|
},
|
||||||
|
component: FormBlockInitializer,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
initializers: {
|
initializers: {
|
||||||
|
Loading…
Reference in New Issue
Block a user