From ff16f59908e68fd9d3a94aa0a2e55be722227b0a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=A2=AB=E9=9B=A8=E6=B0=B4=E8=BF=87=E6=BB=A4=E7=9A=84?=
=?UTF-8?q?=E7=A9=BA=E6=B0=94-Rain?= <958414905@qq.com>
Date: Tue, 26 Sep 2023 13:47:20 +0800
Subject: [PATCH] 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
---
.../core/client/src/hooks/useMenuItem.tsx | 12 +-
.../schema-initializer/SchemaInitializer.tsx | 321 +++++++++++++++---
...ctCollection.tsx => SearchCollections.tsx} | 2 +-
.../client/src/schema-initializer/types.ts | 2 +
.../client/src/schema-initializer/utils.ts | 235 ++++++-------
.../src/schema-settings/SchemaSettings.tsx | 2 +-
.../plugin-charts/src/client/index.tsx | 1 +
.../src/client/block/ChartBlock.tsx | 7 +-
.../client/block/ChartBlockInitializer.tsx | 39 ++-
.../plugin-workflow/src/client/AddButton.tsx | 4 +-
.../src/client/nodes/manual/SchemaConfig.tsx | 71 ++--
.../src/client/nodes/manual/forms/create.tsx | 48 +--
.../src/client/nodes/manual/forms/update.tsx | 50 +--
13 files changed, 526 insertions(+), 268 deletions(-)
rename packages/core/client/src/schema-initializer/{SelectCollection.tsx => SearchCollections.tsx} (95%)
diff --git a/packages/core/client/src/hooks/useMenuItem.tsx b/packages/core/client/src/hooks/useMenuItem.tsx
index bc5cc6e504..6794e1811a 100644
--- a/packages/core/client/src/hooks/useMenuItem.tsx
+++ b/packages/core/client/src/hooks/useMenuItem.tsx
@@ -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) => (
+
+ ))}
+ >
+ );
+ }
+
return (
<>
{list.current.map((Com, index) => (
diff --git a/packages/core/client/src/schema-initializer/SchemaInitializer.tsx b/packages/core/client/src/schema-initializer/SchemaInitializer.tsx
index 6466fc2e28..307e7317b5 100644
--- a/packages/core/client/src/schema-initializer/SchemaInitializer.tsx
+++ b/packages/core/client/src/schema-initializer/SchemaInitializer.tsx
@@ -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 ;
+};
+
+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 (
+
+
+
+ );
+};
+
+// 清除所有的 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: (
+ {
+ 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 = (
+ {
+ item._count = minStep;
+ beforeLoading?.();
+ item._allChildren = item.loadChildren({ searchValue: value });
+
+ if (isEmpty(item._allChildren)) {
+ item.children = [
+ {
+ key: 'empty',
+ label: ,
+ },
+ ];
+ } 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 */}
+
+
+ >
+ );
+};
+
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);
- });
+ setVisible(v);
};
if (!designable && props.designable !== true) {
return null;
}
- const buttonDom = component ? (
- component
- ) : (
+ const buttonDom = component || (