mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
* fix: use appends param to load association data * chore: update yarn.lock * test: add test * test: remove the 'BUG:' text * test: fix 'window is not defined' * test: increase timeout
319 lines
9.7 KiB
TypeScript
319 lines
9.7 KiB
TypeScript
import { untracked } from '@formily/reactive';
|
|
import { getValuesByPath } from '@nocobase/utils/client';
|
|
import _ from 'lodash';
|
|
import React, { createContext, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
import { useAPIClient } from '../api-client';
|
|
import type { CollectionFieldOptions } from '../collection-manager';
|
|
import { useCollectionManager } from '../collection-manager';
|
|
import { useCompile } from '../schema-component';
|
|
import useBuiltInVariables from './hooks/useBuiltinVariables';
|
|
import { VariableOption, VariablesContextType } from './types';
|
|
import { filterEmptyValues } from './utils/filterEmptyValues';
|
|
import { getAction } from './utils/getAction';
|
|
import { getPath } from './utils/getPath';
|
|
import { clearRequested, getRequested, hasRequested, stashRequested } from './utils/hasRequested';
|
|
import { isVariable } from './utils/isVariable';
|
|
import { uniq } from './utils/uniq';
|
|
|
|
export const VariablesContext = createContext<VariablesContextType>(null);
|
|
|
|
const variableToCollectionName = {};
|
|
|
|
const getFieldPath = (variablePath: string, variableToCollectionName: Record<string, any>) => {
|
|
const list = variablePath.split('.');
|
|
const result = list.map((item) => {
|
|
if (variableToCollectionName[item]) {
|
|
return variableToCollectionName[item];
|
|
}
|
|
return item;
|
|
});
|
|
return result.join('.');
|
|
};
|
|
|
|
const VariablesProvider = ({ children }) => {
|
|
const ctxRef = useRef<Record<string, any>>({});
|
|
const api = useAPIClient();
|
|
const { getCollectionJoinField } = useCollectionManager();
|
|
const compile = useCompile();
|
|
const { builtinVariables } = useBuiltInVariables();
|
|
|
|
const setCtx = useCallback((ctx: Record<string, any> | ((prev: Record<string, any>) => Record<string, any>)) => {
|
|
if (_.isFunction(ctx)) {
|
|
ctxRef.current = ctx(ctxRef.current);
|
|
} else {
|
|
ctxRef.current = ctx;
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* 1. 从 `ctx` 中根据 `path` 取值
|
|
* 2. 如果某个 `key` 不存在,且 `key` 是一个关联字段,则从 api 中获取数据,并缓存到 `ctx` 中
|
|
* 3. 如果某个 `key` 不存在,且 `key` 不是一个关联字段,则返回当前值
|
|
*/
|
|
const getValue = useCallback(
|
|
async (
|
|
variablePath: string,
|
|
localVariables?: VariableOption[],
|
|
options?: {
|
|
/** 第一次请求时,需要包含的关系字段 */
|
|
appends?: string[];
|
|
},
|
|
) => {
|
|
const list = variablePath.split('.');
|
|
const variableName = list[0];
|
|
const _variableToCollectionName = mergeVariableToCollectionNameWithLocalVariables(
|
|
variableToCollectionName,
|
|
localVariables,
|
|
);
|
|
let current = mergeCtxWithLocalVariables(ctxRef.current, localVariables);
|
|
let collectionName = getFieldPath(variableName, _variableToCollectionName);
|
|
|
|
if (!current[variableName]) {
|
|
throw new Error(`VariablesProvider: ${variableName} is not found`);
|
|
}
|
|
|
|
for (let index = 0; index < list.length; index++) {
|
|
if (current == null) {
|
|
return current;
|
|
}
|
|
|
|
const key = list[index];
|
|
const associationField: CollectionFieldOptions = getCollectionJoinField(
|
|
getFieldPath(list.slice(0, index + 1).join('.'), _variableToCollectionName),
|
|
);
|
|
if (Array.isArray(current)) {
|
|
const result = current.map((item) => {
|
|
if (shouldToRequest(item?.[key]) && item?.id != null) {
|
|
if (associationField?.target) {
|
|
const url = `/${collectionName}/${item.id}/${key}:${getAction(associationField.type)}`;
|
|
if (hasRequested(url)) {
|
|
return getRequested(url);
|
|
}
|
|
const result = api
|
|
.request({
|
|
url,
|
|
params: {
|
|
appends: options?.appends,
|
|
},
|
|
})
|
|
.then((data) => {
|
|
clearRequested(url);
|
|
item[key] = data.data.data;
|
|
return item[key];
|
|
});
|
|
stashRequested(url, result);
|
|
return result;
|
|
}
|
|
}
|
|
return item?.[key];
|
|
});
|
|
current = _.flatten(await Promise.all(result));
|
|
} else if (shouldToRequest(current[key]) && current.id != null && associationField?.target) {
|
|
const url = `/${collectionName}/${current.id}/${key}:${getAction(associationField.type)}`;
|
|
let data = null;
|
|
if (hasRequested(url)) {
|
|
data = await getRequested(url);
|
|
} else {
|
|
const waitForData = api.request({
|
|
url,
|
|
params: {
|
|
appends: options?.appends,
|
|
},
|
|
});
|
|
stashRequested(url, waitForData);
|
|
data = await waitForData;
|
|
clearRequested(url);
|
|
}
|
|
current[key] = data.data.data;
|
|
current = getValuesByPath(current, key);
|
|
} else {
|
|
current = getValuesByPath(current, key);
|
|
}
|
|
|
|
if (associationField?.target) {
|
|
collectionName = associationField.target;
|
|
}
|
|
}
|
|
|
|
return compile(_.isFunction(current) ? current() : current);
|
|
},
|
|
[getCollectionJoinField],
|
|
);
|
|
|
|
/**
|
|
* 注册一个全局变量
|
|
*/
|
|
const registerVariable = useCallback(
|
|
(variableOption: VariableOption) => {
|
|
if (!isVariable(`{{${variableOption.name}}}`)) {
|
|
throw new Error(`VariablesProvider: ${variableOption.name} is not a valid name`);
|
|
}
|
|
|
|
setCtx((prev) => {
|
|
return {
|
|
...prev,
|
|
[variableOption.name]: variableOption.ctx,
|
|
};
|
|
});
|
|
if (variableOption.collectionName) {
|
|
variableToCollectionName[variableOption.name] = variableOption.collectionName;
|
|
}
|
|
},
|
|
[setCtx],
|
|
);
|
|
|
|
const getVariable = useCallback((variableName: string): VariableOption => {
|
|
if (!ctxRef.current[variableName]) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
name: variableName,
|
|
ctx: ctxRef.current[variableName],
|
|
collectionName: variableToCollectionName[variableName],
|
|
};
|
|
}, []);
|
|
|
|
const removeVariable = useCallback(
|
|
(variableName: string) => {
|
|
setCtx((prev) => {
|
|
const next = { ...prev };
|
|
delete next[variableName];
|
|
return next;
|
|
});
|
|
delete variableToCollectionName[variableName];
|
|
},
|
|
[setCtx],
|
|
);
|
|
|
|
const parseVariable = useCallback(
|
|
/**
|
|
* 将变量字符串解析为真正的值
|
|
* @param str 变量字符串
|
|
* @param localVariables 局部变量,解析完成后会被清除
|
|
* @returns
|
|
*/
|
|
async (
|
|
str: string,
|
|
localVariables?: VariableOption | VariableOption[],
|
|
options?: {
|
|
/** 第一次请求时,需要包含的关系字段 */
|
|
appends?: string[];
|
|
},
|
|
) => {
|
|
if (!isVariable(str)) {
|
|
return str;
|
|
}
|
|
|
|
if (localVariables) {
|
|
localVariables = _.isArray(localVariables) ? localVariables : [localVariables];
|
|
}
|
|
|
|
const path = getPath(str);
|
|
const value = await getValue(path, localVariables as VariableOption[], options);
|
|
|
|
return uniq(filterEmptyValues(value));
|
|
},
|
|
[getValue],
|
|
);
|
|
|
|
const getCollectionField = useCallback(
|
|
async (variableString: string, localVariables?: VariableOption | VariableOption[]) => {
|
|
if (!isVariable(variableString)) {
|
|
throw new Error(`VariablesProvider: ${variableString} is not a variable string`);
|
|
}
|
|
|
|
if (localVariables) {
|
|
localVariables = _.isArray(localVariables) ? localVariables : [localVariables];
|
|
}
|
|
|
|
const path = getPath(variableString);
|
|
let result = getCollectionJoinField(
|
|
getFieldPath(
|
|
path,
|
|
mergeVariableToCollectionNameWithLocalVariables(variableToCollectionName, localVariables as VariableOption[]),
|
|
),
|
|
);
|
|
|
|
// 当仅有一个例如 `$user` 这样的字符串时,需要拼一个假的 `collectionField` 返回
|
|
if (!result && !path.includes('.')) {
|
|
result = {
|
|
target: variableToCollectionName[path],
|
|
};
|
|
}
|
|
|
|
return result;
|
|
},
|
|
[getCollectionJoinField],
|
|
);
|
|
|
|
useEffect(() => {
|
|
builtinVariables.forEach((variableOption) => {
|
|
registerVariable(variableOption);
|
|
});
|
|
}, [builtinVariables, registerVariable]);
|
|
|
|
const value = useMemo(
|
|
() =>
|
|
({
|
|
ctxRef,
|
|
setCtx,
|
|
parseVariable,
|
|
registerVariable,
|
|
getVariable,
|
|
getCollectionField,
|
|
removeVariable,
|
|
}) as VariablesContextType,
|
|
[getCollectionField, getVariable, parseVariable, registerVariable, removeVariable, setCtx],
|
|
);
|
|
|
|
return <VariablesContext.Provider value={value}>{children}</VariablesContext.Provider>;
|
|
};
|
|
|
|
VariablesProvider.displayName = 'VariablesProvider';
|
|
|
|
export default VariablesProvider;
|
|
|
|
function shouldToRequest(value) {
|
|
let result = false;
|
|
|
|
// value 有可能是一个响应式对象,使用 untracked 可以避免意外触发 autorun
|
|
untracked(() => {
|
|
// fix https://nocobase.height.app/T-2502
|
|
// 兼容 `对多` 和 `对一` 子表单子表格字段的情况
|
|
if (JSON.stringify(value) === '[{}]' || JSON.stringify(value) === '{}') {
|
|
result = true;
|
|
return;
|
|
}
|
|
|
|
result = _.isEmpty(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;
|
|
}
|