feat(variable): add current parent record (#2857)

* feat(variable): add currentParentRecord

* chore: resolve table actions

* chore: get collection name of parent

* chore: table block and details block

* chore: list block

* chore: grid card block

* chore: calendar

* fix: bulk button

* refactor: use useMemo

* fix: fix form block
This commit is contained in:
被雨水过滤的空气-Rain 2023-10-19 22:55:05 +08:00 committed by GitHub
parent 010c286f7c
commit e05a30380a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 857 additions and 621 deletions

View File

@ -5,7 +5,7 @@ import { useRequest } from 'ahooks';
import { Col, Row } from 'antd'; import { Col, Row } from 'antd';
import merge from 'deepmerge'; import merge from 'deepmerge';
import template from 'lodash/template'; import template from 'lodash/template';
import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { import {
TableFieldResource, TableFieldResource,
@ -285,7 +285,7 @@ export const RenderChildrenWithAssociationFilter: React.FC<any> = (props) => {
export const BlockProvider = (props) => { export const BlockProvider = (props) => {
const { collection, association } = props; const { collection, association } = props;
const resource = useResource(props); const resource = useResource(props);
const params = { ...props.params }; const params = useMemo(() => ({ ...props.params }), [props.params]);
const { appends, updateAssociationValues } = useAssociationNames(); const { appends, updateAssociationValues } = useAssociationNames();
if (!Object.keys(params).includes('appends')) { if (!Object.keys(params).includes('appends')) {
params['appends'] = appends; params['appends'] = appends;

View File

@ -1,8 +1,11 @@
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { useField } from '@formily/react'; import { useField } from '@formily/react';
import _ from 'lodash';
import React, { createContext, useContext, useEffect } from 'react'; import React, { createContext, useContext, useEffect } from 'react';
import { useRecord } from '../record-provider';
import { FixedBlockWrapper } from '../schema-component'; import { FixedBlockWrapper } from '../schema-component';
import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { BlockProvider, useBlockRequestContext } from './BlockProvider';
import { useParsedFilter } from './hooks';
export const CalendarBlockContext = createContext<any>({}); export const CalendarBlockContext = createContext<any>({});
@ -10,6 +13,18 @@ const InternalCalendarBlockProvider = (props) => {
const { fieldNames, showLunar } = props; const { fieldNames, showLunar } = props;
const field = useField(); const field = useField();
const { resource, service } = useBlockRequestContext(); const { resource, service } = useBlockRequestContext();
const record = useRecord();
const { filter } = useParsedFilter({
filterOption: service?.params?.[0]?.filter,
currentRecord: { __parent: record, __collectionName: props.collection },
});
useEffect(() => {
if (!_.isEmpty(filter)) {
service?.run({ ...service?.params?.[0], filter });
}
}, [JSON.stringify(filter)]);
// if (service.loading) { // if (service.loading) {
// return <Spin />; // return <Spin />;
// } // }

View File

@ -1,9 +1,11 @@
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { useField } from '@formily/react'; import { useField } from '@formily/react';
import { Spin } from 'antd'; import { Spin } from 'antd';
import _ from 'lodash';
import React, { createContext, useContext, useEffect, useMemo } from 'react'; import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { RecordProvider } from '../record-provider'; import { RecordProvider, useRecord } from '../record-provider';
import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { BlockProvider, useBlockRequestContext } from './BlockProvider';
import { useParsedFilter } from './hooks';
export const DetailsBlockContext = createContext<any>({}); export const DetailsBlockContext = createContext<any>({});
@ -18,21 +20,36 @@ const InternalDetailsBlockProvider = (props) => {
[], [],
); );
const { resource, service } = useBlockRequestContext(); const { resource, service } = useBlockRequestContext();
if (service.loading && !field.loaded) { const record = useRecord();
return <Spin />; const currentRecord = service?.data?.data?.[0] || {};
} const detailsBLockValue = useMemo(() => {
field.loaded = true; return {
return (
<DetailsBlockContext.Provider
value={{
action, action,
form, form,
field, field,
service, service,
resource, resource,
}} };
> }, [action, field, form, resource, service]);
<RecordProvider record={service?.data?.data?.[0] || {}}>{props.children}</RecordProvider>
const { filter } = useParsedFilter({
filterOption: service?.params?.[0]?.filter,
currentRecord: { ...currentRecord, __parent: record, __collectionName: props.collection },
});
useEffect(() => {
if (!_.isEmpty(filter)) {
service?.run({ ...service?.params?.[0], filter });
}
}, [JSON.stringify(filter)]);
if (service.loading && !field.loaded) {
return <Spin />;
}
field.loaded = true;
return (
<DetailsBlockContext.Provider value={detailsBLockValue}>
<RecordProvider record={currentRecord}>{props.children}</RecordProvider>
</DetailsBlockContext.Provider> </DetailsBlockContext.Provider>
); );
}; };
@ -53,9 +70,12 @@ export const useDetailsBlockProps = () => {
const ctx = useDetailsBlockContext(); const ctx = useDetailsBlockContext();
useEffect(() => { useEffect(() => {
if (!ctx.service.loading) { if (!ctx.service.loading) {
ctx.form.reset().then(() => { ctx.form
.reset()
.then(() => {
ctx.form.setValues(ctx.service?.data?.data?.[0] || {}); ctx.form.setValues(ctx.service?.data?.data?.[0] || {});
}); })
.catch(console.error);
} }
}, [ctx.service.loading]); }, [ctx.service.loading]);
return { return {

View File

@ -1,7 +1,7 @@
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
import { Spin } from 'antd'; import { Spin } from 'antd';
import { isEmpty } from 'lodash'; import _, { isEmpty } from 'lodash';
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
import { useCollection } from '../collection-manager'; import { useCollection } from '../collection-manager';
import { RecordProvider, useRecord } from '../record-provider'; import { RecordProvider, useRecord } from '../record-provider';
@ -13,7 +13,7 @@ import { FormActiveFieldsProvider } from './hooks';
export const FormBlockContext = createContext<any>({}); export const FormBlockContext = createContext<any>({});
const InternalFormBlockProvider = (props) => { const InternalFormBlockProvider = (props) => {
const { action, readPretty, params } = props; const { action, readPretty, params, association } = props;
const field = useField(); const field = useField();
const form = useMemo( const form = useMemo(
() => () =>
@ -25,13 +25,8 @@ const InternalFormBlockProvider = (props) => {
const { resource, service, updateAssociationValues } = useBlockRequestContext(); const { resource, service, updateAssociationValues } = useBlockRequestContext();
const formBlockRef = useRef(); const formBlockRef = useRef();
const record = useRecord(); const record = useRecord();
if (service.loading && Object.keys(form?.initialValues)?.length === 0 && action) { const formBlockValue = useMemo(() => {
return <Spin />; return {
}
return (
<FormBlockContext.Provider
value={{
params, params,
action, action,
form, form,
@ -42,21 +37,39 @@ const InternalFormBlockProvider = (props) => {
resource, resource,
updateAssociationValues, updateAssociationValues,
formBlockRef, formBlockRef,
}} };
> }, [action, field, form, params, resource, service, updateAssociationValues]);
{readPretty ? (
<RecordProvider parent={isEmpty(record?.__parent) ? record : record?.__parent} record={service?.data?.data}> if (service.loading && Object.keys(form?.initialValues)?.length === 0 && action) {
return <Spin />;
}
let content = (
<div ref={formBlockRef}> <div ref={formBlockRef}>
<RenderChildrenWithDataTemplates form={form} /> <RenderChildrenWithDataTemplates form={form} />
</div> </div>
</RecordProvider>
) : (
<div ref={formBlockRef}>
<RenderChildrenWithDataTemplates form={form} />
</div>
)}
</FormBlockContext.Provider>
); );
if (readPretty) {
content = (
<RecordProvider parent={isEmpty(record?.__parent) ? record : record?.__parent} record={service?.data?.data}>
{content}
</RecordProvider>
);
} else if (
formBlockValue.type === 'create' &&
// 点击关系表格区块的 Add new 按钮,在弹窗中新增的表单区块,是不需要重置 record 的。在这里用 record 是否为空来判断
!_.isEmpty(_.omit(record, ['__parent', '__collectionName'])) &&
// association 不为空,说明是关系区块
association
) {
content = (
<RecordProvider parent={record} record={{}}>
{content}
</RecordProvider>
);
}
return <FormBlockContext.Provider value={formBlockValue}>{content}</FormBlockContext.Provider>;
}; };
/** /**

View File

@ -3,10 +3,11 @@ import { FormContext, useField, useFieldSchema } from '@formily/react';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useCollectionManager } from '../collection-manager'; import { useCollectionManager } from '../collection-manager';
import { useFilterBlock } from '../filter-provider/FilterProvider'; import { useFilterBlock } from '../filter-provider/FilterProvider';
import { useRecord } from '../record-provider';
import { FixedBlockWrapper, SchemaComponentOptions, removeNullCondition } from '../schema-component'; import { FixedBlockWrapper, SchemaComponentOptions, removeNullCondition } from '../schema-component';
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider'; import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
import { mergeFilter } from './SharedFilterProvider'; import { mergeFilter } from './SharedFilterProvider';
import { findFilterTargets } from './hooks'; import { findFilterTargets, useParsedFilter } from './hooks';
export const TableBlockContext = createContext<any>({}); export const TableBlockContext = createContext<any>({});
export function getIdsWithChildren(nodes) { export function getIdsWithChildren(nodes) {
@ -74,9 +75,11 @@ const InternalTableBlockProvider = (props: Props) => {
export const TableBlockProvider = (props) => { export const TableBlockProvider = (props) => {
const resourceName = props.resource; const resourceName = props.resource;
const params = { ...props.params }; const params = useMemo(() => ({ ...props.params }), [props.params]);
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { getCollection, getCollectionField } = useCollectionManager(); const { getCollection, getCollectionField } = useCollectionManager();
const record = useRecord();
const collection = getCollection(props.collection); const collection = getCollection(props.collection);
const { treeTable } = fieldSchema?.['x-decorator-props'] || {}; const { treeTable } = fieldSchema?.['x-decorator-props'] || {};
if (props.dragSort) { if (props.dragSort) {
@ -99,12 +102,22 @@ export const TableBlockProvider = (props) => {
} }
} }
const form = useMemo(() => createForm(), [treeTable]); const form = useMemo(() => createForm(), [treeTable]);
const { filter: parsedFilter } = useParsedFilter({
filterOption: params?.filter,
currentRecord: { __parent: record, __collectionName: props.collection },
});
const paramsWithFilter = useMemo(() => {
return {
...params,
filter: parsedFilter,
};
}, [parsedFilter, params]);
return ( return (
<SchemaComponentOptions scope={{ treeTable }}> <SchemaComponentOptions scope={{ treeTable }}>
<FormContext.Provider value={form}> <FormContext.Provider value={form}>
<BlockProvider data-testid="table-block" {...props} params={params} runWhenParamsChanged> <BlockProvider data-testid="table-block" {...props} params={paramsWithFilter} runWhenParamsChanged>
<InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={params} /> <InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={paramsWithFilter} />
</BlockProvider> </BlockProvider>
</FormContext.Provider> </FormContext.Provider>
</SchemaComponentOptions> </SchemaComponentOptions>

View File

@ -26,6 +26,7 @@ import { mergeFilter } from '../SharedFilterProvider';
import { TableFieldResource } from '../TableFieldProvider'; import { TableFieldResource } from '../TableFieldProvider';
export * from './useFormActiveFields'; export * from './useFormActiveFields';
export * from './useParsedFilter';
export const usePickActionProps = () => { export const usePickActionProps = () => {
const form = useForm(); const form = useForm();
@ -587,8 +588,9 @@ export const useCustomizeBulkUpdateActionProps = () => {
const actionField = useField(); const actionField = useField();
const { modal } = App.useApp(); const { modal } = App.useApp();
const variables = useVariables(); const variables = useVariables();
const localVariables = useLocalVariables(); const record = useRecord();
const { name, getField } = useCollection(); const { name, getField } = useCollection();
const localVariables = useLocalVariables({ currentRecord: { __parent: record, __collectionName: name } });
return { return {
async onClick() { async onClick() {

View File

@ -0,0 +1,45 @@
import { reaction } from '@formily/reactive';
import { flatten } from '@nocobase/utils/client';
import _ from 'lodash';
import { useEffect, useState } from 'react';
import { useParseDataScopeFilter } from '../../schema-settings';
import { DEBOUNCE_WAIT } from '../../variables';
import { getPath } from '../../variables/utils/getPath';
import { isVariable } from '../../variables/utils/isVariable';
import { useFormBlockContext } from '../FormBlockProvider';
export function useParsedFilter({ filterOption, currentRecord }: { filterOption: any; currentRecord?: any }) {
const { parseFilter } = useParseDataScopeFilter({ currentRecord });
const [filter, setFilter] = useState({});
const { form } = useFormBlockContext();
useEffect(() => {
if (!filterOption) return;
const _run = async () => {
const result = await parseFilter(filterOption);
setFilter(result);
};
_run();
const run = _.debounce(_run, DEBOUNCE_WAIT);
reaction(() => {
// 这一步主要是为了使 reaction 能够收集到依赖
const flat = flatten(filterOption, {
breakOn({ key }) {
return key.startsWith('$') && key !== '$and' && key !== '$or';
},
transformValue(value) {
if (!isVariable(value)) {
return value;
}
const result = _.get({ $nForm: form?.values }, getPath(value));
return result;
},
});
return flat;
}, run);
}, [JSON.stringify(filterOption)]);
return { filter };
}

View File

@ -79,7 +79,10 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema
export const useValuesFromRecord = (options) => { export const useValuesFromRecord = (options) => {
const record = useRecord(); const record = useRecord();
const result = useRequest( const result = useRequest(
() => Promise.resolve({ data: { ...omit(record, ['__parent']), category: record?.category.map((v) => v.id) } }), () =>
Promise.resolve({
data: { ...omit(record, ['__parent', '__collectionName']), category: record?.category.map((v) => v.id) },
}),
{ {
...options, ...options,
manual: true, manual: true,

View File

@ -23,7 +23,7 @@ export const useCancelAction = () => {
export const useValuesFromRecord = (options) => { export const useValuesFromRecord = (options) => {
const record = useRecord(); const record = useRecord();
const result = useRequest(() => Promise.resolve({ data: omit(record, ['__parent']) }), { const result = useRequest(() => Promise.resolve({ data: omit(record, ['__parent', '__collectionName']) }), {
...options, ...options,
manual: true, manual: true,
}); });

View File

@ -61,6 +61,11 @@ export const FilterBlockProvider: React.FC = ({ children }) => {
return <FilterContext.Provider value={{ dataBlocks, setDataBlocks }}>{children}</FilterContext.Provider>; return <FilterContext.Provider value={{ dataBlocks, setDataBlocks }}>{children}</FilterContext.Provider>;
}; };
/**
*
* @param param0
* @returns
*/
export const FilterBlockRecord = ({ export const FilterBlockRecord = ({
children, children,
params, params,

View File

@ -619,6 +619,7 @@ export default {
"Dynamic value": "Dynamic value", "Dynamic value": "Dynamic value",
"Current user": "Current user", "Current user": "Current user",
"Current record": "Current record", "Current record": "Current record",
"Current parent record": "Current parent record",
"Current time": "Current time", "Current time": "Current time",
"System variables": "System variables", "System variables": "System variables",
"Date variables": "Date variables", "Date variables": "Date variables",

View File

@ -603,6 +603,7 @@ export default {
"Dynamic value": "Valor dinámico", "Dynamic value": "Valor dinámico",
"Current user": "Usuario actual", "Current user": "Usuario actual",
"Current record": "Registro actual", "Current record": "Registro actual",
"Current parent record": "Registro padre actual",
"Current time": "Hora actual", "Current time": "Hora actual",
"System variables": "Variables del sistema", "System variables": "Variables del sistema",
"Date variables": "Variables de fecha", "Date variables": "Variables de fecha",

View File

@ -600,6 +600,7 @@ export default {
"Dynamic value": "Valeur dynamique", "Dynamic value": "Valeur dynamique",
"Current user": "Utilisateur actuel", "Current user": "Utilisateur actuel",
"Current record": "Enregistrement actuel", "Current record": "Enregistrement actuel",
"Current parent record": "Enregistrement parent actuel",
"Current time": "Heure actuelle", "Current time": "Heure actuelle",
"System variables": "Variables système", "System variables": "Variables système",
"Date variables": "Variables de date", "Date variables": "Variables de date",

View File

@ -502,6 +502,7 @@ export default {
"Dynamic value": "動的値", "Dynamic value": "動的値",
"Current user": "現在のユーザー", "Current user": "現在のユーザー",
"Current record": "現在のレコード", "Current record": "現在のレコード",
"Current parent record": "現在の親レコード",
"Popup close method": "ポップアップを閉じる方法", "Popup close method": "ポップアップを閉じる方法",
"Automatic close": "自動で閉じる", "Automatic close": "自動で閉じる",
"Manually close": "手動で閉じる", "Manually close": "手動で閉じる",

View File

@ -508,6 +508,7 @@ export default {
"Single select and radio fields can be used as the grouping field": "Campos de seleção única e de rádio podem ser usados como o campo de agrupamento", "Single select and radio fields can be used as the grouping field": "Campos de seleção única e de rádio podem ser usados como o campo de agrupamento",
"Tab name": "Nome da aba", "Tab name": "Nome da aba",
"Current record blocks": "Blocos de registro atual", "Current record blocks": "Blocos de registro atual",
"Current parent record": "Registro pai atual",
"Popup message": "Mensagem pop-up", "Popup message": "Mensagem pop-up",
"Delete role": "Excluir função", "Delete role": "Excluir função",
"Role display name": "Nome de exibição da função", "Role display name": "Nome de exibição da função",

View File

@ -439,6 +439,7 @@ export default {
"Dynamic value": "Динамическое значение", "Dynamic value": "Динамическое значение",
"Current user": "Текущий пользователь", "Current user": "Текущий пользователь",
"Current record": "Текущая запись", "Current record": "Текущая запись",
"Current parent record": "Текущая родительская запись",
"Popup close method": "Метод закрытия всплывающего окна", "Popup close method": "Метод закрытия всплывающего окна",
"Automatic close": "Автоматическое закрытие", "Automatic close": "Автоматическое закрытие",
"Manually close": "Ручное закрытие", "Manually close": "Ручное закрытие",

View File

@ -438,6 +438,7 @@ export default {
"Dynamic value": "Dinamik değer", "Dynamic value": "Dinamik değer",
"Current user": "Seçili kullanıcı", "Current user": "Seçili kullanıcı",
"Current record": "Seçili kayıt", "Current record": "Seçili kayıt",
"Current parent record": "Seçili üst kayıt",
"Popup close method": "Açılır pencere kapatma metodu", "Popup close method": "Açılır pencere kapatma metodu",
"Automatic close": "Otomatik kapat", "Automatic close": "Otomatik kapat",
"Manually close": "Manuel kapat", "Manually close": "Manuel kapat",

View File

@ -620,6 +620,7 @@ export default {
"Dynamic value": "Динамічне значення", "Dynamic value": "Динамічне значення",
"Current user": "Поточний користувач", "Current user": "Поточний користувач",
"Current record": "Поточний запис", "Current record": "Поточний запис",
"Current parent record": "Поточний батьківський запис",
"Current time": "Поточний час", "Current time": "Поточний час",
"System variables": "Системні змінні", "System variables": "Системні змінні",
"Date variables": "Змінні дати", "Date variables": "Змінні дати",

View File

@ -661,6 +661,7 @@ export default {
'Dynamic value': '动态值', 'Dynamic value': '动态值',
'Current user': '当前用户', 'Current user': '当前用户',
'Current record': '当前记录', 'Current record': '当前记录',
"Current parent record": "当前父记录",
'Current time': '当前时间', 'Current time': '当前时间',
Now: '现在', Now: '现在',
'Popup close method': '弹窗关闭方式', 'Popup close method': '弹窗关闭方式',

View File

@ -1,14 +1,17 @@
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import { useCollection } from '../collection-manager';
import { useCurrentUserContext } from '../user'; import { useCurrentUserContext } from '../user';
export const RecordContext = createContext({}); export const RecordContext = createContext({});
export const RecordIndexContext = createContext(null); export const RecordIndexContext = createContext(null);
export const RecordProvider: React.FC<{ record: any; parent?: any }> = (props) => { export const RecordProvider: React.FC<{ record: any; parent?: any; collectionName?: string }> = (props) => {
const { record, children, parent = false } = props; const { record, children, collectionName, parent = false } = props;
const { name: __collectionName } = useCollection();
const __parent = useContext(RecordContext); const __parent = useContext(RecordContext);
const value = { ...record }; const value = { ...record };
value['__parent'] = parent ? parent : __parent; value['__parent'] = parent ? parent : __parent;
value['__collectionName'] = collectionName || __collectionName;
return <RecordContext.Provider value={value}>{children}</RecordContext.Provider>; return <RecordContext.Provider value={value}>{children}</RecordContext.Provider>;
}; };

View File

@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
import { useActionContext } from '../..'; import { useActionContext } from '../..';
import { useDesignable } from '../../'; import { useDesignable } from '../../';
import { Icon } from '../../../icon'; import { Icon } from '../../../icon';
import { useRecord } from '../../../record-provider'; import { RecordProvider, useRecord } from '../../../record-provider';
import { useLocalVariables, useVariables } from '../../../variables'; import { useLocalVariables, useVariables } from '../../../variables';
import { SortableItem } from '../../common'; import { SortableItem } from '../../common';
import { useCompile, useComponent, useDesigner } from '../../hooks'; import { useCompile, useComponent, useDesigner } from '../../hooks';
@ -52,7 +52,7 @@ export const Action: ComposedAction = observer(
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const compile = useCompile(); const compile = useCompile();
const form = useForm(); const form = useForm();
const values = useRecord(); const record = useRecord();
const designerProps = fieldSchema['x-designer-props']; const designerProps = fieldSchema['x-designer-props'];
const openMode = fieldSchema?.['x-component-props']?.['openMode']; const openMode = fieldSchema?.['x-component-props']?.['openMode'];
const disabled = form.disabled || field.disabled || field.data?.disabled || props.disabled; const disabled = form.disabled || field.disabled || field.data?.disabled || props.disabled;
@ -61,7 +61,12 @@ export const Action: ComposedAction = observer(
const tarComponent = useComponent(component) || component; const tarComponent = useComponent(component) || component;
const { modal } = App.useApp(); const { modal } = App.useApp();
const variables = useVariables(); const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: { values } as any }); const localVariables = useLocalVariables({ currentForm: { values: record } as any });
// fix https://nocobase.height.app/T-2259
const shouldResetRecord = ['create', 'customize:bulkUpdate', 'customize:bulkEdit', 'customize:create'].includes(
fieldSchema['x-action'],
);
let actionTitle = title || compile(fieldSchema.title); let actionTitle = title || compile(fieldSchema.title);
actionTitle = lodash.isString(actionTitle) ? t(actionTitle) : actionTitle; actionTitle = lodash.isString(actionTitle) ? t(actionTitle) : actionTitle;
@ -75,13 +80,13 @@ export const Action: ComposedAction = observer(
operator: h.operator, operator: h.operator,
field, field,
condition: v.condition, condition: v.condition,
values, values: record,
variables, variables,
localVariables, localVariables,
}); });
}); });
}); });
}, [JSON.stringify(linkageRules), values, designable, field]); }, [JSON.stringify(linkageRules), record, designable, field]);
const handleButtonClick = useCallback( const handleButtonClick = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
@ -142,7 +147,7 @@ export const Action: ComposedAction = observer(
); );
}; };
return wrapSSR( const result = (
<ActionContextProvider <ActionContextProvider
button={renderButton()} button={renderButton()}
visible={visible} visible={visible}
@ -158,7 +163,17 @@ export const Action: ComposedAction = observer(
{!popover && renderButton()} {!popover && renderButton()}
{!popover && props.children} {!popover && props.children}
{element} {element}
</ActionContextProvider>, </ActionContextProvider>
);
return wrapSSR(
shouldResetRecord ? (
<RecordProvider parent={record} record={{}}>
{result}
</RecordProvider>
) : (
result
),
); );
}, },
{ displayName: 'Action' }, { displayName: 'Action' },

View File

@ -15,7 +15,6 @@ import { DEBOUNCE_WAIT } from '../../../variables';
import { getPath } from '../../../variables/utils/getPath'; import { getPath } from '../../../variables/utils/getPath';
import { isVariable } from '../../../variables/utils/isVariable'; import { isVariable } from '../../../variables/utils/isVariable';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../hooks';
import { removeNullCondition } from '../filter';
import { AssociationFieldContext } from './context'; import { AssociationFieldContext } from './context';
export const useInsertSchema = (component) => { export const useInsertSchema = (component) => {

View File

@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { FixedBlockDesignerItem, removeNullCondition, useDesignable } from '../..'; import { FixedBlockDesignerItem, removeNullCondition, useDesignable } from '../..';
import { useCalendarBlockContext, useFormBlockContext } from '../../../block-provider'; import { useCalendarBlockContext, useFormBlockContext } from '../../../block-provider';
import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useCollection, useCollectionManager } from '../../../collection-manager';
import { RecordProvider, useRecord } from '../../../record-provider';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
@ -17,10 +18,12 @@ export const CalendarDesigner = () => {
const { dn } = useDesignable(); const { dn } = useDesignable();
const { t } = useTranslation(); const { t } = useTranslation();
const template = useSchemaTemplate(); const template = useSchemaTemplate();
const record = useRecord();
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {}; const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
return ( return (
<RecordProvider parent={record} record={{}}>
<GeneralSchemaDesigner template={template} title={title || name}> <GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettings.BlockTitleItem /> <SchemaSettings.BlockTitleItem />
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
@ -131,5 +134,6 @@ export const CalendarDesigner = () => {
}} }}
/> />
</GeneralSchemaDesigner> </GeneralSchemaDesigner>
</RecordProvider>
); );
}; };

View File

@ -735,7 +735,7 @@ FormItem.FilterFormDesigner = FilterFormDesigner;
function useIsAddNewForm() { function useIsAddNewForm() {
const record = useRecord(); const record = useRecord();
const isAddNewForm = _.isEmpty(_.omit(record, '__parent')); const isAddNewForm = _.isEmpty(_.omit(record, ['__parent', '__collectionName']));
return isAddNewForm; return isAddNewForm;
} }

View File

@ -39,6 +39,7 @@ const useLazyLoadAssociationFieldOfForm = () => {
const cloneRecord = { ...record }; const cloneRecord = { ...record };
delete cloneRecord['__parent']; delete cloneRecord['__parent'];
delete cloneRecord['__collectionName'];
if (_.isEmpty(cloneRecord) || !variables || record[schemaName] != null) { if (_.isEmpty(cloneRecord) || !variables || record[schemaName] != null) {
return; return;

View File

@ -2,7 +2,8 @@ import { FormLayout } from '@formily/antd-v5';
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { FormContext, useField } from '@formily/react'; import { FormContext, useField } from '@formily/react';
import React, { createContext, useContext, useEffect, useMemo } from 'react'; import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { BlockProvider, useBlockRequestContext } from '../../../block-provider'; import { BlockProvider, useBlockRequestContext, useParsedFilter } from '../../../block-provider';
import { useRecord } from '../../../record-provider';
import useStyles from './GridCard.Decorator.style'; import useStyles from './GridCard.Decorator.style';
export const GridCardBlockContext = createContext<any>({}); export const GridCardBlockContext = createContext<any>({});
@ -41,8 +42,22 @@ const InternalGridCardBlockProvider = (props) => {
}; };
export const GridCardBlockProvider = (props) => { export const GridCardBlockProvider = (props) => {
const { params } = props;
const record = useRecord();
const { filter: parsedFilter } = useParsedFilter({
filterOption: params?.filter,
currentRecord: { __parent: record, __collectionName: props.collection },
});
const paramsWithFilter = useMemo(() => {
return {
...params,
filter: parsedFilter,
};
}, [parsedFilter, params]);
return ( return (
<BlockProvider data-testid="grid-card-block" {...props}> <BlockProvider data-testid="grid-card-block" {...props} params={paramsWithFilter}>
<InternalGridCardBlockProvider {...props} /> <InternalGridCardBlockProvider {...props} />
</BlockProvider> </BlockProvider>
); );

View File

@ -6,6 +6,7 @@ import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useFormBlockContext } from '../../../block-provider'; import { useFormBlockContext } from '../../../block-provider';
import { useCollection, useSortFields } from '../../../collection-manager'; import { useCollection, useSortFields } from '../../../collection-manager';
import { RecordProvider, useRecord } from '../../../record-provider';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
import { SchemaComponentOptions } from '../../core'; import { SchemaComponentOptions } from '../../core';
@ -27,6 +28,7 @@ export const GridCardDesigner = () => {
const field = useField(); const field = useField();
const { dn } = useDesignable(); const { dn } = useDesignable();
const sortFields = useSortFields(name); const sortFields = useSortFields(name);
const record = useRecord();
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
const columnCount = field.decoratorProps.columnCount || defaultColumnCount; const columnCount = field.decoratorProps.columnCount || defaultColumnCount;
@ -70,6 +72,8 @@ export const GridCardDesigner = () => {
}; };
}); });
return ( return (
// fix https://nocobase.height.app/T-2259
<RecordProvider parent={record} record={{}}>
<GeneralSchemaDesigner template={template} title={title || name}> <GeneralSchemaDesigner template={template} title={title || name}>
<SchemaComponentOptions components={{ Slider }}> <SchemaComponentOptions components={{ Slider }}>
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
@ -224,5 +228,6 @@ export const GridCardDesigner = () => {
/> />
</SchemaComponentOptions> </SchemaComponentOptions>
</GeneralSchemaDesigner> </GeneralSchemaDesigner>
</RecordProvider>
); );
}; };

View File

@ -3,7 +3,8 @@ import { FormLayout } from '@formily/antd-v5';
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { FormContext, useField } from '@formily/react'; import { FormContext, useField } from '@formily/react';
import React, { createContext, useContext, useEffect, useMemo } from 'react'; import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { BlockProvider, useBlockRequestContext } from '../../../block-provider'; import { BlockProvider, useBlockRequestContext, useParsedFilter } from '../../../block-provider';
import { useRecord } from '../../../record-provider';
export const ListBlockContext = createContext<any>({}); export const ListBlockContext = createContext<any>({});
@ -51,8 +52,22 @@ const InternalListBlockProvider = (props) => {
}; };
export const ListBlockProvider = (props) => { export const ListBlockProvider = (props) => {
const { params } = props;
const record = useRecord();
const { filter: parsedFilter } = useParsedFilter({
filterOption: params?.filter,
currentRecord: { __parent: record, __collectionName: props.collection },
});
const paramsWithFilter = useMemo(() => {
return {
...params,
filter: parsedFilter,
};
}, [parsedFilter, params]);
return ( return (
<BlockProvider data-testid="list-block" {...props}> <BlockProvider data-testid="list-block" {...props} params={paramsWithFilter}>
<InternalListBlockProvider {...props} /> <InternalListBlockProvider {...props} />
</BlockProvider> </BlockProvider>
); );

View File

@ -5,6 +5,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useFormBlockContext } from '../../../block-provider'; import { useFormBlockContext } from '../../../block-provider';
import { useCollection, useSortFields } from '../../../collection-manager'; import { useCollection, useSortFields } from '../../../collection-manager';
import { RecordProvider, useRecord } from '../../../record-provider';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../hooks';
@ -19,6 +20,7 @@ export const ListDesigner = () => {
const field = useField(); const field = useField();
const { dn } = useDesignable(); const { dn } = useDesignable();
const sortFields = useSortFields(name); const sortFields = useSortFields(name);
const record = useRecord();
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
const sort = defaultSort?.map((item: string) => { const sort = defaultSort?.map((item: string) => {
@ -33,6 +35,8 @@ export const ListDesigner = () => {
}; };
}); });
return ( return (
// fix https://nocobase.height.app/T-2259
<RecordProvider parent={record} record={{}}>
<GeneralSchemaDesigner template={template} title={title || name}> <GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettings.BlockTitleItem /> <SchemaSettings.BlockTitleItem />
<SchemaSettings.DataScope <SchemaSettings.DataScope
@ -170,5 +174,6 @@ export const ListDesigner = () => {
}} }}
/> />
</GeneralSchemaDesigner> </GeneralSchemaDesigner>
</RecordProvider>
); );
}; };

