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