feat(Variable): add a new variable (#4025)

* feat: add DeclareVariable

* feat: add a new variable

* test: add e2e

* fix: current form variable

* chore: rename '弹窗记录' to 'Current popup record'

* fix(Details): fix data scope error

* fix(Calendar): fix data scope

* fix: varaible's data souce

* fix: data souce
This commit is contained in:
Zeke Zhang 2024-04-15 14:50:38 +08:00 committed by GitHub
parent 31d68f91e8
commit 0274e65cec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 495 additions and 131 deletions

View File

@ -1,17 +1,16 @@
import { createForm } from '@formily/core';
import { useField } from '@formily/react';
import { useField, useFieldSchema } from '@formily/react';
import { Spin } from 'antd';
import _ from 'lodash';
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
import { withDynamicSchemaProps } from '../application/hoc/withDynamicSchemaProps';
import { useCollectionParentRecord } from '../data-source/collection-record/CollectionRecordProvider';
import { RecordProvider } from '../record-provider';
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
import { TemplateBlockProvider } from './TemplateBlockProvider';
import { useParsedFilter } from './hooks';
import { useCollectionManager_deprecated } from '../collection-manager';
import { useCollectionRecordData } from '../data-source';
import { useCollectionParentRecord } from '../data-source/collection-record/CollectionRecordProvider';
import { useDetailsWithPaginationBlockParams } from '../modules/blocks/data-blocks/details-multi/hooks/useDetailsWithPaginationBlockParams';
import { RecordProvider } from '../record-provider';
import { useDesignable } from '../schema-component';
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
import { TemplateBlockProvider } from './TemplateBlockProvider';
/**
* @internal
@ -44,15 +43,6 @@ const InternalDetailsBlockProvider = (props) => {
};
}, [action, field, form, resource, service]);
const { filter } = useParsedFilter({
filterOption: service?.params?.[0]?.filter,
});
useEffect(() => {
if (!_.isEmpty(filter) && !service.loading) {
service?.run({ ...service?.params?.[0], filter });
}
}, [JSON.stringify(filter)]);
if (service.loading && !field.loaded) {
return <Spin />;
}
@ -69,7 +59,31 @@ const InternalDetailsBlockProvider = (props) => {
);
};
/**
* @internal
* schema
* @param props
* @returns
*/
const useCompatDetailsBlockParams = (props) => {
const fieldSchema = useFieldSchema();
let params;
// 1. 新版本的 schema 存在 x-use-decorator-props 属性
if (fieldSchema['x-use-decorator-props']) {
params = props?.params;
} else {
// 2. 旧版本的 schema 不存在 x-use-decorator-props 属性
// 因为 schema 中是否存在 x-use-decorator-props 是固定不变的,所以这里可以使用 hooks
// eslint-disable-next-line react-hooks/rules-of-hooks
params = useDetailsWithPaginationBlockParams(props);
}
return params;
};
export const DetailsBlockProvider = withDynamicSchemaProps((props) => {
const params = useCompatDetailsBlockParams(props);
const record = useCollectionRecordData();
const { association, dataSource } = props;
const { getCollection } = useCollectionManager_deprecated(dataSource);
@ -87,7 +101,7 @@ export const DetailsBlockProvider = withDynamicSchemaProps((props) => {
return (
<TemplateBlockProvider>
<BlockProvider name="details" {...props}>
<BlockProvider name="details" {...props} params={params}>
<InternalDetailsBlockProvider {...props} />
</BlockProvider>
</TemplateBlockProvider>

View File

@ -4,7 +4,12 @@ import { Spin } from 'antd';
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
import { withDynamicSchemaProps } from '../application/hoc/withDynamicSchemaProps';
import { useCollection_deprecated } from '../collection-manager';
import { CollectionRecord, useCollectionParentRecordData, useCollectionRecord } from '../data-source';
import {
CollectionRecord,
useCollectionManager,
useCollectionParentRecordData,
useCollectionRecord,
} from '../data-source';
import { useTreeParentRecord } from '../modules/blocks/data-blocks/table/TreeRecordProvider';
import { RecordProvider, useRecord } from '../record-provider';
import { useActionContext, useDesignable } from '../schema-component';
@ -30,8 +35,9 @@ export const FormBlockContext = createContext<{
FormBlockContext.displayName = 'FormBlockContext';
const InternalFormBlockProvider = (props) => {
const cm = useCollectionManager();
const ctx = useFormBlockContext();
const { action, readPretty, params, collection } = props;
const { action, readPretty, params, collection, association } = props;
const field = useField();
const form = useMemo(
() =>
@ -56,10 +62,23 @@ const InternalFormBlockProvider = (props) => {
resource,
updateAssociationValues,
formBlockRef,
collectionName: collection,
collectionName: collection || cm.getCollectionField(association)?.target,
formRecord: record,
};
}, [action, collection, ctx, field, form, params, record, resource, service, updateAssociationValues]);
}, [
action,
association,
cm,
collection,
ctx,
field,
form,
params,
record,
resource,
service,
updateAssociationValues,
]);
if (service.loading && Object.keys(form?.initialValues)?.length === 0 && action) {
return <Spin />;

View File

@ -1,9 +1,10 @@
// import { CascaderProps } from 'antd';
import _ from 'lodash';
import { useMemo } from 'react';
import { CollectionFieldOptions } from './collection';
import { DEFAULT_DATA_SOURCE_KEY, DataSourceManager } from './data-source/DataSourceManager';
const HEADERS_DATA_SOURCE_KEY = 'x-data-source';
// 等把老的去掉后,再把这个函数的实现从那边移动过来
// export function getCollectionFieldsOptions(){}
@ -14,10 +15,17 @@ export const isTitleField = (dm: DataSourceManager, field: CollectionFieldOption
export const useDataSourceHeaders = (dataSource?: string) => {
const headers = useMemo(() => {
if (dataSource && dataSource !== DEFAULT_DATA_SOURCE_KEY) {
return { 'x-data-source': dataSource };
return { [HEADERS_DATA_SOURCE_KEY]: dataSource };
// return { 'x-connection': dataSource };
}
}, [dataSource]);
return headers;
};
export const getDataSourceHeaders = (dataSource?: string) => {
if (dataSource && dataSource !== DEFAULT_DATA_SOURCE_KEY) {
return { [HEADERS_DATA_SOURCE_KEY]: dataSource };
}
return {};
};

View File

@ -628,6 +628,7 @@
"Current user": "Current user",
"Current role": "Current role",
"Current record": "Current record",
"Current popup record": "Current popup record",
"Associated records": "Associated records",
"Parent record": "Parent record",
"Current time": "Current time",

View File

@ -617,6 +617,7 @@
"Current user": "Usuario actual",
"Current role": "Rol actual",
"Current record": "Registro actual",
"Current popup record": "Registro actual del popup",
"Associated records": "Registros asociados",
"Parent record": "Registro padre",
"Current time": "Hora actual",

View File

@ -614,6 +614,7 @@
"Current user": "Utilisateur actuel",
"Current role": "Rôle actuel",
"Current record": "Enregistrement actuel",
"Current popup record": "Enregistrement popup actuel",
"Associated records": "Enregistrements associés",
"Parent record": "Enregistrement parent",
"Current time": "Heure actuelle",

View File

@ -517,6 +517,7 @@
"Current user": "現在のユーザー",
"Current role": "現在の役割",
"Current record": "現在のレコード",
"Current popup record": "現在のポップアップレコード",
"Associated records": "関連付けられたレコード",
"Popup close method": "ポップアップを閉じる方法",
"Automatic close": "自動で閉じる",

View File

@ -642,6 +642,7 @@
"Current user": "현재 사용자",
"Current role": "현재 역할",
"Current record": "현재 레코드",
"Current popup record": "현재 팝업 레코드",
"Associated records": "관련 레코드",
"Parent record": "상위 레코드",
"Current time": "현재 시간",

View File

@ -452,6 +452,7 @@
"Current user": "Текущий пользователь",
"Current role": "Текущая роль",
"Current record": "Текущая запись",
"Current popup record": "Текущая запись всплывающего окна",
"Associated records": "Связанные записи",
"Parent record": "Родительская запись",
"Popup close method": "Метод закрытия всплывающего окна",

View File

@ -452,6 +452,7 @@
"Current user": "Seçili kullanıcı",
"Current role": "Seçili rol",
"Current record": "Seçili kayıt",
"Current popup record": "Açılır pencere kaydı",
"Associated records": "İlişkili kayıtlar",
"Parent record": "Üst kayıt",
"Popup close method": "Açılır pencere kapatma metodu",

View File

@ -634,6 +634,7 @@
"Current user": "Поточний користувач",
"Current role": "Поточна роль",
"Current record": "Поточний запис",
"Current popup record": "Поточний запис спливаючого вікна",
"Associated records": "Пов'язані записи",
"Parent record": "Батьківський запис",
"Current time": "Поточний час",

View File

@ -646,6 +646,7 @@
"Current user": "当前用户",
"Current role": "当前角色",
"Current record": "当前记录",
"Current popup record": "当前弹窗记录",
"Associated records": "关联记录",
"Parent record": "上级记录",
"Current time": "当前时间",

View File

@ -642,6 +642,7 @@
"Current user": "當前使用者",
"Current role": "當前角色",
"Current record": "當前記錄",
"Current popup record": "當前彈窗記錄",
"Associated records": "關聯記錄",
"Parent record": "上級記錄",
"Current time": "當前時間",

View File

@ -0,0 +1,14 @@
import { useMemo } from 'react';
import { useParsedFilter } from '../../../../../block-provider/hooks/useParsedFilter';
export const useDetailsWithPaginationBlockParams = (props) => {
const { params } = props;
const { filter } = useParsedFilter({
filterOption: params?.filter,
});
return useMemo(() => {
return { ...params, filter };
}, [JSON.stringify(filter)]);
};

View File

@ -1,8 +1,11 @@
import { useParentRecordCommon } from '../../../useParentRecordCommon';
import { useDetailsWithPaginationBlockParams } from './useDetailsWithPaginationBlockParams';
export function useDetailsWithPaginationDecoratorProps(props) {
let parentRecord;
const params = useDetailsWithPaginationBlockParams(props);
// association 的值是固定不变的,所以可以在条件中使用 hooks
if (props.association) {
// eslint-disable-next-line react-hooks/rules-of-hooks
@ -11,5 +14,6 @@ export function useDetailsWithPaginationDecoratorProps(props) {
return {
parentRecord,
params,
};
}

View File

@ -9,6 +9,7 @@ import {
oneTableBlockWithAddNewAndViewAndEditAndBasicFields,
test,
} from '@nocobase/test/e2e';
import { oneEmptyTableWithUsers } from '../../../details-multi/__e2e__/templatesOfBug';
import { T2165, T2174, T3251, T3806, T3815, T3871 } from './templatesOfBug';
const clickOption = async (page: Page, optionName: string) => {
@ -782,6 +783,93 @@ test.describe('creation form block schema settings', () => {
// 2. 设置的 abcd 应该立即显示在 Nickname 字段的输入框中
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('abcd');
});
test('Current popup record', async ({ page, mockPage }) => {
await mockPage(oneEmptyTableWithUsers).goto();
// 1. 表单字段默认值中使用 `Current popup record`
await page.getByLabel('action-Action.Link-View').click();
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'form Form (Add new) right' }).hover();
await page.getByRole('menuitem', { name: 'Other records right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
await page.mouse.move(300, 0);
await page.getByLabel('schema-initializer-Grid-form:').hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click();
await page.getByLabel('block-item-CollectionField-').hover();
await page.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-users-users.').hover();
await page.getByRole('menuitem', { name: 'Set default value' }).click();
await page.getByLabel('variable-button').click();
await page.getByRole('menuitemcheckbox', { name: 'Current popup record right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin');
// 2. 表单联动规则中使用 `Current popup record`
// 创建 Username 字段
await page.getByLabel('schema-initializer-Grid-form:').hover();
await page.getByRole('menuitem', { name: 'Username' }).click();
// 设置联动规则
await page.getByLabel('block-item-CardItem-users-form').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:createForm-users').hover();
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
await page.mouse.move(300, 0);
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add property').click();
await page.getByTestId('select-linkage-property-field').click();
await page.getByTitle('Username').click();
await page.getByTestId('select-linkage-action-field').click();
await page.getByRole('option', { name: 'Value', exact: true }).click();
await page.getByTestId('select-linkage-value-type').click();
await page.getByTitle('Expression').click();
await page.getByLabel('variable-button').click();
await page.getByRole('menuitemcheckbox', { name: 'Current popup record right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Username' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
// 需正确显示变量的值
await expect(
page.getByLabel('block-item-CollectionField-users-form-users.username-Username').getByRole('textbox'),
).toHaveValue('nocobase');
// 3. Table 数据选择器中使用 `Current popup record`
// 创建 Table 区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
await page.getByRole('menuitem', { name: 'Other records right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
await page.mouse.move(300, 0);
// 显示 Nickname 字段
await page
.getByTestId('drawer-Action.Container-users-View record')
.getByLabel('schema-initializer-TableV2-')
.hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click();
await page.mouse.move(300, 0);
// 设置数据范围(使用 `Current popup record` 变量)
await page
.getByTestId('drawer-Action.Container-users-View record')
.getByLabel('block-item-CardItem-users-table')
.hover();
await page
.getByTestId('drawer-Action.Container-users-View record')
.getByLabel('designer-schema-settings-CardItem-blockSettings:table-users')
.hover();
await page.getByRole('menuitem', { name: 'Set the data scope' }).click();
await page.getByText('Add condition', { exact: true }).click();
await page.getByTestId('select-filter-field').click();
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
await page.getByLabel('variable-button').click();
await page.getByRole('menuitemcheckbox', { name: 'Current popup record right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
// 数据需显示正确
await expect(
page
.getByTestId('drawer-Action.Container-users-View record')
.getByLabel('block-item-CardItem-users-table')
.getByRole('button', { name: 'Super Admin' }),
).toBeVisible();
});
});
test('save block template & using block template', async ({ page, mockPage }) => {

View File

@ -0,0 +1,27 @@
import React, { FC, createContext } from 'react';
import { Collection } from '../../data-source';
interface DeclareVariableProps {
/* 变量名称 */
name: string;
/** 变量值 */
value: any;
/** 显示给用户的名字 */
title?: string;
/** 变量对应的数据表信息 */
collection?: Collection;
}
export const DeclareVariableContext = createContext<DeclareVariableProps>(null);
/**
*
*/
export const DeclareVariable: FC<DeclareVariableProps> = (props) => {
const { name, value, title, collection } = props;
return (
<DeclareVariableContext.Provider value={{ name, value, title, collection }}>
{props.children}
</DeclareVariableContext.Provider>
);
};

View File

@ -0,0 +1,53 @@
import { renderHook } from '@nocobase/test/client';
import React from 'react';
import { Collection } from '../../../data-source';
import { DeclareVariable } from '../DeclareVariable';
import { useVariable } from '../useVariable';
describe('useVariable', () => {
it('should return the variable value, title, and collection when the variable name matches', () => {
const variableName = 'testVariable';
const value = 'testValue';
const title = 'Test Variable';
const collection: Collection = new Collection(
{
name: 'testCollection',
title: 'Test Collection',
},
null,
);
const wrapper = ({ children }: { children: React.ReactNode }) => (
<DeclareVariable name={variableName} value={value} title={title} collection={collection}>
{children}
</DeclareVariable>
);
const { result } = renderHook(() => useVariable(variableName), { wrapper });
expect(result.current).toEqual({ value, title, collection });
});
it('should return an empty object when the variable name does not match', () => {
const variableName = 'testVariable';
const value = 'testValue';
const title = 'Test Variable';
const collection = new Collection(
{
name: 'testCollection',
title: 'Test Collection',
},
null,
);
const wrapper = ({ children }: { children: React.ReactNode }) => (
<DeclareVariable name={variableName} value={value} title={title} collection={collection}>
{children}
</DeclareVariable>
);
const { result } = renderHook(() => useVariable('otherVariable'), { wrapper });
expect(result.current).toEqual({});
});
});

View File

@ -0,0 +1,15 @@
import { useContext } from 'react';
import { DeclareVariableContext } from './DeclareVariable';
/**
*
* @param variableName
* @returns
*/
export const useVariable = (variableName: string) => {
const { name, value, title, collection } = useContext(DeclareVariableContext) || {};
if (name === variableName) {
return { value, title, collection };
}
return {};
};

View File

@ -1,9 +1,12 @@
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { toArr } from '@formily/shared';
import React, { Fragment, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDesignable } from '../../';
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
import { CollectionProvider_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
import { Collection } from '../../../data-source';
import { DeclareVariable } from '../../../modules/variable/DeclareVariable';
import { RecordProvider, useRecord } from '../../../record-provider';
import { FormProvider } from '../../core';
import { useCompile } from '../../hooks';
@ -48,6 +51,7 @@ export const ReadPrettyInternalViewer: React.FC = observer(
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
const getLabelUiSchema = useLabelUiSchemaV2();
const [btnHover, setBtnHover] = useState(false);
const { t } = useTranslation();
const renderRecords = () =>
toArr(props.value).map((record, index, arr) => {
@ -108,6 +112,12 @@ export const ReadPrettyInternalViewer: React.FC = observer(
return btnElement;
}
const renderWithoutTableFieldResourceProvider = () => (
<DeclareVariable
name="$nPopupRecord"
title={t('Current popup record')}
value={record}
collection={targetCollection as Collection}
>
<WithoutTableFieldResource.Provider value={true}>
<FormProvider>
<RecursionField
@ -120,6 +130,7 @@ export const ReadPrettyInternalViewer: React.FC = observer(
/>
</FormProvider>
</WithoutTableFieldResource.Provider>
</DeclareVariable>
);
const renderRecordProvider = () => {

View File

@ -28,6 +28,7 @@ import {
import { useACLFieldWhitelist } from '../../../acl/ACLProvider';
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
import { isNewRecord } from '../../../data-source/collection-record/isNewRecord';
import { DeclareVariable } from '../../../modules/variable/DeclareVariable';
import { useToken } from '../__builtins__';
import { SubFormProvider } from '../association-field/hooks';
import { ColumnFieldProvider } from './components/ColumnFieldProvider';
@ -56,6 +57,7 @@ export const useColumnsDeepMemoized = (columns: any[]) => {
};
const useTableColumns = (props: { showDel?: boolean; isSubTable?: boolean }) => {
const { t } = useTranslation();
const { token } = useToken();
const field = useArrayField(props);
const schema = useFieldSchema();
@ -106,6 +108,12 @@ const useTableColumns = (props: { showDel?: boolean; isSubTable?: boolean }) =>
const index = field.value?.indexOf(record);
const basePath = field.address.concat(record.__index || index);
return (
<DeclareVariable
name="$nPopupRecord"
title={t('Current popup record')}
value={record}
collection={collection}
>
<SubFormProvider value={{ value: record, collection }}>
<RecordIndexProvider index={record.__index || index}>
<RecordProvider isNew={isNewRecord(record)} record={record} parent={parentRecordData}>
@ -117,6 +125,7 @@ const useTableColumns = (props: { showDel?: boolean; isSubTable?: boolean }) =>
</RecordProvider>
</RecordIndexProvider>
</SubFormProvider>
</DeclareVariable>
);
},
} as TableColumnProps<any>;

View File

@ -94,6 +94,8 @@ import {
} from '../filter-provider/utils';
import { FlagProvider } from '../flag-provider';
import { useCollectMenuItem, useCollectMenuItems, useMenuItem } from '../hooks/useMenuItem';
import { DeclareVariable } from '../modules/variable/DeclareVariable';
import { useVariable } from '../modules/variable/useVariable';
import { SubFormProvider, useSubFormValue } from '../schema-component/antd/association-field/hooks';
import { getTargetKey } from '../schema-component/antd/association-filter/utilts';
import { useSchemaTemplateManager } from '../schema-templates';
@ -975,6 +977,9 @@ export const SchemaSettingsModalItem: FC<SchemaSettingsModalItemProps> = (props)
// 解决变量`当前对象`值在弹窗中丢失的问题
const { formValue: subFormValue, collection: subFormCollection } = useSubFormValue();
// 解决变量`$nPopupRecord`值在弹窗中丢失的问题
const popupRecordVariable = useVariable('$nPopupRecord');
if (hidden) {
return null;
}
@ -989,6 +994,12 @@ export const SchemaSettingsModalItem: FC<SchemaSettingsModalItemProps> = (props)
{ title: schema.title || title, width },
() => {
return (
<DeclareVariable
name="$nPopupRecord"
title={popupRecordVariable.title}
value={popupRecordVariable.value}
collection={popupRecordVariable.collection}
>
<CollectionRecordProvider record={record}>
<FormBlockContext.Provider value={formCtx}>
<SubFormProvider value={{ value: subFormValue, collection: subFormCollection }}>
@ -1034,6 +1045,7 @@ export const SchemaSettingsModalItem: FC<SchemaSettingsModalItemProps> = (props)
</SubFormProvider>
</FormBlockContext.Provider>
</CollectionRecordProvider>
</DeclareVariable>
);
},
theme,

View File

@ -1,8 +1,8 @@
import { Schema } from '@formily/json-schema';
import { useTranslation } from 'react-i18next';
import { CollectionFieldOptions_deprecated } from '../../../collection-manager';
import { useParentCollection } from '../../../data-source/collection/AssociationProvider';
import { useCollectionRecord } from '../../../data-source/collection-record/CollectionRecordProvider';
import { useParentCollection } from '../../../data-source/collection/AssociationProvider';
import { useFlag } from '../../../flag-provider/hooks/useFlag';
import { useBaseVariable } from './useBaseVariable';
@ -47,7 +47,7 @@ export const useParentRecordVariable = (props: Props) => {
export const useCurrentParentRecordVariable = (props: Props = {}) => {
const { t } = useTranslation();
const record = useCollectionRecord();
const { name: parentCollectionName } = useParentCollection() || {};
const { name: parentCollectionName, dataSource } = useParentCollection() || {};
const { isInSubForm, isInSubTable } = useFlag() || {};
const currentParentRecordSettings = useBaseVariable({
@ -58,6 +58,7 @@ export const useCurrentParentRecordVariable = (props: Props = {}) => {
collectionName: parentCollectionName,
noDisabled: props.noDisabled,
targetFieldSchema: props.targetFieldSchema,
dataSource,
});
return {
@ -65,5 +66,6 @@ export const useCurrentParentRecordVariable = (props: Props = {}) => {
currentParentRecordCtx: record?.parentRecord?.data,
shouldDisplayCurrentParentRecord: !!record?.parentRecord?.data && !isInSubForm && !isInSubTable,
collectionName: parentCollectionName,
dataSource,
};
};

View File

@ -0,0 +1,33 @@
import { useVariable } from '../../../modules/variable/useVariable';
import { useBaseVariable } from './useBaseVariable';
/**
* `Current popup record`
* @param props
* @returns
*/
export const usePopupVariable = (props: any = {}) => {
const { value, title, collection } = useVariable('$nPopupRecord');
const settings = useBaseVariable({
collectionField: props.collectionField,
uiSchema: props.schema,
name: '$nPopupRecord',
title,
collectionName: collection?.name,
noDisabled: props.noDisabled,
targetFieldSchema: props.targetFieldSchema,
dataSource: collection?.dataSource,
});
return {
/** 变量配置 */
settings,
/** 变量值 */
popupRecordCtx: value,
/** 用于判断是否需要显示配置项 */
shouldDisplayPopupRecord: !!value,
/** 当前记录对应的 collection name */
collectionName: collection?.name,
dataSource: collection?.dataSource,
};
};

View File

@ -74,6 +74,7 @@ export const useCurrentUserVariable = ({
collectionName: 'users',
noDisabled,
targetFieldSchema,
dataSource: DEFAULT_DATA_SOURCE_KEY,
});
return {

View File

@ -1,12 +1,13 @@
import { Form } from '@formily/core';
import { ISchema, Schema } from '@formily/react';
import { useMemo } from 'react';
import { CollectionFieldOptions_deprecated, useCollection_deprecated } from '../../../collection-manager';
import { CollectionFieldOptions_deprecated } from '../../../collection-manager';
import { useBlockCollection } from './useBlockCollection';
import { useDatetimeVariable } from './useDateVariable';
import { useCurrentFormVariable } from './useFormVariable';
import { useCurrentObjectVariable } from './useIterationVariable';
import { useCurrentParentRecordVariable } from './useParentRecordVariable';
import { usePopupVariable } from './usePopupVariable';
import { useCurrentRecordVariable } from './useRecordVariable';
import { useCurrentRoleVariable } from './useRoleVariable';
import { useCurrentUserVariable } from './useUserVariable';
@ -81,6 +82,12 @@ export const useVariableOptions = ({
noDisabled,
targetFieldSchema,
});
const { settings: popupRecordSettings, shouldDisplayPopupRecord } = usePopupVariable({
schema: uiSchema,
collectionField,
noDisabled,
targetFieldSchema,
});
const { currentParentRecordSettings, shouldDisplayCurrentParentRecord } = useCurrentParentRecordVariable({
schema: uiSchema,
collectionName: blockParentCollectionName,
@ -98,6 +105,7 @@ export const useVariableOptions = ({
shouldDisplayCurrentObject && currentObjectSettings,
shouldDisplayCurrentRecord && currentRecordSettings,
shouldDisplayCurrentParentRecord && currentParentRecordSettings,
shouldDisplayPopupRecord && popupRecordSettings,
].filter(Boolean);
}, [
currentUserSettings,
@ -111,5 +119,7 @@ export const useVariableOptions = ({
currentRecordSettings,
shouldDisplayCurrentParentRecord,
currentParentRecordSettings,
shouldDisplayPopupRecord,
popupRecordSettings,
]);
};

View File

@ -5,6 +5,7 @@ import React, { createContext, useCallback, useEffect, useMemo, useRef } from 'r
import { useAPIClient } from '../api-client';
import type { CollectionFieldOptions_deprecated } from '../collection-manager';
import { useCollectionManager_deprecated } from '../collection-manager';
import { getDataSourceHeaders } from '../data-source/utils';
import { useCompile } from '../schema-component';
import useBuiltInVariables from './hooks/useBuiltinVariables';
import { VariableOption, VariablesContextType } from './types';
@ -18,17 +19,25 @@ import { uniq } from './utils/uniq';
export const VariablesContext = createContext<VariablesContextType>(null);
VariablesContext.displayName = 'VariablesContext';
const variableToCollectionName = {};
const variableToCollectionName: {
collectionName?: string;
dataSource?: string;
} = {};
const getFieldPath = (variablePath: string, variableToCollectionName: Record<string, any>) => {
let dataSource;
const list = variablePath.split('.');
const result = list.map((item) => {
if (variableToCollectionName[item]) {
return variableToCollectionName[item];
dataSource = variableToCollectionName[item].dataSource;
return variableToCollectionName[item].collectionName;
}
return item;
});
return result.join('.');
return {
fieldPath: result.join('.'),
dataSource,
};
};
const VariablesProvider = ({ children }) => {
@ -67,7 +76,8 @@ const VariablesProvider = ({ children }) => {
localVariables,
);
let current = mergeCtxWithLocalVariables(ctxRef.current, localVariables);
let collectionName = getFieldPath(variableName, _variableToCollectionName);
const { fieldPath, dataSource } = getFieldPath(variableName, _variableToCollectionName);
let collectionName = fieldPath;
if (!(variableName in current)) {
throw new Error(`VariablesProvider: ${variableName} is not found`);
@ -79,9 +89,8 @@ const VariablesProvider = ({ children }) => {
}
const key = list[index];
const associationField: CollectionFieldOptions_deprecated = getCollectionJoinField(
getFieldPath(list.slice(0, index + 1).join('.'), _variableToCollectionName),
);
const { fieldPath } = getFieldPath(list.slice(0, index + 1).join('.'), _variableToCollectionName);
const associationField: CollectionFieldOptions_deprecated = getCollectionJoinField(fieldPath, dataSource);
if (Array.isArray(current)) {
const result = current.map((item) => {
if (shouldToRequest(item?.[key]) && item?.id != null) {
@ -92,6 +101,7 @@ const VariablesProvider = ({ children }) => {
}
const result = api
.request({
headers: getDataSourceHeaders(dataSource),
url,
params: {
appends: options?.appends,
@ -116,6 +126,7 @@ const VariablesProvider = ({ children }) => {
data = await getRequested(url);
} else {
const waitForData = api.request({
headers: getDataSourceHeaders(dataSource),
url,
params: {
appends: options?.appends,
@ -163,7 +174,10 @@ const VariablesProvider = ({ children }) => {
};
});
if (variableOption.collectionName) {
variableToCollectionName[variableOption.name] = variableOption.collectionName;
variableToCollectionName[variableOption.name] = {
collectionName: variableOption.collectionName,
dataSource: variableOption.dataSource,
};
}
},
[setCtx],
@ -174,10 +188,13 @@ const VariablesProvider = ({ children }) => {
return null;
}
const { collectionName, dataSource } = variableToCollectionName[variableName] || {};
return {
name: variableName,
ctx: ctxRef.current[variableName],
collectionName: variableToCollectionName[variableName],
collectionName,
dataSource,
};
}, []);
@ -235,17 +252,16 @@ const VariablesProvider = ({ children }) => {
}
const path = getPath(variableString);
let result = getCollectionJoinField(
getFieldPath(
const { fieldPath, dataSource } = getFieldPath(
path,
mergeVariableToCollectionNameWithLocalVariables(variableToCollectionName, localVariables as VariableOption[]),
),
);
let result = getCollectionJoinField(fieldPath, dataSource);
// 当仅有一个例如 `$user` 这样的字符串时,需要拼一个假的 `collectionField` 返回
if (!result && !path.includes('.')) {
result = {
target: variableToCollectionName[path],
target: variableToCollectionName[path]?.collectionName,
};
}
@ -317,7 +333,10 @@ function mergeVariableToCollectionNameWithLocalVariables(
localVariables?.forEach((item) => {
if (item.collectionName) {
variableToCollectionName[item.name] = item.collectionName;
variableToCollectionName[item.name] = {
collectionName: item.collectionName,
dataSource: item.dataSource,
};
}
});

View File

@ -1,5 +1,6 @@
import { dayjs } from '@nocobase/utils/client';
import { useMemo } from 'react';
import { DEFAULT_DATA_SOURCE_KEY } from '../../data-source/data-source/DataSourceManager';
import { useCurrentUserVariable, useDatetimeVariable } from '../../schema-settings';
import { useCurrentRoleVariable } from '../../schema-settings/VariableInput/hooks/useRoleVariable';
import { VariableOption } from '../types';
@ -12,12 +13,13 @@ const useBuiltInVariables = () => {
return [
{
name: '$user',
ctx: currentUserCtx,
ctx: currentUserCtx as any,
collectionName: 'users',
dataSource: DEFAULT_DATA_SOURCE_KEY as string,
},
{
name: '$nRole',
ctx: currentRoleCtx,
ctx: currentRoleCtx as any,
collectionName: 'roles',
},
/**
@ -28,6 +30,7 @@ const useBuiltInVariables = () => {
name: 'currentUser',
ctx: currentUserCtx,
collectionName: 'users',
dataSource: DEFAULT_DATA_SOURCE_KEY as string,
},
{
name: '$nDate',
@ -60,7 +63,7 @@ const useBuiltInVariables = () => {
ctx: () => dayjs().toISOString(),
},
];
}, [currentUserCtx]);
}, [currentRoleCtx, currentUserCtx, datetimeCtx]);
return { builtinVariables };
};

View File

@ -6,6 +6,7 @@ import { useDatetimeVariable } from '../../schema-settings/VariableInput/hooks/u
import { useCurrentFormVariable } from '../../schema-settings/VariableInput/hooks/useFormVariable';
import { useCurrentObjectVariable } from '../../schema-settings/VariableInput/hooks/useIterationVariable';
import { useCurrentParentRecordVariable } from '../../schema-settings/VariableInput/hooks/useParentRecordVariable';
import { usePopupVariable } from '../../schema-settings/VariableInput/hooks/usePopupVariable';
import { useCurrentRecordVariable } from '../../schema-settings/VariableInput/hooks/useRecordVariable';
import { VariableOption } from '../types';
@ -17,7 +18,16 @@ interface Props {
const useLocalVariables = (props?: Props) => {
const { currentObjectCtx, shouldDisplayCurrentObject } = useCurrentObjectVariable();
const { currentRecordCtx, collectionName: collectionNameOfRecord } = useCurrentRecordVariable();
const { currentParentRecordCtx, collectionName: collectionNameOfParentRecord } = useCurrentParentRecordVariable();
const {
currentParentRecordCtx,
collectionName: collectionNameOfParentRecord,
dataSource: currentParentRecordDataSource,
} = useCurrentParentRecordVariable();
const {
popupRecordCtx,
collectionName: collectionNameOfPopupRecord,
dataSource: popupDataSource,
} = usePopupVariable();
const { datetimeCtx } = useDatetimeVariable();
const { currentFormCtx } = useCurrentFormVariable({ form: props?.currentForm });
const { name: currentCollectionName } = useCollection_deprecated();
@ -66,6 +76,13 @@ const useLocalVariables = (props?: Props) => {
name: '$nParentRecord',
ctx: currentParentRecordCtx,
collectionName: collectionNameOfParentRecord,
dataSource: currentParentRecordDataSource,
},
{
name: '$nPopupRecord',
ctx: popupRecordCtx,
collectionName: collectionNameOfPopupRecord,
dataSource: popupDataSource,
},
{
name: '$nForm',
@ -98,6 +115,10 @@ const useLocalVariables = (props?: Props) => {
currentFormCtx,
currentParentRecordCtx,
collectionNameOfParentRecord,
currentParentRecordDataSource,
popupRecordCtx,
collectionNameOfPopupRecord,
popupDataSource,
datetimeCtx,
shouldDisplayCurrentObject,
currentObjectCtx,

View File

@ -77,4 +77,6 @@ export interface VariableOption {
};
/** 变量所对应的数据表的名称 */
collectionName?: string;
/** 数据表所对应的数据源 */
dataSource?: string;
}

View File

@ -1,6 +1,10 @@
import { useParsedFilter } from '@nocobase/client';
import { useMemo } from 'react';
export function useCalendarBlockParams(props) {
const { filter } = useParsedFilter({
filterOption: props.params?.filter,
});
const appends = useMemo(() => {
const arr: string[] = [];
const start = props.fieldNames?.start;
@ -17,6 +21,6 @@ export function useCalendarBlockParams(props) {
}, [props.fieldNames]);
return useMemo(() => {
return { ...props.params, appends: [...appends, ...(props.params.appends || [])], paginate: false };
}, [appends, props.params]);
return { ...props.params, appends: [...appends, ...(props.params.appends || [])], paginate: false, filter };
}, [appends, JSON.stringify(filter), props.params]);
}

View File

@ -1,13 +1,6 @@
import { ArrayField } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react';
import {
BlockProvider,
FixedBlockWrapper,
useBlockRequestContext,
useParsedFilter,
withDynamicSchemaProps,
} from '@nocobase/client';
import _ from 'lodash';
import { BlockProvider, FixedBlockWrapper, useBlockRequestContext, withDynamicSchemaProps } from '@nocobase/client';
import React, { createContext, useContext, useEffect } from 'react';
import { useCalendarBlockParams } from '../hooks/useCalendarBlockParams';
@ -18,14 +11,6 @@ const InternalCalendarBlockProvider = (props) => {
const { fieldNames, showLunar } = props;
const field = useField();
const { resource, service } = useBlockRequestContext();
const { filter } = useParsedFilter({
filterOption: service?.params?.[0]?.filter,
});
useEffect(() => {
if (!_.isEmpty(filter)) {
service?.run({ ...service?.params?.[0], filter });
}
}, [JSON.stringify(filter)]);
return (
<FixedBlockWrapper>