fix(variable): local variables should not affect global variables (#3214)
This commit is contained in:
parent
6bd0568d08
commit
de902c538b
@ -108,30 +108,29 @@ 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();
|
||||
|
||||
// 实现联动的效果,当依赖的变量变化时(如 `$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) {
|
||||
if (value === undefined) {
|
||||
// 返回一个随机值,确保能触发 run 函数
|
||||
return Math.random();
|
||||
}
|
||||
|
||||
return getValuesByPath(obj, path);
|
||||
return value;
|
||||
}, run);
|
||||
|
||||
return dispose;
|
||||
}
|
||||
}
|
||||
}, [fieldSchema.default]);
|
||||
};
|
||||
|
||||
|
@ -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) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
[getVariable, registerVariable, removeVariable],
|
||||
[setCtx],
|
||||
);
|
||||
|
||||
const parseVariable = useCallback(
|
||||
@ -215,28 +185,35 @@ const VariablesProvider = ({ children }) => {
|
||||
return str;
|
||||
}
|
||||
|
||||
let value = null;
|
||||
await onLocalVariablesReady(localVariables, async () => {
|
||||
if (localVariables) {
|
||||
localVariables = _.isArray(localVariables) ? localVariables : [localVariables];
|
||||
}
|
||||
|
||||
const path = getPath(str);
|
||||
value = await getValue(path);
|
||||
});
|
||||
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));
|
||||
let result = getCollectionJoinField(
|
||||
getFieldPath(
|
||||
path,
|
||||
mergeVariableToCollectionNameWithLocalVariables(variableToCollectionName, localVariables as VariableOption[]),
|
||||
),
|
||||
);
|
||||
|
||||
// 当仅有一个例如 `$user` 这样的字符串时,需要拼一个假的 `collectionField` 返回
|
||||
if (!result && !path.includes('.')) {
|
||||
@ -244,11 +221,10 @@ const VariablesProvider = ({ children }) => {
|
||||
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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user