mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-12-03 12:47:44 +08:00
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:
parent
cee210fa27
commit
4e981ed339
@ -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;
|
||||
|
@ -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} />;
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
|
@ -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) => {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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); // 新版 UISchema(1.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';
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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} />;
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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) => {
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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[]) || []);
|
||||
|
@ -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>;
|
||||
},
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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} />);
|
||||
};
|
||||
|
||||
|
@ -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 };
|
||||
}
|
||||
|
||||
|
@ -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']}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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' },
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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' },
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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)}
|
||||
/>,
|
||||
);
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
/** 变量的配置项 */
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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 = [
|
||||
|
@ -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();
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user