View File

@ -7,6 +7,7 @@ import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useCollection, useCollectionManager } from '../../../collection-manager';
import { useSortFields } from '../../../collection-manager/action-hooks'; import { useSortFields } from '../../../collection-manager/action-hooks';
import { FilterBlockType } from '../../../filter-provider/utils'; import { FilterBlockType } from '../../../filter-provider/utils';
import { RecordProvider, useRecord } from '../../../record-provider';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../hooks';
@ -23,6 +24,8 @@ export const TableBlockDesigner = () => {
const { service } = useTableBlockContext(); const { service } = useTableBlockContext();
const { t } = useTranslation(); const { t } = useTranslation();
const { dn } = useDesignable(); const { dn } = useDesignable();
const record = useRecord();
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
const supportTemplate = !fieldSchema?.['x-decorator-props']?.disableTemplate; const supportTemplate = !fieldSchema?.['x-decorator-props']?.disableTemplate;
@ -64,6 +67,8 @@ export const TableBlockDesigner = () => {
[dn, field.decoratorProps, fieldSchema, service], [dn, field.decoratorProps, fieldSchema, service],
); );
return ( return (
// fix https://nocobase.height.app/T-2259
<RecordProvider parent={record} record={{}}>
<GeneralSchemaDesigner template={template} title={title || name}> <GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettings.BlockTitleItem /> <SchemaSettings.BlockTitleItem />
{collection?.tree && collectionField?.collectionName === collectionField?.target && ( {collection?.tree && collectionField?.collectionName === collectionField?.target && (
@ -241,5 +246,6 @@ export const TableBlockDesigner = () => {
}} }}
/> />
</GeneralSchemaDesigner> </GeneralSchemaDesigner>
</RecordProvider>
); );
}; };

