fix(variable): local variables should not affect global variables (#3214)

This commit is contained in:
Zeke Zhang 2023-12-18 15:48:39 +08:00 committed by GitHub
parent 6bd0568d08
commit de902c538b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 114 additions and 86 deletions

View File

@ -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]);
}; };

View File

@ -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;
}

View File

@ -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,