fix(variable): local variables should not affect global variables (#3214)
This commit is contained in:
		
							parent
							
								
									6bd0568d08
								
							
						
					
					
						commit
						de902c538b
					
				| @ -108,29 +108,28 @@ const useParseDefaultValue = () => { | |||||||
|       const variableName = getVariableName(fieldSchema.default); |       const variableName = getVariableName(fieldSchema.default); | ||||||
|       const variable = findVariable(variableName); |       const variable = findVariable(variableName); | ||||||
| 
 | 
 | ||||||
|       if (process.env.NODE_ENV !== 'production' && !variable) { |       if (!variable) { | ||||||
|         console.error(`useParseDefaultValue: can not find variable ${variableName}`); |         return console.error(`useParseDefaultValue: can not find variable ${variableName}`); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (variable) { |       _run(); | ||||||
|         _run(); |  | ||||||
| 
 | 
 | ||||||
|         // 实现联动的效果,当依赖的变量变化时(如 `$nForm` 变量),重新解析默认值
 |       // 实现联动的效果,当依赖的变量变化时(如 `$nForm` 变量),重新解析默认值
 | ||||||
|         const dispose = reaction(() => { |       const dispose = reaction(() => { | ||||||
|           const obj = { [variableName]: variable?.ctx || {} }; |         const obj = { [variableName]: variable?.ctx || {} }; | ||||||
|           const path = getPath(fieldSchema.default); |         const path = getPath(fieldSchema.default); | ||||||
|  |         const value = getValuesByPath(obj, path); | ||||||
| 
 | 
 | ||||||
|           // fix https://nocobase.height.app/T-2212
 |         // fix https://nocobase.height.app/T-2212
 | ||||||
|           if (getValuesByPath(obj, path) === undefined) { |         if (value === undefined) { | ||||||
|             // 返回一个随机值,确保能触发 run 函数
 |           // 返回一个随机值,确保能触发 run 函数
 | ||||||
|             return Math.random(); |           return Math.random(); | ||||||
|           } |         } | ||||||
| 
 | 
 | ||||||
|           return getValuesByPath(obj, path); |         return value; | ||||||
|         }, run); |       }, run); | ||||||
| 
 | 
 | ||||||
|         return dispose; |       return dispose; | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|   }, [fieldSchema.default]); |   }, [fieldSchema.default]); | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ export const VariablesContext = createContext<VariablesContextType>(null); | |||||||
| 
 | 
 | ||||||
| const variableToCollectionName = {}; | const variableToCollectionName = {}; | ||||||
| 
 | 
 | ||||||
| const getFieldPath = (variablePath: string) => { | const getFieldPath = (variablePath: string, variableToCollectionName: Record<string, any>) => { | ||||||
|   const list = variablePath.split('.'); |   const list = variablePath.split('.'); | ||||||
|   const result = list.map((item) => { |   const result = list.map((item) => { | ||||||
|     if (variableToCollectionName[item]) { |     if (variableToCollectionName[item]) { | ||||||
| @ -51,13 +51,17 @@ const VariablesProvider = ({ children }) => { | |||||||
|    * 3. 如果某个 `key` 不存在,且 `key` 不是一个关联字段,则返回当前值 |    * 3. 如果某个 `key` 不存在,且 `key` 不是一个关联字段,则返回当前值 | ||||||
|    */ |    */ | ||||||
|   const getValue = useCallback( |   const getValue = useCallback( | ||||||
|     async (variablePath: string) => { |     async (variablePath: string, localVariables?: VariableOption[]) => { | ||||||
|       const list = variablePath.split('.'); |       const list = variablePath.split('.'); | ||||||
|       const variableName = list[0]; |       const variableName = list[0]; | ||||||
|       let current = ctxRef.current; |       const _variableToCollectionName = mergeVariableToCollectionNameWithLocalVariables( | ||||||
|       let collectionName = getFieldPath(variableName); |         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`); |         throw new Error(`VariablesProvider: ${variableName} is not found`); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
| @ -68,7 +72,7 @@ const VariablesProvider = ({ children }) => { | |||||||
| 
 | 
 | ||||||
|         const key = list[index]; |         const key = list[index]; | ||||||
|         const associationField: CollectionFieldOptions = getCollectionJoinField( |         const associationField: CollectionFieldOptions = getCollectionJoinField( | ||||||
|           getFieldPath(list.slice(0, index + 1).join('.')), |           getFieldPath(list.slice(0, index + 1).join('.'), _variableToCollectionName), | ||||||
|         ); |         ); | ||||||
|         if (Array.isArray(current)) { |         if (Array.isArray(current)) { | ||||||
|           const result = current.map((item) => { |           const result = current.map((item) => { | ||||||
| @ -128,7 +132,7 @@ const VariablesProvider = ({ children }) => { | |||||||
|    */ |    */ | ||||||
|   const registerVariable = useCallback( |   const registerVariable = useCallback( | ||||||
|     (variableOption: VariableOption) => { |     (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`); |         throw new Error(`VariablesProvider: ${variableOption.name} is not a valid name`); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
| @ -157,50 +161,16 @@ const VariablesProvider = ({ children }) => { | |||||||
|     }; |     }; | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|   const removeVariable = useCallback((variableName: string) => { |   const removeVariable = useCallback( | ||||||
|     setCtx((prev) => { |     (variableName: string) => { | ||||||
|       const next = { ...prev }; |       setCtx((prev) => { | ||||||
|       delete next[variableName]; |         const next = { ...prev }; | ||||||
|       return next; |         delete next[variableName]; | ||||||
|     }); |         return next; | ||||||
|     delete variableToCollectionName[variableName]; |       }); | ||||||
|   }, []); |       delete variableToCollectionName[variableName]; | ||||||
| 
 |  | ||||||
|   const onLocalVariablesReady = useCallback( |  | ||||||
|     async (localVariables: VariableOption | VariableOption[], handler: () => Promise<void>) => { |  | ||||||
|       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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }, |     }, | ||||||
|     [getVariable, registerVariable, removeVariable], |     [setCtx], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const parseVariable = useCallback( |   const parseVariable = useCallback( | ||||||
| @ -215,40 +185,46 @@ const VariablesProvider = ({ children }) => { | |||||||
|         return str; |         return str; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       let value = null; |       if (localVariables) { | ||||||
|       await onLocalVariablesReady(localVariables, async () => { |         localVariables = _.isArray(localVariables) ? localVariables : [localVariables]; | ||||||
|         const path = getPath(str); |       } | ||||||
|         value = await getValue(path); | 
 | ||||||
|       }); |       const path = getPath(str); | ||||||
|  |       const value = await getValue(path, localVariables as VariableOption[]); | ||||||
| 
 | 
 | ||||||
|       return uniq(filterEmptyValues(value)); |       return uniq(filterEmptyValues(value)); | ||||||
|     }, |     }, | ||||||
|     [getValue, onLocalVariablesReady], |     [getValue], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const getCollectionField = useCallback( |   const getCollectionField = useCallback( | ||||||
|     async (variableString: string, localVariables?: VariableOption | VariableOption[]) => { |     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`); |         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); | ||||||
|         const path = getPath(variableString); |       let result = getCollectionJoinField( | ||||||
|         result = getCollectionJoinField(getFieldPath(path)); |         getFieldPath( | ||||||
|  |           path, | ||||||
|  |           mergeVariableToCollectionNameWithLocalVariables(variableToCollectionName, localVariables as VariableOption[]), | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
| 
 | 
 | ||||||
|         // 当仅有一个例如 `$user` 这样的字符串时,需要拼一个假的 `collectionField` 返回
 |       // 当仅有一个例如 `$user` 这样的字符串时,需要拼一个假的 `collectionField` 返回
 | ||||||
|         if (!result && !path.includes('.')) { |       if (!result && !path.includes('.')) { | ||||||
|           result = { |         result = { | ||||||
|             target: variableToCollectionName[path], |           target: variableToCollectionName[path], | ||||||
|           }; |         }; | ||||||
|         } |       } | ||||||
|       }); |  | ||||||
| 
 | 
 | ||||||
|       return result; |       return result; | ||||||
|     }, |     }, | ||||||
|     [getCollectionJoinField, onLocalVariablesReady], |     [getCollectionJoinField], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @ -295,3 +271,28 @@ function shouldToRequest(value) { | |||||||
| 
 | 
 | ||||||
|   return result; |   return result; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | function mergeCtxWithLocalVariables(ctx: Record<string, any>, localVariables?: VariableOption[]) { | ||||||
|  |   ctx = { ...ctx }; | ||||||
|  | 
 | ||||||
|  |   localVariables?.forEach((item) => { | ||||||
|  |     ctx[item.name] = item.ctx; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   return ctx; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function mergeVariableToCollectionNameWithLocalVariables( | ||||||
|  |   variableToCollectionName: Record<string, any>, | ||||||
|  |   localVariables?: VariableOption[], | ||||||
|  | ) { | ||||||
|  |   variableToCollectionName = { ...variableToCollectionName }; | ||||||
|  | 
 | ||||||
|  |   localVariables?.forEach((item) => { | ||||||
|  |     if (item.collectionName) { | ||||||
|  |       variableToCollectionName[item.name] = item.collectionName; | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   return variableToCollectionName; | ||||||
|  | } | ||||||
|  | |||||||
| @ -603,6 +603,34 @@ describe('useVariables', () => { | |||||||
|     expect(result.current.getVariable('$local')).toBe(null); |     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 () => { |   it('no id', async () => { | ||||||
|     const { result } = renderHook(() => useVariables(), { |     const { result } = renderHook(() => useVariables(), { | ||||||
|       wrapper: Providers, |       wrapper: Providers, | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user