perf(Table): improve performance (#5438)

* perf(Table): improve performance

* perf(RecordProvider): improve performance

* perf(Table): improve performance

* refactor: migrate component to out

* perf(Action): improve performance

* perf(useInView): registor in row instead of cell

* perf(useCompile): improve performance

* fix: fix undefined error

* perf: use startTransition API

* fix: make e2e test pass

* fix: make e2e test pass

* fix: make unit test pass

* chore: fix unit test

* perf: replace antd skeleton component with custom skeleton component

* perf: split useParentObjectVariable

* perf(ColumnFieldProvider): remove observer and use useMemo

* perf(ReadPretty.JSON): remove Typography

* refactor(Map): use new API

* perf(markdown): use memoize

* fix: make unit test pass

* refactor: extract constant

* perf(ButtonLinkList): use asynchronous rendering to prevent blocking the main process

* fix(ButtonLinkList): fix render issue

* refactor: remove observer

* perf: optimize code

* refactor(EllipsisWithTooltip): extract function out

* refactor(EllipsisWithTooltip): optimize code

* perf(SelectReadPretty): improve performance

* chore: make e2e test more stable
This commit is contained in:
Zeke Zhang 2024-10-24 11:58:20 +08:00 committed by GitHub
parent cee210fa27
commit 4e981ed339
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 1516 additions and 908 deletions

View File

@ -15,12 +15,11 @@ import { Navigate } from 'react-router-dom';
import { useAPIClient, useRequest } from '../api-client';
import { useAppSpin } from '../application/hooks/useAppSpin';
import { useBlockRequestContext } from '../block-provider/BlockProvider';
import { useCollectionManager_deprecated, useCollection_deprecated } from '../collection-manager';
import { useResourceActionContext } from '../collection-manager/ResourceActionProvider';
import { CollectionNotAllowViewPlaceholder, useCollection, useCollectionManager } from '../data-source';
import { useDataSourceKey } from '../data-source/data-source/DataSourceProvider';
import { useRecord } from '../record-provider';
import { SchemaComponentOptions, useDesignable } from '../schema-component';
import { CollectionNotAllowViewPlaceholder } from '../data-source';
import { useApp } from '../application';
@ -115,29 +114,41 @@ export const useACLRolesCheck = () => {
const dataSourceName = useDataSourceKey();
const { dataSources: dataSourcesAcl } = ctx?.data?.meta || {};
const data = { ...ctx?.data?.data, ...omit(dataSourcesAcl?.[dataSourceName], 'snippets') };
const getActionAlias = (actionPath: string) => {
const actionName = actionPath.split(':').pop();
return data?.actionAlias?.[actionName] || actionName;
};
const getActionAlias = useCallback(
(actionPath: string) => {
const actionName = actionPath.split(':').pop();
return data?.actionAlias?.[actionName] || actionName;
},
[data?.actionAlias],
);
return {
data,
getActionAlias,
inResources: (resourceName: string) => {
return data?.resources?.includes?.(resourceName);
},
getResourceActionParams: (actionPath: string) => {
const [resourceName] = actionPath.split(':');
const actionAlias = getActionAlias(actionPath);
return data?.actions?.[`${resourceName}:${actionAlias}`] || data?.actions?.[actionPath];
},
getStrategyActionParams: (actionPath: string) => {
const actionAlias = getActionAlias(actionPath);
const strategyAction = data?.strategy?.actions?.find((action) => {
const [value] = action.split(':');
return value === actionAlias;
});
return strategyAction ? {} : null;
},
inResources: useCallback(
(resourceName: string) => {
return data?.resources?.includes?.(resourceName);
},
[data?.resources],
),
getResourceActionParams: useCallback(
(actionPath: string) => {
const [resourceName] = actionPath.split(':');
const actionAlias = getActionAlias(actionPath);
return data?.actions?.[`${resourceName}:${actionAlias}`] || data?.actions?.[actionPath];
},
[data?.actions, getActionAlias],
),
getStrategyActionParams: useCallback(
(actionPath: string) => {
const actionAlias = getActionAlias(actionPath);
const strategyAction = data?.strategy?.actions?.find((action) => {
const [value] = action.split(':');
return value === actionAlias;
});
return strategyAction ? {} : null;
},
[data?.strategy?.actions, getActionAlias],
),
};
};
@ -179,36 +190,43 @@ const useResourceName = () => {
export function useACLRoleContext() {
const { data, getActionAlias, inResources, getResourceActionParams, getStrategyActionParams } = useACLRolesCheck();
const allowedActions = useAllowedActions();
const { getCollectionJoinField } = useCollectionManager_deprecated();
const verifyScope = (actionName: string, recordPkValue: any) => {
const actionAlias = getActionAlias(actionName);
if (!Array.isArray(allowedActions?.[actionAlias])) {
return null;
}
return allowedActions[actionAlias].includes(recordPkValue);
};
const cm = useCollectionManager();
const verifyScope = useCallback(
(actionName: string, recordPkValue: any) => {
const actionAlias = getActionAlias(actionName);
if (!Array.isArray(allowedActions?.[actionAlias])) {
return null;
}
return allowedActions[actionAlias].includes(recordPkValue);
},
[allowedActions, getActionAlias],
);
return {
...data,
parseAction: (actionPath: string, options: any = {}) => {
const [resourceName, actionName] = actionPath.split(':');
const targetResource = resourceName?.includes('.') && getCollectionJoinField(resourceName)?.target;
if (!getIgnoreScope(options)) {
const r = verifyScope(actionName, options.recordPkValue);
if (r !== null) {
return r ? {} : null;
parseAction: useCallback(
(actionPath: string, options: any = {}) => {
const [resourceName, actionName] = actionPath?.split(':') || [];
const targetResource = resourceName?.includes('.') && cm.getCollectionField(resourceName)?.target;
if (!getIgnoreScope(options)) {
const r = verifyScope(actionName, options.recordPkValue);
if (r !== null) {
return r ? {} : null;
}
}
}
if (data?.allowAll) {
return {};
}
if (inResources(targetResource)) {
return getResourceActionParams(`${targetResource}:${actionName}`);
}
if (inResources(resourceName)) {
return getResourceActionParams(actionPath);
}
return getStrategyActionParams(actionPath);
},
if (data?.allowAll) {
return {};
}
if (inResources(targetResource)) {
return getResourceActionParams(`${targetResource}:${actionName}`);
}
if (inResources(resourceName)) {
return getResourceActionParams(actionPath);
}
return getStrategyActionParams(actionPath);
},
[cm, data?.allowAll, getResourceActionParams, getStrategyActionParams, inResources, verifyScope],
),
};
}
@ -228,19 +246,29 @@ export const ACLCollectionProvider = (props) => {
const { allowAll: customAllowAll } = useACLCustomContext();
const app = useApp();
const schema = useFieldSchema();
if (allowAll || app.disableAcl || customAllowAll) {
return props.children;
}
let actionPath = schema?.['x-acl-action'] || props.actionPath;
const resoureName = schema?.['x-decorator-props']?.['association'] || schema?.['x-decorator-props']?.['collection'];
// 兼容 undefined 的情况
if (actionPath === 'undefined:list' && resoureName && resoureName !== 'undefined') {
actionPath = `${resoureName}:list`;
}
const params = useMemo(() => {
if (!actionPath) {
return null;
}
return parseAction(actionPath, { schema });
}, [parseAction, actionPath, schema]);
if (allowAll || app.disableAcl || customAllowAll) {
return props.children;
}
if (!actionPath) {
return props.children;
}
const params = parseAction(actionPath, { schema });
if (!params) {
return <CollectionNotAllowViewPlaceholder />;
}
@ -254,39 +282,51 @@ export const useACLActionParamsContext = () => {
};
export const useRecordPkValue = () => {
const { getPrimaryKey } = useCollection_deprecated();
const collection = useCollection();
const record = useRecord();
const primaryKey = getPrimaryKey();
if (!collection) {
return;
}
const primaryKey = collection.getPrimaryKey();
return record?.[primaryKey];
};
export const ACLActionProvider = (props) => {
const { template, writableView } = useCollection_deprecated();
const collection = useCollection();
const recordPkValue = useRecordPkValue();
const resource = useResourceName();
const { parseAction } = useACLRoleContext();
const schema = useFieldSchema();
let actionPath = schema['x-acl-action'];
const editablePath = ['create', 'update', 'destroy', 'importXlsx'];
if (!actionPath && resource && schema['x-action']) {
actionPath = `${resource}:${schema['x-action']}`;
}
if (!actionPath?.includes(':')) {
actionPath = `${resource}:${actionPath}`;
}
const params = useMemo(
() => parseAction(actionPath, { schema, recordPkValue }),
[parseAction, actionPath, schema, recordPkValue],
);
if (!actionPath) {
return <>{props.children}</>;
}
if (!resource) {
return <>{props.children}</>;
}
const params = parseAction(actionPath, { schema, recordPkValue });
if (!params) {
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
}
//视图表无编辑权限时不显示
if (editablePath.includes(actionPath) || editablePath.includes(actionPath?.split(':')[1])) {
if (template !== 'view' || writableView) {
if ((collection && collection.template !== 'view') || collection?.writableView) {
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
}
return null;

View File

@ -8,10 +8,11 @@
*/
import { Field } from '@formily/core';
import { connect, useField, useFieldSchema } from '@formily/react';
import { connect, Schema, useField, useFieldSchema } from '@formily/react';
import { untracked } from '@formily/reactive';
import { merge } from '@formily/shared';
import { concat } from 'lodash';
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useFormBlockContext } from '../../block-provider/FormBlockProvider';
import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps';
@ -24,54 +25,48 @@ type Props = {
children?: React.ReactNode;
};
const setFieldProps = (field: Field, key: string, value: any) => {
untracked(() => {
if (field[key] === undefined) {
field[key] = value;
}
});
};
const setRequired = (field: Field, fieldSchema: Schema, uiSchema: Schema) => {
if (typeof fieldSchema['required'] === 'undefined') {
field.required = !!uiSchema['required'];
}
};
/**
* TODO: 初步适配
* @internal
*/
export const CollectionFieldInternalField: React.FC = (props: Props) => {
const { component } = props;
const compile = useCompile();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const collectionField = useCollectionField();
const { uiSchema: uiSchemaOrigin, defaultValue } = collectionField;
const { uiSchema: uiSchemaOrigin, defaultValue } = useCollectionField();
const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue();
const uiSchema = useMemo(() => compile(uiSchemaOrigin), [JSON.stringify(uiSchemaOrigin)]);
const Component = useComponent(
fieldSchema['x-component-props']?.['component'] || uiSchema?.['x-component'] || 'Input',
fieldSchema['x-component-props']?.['component'] || uiSchemaOrigin?.['x-component'] || 'Input',
);
const setFieldProps = useCallback(
(key, value) => {
field[key] = typeof field[key] === 'undefined' ? value : field[key];
},
[field],
);
const setRequired = useCallback(() => {
if (typeof fieldSchema['required'] === 'undefined') {
field.required = !!uiSchema['required'];
}
}, [fieldSchema, uiSchema]);
const ctx = useFormBlockContext();
const dynamicProps = useDynamicComponentProps(uiSchemaOrigin?.['x-use-component-props'], props);
const dynamicProps = useDynamicComponentProps(uiSchema?.['x-use-component-props'], props);
useEffect(() => {
if (ctx?.field) {
ctx.field.added = ctx.field.added || new Set();
ctx.field.added.add(fieldSchema.name);
}
});
// TODO: 初步适配
useEffect(() => {
if (!uiSchema) {
if (!uiSchemaOrigin) {
return;
}
setFieldProps('content', uiSchema['x-content']);
setFieldProps('title', uiSchema.title);
setFieldProps('description', uiSchema.description);
const uiSchema = compile(uiSchemaOrigin);
setFieldProps(field, 'content', uiSchema['x-content']);
setFieldProps(field, 'title', uiSchema.title);
setFieldProps(field, 'description', uiSchema.description);
if (ctx?.form) {
const defaultVal = isAllowToSetDefaultValue() ? fieldSchema.default || defaultValue : undefined;
defaultVal !== null && defaultVal !== undefined && setFieldProps('initialValue', defaultVal);
defaultVal !== null && defaultVal !== undefined && setFieldProps(field, 'initialValue', defaultVal);
}
if (!field.validator && (uiSchema['x-validator'] || fieldSchema['x-validator'])) {
@ -84,14 +79,14 @@ export const CollectionFieldInternalField: React.FC = (props: Props) => {
if (fieldSchema['x-read-pretty'] === true) {
field.readPretty = true;
}
setRequired();
setRequired(field, fieldSchema, uiSchema);
// @ts-ignore
field.dataSource = uiSchema.enum;
const originalProps = compile(uiSchema['x-component-props']) || {};
field.componentProps = merge(originalProps, field.componentProps || {}, dynamicProps || {});
}, [uiSchema]);
}, [uiSchemaOrigin]);
if (!uiSchema) return null;
if (!uiSchemaOrigin) return null;
return <Component {...props} {...dynamicProps} />;
};

View File

@ -27,29 +27,28 @@ const getHook = (str: string, scope: Record<string, any>, allText: string) => {
return res || useDefaultDynamicComponentProps;
};
const getUseDynamicProps = (useComponentPropsStr: string, scope: Record<string, any>) => {
if (!useComponentPropsStr) {
return useDefaultDynamicComponentProps;
}
if (_.isFunction(useComponentPropsStr)) {
return useComponentPropsStr;
}
const pathList = useComponentPropsStr.split('.');
let result;
for (const item of pathList) {
result = getHook(item, result || scope, useComponentPropsStr);
}
return result;
};
export function useDynamicComponentProps(useComponentPropsStr?: string, props?: any) {
const scope = useExpressionScope();
const useDynamicProps = useMemo(() => {
if (!useComponentPropsStr) {
return useDefaultDynamicComponentProps;
}
if (_.isFunction(useComponentPropsStr)) {
return useComponentPropsStr;
}
const pathList = useComponentPropsStr.split('.');
let result;
for (const item of pathList) {
result = getHook(item, result || scope, useComponentPropsStr);
}
return result;
}, [useComponentPropsStr]);
const res = useDynamicProps(props);
const res = getUseDynamicProps(useComponentPropsStr, scope)(props);
return res;
}

View File

@ -122,6 +122,7 @@ test.describe('configure item actions', () => {
await page.getByLabel('schema-initializer-ActionBar-list:configureItemActions-general').first().hover();
await page.getByRole('menuitem', { name: 'Popup' }).click();
await page.mouse.move(300, 0);
await page.getByLabel('schema-initializer-ActionBar-list:configureItemActions-general').first().hover();
await page.getByRole('menuitem', { name: 'Update record' }).click();

View File

@ -13,14 +13,14 @@ import { T4334 } from '../templatesOfBug';
// fix https://nocobase.height.app/T-2187
test('action linkage by row data', async ({ page, mockPage }) => {
await mockPage(T4334).goto();
const adminEditAction = await page.getByLabel('action-Action.Link-Edit-update-roles-table-admin');
const adminEditAction = page.getByLabel('action-Action.Link-Edit-update-roles-table-admin');
const adminEditActionStyle = await adminEditAction.evaluate((element) => {
const computedStyle = window.getComputedStyle(element);
return {
opacity: computedStyle.opacity,
};
});
const rootEditAction = await page.getByLabel('action-Action.Link-Edit-update-roles-table-root');
const rootEditAction = page.getByLabel('action-Action.Link-Edit-update-roles-table-root');
const rootEditActionStyle = await rootEditAction.evaluate((element) => {
const computedStyle = window.getComputedStyle(element);
return {
@ -29,6 +29,6 @@ test('action linkage by row data', async ({ page, mockPage }) => {
};
});
await expect(adminEditActionStyle.opacity).not.toBe('0.1');
await expect(rootEditActionStyle.opacity).not.toBe('1');
expect(adminEditActionStyle.opacity).not.toBe('0.1');
expect(rootEditActionStyle.opacity).not.toBe('1');
});

View File

@ -10,7 +10,7 @@
import { ArrayField } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react';
import { isEqual } from 'lodash';
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { useTableBlockContext } from '../../../../../block-provider/TableBlockProvider';
import { findFilterTargets } from '../../../../../block-provider/hooks';
import { DataBlock, useFilterBlock } from '../../../../../filter-provider/FilterProvider';
@ -21,10 +21,12 @@ export const useTableBlockProps = () => {
const field = useField<ArrayField>();
const fieldSchema = useFieldSchema();
const ctx = useTableBlockContext();
const globalSort = fieldSchema.parent?.['x-decorator-props']?.['params']?.['sort'];
const { getDataBlocks } = useFilterBlock();
const isLoading = ctx?.service?.loading;
const params = useMemo(() => ctx?.service?.params, [JSON.stringify(ctx?.service?.params)]);
const ctxRef = useRef(null);
ctxRef.current = ctx;
useEffect(() => {
if (!isLoading) {
const serviceResponse = ctx?.service?.data;
@ -78,19 +80,20 @@ export const useTableBlockProps = () => {
),
onChange: useCallback(
({ current, pageSize }, filters, sorter) => {
const globalSort = fieldSchema.parent?.['x-decorator-props']?.['params']?.['sort'];
const sort = sorter.order
? sorter.order === `ascend`
? [sorter.field]
: [`-${sorter.field}`]
: globalSort || ctx.dragSortBy;
: globalSort || ctxRef.current.dragSortBy;
const currentPageSize = pageSize || fieldSchema.parent?.['x-decorator-props']?.['params']?.pageSize;
const args = { ...params?.[0], page: current || 1, pageSize: currentPageSize };
const args = { ...ctxRef.current?.service?.params?.[0], page: current || 1, pageSize: currentPageSize };
if (sort) {
args['sort'] = sort;
}
ctx.service.run(args);
ctxRef.current?.service.run(args);
},
[globalSort, params, ctx.dragSort],
[fieldSchema.parent],
),
onClickRow: useCallback(
(record, setSelectedRow, selectedRow) => {

View File

@ -11,14 +11,18 @@ import { useField, useFieldSchema } from '@formily/react';
import { useTranslation } from 'react-i18next';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { SchemaSettingsItemType } from '../../../../application/schema-settings/types';
import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator';
import {
useColumnSchema,
useTableFieldInstanceList,
} from '../../../../schema-component/antd/table-v2/Table.Column.Decorator';
import { useDesignable } from '../../../../schema-component/hooks/useDesignable';
export const ellipsisSettingsItem: SchemaSettingsItemType = {
name: 'ellipsis',
type: 'switch',
useComponentProps() {
const { fieldSchema: tableFieldSchema, filedInstanceList } = useColumnSchema();
const { fieldSchema: tableFieldSchema } = useColumnSchema();
const tableFieldInstanceList = useTableFieldInstanceList();
const fieldSchema = useFieldSchema();
const formField = useField();
const { dn } = useDesignable();
@ -26,8 +30,8 @@ export const ellipsisSettingsItem: SchemaSettingsItemType = {
const schema = tableFieldSchema || fieldSchema;
const hidden = tableFieldSchema
? filedInstanceList[0]
? !filedInstanceList[0].readPretty
? tableFieldInstanceList[0]
? !tableFieldInstanceList[0].readPretty
: !tableFieldSchema['x-read-pretty']
: !formField.readPretty;
@ -46,8 +50,8 @@ export const ellipsisSettingsItem: SchemaSettingsItemType = {
},
});
if (tableFieldSchema && filedInstanceList) {
filedInstanceList.forEach((fieldInstance) => {
if (tableFieldSchema && tableFieldInstanceList) {
tableFieldInstanceList.forEach((fieldInstance) => {
fieldInstance.componentProps.ellipsis = checked;
});
} else {

View File

@ -7,6 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { raw } from '@formily/reactive';
import React, { createContext, useContext, useMemo } from 'react';
import { CollectionRecordProvider, useCollection } from '../data-source';
import { useCurrentUserContext } from '../user';
@ -28,7 +29,8 @@ export const RecordProvider: React.FC<{
const { record, children, parent, isNew } = props;
const collection = useCollection();
const value = useMemo(() => {
const res = { ...record };
// Directly destructuring reactive objects can cause performance issues, so we use raw to wrap it here
const res = { ...raw(record) };
res['__parent'] = parent;
res['__collectionName'] = collection?.name;
return res;

View File

@ -7,12 +7,13 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
import { Field } from '@formily/core';
import { observer, RecursionField, Schema, useField, useFieldSchema, useForm } from '@formily/react';
import { isPortalInBody } from '@nocobase/utils/client';
import { App, Button } from 'antd';
import classnames from 'classnames';
import _, { default as lodash } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { default as lodash } from 'lodash';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
import { ErrorFallback, StablePopover, TabsContextProvider, useActionContext } from '../..';
@ -40,7 +41,7 @@ import { ActionPage } from './Action.Page';
import useStyles from './Action.style';
import { ActionContextProvider } from './context';
import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
import { ActionProps, ComposedAction } from './types';
import { ActionContextProps, ActionProps, ComposedAction } from './types';
import { linkageAction, setInitialActionState } from './utils';
const useA = () => {
@ -75,35 +76,22 @@ export const Action: ComposedAction = withDynamicSchemaProps(
confirmTitle,
...others
} = useProps(props); // 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const aclCtx = useACLActionParamsContext();
const { wrapSSR, componentCls, hashId } = useStyles();
const { t } = useTranslation();
const { visibleWithURL, setVisibleWithURL } = usePopupUtils();
const [visible, setVisible] = useState(false);
const [formValueChanged, setFormValueChanged] = useState(false);
const { setSubmitted: setParentSubmitted } = useActionContext();
const Designer = useDesigner();
const field = useField<any>();
const { run, element, disabled: disableAction } = _.isFunction(useAction) ? useAction(actionCallback) : ({} as any);
const fieldSchema = useFieldSchema();
const compile = useCompile();
const form = useForm();
const recordData = useCollectionRecordData();
const parentRecordData = useCollectionParentRecordData();
const designerProps = fieldSchema['x-toolbar-props'] || fieldSchema['x-designer-props'];
const openMode = fieldSchema?.['x-component-props']?.['openMode'];
const openSize = fieldSchema?.['x-component-props']?.['openSize'];
const refreshDataBlockRequest = fieldSchema?.['x-component-props']?.['refreshDataBlockRequest'];
const confirm = compile(fieldSchema['x-component-props']?.confirm) || propsConfirm;
const disabled = form.disabled || field.disabled || field.data?.disabled || propsDisabled || disableAction;
const linkageRules = useMemo(() => fieldSchema?.['x-linkage-rules'] || [], [fieldSchema?.['x-linkage-rules']]);
const { designable } = useDesignable();
const tarComponent = useComponent(component) || component;
const { modal } = App.useApp();
const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: { values: recordData, readPretty: false } as any });
const { visibleWithURL, setVisibleWithURL } = usePopupUtils();
const { setSubmitted } = useActionContext();
const { getAriaLabel } = useGetAriaLabelOfAction(title);
const service = useDataBlockRequest();
const parentRecordData = useCollectionParentRecordData();
const actionTitle = useMemo(() => {
const res = title || compile(fieldSchema.title);
@ -130,14 +118,6 @@ export const Action: ComposedAction = withDynamicSchemaProps(
});
}, [field, linkageRules, localVariables, variables]);
const buttonStyle = useMemo(() => {
return {
...style,
opacity: designable && (field?.data?.hidden || !aclCtx) && 0.1,
color: disabled ? 'rgba(0, 0, 0, 0.25)' : style?.color,
};
}, [aclCtx, designable, field?.data?.hidden, style, disabled]);
const handleMouseEnter = useCallback(
(e) => {
onMouseEnter?.(e);
@ -145,129 +125,247 @@ export const Action: ComposedAction = withDynamicSchemaProps(
[onMouseEnter],
);
const buttonProps = {
designable,
field,
aclCtx,
actionTitle,
icon,
loading,
disabled,
buttonStyle,
handleMouseEnter,
tarComponent,
designerProps,
componentCls,
hashId,
className,
others,
getAriaLabel,
type: props.type,
Designer,
openMode,
onClick,
refreshDataBlockRequest,
service,
fieldSchema,
setVisible,
run,
confirm,
modal,
setSubmitted: setParentSubmitted,
confirmTitle,
};
const buttonElement = RenderButton(buttonProps);
// if (!btnHover) {
// return buttonElement;
// }
let result = (
<PopupVisibleProvider visible={false}>
<ActionContextProvider
button={buttonElement}
visible={visible || visibleWithURL}
setVisible={(value) => {
setVisible?.(value);
setVisibleWithURL?.(value);
}}
formValueChanged={formValueChanged}
setFormValueChanged={setFormValueChanged}
openMode={openMode}
openSize={openSize}
containerRefKey={containerRefKey}
fieldSchema={fieldSchema}
setSubmitted={setParentSubmitted}
>
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
{!popover && <RenderButton {...buttonProps} />}
<VariablePopupRecordProvider>{!popover && props.children}</VariablePopupRecordProvider>
{element}
</ActionContextProvider>
</PopupVisibleProvider>
return (
<InternalAction
containerRefKey={containerRefKey}
fieldSchema={fieldSchema}
designable={designable}
field={field}
actionTitle={actionTitle}
icon={icon}
loading={loading}
handleMouseEnter={handleMouseEnter}
tarComponent={tarComponent}
className={className}
type={props.type}
Designer={Designer}
onClick={onClick}
confirm={confirm}
confirmTitle={confirmTitle}
popover={popover}
addChild={addChild}
recordData={recordData}
title={title}
style={style}
propsDisabled={propsDisabled}
useAction={useAction}
visibleWithURL={visibleWithURL}
setVisibleWithURL={setVisibleWithURL}
setSubmitted={setSubmitted}
getAriaLabel={getAriaLabel}
parentRecordData={parentRecordData}
{...others}
/>
);
if (isBulkEditAction(fieldSchema)) {
// Clear the context of Tabs to avoid affecting the Tabs of the upper-level popup
result = <TabsContextProvider>{result}</TabsContextProvider>;
}
// fix https://nocobase.height.app/T-3235/description
if (addChild) {
return wrapSSR(
// fix https://nocobase.height.app/T-3966
<RecordProvider record={null} parent={parentRecordData}>
<TreeRecordProvider parent={recordData}>{result}</TreeRecordProvider>
</RecordProvider>,
);
}
return wrapSSR(result);
}),
{ displayName: 'Action' },
);
Action.Popover = observer(
(props) => {
const { button, visible, setVisible } = useActionContext();
const content = (
<ErrorBoundary FallbackComponent={ErrorFallback} onError={handleError}>
{props.children}
</ErrorBoundary>
);
return (
<StablePopover
{...props}
destroyTooltipOnHide
open={visible}
onOpenChange={(visible) => {
setVisible(visible);
}}
content={content}
>
{button}
</StablePopover>
);
},
{ displayName: 'Action.Popover' },
);
interface InternalActionProps {
containerRefKey: ActionContextProps['containerRefKey'];
fieldSchema: Schema;
designable: boolean;
field: Field;
actionTitle: string;
icon: string;
loading: boolean;
handleMouseEnter: (e: React.MouseEvent) => void;
tarComponent: React.ElementType;
className: string;
type: string;
Designer: React.ElementType;
onClick: (e: React.MouseEvent) => void;
confirm: {
enable: boolean;
content: string;
title: string;
};
confirmTitle: string;
popover: boolean;
addChild: boolean;
recordData: any;
title: string;
style: React.CSSProperties;
propsDisabled: boolean;
useAction: (actionCallback: (...args: any[]) => any) => {
run: () => void;
element: React.ReactNode;
disabled: boolean;
};
actionCallback: (...args: any[]) => any;
visibleWithURL: boolean;
setVisibleWithURL: (visible: boolean) => void;
setSubmitted: (v: boolean) => void;
getAriaLabel: (postfix?: string) => string;
parentRecordData: any;
}
Action.Popover.Footer = observer(
(props) => {
return (
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
width: '100%',
const InternalAction: React.FC<InternalActionProps> = observer(function Com(props) {
const {
containerRefKey,
fieldSchema,
designable,
field,
actionTitle,
icon,
loading,
handleMouseEnter,
tarComponent,
className,
type,
Designer,
onClick,
confirm,
confirmTitle,
popover,
addChild,
recordData,
title,
style,
propsDisabled,
useAction,
actionCallback,
visibleWithURL,
setVisibleWithURL,
setSubmitted,
getAriaLabel,
parentRecordData,
...others
} = props;
const [visible, setVisible] = useState(false);
const { wrapSSR, componentCls, hashId } = useStyles();
const [formValueChanged, setFormValueChanged] = useState(false);
const designerProps = fieldSchema['x-toolbar-props'] || fieldSchema['x-designer-props'];
const openMode = fieldSchema?.['x-component-props']?.['openMode'];
const openSize = fieldSchema?.['x-component-props']?.['openSize'];
const refreshDataBlockRequest = fieldSchema?.['x-component-props']?.['refreshDataBlockRequest'];
const { modal } = App.useApp();
const form = useForm();
const aclCtx = useACLActionParamsContext();
const { run, element, disabled: disableAction } = useAction?.(actionCallback) || ({} as any);
const disabled = form.disabled || field.disabled || field.data?.disabled || propsDisabled || disableAction;
const buttonStyle = useMemo(() => {
return {
...style,
opacity: designable && (field?.data?.hidden || !aclCtx) && 0.1,
color: disabled ? 'rgba(0, 0, 0, 0.25)' : style?.color,
};
}, [aclCtx, designable, field?.data?.hidden, style, disabled]);
const buttonProps = {
designable,
field,
aclCtx,
actionTitle,
icon,
loading,
disabled,
buttonStyle,
handleMouseEnter,
tarComponent,
designerProps,
componentCls,
hashId,
className,
others,
getAriaLabel,
type,
Designer,
openMode,
onClick,
refreshDataBlockRequest,
fieldSchema,
setVisible,
run,
confirm,
modal,
setSubmitted,
confirmTitle,
};
let result = (
<PopupVisibleProvider visible={false}>
<ActionContextProvider
button={RenderButton(buttonProps)}
visible={visible || visibleWithURL}
setVisible={(value) => {
setVisible?.(value);
setVisibleWithURL?.(value);
}}
formValueChanged={formValueChanged}
setFormValueChanged={setFormValueChanged}
openMode={openMode}
openSize={openSize}
containerRefKey={containerRefKey}
fieldSchema={fieldSchema}
setSubmitted={setSubmitted}
>
{props.children}
</div>
);
},
{ displayName: 'Action.Popover.Footer' },
);
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
{!popover && <RenderButton {...buttonProps} />}
<VariablePopupRecordProvider>{!popover && props.children}</VariablePopupRecordProvider>
{element}
</ActionContextProvider>
</PopupVisibleProvider>
);
if (isBulkEditAction(fieldSchema)) {
// Clear the context of Tabs to avoid affecting the Tabs of the upper-level popup
result = <TabsContextProvider>{result}</TabsContextProvider>;
}
if (addChild) {
return wrapSSR(
<RecordProvider record={null} parent={parentRecordData}>
<TreeRecordProvider parent={recordData}>{result}</TreeRecordProvider>
</RecordProvider>,
) as React.ReactElement;
}
return wrapSSR(result) as React.ReactElement;
});
InternalAction.displayName = 'InternalAction';
Action.Popover = function ActionPopover(props) {
const { button, visible, setVisible } = useActionContext();
const content = (
<ErrorBoundary FallbackComponent={ErrorFallback} onError={handleError}>
{props.children}
</ErrorBoundary>
);
return (
<StablePopover
{...props}
destroyTooltipOnHide
open={visible}
onOpenChange={(visible) => {
setVisible(visible);
}}
content={content}
>
{button}
</StablePopover>
);
};
Action.Popover.displayName = 'Action.Popover';
Action.Popover.Footer = (props) => {
return (
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
width: '100%',
}}
>
{props.children}
</div>
);
};
Action.Popover.Footer.displayName = 'Action.Popover.Footer';
Action.Link = ActionLink;
Action.Designer = ActionDesigner;
@ -305,7 +403,6 @@ function RenderButton({
openMode,
onClick,
refreshDataBlockRequest,
service,
fieldSchema,
setVisible,
run,
@ -314,9 +411,17 @@ function RenderButton({
setSubmitted,
confirmTitle,
}) {
const service = useDataBlockRequest();
const { t } = useTranslation();
const { isPopupVisibleControlledByURL } = usePopupSettings();
const { openPopup } = usePopupUtils();
const serviceRef = useRef(null);
serviceRef.current = service;
const openPopupRef = useRef(null);
openPopupRef.current = openPopup;
const handleButtonClick = useCallback(
(e: React.MouseEvent, checkPortal = true) => {
if (checkPortal && isPortalInBody(e.target as Element)) {
@ -331,7 +436,7 @@ function RenderButton({
onClick(e, () => {
if (refreshDataBlockRequest !== false) {
setSubmitted?.(true);
service?.refresh?.();
serviceRef.current?.refresh?.();
}
});
} else if (isBulkEditAction(fieldSchema) || !isPopupVisibleControlledByURL()) {
@ -343,7 +448,7 @@ function RenderButton({
['view', 'update', 'create', 'customize:popup'].includes(fieldSchema['x-action']) &&
fieldSchema['x-uid']
) {
openPopup();
openPopupRef.current();
} else {
setVisible(true);
run?.();
@ -362,43 +467,118 @@ function RenderButton({
}
},
[
disabled,
aclCtx,
actionTitle,
confirm?.enable,
confirm?.content,
confirm?.title,
confirm?.enable,
disabled,
modal,
onClick,
openPopup,
fieldSchema,
isPopupVisibleControlledByURL,
refreshDataBlockRequest,
run,
service,
setSubmitted,
setVisible,
run,
modal,
t,
confirmTitle,
actionTitle,
],
);
if (!designable && (field?.data?.hidden || !aclCtx)) {
return null;
}
return (
<SortableItem
role="button"
aria-label={getAriaLabel()}
{...others}
onMouseEnter={handleMouseEnter}
loading={field?.data?.loading || loading}
icon={typeof icon === 'string' ? <Icon type={icon} /> : icon}
<RenderButtonInner
designable={designable}
field={field}
aclCtx={aclCtx}
actionTitle={actionTitle}
icon={icon}
loading={loading}
disabled={disabled}
style={buttonStyle}
onClick={handleButtonClick}
component={tarComponent || Button}
className={classnames(componentCls, hashId, className, 'nb-action')}
type={type === 'danger' ? undefined : type}
>
{actionTitle && <span className={icon ? 'nb-action-title' : null}>{actionTitle}</span>}
<Designer {...designerProps} />
</SortableItem>
buttonStyle={buttonStyle}
handleMouseEnter={handleMouseEnter}
getAriaLabel={getAriaLabel}
handleButtonClick={handleButtonClick}
tarComponent={tarComponent}
componentCls={componentCls}
hashId={hashId}
className={className}
type={type}
Designer={Designer}
designerProps={designerProps}
{...others}
/>
);
}
const RenderButtonInner = observer(
(props: {
designable: boolean;
field: Field;
aclCtx: any;
actionTitle: string;
icon: string;
loading: boolean;
disabled: boolean;
buttonStyle: React.CSSProperties;
handleMouseEnter: (e: React.MouseEvent) => void;
getAriaLabel: (postfix?: string) => string;
handleButtonClick: (e: React.MouseEvent) => void;
tarComponent: React.ElementType;
componentCls: string;
hashId: string;
className: string;
type: string;
Designer: React.ElementType;
designerProps: any;
}) => {
const {
designable,
field,
aclCtx,
actionTitle,
icon,
loading,
disabled,
buttonStyle,
handleMouseEnter,
getAriaLabel,
handleButtonClick,
tarComponent,
componentCls,
hashId,
className,
type,
Designer,
designerProps,
...others
} = props;
if (!designable && (field?.data?.hidden || !aclCtx)) {
return null;
}
return (
<SortableItem
role="button"
aria-label={getAriaLabel()}
{...others}
onMouseEnter={handleMouseEnter}
// @ts-ignore
loading={field?.data?.loading || loading}
icon={typeof icon === 'string' ? <Icon type={icon} /> : icon}
disabled={disabled}
style={buttonStyle}
onClick={handleButtonClick}
component={tarComponent || Button}
className={classnames(componentCls, hashId, className, 'nb-action')}
type={type === 'danger' ? undefined : type}
>
{actionTitle && <span className={icon ? 'nb-action-title' : null}>{actionTitle}</span>}
<Designer {...designerProps} />
</SortableItem>
);
},
);
RenderButtonInner.displayName = 'RenderButtonInner';

View File

@ -106,7 +106,7 @@ describe('Action.Popover', () => {
const { container } = render(<App4 />);
const btn = container.querySelector('.ant-btn') as HTMLElement;
fireEvent.mouseEnter(btn);
fireEvent.click(btn);
await waitFor(() => {
// popover

View File

@ -1,4 +1,4 @@
import { ISchema, observer, useForm } from '@formily/react';
import { ISchema, useForm } from '@formily/react';
import {
Action,
CustomRouterContextProvider,
@ -53,7 +53,7 @@ const schema: ISchema = {
},
};
export default observer(() => {
export default () => {
return (
<Router location={window.location} navigator={null}>
<CustomRouterContextProvider>
@ -63,4 +63,4 @@ export default observer(() => {
</CustomRouterContextProvider>
</Router>
);
});
};

View File

@ -43,8 +43,6 @@ export const AssociationFieldProvider = observer(
[fieldSchema['x-component-props']?.mode],
);
const fieldValue = useMemo(() => JSON.stringify(field.value), [field.value]);
const [loading, setLoading] = useState(!field.readPretty);
useEffect(() => {
@ -93,8 +91,7 @@ export const AssociationFieldProvider = observer(
field.value = [];
}
setLoading(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentMode, collectionField, fieldValue]);
}, [currentMode, collectionField, field]);
if (loading) {
return null;

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { observer, useFieldSchema } from '@formily/react';
import { useFieldSchema } from '@formily/react';
import { toArr } from '@formily/shared';
import React, { Fragment, useRef } from 'react';
import { useDesignable } from '../../';
@ -100,9 +100,6 @@ const ButtonTabList: React.FC<ButtonListProps> = (props) => {
return <>{renderRecords()}</>;
};
export const ReadPrettyInternalTag: React.FC = observer(
(props: any) => {
return <ReadPrettyInternalViewer {...props} ButtonList={ButtonTabList} />;
},
{ displayName: 'ReadPrettyInternalTag' },
);
export const ReadPrettyInternalTag: React.FC = (props: any) => {
return <ReadPrettyInternalViewer {...props} ButtonList={ButtonTabList} />;
};

View File

@ -10,7 +10,7 @@
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { toArr } from '@formily/shared';
import _ from 'lodash';
import React, { FC, Fragment, useRef, useState } from 'react';
import React, { FC, Fragment, useEffect, useRef, useState } from 'react';
import { useDesignable } from '../../';
import { WithoutTableFieldResource } from '../../../block-provider';
import { CollectionRecordProvider, useCollectionManager, useCollectionRecordData } from '../../../data-source';
@ -49,6 +49,132 @@ export interface ButtonListProps {
};
}
const RenderRecord = React.memo(
({
fieldNames,
isTreeCollection,
compile,
getLabelUiSchema,
collectionField,
snapshot,
enableLink,
designable,
insertViewer,
fieldSchema,
openPopup,
recordData,
ellipsisWithTooltipRef,
value,
setBtnHover,
}: {
fieldNames: any;
isTreeCollection: boolean;
compile: (source: any, ext?: any) => any;
getLabelUiSchema;
collectionField: any;
snapshot: boolean;
enableLink: any;
designable: boolean;
insertViewer: (ss: any) => void;
fieldSchema;
openPopup;
recordData: any;
ellipsisWithTooltipRef: React.MutableRefObject<IEllipsisWithTooltipRef>;
value: any;
setBtnHover: any;
}) => {
const [loading, setLoading] = useState(true);
const [result, setResult] = useState<React.ReactNode[]>([]);
// The map method here maybe quite time-consuming, especially in table blocks.
// Therefore, we use an asynchronous approach to render the list,
// which allows us to avoid blocking the main rendering process.
useEffect(() => {
const result = toArr(value).map((record, index, arr) => {
const value = record?.[fieldNames?.label || 'label'];
const label = isTreeCollection
? transformNestedData(record)
.map((o) => o?.[fieldNames?.label || 'label'])
.join(' / ')
: isObject(value)
? JSON.stringify(value)
: value;
const val = toValue(compile(label), 'N/A');
const labelUiSchema = getLabelUiSchema(
record?.__collection || collectionField?.target,
fieldNames?.label || 'label',
);
const text = getLabelFormatValue(compile(labelUiSchema), val, true);
return (
<Fragment key={`${record?.id}_${index}`}>
<span>
{snapshot ? (
text
) : enableLink !== false ? (
<a
onMouseEnter={() => {
setBtnHover(true);
}}
onClick={(e) => {
setBtnHover(true);
e.stopPropagation();
e.preventDefault();
if (designable) {
insertViewer(schema.Viewer);
}
if (fieldSchema.properties) {
openPopup({
recordData: record,
parentRecordData: recordData,
});
}
ellipsisWithTooltipRef?.current?.setPopoverVisible(false);
}}
>
{text}
</a>
) : (
text
)}
</span>
{index < arr.length - 1 ? <span style={{ marginRight: 4, color: '#aaa' }}>,</span> : null}
</Fragment>
);
});
setResult(result);
setLoading(false);
}, [
collectionField?.target,
compile,
designable,
ellipsisWithTooltipRef,
enableLink,
fieldNames?.label,
fieldSchema?.properties,
getLabelUiSchema,
insertViewer,
isTreeCollection,
openPopup,
recordData,
setBtnHover,
snapshot,
value,
]);
if (loading) {
return null;
}
return <>{result}</>;
},
);
RenderRecord.displayName = 'RenderRecord';
const ButtonLinkList: FC<ButtonListProps> = (props) => {
const fieldSchema = useFieldSchema();
const cm = useCollectionManager();
@ -66,63 +192,25 @@ const ButtonLinkList: FC<ButtonListProps> = (props) => {
const { openPopup } = usePopupUtils();
const recordData = useCollectionRecordData();
const renderRecords = () =>
toArr(props.value).map((record, index, arr) => {
const value = record?.[fieldNames?.label || 'label'];
const label = isTreeCollection
? transformNestedData(record)
.map((o) => o?.[fieldNames?.label || 'label'])
.join(' / ')
: isObject(value)
? JSON.stringify(value)
: value;
const val = toValue(compile(label), 'N/A');
const labelUiSchema = getLabelUiSchema(
record?.__collection || collectionField?.target,
fieldNames?.label || 'label',
);
const text = getLabelFormatValue(compile(labelUiSchema), val, true);
return (
<Fragment key={`${record?.id}_${index}`}>
<span>
{snapshot ? (
text
) : enableLink !== false ? (
<a
onMouseEnter={() => {
props.setBtnHover(true);
}}
onClick={(e) => {
props.setBtnHover(true);
e.stopPropagation();
e.preventDefault();
if (designable) {
insertViewer(schema.Viewer);
}
// fix https://nocobase.height.app/T-4794/description
if (fieldSchema.properties) {
openPopup({
recordData: record,
parentRecordData: recordData,
});
}
ellipsisWithTooltipRef?.current?.setPopoverVisible(false);
}}
>
{text}
</a>
) : (
text
)}
</span>
{index < arr.length - 1 ? <span style={{ marginRight: 4, color: '#aaa' }}>,</span> : null}
</Fragment>
);
});
return <>{renderRecords()}</>;
return (
<RenderRecord
fieldNames={fieldNames}
isTreeCollection={isTreeCollection}
compile={compile}
getLabelUiSchema={getLabelUiSchema}
collectionField={collectionField}
snapshot={snapshot}
enableLink={enableLink}
designable={designable}
insertViewer={insertViewer}
fieldSchema={fieldSchema}
openPopup={openPopup}
recordData={recordData}
ellipsisWithTooltipRef={ellipsisWithTooltipRef}
value={props.value}
setBtnHover={props.setBtnHover}
/>
);
};
interface ReadPrettyInternalViewerProps {

View File

@ -17,21 +17,18 @@ import { ReadPrettyInternalTag } from './InternalTag';
import { ReadPrettyInternalViewer } from './InternalViewer';
import { useAssociationFieldContext } from './hooks';
const ReadPrettyAssociationField = observer(
(props: any) => {
const { currentMode } = useAssociationFieldContext();
return (
<>
{['Select', 'Picker', 'CascadeSelect'].includes(currentMode) && <ReadPrettyInternalViewer {...props} />}
{currentMode === 'Tag' && <ReadPrettyInternalTag {...props} />}
{currentMode === 'Nester' && <InternalNester {...props} />}
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
{currentMode === 'FileManager' && <FileManageReadPretty {...props} />}
</>
);
},
{ displayName: 'ReadPrettyAssociationField' },
);
const ReadPrettyAssociationField = (props: any) => {
const { currentMode } = useAssociationFieldContext();
return (
<>
{['Select', 'Picker', 'CascadeSelect'].includes(currentMode) && <ReadPrettyInternalViewer {...props} />}
{currentMode === 'Tag' && <ReadPrettyInternalTag {...props} />}
{currentMode === 'Nester' && <InternalNester {...props} />}
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
{currentMode === 'FileManager' && <FileManageReadPretty {...props} />}
</>
);
};
export const ReadPretty = observer(
(props) => {

View File

@ -137,7 +137,7 @@ export const useFieldNames = (
const fieldSchema = useFieldSchema();
const fieldNames =
fieldSchema['x-component-props']?.['field']?.['uiSchema']?.['x-component-props']?.['fieldNames'] ||
fieldSchema?.['x-component-props']?.['fieldNames'] ||
fieldSchema['x-component-props']?.['fieldNames'] ||
props.fieldNames;
return { label: 'label', value: 'value', ...fieldNames };
};

View File

@ -7,33 +7,33 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { ISchema, Schema } from '@formily/react';
import { Field } from '@formily/core';
import { ISchema } from '@formily/react';
import { isArr } from '@formily/shared';
import { getDefaultFormat, str2moment } from '@nocobase/utils/client';
import { Tag } from 'antd';
import dayjs from 'dayjs';
import React from 'react';
import { CollectionFieldOptions_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
import { Field } from '@formily/core';
import { useCollectionManager } from '../../../data-source/collection/CollectionManagerProvider';
export const useLabelUiSchemaV2 = () => {
const { getCollectionJoinField } = useCollectionManager_deprecated();
const cm = useCollectionManager();
return (collectionName: string, label: string): ISchema => {
if (!collectionName) {
return;
}
const labelField = getCollectionJoinField(`${collectionName}.${label}`) as CollectionFieldOptions_deprecated;
const labelField = cm?.getCollectionField(`${collectionName}.${label}`);
return labelField?.uiSchema;
};
};
export const useLabelUiSchema = (collectionName: string, label: string): ISchema => {
const { getCollectionJoinField } = useCollectionManager_deprecated();
const cm = useCollectionManager();
if (!collectionName) {
return;
}
const labelField = getCollectionJoinField(`${collectionName}.${label}`) as CollectionFieldOptions_deprecated;
const labelField = cm?.getCollectionField(`${collectionName}.${label}`);
return labelField?.uiSchema;
};

View File

@ -16,7 +16,7 @@ import type {
CheckboxProps as AntdCheckboxProps,
} from 'antd/es/checkbox';
import uniq from 'lodash/uniq';
import React, { FC, useMemo } from 'react';
import React, { FC, useEffect, useState } from 'react';
import { useCollectionField } from '../../../data-source/collection-field/CollectionFieldProvider';
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
@ -74,23 +74,32 @@ Checkbox.Group = connect(
if (!isValid(props.value)) {
return null;
}
const [content, setContent] = useState<React.ReactNode[]>([]);
const [loading, setLoading] = useState(true);
const field = useField<any>();
const collectionField = useCollectionField();
const tags = useMemo(() => {
// The map method here maybe quite time-consuming, especially in table blocks.
// Therefore, we use an asynchronous approach to render the list,
// which allows us to avoid blocking the main rendering process.
useEffect(() => {
const dataSource = field.dataSource || collectionField?.uiSchema.enum || [];
const value = uniq(field.value ? field.value : []);
return dataSource.filter((option) => value.includes(option.value));
const tags = dataSource.filter((option) => value.includes(option.value));
const content = tags.map((option, key) => (
<Tag key={key} color={option.color} icon={option.icon}>
{option.label}
</Tag>
));
setContent(content);
setLoading(false);
}, [field.value]);
return (
<EllipsisWithTooltip ellipsis={props.ellipsis}>
{tags.map((option, key) => (
<Tag key={key} color={option.color} icon={option.icon}>
{option.label}
</Tag>
))}
</EllipsisWithTooltip>
);
if (loading) {
return null;
}
return <EllipsisWithTooltip ellipsis={props.ellipsis}>{content}</EllipsisWithTooltip>;
}),
);
Checkbox.Group.displayName = 'Checkbox.Group';

View File

@ -11,6 +11,7 @@ import { connect, mapReadPretty, useFieldSchema } from '@formily/react';
import { Select, SelectProps } from 'antd';
import React, { useEffect, useMemo, useState } from 'react';
import { useCollection_deprecated } from '../../../collection-manager';
import { useCollection } from '../../../data-source/collection/CollectionProvider';
import { useCompile } from '../../hooks';
import { EllipsisWithTooltip } from '../input';
import Cron from './Cron';
@ -101,8 +102,8 @@ const ReadPretty = (props: CronReadPrettyProps) => {
const { value } = props;
const compile = useCompile();
const fieldSchema = useFieldSchema();
const { getField } = useCollection_deprecated();
const uiSchemaOptions = getField(fieldSchema?.name)?.uiSchema.enum;
const collection = useCollection();
const uiSchemaOptions = collection?.getField(fieldSchema?.name)?.uiSchema.enum;
const options = useMemo(() => {
return (props.options || []).concat((uiSchemaOptions as any[]) || []);

View File

@ -9,7 +9,6 @@
import { observer, useField, useFieldSchema } from '@formily/react';
import React, { useEffect } from 'react';
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import { useCollection_deprecated } from '../../../collection-manager';
import { useCompile } from '../../hooks';
@ -20,15 +19,10 @@ export const FormField: any = observer(
const field = useField();
const collectionField = getField(fieldSchema.name);
const compile = useCompile();
const ctx = useFormBlockContext();
useEffect(() => {
if (!field.title) {
field.title = compile(collectionField?.uiSchema?.title);
}
if (ctx?.field) {
ctx.field.added = ctx.field.added || new Set();
ctx.field.added.add(fieldSchema.name);
}
}, []);
return <div>{props.children}</div>;
},

View File

@ -8,12 +8,12 @@
*/
import { Popover } from 'antd';
import React, { CSSProperties, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
import React, { CSSProperties, forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react';
const getContentWidth = (element) => {
if (element) {
const getContentWidth = (el: HTMLElement) => {
if (el) {
const range = document.createRange();
range.selectNodeContents(element);
range.selectNodeContents(el);
const contentWidth = range.getBoundingClientRect().width;
return contentWidth;
}
@ -26,6 +26,13 @@ const ellipsisDefaultStyle: CSSProperties = {
wordBreak: 'break-all',
};
const isOverflowTooltip = (el: HTMLElement) => {
if (!el) return false;
const contentWidth = getContentWidth(el);
const offsetWidth = el.offsetWidth;
return contentWidth > offsetWidth;
};
interface IEllipsisWithTooltipProps {
ellipsis: boolean;
popoverContent: unknown;
@ -36,28 +43,25 @@ export const EllipsisWithTooltip = forwardRef((props: Partial<IEllipsisWithToolt
const [ellipsis, setEllipsis] = useState(false);
const [visible, setVisible] = useState(false);
const elRef: any = useRef();
useImperativeHandle(ref, () => {
return {
setPopoverVisible: setVisible,
};
});
const isOverflowTooltip = useCallback(() => {
if (!elRef.current) return false;
const contentWidth = getContentWidth(elRef.current);
const offsetWidth = elRef.current?.offsetWidth;
return contentWidth > offsetWidth;
}, [elRef.current]);
useImperativeHandle(
ref,
() => {
return {
setPopoverVisible: setVisible,
};
},
[],
);
const divContent = useMemo(
() =>
props.ellipsis ? (
<div
ref={elRef}
style={{ ...ellipsisDefaultStyle }}
style={ellipsisDefaultStyle}
onMouseEnter={(e) => {
const el = e.target as any;
const isShowTooltips = isOverflowTooltip();
const isShowTooltips = isOverflowTooltip(elRef.current);
if (isShowTooltips) {
setEllipsis(el.scrollWidth >= el.clientWidth);
}
@ -74,7 +78,6 @@ export const EllipsisWithTooltip = forwardRef((props: Partial<IEllipsisWithToolt
if (!props.ellipsis || !ellipsis) {
return divContent;
}
const { popoverContent } = props;
return (
<Popover
@ -90,7 +93,7 @@ export const EllipsisWithTooltip = forwardRef((props: Partial<IEllipsisWithToolt
maxHeight: 400,
}}
>
{popoverContent || props.children}
{props.popoverContent || props.children}
</div>
}
>

View File

@ -12,7 +12,7 @@ import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__';
import { useFieldSchema } from '@formily/react';
import { Image, Typography } from 'antd';
import cls from 'classnames';
import React from 'react';
import React, { useMemo } from 'react';
import { useCompile } from '../../hooks';
import { EllipsisWithTooltip } from './EllipsisWithTooltip';
import { HTMLEncode } from './shared';
@ -45,6 +45,12 @@ ReadPretty.Input = (props: InputReadPrettyProps) => {
const prefixCls = usePrefixCls('description-input', props);
// eslint-disable-next-line react-hooks/rules-of-hooks
const compile = useCompile();
// eslint-disable-next-line react-hooks/rules-of-hooks
const content = useMemo(
() => (props.value && typeof props.value === 'object' ? JSON.stringify(props.value) : compile(props.value)),
[props.value],
);
return (
<div
className={cls(prefixCls, props.className)}
@ -52,9 +58,7 @@ ReadPretty.Input = (props: InputReadPrettyProps) => {
>
{props.addonBefore}
{props.prefix}
<EllipsisWithTooltip ellipsis={props.ellipsis}>
{props.value && typeof props.value === 'object' ? JSON.stringify(props.value) : compile(props.value)}
</EllipsisWithTooltip>
<EllipsisWithTooltip ellipsis={props.ellipsis}>{content}</EllipsisWithTooltip>
{props.suffix}
{props.addonAfter}
</div>
@ -80,26 +84,31 @@ ReadPretty.TextArea = (props) => {
const prefixCls = usePrefixCls('description-textarea', props);
// eslint-disable-next-line react-hooks/rules-of-hooks
const compile = useCompile();
const value = compile(props.value ?? '');
const { autop = true, ellipsis, text } = props;
const html = (
<div
style={{ lineHeight: 'inherit' }}
dangerouslySetInnerHTML={{
__html: HTMLEncode(value).split('\n').join('<br/>'),
}}
/>
);
const { autop: atop = true, ellipsis, text } = props;
// eslint-disable-next-line react-hooks/rules-of-hooks
const content = useMemo(() => {
const value = compile(props.value ?? '');
const html = (
<div
style={{ lineHeight: 'inherit' }}
dangerouslySetInnerHTML={{
__html: HTMLEncode(value).split('\n').join('<br/>'),
}}
/>
);
return ellipsis ? (
<EllipsisWithTooltip ellipsis={ellipsis} popoverContent={atop ? html : value}>
{text || value}
</EllipsisWithTooltip>
) : atop ? (
html
) : (
value
);
}, [atop, ellipsis, props.value, text]);
const content = ellipsis ? (
<EllipsisWithTooltip ellipsis={ellipsis} popoverContent={autop ? html : value}>
{text || value}
</EllipsisWithTooltip>
) : autop ? (
html
) : (
value
);
return (
<div
className={cls(prefixCls, props.className)}
@ -139,22 +148,26 @@ ReadPretty.Html = (props) => {
const prefixCls = usePrefixCls('description-textarea', props);
// eslint-disable-next-line react-hooks/rules-of-hooks
const compile = useCompile();
const value = compile(props.value ?? '');
const { autop = true, ellipsis } = props;
const html = (
<div
style={{ lineHeight: '1.42' }}
dangerouslySetInnerHTML={{
__html: value,
}}
/>
);
const text = convertToText(value);
const content = (
<EllipsisWithTooltip ellipsis={ellipsis} popoverContent={autop ? html : value}>
{ellipsis ? text : html}
</EllipsisWithTooltip>
);
// eslint-disable-next-line react-hooks/rules-of-hooks
const content = useMemo(() => {
const value = compile(props.value ?? '');
const { autop = true, ellipsis } = props;
const html = (
<div
style={{ lineHeight: '1.42' }}
dangerouslySetInnerHTML={{
__html: value,
}}
/>
);
const text = convertToText(value);
return (
<EllipsisWithTooltip ellipsis={ellipsis} popoverContent={autop ? html : value}>
{ellipsis ? text : html}
</EllipsisWithTooltip>
);
}, [props.value]);
return (
<div
className={cls(prefixCls, props.className)}
@ -236,23 +249,22 @@ export interface JSONTextAreaReadPrettyProps {
ellipsis?: boolean;
}
const JSONClassName = css`
margin-bottom: 0;
line-height: 1.5;
font-size: 90%;
`;
ReadPretty.JSON = (props: JSONTextAreaReadPrettyProps) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const prefixCls = usePrefixCls('json', props);
const content = props.value != null ? JSON.stringify(props.value, null, props.space ?? 2) : '';
// eslint-disable-next-line react-hooks/rules-of-hooks
const content = useMemo(
() => (props.value != null ? JSON.stringify(props.value, null, props.space ?? 2) : ''),
[props.space, props.value],
);
const JSONContent = (
<pre
className={cx(
prefixCls,
props.className,
css`
margin-bottom: 0;
line-height: 1.5;
font-size: 90%;
`,
)}
style={props.style}
>
<pre className={cx(prefixCls, props.className, JSONClassName)} style={props.style}>
{content}
</pre>
);
@ -260,7 +272,7 @@ ReadPretty.JSON = (props: JSONTextAreaReadPrettyProps) => {
if (props.ellipsis) {
return (
<EllipsisWithTooltip ellipsis={props.ellipsis} popoverContent={JSONContent}>
<Typography.Text>{content}</Typography.Text>
{content}
</EllipsisWithTooltip>
);
}

View File

@ -10,7 +10,7 @@
import { LoadingOutlined } from '@ant-design/icons';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { Input as AntdInput, Spin } from 'antd';
import React from 'react';
import React, { useMemo } from 'react';
import { useGlobalTheme } from '../../../global-theme';
import { ReadPretty as InputReadPretty } from '../input';
import { MarkdownVoid } from './Markdown.Void';
@ -36,16 +36,19 @@ export const MarkdownReadPretty = (props) => {
const { isDarkTheme } = useGlobalTheme();
const { wrapSSR, hashId, componentCls: className } = useStyles({ isDarkTheme });
const { html = '', loading } = useParseMarkdown(props.value);
const text = convertToText(html);
const text = useMemo(() => convertToText(html), [html]);
if (loading) {
return wrapSSR(<Spin />);
}
const value = (
<div
className={`${hashId} ${className} nb-markdown nb-markdown-default nb-markdown-table`}
dangerouslySetInnerHTML={{ __html: html }}
/>
);
if (loading) {
return wrapSSR(<Spin />);
}
return wrapSSR(<InputReadPretty.TextArea {...props} autop={false} text={text} value={value} />);
};

View File

@ -7,19 +7,21 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import _ from 'lodash';
import { useEffect, useState } from 'react';
export async function parseMarkdown(text: string) {
export const parseMarkdown = _.memoize(async (text: string) => {
if (!text) {
return text;
}
const m = await import('./md');
return m.default.render(text);
}
});
export function useParseMarkdown(text: string) {
const [html, setHtml] = useState<any>('');
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
parseMarkdown(text)
@ -29,6 +31,7 @@ export function useParseMarkdown(text: string) {
})
.catch((error) => console.log(error));
}, [text]);
return { html, loading };
}

View File

@ -20,7 +20,6 @@ import {
import { uid } from '@formily/shared';
import { error } from '@nocobase/utils/client';
import { Menu as AntdMenu, MenuProps } from 'antd';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { createDesignable, DndContext, SortableItem, useDesignable, useDesigner } from '../..';
@ -31,6 +30,18 @@ import { useMenuTranslation } from './locale';
import { MenuDesigner } from './Menu.Designer';
import { findKeysByUid, findMenuItem } from './util';
import React, {
createContext,
// @ts-ignore
startTransition,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
const subMenuDesignerCss = css`
position: relative;
display: inline-block;
@ -231,39 +242,41 @@ const HeaderMenu = ({
const handleSelect = useCallback(
(info: { item; key; keyPath; domEvent }) => {
const s = schema.properties?.[info.key];
startTransition(() => {
const s = schema.properties?.[info.key];
if (!s) {
return;
}
if (mode === 'mix') {
if (s['x-component'] !== 'Menu.SubMenu') {
onSelect?.(info);
} else {
const menuItemSchema = findMenuItem(s);
if (!menuItemSchema) {
return onSelect?.(info);
}
// TODO
setLoading(true);
const keys = findKeysByUid(schema, menuItemSchema['x-uid']);
setDefaultSelectedKeys(keys);
setTimeout(() => {
setLoading(false);
}, 100);
onSelect?.({
key: menuItemSchema.name,
item: {
props: {
schema: menuItemSchema,
},
},
});
if (!s) {
return;
}
} else {
onSelect?.(info);
}
if (mode === 'mix') {
if (s['x-component'] !== 'Menu.SubMenu') {
onSelect?.(info);
} else {
const menuItemSchema = findMenuItem(s);
if (!menuItemSchema) {
return onSelect?.(info);
}
// TODO
setLoading(true);
const keys = findKeysByUid(schema, menuItemSchema['x-uid']);
setDefaultSelectedKeys(keys);
setTimeout(() => {
setLoading(false);
}, 100);
onSelect?.({
key: menuItemSchema.name,
item: {
props: {
schema: menuItemSchema,
},
},
});
}
} else {
onSelect?.(info);
}
});
},
[schema, mode, onSelect, setLoading, setDefaultSelectedKeys],
);
@ -301,11 +314,19 @@ const SideMenu = ({
}) => {
const { Component, getMenuItems } = useMenuItem();
// fix https://nocobase.height.app/T-3331/description
// 使用 ref 用来防止闭包问题
const sideMenuSchemaRef = useRef(sideMenuSchema);
sideMenuSchemaRef.current = sideMenuSchema;
const handleSelect = useCallback(
(info) => {
startTransition(() => {
onSelect?.(info);
});
},
[onSelect],
);
const items = useMemo(() => {
const result = getMenuItems(() => {
return <RecursionField key={uid()} schema={sideMenuSchema} onlyRenderProperties />;
@ -351,7 +372,7 @@ const SideMenu = ({
mode={'inline'}
openKeys={openKeys}
selectedKeys={selectedKeys}
onClick={onSelect}
onClick={handleSelect}
onOpenChange={setOpenKeys}
className={sideMenuClass}
items={items as MenuProps['items']}

View File

@ -145,7 +145,7 @@ export const usePopupUtils = (
const collection = useCollection();
const cm = useCollectionManager();
const association = useAssociationName();
const { visible, setVisible } = useContext(PopupVisibleProviderContext) || { visible: false, setVisible: () => {} };
const { visible, setVisible } = useContext(PopupVisibleProviderContext) || { visible: false, setVisible: _.noop };
const { params: popupParams } = useCurrentPopupContext();
const service = useDataBlockRequest();
const { isPopupVisibleControlledByURL } = usePopupSettings();

View File

@ -13,11 +13,9 @@ import React, { Fragment, useRef, useState } from 'react';
import { WithoutTableFieldResource } from '../../../block-provider';
// TODO: 不要使用 '../../../block-provider' 这个路径引用 BlockAssociationContext在 Vitest 中会报错,待修复
import { BlockAssociationContext } from '../../../block-provider/BlockProvider';
import {
CollectionProvider_deprecated,
useCollection_deprecated,
useCollectionManager_deprecated,
} from '../../../collection-manager';
import { CollectionProvider_deprecated } from '../../../collection-manager';
import { useCollectionManager } from '../../../data-source/collection/CollectionManagerProvider';
import { useCollection } from '../../../data-source/collection/CollectionProvider';
import { RecordProvider, useRecord } from '../../../record-provider';
import { FormProvider } from '../../core';
import { useCompile } from '../../hooks';
@ -44,23 +42,23 @@ export const ReadPrettyRecordPicker: React.FC = observer(
const { ellipsis } = props;
const fieldSchema = useFieldSchema();
const recordCtx = useRecord();
const { getCollectionJoinField } = useCollectionManager_deprecated();
const cm = useCollectionManager();
// value 做了转换,但 props.value 和原来 useField().value 的值不一致
// const field = useField<Field>();
const fieldNames = useFieldNames(props);
const [visible, setVisible] = useState(false);
const { getField } = useCollection_deprecated();
const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema?.['x-collection-field']);
const collection = useCollection();
const collectionField =
collection?.getField(fieldSchema.name) || cm?.getCollectionField(fieldSchema?.['x-collection-field']);
const [record, setRecord] = useState({});
const compile = useCompile();
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
const showFilePicker = isShowFilePicker(labelUiSchema);
const { snapshot } = useActionContext();
const isTagsMode = fieldSchema['x-component-props']?.mode === 'tags';
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
if (showFilePicker) {
if (isShowFilePicker(labelUiSchema)) {
return collectionField ? <Preview {...props} /> : null;
}

View File

@ -11,10 +11,10 @@ import { isArrayField } from '@formily/core';
import { observer, useField } from '@formily/react';
import { isValid } from '@formily/shared';
import { Tag } from 'antd';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { useCollectionField } from '../../../data-source/collection-field/CollectionFieldProvider';
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
import { FieldNames, defaultFieldNames, getCurrentOptions } from './utils';
import { useCollectionField } from '../../../data-source/collection-field/CollectionFieldProvider';
export interface SelectReadPrettyProps {
value: any;
@ -29,30 +29,49 @@ export interface SelectReadPrettyProps {
export const ReadPretty = observer(
(props: SelectReadPrettyProps) => {
const fieldNames = { ...defaultFieldNames, ...props.fieldNames };
const [loading, setLoading] = useState(true);
const [content, setContent] = useState<React.ReactNode[]>([]);
const field = useField<any>();
const collectionField = useCollectionField();
const dataSource = field.dataSource || props.options || collectionField?.uiSchema.enum || [];
const currentOptions = getCurrentOptions(field.value, dataSource, fieldNames);
if (!isValid(props.value) && !currentOptions.length) {
return <div />;
}
if (isArrayField(field) && field?.value?.length === 0) {
return <div />;
// The map method here maybe quite time-consuming, especially in table blocks.
// Therefore, we use an asynchronous approach to render the list,
// which allows us to avoid blocking the main rendering process.
useEffect(() => {
const fieldNames = { ...defaultFieldNames, ...props.fieldNames };
const dataSource = field.dataSource || props.options || collectionField?.uiSchema.enum || [];
const currentOptions = getCurrentOptions(field.value, dataSource, fieldNames);
if (!isValid(props.value) && !currentOptions.length) {
return;
}
if (isArrayField(field) && field?.value?.length === 0) {
return;
}
const content = currentOptions.map((option, index) => (
<Tag key={index} color={option[fieldNames.color]} icon={option.icon}>
{option[fieldNames.label]}
</Tag>
));
setContent(content);
setLoading(false);
}, [
collectionField?.uiSchema.enum,
field,
field.dataSource,
field.value,
props.fieldNames,
props.options,
props.value,
]);
if (loading) {
return null;
}
return (
<div>
<EllipsisWithTooltip ellipsis={props.ellipsis}>
{currentOptions.map((option, key) => (
<Tag key={key} color={option[fieldNames.color]} icon={option.icon}>
{option[fieldNames.label]}
</Tag>
))}
</EllipsisWithTooltip>
</div>
);
return <EllipsisWithTooltip ellipsis={props.ellipsis}>{content}</EllipsisWithTooltip>;
},
{ displayName: 'ReadPretty' },
{ displayName: 'SelectReadPretty' },
);

View File

@ -43,22 +43,22 @@ function flatData(data: any[], fieldNames: FieldNames): any[] {
return newArr;
}
function findOptions(options: any[], fieldNames: FieldNames, arrValues: any[]): Option[] {
if (!options) return [];
const current: Option[] = [];
for (const value of arrValues) {
const option = options.find((v) => v[fieldNames.value] == value) || {
value,
label: value ? value.toString() : value,
};
current.push(option);
}
return current;
}
export function getCurrentOptions(values: any | any[], dataSource: any[], fieldNames: FieldNames): Option[] {
const result = flatData(dataSource, fieldNames);
const arrValues = castArray(values).map((val) => (isPlainObject(val) ? val[fieldNames.value] : val)) as any[];
function findOptions(options: any[]): Option[] {
if (!options) return [];
const current: Option[] = [];
for (const value of arrValues) {
const option = options.find((v) => v[fieldNames.value] == value) || {
value,
label: value ? value.toString() : value,
};
current.push(option);
}
return current;
}
return findOptions(result);
return findOptions(result, fieldNames, arrValues);
}

View File

@ -8,7 +8,7 @@
*/
import { useField, useFieldSchema } from '@formily/react';
import React, { useLayoutEffect } from 'react';
import React, { useLayoutEffect, useMemo } from 'react';
import {
CollectionFieldContext,
SortableItem,
@ -25,7 +25,6 @@ export const useColumnSchema = () => {
const { getField } = useCollection_deprecated();
const compile = useCompile();
const columnSchema = useFieldSchema();
const columnField = useField();
const { getCollectionJoinField } = useCollectionManager_deprecated();
const fieldSchema = columnSchema?.reduceProperties((buf, s) => {
if (isCollectionFieldComponent(s)) {
@ -38,19 +37,36 @@ export const useColumnSchema = () => {
return {};
}
const path = columnField.path?.splice(columnField.path?.length - 1, 1);
const filedInstanceList = columnField.form.query(`${path.concat(`*.` + fieldSchema.name)}`).map();
const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema?.['x-collection-field']);
return {
columnSchema,
fieldSchema,
collectionField,
uiSchema: compile(collectionField?.uiSchema),
filedInstanceList,
};
};
export const useTableFieldInstanceList = () => {
const columnField = useField();
const { fieldSchema } = useColumnSchema();
const filedInstanceList = useMemo(() => {
if (!fieldSchema || !columnField) {
return [];
}
const path = columnField.path?.splice(columnField.path?.length - 1, 1);
// TODO: 这里需要优化,性能比较差,在 M2 pro 的机器上这行代码会运行将近 0.1 毫秒
return columnField.form.query(`${path.concat(`*.` + fieldSchema.name)}`).map();
}, [columnField, fieldSchema]);
if (!fieldSchema) {
return [];
}
return filedInstanceList;
};
export const TableColumnDecorator = (props) => {
const Designer = useDesigner();
const field = useField();

View File

@ -18,10 +18,10 @@ import { action } from '@formily/reactive';
import { uid } from '@formily/shared';
import { isPortalInBody } from '@nocobase/utils/client';
import { useCreation, useDeepCompareEffect, useMemoizedFn } from 'ahooks';
import { Table as AntdTable, Skeleton, TableColumnProps } from 'antd';
import { Table as AntdTable, Spin, TableColumnProps } from 'antd';
import { default as classNames, default as cls } from 'classnames';
import _, { omit } from 'lodash';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import React, { useCallback, useContext, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useInView } from 'react-intersection-observer';
import { DndContext, useDesignable, useTableSize } from '../..';
@ -30,6 +30,8 @@ import {
RecordProvider,
useCollection,
useCollectionParentRecordData,
useDataBlockRequest,
useFlag,
useSchemaInitializerRender,
useTableSelectorContext,
} from '../../../';
@ -42,8 +44,8 @@ import { useToken } from '../__builtins__';
import { SubFormProvider } from '../association-field/hooks';
import { ColumnFieldProvider } from './components/ColumnFieldProvider';
import { extractIndex, isCollectionFieldComponent, isColumnComponent } from './utils';
import { useDataBlockRequest } from '../../../';
const MemoizedAntdTable = React.memo(AntdTable);
const InViewContext = React.createContext(false);
const useArrayField = (props) => {
const field = useField<ArrayField>();
@ -147,7 +149,7 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }) => {
);
},
onCell: (record, rowIndex) => {
return { record, schema: s, rowIndex };
return { record, schema: s, rowIndex, isSubTable: props.isSubTable };
},
} as TableColumnProps<any>;
}),
@ -211,13 +213,29 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }) => {
return tableColumns;
};
const SortableRow = (props) => {
// How many rows should be displayed on initial render
const INITIAL_ROWS_NUMBER = 20;
const SortableRow = (props: {
rowIndex: number;
onClick: (e: any) => void;
style: React.CSSProperties;
className: string;
}) => {
const { isInSubTable } = useFlag();
const { token } = useToken();
const id = props['data-row-key']?.toString();
const { setNodeRef, isOver, active, over } = useSortable({
id,
});
const { ref, inView } = useInView({
threshold: 0,
triggerOnce: true,
initialInView: !!process.env.__E2E__ || isInSubTable || props.rowIndex < INITIAL_ROWS_NUMBER,
skip: !!process.env.__E2E__ || isInSubTable || props.rowIndex < INITIAL_ROWS_NUMBER,
});
const classObj = useMemo(() => {
const borderColor = new TinyColor(token.colorSettings).setAlpha(0.6).toHex8String();
return {
@ -240,11 +258,18 @@ const SortableRow = (props) => {
: classObj.bottomActiveClass;
return (
<tr
ref={active?.id !== id ? setNodeRef : null}
{...props}
className={classNames(props.className, { [className]: active && isOver })}
/>
<InViewContext.Provider value={inView}>
<tr
ref={(node) => {
if (active?.id !== id) {
setNodeRef(node);
}
ref(node);
}}
{...props}
className={classNames(props.className, { [className]: active && isOver })}
/>
</InViewContext.Provider>
);
};
@ -416,10 +441,36 @@ const HeaderCellComponent = (props) => {
return <th {...props} className={cls(props.className, headerClass)} />;
};
const BodyRowComponent = (props) => {
const BodyRowComponent = (props: {
rowIndex: number;
onClick: (e: any) => void;
style: React.CSSProperties;
className: string;
}) => {
return <SortableRow {...props} />;
};
const BodyCellComponent = (props) => {
const { token } = useToken();
const inView = useContext(InViewContext);
const isIndex = props.className?.includes('selection-column');
const { record, schema, rowIndex, isSubTable, ...others } = props;
const { valueMap } = useSatisfiedActionValues({ formValues: record, category: 'style', schema });
const style = useMemo(() => Object.assign({ ...props.style }, valueMap), [props.style, valueMap]);
const skeletonStyle = {
height: '1em',
backgroundColor: 'rgba(0, 0, 0, 0.06)',
borderRadius: `${token.borderRadiusSM}px`,
};
return (
<td {...others} className={classNames(props.className, cellClass)} style={style}>
{/* Lazy rendering cannot be used in sub-tables. */}
{isSubTable || inView || isIndex ? props.children : <div style={skeletonStyle} />}
</td>
);
};
interface TableProps {
/** @deprecated */
useProps?: () => any;
@ -437,6 +488,112 @@ interface TableProps {
isSubTable?: boolean;
}
const InternalNocoBaseTable = React.memo(
(props: {
tableHeight: number;
SortableWrapper: React.FC<{}>;
tableSizeRefCallback: (instance: HTMLDivElement) => void;
defaultRowKey: (record: any) => any;
dataSource: any[];
restProps: { rowSelection: any };
paginationProps: any;
components: {
header: { wrapper: (props: any) => React.JSX.Element; cell: (props: any) => React.JSX.Element };
body: {
wrapper: (props: any) => React.JSX.Element;
row: (props: any) => React.JSX.Element;
cell: (props: any) => React.JSX.Element;
};
};
onTableChange: any;
onRow: (record: any) => { onClick: (e: any) => void };
rowClassName: (record: any) => string;
scroll: { x: string; y: number };
columns: any[];
expandable: { onExpand: (flag: any, record: any) => void; expandedRowKeys: any };
field: ArrayField<any, any>;
}): React.ReactElement<any, any> => {
const {
tableHeight,
SortableWrapper,
tableSizeRefCallback,
defaultRowKey,
dataSource,
paginationProps,
components,
onTableChange,
onRow,
rowClassName,
scroll,
columns,
expandable,
field,
...others
} = props;
return (
<div
className={cx(
css`
height: 100%;
overflow: hidden;
.ant-table-wrapper {
height: 100%;
.ant-spin-nested-loading {
height: 100%;
.ant-spin-container {
height: 100%;
display: flex;
flex-direction: column;
.ant-table-expanded-row-fixed {
min-height: ${tableHeight}px;
}
.ant-table-body {
min-height: ${tableHeight}px;
}
}
}
}
.ant-table {
overflow-x: auto;
overflow-y: hidden;
}
`,
'nb-table-container',
)}
>
<SortableWrapper>
<AntdTable
ref={tableSizeRefCallback as any}
rowKey={defaultRowKey}
// rowKey={(record) => record.id}
dataSource={dataSource}
tableLayout="auto"
{...others}
pagination={paginationProps}
components={components}
onChange={onTableChange}
onRow={onRow}
rowClassName={rowClassName}
scroll={scroll}
columns={columns}
expandable={expandable}
/>
</SortableWrapper>
{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">
{field.errors.map((error) => {
return error.messages.map((message) => <div key={message}>{message}</div>);
})}
</div>
)}
</div>
);
},
);
InternalNocoBaseTable.displayName = 'InternalNocoBaseTable';
export const Table: any = withDynamicSchemaProps(
observer((props: TableProps) => {
const { token } = useToken();
@ -488,7 +645,7 @@ export const Table: any = withDynamicSchemaProps(
const onRow = useMemo(() => {
if (onClickRow) {
return (record) => {
return (record, rowIndex) => {
return {
onClick: (e) => {
if (isPortalInBody(e.target)) {
@ -496,6 +653,7 @@ export const Table: any = withDynamicSchemaProps(
}
onClickRow(record, setSelectedRow, selectedRow);
},
rowIndex,
};
};
}
@ -556,67 +714,38 @@ export const Table: any = withDynamicSchemaProps(
[JSON.stringify(rowKey), defaultRowKey],
);
const dataSourceKeys = field?.value?.map?.(getRowKey);
const memoizedDataSourceKeys = useMemo(() => dataSourceKeys, [JSON.stringify(dataSourceKeys)]);
const dataSource = useMemo(() => {
const value = Array.isArray(field?.value) ? field.value : [];
return value.filter(Boolean);
}, [field?.value, field?.value?.length, memoizedDataSourceKeys]);
const bodyWrapperComponent = useMemo(() => {
// If we don't depend on "field?.value?.length", it will cause no response when clicking "Add new" in the SubTable
}, [field?.value, field?.value?.length]);
const BodyWrapperComponent = useMemo(() => {
return (props) => {
const onDragEndCallback = useCallback((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] || e.active;
const to = field.value[toIndex] || e.over;
void field.move(fromIndex, toIndex);
onRowDragEnd({ from, to });
}, []);
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] || e.active;
const to = field.value[toIndex] || e.over;
void field.move(fromIndex, toIndex);
onRowDragEnd({ from, to });
}}
>
<DndContext onDragEnd={onDragEndCallback}>
<tbody {...props} />
</DndContext>
);
};
}, [onRowDragEnd, field]);
}, [field, onRowDragEnd]);
const BodyCellComponent = useCallback(
(props) => {
const isIndex = props.className?.includes('selection-column');
const { record, schema, rowIndex, ...others } = props;
const { ref, inView } = useInView({
threshold: 0,
triggerOnce: true,
initialInView: isIndex || !!process.env.__E2E__,
skip: isIndex || !!process.env.__E2E__,
});
const { valueMap } = useSatisfiedActionValues({ formValues: record, category: 'style', schema });
const style = useMemo(() => Object.assign({ ...props.style }, valueMap), [props.style, valueMap]);
// fix the problem of blank rows at the beginning of a table block
if (rowIndex < 20) {
return (
<td {...others} className={classNames(props.className, cellClass)} style={style}>
{props.children}
</td>
);
}
return (
<td {...props} ref={ref} className={classNames(props.className, cellClass)} style={style}>
{/* 子表格中不能使用懒渲染。详见https://nocobase.height.app/T-4889/description */}
{others.isSubTable || inView || isIndex ? props.children : <Skeleton.Button style={{ height: '100%' }} />}
</td>
);
},
[others.isSubTable],
);
// @ts-ignore
BodyWrapperComponent.displayName = 'BodyWrapperComponent';
const components = useMemo(() => {
return {
@ -625,12 +754,12 @@ export const Table: any = withDynamicSchemaProps(
cell: HeaderCellComponent,
},
body: {
wrapper: bodyWrapperComponent,
wrapper: BodyWrapperComponent,
row: BodyRowComponent,
cell: BodyCellComponent,
},
};
}, [BodyCellComponent, bodyWrapperComponent]);
}, [BodyWrapperComponent]);
const memoizedRowSelection = useMemo(() => rowSelection, [JSON.stringify(rowSelection)]);
@ -761,65 +890,29 @@ export const Table: any = withDynamicSchemaProps(
expandedRowKeys: expandedKeys,
};
}, [expandedKeys, onExpandValue]);
return (
<div
className={cx(
css`
height: 100%;
overflow: hidden;
.ant-table-wrapper {
height: 100%;
.ant-spin-nested-loading {
height: 100%;
.ant-spin-container {
height: 100%;
display: flex;
flex-direction: column;
.ant-table-expanded-row-fixed {
min-height: ${tableHeight}px;
}
.ant-table-body {
min-height: ${tableHeight}px;
}
}
}
}
.ant-table {
overflow-x: auto;
overflow-y: hidden;
}
`,
'nb-table-container',
)}
>
<SortableWrapper>
<MemoizedAntdTable
ref={tableSizeRefCallback}
rowKey={defaultRowKey}
// rowKey={(record) => record.id}
dataSource={dataSource}
tableLayout="auto"
{...others}
{...restProps}
loading={loading}
pagination={paginationProps}
components={components}
onChange={onTableChange}
onRow={onRow}
rowClassName={rowClassName}
scroll={scroll}
columns={columns}
expandable={expandable}
/>
</SortableWrapper>
{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">
{field.errors.map((error) => {
return error.messages.map((message) => <div key={message}>{message}</div>);
})}
</div>
)}
</div>
// If spinning is set to undefined, it will cause the subtable to always display loading, so we need to convert it here
<Spin spinning={!!loading}>
<InternalNocoBaseTable
tableHeight={tableHeight}
SortableWrapper={SortableWrapper}
tableSizeRefCallback={tableSizeRefCallback}
defaultRowKey={defaultRowKey}
dataSource={dataSource}
{...others}
{...restProps}
paginationProps={paginationProps}
components={components}
onTableChange={onTableChange}
onRow={onRow}
rowClassName={rowClassName}
scroll={scroll}
columns={columns}
expandable={expandable}
field={field}
/>
</Spin>
);
}),
{ displayName: 'NocoBaseTable' },

View File

@ -10,7 +10,6 @@
import { Field } from '@formily/core';
import { observer, useField, useFieldSchema, useForm } from '@formily/react';
import React, { useEffect } from 'react';
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import { useCollection_deprecated } from '../../../collection-manager';
import { useCompile } from '../../hooks';
import { ActionBar } from '../action';
@ -22,15 +21,10 @@ export const TableField: any = observer(
const field = useField<Field>();
const collectionField = getField(fieldSchema.name);
const compile = useCompile();
const ctx = useFormBlockContext();
useEffect(() => {
if (!field.title) {
field.title = compile(collectionField?.uiSchema?.title);
}
if (ctx?.field) {
ctx.field.added = ctx.field.added || new Set();
ctx.field.added.add(fieldSchema.name);
}
}, []);
useEffect(() => {
field.decoratorProps.asterisk = fieldSchema.required;

View File

@ -7,42 +7,45 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { observer, RecursionField } from '@formily/react';
import React from 'react';
import { useRecord } from '../../../../record-provider';
import { RecursionField } from '@formily/react';
import React, { useMemo } from 'react';
import { useCollection } from '../../../../data-source';
export const ColumnFieldProvider = observer(
(props: { schema: any; basePath: any; children: any }) => {
const { schema, basePath } = props;
const record = useRecord();
const collection = useCollection();
const fieldSchema = schema.reduceProperties((buf, s) => {
if (s['x-component'] === 'CollectionField') {
return s;
}
return buf;
}, null);
const collectionField = fieldSchema && collection.getField(fieldSchema['x-collection-field']);
import { useRecord } from '../../../../record-provider';
if (
fieldSchema &&
record?.__collection &&
collectionField &&
['select', 'multipleSelect'].includes(collectionField.interface)
) {
const fieldName = `${record.__collection}.${fieldSchema.name}`;
const newSchema = {
...schema.toJSON(),
properties: {
[fieldSchema.name]: {
...fieldSchema.toJSON(),
'x-collection-field': fieldName,
},
export const ColumnFieldProvider = (props: { schema: any; basePath: any; children: any }) => {
const { schema, basePath } = props;
const record = useRecord();
const collection = useCollection();
const fieldSchema = useMemo(
() =>
schema.reduceProperties((buf, s) => {
if (s['x-component'] === 'CollectionField') {
return s;
}
return buf;
}, null),
[schema],
);
const collectionField = fieldSchema && collection.getField(fieldSchema['x-collection-field']);
if (
fieldSchema &&
record?.__collection &&
collectionField &&
['select', 'multipleSelect'].includes(collectionField.interface)
) {
const fieldName = `${record.__collection}.${fieldSchema.name}`;
const newSchema = {
...schema.toJSON(),
properties: {
[fieldSchema.name]: {
...fieldSchema.toJSON(),
'x-collection-field': fieldName,
},
};
return <RecursionField basePath={basePath} schema={newSchema} onlyRenderProperties />;
}
return props.children;
},
{ displayName: 'ColumnFieldProvider' },
);
},
};
return <RecursionField basePath={basePath} schema={newSchema} onlyRenderProperties />;
}
return props.children;
};

View File

@ -495,13 +495,25 @@ async function preloadOptions(scope, value: string) {
return options;
}
const textAreaReadPrettyClassName = css`
overflow: auto;
.ant-tag {
display: inline;
line-height: 19px;
margin: 0 0.25em;
padding: 2px 7px;
border-radius: 10px;
}
`;
TextArea.ReadPretty = function ReadPretty(props): JSX.Element {
const { value } = props;
const scope = typeof props.scope === 'function' ? props.scope() : props.scope;
const { wrapSSR, hashId, componentCls } = useStyles();
const [options, setOptions] = useState([]);
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [options]);
const html = useMemo(() => renderHTML(value ?? '', keyLabelMap), [keyLabelMap, value]);
useEffect(() => {
preloadOptions(scope, value)
@ -510,26 +522,11 @@ TextArea.ReadPretty = function ReadPretty(props): JSX.Element {
})
.catch(error);
}, [scope, value]);
const html = renderHTML(value ?? '', keyLabelMap);
const content = wrapSSR(
<span
dangerouslySetInnerHTML={{ __html: html }}
className={cx(
componentCls,
hashId,
css`
overflow: auto;
.ant-tag {
display: inline;
line-height: 19px;
margin: 0 0.25em;
padding: 2px 7px;
border-radius: 10px;
}
`,
)}
className={cx(componentCls, hashId, textAreaReadPrettyClassName)}
/>,
);

View File

@ -19,6 +19,11 @@ interface Props {
const compileCache = {};
const hasVariable = (source: string) => {
const reg = /{{.*?}}/g;
return reg.test(source);
};
export const useCompile = ({ noCache }: Props = { noCache: false }) => {
const options = useContext(SchemaOptionsContext);
const scope = useContext(SchemaExpressionScopeContext);
@ -34,13 +39,13 @@ export const useCompile = ({ noCache }: Props = { noCache: false }) => {
// source is Component Object, for example: { 'x-component': "Cascader", type: "array", title: "所属地区(行政区划)" }
if (source && typeof source === 'object' && !isValidElement(source)) {
shouldCompile = true;
cacheKey = JSON.stringify(source);
shouldCompile = hasVariable(cacheKey);
}
// source is Array, for example: [{ 'title': "{{ ('Admin')}}", name: 'admin' }, { 'title': "{{ ('Root')}}", name: 'root' }]
if (Array.isArray(source)) {
shouldCompile = true;
shouldCompile = hasVariable(JSON.stringify(source));
}
if (shouldCompile) {

View File

@ -10,15 +10,17 @@
import { Field } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react';
import { useEffect } from 'react';
import { useCollectionManager } from '../../data-source/collection/CollectionManagerProvider';
import { useCollection } from '../../data-source/collection/CollectionProvider';
import { useCompile } from './useCompile';
import { useCollection_deprecated, useCollectionManager_deprecated } from '../../collection-manager';
export const useFieldTitle = () => {
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { getField } = useCollection_deprecated();
const { getCollectionJoinField } = useCollectionManager_deprecated();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const collection = useCollection();
const cm = useCollectionManager();
const collectionField =
collection?.getField(fieldSchema['name']) || cm?.getCollectionField(fieldSchema['x-collection-field']);
const compile = useCompile();
useEffect(() => {
if (!field?.title) {

View File

@ -47,14 +47,6 @@ const InternalField: React.FC = (props) => {
field.required = !!uiSchema['required'];
}
};
const ctx = useFormBlockContext();
useEffect(() => {
if (ctx?.field) {
ctx.field.added = ctx.field.added || new Set();
ctx.field.added.add(fieldSchema.name);
}
});
useEffect(() => {
if (!uiSchema) {

View File

@ -189,6 +189,19 @@ export const useDateVariable = ({ operator, schema, noDisabled }: Props) => {
return result;
};
/**
* `日期变量`
* @returns
*/
export const useDatetimeVariableContext = () => {
const { utc = true } = useDatePickerContext();
const datetimeCtx = useMemo(() => getDateRanges({ shouldBeString: true, utc }), [utc]);
return {
datetimeCtx,
};
};
/**
* `日期变量`
* @param param0
@ -197,7 +210,6 @@ export const useDateVariable = ({ operator, schema, noDisabled }: Props) => {
export const useDatetimeVariable = ({ operator, schema, noDisabled, targetFieldSchema }: Props = {}) => {
const { t } = useTranslation();
const { getOperator } = useOperators();
const { utc = true } = useDatePickerContext();
const datetimeSettings = useMemo(() => {
const operatorValue = operator?.value || getOperator(targetFieldSchema?.name) || '';
@ -348,7 +360,7 @@ export const useDatetimeVariable = ({ operator, schema, noDisabled, targetFieldS
};
}, [schema?.['x-component'], targetFieldSchema]);
const datetimeCtx = useMemo(() => getDateRanges({ shouldBeString: true, utc }), [utc]);
const { datetimeCtx } = useDatetimeVariableContext();
return {
datetimeSettings,

View File

@ -70,6 +70,28 @@ const useCurrentFormData = () => {
return ctx?.data?.data?.[0] || ctx?.data?.data;
};
/**
* `当前表单` hook
* @param param0
* @returns
*/
export const useCurrentFormContext = ({ form: _form }: Pick<Props, 'form'> = {}) => {
const { form } = useFormBlockContext();
const formData = useCurrentFormData();
const { isVariableParsedInOtherContext } = useFlag();
const formInstance = _form || form;
return {
/** 变量值 */
currentFormCtx:
formInstance?.readPretty === false && formInstance?.values && Object.keys(formInstance?.values)?.length
? formInstance?.values
: formData || formInstance?.values,
/** 用来判断是否可以显示`当前表单`变量 */
shouldDisplayCurrentForm: formInstance && !formInstance.readPretty && !isVariableParsedInOtherContext,
};
};
/**
* `当前表单`
* @param param0
@ -82,11 +104,9 @@ export const useCurrentFormVariable = ({
targetFieldSchema,
form: _form,
}: Props = {}) => {
// const { getActiveFieldsName } = useFormActiveFields() || {};
const { currentFormCtx, shouldDisplayCurrentForm } = useCurrentFormContext({ form: _form });
const { t } = useTranslation();
const { form, collectionName } = useFormBlockContext();
const formData = useCurrentFormData();
const { isVariableParsedInOtherContext } = useFlag();
const { collectionName } = useFormBlockContext();
const currentFormSettings = useBaseVariable({
collectionField,
uiSchema: schema,
@ -109,16 +129,12 @@ export const useCurrentFormVariable = ({
},
});
const formInstance = _form || form;
return {
/** 变量配置 */
currentFormSettings,
/** 变量值 */
currentFormCtx:
formInstance?.readPretty === false && formInstance?.values && Object.keys(formInstance?.values)?.length
? formInstance?.values
: formData || formInstance?.values,
currentFormCtx,
/** 用来判断是否可以显示`当前表单`变量 */
shouldDisplayCurrentForm: formInstance && !formInstance.readPretty && !isVariableParsedInOtherContext,
shouldDisplayCurrentForm,
};
};

View File

@ -65,6 +65,24 @@ export const useIterationVariable = ({
return result;
};
/**
* `当前对象` hook
* @returns
*/
export const useCurrentObjectContext = () => {
const { isInSubForm, isInSubTable } = useFlag() || {};
const { formValue: currentObjectCtx, collection: collectionOfCurrentObject } = useSubFormValue();
return {
/** 是否显示变量 */
shouldDisplayCurrentObject: isInSubForm || isInSubTable,
/** 变量的值 */
currentObjectCtx,
/** 变量的 collection */
collectionOfCurrentObject,
};
};
/**
* `当前对象`
* @param param0
@ -84,9 +102,8 @@ export const useCurrentObjectVariable = ({
} = {}) => {
// const { getActiveFieldsName } = useFormActiveFields() || {};
const collection = useCollection();
const { formValue: currentObjectCtx, collection: collectionOfCurrentObject } = useSubFormValue();
const { isInSubForm, isInSubTable } = useFlag() || {};
const { t } = useTranslation();
const { shouldDisplayCurrentObject, currentObjectCtx, collectionOfCurrentObject } = useCurrentObjectContext();
const currentObjectSettings = useBaseVariable({
collectionField,
uiSchema: schema,
@ -111,7 +128,7 @@ export const useCurrentObjectVariable = ({
return {
/** 是否显示变量 */
shouldDisplayCurrentObject: isInSubForm || isInSubTable,
shouldDisplayCurrentObject,
/** 变量的值 */
currentObjectCtx,
/** 变量的配置项 */

View File

@ -14,6 +14,20 @@ import { useFlag } from '../../../flag-provider';
import { useSubFormValue } from '../../../schema-component/antd/association-field/hooks';
import { useBaseVariable } from './useBaseVariable';
export const useParentObjectContext = () => {
const { parent } = useSubFormValue();
const { value: parentObjectCtx, collection: collectionOfParentObject } = parent || {};
const { isInSubForm, isInSubTable } = useFlag() || {};
return {
/** 是否显示变量 */
shouldDisplayParentObject: (isInSubForm || isInSubTable) && !!collectionOfParentObject,
/** 变量的值 */
parentObjectCtx,
collectionName: collectionOfParentObject?.name,
};
};
/**
* `上级对象`
* @param param0
@ -32,10 +46,8 @@ export const useParentObjectVariable = ({
targetFieldSchema?: Schema;
} = {}) => {
// const { getActiveFieldsName } = useFormActiveFields() || {};
const { parent } = useSubFormValue();
const { value: parentObjectCtx, collection: collectionOfParentObject } = parent || {};
const { isInSubForm, isInSubTable } = useFlag() || {};
const { t } = useTranslation();
const { shouldDisplayParentObject, parentObjectCtx, collectionName } = useParentObjectContext();
const parentObjectSettings = useBaseVariable({
collectionField,
uiSchema: schema,
@ -43,7 +55,7 @@ export const useParentObjectVariable = ({
maxDepth: 4,
name: '$nParentIteration',
title: t('Parent object'),
collectionName: collectionOfParentObject?.name,
collectionName,
noDisabled,
returnFields: (fields, option) => {
return fields;
@ -58,12 +70,12 @@ export const useParentObjectVariable = ({
});
return {
/** 是否显示变量 */
shouldDisplayParentObject: (isInSubForm || isInSubTable) && !!collectionOfParentObject,
/** 变量的值 */
parentObjectCtx,
/** 变量的配置项 */
parentObjectSettings,
collectionName: collectionOfParentObject?.name,
/** 是否显示变量 */
shouldDisplayParentObject,
/** 变量的值 */
parentObjectCtx,
collectionName,
};
};

View File

@ -12,27 +12,14 @@ import { useParentPopupRecord } from '../../../modules/variable/variablesProvide
import { useBaseVariable } from './useBaseVariable';
/**
* `Parent popup record`
* @param props
* `Parent popup record`
* @returns
*/
export const useParentPopupVariable = (props: any = {}) => {
export const useParentPopupVariableContext = () => {
const { value, title, collection } = useParentPopupRecord() || {};
const { isVariableParsedInOtherContext } = useFlag();
const settings = useBaseVariable({
collectionField: props.collectionField,
uiSchema: props.schema,
name: '$nParentPopupRecord',
title,
collectionName: collection?.name,
noDisabled: props.noDisabled,
targetFieldSchema: props.targetFieldSchema,
dataSource: collection?.dataSource,
});
return {
/** 变量配置 */
settings,
/** 变量值 */
parentPopupRecordCtx: value,
/** 用于判断是否需要显示配置项 */
@ -40,6 +27,38 @@ export const useParentPopupVariable = (props: any = {}) => {
/** 当前记录对应的 collection name */
collectionName: collection?.name,
dataSource: collection?.dataSource,
/** 不可删除 */
defaultValue: undefined,
title,
};
};
/**
* `Parent popup record`
* @param props
* @returns
*/
export const useParentPopupVariable = (props: any = {}) => {
const { parentPopupRecordCtx, shouldDisplayParentPopupRecord, collectionName, dataSource, defaultValue, title } =
useParentPopupVariableContext();
const settings = useBaseVariable({
collectionField: props.collectionField,
uiSchema: props.schema,
name: '$nParentPopupRecord',
title,
collectionName,
noDisabled: props.noDisabled,
targetFieldSchema: props.targetFieldSchema,
dataSource,
});
return {
/** 变量配置 */
settings,
parentPopupRecordCtx,
shouldDisplayParentPopupRecord,
collectionName,
dataSource,
defaultValue,
};
};

View File

@ -50,31 +50,17 @@ export const useParentRecordVariable = (props: Props) => {
};
/**
* `上级记录`
* @param props
* `上级记录`
* @returns
*/
export const useCurrentParentRecordVariable = (props: Props = {}) => {
const { t } = useTranslation();
export const useCurrentParentRecordContext = () => {
const record = useCollectionRecord();
const { name: parentCollectionName, dataSource: parentDataSource } = useParentCollection() || {};
const collection = useCollection();
const { isInSubForm, isInSubTable } = useFlag() || {};
const dataSource = parentCollectionName ? parentDataSource : collection?.dataSource;
const currentParentRecordSettings = useBaseVariable({
collectionField: props.collectionField,
uiSchema: props.schema,
name: '$nParentRecord',
title: t('Parent record'),
collectionName: parentCollectionName || collection?.name,
noDisabled: props.noDisabled,
targetFieldSchema: props.targetFieldSchema,
dataSource,
});
return {
currentParentRecordSettings,
// 当该变量使用在区块数据范围的时候,由于某些区块(如 Table是在 DataBlockProvider 之前解析 filter 的,
// 导致此时 record.parentRecord 的值还是空的,此时正确的值应该是 record所以在后面加了 record?.data 来防止这种情况
currentParentRecordCtx: record?.parentRecord?.data || record?.data,
@ -84,3 +70,33 @@ export const useCurrentParentRecordVariable = (props: Props = {}) => {
dataSource,
};
};
/**
* `上级记录`
* @param props
* @returns
*/
export const useCurrentParentRecordVariable = (props: Props = {}) => {
const { t } = useTranslation();
const { currentParentRecordCtx, shouldDisplayCurrentParentRecord, collectionName, dataSource } =
useCurrentParentRecordContext();
const currentParentRecordSettings = useBaseVariable({
collectionField: props.collectionField,
uiSchema: props.schema,
name: '$nParentRecord',
title: t('Parent record'),
collectionName,
noDisabled: props.noDisabled,
targetFieldSchema: props.targetFieldSchema,
dataSource,
});
return {
currentParentRecordSettings,
currentParentRecordCtx,
shouldDisplayCurrentParentRecord,
collectionName,
dataSource,
};
};

View File

@ -12,27 +12,14 @@ import { useCurrentPopupRecord } from '../../../modules/variable/variablesProvid
import { useBaseVariable } from './useBaseVariable';
/**
* `Current popup record`
* @param props
* `Current popup record`
* @returns
*/
export const usePopupVariable = (props: any = {}) => {
export const usePopupVariableContext = () => {
const { value, title, collection } = useCurrentPopupRecord() || {};
const { isVariableParsedInOtherContext } = useFlag();
const settings = useBaseVariable({
collectionField: props.collectionField,
uiSchema: props.schema,
name: '$nPopupRecord',
title,
collectionName: collection?.name,
noDisabled: props.noDisabled,
targetFieldSchema: props.targetFieldSchema,
dataSource: collection?.dataSource,
});
return {
/** 变量配置 */
settings,
/** 变量值 */
popupRecordCtx: value,
/** 用于判断是否需要显示配置项 */
@ -40,6 +27,38 @@ export const usePopupVariable = (props: any = {}) => {
/** 当前记录对应的 collection name */
collectionName: collection?.name,
dataSource: collection?.dataSource,
/** 不可删除*/
defaultValue: undefined,
title,
};
};
/**
* `Current popup record`
* @param props
* @returns
*/
export const usePopupVariable = (props: any = {}) => {
const { popupRecordCtx, shouldDisplayPopupRecord, collectionName, dataSource, defaultValue, title } =
usePopupVariableContext();
const settings = useBaseVariable({
collectionField: props.collectionField,
uiSchema: props.schema,
name: '$nPopupRecord',
title,
collectionName,
noDisabled: props.noDisabled,
targetFieldSchema: props.targetFieldSchema,
dataSource,
});
return {
/** 变量配置 */
settings,
popupRecordCtx,
shouldDisplayPopupRecord,
collectionName,
dataSource,
defaultValue,
};
};

View File

@ -49,6 +49,29 @@ export const useRecordVariable = (props: Props) => {
return currentRecordVariable;
};
/**
* `当前记录`
* @returns
*/
export const useCurrentRecordContext = () => {
const { name: blockType } = useBlockContext() || {};
const collection = useCollection();
const recordData = useCollectionRecordData();
const { formRecord, collectionName } = useFormBlockContext();
const realCollectionName = formRecord?.data ? collectionName : collection?.name;
return {
/** 变量值 */
currentRecordCtx: formRecord?.data || recordData,
/** 用于判断是否需要显示配置项 */
shouldDisplayCurrentRecord: !_.isEmpty(_.omit(recordData, ['__collectionName', '__parent'])) || !!formRecord?.data,
/** 当前记录对应的 collection name */
collectionName: realCollectionName,
/** 块类型 */
blockType,
};
};
/**
* `当前记录`
* @param props
@ -56,17 +79,13 @@ export const useRecordVariable = (props: Props) => {
*/
export const useCurrentRecordVariable = (props: Props = {}) => {
const { t } = useTranslation();
const { name: blockType } = useBlockContext() || {};
const collection = useCollection();
const recordData = useCollectionRecordData();
const { formRecord, collectionName } = useFormBlockContext();
const realCollectionName = formRecord?.data ? collectionName : collection?.name;
const { currentRecordCtx, shouldDisplayCurrentRecord, collectionName, blockType } = useCurrentRecordContext();
const currentRecordSettings = useBaseVariable({
collectionField: props.collectionField,
uiSchema: props.schema,
name: '$nRecord',
title: t('Current record'),
collectionName: realCollectionName,
collectionName,
noDisabled: props.noDisabled,
targetFieldSchema: props.targetFieldSchema,
deprecated: blockType === 'form',
@ -77,10 +96,10 @@ export const useCurrentRecordVariable = (props: Props = {}) => {
/** 变量配置 */
currentRecordSettings,
/** 变量值 */
currentRecordCtx: formRecord?.data || recordData,
currentRecordCtx,
/** 用于判断是否需要显示配置项 */
shouldDisplayCurrentRecord: !_.isEmpty(_.omit(recordData, ['__collectionName', '__parent'])) || !!formRecord?.data,
shouldDisplayCurrentRecord,
/** 当前记录对应的 collection name */
collectionName: realCollectionName,
collectionName,
};
};

View File

@ -9,11 +9,12 @@
import { Form } from '@formily/core';
import { Schema, useFieldSchema } from '@formily/react';
import React, { useContext, useMemo } from 'react';
import React, { useCallback, useContext, useMemo } from 'react';
import { useFormBlockContext, useFormBlockType } from '../../block-provider/FormBlockProvider';
import { useCollectionManager_deprecated } from '../../collection-manager/hooks/useCollectionManager_deprecated';
import { useCollection_deprecated } from '../../collection-manager/hooks/useCollection_deprecated';
import { CollectionFieldOptions_deprecated } from '../../collection-manager/types';
import { useCollectionManager } from '../../data-source/collection/CollectionManagerProvider';
import { useCollection } from '../../data-source/collection/CollectionProvider';
import { useDataSourceManager } from '../../data-source/data-source/DataSourceManagerProvider';
import { isSystemField } from '../SchemaSettings';
import { isPatternDisabled } from '../isPatternDisabled';
@ -50,27 +51,45 @@ export const DefaultValueProvider = (props: DefaultValueProviderProps) => {
};
export const useIsAllowToSetDefaultValue = ({ form, fieldSchema, collectionField }: Props = {}) => {
const { getInterface, getCollectionJoinField } = useCollectionManager_deprecated();
const { getField } = useCollection_deprecated();
const dm = useDataSourceManager();
const cm = useCollectionManager();
const collection = useCollection();
const { form: innerForm } = useFormBlockContext();
const innerFieldSchema = useFieldSchema();
const { type } = useFormBlockType();
const { isAllowToSetDefaultValue = _isAllowToSetDefaultValue } = useContext(DefaultValueContext) || {};
const innerCollectionField =
getField(innerFieldSchema['name']) || getCollectionJoinField(innerFieldSchema['x-collection-field']);
return {
isAllowToSetDefaultValue: (isSubTableColumn?: boolean) => {
return isAllowToSetDefaultValue({
collectionField: collectionField || innerCollectionField,
getInterface,
form: form || innerForm,
formBlockType: type,
fieldSchema: fieldSchema || innerFieldSchema,
isSubTableColumn,
});
},
const result = {
isAllowToSetDefaultValue: useCallback(
(isSubTableColumn?: boolean) => {
const innerCollectionField =
collection.getField(innerFieldSchema['name']) ||
cm.getCollectionField(innerFieldSchema['x-collection-field']);
return isAllowToSetDefaultValue({
collectionField: collectionField || innerCollectionField,
getInterface: dm?.collectionFieldInterfaceManager.getFieldInterface.bind(dm?.collectionFieldInterfaceManager),
form: form || innerForm,
formBlockType: type,
fieldSchema: fieldSchema || innerFieldSchema,
isSubTableColumn,
});
},
[
cm,
collection,
collectionField,
dm?.collectionFieldInterfaceManager,
fieldSchema,
form,
innerFieldSchema,
innerForm,
isAllowToSetDefaultValue,
type,
],
),
};
return result;
};
export const interfacesOfUnsupportedDefaultValue = [

View File

@ -11,14 +11,14 @@ import { Form } from '@formily/core';
import { useMemo } from 'react';
import { useCollection_deprecated } from '../../collection-manager';
import { useBlockCollection } from '../../schema-settings/VariableInput/hooks/useBlockCollection';
import { useDatetimeVariable } from '../../schema-settings/VariableInput/hooks/useDateVariable';
import { useCurrentFormVariable } from '../../schema-settings/VariableInput/hooks/useFormVariable';
import { useCurrentObjectVariable } from '../../schema-settings/VariableInput/hooks/useIterationVariable';
import { useParentObjectVariable } from '../../schema-settings/VariableInput/hooks/useParentIterationVariable';
import { useParentPopupVariable } from '../../schema-settings/VariableInput/hooks/useParentPopupVariable';
import { useCurrentParentRecordVariable } from '../../schema-settings/VariableInput/hooks/useParentRecordVariable';
import { usePopupVariable } from '../../schema-settings/VariableInput/hooks/usePopupVariable';
import { useCurrentRecordVariable } from '../../schema-settings/VariableInput/hooks/useRecordVariable';
import { useDatetimeVariableContext } from '../../schema-settings/VariableInput/hooks/useDateVariable';
import { useCurrentFormContext } from '../../schema-settings/VariableInput/hooks/useFormVariable';
import { useCurrentObjectContext } from '../../schema-settings/VariableInput/hooks/useIterationVariable';
import { useParentObjectContext } from '../../schema-settings/VariableInput/hooks/useParentIterationVariable';
import { useParentPopupVariableContext } from '../../schema-settings/VariableInput/hooks/useParentPopupVariable';
import { useCurrentParentRecordContext } from '../../schema-settings/VariableInput/hooks/useParentRecordVariable';
import { usePopupVariableContext } from '../../schema-settings/VariableInput/hooks/usePopupVariable';
import { useCurrentRecordContext } from '../../schema-settings/VariableInput/hooks/useRecordVariable';
import { VariableOption } from '../types';
import useContextVariable from './useContextVariable';
@ -32,28 +32,28 @@ const useLocalVariables = (props?: Props) => {
parentObjectCtx,
shouldDisplayParentObject,
collectionName: collectionNameOfParentObject,
} = useParentObjectVariable();
const { currentObjectCtx, shouldDisplayCurrentObject } = useCurrentObjectVariable();
const { currentRecordCtx, collectionName: collectionNameOfRecord } = useCurrentRecordVariable();
} = useParentObjectContext();
const { currentObjectCtx, shouldDisplayCurrentObject } = useCurrentObjectContext();
const { currentRecordCtx, collectionName: collectionNameOfRecord } = useCurrentRecordContext();
const {
currentParentRecordCtx,
collectionName: collectionNameOfParentRecord,
dataSource: currentParentRecordDataSource,
} = useCurrentParentRecordVariable();
} = useCurrentParentRecordContext();
const {
popupRecordCtx,
collectionName: collectionNameOfPopupRecord,
dataSource: popupDataSource,
defaultValue: defaultValueOfPopupRecord,
} = usePopupVariable();
} = usePopupVariableContext();
const {
parentPopupRecordCtx,
collectionName: collectionNameOfParentPopupRecord,
dataSource: parentPopupDataSource,
defaultValue: defaultValueOfParentPopupRecord,
} = useParentPopupVariable();
const { datetimeCtx } = useDatetimeVariable();
const { currentFormCtx } = useCurrentFormVariable({ form: props?.currentForm });
} = useParentPopupVariableContext();
const { datetimeCtx } = useDatetimeVariableContext();
const { currentFormCtx } = useCurrentFormContext({ form: props?.currentForm });
const { name: currentCollectionName } = useCollection_deprecated();
const contextVariable = useContextVariable();
let { name } = useBlockCollection();

View File

@ -17,7 +17,6 @@ import {
useCollection_deprecated,
useCompile,
useComponent,
useFormBlockContext,
} from '@nocobase/client';
import { Checkbox, Select, Space } from 'antd';
import React, { useEffect, useState } from 'react';
@ -36,14 +35,6 @@ const InternalField: React.FC = (props) => {
const setFieldProps = (key, value) => {
field[key] = typeof field[key] === 'undefined' ? value : field[key];
};
const ctx = useFormBlockContext();
useEffect(() => {
if (ctx?.field) {
ctx.field.added = ctx.field.added || new Set();
ctx.field.added.add(fieldSchema.name);
}
});
useEffect(() => {
if (!uiSchema) {

View File

@ -8,15 +8,15 @@
*/
import { useFieldSchema, useForm } from '@formily/react';
import { EllipsisWithTooltip, useCollection_deprecated, useFieldTitle } from '@nocobase/client';
import { EllipsisWithTooltip, useCollection, useFieldTitle } from '@nocobase/client';
import React from 'react';
import { MapComponent } from './MapComponent';
const ReadPretty = (props) => {
const { value } = props;
const fieldSchema = useFieldSchema();
const { getField } = useCollection_deprecated();
const collectionField = getField(fieldSchema.name);
const collection = useCollection();
const collectionField = collection.getField(fieldSchema.name);
const mapType = props.mapType || collectionField?.uiSchema['x-component-props']?.mapType;
const form = useForm();
useFieldTitle();