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

View File

@ -19,7 +19,7 @@ export const VariablesContext = createContext<VariablesContextType>(null);
const variableToCollectionName = {};
const getFieldPath = (variablePath: string) => {
const getFieldPath = (variablePath: string, variableToCollectionName: Record<string, any>) => {
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<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);
}
}
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<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);
});
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,