diff --git a/packages/core/client/src/variables/VariablesProvider.tsx b/packages/core/client/src/variables/VariablesProvider.tsx index 2c13c69de5..85c14a4a18 100644 --- a/packages/core/client/src/variables/VariablesProvider.tsx +++ b/packages/core/client/src/variables/VariablesProvider.tsx @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { raw, untracked } from '@formily/reactive'; +import { untracked } from '@formily/reactive'; import { getValuesByPath } from '@nocobase/utils/client'; import _ from 'lodash'; import React, { createContext, useCallback, useEffect, useMemo, useRef } from 'react'; @@ -18,6 +18,7 @@ import { getDataSourceHeaders } from '../data-source/utils'; import { useCompile } from '../schema-component'; import useBuiltInVariables from './hooks/useBuiltinVariables'; import { VariableOption, VariablesContextType } from './types'; +import { cacheLazyLoadedValues, getCachedLazyLoadedValues } from './utils/cacheLazyLoadedValues'; import { filterEmptyValues } from './utils/filterEmptyValues'; import { getAction } from './utils/getAction'; import { getPath } from './utils/getPath'; @@ -68,18 +69,18 @@ const VariablesProvider = ({ children, filterVariables }: any) => { }, []); /** - * 1. 从 `ctx` 中根据 `path` 取值 - * 2. 如果某个 `key` 不存在,且 `key` 是一个关联字段,则从 api 中获取数据,并缓存到 `ctx` 中 - * 3. 如果某个 `key` 不存在,且 `key` 不是一个关联字段,则返回当前值 + * 1. Get value from `ctx` based on `path` + * 2. If a `key` does not exist and is an association field, fetch data from api and cache it in `ctx` + * 3. If a `key` does not exist and is not an association field, return the current value */ const getResult = useCallback( async ( variablePath: string, localVariables?: VariableOption[], options?: { - /** 第一次请求时,需要包含的关系字段 */ + /** Related fields that need to be included in the first request */ appends?: string[]; - /** do not request when the association field is empty */ + /** Do not request when the association field is empty */ doNotRequest?: boolean; /** * The operator related to the current field, provided when parsing the default value of the field @@ -114,12 +115,17 @@ const VariablesProvider = ({ children, filterVariables }: any) => { } const key = list[index]; - const { fieldPath } = getFieldPath(list.slice(0, index + 1).join('.'), _variableToCollectionName); + const currentVariablePath = list.slice(0, index + 1).join('.'); + const { fieldPath } = getFieldPath(currentVariablePath, _variableToCollectionName); const associationField: CollectionFieldOptions_deprecated = getCollectionJoinField(fieldPath, dataSource); const collectionPrimaryKey = getCollection(collectionName, dataSource)?.getPrimaryKey(); if (Array.isArray(current)) { const result = current.map((item) => { - if (!options?.doNotRequest && shouldToRequest(item?.[key]) && item?.[collectionPrimaryKey] != null) { + if ( + !options?.doNotRequest && + shouldToRequest(item?.[key], item, currentVariablePath) && + item?.[collectionPrimaryKey] != null + ) { if (associationField?.target) { const url = `/${collectionName}/${ item[associationField.sourceKey || collectionPrimaryKey] @@ -137,19 +143,20 @@ const VariablesProvider = ({ children, filterVariables }: any) => { }) .then((data) => { clearRequested(url); - item[key] = data.data.data; - return item[key]; + const value = data.data.data; + cacheLazyLoadedValues(item, currentVariablePath, value); + return value; }); stashRequested(url, result); return result; } } - return item?.[key]; + return getCachedLazyLoadedValues(item, currentVariablePath) || item?.[key]; }); current = removeThroughCollectionFields(_.flatten(await Promise.all(result)), associationField); } else if ( !options?.doNotRequest && - shouldToRequest(current[key]) && + shouldToRequest(current[key], current, currentVariablePath) && current[collectionPrimaryKey] != null && associationField?.target ) { @@ -172,15 +179,18 @@ const VariablesProvider = ({ children, filterVariables }: any) => { clearRequested(url); } - // fix https://nocobase.height.app/T-3144,使用 `raw` 方法是为了避免触发 autorun,以修复 T-3144 的错误 - if (!raw(current)[key]) { - // 把接口返回的数据保存起来,避免重复请求 - raw(current)[key] = data.data.data; + const value = data.data.data; + if (!getCachedLazyLoadedValues(current, currentVariablePath)) { + // Cache the API response data to avoid repeated requests + cacheLazyLoadedValues(current, currentVariablePath, value); } - current = removeThroughCollectionFields(getValuesByPath(current, key), associationField); + current = removeThroughCollectionFields(value, associationField); } else { - current = removeThroughCollectionFields(getValuesByPath(current, key), associationField); + current = removeThroughCollectionFields( + getCachedLazyLoadedValues(current, currentVariablePath) || getValuesByPath(current, key), + associationField, + ); } if (associationField?.target) { @@ -201,7 +211,7 @@ const VariablesProvider = ({ children, filterVariables }: any) => { ); /** - * 注册一个全局变量 + * Register a global variable */ const registerVariable = useCallback( (variableOption: VariableOption) => { @@ -247,18 +257,18 @@ const VariablesProvider = ({ children, filterVariables }: any) => { const parseVariable = useCallback( /** - * 将变量字符串解析为真正的值 - * @param str 变量字符串 - * @param localVariables 局部变量,解析完成后会被清除 + * Parse the variable string to the actual value + * @param str Variable string + * @param localVariables Local variables, will be cleared after parsing * @returns */ async ( str: string, localVariables?: VariableOption | VariableOption[], options?: { - /** 第一次请求时,需要包含的关系字段 */ + /** Related fields that need to be included in the first request */ appends?: string[]; - /** do not request when the association field is empty */ + /** Do not request when the association field is empty */ doNotRequest?: boolean; /** * The operator related to the current field, provided when parsing the default value of the field @@ -303,7 +313,7 @@ const VariablesProvider = ({ children, filterVariables }: any) => { const { fieldPath, dataSource } = getFieldPath(path, _variableToCollectionName); let result = getCollectionJoinField(fieldPath, dataSource); - // 当仅有一个例如 `$user` 这样的字符串时,需要拼一个假的 `collectionField` 返回 + // When there is only a string like `$user`, a fake `collectionField` needs to be returned if (!result && !path.includes('.')) { result = { target: _variableToCollectionName[path]?.collectionName, @@ -346,13 +356,17 @@ VariablesProvider.displayName = 'VariablesProvider'; export default VariablesProvider; -function shouldToRequest(value) { +function shouldToRequest(value, variableCtx: Record, variablePath: string) { let result = false; - // value 有可能是一个响应式对象,使用 untracked 可以避免意外触发 autorun + if (getCachedLazyLoadedValues(variableCtx, variablePath)) { + return false; + } + + // value may be a reactive object, using untracked to avoid unexpected autorun untracked(() => { // fix https://nocobase.height.app/T-2502 - // 兼容 `对多` 和 `对一` 子表单子表格字段的情况 + // Compatible with `xxx to many` and `xxx to one` subform fields and subtable fields if (JSON.stringify(value) === '[{}]' || JSON.stringify(value) === '{}') { result = true; return; @@ -391,8 +405,9 @@ function mergeVariableToCollectionNameWithLocalVariables( } /** - * 去除关系字段中的中间表字段。 - * 如果在创建新记录的时候,存在关系字段的中间表字段,提交的时候会报错,所以需要去除。 + * Remove `through collection fields` from association fields. + * If `through collection fields` exist in association fields when creating new records, + * it will cause errors during submission, so they need to be removed. * @param value * @param associationField * @returns diff --git a/packages/core/client/src/variables/__tests__/useVariables.test.tsx b/packages/core/client/src/variables/__tests__/useVariables.test.tsx index 3af65963e6..b6852af739 100644 --- a/packages/core/client/src/variables/__tests__/useVariables.test.tsx +++ b/packages/core/client/src/variables/__tests__/useVariables.test.tsx @@ -344,6 +344,14 @@ describe('useVariables', () => { name: '$user.belongsToField', }); }); + + await waitFor(async () => { + // After lazy loading the association field value, the original $user variable value should not contain the association field value + expect(await result.current.parseVariable('{{ $user }}').then(({ value }) => value)).toEqual({ + id: 0, + nickname: 'from request', + }); + }); }); it('set doNotRequest to true to ensure the result is empty', async () => { diff --git a/packages/core/client/src/variables/utils/cacheLazyLoadedValues.ts b/packages/core/client/src/variables/utils/cacheLazyLoadedValues.ts new file mode 100644 index 0000000000..9d07c2959d --- /dev/null +++ b/packages/core/client/src/variables/utils/cacheLazyLoadedValues.ts @@ -0,0 +1,25 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +const cache = new Map, any>(); + +export const cacheLazyLoadedValues = (variableCtx: Record, variablePath: string, value: any) => { + const cachedValue = cache.get(variableCtx); + + if (cachedValue) { + cachedValue[variablePath] = value; + } else { + cache.set(variableCtx, { [variablePath]: value }); + } +}; + +export const getCachedLazyLoadedValues = (variableCtx: Record, variablePath: string) => { + const cachedValue = cache.get(variableCtx); + return cachedValue?.[variablePath]; +};