mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-09 23:49:27 +08:00
fix(variable): lazy loaded data should not be saved in the original c… (#5540)
* fix(variable): lazy loaded data should not be saved in the original context object * fix: make unit test pass
This commit is contained in:
parent
0cea7eb0a7
commit
b680f3f9b4
@ -7,7 +7,7 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { raw, untracked } from '@formily/reactive';
|
import { untracked } from '@formily/reactive';
|
||||||
import { getValuesByPath } from '@nocobase/utils/client';
|
import { getValuesByPath } from '@nocobase/utils/client';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { createContext, useCallback, useEffect, useMemo, useRef } from 'react';
|
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 { useCompile } from '../schema-component';
|
||||||
import useBuiltInVariables from './hooks/useBuiltinVariables';
|
import useBuiltInVariables from './hooks/useBuiltinVariables';
|
||||||
import { VariableOption, VariablesContextType } from './types';
|
import { VariableOption, VariablesContextType } from './types';
|
||||||
|
import { cacheLazyLoadedValues, getCachedLazyLoadedValues } from './utils/cacheLazyLoadedValues';
|
||||||
import { filterEmptyValues } from './utils/filterEmptyValues';
|
import { filterEmptyValues } from './utils/filterEmptyValues';
|
||||||
import { getAction } from './utils/getAction';
|
import { getAction } from './utils/getAction';
|
||||||
import { getPath } from './utils/getPath';
|
import { getPath } from './utils/getPath';
|
||||||
@ -69,18 +70,18 @@ const VariablesProvider = ({ children, filterVariables }: any) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. 从 `ctx` 中根据 `path` 取值
|
* 1. Get value from `ctx` based on `path`
|
||||||
* 2. 如果某个 `key` 不存在,且 `key` 是一个关联字段,则从 api 中获取数据,并缓存到 `ctx` 中
|
* 2. If a `key` does not exist and is an association field, fetch data from api and cache it in `ctx`
|
||||||
* 3. 如果某个 `key` 不存在,且 `key` 不是一个关联字段,则返回当前值
|
* 3. If a `key` does not exist and is not an association field, return the current value
|
||||||
*/
|
*/
|
||||||
const getResult = useCallback(
|
const getResult = useCallback(
|
||||||
async (
|
async (
|
||||||
variablePath: string,
|
variablePath: string,
|
||||||
localVariables?: VariableOption[],
|
localVariables?: VariableOption[],
|
||||||
options?: {
|
options?: {
|
||||||
/** 第一次请求时,需要包含的关系字段 */
|
/** Related fields that need to be included in the first request */
|
||||||
appends?: string[];
|
appends?: string[];
|
||||||
/** do not request when the association field is empty */
|
/** Do not request when the association field is empty */
|
||||||
doNotRequest?: boolean;
|
doNotRequest?: boolean;
|
||||||
/**
|
/**
|
||||||
* The operator related to the current field, provided when parsing the default value of the field
|
* The operator related to the current field, provided when parsing the default value of the field
|
||||||
@ -115,12 +116,17 @@ const VariablesProvider = ({ children, filterVariables }: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const key = list[index];
|
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 associationField: CollectionFieldOptions_deprecated = getCollectionJoinField(fieldPath, dataSource);
|
||||||
const collectionPrimaryKey = getCollection(collectionName, dataSource)?.getPrimaryKey();
|
const collectionPrimaryKey = getCollection(collectionName, dataSource)?.getPrimaryKey();
|
||||||
if (Array.isArray(current)) {
|
if (Array.isArray(current)) {
|
||||||
const result = current.map((item) => {
|
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) {
|
if (associationField?.target) {
|
||||||
const url = `/${collectionName}/${
|
const url = `/${collectionName}/${
|
||||||
item[associationField.sourceKey || collectionPrimaryKey]
|
item[associationField.sourceKey || collectionPrimaryKey]
|
||||||
@ -138,19 +144,20 @@ const VariablesProvider = ({ children, filterVariables }: any) => {
|
|||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
clearRequested(url);
|
clearRequested(url);
|
||||||
item[key] = data.data.data;
|
const value = data.data.data;
|
||||||
return item[key];
|
cacheLazyLoadedValues(item, currentVariablePath, value);
|
||||||
|
return value;
|
||||||
});
|
});
|
||||||
stashRequested(url, result);
|
stashRequested(url, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return item?.[key];
|
return getCachedLazyLoadedValues(item, currentVariablePath) || item?.[key];
|
||||||
});
|
});
|
||||||
current = removeThroughCollectionFields(_.flatten(await Promise.all(result)), associationField);
|
current = removeThroughCollectionFields(_.flatten(await Promise.all(result)), associationField);
|
||||||
} else if (
|
} else if (
|
||||||
!options?.doNotRequest &&
|
!options?.doNotRequest &&
|
||||||
shouldToRequest(current[key]) &&
|
shouldToRequest(current[key], current, currentVariablePath) &&
|
||||||
current[collectionPrimaryKey] != null &&
|
current[collectionPrimaryKey] != null &&
|
||||||
associationField?.target
|
associationField?.target
|
||||||
) {
|
) {
|
||||||
@ -173,15 +180,18 @@ const VariablesProvider = ({ children, filterVariables }: any) => {
|
|||||||
clearRequested(url);
|
clearRequested(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix https://nocobase.height.app/T-3144,使用 `raw` 方法是为了避免触发 autorun,以修复 T-3144 的错误
|
const value = data.data.data;
|
||||||
if (!raw(current)[key]) {
|
if (!getCachedLazyLoadedValues(current, currentVariablePath)) {
|
||||||
// 把接口返回的数据保存起来,避免重复请求
|
// Cache the API response data to avoid repeated requests
|
||||||
raw(current)[key] = data.data.data;
|
cacheLazyLoadedValues(current, currentVariablePath, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
current = removeThroughCollectionFields(getValuesByPath(current, key), associationField);
|
current = removeThroughCollectionFields(value, associationField);
|
||||||
} else {
|
} else {
|
||||||
current = removeThroughCollectionFields(getValuesByPath(current, key), associationField);
|
current = removeThroughCollectionFields(
|
||||||
|
getCachedLazyLoadedValues(current, currentVariablePath) || getValuesByPath(current, key),
|
||||||
|
associationField,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (associationField?.target) {
|
if (associationField?.target) {
|
||||||
@ -202,7 +212,7 @@ const VariablesProvider = ({ children, filterVariables }: any) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册一个全局变量
|
* Register a global variable
|
||||||
*/
|
*/
|
||||||
const registerVariable = useCallback(
|
const registerVariable = useCallback(
|
||||||
(variableOption: VariableOption) => {
|
(variableOption: VariableOption) => {
|
||||||
@ -248,18 +258,18 @@ const VariablesProvider = ({ children, filterVariables }: any) => {
|
|||||||
|
|
||||||
const parseVariable = useCallback(
|
const parseVariable = useCallback(
|
||||||
/**
|
/**
|
||||||
* 将变量字符串解析为真正的值
|
* Parse the variable string to the actual value
|
||||||
* @param str 变量字符串
|
* @param str Variable string
|
||||||
* @param localVariables 局部变量,解析完成后会被清除
|
* @param localVariables Local variables, will be cleared after parsing
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
async (
|
async (
|
||||||
str: string,
|
str: string,
|
||||||
localVariables?: VariableOption | VariableOption[],
|
localVariables?: VariableOption | VariableOption[],
|
||||||
options?: {
|
options?: {
|
||||||
/** 第一次请求时,需要包含的关系字段 */
|
/** Related fields that need to be included in the first request */
|
||||||
appends?: string[];
|
appends?: string[];
|
||||||
/** do not request when the association field is empty */
|
/** Do not request when the association field is empty */
|
||||||
doNotRequest?: boolean;
|
doNotRequest?: boolean;
|
||||||
/**
|
/**
|
||||||
* The operator related to the current field, provided when parsing the default value of the field
|
* The operator related to the current field, provided when parsing the default value of the field
|
||||||
@ -304,7 +314,7 @@ const VariablesProvider = ({ children, filterVariables }: any) => {
|
|||||||
const { fieldPath, dataSource } = getFieldPath(path, _variableToCollectionName);
|
const { fieldPath, dataSource } = getFieldPath(path, _variableToCollectionName);
|
||||||
let result = getCollectionJoinField(fieldPath, dataSource);
|
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('.')) {
|
if (!result && !path.includes('.')) {
|
||||||
result = {
|
result = {
|
||||||
target: _variableToCollectionName[path]?.collectionName,
|
target: _variableToCollectionName[path]?.collectionName,
|
||||||
@ -347,13 +357,17 @@ VariablesProvider.displayName = 'VariablesProvider';
|
|||||||
|
|
||||||
export default VariablesProvider;
|
export default VariablesProvider;
|
||||||
|
|
||||||
function shouldToRequest(value) {
|
function shouldToRequest(value, variableCtx: Record<string, any>, variablePath: string) {
|
||||||
let result = false;
|
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(() => {
|
untracked(() => {
|
||||||
// fix https://nocobase.height.app/T-2502
|
// 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) === '{}') {
|
if (JSON.stringify(value) === '[{}]' || JSON.stringify(value) === '{}') {
|
||||||
result = true;
|
result = true;
|
||||||
return;
|
return;
|
||||||
@ -392,8 +406,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 value
|
||||||
* @param associationField
|
* @param associationField
|
||||||
* @returns
|
* @returns
|
||||||
|
@ -332,6 +332,14 @@ describe('useVariables', () => {
|
|||||||
name: '$user.belongsToField',
|
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 () => {
|
it('set doNotRequest to true to ensure the result is empty', async () => {
|
||||||
|
@ -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<Record<string, any>, any>();
|
||||||
|
|
||||||
|
export const cacheLazyLoadedValues = (variableCtx: Record<string, any>, 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<string, any>, variablePath: string) => {
|
||||||
|
const cachedValue = cache.get(variableCtx);
|
||||||
|
return cachedValue?.[variablePath];
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user