View File

@ -0,0 +1,29 @@
import { Schema } from '@formily/json-schema';
import { useTranslation } from 'react-i18next';
import { CollectionFieldOptions } from '../../../collection-manager';
import { useBaseVariable } from './useBaseVariable';
interface Props {
collectionField: CollectionFieldOptions;
schema: any;
collectionName: string;
noDisabled?: boolean;
/** 消费变量值的字段 */
targetFieldSchema?: Schema;
}
export const useParentRecordVariable = (props: Props) => {
const { t } = useTranslation();
const currentRecordVariable = useBaseVariable({
collectionField: props.collectionField,
uiSchema: props.schema,
name: '$nParentRecord',
title: t('Current parent record'),
collectionName: props.collectionName,
noDisabled: props.noDisabled,
targetFieldSchema: props.targetFieldSchema,
});
return currentRecordVariable;
};

View File

@ -1,13 +1,14 @@
import { Form } from '@formily/core'; import { Form } from '@formily/core';
import { ISchema, Schema } from '@formily/react'; import { ISchema, Schema } from '@formily/react';
import _ from 'lodash';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useFormBlockType } from '../../../block-provider/FormBlockProvider';
import { CollectionFieldOptions, useCollection } from '../../../collection-manager'; import { CollectionFieldOptions, useCollection } from '../../../collection-manager';
import { useFlag } from '../../../flag-provider'; import { useFlag } from '../../../flag-provider';
import { useBlockCollection } from './useBlockCollection'; import { useBlockCollection } from './useBlockCollection';
import { useDateVariable } from './useDateVariable'; import { useDateVariable } from './useDateVariable';
import { useFormVariable } from './useFormVariable'; import { useFormVariable } from './useFormVariable';
import { useIterationVariable } from './useIterationVariable'; import { useIterationVariable } from './useIterationVariable';
import { useParentRecordVariable } from './useParentRecordVariable';
import { useRecordVariable } from './useRecordVariable'; import { useRecordVariable } from './useRecordVariable';
import { useUserVariable } from './useUserVariable'; import { useUserVariable } from './useUserVariable';
@ -44,11 +45,12 @@ export const useVariableOptions = ({
operator, operator,
noDisabled, noDisabled,
targetFieldSchema, targetFieldSchema,
record,
}: Props) => { }: Props) => {
const { name: blockCollectionName } = useBlockCollection(); const { name: blockCollectionName = record?.__collectionName } = useBlockCollection();
const { isInSubForm, isInSubTable } = useFlag() || {}; const { isInSubForm, isInSubTable } = useFlag() || {};
const { type: formBlockType } = useFormBlockType();
const { name } = useCollection(); const { name } = useCollection();
const blockParentCollectionName = record?.__parent?.__collectionName;
const userVariable = useUserVariable({ const userVariable = useUserVariable({
maxDepth: 3, maxDepth: 3,
uiSchema: uiSchema, uiSchema: uiSchema,
@ -78,6 +80,13 @@ export const useVariableOptions = ({
noDisabled, noDisabled,
targetFieldSchema, targetFieldSchema,
}); });
const currentParentRecordVariable = useParentRecordVariable({
schema: uiSchema,
collectionName: blockParentCollectionName,
collectionField,
noDisabled,
targetFieldSchema,
});
return useMemo(() => { return useMemo(() => {
return [ return [
@ -85,7 +94,10 @@ export const useVariableOptions = ({
dateVariable, dateVariable,
form && !form.readPretty && formVariable, form && !form.readPretty && formVariable,
(isInSubForm || isInSubTable) && iterationVariable, (isInSubForm || isInSubTable) && iterationVariable,
formBlockType === 'update' && currentRecordVariable, blockCollectionName && !_.isEmpty(_.omit(record, ['__parent', '__collectionName'])) && currentRecordVariable,
blockParentCollectionName &&
!_.isEmpty(_.omit(record?.__parent, ['__parent', '__collectionName'])) &&
currentParentRecordVariable,
].filter(Boolean); ].filter(Boolean);
}, [ }, [
userVariable, userVariable,
@ -95,6 +107,10 @@ export const useVariableOptions = ({
isInSubForm, isInSubForm,
isInSubTable, isInSubTable,
iterationVariable, iterationVariable,
blockCollectionName,
record,
currentRecordVariable, currentRecordVariable,
blockParentCollectionName,
currentParentRecordVariable,
]); ]);
}; };

View File

@ -9,14 +9,15 @@ interface Props {
* ['$user', '$date'] * ['$user', '$date']
* *
*/ */
exclude: string[]; exclude?: string[];
currentRecord?: any;
} }
// TODO: 建议变量名统一命名为 `$n` 开头,以防止与 formily 内置变量冲突 // TODO: 建议变量名统一命名为 `$n` 开头,以防止与 formily 内置变量冲突
const defaultExclude = ['$user', '$date', '$nDate']; const defaultExclude = ['$user', '$date', '$nDate'];
const useParseDataScopeFilter = ({ exclude }: Props = { exclude: defaultExclude }) => { const useParseDataScopeFilter = ({ exclude = defaultExclude, currentRecord }: Props = {}) => {
const localVariables = useLocalVariables(); const localVariables = useLocalVariables({ currentRecord });
const variables = useVariables(); const variables = useVariables();
const parseFilter = useCallback( const parseFilter = useCallback(

View File

@ -1,5 +1,4 @@
import { Form } from '@formily/core'; import { Form } from '@formily/core';
import _ from 'lodash';
import React, { useContext, useMemo } from 'react'; import React, { useContext, useMemo } from 'react';
import { useFormBlockContext } from '../../block-provider'; import { useFormBlockContext } from '../../block-provider';
import { useCollection } from '../../collection-manager'; import { useCollection } from '../../collection-manager';
@ -42,12 +41,6 @@ const useLocalVariables = (props?: Props) => {
name = props.collectionName; name = props.collectionName;
} }
let blockRecord = currentRecord;
// 获取到最顶层的 record即当前区块所在的 record
while (!_.isEmpty(blockRecord.__parent)) {
blockRecord = blockRecord.__parent;
}
return useMemo(() => { return useMemo(() => {
return ( return (
[ [
@ -57,7 +50,7 @@ const useLocalVariables = (props?: Props) => {
*/ */
{ {
name: 'currentRecord', name: 'currentRecord',
ctx: blockRecord, ctx: currentRecord,
collectionName: name, collectionName: name,
}, },
/** /**
@ -66,7 +59,7 @@ const useLocalVariables = (props?: Props) => {
*/ */
{ {
name, name,
ctx: form?.values || blockRecord, ctx: form?.values || currentRecord,
collectionName: name, collectionName: name,
}, },
/** /**
@ -80,8 +73,13 @@ const useLocalVariables = (props?: Props) => {
}, },
{ {
name: '$nRecord', name: '$nRecord',
ctx: blockRecord, ctx: currentRecord,
collectionName: name, collectionName: currentRecord?.__collectionName,
},
{
name: '$nParentRecord',
ctx: currentRecord?.__parent,
collectionName: currentRecord?.__parent?.__collectionName,
}, },
{ {
name: '$nForm', name: '$nForm',
@ -91,7 +89,7 @@ const useLocalVariables = (props?: Props) => {
iterationCtx && { name: '$iteration', ctx: iterationCtx, collectionName: currentCollectionName }, iterationCtx && { name: '$iteration', ctx: iterationCtx, collectionName: currentCollectionName },
] as VariableOption[] ] as VariableOption[]
).filter(Boolean); ).filter(Boolean);
}, [blockRecord, name, form?.values, iterationCtx, currentCollectionName]); // 尽量保持返回的值不变,这样可以减少接口的请求次数,因为关系字段会缓存到变量的 ctx 中 }, [currentRecord, name, form?.values, iterationCtx, currentCollectionName]); // 尽量保持返回的值不变,这样可以减少接口的请求次数,因为关系字段会缓存到变量的 ctx 中
}; };
export default useLocalVariables; export default useLocalVariables;

View File

@ -148,7 +148,6 @@ export function useTriggerWorkflowsActionProps() {
return { return {
async onClick() { async onClick() {
const fieldNames = fields.map((field) => field.name);
const { const {
assignedValues: originalAssignedValues = {}, assignedValues: originalAssignedValues = {},
onSuccess, onSuccess,