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:
被雨水过滤的空气-Rain 2023-09-26 13:47:20 +08:00 committed by GitHub
parent 8db9fda61b
commit ff16f59908
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 526 additions and 268 deletions

View File

@ -33,7 +33,7 @@ export const useMenuItem = () => {
const renderItems = useRef<() => JSX.Element>(null);
const shouldRerender = useRef(false);
const Component = useCallback(() => {
const Component = useCallback(({ limitCount }) => {
if (!shouldRerender.current) {
return null;
}
@ -43,6 +43,16 @@ export const useMenuItem = () => {
return renderItems.current();
}
if (limitCount && list.current.length > limitCount) {
return (
<>
{list.current.slice(0, limitCount).map((Com, index) => (
<Com key={index} />
))}
</>
);
}
return (
<>
{list.current.map((Com, index) => (

View File

@ -1,13 +1,15 @@
import { ISchema, observer, useForm } from '@formily/react';
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';
// @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 { Icon } from '../icon';
import { SchemaComponent, useActionContext } from '../schema-component';
import { useCompile, useDesignable } from '../schema-component/hooks';
import { SearchCollections } from './SearchCollections';
import { useStyles } from './style';
import {
SchemaInitializerButtonProps,
@ -22,12 +24,212 @@ export const SchemaInitializerItemContext = createContext(null);
export const SchemaInitializerButtonContext = createContext<{
visible?: boolean;
setVisible?: (v: boolean) => void;
searchValue?: string;
setSearchValue?: (v: string) => void;
}>({});
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(
(props: SchemaInitializerButtonProps) => {
const {
@ -46,26 +248,19 @@ SchemaInitializer.Button = observer(
const compile = useCompile();
const { insertAdjacent, findComponent, designable } = useDesignable();
const [visible, setVisible] = useState(false);
const { Component: CollectionComponent, getMenuItem, clean } = useMenuItem();
const [searchValue, setSearchValue] = useState('');
const [isPending, startTransition] = useTransition();
const { Component: CollectComponent, getMenuItem, clean } = useMenuItem();
const menuItems = useRef([]);
const { styles } = useStyles();
const changeMenu = (v: boolean) => {
// 这里是为了防止当鼠标快速滑过时,终止菜单的渲染,防止卡顿
startTransition(() => {
setVisible(v);
});
};
if (!designable && props.designable !== true) {
return null;
}
const buttonDom = component ? (
component
) : (
const buttonDom = component || (
<Button
type={'dashed'}
style={{
@ -131,27 +326,30 @@ SchemaInitializer.Button = observer(
}
if (item.type === 'itemGroup') {
const label = isString(item.title) ? compile(item.title) : item.title;
return (
!!item.children?.length && {
return {
type: 'group',
key: item.key || `item-group-${indexA}`,
label,
title: label,
children: renderItems(item.children),
}
);
style: item.style,
loadChildren: isEmpty(item.children)
? ({ searchValue } = { searchValue: '' }) => renderItems(item.loadChildren?.({ searchValue }) || [])
: null,
children: isEmpty(item.children) ? [] : renderItems(item.children),
};
}
if (item.type === 'subMenu') {
const label = compile(item.title);
return (
!!item.children?.length && {
return {
key: item.key || `item-group-${indexA}`,
label,
title: label,
popupClassName: styles.nbMenuItemSubMenu,
children: renderItems(item.children),
}
);
loadChildren: isEmpty(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);
}
const dropdownRender = () => (
<MenuWithLazyLoadChildren
style={{
maxHeight: '50vh',
overflowY: 'auto',
}}
items={menuItems.current}
clean={clean}
component={CollectComponent}
/>
);
useEffect(() => {
if (visible === false) {
clearSearchValue(menuItems.current);
}
}, [visible]);
return (
<SchemaInitializerButtonContext.Provider value={{ visible, setVisible, searchValue, setSearchValue }}>
{visible ? <CollectionComponent /> : null}
<SchemaInitializerButtonContext.Provider value={{ visible, setVisible }}>
{visible ? <CollectComponent /> : null}
<Dropdown
className={classNames('nb-schema-initializer-button')}
openClassName={`nb-schema-initializer-button-open`}
open={visible}
onOpenChange={(open) => {
// 如果不清空输入框的值,那么下次打开的时候会出现上次输入的值
setSearchValue('');
changeMenu(open);
}}
menu={{
style: {
maxHeight: '50vh',
overflowY: 'auto',
},
items: menuItems.current,
}}
dropdownRender={dropdownRender}
{...dropdown}
>
{component ? component : buttonDom}
{component || buttonDom}
</Dropdown>
</SchemaInitializerButtonContext.Provider>
);
@ -197,15 +405,14 @@ SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
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 (process.env.NODE_ENV !== 'production' && !collectMenuItem) {
throw new Error('SchemaInitializer.Item: collectMenuItem is undefined, please check the context');
}
if (items?.length > 0) {
const renderMenuItem = (items: SchemaInitializerItemOptions[], parentKey: string) => {
if (!items?.length) {
return null;
return [];
}
return items.map((item, indexA) => {
if (item.type === 'divider') {
@ -220,8 +427,12 @@ SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
label,
title: label,
className: styles.nbMenuItemGroup,
children: renderMenuItem(item.children, key),
} as MenuProps['items'][0];
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[] };
}
if (item.type === 'subMenu') {
const label = compile(item.title);
@ -230,8 +441,12 @@ SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
key,
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);
return {
@ -239,7 +454,6 @@ SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
label,
title: label,
onClick: (info) => {
item?.clearKeywords?.();
if (item.onClick) {
item.onClick({ ...info, item });
} else {
@ -268,7 +482,6 @@ SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
title: label,
icon: typeof icon === 'string' ? <Icon type={icon as string} /> : icon,
onClick: (opts) => {
info?.clearKeywords?.();
onClick({ ...opts, item: info });
},
};

View File

@ -2,7 +2,7 @@ import { Divider, Input } from 'antd';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
export const SelectCollection = ({ value: outValue, onChange }) => {
export const SearchCollections = ({ value: outValue, onChange }) => {
const { t } = useTranslation();
const [value, setValue] = useState<string>(outValue);
const inputRef = React.useRef(null);

View File

@ -22,11 +22,13 @@ interface ItemCommonOptions {
interface ItemGroupOptions extends ItemCommonOptions {
type: 'itemGroup';
children?: SchemaInitializerItemOptions[];
loadChildren?: ({ searchValue }?: { searchValue: string }) => SchemaInitializerItemOptions[];
}
interface SubMenuOptions extends ItemCommonOptions {
type: 'subMenu';
children?: SchemaInitializerItemOptions[];
loadChildren?: ({ searchValue }?: { searchValue: string }) => SchemaInitializerItemOptions[];
}
type BreakFn = (s: ISchema) => boolean;

View File

@ -1,15 +1,12 @@
import { ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
import { uid } from '@formily/shared';
import { error } from '@nocobase/utils/client';
import _ from 'lodash';
import React, { useCallback, useContext, useMemo } from 'react';
import { useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { BlockRequestContext, SchemaInitializerButtonContext, SchemaInitializerItemOptions } from '../';
import { FieldOptions, useCollection, useCollectionManager } from '../collection-manager';
import { BlockRequestContext, SchemaInitializerItemOptions } from '..';
import { CollectionFieldOptions, FieldOptions, useCollection, useCollectionManager } from '../collection-manager';
import { isAssocField } from '../filter-provider/utils';
import { useActionContext, useDesignable } from '../schema-component';
import { useSchemaTemplateManager } from '../schema-templates';
import { SelectCollection } from './SelectCollection';
export const itemsMerge = (items1) => {
return items1;
@ -834,120 +831,23 @@ export const useCollectionDataSourceItems = (componentName) => {
const { t } = useTranslation();
const { collections, getCollectionFields } = useCollectionManager();
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 [
{
key: 'tableBlock',
type: 'itemGroup',
title: React.createElement(SelectCollection, {
value: searchValue,
onChange,
}),
children: 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)
);
title: null,
children: [],
loadChildren: ({ searchValue } = { searchValue: '' }) => {
return getChildren({
collections,
getCollectionFields,
componentName,
searchValue,
getTemplatesByCollection,
t,
});
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;
};
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'),
};
}),
},
],
};
});
};

View File

@ -151,7 +151,7 @@ export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNeste
const [isPending, startTransition] = useReactTransition();
const changeMenu = (v: boolean) => {
// 这里是为了防止当鼠标快速滑过时,终止菜单的渲染,防止卡顿
// 当鼠标快速滑过时,终止菜单的渲染,防止卡顿
startTransition(() => {
setVisible(v);
});

View File

@ -45,6 +45,7 @@ const ChartsProvider = React.memo((props) => {
const api = useAPIClient();
const items = useContext<any>(SchemaInitializerContext);
const children = items.BlockInitializers.items[0].children;
if (children) {
const hasChartItem = children.some((child) => child?.component === 'ChartBlockInitializer');
if (!hasChartItem) {

View File

@ -1,18 +1,15 @@
import { SchemaInitializerButtonContext, useDesignable } from '@nocobase/client';
import React, { useState } from 'react';
import { ChartRendererProvider } from '../renderer';
import { ChartConfigContext, ChartConfigCurrent, ChartConfigure } from '../configure/ChartConfigure';
import { ChartRendererProvider } from '../renderer';
export const ChartV2Block: React.FC = (props) => {
const { insertAdjacent } = useDesignable();
const [visible, setVisible] = useState(false);
const [current, setCurrent] = useState<ChartConfigCurrent>({} as any);
const [initialVisible, setInitialVisible] = useState(false);
const [searchValue, setSearchValue] = useState('');
return (
<SchemaInitializerButtonContext.Provider
value={{ visible: initialVisible, setVisible: setInitialVisible, searchValue, setSearchValue }}
>
<SchemaInitializerButtonContext.Provider value={{ visible: initialVisible, setVisible: setInitialVisible }}>
<ChartConfigContext.Provider value={{ visible, setVisible, current, setCurrent }}>
{props.children}
<ChartRendererProvider {...current.field?.decoratorProps}>

View File

@ -3,8 +3,8 @@ import { ISchema } from '@formily/react';
import { uid } from '@formily/shared';
import { SchemaInitializer, useACLRoleContext, useCollectionDataSourceItems } from '@nocobase/client';
import React, { useContext } from 'react';
import { useChartsTranslation } from '../locale';
import { ChartConfigContext } from '../configure/ChartConfigure';
import { useChartsTranslation } from '../locale';
const itemWrap = SchemaInitializer.itemWrap;
const ConfigureButton = itemWrap((props) => {
@ -24,7 +24,12 @@ export const ChartInitializers = () => {
const { t } = useChartsTranslation();
const collections = useCollectionDataSourceItems('Chart');
const { allowAll, parseAction } = useACLRoleContext();
const children = collections[0].children
if (collections[0].loadChildren) {
const originalLoadChildren = collections[0].loadChildren;
collections[0].loadChildren = ({ searchValue }) => {
const children = originalLoadChildren({ searchValue });
const result = children
.filter((item) => {
if (allowAll) {
return true;
@ -36,11 +41,11 @@ export const ChartInitializers = () => {
...item,
component: ConfigureButton,
}));
if (!children.length) {
// Leave a blank item to show the filter component
children.push({} as any);
return result;
};
}
collections[0].children = children;
return (
<SchemaInitializer.Button
icon={'PlusOutlined'}

View File

@ -1,5 +1,5 @@
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 React, { useCallback, useMemo } from 'react';
import { useFlowContext } from './FlowContext';
@ -98,7 +98,7 @@ export function AddButton({ upstream, branchIndex = null }: AddButtonProps) {
menu={menu}
disabled={workflow.executed}
overlayClassName={css`
.ant-dropdown-menu-root{
.ant-dropdown-menu-root {
max-height: 30em;
overflow-y: auto;
}

View File

@ -1,8 +1,8 @@
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { FormLayout } from '@formily/antd-v5';
import { createForm } from '@formily/core';
import { FormProvider, ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
import { FormLayout } from '@formily/antd-v5';
import { Alert, Button, Modal, Space } from 'antd';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
@ -18,22 +18,23 @@ import {
SchemaSettings,
VariableScopeProvider,
gridRowColWrap,
useCollectionManager,
useCompile,
useFormBlockContext,
useSchemaOptionsContext,
} from '@nocobase/client';
import { Registry, lodash } from '@nocobase/utils/client';
import { instructions, useAvailableUpstreams, useNodeContext } from '..';
import { JOB_STATUS } from '../../constants';
import { useFlowContext } from '../../FlowContext';
import { JOB_STATUS } from '../../constants';
import { NAMESPACE, lang } from '../../locale';
import { useTrigger } from '../../triggers';
import { useWorkflowVariableOptions } from '../../variable';
import { DetailsBlockProvider } from './DetailsBlockProvider';
import { FormBlockProvider } from './FormBlockProvider';
import createRecordForm from './forms/create';
import customRecordForm from './forms/custom';
import updateRecordForm from './forms/update';
import { useWorkflowVariableOptions } from '../../variable';
type ValueOf<T> = T[keyof T];
@ -53,7 +54,7 @@ export type FormType = {
export type ManualFormType = {
title: string;
config: {
useInitializer: () => SchemaInitializerItemOptions;
useInitializer: ({ collections }?: { collections: any[] }) => SchemaInitializerItemOptions;
initializers?: {
[key: string]: React.FC;
};
@ -108,6 +109,7 @@ function SimpleDesigner() {
}
function AddBlockButton(props: any) {
const { collections } = useCollectionManager();
const current = useNodeContext();
const nodes = useAvailableUpstreams(current);
const triggerInitializers = [useTriggerInitializers()].filter(Boolean);
@ -146,7 +148,7 @@ function AddBlockButton(props: any) {
title: '{{t("Form")}}',
children: Array.from(manualFormTypes.getValues()).map((item: ManualFormType) => {
const { useInitializer: getInitializer } = item.config;
return getInitializer();
return getInitializer({ collections });
}),
},
{
@ -171,31 +173,32 @@ function AssignedFieldValues() {
const fieldSchema = useFieldSchema();
const scope = useWorkflowVariableOptions({ fieldNames: { label: 'title', value: 'name' } });
const [open, setOpen] = useState(false);
const [initialSchema, setInitialSchema] = useState(fieldSchema?.['x-action-settings']?.assignedValues?.schema ?? {
const [initialSchema, setInitialSchema] = useState(
fieldSchema?.['x-action-settings']?.assignedValues?.schema ?? {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'CustomFormItemInitializers',
properties: {},
});
},
);
const [schema, setSchema] = useState<Schema>(null);
const { components } = useSchemaOptionsContext();
useEffect(() => {
setSchema(new Schema({
setSchema(
new Schema({
properties: {
grid: initialSchema
grid: initialSchema,
},
}));
}),
);
}, [initialSchema]);
const form = useMemo(
() => {
const form = useMemo(() => {
const initialValues = fieldSchema?.['x-action-settings']?.assignedValues?.values;
return createForm({
initialValues: lodash.cloneDeep(initialValues),
values: lodash.cloneDeep(initialValues),
});
},
[],
);
}, []);
const title = t('Assign field values');
@ -220,9 +223,7 @@ function AssignedFieldValues() {
return (
<>
<SchemaSettings.Item onClick={() => setOpen(true)}>
{title}
</SchemaSettings.Item>
<SchemaSettings.Item onClick={() => setOpen(true)}>{title}</SchemaSettings.Item>
<Modal
width={'50%'}
title={title}
@ -231,14 +232,18 @@ function AssignedFieldValues() {
footer={
<Space>
<Button onClick={onCancel}>{t('Cancel')}</Button>
<Button type="primary" onClick={onSubmit}>{t('Submit')}</Button>
<Button type="primary" onClick={onSubmit}>
{t('Submit')}
</Button>
</Space>
}
>
<VariableScopeProvider scope={scope}>
<FormProvider form={form}>
<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 />
{open && schema && (
<SchemaComponentContext.Provider
@ -246,7 +251,7 @@ function AssignedFieldValues() {
...ctx,
refresh() {
setInitialSchema(lodash.get(schema.toJSON(), 'properties.grid'));
}
},
}}
>
<SchemaComponent schema={schema} components={components} />

View File

@ -1,12 +1,11 @@
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 { findSchema } from '../utils';
import { ManualFormType } from '../SchemaConfig';
import { FormBlockInitializer } from '../FormBlockInitializer';
import { ManualFormType } from '../SchemaConfig';
import { findSchema } from '../utils';
function CreateFormDesigner() {
const { name, title } = useCollection();
@ -30,16 +29,24 @@ function CreateFormDesigner() {
export default {
title: `{{t("Create record form", { ns: "${NAMESPACE}" })}}`,
config: {
useInitializer() {
const { collections } = useCollectionManager();
useInitializer({ collections }) {
return {
key: 'createRecordForm',
type: 'subMenu',
title: `{{t("Create record form", { ns: "${NAMESPACE}" })}}`,
children: collections
.filter((item) => !item.hidden)
children: [
{
key: 'createRecordForm-child',
type: 'itemGroup',
style: {
maxHeight: '48vh',
overflowY: 'auto',
},
loadChildren: ({ searchValue }) => {
return collections
.filter((item) => !item.hidden && item.title.includes(searchValue))
.map((item) => ({
key: `createForm-${item.name}`,
key: `createRecordForm-child-${item.name}`,
type: 'item',
title: item.title,
schema: {
@ -49,7 +56,10 @@ export default {
'x-designer': 'CreateFormDesigner',
},
component: FormBlockInitializer,
})),
}));
},
},
],
};
},
initializers: {

View File

@ -1,5 +1,5 @@
import React from 'react';
import { useFieldSchema } from '@formily/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
@ -7,15 +7,14 @@ import {
SchemaSettings,
useCollection,
useCollectionFilterOptions,
useCollectionManager,
useDesignable,
} from '@nocobase/client';
import { NAMESPACE } from '../../../locale';
import { findSchema } from '../utils';
import { ManualFormType } from '../SchemaConfig';
import { FilterDynamicComponent } from '../../../components/FilterDynamicComponent';
import { NAMESPACE } from '../../../locale';
import { FormBlockInitializer } from '../FormBlockInitializer';
import { ManualFormType } from '../SchemaConfig';
import { findSchema } from '../utils';
function UpdateFormDesigner() {
const { name, title } = useCollection();
@ -71,16 +70,24 @@ function UpdateFormDesigner() {
export default {
title: `{{t("Update record form", { ns: "${NAMESPACE}" })}}`,
config: {
useInitializer() {
const { collections } = useCollectionManager();
useInitializer({ collections }) {
return {
key: 'updateRecordForm',
type: 'subMenu',
title: `{{t("Update record form", { ns: "${NAMESPACE}" })}}`,
children: collections
.filter((item) => !item.hidden)
children: [
{
key: 'updateRecordForm-child',
type: 'itemGroup',
style: {
maxHeight: '48vh',
overflowY: 'auto',
},
loadChildren: ({ searchValue }) => {
return collections
.filter((item) => !item.hidden && item.title.includes(searchValue))
.map((item) => ({
key: `updateForm-${item.name}`,
key: `updateRecordForm-child-${item.name}`,
type: 'item',
title: item.title,
schema: {
@ -90,7 +97,10 @@ export default {
'x-designer': 'UpdateFormDesigner',
},
component: FormBlockInitializer,
})),
}));
},
},
],
};
},
initializers: {