From de902c538b82ee0cf54b18805ee948a975a9d0b5 Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Mon, 18 Dec 2023 15:48:39 +0800 Subject: [PATCH] fix(variable): local variables should not affect global variables (#3214) --- .../form-item/hooks/useParseDefaultValue.ts | 33 ++--- .../src/variables/VariablesProvider.tsx | 139 +++++++++--------- .../variables/__tests__/useVariables.test.tsx | 28 ++++ 3 files changed, 114 insertions(+), 86 deletions(-) diff --git a/packages/core/client/src/schema-component/antd/form-item/hooks/useParseDefaultValue.ts b/packages/core/client/src/schema-component/antd/form-item/hooks/useParseDefaultValue.ts index 6e446324d..b995e4854 100644 --- a/packages/core/client/src/schema-component/antd/form-item/hooks/useParseDefaultValue.ts +++ b/packages/core/client/src/schema-component/antd/form-item/hooks/useParseDefaultValue.ts @@ -108,29 +108,28 @@ const useParseDefaultValue = () => { const variableName = getVariableName(fieldSchema.default); const variable = findVariable(variableName); - if (process.env.NODE_ENV !== 'production' && !variable) { - console.error(`useParseDefaultValue: can not find variable ${variableName}`); + if (!variable) { + return console.error(`useParseDefaultValue: can not find variable ${variableName}`); } - if (variable) { - _run(); + _run(); - // 实现联动的效果,当依赖的变量变化时(如 `$nForm` 变量),重新解析默认值 - const dispose = reaction(() => { - const obj = { [variableName]: variable?.ctx || {} }; - const path = getPath(fieldSchema.default); + // 实现联动的效果,当依赖的变量变化时(如 `$nForm` 变量),重新解析默认值 + const dispose = reaction(() => { + const obj = { [variableName]: variable?.ctx || {} }; + const path = getPath(fieldSchema.default); + const value = getValuesByPath(obj, path); - // fix https://nocobase.height.app/T-2212 - if (getValuesByPath(obj, path) === undefined) { - // 返回一个随机值,确保能触发 run 函数 - return Math.random(); - } + // fix https://nocobase.height.app/T-2212 + if (value === undefined) { + // 返回一个随机值,确保能触发 run 函数 + return Math.random(); + } - return getValuesByPath(obj, path); - }, run); + return value; + }, run); - return dispose; - } + return dispose; } }, [fieldSchema.default]); }; diff --git a/packages/core/client/src/variables/VariablesProvider.tsx b/packages/core/client/src/variables/VariablesProvider.tsx index e3fee87ba..954ad58b3 100644 --- a/packages/core/client/src/variables/VariablesProvider.tsx +++ b/packages/core/client/src/variables/VariablesProvider.tsx @@ -19,7 +19,7 @@ export const VariablesContext = createContext(null); const variableToCollectionName = {}; -const getFieldPath = (variablePath: string) => { +const getFieldPath = (variablePath: string, variableToCollectionName: Record) => { const list = variablePath.split('.'); const result = list.map((item) => { if (variableToCollectionName[item]) { @@ -51,13 +51,17 @@ const VariablesProvider = ({ children }) => { * 3. 如果某个 `key` 不存在,且 `key` 不是一个关联字段,则返回当前值 */ const getValue = useCallback( - async (variablePath: string) => { + async (variablePath: string, localVariables?: VariableOption[]) => { const list = variablePath.split('.'); const variableName = list[0]; - let current = ctxRef.current; - let collectionName = getFieldPath(variableName); + const _variableToCollectionName = mergeVariableToCollectionNameWithLocalVariables( + variableToCollectionName, + localVariables, + ); + let current = mergeCtxWithLocalVariables(ctxRef.current, localVariables); + let collectionName = getFieldPath(variableName, _variableToCollectionName); - if (process.env.NODE_ENV !== 'production' && !ctxRef.current[variableName]) { + if (!current[variableName]) { throw new Error(`VariablesProvider: ${variableName} is not found`); } @@ -68,7 +72,7 @@ const VariablesProvider = ({ children }) => { const key = list[index]; const associationField: CollectionFieldOptions = getCollectionJoinField( - getFieldPath(list.slice(0, index + 1).join('.')), + getFieldPath(list.slice(0, index + 1).join('.'), _variableToCollectionName), ); if (Array.isArray(current)) { const result = current.map((item) => { @@ -128,7 +132,7 @@ const VariablesProvider = ({ children }) => { */ const registerVariable = useCallback( (variableOption: VariableOption) => { - if (process.env.NODE_ENV !== 'production' && !isVariable(`{{${variableOption.name}}}`)) { + if (!isVariable(`{{${variableOption.name}}}`)) { throw new Error(`VariablesProvider: ${variableOption.name} is not a valid name`); } @@ -157,50 +161,16 @@ const VariablesProvider = ({ children }) => { }; }, []); - const removeVariable = useCallback((variableName: string) => { - setCtx((prev) => { - const next = { ...prev }; - delete next[variableName]; - return next; - }); - delete variableToCollectionName[variableName]; - }, []); - - const onLocalVariablesReady = useCallback( - async (localVariables: VariableOption | VariableOption[], handler: () => Promise) => { - let old = null; - if (localVariables) { - if (Array.isArray(localVariables)) { - old = localVariables.map((item) => getVariable(item.name)); - localVariables.forEach((item) => registerVariable(item)); - } else { - // 1. 如果有局部变量,先把全局中同名的变量取出来 - old = getVariable(localVariables.name); - // 2. 把局部变量注册到全局,这样就可以使用了 - registerVariable(localVariables); - } - } - - await handler(); - - // 3. 局部变量使用完成后,需要在全局中清除 - if (localVariables) { - if (Array.isArray(localVariables)) { - localVariables.forEach((item) => removeVariable(item.name)); - } else { - removeVariable(localVariables.name); - } - } - // 4. 如果有同名的全局变量,把它重新注册回去 - if (old) { - if (Array.isArray(old)) { - old.filter(Boolean).forEach((item) => registerVariable(item)); - } else { - registerVariable(old); - } - } + const removeVariable = useCallback( + (variableName: string) => { + setCtx((prev) => { + const next = { ...prev }; + delete next[variableName]; + return next; + }); + delete variableToCollectionName[variableName]; }, - [getVariable, registerVariable, removeVariable], + [setCtx], ); const parseVariable = useCallback( @@ -215,40 +185,46 @@ const VariablesProvider = ({ children }) => { return str; } - let value = null; - await onLocalVariablesReady(localVariables, async () => { - const path = getPath(str); - value = await getValue(path); - }); + if (localVariables) { + localVariables = _.isArray(localVariables) ? localVariables : [localVariables]; + } + + const path = getPath(str); + const value = await getValue(path, localVariables as VariableOption[]); return uniq(filterEmptyValues(value)); }, - [getValue, onLocalVariablesReady], + [getValue], ); const getCollectionField = useCallback( async (variableString: string, localVariables?: VariableOption | VariableOption[]) => { - if (process.env.NODE_ENV !== 'production' && !isVariable(variableString)) { + if (!isVariable(variableString)) { throw new Error(`VariablesProvider: ${variableString} is not a variable string`); } - let result = null; + if (localVariables) { + localVariables = _.isArray(localVariables) ? localVariables : [localVariables]; + } - await onLocalVariablesReady(localVariables, async () => { - const path = getPath(variableString); - result = getCollectionJoinField(getFieldPath(path)); + const path = getPath(variableString); + let result = getCollectionJoinField( + getFieldPath( + path, + mergeVariableToCollectionNameWithLocalVariables(variableToCollectionName, localVariables as VariableOption[]), + ), + ); - // 当仅有一个例如 `$user` 这样的字符串时,需要拼一个假的 `collectionField` 返回 - if (!result && !path.includes('.')) { - result = { - target: variableToCollectionName[path], - }; - } - }); + // 当仅有一个例如 `$user` 这样的字符串时,需要拼一个假的 `collectionField` 返回 + if (!result && !path.includes('.')) { + result = { + target: variableToCollectionName[path], + }; + } return result; }, - [getCollectionJoinField, onLocalVariablesReady], + [getCollectionJoinField], ); useEffect(() => { @@ -295,3 +271,28 @@ function shouldToRequest(value) { return result; } + +function mergeCtxWithLocalVariables(ctx: Record, localVariables?: VariableOption[]) { + ctx = { ...ctx }; + + localVariables?.forEach((item) => { + ctx[item.name] = item.ctx; + }); + + return ctx; +} + +function mergeVariableToCollectionNameWithLocalVariables( + variableToCollectionName: Record, + localVariables?: VariableOption[], +) { + variableToCollectionName = { ...variableToCollectionName }; + + localVariables?.forEach((item) => { + if (item.collectionName) { + variableToCollectionName[item.name] = item.collectionName; + } + }); + + return variableToCollectionName; +} diff --git a/packages/core/client/src/variables/__tests__/useVariables.test.tsx b/packages/core/client/src/variables/__tests__/useVariables.test.tsx index 86b306c54..88ad6a6fd 100644 --- a/packages/core/client/src/variables/__tests__/useVariables.test.tsx +++ b/packages/core/client/src/variables/__tests__/useVariables.test.tsx @@ -603,6 +603,34 @@ describe('useVariables', () => { expect(result.current.getVariable('$local')).toBe(null); }); + it('parse multiple variables concurrently using local variables', async () => { + const { result } = renderHook(() => useVariables(), { + wrapper: Providers, + }); + + const promises = []; + + await waitFor(() => { + for (let i = 0; i < 3; i++) { + promises.push( + result.current.parseVariable('{{ $user.nickname }}', [ + { + name: `$local`, + ctx: { + name: `local variable ${i}`, + }, + }, + ]), + ); + } + }); + + await Promise.all(promises); + + // 并发多次解析之后,最终的全局变量不应该包含之前注册的局部变量 + expect(result.current.getVariable('$local')).toBe(null); + }); + it('no id', async () => { const { result } = renderHook(() => useVariables(), { wrapper: Providers,