perf(Table): remove Formily components to improve performance (#5738)

* perf(Table): remove Formily components to improve performance

* refactor: rename and modify comments

* fix(UIEditor): fix hover style

* fix(Table): fix style configuration option not showing

* fix(ellipsis): fix refresh issue

* fix: need to update field value

* perf(Table): improve performance of configuring top buttons

* refactor: skip style-related code when no style rules are set

* chore(e2e): fix e2e errors

* fix(e2e): fix e2e errors

* fix(workflow): fix refresh issue

* fix(workflow): fix refresh issue

* fix(Action.Container): fix refresh issue

* fix(drawer): fix refresh issue

* fix(Table): fix refresh issue

* fix(AssociationField): fix refresh issues

* fix(NocoBaseRecursionField): fix refresh code

* refactor(SchemaComponentContext): remove useless code

* refactor: rename RefreshContext to RefreshFieldSchemaContext

* refactor(useDataBlockRequestGetter): optimize comment

* refactor: rename and new refresh context

* fix(Table): fix some refresh issues

* fix(AssociationField): fix refresh issue

* refactor(Table): make code better

* fix: replace RecursionField with NocoBaseRecursionField

* refactor: remove useless code

* fix(Menu): fix draging issue

* fix: refresh entire page after drag and drop operation

* fix(Page): fix draging issue

* chore: fix build error

* fix(test): make unit tests pass

* chore(test): fix unit test

* chore(e2e): fix e2e errors

* chore(e2e): update e2e to make it pass

* chore(e2e): update e2e to make it pass

* chore(e2e): fix e2e errors

* fix(e2e): fix some e2e errors

* fix(SchemaComponent): fix onChange issue

* chore(e2e): make e2e more stable
This commit is contained in:
Zeke Zhang 2024-12-04 21:05:50 +08:00 committed by GitHub
parent 51cb1d0a71
commit 0fa56d7407
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 805 additions and 653 deletions

View File

@ -11,7 +11,7 @@ import {
SchemaSettingsModalItem, SchemaSettingsModalItem,
createDesignable, createDesignable,
useAPIClient, useAPIClient,
useSchemaComponentContext, useRefreshFieldSchema,
useSchemaSettings, useSchemaSettings,
useSchemaSettingsRender, useSchemaSettingsRender,
} from '@nocobase/client'; } from '@nocobase/client';
@ -82,16 +82,16 @@ const Demo = () => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const field = useField(); const field = useField();
const api = useAPIClient(); const api = useAPIClient();
const { refresh } = useSchemaComponentContext(); const refreshFieldSchema = useRefreshFieldSchema();
const dn = useMemo( const dn = useMemo(
() => () =>
createDesignable({ createDesignable({
current: fieldSchema.parent, current: fieldSchema.parent,
model: field.parent, model: field.parent,
api, api,
refresh, refresh: () => refreshFieldSchema({ refreshParentSchema: true }),
}), }),
[], [api, field.parent, fieldSchema.parent, refreshFieldSchema],
); );
const { render, exists } = useSchemaSettingsRender(fieldSchema['x-settings'], { const { render, exists } = useSchemaSettingsRender(fieldSchema['x-settings'], {
fieldSchema: dn.current, fieldSchema: dn.current,

View File

@ -11,7 +11,7 @@ import {
SchemaSettingsModalItem, SchemaSettingsModalItem,
createDesignable, createDesignable,
useAPIClient, useAPIClient,
useSchemaComponentContext, useRefreshFieldSchema,
useSchemaSettings, useSchemaSettings,
useSchemaSettingsRender, useSchemaSettingsRender,
} from '@nocobase/client'; } from '@nocobase/client';
@ -82,16 +82,16 @@ const Demo = () => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const field = useField(); const field = useField();
const api = useAPIClient(); const api = useAPIClient();
const { refresh } = useSchemaComponentContext(); const refreshFieldSchema = useRefreshFieldSchema();
const dn = useMemo( const dn = useMemo(
() => () =>
createDesignable({ createDesignable({
current: fieldSchema.parent, current: fieldSchema.parent,
model: field.parent, model: field.parent,
api, api,
refresh, refresh: () => refreshFieldSchema({ refreshParentSchema: true }),
}), }),
[], [api, field.parent, fieldSchema.parent, refreshFieldSchema],
); );
const { render, exists } = useSchemaSettingsRender(fieldSchema['x-settings'], { const { render, exists } = useSchemaSettingsRender(fieldSchema['x-settings'], {
fieldSchema: dn.current, fieldSchema: dn.current,

View File

@ -17,7 +17,7 @@ import { SchemaInitializer } from '../SchemaInitializer';
import { SchemaInitializerOptions } from '../types'; import { SchemaInitializerOptions } from '../types';
import { withInitializer } from '../withInitializer'; import { withInitializer } from '../withInitializer';
const InitializerComponent: FC<SchemaInitializerOptions<any, any>> = React.memo((options) => { const InitializerComponent: FC<SchemaInitializerOptions<any, any>> = (options) => {
const Component: any = options.Component || SchemaInitializerButton; const Component: any = options.Component || SchemaInitializerButton;
const ItemsComponent: any = options.ItemsComponent || SchemaInitializerItems; const ItemsComponent: any = options.ItemsComponent || SchemaInitializerItems;
@ -31,7 +31,8 @@ const InitializerComponent: FC<SchemaInitializerOptions<any, any>> = React.memo(
const C = useMemo(() => withInitializer(Component), [Component]); const C = useMemo(() => withInitializer(Component), [Component]);
return React.createElement(C, options, React.createElement(ItemsComponent, itemsComponentProps)); return React.createElement(C, options, React.createElement(ItemsComponent, itemsComponentProps));
}); };
InitializerComponent.displayName = 'InitializerComponent'; InitializerComponent.displayName = 'InitializerComponent';
export function useSchemaInitializerRender<P1 = ButtonProps, P2 = {}>( export function useSchemaInitializerRender<P1 = ButtonProps, P2 = {}>(

View File

@ -54,7 +54,7 @@ export function withInitializer<T>(C: ComponentType<T>) {
insertAdjacent(insertPosition, wrapCallback(schema, { isInSubTable }), { onSuccess }); insertAdjacent(insertPosition, wrapCallback(schema, { isInSubTable }), { onSuccess });
} }
}, },
[insertCallback, wrapCallback, insertAdjacent, insertPosition, onSuccess], [insertCallback, wrapCallback, isInSubTable, insertAdjacent, insertPosition, onSuccess],
); );
const { wrapSSR, hashId, componentCls } = useSchemaInitializerStyles(); const { wrapSSR, hashId, componentCls } = useSchemaInitializerStyles();

View File

@ -10,7 +10,7 @@
import { Field, GeneralField } from '@formily/core'; import { Field, GeneralField } from '@formily/core';
import { RecursionField, useField, useFieldSchema } from '@formily/react'; import { RecursionField, useField, useFieldSchema } from '@formily/react';
import { Col, Row } from 'antd'; import { Col, Row } from 'antd';
import { isArray } from 'lodash'; import _, { isArray } from 'lodash';
import template from 'lodash/template'; import template from 'lodash/template';
import React, { createContext, useContext, useMemo } from 'react'; import React, { createContext, useContext, useMemo } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -33,10 +33,12 @@ import {
useCollectionManager_deprecated, useCollectionManager_deprecated,
useCollection_deprecated, useCollection_deprecated,
} from '../collection-manager'; } from '../collection-manager';
import { RefreshComponentProvider } from '../formily/NocoBaseRecursionField';
import { useSourceId } from '../modules/blocks/useSourceId'; import { useSourceId } from '../modules/blocks/useSourceId';
import { RecordProvider, useRecordIndex } from '../record-provider'; import { RecordProvider, useRecordIndex } from '../record-provider';
import { useAssociationNames } from './hooks'; import { useAssociationNames } from './hooks';
import { useDataBlockParentRecord } from './hooks/useDataBlockParentRecord'; import { useDataBlockParentRecord } from './hooks/useDataBlockParentRecord';
import { useUpdate } from 'ahooks';
/** /**
* @deprecated * @deprecated
@ -243,6 +245,7 @@ export const BlockProvider = (props: {
}) => { }) => {
const { name, dataSource, useParams, parentRecord } = props; const { name, dataSource, useParams, parentRecord } = props;
const parentRecordFromHook = useCompatDataBlockParentRecord(props); const parentRecordFromHook = useCompatDataBlockParentRecord(props);
const refresh = useUpdate();
// 新版1.0)已弃用 useParams这里之所以继续保留是为了兼容旧版的 UISchema // 新版1.0)已弃用 useParams这里之所以继续保留是为了兼容旧版的 UISchema
const paramsFromHook = useParams?.(); const paramsFromHook = useParams?.();
@ -261,7 +264,7 @@ export const BlockProvider = (props: {
<BlockContext.Provider value={blockValue}> <BlockContext.Provider value={blockValue}>
<DataBlockProvider {...(props as any)} params={params} parentRecord={parentRecord || parentRecordFromHook}> <DataBlockProvider {...(props as any)} params={params} parentRecord={parentRecord || parentRecordFromHook}>
<BlockRequestProvider_deprecated {...props} updateAssociationValues={updateAssociationValues} params={params}> <BlockRequestProvider_deprecated {...props} updateAssociationValues={updateAssociationValues} params={params}>
{props.children} <RefreshComponentProvider refresh={refresh}>{props.children}</RefreshComponentProvider>
</BlockRequestProvider_deprecated> </BlockRequestProvider_deprecated>
</DataBlockProvider> </DataBlockProvider>
</BlockContext.Provider> </BlockContext.Provider>

View File

@ -1590,7 +1590,7 @@ export const useAssociationNames = (dataSource?: string) => {
const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated(dataSource); const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated(dataSource);
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const getAssociationAppends = () => { const getAssociationAppends = useCallback(() => {
const updateAssociationValues = new Set([]); const updateAssociationValues = new Set([]);
let appends = new Set([]); let appends = new Set([]);
@ -1606,7 +1606,7 @@ export const useAssociationNames = (dataSource?: string) => {
appends = fillParentFields(appends); appends = fillParentFields(appends);
return { appends: [...appends], updateAssociationValues: [...updateAssociationValues] }; return { appends: [...appends], updateAssociationValues: [...updateAssociationValues] };
}; }, [dataSource, fieldSchema, getCollection, getCollectionJoinField]);
return { getAssociationAppends }; return { getAssociationAppends };
}; };

View File

@ -16,7 +16,6 @@ import { useDataSourceManager } from '../data-source';
import { DEFAULT_DATA_SOURCE_KEY } from '../../data-source/data-source/DataSourceManager'; import { DEFAULT_DATA_SOURCE_KEY } from '../../data-source/data-source/DataSourceManager';
import { useCollection } from '../collection'; import { useCollection } from '../collection';
import { BlockItemCard } from '../../schema-component/antd/block-item/BlockItemCard'; import { BlockItemCard } from '../../schema-component/antd/block-item/BlockItemCard';
import { AnyKindOfDictionary } from 'lodash';
export interface CollectionDeletedPlaceholderProps { export interface CollectionDeletedPlaceholderProps {
type: 'Collection' | 'Field' | 'Data Source' | 'Block template'; type: 'Collection' | 'Field' | 'Data Source' | 'Block template';
@ -99,6 +98,7 @@ export const CollectionDeletedPlaceholder: FC<CollectionDeletedPlaceholderProps>
...confirm, ...confirm,
onOk() { onOk() {
dn.remove(null, { removeParentsIfNoChildren: true, breakRemoveOn: { 'x-component': 'Grid' } }); dn.remove(null, { removeParentsIfNoChildren: true, breakRemoveOn: { 'x-component': 'Grid' } });
dn.refresh({ refreshParentSchema: true });
}, },
}) })
} }

View File

@ -225,10 +225,27 @@ export const useDataBlockRequest = <T extends {}>(): UseRequestResult<{ data: T
}; };
/** /**
* Compared to `useDataBlockRequest`, the advantage of this hook is that it prevents unnecessary re-renders. * Compared to `useDataBlockRequest`, this Hook helps prevent unnecessary re-renders.
* For example, if you only need to use methods like `refresh` or `run`, it's recommended to use this hook, *
* as it avoids component re-rendering when re-triggering requests. * This Hook returns a stable function reference that won't change between renders. When you only need
* @returns * methods like `refresh` or `run`, using this Hook is recommended because:
*
* 1. It returns a memoized object containing only the getter function
* 2. The getter function accesses the latest request data through a ref, avoiding re-renders
* 3. Unlike useDataBlockRequest which returns request state directly, this Hook provides indirect access
* through a getter, breaking the reactive dependency chain
*
* For example:
* ```ts
* // This will re-render when request state changes
* const { refresh } = useDataBlockRequest();
*
* // This won't re-render when request state changes
* const { getDataBlockRequest } = useDataBlockRequestGetter();
* const refresh = getDataBlockRequest().refresh;
* ```
*
* @returns An object containing the getDataBlockRequest method that provides access to the request instance
*/ */
export const useDataBlockRequestGetter = () => { export const useDataBlockRequestGetter = () => {
const contextRef = useContext(BlockRequestRefContext); const contextRef = useContext(BlockRequestRefContext);

View File

@ -7,23 +7,32 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { FieldContext, IFieldProps, JSXComponent, Schema, useField, useForm } from '@formily/react'; import { FieldContext, IFieldProps, JSXComponent, Schema, useFieldSchema } from '@formily/react';
import React from 'react'; import React, { useMemo } from 'react';
import { useCompile } from '../schema-component/hooks/useCompile'; import { useCompile } from '../schema-component/hooks/useCompile';
import { NocoBaseReactiveField } from './NocoBaseReactiveField'; import { NocoBaseReactiveField } from './NocoBaseReactiveField';
import { createNocoBaseField } from './createNocoBaseField'; import { createNocoBaseField } from './createNocoBaseField';
/**
* To maintain high performance of Table, this component removes Formily-related components
* @param props component props
* @returns
*/
export const NocoBaseField = <D extends JSXComponent, C extends JSXComponent>( export const NocoBaseField = <D extends JSXComponent, C extends JSXComponent>(
props: IFieldProps<D, C> & { schema: Schema }, props: IFieldProps<D, C> & { schema: Schema },
) => { ) => {
const compile = useCompile(); const compile = useCompile();
const form = useForm(); const fieldSchema = useFieldSchema();
const parent = useField(); // eslint-disable-next-line react-hooks/exhaustive-deps
const field = createNocoBaseField.call(form, { basePath: parent?.address, compile, ...props }); const field = useMemo(() => createNocoBaseField({ ...props, compile }), []);
// update componentProps to rerender field component
Object.assign(field.componentProps, fieldSchema['x-component-props']);
field.value = props.value;
return ( return (
<FieldContext.Provider value={field}> <FieldContext.Provider value={field as any}>
<NocoBaseReactiveField field={field}>{props.children}</NocoBaseReactiveField> <NocoBaseReactiveField field={field as any}>{props.children}</NocoBaseReactiveField>
</FieldContext.Provider> </FieldContext.Provider>
); );
}; };

View File

@ -7,9 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { Form, GeneralField, isVoidField } from '@formily/core'; import { Form, GeneralField } from '@formily/core';
import { RenderPropsChildren, SchemaComponentsContext } from '@formily/react'; import { RenderPropsChildren, SchemaComponentsContext } from '@formily/react';
import { observer } from '@formily/reactive-react';
import { FormPath, isFn } from '@formily/shared'; import { FormPath, isFn } from '@formily/shared';
import React, { Fragment, useContext } from 'react'; import React, { Fragment, useContext } from 'react';
interface IReactiveFieldProps { interface IReactiveFieldProps {
@ -34,19 +33,16 @@ const renderChildren = (children: RenderPropsChildren<GeneralField>, field?: Gen
isFn(children) ? children(field, form) : children; isFn(children) ? children(field, form) : children;
/** /**
* Based on @formily/react v2.3.2 ReactiveInternal component * To maintain high performance of Table, this component removes Formily-related code
* Modified to better adapt to NocoBase's needs
*/ */
const ReactiveInternal: React.FC<IReactiveFieldProps> = (props) => { const NocoBaseReactiveInternal: React.FC<IReactiveFieldProps> = (props) => {
const components = useContext(SchemaComponentsContext); const components = useContext(SchemaComponentsContext);
if (!props.field) { const field: any = props.field;
return <Fragment>{renderChildren(props.children)}</Fragment>;
}
const field = props.field;
const content = mergeChildren( const content = mergeChildren(
renderChildren(props.children, field, field.form), renderChildren(props.children, field, field.form),
field.content ?? field.componentProps.children, field.content ?? field.componentProps.children,
); );
if (field.display !== 'visible') return null; if (field.display !== 'visible') return null;
const getComponent = (target: any) => { const getComponent = (target: any) => {
@ -63,27 +59,8 @@ const ReactiveInternal: React.FC<IReactiveFieldProps> = (props) => {
const renderComponent = () => { const renderComponent = () => {
if (!field.componentType) return content; if (!field.componentType) return content;
const value = !isVoidField(field) ? field.value : undefined; const disabled = true;
const onChange = !isVoidField(field) const readOnly = true;
? (...args: any[]) => {
field.onInput(...args);
field.componentProps?.onChange?.(...args);
}
: field.componentProps?.onChange;
const onFocus = !isVoidField(field)
? (...args: any[]) => {
field.onFocus(...args);
field.componentProps?.onFocus?.(...args);
}
: field.componentProps?.onFocus;
const onBlur = !isVoidField(field)
? (...args: any[]) => {
field.onBlur(...args);
field.componentProps?.onBlur?.(...args);
}
: field.componentProps?.onBlur;
const disabled = !isVoidField(field) ? field.pattern === 'disabled' || field.pattern === 'readPretty' : undefined;
const readOnly = !isVoidField(field) ? field.pattern === 'readOnly' : undefined;
return React.createElement( return React.createElement(
getComponent(field.componentType), getComponent(field.componentType),
@ -91,10 +68,7 @@ const ReactiveInternal: React.FC<IReactiveFieldProps> = (props) => {
disabled, disabled,
readOnly, readOnly,
...field.componentProps, ...field.componentProps,
value, value: field.value,
onChange,
onFocus,
onBlur,
}, },
content, content,
); );
@ -103,14 +77,12 @@ const ReactiveInternal: React.FC<IReactiveFieldProps> = (props) => {
return renderDecorator(renderComponent()); return renderDecorator(renderComponent());
}; };
ReactiveInternal.displayName = 'NocoBaseReactiveInternal'; NocoBaseReactiveInternal.displayName = 'NocoBaseReactiveInternal';
/** /**
* Based on @formily/react v2.3.2 NocoBaseReactiveField component * Based on @formily/react v2.3.2 NocoBaseReactiveField component
* Modified to better adapt to NocoBase's needs * Modified to better adapt to NocoBase's needs
*/ */
export const NocoBaseReactiveField = observer(ReactiveInternal, { export const NocoBaseReactiveField = NocoBaseReactiveInternal;
forwardRef: true,
});
NocoBaseReactiveField.displayName = 'NocoBaseReactiveField'; NocoBaseReactiveField.displayName = 'NocoBaseReactiveField';

View File

@ -24,10 +24,11 @@ import {
import { isBool, isFn, isValid, merge } from '@formily/shared'; import { isBool, isFn, isValid, merge } from '@formily/shared';
import { useUpdate } from 'ahooks'; import { useUpdate } from 'ahooks';
import _ from 'lodash'; import _ from 'lodash';
import React, { FC, Fragment, useCallback, useMemo } from 'react'; import React, { FC, Fragment, useCallback, useContext, useMemo, useRef } from 'react';
import { CollectionFieldOptions } from '../data-source/collection/Collection'; import { CollectionFieldOptions } from '../data-source/collection/Collection';
import { useCollectionManager } from '../data-source/collection/CollectionManagerProvider'; import { useCollectionManager } from '../data-source/collection/CollectionManagerProvider';
import { useCollection } from '../data-source/collection/CollectionProvider'; import { useCollection } from '../data-source/collection/CollectionProvider';
import { SchemaComponentOnChangeContext } from '../schema-component/core/SchemaComponent';
import { EMPTY_OBJECT } from '../variables'; import { EMPTY_OBJECT } from '../variables';
import { NocoBaseField } from './NocoBaseField'; import { NocoBaseField } from './NocoBaseField';
@ -51,22 +52,36 @@ interface INocoBaseRecursionFieldProps extends IRecursionFieldProps {
const CollectionFieldUISchemaContext = React.createContext<CollectionFieldOptions>({}); const CollectionFieldUISchemaContext = React.createContext<CollectionFieldOptions>({});
const RefreshContext = React.createContext<(options?: { refreshParent?: boolean }) => void>(_.noop); const RefreshFieldSchemaContext = React.createContext<(options?: { refreshParentSchema?: boolean }) => void>(_.noop);
const RefreshProvider: FC<{ refresh: (options?: { refreshParent?: boolean }) => void }> = ({ children, refresh }) => { const RefreshFieldSchemaProvider: FC<{ refresh: (options?: { refreshParentSchema?: boolean }) => void }> = ({
children,
refresh,
}) => {
const refreshParent = useRefreshFieldSchema(); const refreshParent = useRefreshFieldSchema();
const value = useCallback( const value = useCallback(
(options?: { refreshParent?: boolean }) => { (options?: { refreshParentSchema?: boolean }) => {
if (options?.refreshParent) { refresh();
if (options?.refreshParentSchema) {
refreshParent?.(); refreshParent?.();
} }
refresh();
}, },
[refreshParent, refresh], [refreshParent, refresh],
); );
return <RefreshContext.Provider value={value}>{children}</RefreshContext.Provider>; return <RefreshFieldSchemaContext.Provider value={value}>{children}</RefreshFieldSchemaContext.Provider>;
};
const RefreshComponentContext = React.createContext<() => void>(_.noop);
export const RefreshComponentProvider: FC<{ refresh: () => void }> = ({ children, refresh }) => {
return <RefreshComponentContext.Provider value={refresh}>{children}</RefreshComponentContext.Provider>;
};
export const useRefreshComponent = () => {
return React.useContext(RefreshComponentContext);
}; };
/** /**
@ -74,7 +89,7 @@ const RefreshProvider: FC<{ refresh: (options?: { refreshParent?: boolean }) =>
* @returns * @returns
*/ */
export const useRefreshFieldSchema = () => { export const useRefreshFieldSchema = () => {
return React.useContext(RefreshContext); return React.useContext(RefreshFieldSchemaContext);
}; };
/** /**
@ -121,15 +136,11 @@ const useFieldProps = (schema: Schema) => {
const useBasePath = (props: IRecursionFieldProps) => { const useBasePath = (props: IRecursionFieldProps) => {
const parent = useField(); const parent = useField();
if (props.onlyRenderProperties) { if (props.onlyRenderProperties) {
return props.basePath || parent?.address.concat(props.name); return props.basePath || parent?.address?.concat(props.name);
} }
return props.basePath || parent?.address; return props.basePath || parent?.address;
}; };
const createSchemaInstance = _.memoize((schema: ISchema): Schema => {
return new Schema(schema);
});
const createMergedSchemaInstance = (schema: Schema, uiSchema: ISchema, onlyRenderProperties: boolean) => { const createMergedSchemaInstance = (schema: Schema, uiSchema: ISchema, onlyRenderProperties: boolean) => {
const clonedSchema = schema.toJSON(); const clonedSchema = schema.toJSON();
@ -257,14 +268,32 @@ export const NocoBaseRecursionField: ReactFC<INocoBaseRecursionFieldProps> = Rea
uiSchema, uiSchema,
} = props; } = props;
const basePath = useBasePath(props); const basePath = useBasePath(props);
const fieldSchema = createSchemaInstance(schema); const newFieldSchemaRef = useRef(null);
const oldFieldSchema = useMemo(() => {
newFieldSchemaRef.current = null;
return new Schema(schema);
}, [schema]);
const { uiSchema: collectionFiledUiSchema, defaultValue } = useCollectionFieldUISchema(); const { uiSchema: collectionFiledUiSchema, defaultValue } = useCollectionFieldUISchema();
const update = useUpdate(); const update = useUpdate();
const { onChange: onChangeFromContext } = useContext(SchemaComponentOnChangeContext);
const fieldSchema: Schema = newFieldSchemaRef.current || oldFieldSchema;
const refresh = useCallback(() => { const refresh = useCallback(() => {
createSchemaInstance.cache.delete(schema); const parent = fieldSchema.parent;
newFieldSchemaRef.current = new Schema(fieldSchema.toJSON(), parent);
if (parent?.properties) {
Object.keys(parent.properties).forEach((key) => {
if (key === fieldSchema.name) {
parent.properties[key] = newFieldSchemaRef.current;
}
});
}
update(); update();
}, [schema, update]); onChangeFromContext?.();
}, [fieldSchema, onChangeFromContext, update]);
// Merge default Schema of collection fields // Merge default Schema of collection fields
const mergedFieldSchema = useMemo(() => { const mergedFieldSchema = useMemo(() => {
@ -330,7 +359,7 @@ export const NocoBaseRecursionField: ReactFC<INocoBaseRecursionFieldProps> = Rea
// some default schema values would also be saved in fieldSchema. // some default schema values would also be saved in fieldSchema.
return ( return (
<SchemaContext.Provider value={fieldSchema}> <SchemaContext.Provider value={fieldSchema}>
<RefreshProvider refresh={refresh}>{render()}</RefreshProvider> <RefreshFieldSchemaProvider refresh={refresh}>{render()}</RefreshFieldSchemaProvider>
</SchemaContext.Provider> </SchemaContext.Provider>
); );
}); });

View File

@ -7,29 +7,12 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { FormPath, FormPathPattern, IFieldFactoryProps, IFieldProps, LifeCycleTypes } from '@formily/core'; import { IFieldProps } from '@formily/core';
import { Field } from '@formily/core/esm/models/Field';
import { locateNode } from '@formily/core/esm/shared/internals';
import { JSXComponent, Schema } from '@formily/react'; import { JSXComponent, Schema } from '@formily/react';
import { batch, define, observable, raw } from '@formily/reactive';
import { toArr } from '@formily/shared'; import { toArr } from '@formily/shared';
export function createNocoBaseField<Decorator extends JSXComponent, Component extends JSXComponent>( export function createNocoBaseField<Decorator extends JSXComponent, Component extends JSXComponent>(props: any) {
props: IFieldFactoryProps<Decorator, Component> & { compile: (source: any) => any }, return new NocoBaseField(props);
): Field<Decorator, Component> {
const address = FormPath.parse(props.basePath).concat(props.name);
const identifier = address.toString();
if (!identifier) return;
if (!this.fields[identifier]) {
batch(() => {
new NocoBaseField(address, props, this, this.props.designable);
});
this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE);
}
this.fields[identifier].value = props.value;
return this.fields[identifier] as any;
} }
/** /**
@ -40,35 +23,79 @@ class NocoBaseField<
Component extends JSXComponent = any, Component extends JSXComponent = any,
TextType = any, TextType = any,
ValueType = any, ValueType = any,
> extends Field { > {
declare props: IFieldProps<Decorator, Component, TextType, ValueType> & { props: IFieldProps<Decorator, Component, TextType, ValueType> & {
schema: Schema; schema: Schema;
compile: (source: any) => any; compile: (source: any) => any;
}; };
initialized: boolean;
loading: boolean;
validating: boolean;
submitting: boolean;
selfModified: boolean;
active: boolean;
visited: boolean;
mounted: boolean;
unmounted: boolean;
inputValues: any[];
inputValue: any;
feedbacks: any[];
title: string;
description: string;
display: string;
pattern: string;
editable: boolean;
disabled: boolean;
readOnly: boolean;
readPretty: boolean;
visible: boolean;
hidden: boolean;
dataSource: any[];
validator: any;
required: boolean;
content: string;
initialValue: any;
value: any;
data: any;
decorator: any[];
component: any[];
decoratorProps: any;
componentProps: any;
decoratorType: any;
componentType: any;
path: any;
form: any;
constructor(props: any) {
this.props = props;
this.initialize();
// this.makeObservable();
}
protected initialize() { protected initialize() {
const compile = this.props.compile; const compile = this.props.compile;
this.initialized = false; this.pattern = 'readPretty';
this.readPretty = true;
this.initialized = true;
this.loading = false; this.loading = false;
this.validating = false; this.validating = false;
this.submitting = false; this.submitting = false;
this.selfModified = false; this.selfModified = false;
this.active = false; this.active = false;
this.visited = false; this.visited = true;
this.mounted = false; this.mounted = true;
this.unmounted = false; this.unmounted = false;
this.inputValues = []; this.inputValues = [];
this.inputValue = null; this.inputValue = null;
this.feedbacks = []; this.feedbacks = [];
this.title = compile(this.props.title || this.props.schema?.title); this.title = compile(this.props.title || this.props.schema?.title);
this.description = compile(this.props.description || this.props.schema?.['description']); this.description = compile(this.props.description || this.props.schema?.['description']);
this.display = this.props.display || this.props.schema?.['x-display']; this.display = 'visible';
this.pattern = this.props.pattern || this.props.schema?.['x-pattern'];
this.editable = this.props.editable || this.props.schema?.['x-editable']; this.editable = this.props.editable || this.props.schema?.['x-editable'];
this.disabled = this.props.disabled || this.props.schema?.['x-disabled']; this.disabled = this.props.disabled || this.props.schema?.['x-disabled'];
this.readOnly = this.props.readOnly || this.props.schema?.['x-read-only']; this.readOnly = this.props.readOnly || this.props.schema?.['x-read-only'];
this.readPretty = this.props.readPretty || this.props.schema?.['x-read-pretty'];
this.visible = this.props.visible || this.props.schema?.['x-visible']; this.visible = this.props.visible || this.props.schema?.['x-visible'];
this.hidden = this.props.hidden || this.props.schema?.['x-hidden']; this.hidden = this.props.hidden || this.props.schema?.['x-hidden'];
this.dataSource = compile(this.props.dataSource || (this.props.schema?.enum as any)); this.dataSource = compile(this.props.dataSource || (this.props.schema?.enum as any));
@ -84,19 +111,18 @@ class NocoBaseField<
this.component = this.props.component this.component = this.props.component
? toArr(this.props.component) ? toArr(this.props.component)
: [this.props.schema?.['x-component'], this.props.schema?.['x-component-props']]; : [this.props.schema?.['x-component'], this.props.schema?.['x-component-props']];
this.decoratorProps = this.props.schema?.['x-decorator-props'] || {};
this.componentProps = this.props.schema?.['x-component-props'] || {};
this.decoratorType = this.props.schema?.['x-decorator'];
this.componentType = this.props.schema?.['x-component'];
this.path = {};
this.form = {};
} }
locate(address: FormPathPattern) { // protected makeObservable() {
raw(this.form.fields)[address.toString()] = this as any; // define(this, {
locateNode(this as any, address); // componentProps: observable,
} // });
// }
protected makeObservable() {
define(this, {
componentProps: observable,
});
}
// Set as an empty function to prevent parent class from executing this method
protected makeReactive() {}
} }

View File

@ -41,6 +41,7 @@ export * from './global-theme';
export * from './hooks'; export * from './hooks';
export * from './i18n'; export * from './i18n';
export * from './icon'; export * from './icon';
export * from './lazy-helper';
export { default as locale } from './locale'; export { default as locale } from './locale';
export * from './nocobase-buildin-plugin'; export * from './nocobase-buildin-plugin';
export * from './plugin-manager'; export * from './plugin-manager';
@ -58,7 +59,6 @@ export * from './system-settings';
export * from './testUtils'; export * from './testUtils';
export * from './user'; export * from './user';
export * from './variables'; export * from './variables';
export * from './lazy-helper';
export { withDynamicSchemaProps } from './hoc/withDynamicSchemaProps'; export { withDynamicSchemaProps } from './hoc/withDynamicSchemaProps';
export { withSkeletonComponent } from './hoc/withSkeletonComponent'; export { withSkeletonComponent } from './hoc/withSkeletonComponent';
@ -83,7 +83,8 @@ export { languageCodes } from './locale';
// Override Formily API // Override Formily API
export { export {
NocoBaseRecursionField,
CollectionFieldUISchemaProvider, CollectionFieldUISchemaProvider,
IsInNocoBaseRecursionFieldContext, IsInNocoBaseRecursionFieldContext,
NocoBaseRecursionField,
useRefreshFieldSchema,
} from './formily/NocoBaseRecursionField'; } from './formily/NocoBaseRecursionField';

View File

@ -17,7 +17,7 @@ test.describe('Add new: inherit', () => {
// 1. click the "Add new" button, and then create a block, the block's collection should be "parent" // 1. click the "Add new" button, and then create a block, the block's collection should be "parent"
await page.getByRole('button', { name: 'plus Add new' }).click(); await page.getByRole('button', { name: 'plus Add new' }).click();
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'form Form right' }).hover(); await page.getByRole('menuitem', { name: 'Form right' }).hover();
await page.getByRole('menuitem', { name: 'Current collection' }).click(); await page.getByRole('menuitem', { name: 'Current collection' }).click();
await page.getByLabel('block-item-CardItem-parent-form').hover(); await page.getByLabel('block-item-CardItem-parent-form').hover();
await expect(page.getByLabel('block-item-CardItem-parent-form').getByText('parent')).toBeVisible(); await expect(page.getByLabel('block-item-CardItem-parent-form').getByText('parent')).toBeVisible();
@ -28,7 +28,7 @@ test.describe('Add new: inherit', () => {
await page.getByRole('button', { name: 'down' }).hover(); await page.getByRole('button', { name: 'down' }).hover();
await page.getByRole('menuitem', { name: 'child1' }).click(); await page.getByRole('menuitem', { name: 'child1' }).click();
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'form Form right' }).hover(); await page.getByRole('menuitem', { name: 'Form right' }).hover();
await page.getByRole('menuitem', { name: 'Current collection' }).click(); await page.getByRole('menuitem', { name: 'Current collection' }).click();
await page.getByLabel('block-item-CardItem-child1-').hover(); await page.getByLabel('block-item-CardItem-child1-').hover();
await expect(page.getByLabel('block-item-CardItem-child1-').getByText('child1')).toBeVisible(); await expect(page.getByLabel('block-item-CardItem-child1-').getByText('child1')).toBeVisible();
@ -39,7 +39,7 @@ test.describe('Add new: inherit', () => {
await page.getByRole('button', { name: 'down' }).hover(); await page.getByRole('button', { name: 'down' }).hover();
await page.getByRole('menuitem', { name: 'child2' }).click(); await page.getByRole('menuitem', { name: 'child2' }).click();
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'form Form right' }).hover(); await page.getByRole('menuitem', { name: 'Form right' }).hover();
await page.getByRole('menuitem', { name: 'Current collection' }).click(); await page.getByRole('menuitem', { name: 'Current collection' }).click();
await page.getByLabel('block-item-CardItem-child2-').hover(); await page.getByLabel('block-item-CardItem-child2-').hover();
await expect(page.getByLabel('block-item-CardItem-child2-').getByText('child2')).toBeVisible(); await expect(page.getByLabel('block-item-CardItem-child2-').getByText('child2')).toBeVisible();

View File

@ -17,7 +17,7 @@ test('basic', async ({ page, mockPage, mockRecord }) => {
// 1. 打开弹窗,并创建一个 Table 关系区块 // 1. 打开弹窗,并创建一个 Table 关系区块
await page.getByLabel('action-Action.Link-Edit record-update-collection1-table-0').click(); await page.getByLabel('action-Action.Link-Edit record-update-collection1-table-0').click();
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover(); await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'manyToMany' }).click(); await page.getByRole('menuitem', { name: 'manyToMany' }).click();

View File

@ -337,7 +337,7 @@ test.describe('set default value', () => {
// 3. Table 数据选择器中使用 `Current popup record` // 3. Table 数据选择器中使用 `Current popup record`
// 创建 Table 区块 // 创建 Table 区块
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Other records right' }).hover(); await page.getByRole('menuitem', { name: 'Other records right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click(); await page.getByRole('menuitem', { name: 'Users' }).click();
await page.mouse.move(300, 0); await page.mouse.move(300, 0);
@ -465,7 +465,7 @@ test.describe('set default value', () => {
// 3. Table 数据选择器中使用 `Parent popup record` // 3. Table 数据选择器中使用 `Parent popup record`
// 创建 Table 区块 // 创建 Table 区块
await page.getByLabel('schema-initializer-Grid-popup').nth(1).hover(); await page.getByLabel('schema-initializer-Grid-popup').nth(1).hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Other records right' }).hover(); await page.getByRole('menuitem', { name: 'Other records right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click(); await page.getByRole('menuitem', { name: 'Users' }).click();
await page.mouse.move(300, 0); await page.mouse.move(300, 0);
@ -595,7 +595,7 @@ test.describe('set default value', () => {
// 3. Table 数据选择器中使用 `Parent popup record` // 3. Table 数据选择器中使用 `Parent popup record`
// 创建 Table 区块 // 创建 Table 区块
await page.getByLabel('schema-initializer-Grid-popup').nth(1).hover(); await page.getByLabel('schema-initializer-Grid-popup').nth(1).hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Other records right' }).hover(); await page.getByRole('menuitem', { name: 'Other records right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click(); await page.getByRole('menuitem', { name: 'Users' }).click();
await page.mouse.move(300, 0); await page.mouse.move(300, 0);
@ -733,7 +733,7 @@ test.describe('set default value', () => {
// 3. Table 数据选择器中使用 `Parent popup record` // 3. Table 数据选择器中使用 `Parent popup record`
// 创建 Table 区块 // 创建 Table 区块
await page.getByLabel('schema-initializer-Grid-popup').nth(1).hover(); await page.getByLabel('schema-initializer-Grid-popup').nth(1).hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Other records right' }).hover(); await page.getByRole('menuitem', { name: 'Other records right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click(); await page.getByRole('menuitem', { name: 'Users' }).click();
await page.mouse.move(300, 0); await page.mouse.move(300, 0);

View File

@ -143,7 +143,7 @@ test.describe('creation form block schema settings', () => {
// 创建区块的时候,可以选择刚才保存的模板 -------------------------------------------------- // 创建区块的时候,可以选择刚才保存的模板 --------------------------------------------------
await page.getByLabel('schema-initializer-Grid-page:addBlock').hover(); await page.getByLabel('schema-initializer-Grid-page:addBlock').hover();
await page.getByRole('menuitem', { name: 'form Form right' }).first().hover(); await page.getByRole('menuitem', { name: 'Form right' }).first().hover();
await page.getByRole('menuitem', { name: 'General right' }).hover(); await page.getByRole('menuitem', { name: 'General right' }).hover();
// Duplicate template // Duplicate template
@ -152,7 +152,7 @@ test.describe('creation form block schema settings', () => {
// Reference template // Reference template
await page.getByLabel('schema-initializer-Grid-page:addBlock').hover(); await page.getByLabel('schema-initializer-Grid-page:addBlock').hover();
await page.getByRole('menuitem', { name: 'form Form right' }).first().hover(); await page.getByRole('menuitem', { name: 'Form right' }).first().hover();
await page.getByRole('menuitem', { name: 'General right' }).hover(); await page.getByRole('menuitem', { name: 'General right' }).hover();
await page.getByRole('menuitem', { name: 'General right' }).click(); await page.getByRole('menuitem', { name: 'General right' }).click();
await page.getByRole('menuitem', { name: 'Reference template right' }).click(); await page.getByRole('menuitem', { name: 'Reference template right' }).click();

View File

@ -32,7 +32,7 @@ test.describe('where list block can be added', () => {
// 1. 打开弹窗,通过 Associated records 创建一个列表区块 // 1. 打开弹窗,通过 Associated records 创建一个列表区块
await page.getByLabel('action-Action.Link-View').click(); await page.getByLabel('action-Action.Link-View').click();
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'ordered-list List right' }).hover(); await page.getByRole('menuitem', { name: 'List right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records right' }).hover(); await page.getByRole('menuitem', { name: 'Associated records right' }).hover();
await page.getByRole('menuitem', { name: 'Roles' }).click(); await page.getByRole('menuitem', { name: 'Roles' }).click();
await page.mouse.move(300, 0); await page.mouse.move(300, 0);
@ -46,7 +46,7 @@ test.describe('where list block can be added', () => {
// 2. 通过 Other records 创建一个列表区块 // 2. 通过 Other records 创建一个列表区块
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'ordered-list List right' }).hover(); await page.getByRole('menuitem', { name: 'List right' }).hover();
await page.getByRole('menuitem', { name: 'Other records right' }).hover(); await page.getByRole('menuitem', { name: 'Other records right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click(); await page.getByRole('menuitem', { name: 'Users' }).click();
await page.mouse.move(300, 0); await page.mouse.move(300, 0);

View File

@ -137,12 +137,15 @@ test.describe('table data selector schema settings', () => {
await expect( await expect(
page.getByLabel('block-item-CardItem-table-selector-data-scope-variable-table-selector').getByRole('row'), page.getByLabel('block-item-CardItem-table-selector-data-scope-variable-table-selector').getByRole('row'),
).toHaveCount(2); // 这里之所以是 2是因为表头也是一个 row ).toHaveCount(2); // 这里之所以是 2是因为表头也是一个 row
await expect( await expect(
page page
.getByLabel('block-item-CardItem-table-selector-data-scope-variable-table-selector') .getByLabel('block-item-CardItem-table-selector-data-scope-variable-table-selector')
.getByRole('row') .getByRole('row')
.getByText(record['manyToMany'][0]['singleLineText']), .getByText(record['manyToMany'][0]['singleLineText']),
).toBeVisible(); ).toBeVisible({
timeout: 3000,
});
}); });
}); });

View File

@ -30,7 +30,7 @@ test.describe('block template', () => {
// The template saved above cannot be used to create a association block. // The template saved above cannot be used to create a association block.
await page.getByLabel('action-Action.Link-View-view-').click(); await page.getByLabel('action-Action.Link-View-view-').click();
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records right' }).hover(); await page.getByRole('menuitem', { name: 'Associated records right' }).hover();
// The saved template should not be displayed (no arrow should be shown) // The saved template should not be displayed (no arrow should be shown)
@ -79,7 +79,7 @@ test.describe('block template', () => {
// The template saved above cannot be used to create a non-association block. // The template saved above cannot be used to create a non-association block.
await page.getByLabel('schema-initializer-Grid-page:').hover(); await page.getByLabel('schema-initializer-Grid-page:').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Roles right' }).hover(); await page.getByRole('menuitem', { name: 'Roles right' }).hover();
await page.getByRole('menuitem', { name: 'Duplicate template right' }).hover(); await page.getByRole('menuitem', { name: 'Duplicate template right' }).hover();
await expect(page.getByRole('menuitem', { name: 'Roles_Table' })).toBeVisible(); await expect(page.getByRole('menuitem', { name: 'Roles_Table' })).toBeVisible();

View File

@ -23,7 +23,7 @@ test.describe('pagination', () => {
// 1. 创建一个 Table // 1. 创建一个 Table
await page.getByLabel('schema-initializer-Grid-page:').hover(); await page.getByLabel('schema-initializer-Grid-page:').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'collectionName' }).click(); await page.getByRole('menuitem', { name: 'collectionName' }).click();
// 显示出 ID // 显示出 ID
await page.getByLabel('schema-initializer-TableV2-').hover(); await page.getByLabel('schema-initializer-TableV2-').hover();

View File

@ -36,7 +36,7 @@ test.describe('where table block can be added', () => {
// 添加当前表关系区块 // 添加当前表关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover(); await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'childAssociationField' }).click(); await page.getByRole('menuitem', { name: 'childAssociationField' }).click();
await page await page
@ -47,7 +47,7 @@ test.describe('where table block can be added', () => {
// 添加父表关系区块 // 添加父表关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover(); await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'parentAssociationField' }).click(); await page.getByRole('menuitem', { name: 'parentAssociationField' }).click();
await page.getByLabel('schema-initializer-TableV2-table:configureColumns-parentTargetCollection').hover(); await page.getByLabel('schema-initializer-TableV2-table:configureColumns-parentTargetCollection').hover();
@ -69,7 +69,7 @@ test.describe('where table block can be added', () => {
// 通过 Other records 创建一个表格区块 // 通过 Other records 创建一个表格区块
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Other records right' }).hover(); await page.getByRole('menuitem', { name: 'Other records right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click(); await page.getByRole('menuitem', { name: 'Users' }).click();
await page.mouse.move(300, 0); await page.mouse.move(300, 0);
@ -99,7 +99,7 @@ test.describe('where table block can be added', () => {
await page.getByLabel('action-Action.Link-View-view-').first().click(); await page.getByLabel('action-Action.Link-View-view-').first().click();
//表格关系区块 //表格关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).click(); await page.getByRole('menuitem', { name: 'Table right' }).click();
await page.getByText('Associated records').hover(); await page.getByText('Associated records').hover();
const [request] = await Promise.all([ const [request] = await Promise.all([
page.waitForRequest((request) => request.url().includes('uiSchemas:insertAdjacent')), page.waitForRequest((request) => request.url().includes('uiSchemas:insertAdjacent')),

View File

@ -104,7 +104,7 @@ test.describe('actions schema settings', () => {
// 配置出一个表单 // 配置出一个表单
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'form Form right' }).hover(); await page.getByRole('menuitem', { name: 'Form right' }).hover();
await page.getByRole('menuitem', { name: 'Current collection' }).click(); await page.getByRole('menuitem', { name: 'Current collection' }).click();
await page.getByLabel('schema-initializer-Grid-form:').hover(); await page.getByLabel('schema-initializer-Grid-form:').hover();

View File

@ -14,7 +14,7 @@ test.describe('save as template', () => {
// 1. 创建一个区块,然后保存为模板 // 1. 创建一个区块,然后保存为模板
await mockPage().goto(); await mockPage().goto();
await page.getByLabel('schema-initializer-Grid-page:').hover(); await page.getByLabel('schema-initializer-Grid-page:').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click(); await page.getByRole('menuitem', { name: 'Users' }).click();
await page.getByLabel('block-item-CardItem-users-').hover(); await page.getByLabel('block-item-CardItem-users-').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:table-users').hover(); await page.getByLabel('designer-schema-settings-CardItem-blockSettings:table-users').hover();

View File

@ -19,6 +19,7 @@ import { useCollectionManager_deprecated } from '../../../../collection-manager'
import { useFieldComponentName } from '../../../../common/useFieldComponentName'; import { useFieldComponentName } from '../../../../common/useFieldComponentName';
import { useCollection } from '../../../../data-source'; import { useCollection } from '../../../../data-source';
import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem'; import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem';
import { useFlag } from '../../../../flag-provider/hooks/useFlag';
import { useDesignable } from '../../../../schema-component'; import { useDesignable } from '../../../../schema-component';
import { useAssociationFieldContext } from '../../../../schema-component/antd/association-field/hooks'; import { useAssociationFieldContext } from '../../../../schema-component/antd/association-field/hooks';
import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator'; import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator';
@ -89,7 +90,13 @@ export const tableColumnSettings = new SchemaSettings({
}, },
useVisible() { useVisible() {
const { fieldSchema } = useColumnSchema(); const { fieldSchema } = useColumnSchema();
const { isInSubTable } = useFlag();
const field: any = useField(); const field: any = useField();
if (!isInSubTable) {
return true;
}
const path = field.path?.splice(field.path?.length - 1, 1); const path = field.path?.splice(field.path?.length - 1, 1);
if (fieldSchema) { if (fieldSchema) {
const isReadPretty = field.form.query(`${path.concat(`*.` + fieldSchema.name)}`).get('readPretty'); const isReadPretty = field.form.query(`${path.concat(`*.` + fieldSchema.name)}`).get('readPretty');

View File

@ -18,7 +18,7 @@ test.describe('where filter block can be added', () => {
// 1. 页面中创建一个 filter form一个 filter collapse // 1. 页面中创建一个 filter form一个 filter collapse
await page.getByLabel('schema-initializer-Grid-page:').hover(); await page.getByLabel('schema-initializer-Grid-page:').hover();
await page.getByRole('menuitem', { name: 'form Form right' }).nth(1).hover(); await page.getByRole('menuitem', { name: 'Form right' }).nth(1).hover();
await page.getByRole('menuitem', { name: 'Users' }).click(); await page.getByRole('menuitem', { name: 'Users' }).click();
await page.getByLabel('schema-initializer-Grid-page:').hover(); await page.getByLabel('schema-initializer-Grid-page:').hover();
await page.getByRole('menuitem', { name: 'Collapse right' }).hover(); await page.getByRole('menuitem', { name: 'Collapse right' }).hover();
@ -99,7 +99,7 @@ test.describe('where filter block can be added', () => {
await page.getByLabel('action-Action.Link-View record-view-users-table-1').click(); await page.getByLabel('action-Action.Link-View record-view-users-table-1').click();
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'form Form right' }).hover(); await page.getByRole('menuitem', { name: 'Form right' }).hover();
await page.getByRole('menuitem', { name: 'Roles' }).click(); await page.getByRole('menuitem', { name: 'Roles' }).click();
await page.getByLabel('schema-initializer-Grid-filterForm:configureFields-roles').hover(); await page.getByLabel('schema-initializer-Grid-filterForm:configureFields-roles').hover();
await page.getByRole('menuitem', { name: 'Role UID' }).click(); await page.getByRole('menuitem', { name: 'Role UID' }).click();
@ -151,7 +151,7 @@ test.describe('where filter block can be added', () => {
// 2. 测试用表单筛选其它区块 // 2. 测试用表单筛选其它区块
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'form Form right' }).hover(); await page.getByRole('menuitem', { name: 'Form right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click(); await page.getByRole('menuitem', { name: 'Users' }).click();
await page.getByLabel('schema-initializer-Grid-filterForm:configureFields-users').hover(); await page.getByLabel('schema-initializer-Grid-filterForm:configureFields-users').hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click(); await page.getByRole('menuitem', { name: 'Nickname' }).click();

View File

@ -42,6 +42,15 @@ export const ellipsisSettingsItem: SchemaSettingsItemType = {
checked: !!schema['x-component-props']?.ellipsis, checked: !!schema['x-component-props']?.ellipsis,
hidden, hidden,
onChange: async (checked) => { onChange: async (checked) => {
if (tableFieldSchema && tableFieldInstanceList) {
tableFieldInstanceList.forEach((fieldInstance) => {
fieldInstance.componentProps.ellipsis = checked;
});
schema['x-component-props']['ellipsis'] = checked;
} else {
formField.componentProps.ellipsis = checked;
}
await dn.emit('patch', { await dn.emit('patch', {
schema: { schema: {
'x-uid': schema['x-uid'], 'x-uid': schema['x-uid'],
@ -51,19 +60,6 @@ export const ellipsisSettingsItem: SchemaSettingsItemType = {
}, },
}, },
}); });
if (tableFieldSchema && tableFieldInstanceList) {
tableFieldInstanceList.forEach((fieldInstance) => {
fieldInstance.componentProps.ellipsis = checked;
});
schema['x-component-props']['ellipsis'] = checked;
const path = formField.path?.splice(formField.path?.length - 1, 1);
formField.form.query(`${path.concat(`*.` + fieldSchema.name)}`).forEach((f) => {
f.componentProps.ellipsis = checked;
});
} else {
formField.componentProps.ellipsis = checked;
}
}, },
}; };
}, },

View File

@ -66,8 +66,6 @@ test.describe('group page menus schema settings', () => {
await page.locator('.ant-select-dropdown').getByText('anchor page').click(); await page.locator('.ant-select-dropdown').getByText('anchor page').click();
await page.getByLabel('Inner').click(); await page.getByLabel('Inner').click();
await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click();
// 当前页面菜单会消失
await expect(page.getByLabel('group page', { exact: true })).not.toBeVisible();
// 跳转到 anchor page 页面,会有一个名为 group page 的子页面菜单 // 跳转到 anchor page 页面,会有一个名为 group page 的子页面菜单
await page.getByLabel('anchor page').click(); await page.getByLabel('anchor page').click();
await expect(page.locator('.ant-layout-sider').getByLabel('group page')).toBeVisible(); await expect(page.locator('.ant-layout-sider').getByLabel('group page')).toBeVisible();

View File

@ -13,7 +13,7 @@ test.describe('page:addBlock', () => {
test('当搜索不到数据时显示空状态', async ({ page, mockPage }) => { test('当搜索不到数据时显示空状态', async ({ page, mockPage }) => {
await mockPage().goto(); await mockPage().goto();
await page.getByLabel('schema-initializer-Grid-').hover(); await page.getByLabel('schema-initializer-Grid-').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('textbox', { name: 'Search and select collection' }).fill('no match'); await page.getByRole('textbox', { name: 'Search and select collection' }).fill('no match');
await expect(page.getByRole('menuitem', { name: 'No data' })).toBeVisible(); await expect(page.getByRole('menuitem', { name: 'No data' })).toBeVisible();

View File

@ -86,7 +86,7 @@ test.describe('add blocks to the popup', () => {
// 通过 Association records 创建关系区块 // 通过 Association records 创建关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover(); await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'manyToMany' }).click(); await page.getByRole('menuitem', { name: 'manyToMany' }).click();
await page.mouse.move(-300, 0); await page.mouse.move(-300, 0);
@ -134,7 +134,7 @@ test.describe('add blocks to the popup', () => {
// 通过 Association records 创建一个关系区块 // 通过 Association records 创建一个关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover(); await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover(); await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'Roles' }).click(); await page.getByRole('menuitem', { name: 'Roles' }).click();
await page await page

View File

@ -13,6 +13,7 @@ import classNames from 'classnames';
// @ts-ignore // @ts-ignore
import React, { FC, startTransition, useCallback, useEffect, useMemo, useState } from 'react'; import React, { FC, startTransition, useCallback, useEffect, useMemo, useState } from 'react';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
import { ErrorFallback } from '../error-fallback'; import { ErrorFallback } from '../error-fallback';
import { useCurrentPopupContext } from '../page/PagePopups'; import { useCurrentPopupContext } from '../page/PagePopups';
import { TabsContextProvider, useTabsContext } from '../tabs/context'; import { TabsContextProvider, useTabsContext } from '../tabs/context';
@ -63,7 +64,7 @@ const ActionDrawerContent: FC<{ footerNodeName: string; field: any; schema: any
} }
return ( return (
<MemoizeRecursionField <NocoBaseRecursionField
basePath={field.address} basePath={field.address}
schema={schema} schema={schema}
onlyRenderProperties onlyRenderProperties
@ -77,7 +78,7 @@ ActionDrawerContent.displayName = 'ActionDrawerContent';
export const InternalActionDrawer: React.FC<ActionDrawerProps> = observer( export const InternalActionDrawer: React.FC<ActionDrawerProps> = observer(
(props) => { (props) => {
const { footerNodeName = 'Action.Drawer.Footer', zIndex: _zIndex, ...others } = props; const { footerNodeName = 'Action.Drawer.Footer', zIndex: _zIndex, onClose: onCloseFromProps, ...others } = props;
const { visible, setVisible, openSize = 'middle', drawerProps } = useActionContext(); const { visible, setVisible, openSize = 'middle', drawerProps } = useActionContext();
const schema = useFieldSchema(); const schema = useFieldSchema();
const field = useField(); const field = useField();
@ -105,7 +106,13 @@ export const InternalActionDrawer: React.FC<ActionDrawerProps> = observer(
const zIndex = _zIndex || parentZIndex + (props.level || 0); const zIndex = _zIndex || parentZIndex + (props.level || 0);
const onClose = useCallback(() => setVisible(false, true), [setVisible]); const onClose = useCallback(
(e) => {
setVisible(false, true);
onCloseFromProps?.(e);
},
[setVisible, onCloseFromProps],
);
const keepFooterNode = useCallback( const keepFooterNode = useCallback(
(s) => { (s) => {
return s['x-component'] === footerNodeName; return s['x-component'] === footerNodeName;

View File

@ -8,9 +8,9 @@
*/ */
import { cx } from '@emotion/css'; import { cx } from '@emotion/css';
import { observer, useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { Space, SpaceProps } from 'antd'; import { Space, SpaceProps } from 'antd';
import React, { CSSProperties, useContext } from 'react'; import React, { CSSProperties, FC, useContext } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useSchemaInitializerRender } from '../../../application'; import { useSchemaInitializerRender } from '../../../application';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField'; import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
@ -58,8 +58,7 @@ const Portal: React.FC = (props) => {
); );
}; };
export const ActionBar = withDynamicSchemaProps( const InternalActionBar: FC = (props: any) => {
observer((props: any) => {
const { forceProps = {} } = useActionBarContext(); const { forceProps = {} } = useActionBarContext();
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema // 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { layout = 'two-columns', style, spaceProps, ...others } = { ...useProps(props), ...forceProps } as any; const { layout = 'two-columns', style, spaceProps, ...others } = { ...useProps(props), ...forceProps } as any;
@ -77,7 +76,7 @@ export const ActionBar = withDynamicSchemaProps(
{...others} {...others}
className={cx(others.className, 'nb-action-bar')} className={cx(others.className, 'nb-action-bar')}
> >
{props.children && ( {
<div> <div>
<Space {...spaceProps} style={{ flexWrap: 'wrap', ...(spaceProps?.style || {}) }}> <Space {...spaceProps} style={{ flexWrap: 'wrap', ...(spaceProps?.style || {}) }}>
{fieldSchema.mapProperties((schema, key) => { {fieldSchema.mapProperties((schema, key) => {
@ -85,7 +84,7 @@ export const ActionBar = withDynamicSchemaProps(
})} })}
</Space> </Space>
</div> </div>
)} }
{render({ style: { margin: '0 !important' } })} {render({ style: { margin: '0 !important' } })}
</div> </div>
</DndContext> </DndContext>
@ -144,6 +143,11 @@ export const ActionBar = withDynamicSchemaProps(
{render()} {render()}
</div> </div>
); );
}), };
export const ActionBar = withDynamicSchemaProps(
(props: any) => {
return <InternalActionBar {...props} />;
},
{ displayName: 'ActionBar' }, { displayName: 'ActionBar' },
); );

View File

@ -54,9 +54,11 @@ export const InternalNester = observer(
labelWidth = 120, labelWidth = 120,
labelWrap = true, labelWrap = true,
} = fieldSchema?.['x-component-props'] || {}; } = fieldSchema?.['x-component-props'] || {};
useEffect(() => { useEffect(() => {
insertNester(schema.Nester); insertNester(schema.Nester);
}, []); }, []);
return ( return (
<CollectionProvider_deprecated name={collectionField.target}> <CollectionProvider_deprecated name={collectionField.target}>
<ACLCollectionProvider actionPath={`${collectionField.target}:${actionName || 'view'}`}> <ACLCollectionProvider actionPath={`${collectionField.target}:${actionName || 'view'}`}>

View File

@ -155,7 +155,7 @@ const RenderRecord = React.memo(
ellipsisWithTooltipRef, ellipsisWithTooltipRef,
enableLink, enableLink,
fieldNames?.label, fieldNames?.label,
fieldSchema?.properties, fieldSchema,
getLabelUiSchema, getLabelUiSchema,
insertViewer, insertViewer,
isTreeCollection, isTreeCollection,

View File

@ -37,7 +37,7 @@ export const useInsertSchema = (component) => {
insertAfterBegin(cloneDeep(ss)); insertAfterBegin(cloneDeep(ss));
} }
}, },
[component], [component, fieldSchema, insertAfterBegin],
); );
return insert; return insert;
}; };

View File

@ -8,12 +8,13 @@
*/ */
import { Schema, useFieldSchema } from '@formily/react'; import { Schema, useFieldSchema } from '@formily/react';
import React, { useContext } from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SchemaComponentContext, createDesignable } from '../..'; import { createDesignable } from '../..';
import { useAPIClient } from '../../../api-client'; import { useAPIClient } from '../../../api-client';
import { useBlockRequestContext } from '../../../block-provider'; import { useBlockRequestContext } from '../../../block-provider';
import { mergeFilter } from '../../../filter-provider/utils'; import { mergeFilter } from '../../../filter-provider/utils';
import { useRefreshFieldSchema } from '../../../formily/NocoBaseRecursionField';
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem'; import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
/** /**
@ -22,11 +23,11 @@ import { ActionInitializerItem } from '../../../schema-initializer/items/ActionI
* @returns * @returns
*/ */
export const ActionBarAssociationFilterAction = (props) => { export const ActionBarAssociationFilterAction = (props) => {
const { refresh } = useContext(SchemaComponentContext); const refreshFieldSchema = useRefreshFieldSchema();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const api = useAPIClient(); const api = useAPIClient();
const { t } = useTranslation(); const { t } = useTranslation();
const dn = createDesignable({ t, api, refresh, current: fieldSchema }); const dn = createDesignable({ t, api, refresh: refreshFieldSchema, current: fieldSchema });
const { service, props: blockProps } = useBlockRequestContext(); const { service, props: blockProps } = useBlockRequestContext();
dn.loadAPIClientEvents(); dn.loadAPIClientEvents();

View File

@ -9,13 +9,14 @@
import { FormLayout } from '@formily/antd-v5'; import { FormLayout } from '@formily/antd-v5';
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { FieldContext, FormContext, observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { FieldContext, FormContext, observer, useField, useFieldSchema } from '@formily/react';
import { Options, Result } from 'ahooks/es/useRequest/src/types'; import { Options, Result } from 'ahooks/es/useRequest/src/types';
import { ConfigProvider, Spin } from 'antd'; import { ConfigProvider, Spin } from 'antd';
import React, { createContext, useContext, useEffect, useMemo } from 'react'; import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { useAttach, useComponent } from '../..'; import { useAttach, useComponent } from '../..';
import { useRequest } from '../../../api-client'; import { useRequest } from '../../../api-client';
import { useCollection_deprecated } from '../../../collection-manager'; import { useCollection_deprecated } from '../../../collection-manager';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
import { GeneralSchemaDesigner, SchemaSettingsDivider, SchemaSettingsRemove } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettingsDivider, SchemaSettingsRemove } from '../../../schema-settings';
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate'; import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
@ -39,7 +40,7 @@ const FormComponent: React.FC<FormProps> = (props) => {
<FieldContext.Provider value={undefined}> <FieldContext.Provider value={undefined}>
<FormContext.Provider value={form}> <FormContext.Provider value={form}>
<FormLayout layout={'vertical'} {...others}> <FormLayout layout={'vertical'} {...others}>
<RecursionField basePath={f.address} schema={fieldSchema} onlyRenderProperties /> <NocoBaseRecursionField basePath={f.address} schema={fieldSchema} onlyRenderProperties />
</FormLayout> </FormLayout>
</FormContext.Provider> </FormContext.Provider>
</FieldContext.Provider> </FieldContext.Provider>
@ -65,7 +66,7 @@ const FormDecorator: React.FC<FormProps> = (props) => {
<FormLayout layout={'vertical'} {...others}> <FormLayout layout={'vertical'} {...others}>
<FieldContext.Provider value={f}> <FieldContext.Provider value={f}>
<Component {...field.componentProps}> <Component {...field.componentProps}>
<RecursionField basePath={f.address} schema={fieldSchema} onlyRenderProperties /> <NocoBaseRecursionField basePath={f.address} schema={fieldSchema} onlyRenderProperties />
</Component> </Component>
</FieldContext.Provider> </FieldContext.Provider>
{/* <FieldContext.Provider value={f}>{children}</FieldContext.Provider> */} {/* <FieldContext.Provider value={f}>{children}</FieldContext.Provider> */}

View File

@ -52,33 +52,6 @@ export const GridCardDesigner = () => {
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
const columnCountSchema = useMemo(() => {
return {
'x-component': 'Slider',
'x-decorator': 'FormItem',
'x-component-props': {
min: 1,
max: 24,
marks: columnCountMarks,
tooltip: {
formatter: (value) => `${value}${t('Column')}`,
},
step: null,
},
};
}, [t]);
const columnCountProperties = useMemo(() => {
return gridSizes.reduce((o, k) => {
o[k] = {
...columnCountSchema,
title: t(screenSizeTitleMaps[k]),
description: `${t('Screen size')} ${screenSizeMaps[k]} ${t('pixels')}`,
};
return o;
}, {});
}, [columnCountSchema, t]);
const sort = defaultSort?.map((item: string) => { const sort = defaultSort?.map((item: string) => {
return item.startsWith('-') return item.startsWith('-')
? { ? {

View File

@ -10,10 +10,11 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { FormLayout } from '@formily/antd-v5'; import { FormLayout } from '@formily/antd-v5';
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; import { Schema, useField, useFieldSchema } from '@formily/react';
import { List as AntdList, Col, PaginationProps } from 'antd'; import { List as AntdList, Col, PaginationProps } from 'antd';
import React, { useCallback, useState } from 'react'; import React, { useCallback } from 'react';
import { getCardItemSchema } from '../../../block-provider'; import { getCardItemSchema } from '../../../block-provider';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
import { withSkeletonComponent } from '../../../hoc/withSkeletonComponent'; import { withSkeletonComponent } from '../../../hoc/withSkeletonComponent';
import { SortableItem } from '../../common'; import { SortableItem } from '../../common';
@ -127,25 +128,18 @@ const InternalGridCard = withSkeletonComponent(
const field = useField<ArrayField>(); const field = useField<ArrayField>();
const Designer = useDesigner(); const Designer = useDesigner();
const height = useGridCardBodyHeight(); const height = useGridCardBodyHeight();
const [schemaMap] = useState(new Map());
const getSchema = useCallback( const getSchema = useCallback(
(key) => { (key) => {
if (!schemaMap.has(key)) { return new Schema({
schemaMap.set(
key,
new Schema({
type: 'object', type: 'object',
properties: { properties: {
[key]: { [key]: {
...fieldSchema.properties['item'], ...fieldSchema.properties['item'],
}, },
}, },
}), });
);
}
return schemaMap.get(key);
}, },
[fieldSchema.properties, schemaMap], [fieldSchema.properties],
); );
const onPaginationChange: PaginationProps['onChange'] = useCallback( const onPaginationChange: PaginationProps['onChange'] = useCallback(
@ -218,13 +212,13 @@ const InternalGridCard = withSkeletonComponent(
renderItem={(item, index) => { renderItem={(item, index) => {
return ( return (
<Col style={{ height: '100%' }} className="nb-card-item-warper"> <Col style={{ height: '100%' }} className="nb-card-item-warper">
<RecursionField <NocoBaseRecursionField
key={index} key={index}
basePath={field.address} basePath={field.address}
name={index} name={index}
onlyRenderProperties onlyRenderProperties
schema={getSchema(index)} schema={getSchema(index)}
></RecursionField> ></NocoBaseRecursionField>
</Col> </Col>
); );
}} }}

View File

@ -297,7 +297,7 @@ const useRowProperties = () => {
} }
return buf; return buf;
}, []); }, []);
}, [Object.keys(fieldSchema.properties || {}).join(',')]); }, [fieldSchema]);
}; };
const useColProperties = () => { const useColProperties = () => {
@ -309,7 +309,7 @@ const useColProperties = () => {
} }
return buf; return buf;
}, []); }, []);
}, [Object.keys(fieldSchema.properties || {}).join(',')]); }, [fieldSchema]);
}; };
const DndWrapper = (props) => { const DndWrapper = (props) => {
@ -361,7 +361,7 @@ export const Grid: any = observer(
useEffect(() => { useEffect(() => {
gridRef.current && setPrintContent?.(gridRef.current); gridRef.current && setPrintContent?.(gridRef.current);
}, [gridRef.current]); }, [setPrintContent]);
const gridContextValue = useMemo(() => { const gridContextValue = useMemo(() => {
return { return {
@ -498,7 +498,7 @@ Grid.Col = observer(
width = `calc(${w}% - ${token.marginBlock}px * ${(showDivider ? cols.length + 1 : 0) / cols.length})`; width = `calc(${w}% - ${token.marginBlock}px * ${(showDivider ? cols.length + 1 : 0) / cols.length})`;
} }
return { width }; return { width };
}, [cols?.length, schema?.['x-component-props']?.['width'], token.marginBlock]); }, [cols.length, schema, showDivider, token.marginBlock]);
const { isOver, setNodeRef } = useDroppable({ const { isOver, setNodeRef } = useDroppable({
id: field.address.toString(), id: field.address.toString(),

View File

@ -10,10 +10,11 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { FormLayout } from '@formily/antd-v5'; import { FormLayout } from '@formily/antd-v5';
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; import { Schema, useField, useFieldSchema } from '@formily/react';
import { List as AntdList, PaginationProps, theme } from 'antd'; import { List as AntdList, PaginationProps, theme } from 'antd';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { getCardItemSchema } from '../../../block-provider'; import { getCardItemSchema } from '../../../block-provider';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
import { withSkeletonComponent } from '../../../hoc/withSkeletonComponent'; import { withSkeletonComponent } from '../../../hoc/withSkeletonComponent';
import { SortableItem } from '../../common'; import { SortableItem } from '../../common';
@ -159,13 +160,13 @@ const InternalList = withSkeletonComponent(
{field.value?.length {field.value?.length
? field.value.map((item, index) => { ? field.value.map((item, index) => {
return ( return (
<RecursionField <NocoBaseRecursionField
basePath={field.address} basePath={field.address}
key={index} key={index}
name={index} name={index}
onlyRenderProperties onlyRenderProperties
schema={getSchema(index)} schema={getSchema(index)}
></RecursionField> ></NocoBaseRecursionField>
); );
}) })
: null} : null}

View File

@ -11,7 +11,6 @@ import { css } from '@emotion/css';
import { import {
FieldContext, FieldContext,
observer, observer,
RecursionField,
SchemaContext, SchemaContext,
SchemaExpressionScopeContext, SchemaExpressionScopeContext,
useField, useField,
@ -23,14 +22,22 @@ import { Menu as AntdMenu, MenuProps } from 'antd';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { createDesignable, DndContext, SchemaComponentContext, SortableItem, useDesignable, useDesigner } from '../..'; import { createDesignable, DndContext, SchemaComponentContext, SortableItem, useDesignable, useDesigner } from '../..';
import { Icon, useAPIClient, useParseURLAndParams, useSchemaInitializerRender } from '../../../'; import {
Icon,
NocoBaseRecursionField,
useAPIClient,
useParseURLAndParams,
useSchemaInitializerRender,
} from '../../../';
import { useCollectMenuItems, useMenuItem } from '../../../hooks/useMenuItem'; import { useCollectMenuItems, useMenuItem } from '../../../hooks/useMenuItem';
import { useProps } from '../../hooks/useProps'; import { useProps } from '../../hooks/useProps';
import { useMenuTranslation } from './locale'; import { useMenuTranslation } from './locale';
import { MenuDesigner } from './Menu.Designer'; import { MenuDesigner } from './Menu.Designer';
import { findKeysByUid, findMenuItem } from './util'; import { findKeysByUid, findMenuItem } from './util';
import { useUpdate } from 'ahooks';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useRefreshComponent, useRefreshFieldSchema } from '../../../formily/NocoBaseRecursionField';
const subMenuDesignerCss = css` const subMenuDesignerCss = css`
position: relative; position: relative;
@ -210,7 +217,6 @@ const HeaderMenu = React.memo<{
onChange: any; onChange: any;
onFocus: any; onFocus: any;
theme: any; theme: any;
refreshId: number;
}>( }>(
({ ({
schema, schema,
@ -228,10 +234,6 @@ const HeaderMenu = React.memo<{
onChange, onChange,
onFocus, onFocus,
theme, theme,
/**
* Used to refresh the component
*/
refreshId,
}) => { }) => {
const { Component, getMenuItems } = useMenuItem(); const { Component, getMenuItems } = useMenuItem();
const items = useMemo(() => { const items = useMemo(() => {
@ -255,7 +257,7 @@ const HeaderMenu = React.memo<{
return result; return result;
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [children, designable, refreshId]); }, [children, designable]);
const handleSelect = useCallback( const handleSelect = useCallback(
(info: { item; key; keyPath; domEvent }) => { (info: { item; key; keyPath; domEvent }) => {
@ -327,17 +329,24 @@ const SideMenu = React.memo<any>(
t, t,
api, api,
designable, designable,
refreshId,
refresh,
}) => { }) => {
// Used to refresh component
refreshId;
const { Component, getMenuItems } = useMenuItem(); const { Component, getMenuItems } = useMenuItem();
// 使用 ref 用来防止闭包问题 const update = useUpdate();
const sideMenuSchemaRef = useRef(sideMenuSchema); const refreshFieldSchema = useRefreshFieldSchema();
sideMenuSchemaRef.current = sideMenuSchema; const refreshComponent = useRefreshComponent();
const refresh = useCallback(
(options?: { refreshParentSchema?: boolean }) => {
console.log('refresh');
// refresh current component
update();
// refresh fieldSchema context value
refreshFieldSchema?.(options);
// refresh component context value
refreshComponent?.();
},
[update, refreshFieldSchema, refreshComponent],
);
const handleSelect = useCallback( const handleSelect = useCallback(
(info) => { (info) => {
@ -348,7 +357,7 @@ const SideMenu = React.memo<any>(
const items = useMemo(() => { const items = useMemo(() => {
const result = getMenuItems(() => { const result = getMenuItems(() => {
return <RecursionField key={uid()} schema={sideMenuSchema} onlyRenderProperties />; return <NocoBaseRecursionField key={uid()} schema={sideMenuSchema} onlyRenderProperties />;
}); });
if (designable) { if (designable) {
@ -362,7 +371,7 @@ const SideMenu = React.memo<any>(
t, t,
api, api,
refresh: refresh, refresh: refresh,
current: sideMenuSchemaRef.current, current: sideMenuSchema,
}); });
dn.loadAPIClientEvents(); dn.loadAPIClientEvents();
dn.insertAdjacent('beforeEnd', s); dn.insertAdjacent('beforeEnd', s);
@ -374,8 +383,7 @@ const SideMenu = React.memo<any>(
} }
return result; return result;
// eslint-disable-next-line react-hooks/exhaustive-deps }, [api, designable, getMenuItems, refresh, render, sideMenuSchema, t]);
}, [api, designable, getMenuItems, refresh, render, sideMenuSchema, t, refreshId]);
return ( return (
mode === 'mix' && mode === 'mix' &&
@ -497,18 +505,9 @@ export const Menu: ComposedMenu = React.memo((props) => {
}, [defaultSelectedKeys]); }, [defaultSelectedKeys]);
const ctx = useContext(SchemaComponentContext); const ctx = useContext(SchemaComponentContext);
const refreshIdRef = useRef(0);
const ctxRefresh = ctx.refresh;
const refresh = useCallback(() => {
refreshIdRef.current += 1;
ctxRefresh?.();
}, [ctxRefresh]);
const newCtx = useMemo(() => ({ ...ctx, refresh }), [ctx, refresh]);
return ( return (
<DndContext> <DndContext>
<SchemaComponentContext.Provider value={newCtx}>
<MenuItemDesignerContext.Provider value={Designer}> <MenuItemDesignerContext.Provider value={Designer}>
<MenuModeContext.Provider value={mode}> <MenuModeContext.Provider value={mode}>
<HeaderMenu <HeaderMenu
@ -526,7 +525,6 @@ export const Menu: ComposedMenu = React.memo((props) => {
selectedKeys={selectedKeys} selectedKeys={selectedKeys}
designable={ctx.designable} designable={ctx.designable}
render={render} render={render}
refreshId={refreshIdRef.current}
> >
{children} {children}
</HeaderMenu> </HeaderMenu>
@ -542,12 +540,9 @@ export const Menu: ComposedMenu = React.memo((props) => {
t={t} t={t}
api={api} api={api}
designable={ctx.designable} designable={ctx.designable}
refreshId={refreshIdRef.current}
refresh={refresh}
/> />
</MenuModeContext.Provider> </MenuModeContext.Provider>
</MenuItemDesignerContext.Provider> </MenuItemDesignerContext.Provider>
</SchemaComponentContext.Provider>
</DndContext> </DndContext>
); );
}); });
@ -728,7 +723,7 @@ Menu.SubMenu = observer(
</SchemaContext.Provider> </SchemaContext.Provider>
), ),
children: getMenuItems(() => { children: getMenuItems(() => {
return <RecursionField schema={schema} onlyRenderProperties />; return <NocoBaseRecursionField schema={schema} onlyRenderProperties />;
}), }),
}; };
}, [field.title, icon, schema, children, Designer]); }, [field.title, icon, schema, children, Designer]);

View File

@ -35,7 +35,6 @@ import { KeepAliveProvider, useKeepAlive } from '../../../route-switch/antd/admi
import { useGetAriaLabelOfSchemaInitializer } from '../../../schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer'; import { useGetAriaLabelOfSchemaInitializer } from '../../../schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer';
import { DndContext } from '../../common'; import { DndContext } from '../../common';
import { SortableItem } from '../../common/sortable-item'; import { SortableItem } from '../../common/sortable-item';
import { SchemaComponentContext, useNewRefreshContext } from '../../context';
import { SchemaComponent, SchemaComponentOptions } from '../../core'; import { SchemaComponent, SchemaComponentOptions } from '../../core';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../hooks';
import { useToken } from '../__builtins__'; import { useToken } from '../__builtins__';
@ -348,7 +347,6 @@ const NocoBasePageHeader = React.memo(({ activeKey, className }: { activeKey: st
const { setTitle: setDocumentTitle } = useDocumentTitle(); const { setTitle: setDocumentTitle } = useDocumentTitle();
const { t } = useTranslation(); const { t } = useTranslation();
const [pageTitle, setPageTitle] = useState(() => t(fieldSchema.title)); const [pageTitle, setPageTitle] = useState(() => t(fieldSchema.title));
const newRefreshCtx = useNewRefreshContext();
const disablePageHeader = fieldSchema['x-component-props']?.disablePageHeader; const disablePageHeader = fieldSchema['x-component-props']?.disablePageHeader;
const enablePageTabs = fieldSchema['x-component-props']?.enablePageTabs; const enablePageTabs = fieldSchema['x-component-props']?.enablePageTabs;
@ -376,7 +374,7 @@ const NocoBasePageHeader = React.memo(({ activeKey, className }: { activeKey: st
); );
return ( return (
<SchemaComponentContext.Provider value={newRefreshCtx}> <>
<PageDesigner title={pageTitle} /> <PageDesigner title={pageTitle} />
{!disablePageHeader && ( {!disablePageHeader && (
<AntdPageHeader <AntdPageHeader
@ -387,7 +385,7 @@ const NocoBasePageHeader = React.memo(({ activeKey, className }: { activeKey: st
footer={<NocoBasePageHeaderTabs className={className} activeKey={activeKey} />} footer={<NocoBasePageHeaderTabs className={className} activeKey={activeKey} />}
/> />
)} )}
</SchemaComponentContext.Provider> </>
); );
}); });

View File

@ -8,7 +8,7 @@
*/ */
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { RecursionField, useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import { toArr } from '@formily/shared'; import { toArr } from '@formily/shared';
import { Select } from 'antd'; import { Select } from 'antd';
import { differenceBy, unionBy } from 'lodash'; import { differenceBy, unionBy } from 'lodash';
@ -18,6 +18,7 @@ import {
useTableSelectorProps as useTsp, useTableSelectorProps as useTsp,
} from '../../../block-provider/TableSelectorProvider'; } from '../../../block-provider/TableSelectorProvider';
import { CollectionProvider_deprecated, useCollection_deprecated } from '../../../collection-manager'; import { CollectionProvider_deprecated, useCollection_deprecated } from '../../../collection-manager';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
import { FormProvider, SchemaComponentOptions } from '../../core'; import { FormProvider, SchemaComponentOptions } from '../../core';
import { useCompile } from '../../hooks'; import { useCompile } from '../../hooks';
import { ActionContextProvider, useActionContext } from '../action'; import { ActionContextProvider, useActionContext } from '../action';
@ -277,7 +278,7 @@ const Drawer: React.FunctionComponent<{
<FormProvider> <FormProvider>
<TableSelectorParamsProvider params={{ filter: getFilter() }}> <TableSelectorParamsProvider params={{ filter: getFilter() }}>
<SchemaComponentOptions scope={{ useTableSelectorProps, usePickActionProps }}> <SchemaComponentOptions scope={{ useTableSelectorProps, usePickActionProps }}>
<RecursionField <NocoBaseRecursionField
schema={fieldSchema} schema={fieldSchema}
onlyRenderProperties onlyRenderProperties
filterProperties={(s) => { filterProperties={(s) => {

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { observer, RecursionField, useFieldSchema } from '@formily/react'; import { observer, useFieldSchema } from '@formily/react';
import { toArr } from '@formily/shared'; import { toArr } from '@formily/shared';
import React, { Fragment, useRef, useState } from 'react'; import React, { Fragment, useRef, useState } from 'react';
import { WithoutTableFieldResource } from '../../../block-provider'; import { WithoutTableFieldResource } from '../../../block-provider';
@ -16,6 +16,7 @@ import { BlockAssociationContext } from '../../../block-provider/BlockProvider';
import { CollectionProvider_deprecated } from '../../../collection-manager'; import { CollectionProvider_deprecated } from '../../../collection-manager';
import { useCollectionManager } from '../../../data-source/collection/CollectionManagerProvider'; import { useCollectionManager } from '../../../data-source/collection/CollectionManagerProvider';
import { useCollection } from '../../../data-source/collection/CollectionProvider'; import { useCollection } from '../../../data-source/collection/CollectionProvider';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
import { RecordProvider, useRecord } from '../../../record-provider'; import { RecordProvider, useRecord } from '../../../record-provider';
import { FormProvider } from '../../core'; import { FormProvider } from '../../core';
import { useCompile } from '../../hooks'; import { useCompile } from '../../hooks';
@ -94,7 +95,7 @@ export const ReadPrettyRecordPicker: React.FC = observer(
const renderWithoutTableFieldResourceProvider = () => ( const renderWithoutTableFieldResourceProvider = () => (
<WithoutTableFieldResource.Provider value={true}> <WithoutTableFieldResource.Provider value={true}>
<FormProvider> <FormProvider>
<RecursionField <NocoBaseRecursionField
schema={fieldSchema} schema={fieldSchema}
onlyRenderProperties onlyRenderProperties
filterProperties={(s) => { filterProperties={(s) => {

View File

@ -13,11 +13,11 @@ import { SortableContext, SortableContextProps, useSortable } from '@dnd-kit/sor
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { spliceArrayState } from '@formily/core/esm/shared/internals'; import { spliceArrayState } from '@formily/core/esm/shared/internals';
import { Schema, SchemaOptionsContext, observer, useField, useFieldSchema } from '@formily/react'; import { observer, Schema, SchemaOptionsContext, useField, useFieldSchema } from '@formily/react';
import { action } from '@formily/reactive'; import { action } from '@formily/reactive';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { isPortalInBody } from '@nocobase/utils/client'; import { isPortalInBody } from '@nocobase/utils/client';
import { useCreation, useDeepCompareEffect, useMemoizedFn } from 'ahooks'; import { useDeepCompareEffect, useMemoizedFn } from 'ahooks';
import { Table as AntdTable, TableColumnProps } from 'antd'; import { Table as AntdTable, TableColumnProps } from 'antd';
import { default as classNames, default as cls } from 'classnames'; import { default as classNames, default as cls } from 'classnames';
import _, { omit } from 'lodash'; import _, { omit } from 'lodash';
@ -29,11 +29,13 @@ import {
BlockRequestLoadingContext, BlockRequestLoadingContext,
RecordIndexProvider, RecordIndexProvider,
RecordProvider, RecordProvider,
useAssociationNames,
useCollection, useCollection,
useCollectionParentRecordData, useCollectionParentRecordData,
useDataBlockProps, useDataBlockProps,
useDataBlockRequest, useDataBlockRequest,
useDataBlockRequestData, useDataBlockRequestData,
useDataBlockRequestGetter,
useFlag, useFlag,
useSchemaInitializerRender, useSchemaInitializerRender,
useTableSelectorContext, useTableSelectorContext,
@ -41,10 +43,15 @@ import {
import { useACLFieldWhitelist } from '../../../acl/ACLProvider'; import { useACLFieldWhitelist } from '../../../acl/ACLProvider';
import { useTableBlockContext } from '../../../block-provider/TableBlockProvider'; import { useTableBlockContext } from '../../../block-provider/TableBlockProvider';
import { isNewRecord } from '../../../data-source/collection-record/isNewRecord'; import { isNewRecord } from '../../../data-source/collection-record/isNewRecord';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField'; import {
NocoBaseRecursionField,
RefreshComponentProvider,
useRefreshFieldSchema,
} from '../../../formily/NocoBaseRecursionField';
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
import { withSkeletonComponent } from '../../../hoc/withSkeletonComponent'; import { withSkeletonComponent } from '../../../hoc/withSkeletonComponent';
import { useSatisfiedActionValues } from '../../../schema-settings/LinkageRules/useActionValues'; import { LinkageRuleDataKeyMap } from '../../../schema-settings/LinkageRules/type';
import { GetStyleRules } from '../../../schema-settings/LinkageRules/useActionValues';
import { HighPerformanceSpin } from '../../common/high-performance-spin/HighPerformanceSpin'; import { HighPerformanceSpin } from '../../common/high-performance-spin/HighPerformanceSpin';
import { useToken } from '../__builtins__'; import { useToken } from '../__builtins__';
import { useAssociationFieldContext } from '../association-field/hooks'; import { useAssociationFieldContext } from '../association-field/hooks';
@ -99,17 +106,6 @@ function adjustColumnOrder(columns) {
return [...leftFixedColumns, ...normalColumns, ...rightFixedColumns]; return [...leftFixedColumns, ...normalColumns, ...rightFixedColumns];
} }
const useColumnsDeepMemoized = (columns: any[]) => {
const columnsJSON = getSchemaArrJSON(columns);
const oldObj = useCreation(() => ({ value: _.cloneDeep(columnsJSON) }), []);
if (!_.isEqual(columnsJSON, oldObj.value)) {
oldObj.value = _.cloneDeep(columnsJSON);
}
return oldObj.value;
};
const TableCellRender: FC<{ const TableCellRender: FC<{
record: any; record: any;
columnSchema: Schema; columnSchema: Schema;
@ -118,7 +114,7 @@ const TableCellRender: FC<{
schemaToolbarBigger: string; schemaToolbarBigger: string;
field: ArrayField; field: ArrayField;
index: number; index: number;
}> = React.memo(({ record, columnSchema, uiSchema, filterProperties, schemaToolbarBigger, field, index }) => { }> = ({ record, columnSchema, uiSchema, filterProperties, schemaToolbarBigger, field, index }) => {
const basePath = field.address.concat(record.__index || index); const basePath = field.address.concat(record.__index || index);
return ( return (
@ -135,9 +131,29 @@ const TableCellRender: FC<{
/> />
</span> </span>
); );
}); };
TableCellRender.displayName = 'TableCellRender'; const useRefreshTableColumns = () => {
const { params: blockParams, dataSource } = useDataBlockProps() || {};
const { getDataBlockRequest } = useDataBlockRequestGetter();
const { getAssociationAppends } = useAssociationNames(dataSource);
const prevParamsRef = useRef(blockParams);
const refreshFieldSchema = useRefreshFieldSchema();
const refresh = useCallback(() => {
const { appends } = getAssociationAppends();
const service = getDataBlockRequest();
if (!_.isEqual(prevParamsRef.current.appends, appends)) {
prevParamsRef.current = { ...blockParams, appends };
service.run(prevParamsRef.current);
}
refreshFieldSchema?.();
}, [blockParams, getAssociationAppends, getDataBlockRequest, refreshFieldSchema]);
return { refresh };
};
const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginationProps) => { const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginationProps) => {
const { token } = useToken(); const { token } = useToken();
@ -146,15 +162,17 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginat
const { schemaInWhitelist } = useACLFieldWhitelist(); const { schemaInWhitelist } = useACLFieldWhitelist();
const { designable } = useDesignable(); const { designable } = useDesignable();
const { exists, render } = useSchemaInitializerRender(schema['x-initializer'], schema['x-initializer-props']); const { exists, render } = useSchemaInitializerRender(schema['x-initializer'], schema['x-initializer-props']);
const columnsSchemas = schema.reduceProperties((buf, s) => { const columnsSchemas = useMemo(() => {
return schema.reduceProperties((buf, s) => {
if (isColumnComponent(s) && schemaInWhitelist(Object.values(s.properties || {}).pop())) { if (isColumnComponent(s) && schemaInWhitelist(Object.values(s.properties || {}).pop())) {
return buf.concat([s]); return buf.concat([s]);
} }
return buf; return buf;
}, []); }, []);
}, [schema, schemaInWhitelist]);
const { current, pageSize } = paginationProps; const { current, pageSize } = paginationProps;
const hasChangedColumns = useColumnsDeepMemoized(columnsSchemas);
const { isPopupVisibleControlledByURL } = usePopupSettings(); const { isPopupVisibleControlledByURL } = usePopupSettings();
const { refresh } = useRefreshTableColumns();
const filterProperties = useCallback( const filterProperties = useCallback(
(schema) => (schema) =>
@ -191,12 +209,14 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginat
return { return {
title: ( title: (
<RefreshComponentProvider refresh={refresh}>
<NocoBaseRecursionField <NocoBaseRecursionField
name={columnSchema.name} name={columnSchema.name}
schema={columnSchema} schema={columnSchema}
onlyRenderSelf onlyRenderSelf
isUseFormilyField={false} isUseFormilyField={false}
/> />
</RefreshComponentProvider>
), ),
dataIndex, dataIndex,
key: columnSchema.name, key: columnSchema.name,
@ -222,7 +242,6 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginat
record, record,
schema: columnSchema, schema: columnSchema,
rowIndex, rowIndex,
isSubTable: props.isSubTable,
columnHidden, columnHidden,
}; };
}, },
@ -234,9 +253,7 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginat
} as TableColumnProps<any>; } as TableColumnProps<any>;
}), }),
// 这里不能把 columnsSchema 作为依赖,因为其每次都会变化,这里使用 hasChangedColumns 作为依赖 [columnsSchemas, collection, refresh, designable, filterProperties, schemaToolbarBigger, field],
// eslint-disable-next-line react-hooks/exhaustive-deps
[hasChangedColumns, field.address, collection, schemaToolbarBigger, designable, filterProperties],
); );
const tableColumns = useMemo(() => { const tableColumns = useMemo(() => {
@ -246,7 +263,7 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginat
const res = [ const res = [
...columns, ...columns,
{ {
title: render(), title: <RefreshComponentProvider refresh={refresh}>{render()}</RefreshComponentProvider>,
dataIndex: 'TABLE_COLUMN_INITIALIZER', dataIndex: 'TABLE_COLUMN_INITIALIZER',
key: 'TABLE_COLUMN_INITIALIZER', key: 'TABLE_COLUMN_INITIALIZER',
render: designable render: designable
@ -602,19 +619,27 @@ const InternalBodyCellComponent = React.memo<BodyCellComponentProps>((props) =>
const inView = useContext(InViewContext); const inView = useContext(InViewContext);
const isIndex = props.className?.includes('selection-column'); const isIndex = props.className?.includes('selection-column');
const { record, schema, rowIndex, isSubTable, ...others } = props; const { record, schema, rowIndex, isSubTable, ...others } = props;
const { valueMap } = useSatisfiedActionValues({ formValues: record, category: 'style', schema }); const styleRules = schema?.[LinkageRuleDataKeyMap['style']];
const style = useMemo(() => Object.assign({ ...props.style }, valueMap), [props.style, valueMap]); const [dynamicStyle, setDynamicStyle] = useState({});
const skeletonStyle = { const style = useMemo(() => ({ ...props.style, ...dynamicStyle }), [props.style, dynamicStyle]);
const skeletonStyle = useMemo(
() => ({
height: '1em', height: '1em',
backgroundColor: token.colorFillSecondary, backgroundColor: token.colorFillSecondary,
borderRadius: `${token.borderRadiusSM}px`, borderRadius: `${token.borderRadiusSM}px`,
}; }),
[token.borderRadiusSM, token.colorFillSecondary],
);
return ( return (
<>
{/* To improve rendering performance, do not render GetStyleRules component when no style rules are set */}
{!_.isEmpty(styleRules) && <GetStyleRules record={record} schema={schema} onStyleChange={setDynamicStyle} />}
<td {...others} className={classNames(props.className, cellClass)} style={style}> <td {...others} className={classNames(props.className, cellClass)} style={style}>
{/* Lazy rendering cannot be used in sub-tables. */} {/* Lazy rendering cannot be used in sub-tables. */}
{isSubTable || inView || isIndex ? props.children : <div style={skeletonStyle} />} {isSubTable || inView || isIndex ? props.children : <div style={skeletonStyle} />}
</td> </td>
</>
); );
}); });

View File

@ -47,7 +47,7 @@ const useDragEnd = (onDragEnd) => {
const dn = createDesignable({ const dn = createDesignable({
t, t,
api, api,
refresh, refresh: ({ refreshParentSchema = true } = {}) => refresh({ refreshParentSchema }),
current: overSchema, current: overSchema,
}); });
@ -70,7 +70,7 @@ const useDragEnd = (onDragEnd) => {
return; return;
} }
}, },
[onDragEnd], [api, onDragEnd, refresh, t],
); );
}; };

View File

@ -78,7 +78,7 @@ const useSortableItemId = (props) => {
if (props.id) { if (props.id) {
return props.id; return props.id;
} }
return field.address.toString(); return field.address?.toString();
}; };
interface SortableItemProps extends HTMLAttributes<HTMLDivElement> { interface SortableItemProps extends HTMLAttributes<HTMLDivElement> {

View File

@ -7,39 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { useUpdate } from 'ahooks'; import { createContext } from 'react';
import { createContext, useCallback, useContext, useMemo } from 'react';
import { useRefreshFieldSchema } from '../formily/NocoBaseRecursionField';
import { ISchemaComponentContext } from './types'; import { ISchemaComponentContext } from './types';
export const SchemaComponentContext = createContext<ISchemaComponentContext>({}); export const SchemaComponentContext = createContext<ISchemaComponentContext>({});
SchemaComponentContext.displayName = 'SchemaComponentContext.Provider'; SchemaComponentContext.displayName = 'SchemaComponentContext.Provider';
/**
* Get a new refresh context, used to refresh the block that uses this hook
* @returns
*/
export const useNewRefreshContext = (refresh?: () => void) => {
const oldCtx = useContext(SchemaComponentContext);
const newCtx = useMemo(() => ({ ...oldCtx }), [oldCtx]);
const refreshFieldSchema = useRefreshFieldSchema();
const update = useUpdate();
const _refresh = useCallback(
(options?: { refreshParent?: boolean }) => {
// refresh fieldSchema
refreshFieldSchema(options);
// refresh current component
update();
refresh?.();
},
[refreshFieldSchema, update, refresh],
);
if (oldCtx) {
Object.assign(newCtx, oldCtx);
newCtx.refresh = _refresh;
}
return newCtx;
};

View File

@ -15,16 +15,19 @@ import { useTranslation } from 'react-i18next';
import { useDesignable } from '..'; import { useDesignable } from '..';
import { useToken } from '../../style'; import { useToken } from '../../style';
const designableStyle = {
backgroundColor: 'var(--colorSettings) !important',
};
const unDesignableStyle = {
backgroundColor: 'transparent',
};
export const DesignableSwitch = () => { export const DesignableSwitch = () => {
const { designable, setDesignable } = useDesignable(); const { designable, setDesignable } = useDesignable();
const { t } = useTranslation(); const { t } = useTranslation();
const { token } = useToken(); const { token } = useToken();
const style = {}; const style = designable ? designableStyle : unDesignableStyle;
if (designable) {
style['backgroundColor'] = 'var(--colorSettings)';
} else {
style['backgroundColor'] = 'transparent';
}
// 快捷键切换编辑状态 // 快捷键切换编辑状态
useHotkeys('Ctrl+Shift+U', () => setDesignable(!designable), [designable]); useHotkeys('Ctrl+Shift+U', () => setDesignable(!designable), [designable]);

View File

@ -8,14 +8,14 @@
*/ */
import { IRecursionFieldProps, ISchemaFieldProps, Schema } from '@formily/react'; import { IRecursionFieldProps, ISchemaFieldProps, Schema } from '@formily/react';
import { useUpdate } from 'ahooks'; import _ from 'lodash';
import React, { memo, useContext, useMemo } from 'react'; import React, { createContext, memo, useContext, useMemo } from 'react';
import { NocoBaseRecursionField } from '../../formily/NocoBaseRecursionField'; import { NocoBaseRecursionField } from '../../formily/NocoBaseRecursionField';
import { SchemaComponentContext } from '../context'; import { SchemaComponentContext } from '../context';
import { SchemaComponentOptions } from './SchemaComponentOptions'; import { SchemaComponentOptions } from './SchemaComponentOptions';
type SchemaComponentOnChange = { type SchemaComponentOnChange = {
onChange?: (s: Schema) => void; onChange?: (s?: Schema) => void;
}; };
function toSchema(schema?: any) { function toSchema(schema?: any) {
@ -45,32 +45,49 @@ interface DistributedProps {
distributed?: boolean; distributed?: boolean;
} }
/**
* Used to pass the onChange callback function.
*
* The onChange callback will be triggered whenever a descendant Schema changes.
*/
export const SchemaComponentOnChangeContext = createContext<SchemaComponentOnChange>({ onChange: _.noop });
const RecursionSchemaComponent = memo((props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => { const RecursionSchemaComponent = memo((props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => {
const { components, scope, schema: _schema, distributed, onChange, ...others } = props; const { components, scope, schema: _schema, distributed, onChange: _onChange, ...others } = props;
const ctx = useContext(SchemaComponentContext); const ctx = useContext(SchemaComponentContext);
const schema = useMemo(() => toSchema(_schema), [_schema]); const schema = useMemo(() => toSchema(_schema), [_schema]);
const refresh = useUpdate();
const value = useMemo( const value = useMemo(
() => ({ () => ({
...ctx, ...ctx,
distributed: ctx.distributed == false ? false : distributed, distributed: ctx.distributed == false ? false : distributed,
refresh: () => { /**
refresh(); * @deprecated
if (ctx.distributed === false || distributed === false) { */
ctx.refresh?.(); refresh: _.noop,
} }),
onChange?.(schema); [ctx, distributed],
);
const { onChange: onChangeFromContext } = useContext(SchemaComponentOnChangeContext);
const onChangeValue = useMemo(
() => ({
onChange: () => {
_onChange?.(schema);
onChangeFromContext?.();
}, },
}), }),
[ctx, distributed, onChange, refresh, schema], [_onChange, onChangeFromContext, schema],
); );
return ( return (
<SchemaComponentOnChangeContext.Provider value={onChangeValue}>
<SchemaComponentContext.Provider value={value}> <SchemaComponentContext.Provider value={value}>
<SchemaComponentOptions inherit components={components} scope={scope}> <SchemaComponentOptions inherit components={components} scope={scope}>
<NocoBaseRecursionField {...others} schema={schema} isUseFormilyField /> <NocoBaseRecursionField {...others} schema={schema} isUseFormilyField />
</SchemaComponentOptions> </SchemaComponentOptions>
</SchemaComponentContext.Provider> </SchemaComponentContext.Provider>
</SchemaComponentOnChangeContext.Provider>
); );
}); });

View File

@ -10,7 +10,7 @@
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { FormProvider, Schema } from '@formily/react'; import { FormProvider, Schema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { useUpdate } from 'ahooks'; import _ from 'lodash';
import React, { useCallback, useContext, useMemo, useState } from 'react'; import React, { useCallback, useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SchemaComponentContext } from '../context'; import { SchemaComponentContext } from '../context';
@ -56,7 +56,6 @@ export const SchemaComponentProvider: React.FC<ISchemaComponentProvider> = (prop
const { designable, onDesignableChange, components, children } = props; const { designable, onDesignableChange, components, children } = props;
const ctx = useContext(SchemaComponentContext); const ctx = useContext(SchemaComponentContext);
const ctxOptions = useSchemaOptionsContext(); const ctxOptions = useSchemaOptionsContext();
const refresh = useUpdate();
const [formId, setFormId] = useState(() => uid()); const [formId, setFormId] = useState(() => uid());
const form = useMemo(() => props.form || createForm(), [formId]); const form = useMemo(() => props.form || createForm(), [formId]);
const { t } = useTranslation(); const { t } = useTranslation();
@ -89,11 +88,14 @@ export const SchemaComponentProvider: React.FC<ISchemaComponentProvider> = (prop
scope, scope,
components, components,
reset, reset,
refresh, /**
* @deprecated
*/
refresh: _.noop,
designable: designableValue, designable: designableValue,
setDesignable, setDesignable,
}), }),
[components, designableValue, refresh, reset, scope, setDesignable], [components, designableValue, reset, scope, setDesignable],
); );
return ( return (

View File

@ -10,6 +10,7 @@
import { GeneralField, Query } from '@formily/core'; import { GeneralField, Query } from '@formily/core';
import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema } from '@formily/react'; import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { useUpdate } from 'ahooks';
import { message } from 'antd'; import { message } from 'antd';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get'; import get from 'lodash/get';
@ -17,6 +18,7 @@ import set from 'lodash/set';
import React, { ComponentType, useCallback, useContext, useEffect, useMemo } from 'react'; import React, { ComponentType, useCallback, useContext, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { APIClient, useAPIClient } from '../../api-client'; import { APIClient, useAPIClient } from '../../api-client';
import { useRefreshComponent, useRefreshFieldSchema } from '../../formily/NocoBaseRecursionField';
import { LAZY_COMPONENT_KEY } from '../../lazy-helper'; import { LAZY_COMPONENT_KEY } from '../../lazy-helper';
import { SchemaComponentContext } from '../context'; import { SchemaComponentContext } from '../context';
import { addAppVersion } from './addAppVersion'; import { addAppVersion } from './addAppVersion';
@ -29,7 +31,7 @@ interface CreateDesignableProps {
model?: GeneralField; model?: GeneralField;
query?: Query; query?: Query;
api?: APIClient; api?: APIClient;
refresh?: (options?: { refreshParent?: boolean }) => void; refresh?: (options?: { refreshParentSchema?: boolean }) => void;
onSuccess?: any; onSuccess?: any;
t?: any; t?: any;
/** /**
@ -219,7 +221,6 @@ export class Designable {
message.success(t('Saved successfully'), 0.2); message.success(t('Saved successfully'), 0.2);
}); });
this.on('initializeActionContext', async ({ schema }) => { this.on('initializeActionContext', async ({ schema }) => {
this.refresh();
if (!schema?.['x-uid']) { if (!schema?.['x-uid']) {
return; return;
} }
@ -316,7 +317,7 @@ export class Designable {
return false; return false;
} }
refresh(options?: { refreshParent?: boolean }) { refresh(options?: { refreshParentSchema?: boolean }) {
const { refresh } = this.options; const { refresh } = this.options;
return refresh?.(options); return refresh?.(options);
} }
@ -743,7 +744,7 @@ export function useFindComponent() {
// TODO // TODO
export function useDesignable() { export function useDesignable() {
const { designable, setDesignable, refresh, reset } = useContext(SchemaComponentContext); const { designable, setDesignable, refresh: refreshFromContext, reset } = useContext(SchemaComponentContext);
const schemaOptions = useContext(SchemaOptionsContext); const schemaOptions = useContext(SchemaOptionsContext);
const components = useMemo(() => schemaOptions?.components || {}, [schemaOptions]); const components = useMemo(() => schemaOptions?.components || {}, [schemaOptions]);
const DesignableBar = useMemo( const DesignableBar = useMemo(
@ -752,6 +753,21 @@ export function useDesignable() {
}, },
[], [],
); );
const update = useUpdate();
const refreshFieldSchema = useRefreshFieldSchema();
const refreshComponent = useRefreshComponent();
const refresh = useCallback(
(options?: { refreshParentSchema?: boolean }) => {
refreshFromContext?.();
// refresh current component
update();
// refresh fieldSchema context value
refreshFieldSchema?.(options);
// refresh component context value
refreshComponent?.();
},
[refreshFromContext, update, refreshFieldSchema, refreshComponent],
);
const field = useField(); const field = useField();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const api = useAPIClient(); const api = useAPIClient();

View File

@ -14,7 +14,7 @@ import React from 'react';
export interface ISchemaComponentContext { export interface ISchemaComponentContext {
scope?: any; scope?: any;
components?: SchemaReactComponents; components?: SchemaReactComponents;
refresh?: (options?: { refreshParent?: boolean }) => void; refresh?: (options?: { refreshParentSchema?: boolean }) => void;
reset?: () => void; reset?: () => void;
designable?: boolean; designable?: boolean;
setDesignable?: (value: boolean) => void; setDesignable?: (value: boolean) => void;

View File

@ -10,6 +10,7 @@
import { merge } from '@formily/shared'; import { merge } from '@formily/shared';
import React from 'react'; import React from 'react';
import { useUpdate } from 'ahooks';
import { SchemaInitializerSwitch, useSchemaInitializer } from '../../application'; import { SchemaInitializerSwitch, useSchemaInitializer } from '../../application';
import { useCurrentSchema } from '../utils'; import { useCurrentSchema } from '../utils';
@ -23,6 +24,7 @@ export const InitializerWithSwitch = (props) => {
schema?.name || item?.schema?.name, schema?.name || item?.schema?.name,
); );
const { insert } = useSchemaInitializer(); const { insert } = useSchemaInitializer();
const update = useUpdate();
return ( return (
<SchemaInitializerSwitch <SchemaInitializerSwitch
checked={exists} checked={exists}
@ -33,11 +35,13 @@ export const InitializerWithSwitch = (props) => {
return; return;
} }
if (exists) { if (exists) {
return remove(); remove();
return update();
} }
const s = merge(schema || {}, item.schema || {}); const s = merge(schema || {}, item.schema || {});
item?.schemaInitialize?.(s); item?.schemaInitialize?.(s);
insert(s); insert(s);
update();
}} }}
/> />
); );

View File

@ -7,14 +7,15 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { useState, useEffect, useCallback } from 'react';
import { uid } from '@formily/shared';
import { Form, onFormValuesChange } from '@formily/core'; import { Form, onFormValuesChange } from '@formily/core';
import { useVariables, useLocalVariables } from '../../variables'; import { Schema, useFieldSchema } from '@formily/react';
import { useFieldSchema } from '@formily/react'; import { uid } from '@formily/shared';
import { LinkageRuleCategory, LinkageRuleDataKeyMap } from './type';
import { getSatisfiedValueMap } from './compute-rules';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import { useLocalVariables, useVariables } from '../../variables';
import { getSatisfiedValueMap } from './compute-rules';
import { LinkageRuleCategory, LinkageRuleDataKeyMap } from './type';
export function useSatisfiedActionValues({ export function useSatisfiedActionValues({
formValues, formValues,
category = 'default', category = 'default',
@ -33,10 +34,11 @@ export function useSatisfiedActionValues({
const variables = useVariables(); const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: { values: formValues } as any }); const localVariables = useLocalVariables({ currentForm: { values: formValues } as any });
const localSchema = schema ?? fieldSchema; const localSchema = schema ?? fieldSchema;
const linkageRules = rules ?? localSchema[LinkageRuleDataKeyMap[category]]; const styleRules = rules ?? localSchema[LinkageRuleDataKeyMap[category]];
const compute = useCallback(() => { const compute = useCallback(() => {
if (linkageRules && formValues) { if (styleRules && formValues) {
getSatisfiedValueMap({ rules: linkageRules, variables, localVariables }) getSatisfiedValueMap({ rules: styleRules, variables, localVariables })
.then((valueMap) => { .then((valueMap) => {
if (!isEmpty(valueMap)) { if (!isEmpty(valueMap)) {
setValueMap(valueMap); setValueMap(valueMap);
@ -46,11 +48,11 @@ export function useSatisfiedActionValues({
throw new Error(err.message); throw new Error(err.message);
}); });
} }
}, [variables, localVariables, linkageRules, formValues]); }, [variables, localVariables, styleRules, formValues]);
useEffect(() => { useEffect(() => {
compute(); compute();
}, [compute]);
useEffect(() => {
if (form) { if (form) {
const id = uid(); const id = uid();
form.addEffects(id, () => { form.addEffects(id, () => {
@ -63,5 +65,22 @@ export function useSatisfiedActionValues({
}; };
} }
}, [form, compute]); }, [form, compute]);
return { valueMap }; return { valueMap };
} }
export const GetStyleRules: React.FC<{
record: Record<string, any>;
schema: Schema;
onStyleChange?: (value: Record<string, any>) => void;
}> = React.memo(({ record, schema, onStyleChange }) => {
const { valueMap } = useSatisfiedActionValues({ formValues: record, category: 'style', schema });
useEffect(() => {
onStyleChange(valueMap);
}, [onStyleChange, valueMap]);
return null;
});
GetStyleRules.displayName = 'GetStyleRules';

View File

@ -96,7 +96,7 @@ import { useRecord } from '../record-provider';
import { ActionContextProvider } from '../schema-component/antd/action/context'; import { ActionContextProvider } from '../schema-component/antd/action/context';
import { SubFormProvider, useSubFormValue } from '../schema-component/antd/association-field/hooks'; import { SubFormProvider, useSubFormValue } from '../schema-component/antd/association-field/hooks';
import { FormDialog } from '../schema-component/antd/form-dialog'; import { FormDialog } from '../schema-component/antd/form-dialog';
import { SchemaComponentContext, useNewRefreshContext } from '../schema-component/context'; import { SchemaComponentContext } from '../schema-component/context';
import { FormProvider } from '../schema-component/core/FormProvider'; import { FormProvider } from '../schema-component/core/FormProvider';
import { RemoteSchemaComponent } from '../schema-component/core/RemoteSchemaComponent'; import { RemoteSchemaComponent } from '../schema-component/core/RemoteSchemaComponent';
import { SchemaComponent } from '../schema-component/core/SchemaComponent'; import { SchemaComponent } from '../schema-component/core/SchemaComponent';
@ -174,14 +174,6 @@ export const SchemaSettingsDropdown: React.FC<SchemaSettingsProps> = React.memo(
// 单测中需要在首次就把菜单渲染出来,否则不会触发菜单的渲染进而报错。原因未知。 // 单测中需要在首次就把菜单渲染出来,否则不会触发菜单的渲染进而报错。原因未知。
const [openDropdown, setOpenDropdown] = useState(process.env.__TEST__ ? true : false); const [openDropdown, setOpenDropdown] = useState(process.env.__TEST__ ? true : false);
const toolbarVisible = useContext(SchemaToolbarVisibleContext); const toolbarVisible = useContext(SchemaToolbarVisibleContext);
const refreshCtx = useContext(SchemaComponentContext);
const newRefreshCtx = useNewRefreshContext(refreshCtx.refresh);
const newDn: any = useMemo(() => {
const result = Object.create(dn);
result.refresh = newRefreshCtx.refresh;
return result;
}, [dn, newRefreshCtx.refresh]);
useEffect(() => { useEffect(() => {
if (toolbarVisible) { if (toolbarVisible) {
@ -214,8 +206,7 @@ export const SchemaSettingsDropdown: React.FC<SchemaSettingsProps> = React.memo(
const items = getMenuItems(() => props.children); const items = getMenuItems(() => props.children);
return ( return (
<SchemaComponentContext.Provider value={newRefreshCtx}> <SchemaSettingsProvider visible={visible} setVisible={setVisible} dn={dn} {...others}>
<SchemaSettingsProvider visible={visible} setVisible={setVisible} dn={newDn} {...others}>
<Component /> <Component />
<Dropdown <Dropdown
open={visible} open={visible}
@ -237,7 +228,6 @@ export const SchemaSettingsDropdown: React.FC<SchemaSettingsProps> = React.memo(
<div data-testid={props['data-testid']}>{typeof title === 'string' ? <span>{title}</span> : title}</div> <div data-testid={props['data-testid']}>{typeof title === 'string' ? <span>{title}</span> : title}</div>
</Dropdown> </Dropdown>
</SchemaSettingsProvider> </SchemaSettingsProvider>
</SchemaComponentContext.Provider>
); );
}); });
@ -300,7 +290,6 @@ export const SchemaSettingsFormItemTemplate = function FormItemTemplate(props) {
const sdn = createDesignable({ const sdn = createDesignable({
t, t,
api, api,
refresh: dn.refresh.bind(dn),
current: templateSchema.parent, current: templateSchema.parent,
}); });
sdn.loadAPIClientEvents(); sdn.loadAPIClientEvents();
@ -371,7 +360,6 @@ export const SchemaSettingsFormItemTemplate = function FormItemTemplate(props) {
const sdn = createDesignable({ const sdn = createDesignable({
t, t,
api, api,
refresh: dn.refresh.bind(dn),
current: gridSchema.parent, current: gridSchema.parent,
}); });
sdn.loadAPIClientEvents(); sdn.loadAPIClientEvents();
@ -400,6 +388,7 @@ export const SchemaSettingsFormItemTemplate = function FormItemTemplate(props) {
'x-template-key': key, 'x-template-key': key,
}, },
}); });
dn.refresh();
}} }}
> >
{t('Save as block template')} {t('Save as block template')}
@ -537,6 +526,7 @@ export const SchemaSettingsRemove: FC<SchemaSettingsRemoveProps> = (props) => {
} }
await confirm?.onOk?.(); await confirm?.onOk?.();
delete form.values[fieldSchema.name]; delete form.values[fieldSchema.name];
dn.refresh({ refreshParentSchema: true });
removeActiveFieldName?.(fieldSchema.name as string); removeActiveFieldName?.(fieldSchema.name as string);
form?.query(new RegExp(`${fieldSchema.parent.name}.${fieldSchema.name}$`)).forEach((field: Field) => { form?.query(new RegExp(`${fieldSchema.parent.name}.${fieldSchema.name}$`)).forEach((field: Field) => {
// 如果字段被删掉,那么在提交的时候不应该提交这个字段 // 如果字段被删掉,那么在提交的时候不应该提交这个字段

View File

@ -9,7 +9,7 @@
import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout'; import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout';
import { Input, Spin } from 'antd'; import { Input, Spin } from 'antd';
import React, { useContext, useState } from 'react'; import React, { useContext, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useAPIClient, useRequest, useSchemaTemplateManager } from '..'; import { useAPIClient, useRequest, useSchemaTemplateManager } from '..';
import { useNavigateNoUpdate } from '../application/CustomRouterContextProvider'; import { useNavigateNoUpdate } from '../application/CustomRouterContextProvider';
@ -73,6 +73,8 @@ export const BlockTemplateDetails = () => {
const params = useParams<any>(); const params = useParams<any>();
const key = params?.key; const key = params?.key;
const value = useContext(SchemaComponentContext); const value = useContext(SchemaComponentContext);
const schemaComponentContext = useMemo(() => ({ ...value, designable: true }), [value]);
const { data, loading } = useRequest<{ const { data, loading } = useRequest<{
data: any; data: any;
}>({ }>({
@ -82,9 +84,11 @@ export const BlockTemplateDetails = () => {
filterByTk: key, filterByTk: key,
}, },
}); });
if (loading) { if (loading) {
return <Spin />; return <Spin />;
} }
return ( return (
<div> <div>
<AntdPageHeader <AntdPageHeader
@ -96,7 +100,7 @@ export const BlockTemplateDetails = () => {
title={<EditableTitle filterByTk={key} title={data?.data?.name} />} title={<EditableTitle filterByTk={key} title={data?.data?.name} />}
/> />
<div style={{ margin: 'var(--nb-spacing)' }}> <div style={{ margin: 'var(--nb-spacing)' }}>
<SchemaComponentContext.Provider value={{ ...value, designable: true }}> <SchemaComponentContext.Provider value={schemaComponentContext}>
<RemoteSchemaComponent uid={data?.data?.uid} /> <RemoteSchemaComponent uid={data?.data?.uid} />
</SchemaComponentContext.Provider> </SchemaComponentContext.Provider>
</div> </div>

View File

@ -7,23 +7,22 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import React, { useState } from 'react'; import { ISchema, Schema } from '@formily/react';
import { Card, Row, Col, Tabs, Divider } from 'antd';
import { import {
CollectionProvider,
CollectionProvider_deprecated, CollectionProvider_deprecated,
ResourceActionProvider, ResourceActionProvider,
SchemaComponentContext, SchemaComponentContext,
usePlugin, usePlugin,
useSchemaComponentContext, useSchemaComponentContext,
} from '@nocobase/client'; } from '@nocobase/client';
import { ISchema, Schema } from '@formily/react'; import { Card, Col, Divider, Row, Tabs } from 'antd';
import React, { useMemo, useState } from 'react';
import ACLPlugin from '.';
import { NewRole } from './NewRole';
import { RolesManagerContext } from './RolesManagerProvider';
import { RolesMenu } from './RolesMenu'; import { RolesMenu } from './RolesMenu';
import { useACLTranslation } from './locale'; import { useACLTranslation } from './locale';
import ACLPlugin from '.';
import { RolesManagerContext } from './RolesManagerProvider';
import { Permissions } from './permissions/Permissions'; import { Permissions } from './permissions/Permissions';
import { NewRole } from './NewRole';
const collection = { const collection = {
name: 'roles', name: 'roles',
@ -75,9 +74,10 @@ export const RolesManagement: React.FC = () => {
})); }));
const [role, setRole] = useState(null); const [role, setRole] = useState(null);
const scCtx = useSchemaComponentContext(); const scCtx = useSchemaComponentContext();
const schemaComponentContext = useMemo(() => ({ ...scCtx, designable: false }), [scCtx]);
return ( return (
<SchemaComponentContext.Provider value={{ ...scCtx, designable: false }}> <SchemaComponentContext.Provider value={schemaComponentContext}>
<RolesManagerContext.Provider value={{ role, setRole }}> <RolesManagerContext.Provider value={{ role, setRole }}>
<Card> <Card>
<Row gutter={24} style={{ flexWrap: 'nowrap' }}> <Row gutter={24} style={{ flexWrap: 'nowrap' }}>

View File

@ -8,11 +8,12 @@
*/ */
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { RecursionField, observer, useField, useFieldSchema } from '@formily/react'; import { observer, useField, useFieldSchema } from '@formily/react';
import { import {
ActionContextProvider, ActionContextProvider,
CollectionProvider_deprecated, CollectionProvider_deprecated,
FormBlockContext, FormBlockContext,
NocoBaseRecursionField,
PopupSettingsProvider, PopupSettingsProvider,
RecordProvider, RecordProvider,
TabsContextProvider, TabsContextProvider,
@ -208,7 +209,7 @@ export const DuplicateAction = observer(
<RecordProvider record={{ ...parentRecordData, __collection: duplicateCollection || __collection }}> <RecordProvider record={{ ...parentRecordData, __collection: duplicateCollection || __collection }}>
<ActionContextProvider value={{ ...ctx, visible, setVisible }}> <ActionContextProvider value={{ ...ctx, visible, setVisible }}>
<PopupSettingsProvider enableURL={false}> <PopupSettingsProvider enableURL={false}>
<RecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties /> <NocoBaseRecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties />
</PopupSettingsProvider> </PopupSettingsProvider>
</ActionContextProvider> </ActionContextProvider>
</RecordProvider> </RecordProvider>

View File

@ -124,7 +124,7 @@ test.describe('direct duplicate & copy into the form and continue to fill in', (
.getByTestId('drawer-Action.Container-general2-Duplicate') .getByTestId('drawer-Action.Container-general2-Duplicate')
.getByLabel('schema-initializer-Grid-popup') .getByLabel('schema-initializer-Grid-popup')
.hover(); .hover();
await page.getByRole('menuitem', { name: 'form Form right' }).hover(); await page.getByRole('menuitem', { name: 'Form right' }).hover();
await page.getByRole('menuitem', { name: 'Current collection' }).click(); await page.getByRole('menuitem', { name: 'Current collection' }).click();
await page await page
.getByTestId('drawer-Action.Container-general2-Duplicate') .getByTestId('drawer-Action.Container-general2-Duplicate')

View File

@ -7,20 +7,21 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { RecursionField, observer, useFieldSchema } from '@formily/react'; import { css } from '@emotion/css';
import { observer, useFieldSchema } from '@formily/react';
import { import {
CollectionContext, CollectionContext,
DataSourceContext, DataSourceContext,
DndContext, DndContext,
Icon,
NocoBaseRecursionField,
useBlockHeight,
useDesignable, useDesignable,
useSchemaInitializerRender, useSchemaInitializerRender,
withDynamicSchemaProps, withDynamicSchemaProps,
Icon,
useBlockHeight,
} from '@nocobase/client'; } from '@nocobase/client';
import { css } from '@emotion/css'; import { Avatar, List, Space, theme } from 'antd';
import { Space, List, Avatar, theme } from 'antd'; import React, { createContext, useEffect, useState } from 'react';
import React, { createContext, useState, useEffect } from 'react';
import { WorkbenchLayout } from './workbenchBlockSettings'; import { WorkbenchLayout } from './workbenchBlockSettings';
const ConfigureActionsButton = observer( const ConfigureActionsButton = observer(
@ -72,7 +73,7 @@ const InternalIcons = () => {
{layout === WorkbenchLayout.Grid ? ( {layout === WorkbenchLayout.Grid ? (
<Space wrap size={gap}> <Space wrap size={gap}>
{fieldSchema.mapProperties((s, key) => ( {fieldSchema.mapProperties((s, key) => (
<RecursionField name={key} schema={s} key={key} /> <NocoBaseRecursionField name={key} schema={s} key={key} />
))} ))}
</Space> </Space>
) : ( ) : (
@ -103,7 +104,7 @@ const InternalIcons = () => {
> >
<List.Item.Meta <List.Item.Meta
avatar={<Avatar style={{ backgroundColor }} icon={<Icon type={icon} />} />} avatar={<Avatar style={{ backgroundColor }} icon={<Icon type={icon} />} />}
title={<RecursionField name={key} schema={s} key={key} />} title={<NocoBaseRecursionField name={key} schema={s} key={key} />}
></List.Item.Meta> ></List.Item.Meta>
</List.Item> </List.Item>
); );

View File

@ -8,44 +8,44 @@
*/ */
import { LeftOutlined, RightOutlined } from '@ant-design/icons'; import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { RecursionField, Schema, observer, useFieldSchema, useField } from '@formily/react'; import { Schema, useField, useFieldSchema } from '@formily/react';
import { import {
ActionContextProvider,
CollectionProvider,
NocoBaseRecursionField,
PopupContextProvider, PopupContextProvider,
RecordProvider, RecordProvider,
SchemaComponentOptions,
getLabelFormatValue, getLabelFormatValue,
handleDateChangeOnForm,
useACLRoleContext, useACLRoleContext,
useActionContext,
useCollection, useCollection,
useCollectionParentRecordData, useCollectionParentRecordData,
useDesignable,
useFormBlockContext,
useLazy, useLazy,
usePopupUtils, usePopupUtils,
useProps, useProps,
useToken, useToken,
withDynamicSchemaProps, withDynamicSchemaProps,
useDesignable,
ActionContextProvider,
useActionContext,
CollectionProvider,
SchemaComponentOptions,
useFormBlockContext,
handleDateChangeOnForm,
withSkeletonComponent, withSkeletonComponent,
} from '@nocobase/client'; } from '@nocobase/client';
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { get, cloneDeep } from 'lodash'; import { cloneDeep, get } from 'lodash';
import React, { useMemo, useState, useEffect, useCallback } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Calendar as BigCalendar, View, dayjsLocalizer } from 'react-big-calendar'; import { View } from 'react-big-calendar';
import * as dates from 'react-big-calendar/lib/utils/dates';
import { i18nt, useTranslation } from '../../locale'; import { i18nt, useTranslation } from '../../locale';
import { CalendarRecordViewer, findEventSchema } from './CalendarRecordViewer'; import { CalendarRecordViewer, findEventSchema } from './CalendarRecordViewer';
import Header from './components/Header'; import Header from './components/Header';
import { CalendarToolbarContext } from './context'; import { CalendarToolbarContext } from './context';
import GlobalStyle from './global.style'; import GlobalStyle from './global.style';
import { useCalenderHeight } from './hook'; import { useCalenderHeight } from './hook';
import { addNew } from './schema';
import useStyle from './style'; import useStyle from './style';
import type { ToolbarProps } from './types'; import type { ToolbarProps } from './types';
import { formatDate } from './utils'; import { formatDate } from './utils';
import { addNew } from './schema';
interface Event { interface Event {
id: string; id: string;
colorFieldValue: string; colorFieldValue: string;
@ -93,7 +93,7 @@ function Toolbar(props: ToolbarProps) {
); );
return ( return (
<CalendarToolbarContext.Provider value={props}> <CalendarToolbarContext.Provider value={props}>
<RecursionField name={toolBarSchema.name} schema={toolBarSchema} /> <NocoBaseRecursionField name={toolBarSchema.name} schema={toolBarSchema} />
</CalendarToolbarContext.Provider> </CalendarToolbarContext.Provider>
); );
} }
@ -464,7 +464,7 @@ export const Calendar: any = withDynamicSchemaProps(
<ActionContextProvider value={{ ...ctx, visible: visibleAddNewer, setVisible: setVisibleAddNewer }}> <ActionContextProvider value={{ ...ctx, visible: visibleAddNewer, setVisible: setVisibleAddNewer }}>
<CollectionProvider name={collection.name}> <CollectionProvider name={collection.name}>
<SchemaComponentOptions scope={{ useCreateFormBlockProps }}> <SchemaComponentOptions scope={{ useCreateFormBlockProps }}>
<RecursionField <NocoBaseRecursionField
onlyRenderProperties onlyRenderProperties
basePath={field?.address} basePath={field?.address}
schema={fieldSchema} schema={fieldSchema}

View File

@ -7,7 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { RecursionField, Schema, useFieldSchema } from '@formily/react'; import { Schema, useFieldSchema } from '@formily/react';
import { NocoBaseRecursionField } from '@nocobase/client';
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
export const CalendarRecordViewer: FC = (props) => { export const CalendarRecordViewer: FC = (props) => {
@ -18,7 +19,7 @@ export const CalendarRecordViewer: FC = (props) => {
return null; return null;
} }
return <RecursionField schema={eventSchema} name={eventSchema.name} />; return <NocoBaseRecursionField schema={eventSchema} name={eventSchema.name} />;
}; };
export function findEventSchema(schema: Schema) { export function findEventSchema(schema: Schema) {

View File

@ -9,11 +9,12 @@
import { FormLayout } from '@formily/antd-v5'; import { FormLayout } from '@formily/antd-v5';
import { Field } from '@formily/core'; import { Field } from '@formily/core';
import { RecursionField, Schema, SchemaOptionsContext, observer, useField, useForm } from '@formily/react'; import { Schema, SchemaOptionsContext, observer, useField, useForm } from '@formily/react';
import { import {
APIClientProvider, APIClientProvider,
FormDialog, FormDialog,
FormProvider, FormProvider,
NocoBaseRecursionField,
SchemaComponent, SchemaComponent,
SchemaComponentOptions, SchemaComponentOptions,
css, css,
@ -49,7 +50,7 @@ export const Options = observer(
setSchema(new Schema(template.configurableProperties || {})); setSchema(new Schema(template.configurableProperties || {}));
} }
}, [form.values.type]); }, [form.values.type]);
return <RecursionField schema={s} />; return <NocoBaseRecursionField schema={s} />;
}, },
{ displayName: 'Options' }, { displayName: 'Options' },
); );

View File

@ -74,10 +74,26 @@ test.describe('form item & view form', () => {
}, },
supportedOptions: ['oneToMany', 'manyToOne', 'manyToMany', 'oneToOneBelongsTo', 'oneToOneHasOne'], supportedOptions: ['oneToMany', 'manyToOne', 'manyToMany', 'oneToOneBelongsTo', 'oneToOneHasOne'],
expectValue: async () => { expectValue: async () => {
await expect(page.getByText(record.oneToMany.map((item: any) => item.id).join(','))).toBeVisible(); await expect(
await expect(page.getByText(record.manyToOne.id)).toBeVisible(); page
await expect(page.getByText(record.manyToMany.map((item: any) => item.id).join(','))).toBeVisible(); .getByLabel('block-item-CollectionField-general-form-general.oneToMany-oneToMany')
await expect(page.getByText(record.oneToOneBelongsTo.id)).toBeVisible(); .getByText(record.oneToMany.map((item: any) => item.id).join(',')),
).toBeVisible();
await expect(
page
.getByLabel('block-item-CollectionField-general-form-general.manyToOne-manyToOne')
.getByText(record.manyToOne.id),
).toBeVisible();
await expect(
page
.getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany')
.getByText(record.manyToMany.map((item: any) => item.id).join(',')),
).toBeVisible();
await expect(
page
.getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-')
.getByText(record.oneToOneBelongsTo.id),
).toBeVisible();
}, },
}); });
}); });

View File

@ -15,7 +15,7 @@ test.describe('form item & filter form', () => {
// 在页面上创建一个筛选表单,并在表单中添加一个字段 // 在页面上创建一个筛选表单,并在表单中添加一个字段
await page.getByLabel('schema-initializer-Grid-page:').hover(); await page.getByLabel('schema-initializer-Grid-page:').hover();
await page.getByRole('menuitem', { name: 'form Form right' }).nth(1).click(); await page.getByRole('menuitem', { name: 'Form right' }).nth(1).click();
await page.getByRole('menuitem', { name: 'Users' }).click(); await page.getByRole('menuitem', { name: 'Users' }).click();
await page.getByLabel('schema-initializer-Grid-filterForm:configureFields-users').hover(); await page.getByLabel('schema-initializer-Grid-filterForm:configureFields-users').hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click(); await page.getByRole('menuitem', { name: 'Nickname' }).click();

View File

@ -186,8 +186,10 @@ export const ConfigurationTable = () => {
}); });
}); });
}; };
const schemaComponentContext = useMemo(() => ({ ...ctx, designable: false, dataSourceData }), [ctx, dataSourceData]);
return ( return (
<SchemaComponentContext.Provider value={{ ...ctx, designable: false, dataSourceData }}> <SchemaComponentContext.Provider value={schemaComponentContext}>
<SchemaComponent <SchemaComponent
schema={collectionSchema} schema={collectionSchema}
components={{ components={{

View File

@ -28,7 +28,7 @@ import {
useRecord, useRecord,
} from '@nocobase/client'; } from '@nocobase/client';
import { getPickerFormat } from '@nocobase/utils/client'; import { getPickerFormat } from '@nocobase/utils/client';
import React, { useContext, useState } from 'react'; import React, { useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CollectionFields } from './CollectionFields'; import { CollectionFields } from './CollectionFields';
import { collectionSchema } from './schemas/collections'; import { collectionSchema } from './schemas/collections';
@ -212,8 +212,10 @@ export const ConfigurationTable = () => {
}; };
const ctx = useContext(SchemaComponentContext); const ctx = useContext(SchemaComponentContext);
const schemaComponentContext = useMemo(() => ({ ...ctx, designable: false }), [ctx]);
return ( return (
<SchemaComponentContext.Provider value={{ ...ctx, designable: false }}> <SchemaComponentContext.Provider value={schemaComponentContext}>
<SchemaComponent <SchemaComponent
schema={collectionSchema} schema={collectionSchema}
components={{ components={{

View File

@ -41,12 +41,13 @@ export const useAvailableActions = () => {
return useContext(AvailableActionsContext); return useContext(AvailableActionsContext);
}; };
const schemaComponentContext = { designable: false };
export const DataSourceTable = () => { export const DataSourceTable = () => {
const record = useRecord(); const record = useRecord();
const plugin = usePlugin(PluginDataSourceManagerClient); const plugin = usePlugin(PluginDataSourceManagerClient);
return ( return (
<div> <div>
<SchemaComponentContext.Provider value={{ designable: false }}> <SchemaComponentContext.Provider value={schemaComponentContext}>
<AvailableActionsProver> <AvailableActionsProver>
<SchemaComponent <SchemaComponent
schema={dataSourceSchema(plugin.getExtendedTabs())} schema={dataSourceSchema(plugin.getExtendedTabs())}

View File

@ -7,7 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { RecursionField, useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { NocoBaseRecursionField } from '@nocobase/client';
import { Schema } from '@nocobase/utils'; import { Schema } from '@nocobase/utils';
import React, { FC } from 'react'; import React, { FC } from 'react';
@ -19,5 +20,5 @@ export const GanttRecordViewer: FC = (props) => {
return null; return null;
} }
return <RecursionField schema={eventSchema} name={eventSchema.name} />; return <NocoBaseRecursionField schema={eventSchema} name={eventSchema.name} />;
}; };

View File

@ -8,8 +8,9 @@
*/ */
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { RecursionField, useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { import {
NocoBaseRecursionField,
PopupContextProvider, PopupContextProvider,
RecordProvider, RecordProvider,
useAPIClient, useAPIClient,
@ -520,8 +521,8 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
<RecordProvider record={record} parent={parentRecordData}> <RecordProvider record={record} parent={parentRecordData}>
<GanttRecordViewer /> <GanttRecordViewer />
</RecordProvider> </RecordProvider>
<RecursionField name={'anctionBar'} schema={fieldSchema.properties.toolBar} /> <NocoBaseRecursionField name={'anctionBar'} schema={fieldSchema.properties.toolBar} />
<RecursionField name={'table'} schema={fieldSchema.properties.table} /> <NocoBaseRecursionField name={'table'} schema={fieldSchema.properties.table} />
<div className={styles.wrapper} onKeyDown={handleKeyDown} tabIndex={0} ref={wrapperRef}> <div className={styles.wrapper} onKeyDown={handleKeyDown} tabIndex={0} ref={wrapperRef}>
<TaskGantt <TaskGantt
gridProps={gridProps} gridProps={gridProps}

View File

@ -18,7 +18,7 @@ test.describe('where map block can be added', () => {
// 1. 在页面中添加地图区块,因为没有配置 Access key 等信息,所以会显示错误提示 // 1. 在页面中添加地图区块,因为没有配置 Access key 等信息,所以会显示错误提示
await page.getByLabel('schema-initializer-Grid-page:').hover(); await page.getByLabel('schema-initializer-Grid-page:').hover();
await page.getByRole('menuitem', { name: 'table Map right' }).hover(); await page.getByRole('menuitem', { name: 'Map right' }).hover();
await page.getByRole('menuitem', { name: 'map', exact: true }).click(); await page.getByRole('menuitem', { name: 'map', exact: true }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect( await expect(

View File

@ -7,8 +7,13 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { RecursionField, useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { useCollection, useCollectionRecordData, VariablePopupRecordProvider } from '@nocobase/client'; import {
NocoBaseRecursionField,
useCollection,
useCollectionRecordData,
VariablePopupRecordProvider,
} from '@nocobase/client';
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
export const MapBlockDrawer: FC = (props) => { export const MapBlockDrawer: FC = (props) => {
@ -32,7 +37,7 @@ export const MapBlockDrawer: FC = (props) => {
return ( return (
<VariablePopupRecordProvider recordData={recordData} collection={collection}> <VariablePopupRecordProvider recordData={recordData} collection={collection}>
<RecursionField schema={schema} name={schema.name} /> <NocoBaseRecursionField schema={schema} name={schema.name} />
</VariablePopupRecordProvider> </VariablePopupRecordProvider>
); );
}; };

View File

@ -7,8 +7,16 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { RecursionField, useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import { ActionBarProvider, SortableItem, TabsContextProvider, css, cx, useDesigner } from '@nocobase/client'; import {
ActionBarProvider,
NocoBaseRecursionField,
SortableItem,
TabsContextProvider,
css,
cx,
useDesigner,
} from '@nocobase/client';
import { TabsProps } from 'antd'; import { TabsProps } from 'antd';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
@ -106,34 +114,34 @@ const InternalPage: React.FC = (props) => {
})} })}
> >
{isHeaderEnabled && ( {isHeaderEnabled && (
<RecursionField <NocoBaseRecursionField
schema={fieldSchema} schema={fieldSchema}
filterProperties={(s) => { filterProperties={(s) => {
return 'MHeader' === s['x-component']; return 'MHeader' === s['x-component'];
}} }}
></RecursionField> ></NocoBaseRecursionField>
)} )}
<TabsContextProvider <TabsContextProvider
PaneRoot={GlobalActionProvider} PaneRoot={GlobalActionProvider}
activeKey={searchParams.get('tab')} activeKey={searchParams.get('tab')}
onChange={onTabsChange} onChange={onTabsChange}
> >
<RecursionField <NocoBaseRecursionField
schema={isTabsEnabled ? fieldSchema : pageSchema} schema={isTabsEnabled ? fieldSchema : pageSchema}
filterProperties={(s) => { filterProperties={(s) => {
return 'Tabs' === s['x-component'] || 'Grid' === s['x-component'] || 'Grid.Row' === s['x-component']; return 'Tabs' === s['x-component'] || 'Grid' === s['x-component'] || 'Grid.Row' === s['x-component'];
}} }}
></RecursionField> ></NocoBaseRecursionField>
</TabsContextProvider> </TabsContextProvider>
</div> </div>
<GlobalActionProvider> <GlobalActionProvider>
{!tabsSchema && ( {!tabsSchema && (
<RecursionField <NocoBaseRecursionField
schema={fieldSchema} schema={fieldSchema}
filterProperties={(s) => { filterProperties={(s) => {
return s['x-component'] !== 'MHeader'; return s['x-component'] !== 'MHeader';
}} }}
></RecursionField> ></NocoBaseRecursionField>
)} )}
</GlobalActionProvider> </GlobalActionProvider>
</SortableItem> </SortableItem>

View File

@ -172,7 +172,7 @@ test.describe('PageHeader', () => {
test('Itemadd and remove', async ({ page }) => { test('Itemadd and remove', async ({ page }) => {
// 添加页面内容 // 添加页面内容
await page.getByLabel('schema-initializer-Grid-mobile:addBlock').hover(); await page.getByLabel('schema-initializer-Grid-mobile:addBlock').hover();
await page.getByRole('menuitem', { name: 'form Markdown' }).click(); await page.getByRole('menuitem', { name: 'Markdown' }).click();
await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible(); await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible();
await page.getByLabel('action-Action-undefined').click(); await page.getByLabel('action-Action-undefined').click();

View File

@ -7,8 +7,15 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { ISchema, observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { ISchema, observer, useField, useFieldSchema } from '@formily/react';
import { Action, SchemaComponent, useActionContext, useZIndexContext, zIndexContext } from '@nocobase/client'; import {
Action,
NocoBaseRecursionField,
SchemaComponent,
useActionContext,
useZIndexContext,
zIndexContext,
} from '@nocobase/client';
import { ConfigProvider } from 'antd'; import { ConfigProvider } from 'antd';
import { Popup } from 'antd-mobile'; import { Popup } from 'antd-mobile';
import { CloseOutline } from 'antd-mobile-icons'; import { CloseOutline } from 'antd-mobile-icons';
@ -109,7 +116,7 @@ export const ActionDrawerUsedInMobile: any = observer((props: { footerNodeName?:
)} )}
{footerSchema ? ( {footerSchema ? (
<div className="nb-mobile-action-drawer-footer" style={isSpecialSchema ? specialStyle : null}> <div className="nb-mobile-action-drawer-footer" style={isSpecialSchema ? specialStyle : null}>
<RecursionField <NocoBaseRecursionField
basePath={field.address} basePath={field.address}
schema={fieldSchema} schema={fieldSchema}
onlyRenderProperties onlyRenderProperties

View File

@ -7,9 +7,10 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { RecursionField, useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import { import {
BackButtonUsedInSubPage, BackButtonUsedInSubPage,
NocoBaseRecursionField,
SchemaComponent, SchemaComponent,
TabsContextProvider, TabsContextProvider,
useActionContext, useActionContext,
@ -67,7 +68,7 @@ export const MobileActionPage = ({ level, footerNodeName }) => {
</TabsContextProvider> </TabsContextProvider>
{footerSchema && ( {footerSchema && (
<div className="nb-mobile-action-page-footer" style={zIndexStyle}> <div className="nb-mobile-action-page-footer" style={zIndexStyle}>
<RecursionField <NocoBaseRecursionField
basePath={field.address} basePath={field.address}
schema={fieldSchema} schema={fieldSchema}
onlyRenderProperties onlyRenderProperties

View File

@ -7,11 +7,12 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { observer, useField, useFieldSchema } from '@formily/react';
import { import {
css, css,
DndContext, DndContext,
Icon, Icon,
NocoBaseRecursionField,
SchemaComponent, SchemaComponent,
SortableItem, SortableItem,
Tabs as TabsOfPC, Tabs as TabsOfPC,
@ -54,7 +55,9 @@ export const MobileTabsForMobileActionPage: any = observer(
const items = useMemo(() => { const items = useMemo(() => {
const result = fieldSchema.mapProperties((schema, key) => { const result = fieldSchema.mapProperties((schema, key) => {
return <Tabs.Tab title={<RecursionField name={key} schema={schema} onlyRenderSelf />} key={key}></Tabs.Tab>; return (
<Tabs.Tab title={<NocoBaseRecursionField name={key} schema={schema} onlyRenderSelf />} key={key}></Tabs.Tab>
);
}); });
return result; return result;

View File

@ -7,8 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { RecursionField, useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { cx, SchemaToolbarProvider } from '@nocobase/client'; import { cx, NocoBaseRecursionField, SchemaToolbarProvider } from '@nocobase/client';
import { NavBar } from 'antd-mobile'; import { NavBar } from 'antd-mobile';
import React, { FC } from 'react'; import React, { FC } from 'react';
@ -31,12 +31,12 @@ export const MobilePageNavigationBar: FC = () => {
back={null} back={null}
left={ left={
<SchemaToolbarProvider position="left"> <SchemaToolbarProvider position="left">
<RecursionField name="actionBarLeft" schema={fieldSchema} onlyRenderProperties /> <NocoBaseRecursionField name="actionBarLeft" schema={fieldSchema} onlyRenderProperties />
</SchemaToolbarProvider> </SchemaToolbarProvider>
} }
right={ right={
<SchemaToolbarProvider position="right"> <SchemaToolbarProvider position="right">
<RecursionField name="actionBarRight" schema={fieldSchema} onlyRenderProperties /> <NocoBaseRecursionField name="actionBarRight" schema={fieldSchema} onlyRenderProperties />
</SchemaToolbarProvider> </SchemaToolbarProvider>
} }
> >
@ -44,7 +44,7 @@ export const MobilePageNavigationBar: FC = () => {
</NavBar> </NavBar>
<SchemaToolbarProvider position="bottom"> <SchemaToolbarProvider position="bottom">
<RecursionField name="actionBarBottom" schema={fieldSchema} onlyRenderProperties /> <NocoBaseRecursionField name="actionBarBottom" schema={fieldSchema} onlyRenderProperties />
</SchemaToolbarProvider> </SchemaToolbarProvider>
</div> </div>
); );

View File

@ -8,16 +8,17 @@
*/ */
import { cx } from '@emotion/css'; import { cx } from '@emotion/css';
import { SpaceProps } from 'antd'; import { ISchema, observer, useFieldSchema } from '@formily/react';
import React, { CSSProperties, useContext } from 'react';
import { ISchema, RecursionField, observer, useFieldSchema } from '@formily/react';
import { import {
DndContext, DndContext,
NocoBaseRecursionField,
useProps, useProps,
useSchemaInitializerRender, useSchemaInitializerRender,
useSchemaToolbar, useSchemaToolbar,
withDynamicSchemaProps, withDynamicSchemaProps,
} from '@nocobase/client'; } from '@nocobase/client';
import { SpaceProps } from 'antd';
import React, { CSSProperties, useContext } from 'react';
export interface ActionBarProps { export interface ActionBarProps {
style?: CSSProperties; style?: CSSProperties;
@ -81,7 +82,7 @@ export const MobileNavigationActionBar = withDynamicSchemaProps(
{position === 'left' && render({})} {position === 'left' && render({})}
{props.children && ( {props.children && (
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<RecursionField <NocoBaseRecursionField
onlyRenderProperties onlyRenderProperties
schema={fieldSchema} schema={fieldSchema}
filterProperties={(schema) => schema['x-position'] === position} filterProperties={(schema) => schema['x-position'] === position}
@ -91,7 +92,7 @@ export const MobileNavigationActionBar = withDynamicSchemaProps(
{position === 'right' && render({})} {position === 'right' && render({})}
</div> </div>
) : ( ) : (
<RecursionField <NocoBaseRecursionField
onlyRenderProperties onlyRenderProperties
schema={fieldSchema} schema={fieldSchema}
filterProperties={(schema) => schema['x-position'] === position} filterProperties={(schema) => schema['x-position'] === position}

View File

@ -18,8 +18,8 @@ import {
useAsyncData, useAsyncData,
useSchemaComponentContext, useSchemaComponentContext,
} from '@nocobase/client'; } from '@nocobase/client';
import { Button, Dropdown, Empty, Card } from 'antd'; import { Button, Card, Dropdown, Empty } from 'antd';
import React, { useState } from 'react'; import React, { useMemo, useState } from 'react';
import channelCollection from '../../../../collections/channel'; import channelCollection from '../../../../collections/channel';
import messageLogCollection from '../../../../collections/messageLog'; import messageLogCollection from '../../../../collections/messageLog';
import { useNotificationTranslation } from '../../../locale'; import { useNotificationTranslation } from '../../../locale';
@ -125,10 +125,11 @@ export const ChannelManager = () => {
const { t } = useNotificationTranslation(); const { t } = useNotificationTranslation();
const notificationTypes = useNotificationTypes(); const notificationTypes = useNotificationTypes();
const scCtx = useSchemaComponentContext(); const scCtx = useSchemaComponentContext();
const schemaComponentContext = useMemo(() => ({ ...scCtx, designable: false }), [scCtx]);
return ( return (
<ExtendCollectionsProvider collections={[channelCollection, messageLogCollection]}> <ExtendCollectionsProvider collections={[channelCollection, messageLogCollection]}>
<SchemaComponentContext.Provider value={{ ...scCtx, designable: false }}> <SchemaComponentContext.Provider value={schemaComponentContext}>
<NotificationTypesContext.Provider value={{ channelTypes: notificationTypes }}> <NotificationTypesContext.Provider value={{ channelTypes: notificationTypes }}>
<Card bordered={false}> <Card bordered={false}>
<SchemaComponent <SchemaComponent

View File

@ -7,23 +7,29 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import React from 'react'; import {
ExtendCollectionsProvider,
SchemaComponent,
SchemaComponentContext,
useSchemaComponentContext,
} from '@nocobase/client';
import { Card } from 'antd'; import { Card } from 'antd';
import { messageLogsManagerSchema } from '../schemas'; import React, { useMemo } from 'react';
import { SchemaComponent, SchemaComponentContext, useSchemaComponentContext } from '@nocobase/client';
import { ExtendCollectionsProvider } from '@nocobase/client';
import { useNotificationTranslation } from '../../../locale';
import messageLogCollection from '../../../../collections/messageLog';
import channelCollection from '../../../../collections/channel'; import channelCollection from '../../../../collections/channel';
import messageLogCollection from '../../../../collections/messageLog';
import { useNotificationTranslation } from '../../../locale';
import { useEditFormProps, useNotificationTypes } from '../../channel/hooks'; import { useEditFormProps, useNotificationTypes } from '../../channel/hooks';
import { messageLogsManagerSchema } from '../schemas';
export const LogManager = () => { export const LogManager = () => {
const { t } = useNotificationTranslation(); const { t } = useNotificationTranslation();
const scCtx = useSchemaComponentContext(); const scCtx = useSchemaComponentContext();
const notificationTypes = useNotificationTypes(); const notificationTypes = useNotificationTypes();
const schemaComponentContext = useMemo(() => ({ ...scCtx, designable: false }), [scCtx]);
return ( return (
<ExtendCollectionsProvider collections={[messageLogCollection, channelCollection]}> <ExtendCollectionsProvider collections={[messageLogCollection, channelCollection]}>
<SchemaComponentContext.Provider value={{ ...scCtx, designable: false }}> <SchemaComponentContext.Provider value={schemaComponentContext}>
<Card bordered={false}> <Card bordered={false}>
<SchemaComponent <SchemaComponent
schema={messageLogsManagerSchema} schema={messageLogsManagerSchema}

View File

@ -11,7 +11,7 @@ import { FormLayout } from '@formily/antd-v5';
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { FormProvider, ISchema, Schema, useFieldSchema, useForm } from '@formily/react'; import { FormProvider, ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
import { Alert, Button, Modal, Space, message } from 'antd'; import { Alert, Button, Modal, Space, message } from 'antd';
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
@ -39,7 +39,6 @@ import {
useSchemaInitializer, useSchemaInitializer,
useSchemaInitializerItem, useSchemaInitializerItem,
useSchemaOptionsContext, useSchemaOptionsContext,
useVariableScope,
} from '@nocobase/client'; } from '@nocobase/client';
import WorkflowPlugin, { import WorkflowPlugin, {
DetailsBlockProvider, DetailsBlockProvider,
@ -430,6 +429,7 @@ export function SchemaConfig({ value, onChange }) {
const nodes = useAvailableUpstreams(node); const nodes = useAvailableUpstreams(node);
const form = useForm(); const form = useForm();
const { workflow } = useFlowContext(); const { workflow } = useFlowContext();
const refreshRef = useRef(() => {});
const nodeComponents = {}; const nodeComponents = {};
nodes.forEach((item) => { nodes.forEach((item) => {
@ -452,6 +452,8 @@ export function SchemaConfig({ value, onChange }) {
background: var(--nb-box-bg); background: var(--nb-box-bg);
} }
`, `,
// Using ref to call refresh ensures accessing the latest refresh function
onClose: () => refreshRef.current(),
}, },
properties: { properties: {
tabs: { tabs: {
@ -501,6 +503,8 @@ export function SchemaConfig({ value, onChange }) {
[form, onChange, schema], [form, onChange, schema],
); );
refreshRef.current = refresh;
return ( return (
<SchemaComponentContext.Provider <SchemaComponentContext.Provider
value={{ value={{