mirror of
https://github.com/nocobase/nocobase.git
synced 2026-09-19 02:23:00 +08:00
fix: incorrect resolving of the parent popup’s record variable (#7637)
This commit is contained in:
@@ -13,6 +13,8 @@ import type { RecordRef } from '../utils/serverContextParams';
|
||||
import type { Collection } from '../data-source';
|
||||
import type { FlowView } from './FlowView';
|
||||
|
||||
type PopupModelLike = { getStepParams?: (a: string, b: string) => any } | undefined;
|
||||
|
||||
// 判断是否为普通对象(Plain Object),避免对类实例/代理等进行深度遍历
|
||||
function isPlainObject(val: any) {
|
||||
if (val === null || typeof val !== 'object') return false;
|
||||
@@ -140,7 +142,8 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
|
||||
const getCurrentCollection = (): Collection | null => {
|
||||
try {
|
||||
const ref = inferViewRecordRef(ctx);
|
||||
if (!ref?.filterByTk) return null;
|
||||
// 避免 0 等 falsy 值被误判为无效主键
|
||||
if (typeof ref?.filterByTk === 'undefined' || ref.filterByTk === null) return null;
|
||||
const ds = ctx.dataSourceManager?.getDataSource?.(ref.dataSourceKey || 'main');
|
||||
return ds?.collectionManager?.getCollection?.(ref.collection) || null;
|
||||
} catch (_) {
|
||||
@@ -149,9 +152,10 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
|
||||
};
|
||||
|
||||
// 从视图堆栈推断 level 级父弹窗(level=1 上一层)
|
||||
const getParentRecordRef = async (level: number): Promise<RecordRef | undefined> => {
|
||||
const getParentRecordRef = async (level: number, flowCtx?: FlowContext): Promise<RecordRef | undefined> => {
|
||||
try {
|
||||
const nav = ctx.view?.navigation;
|
||||
const useCtx = flowCtx || ctx;
|
||||
const nav = useCtx.view?.navigation;
|
||||
const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
|
||||
if (stack.length < 2 || level < 1) return undefined;
|
||||
const idx = stack.length - 1 - level;
|
||||
@@ -159,12 +163,12 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
|
||||
const parent = stack[idx];
|
||||
if (!parent?.viewUid) return undefined;
|
||||
|
||||
let model: any = ctx.engine?.getModel?.(parent.viewUid);
|
||||
if (!model && typeof ctx.engine?.loadModel === 'function') {
|
||||
let model = useCtx.engine?.getModel?.(parent.viewUid) as PopupModelLike;
|
||||
if (!model) {
|
||||
try {
|
||||
model = await ctx.engine.loadModel({ uid: parent.viewUid });
|
||||
model = (await useCtx.engine.loadModel({ uid: parent.viewUid })) as PopupModelLike;
|
||||
} catch (e) {
|
||||
console.warn('[FlowEngine] popup.getParentRecordRef loadModel failed:', e);
|
||||
(useCtx.logger || ctx.logger)?.warn?.({ err: e }, '[FlowEngine] popup.getParentRecordRef loadModel failed');
|
||||
}
|
||||
}
|
||||
const params = model?.getStepParams?.('popupSettings', 'openView') || {};
|
||||
@@ -174,7 +178,7 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
|
||||
if (!collection || typeof filterByTk === 'undefined' || filterByTk === null) return undefined;
|
||||
return { collection, dataSourceKey, filterByTk };
|
||||
} catch (e) {
|
||||
console.warn('[FlowEngine] popup.getParentRecordRef failed:', e);
|
||||
(flowCtx?.logger || ctx.logger)?.warn?.({ err: e }, '[FlowEngine] popup.getParentRecordRef failed');
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
@@ -251,10 +255,37 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
|
||||
const meta: PropertyMeta = {
|
||||
type: 'object',
|
||||
title: t('Current popup'),
|
||||
buildVariablesParams: (c) => {
|
||||
buildVariablesParams: async (c) => {
|
||||
const ref = inferViewRecordRef(c);
|
||||
const inputArgs = (c?.view as any)?.inputArgs || {};
|
||||
const out: Record<string, any> = { record: ref };
|
||||
const inputArgs = c.view?.inputArgs;
|
||||
type PopupVariableParams = {
|
||||
record?: RecordRef;
|
||||
sourceRecord?: RecordRef;
|
||||
parent?: PopupVariableParams;
|
||||
};
|
||||
const params: PopupVariableParams = {};
|
||||
if (ref) params.record = ref;
|
||||
|
||||
// 构建 parent 链(用于服务端解析 ctx.popup.parent[.parent...].record.*)
|
||||
try {
|
||||
const nav = c.view?.navigation;
|
||||
const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
|
||||
if (stack.length >= 2) {
|
||||
let cur: Record<string, any> = params;
|
||||
let level = 1;
|
||||
let parentRef = await getParentRecordRef(level, c);
|
||||
while (parentRef) {
|
||||
if (!cur.parent) cur.parent = {};
|
||||
cur.parent.record = parentRef;
|
||||
cur = cur.parent;
|
||||
level += 1;
|
||||
parentRef = await getParentRecordRef(level, c);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
c.logger?.debug?.({ err }, '[FlowEngine] buildVariablesParams: build parent-chain failed');
|
||||
}
|
||||
|
||||
try {
|
||||
const srcId = inputArgs?.sourceId;
|
||||
const assoc: string | undefined = inputArgs?.associationName;
|
||||
@@ -263,17 +294,17 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
|
||||
// associationName 形如 `posts.comments`,父级集合为 `posts`
|
||||
const parentCollectionName = String(assoc).split('.')[0];
|
||||
if (parentCollectionName) {
|
||||
out.sourceRecord = {
|
||||
params.sourceRecord = {
|
||||
collection: parentCollectionName,
|
||||
dataSourceKey: dsKey,
|
||||
filterByTk: srcId,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// 忽略异常,保持 record 正常返回
|
||||
} catch (err) {
|
||||
c.logger?.debug?.({ err }, '[FlowEngine] buildVariablesParams: infer sourceRecord failed');
|
||||
}
|
||||
return out;
|
||||
return params;
|
||||
},
|
||||
properties: async () => {
|
||||
const props: Record<string, any> = {};
|
||||
@@ -285,20 +316,20 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
|
||||
if (base) props.record = base;
|
||||
// 当 view.inputArgs 带有 sourceId + associationName 时,提供“上级记录”变量(基于 sourceId 推断)
|
||||
try {
|
||||
const inputArgs = (ctx.view as any)?.inputArgs || {};
|
||||
const inputArgs = ctx.view?.inputArgs;
|
||||
const srcId = inputArgs?.sourceId;
|
||||
let assoc: string | undefined = inputArgs?.associationName;
|
||||
let dsKey: string = inputArgs?.dataSourceKey || 'main';
|
||||
|
||||
// 兜底:若 associationName 缺失或不含“.”,尝试从当前视图模型的 openView 参数推断
|
||||
if (!assoc || typeof assoc !== 'string' || !assoc.includes('.')) {
|
||||
const nav = (ctx.view as any)?.navigation;
|
||||
const nav = ctx.view?.navigation;
|
||||
const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
|
||||
const last = stack?.[stack.length - 1];
|
||||
if (last?.viewUid) {
|
||||
let model: any = ctx?.engine?.getModel?.(last.viewUid);
|
||||
let model = ctx?.engine?.getModel?.(last.viewUid) as PopupModelLike;
|
||||
if (!model) {
|
||||
model = await ctx.engine.loadModel({ uid: last.viewUid });
|
||||
model = (await ctx.engine.loadModel({ uid: last.viewUid })) as PopupModelLike;
|
||||
}
|
||||
const p = model?.getStepParams?.('popupSettings', 'openView') || {};
|
||||
assoc = p?.associationName || assoc;
|
||||
@@ -327,8 +358,8 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore
|
||||
} catch (err) {
|
||||
ctx.logger?.debug?.({ err }, '[FlowEngine] popup.properties: build sourceRecord failed');
|
||||
}
|
||||
const resourceMeta: PropertyMeta = {
|
||||
type: 'object',
|
||||
@@ -373,15 +404,15 @@ interface PopupNode {
|
||||
parent?: PopupNode;
|
||||
}
|
||||
|
||||
export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promise<PopupNode> {
|
||||
export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promise<PopupNode | undefined> {
|
||||
const nav = view?.navigation;
|
||||
const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
|
||||
const buildNode = async (idx: number): Promise<PopupNode | undefined> => {
|
||||
if (idx < 0 || !stack[idx]?.viewUid) return undefined;
|
||||
const viewUid = stack[idx].viewUid;
|
||||
let model: any = ctx.engine?.getModel?.(viewUid);
|
||||
if (!model && typeof ctx.engine?.loadModel === 'function') {
|
||||
model = await ctx.engine?.loadModel({ uid: viewUid });
|
||||
let model = ctx.engine?.getModel?.(viewUid) as PopupModelLike;
|
||||
if (!model) {
|
||||
model = (await ctx.engine?.loadModel({ uid: viewUid })) as PopupModelLike;
|
||||
}
|
||||
const p = model?.getStepParams?.('popupSettings', 'openView') || {};
|
||||
const collectionName = p?.collectionName;
|
||||
@@ -407,13 +438,22 @@ export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promi
|
||||
* 在视图上下文中注册 popup 变量(统一消除重复)
|
||||
*/
|
||||
export function registerPopupVariable(ctx: FlowContext, view: FlowView) {
|
||||
// - 顶层 record / sourceRecord 及其子字段
|
||||
// - 任意层级 parent.parent... 下的 record / sourceRecord 及其子字段
|
||||
const POPUP_SERVER_PATH_RE =
|
||||
/^(?:record|sourceRecord)(?:\.|$)|^parent(?:\.parent)*(?:\.(?:record|sourceRecord))(?:\.|$)/;
|
||||
// 始终注册 popup 变量:
|
||||
// - 若当前视图无可推断记录,仅在元信息中不呈现 record 字段;
|
||||
// - 但仍可依据 navigation 推断并展示上级弹窗信息。
|
||||
ctx.defineProperty('popup', {
|
||||
get: async () => buildPopupRuntime(ctx, view),
|
||||
meta: createPopupMeta(ctx),
|
||||
resolveOnServer: (p: string) =>
|
||||
p === 'record' || p?.startsWith('record.') || p === 'sourceRecord' || p?.startsWith('sourceRecord.'),
|
||||
resolveOnServer: (p: string) => {
|
||||
try {
|
||||
return !!p && POPUP_SERVER_PATH_RE.test(p);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -259,19 +259,20 @@ async function fetchRecordWithRequestCache(
|
||||
const json = rec ? rec.toJSON() : undefined;
|
||||
if (cache) cache.set(key, json);
|
||||
return json;
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
const log = koaCtx.app?.logger?.child({
|
||||
module: 'plugin-flow-engine',
|
||||
submodule: 'variables.resolve',
|
||||
method: 'fetchRecordWithRequestCache',
|
||||
});
|
||||
log?.debug('[variables.resolve] fetchRecordWithRequestCache error', {
|
||||
const errMsg = e instanceof Error ? e.message : String(e);
|
||||
log?.warn('[variables.resolve] fetchRecordWithRequestCache error', {
|
||||
ds: dataSourceKey,
|
||||
collection,
|
||||
tk: filterByTk,
|
||||
fields,
|
||||
appends,
|
||||
error: e?.message || String(e),
|
||||
error: errMsg,
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
@@ -321,7 +322,6 @@ function attachGenericRecordVariables(
|
||||
continue; // If top-level is record, nested processing under same varName is unnecessary
|
||||
}
|
||||
|
||||
// Nested record-like under varName
|
||||
// Group paths by first segment(支持首段后直接跟数字索引,如 record[0].name)
|
||||
const segmentMap = new Map<string, string[]>();
|
||||
const splitHead = (path: string): { seg: string; remainder: string } => {
|
||||
@@ -337,7 +337,6 @@ function attachGenericRecordVariables(
|
||||
const seg = m[1];
|
||||
const idxPart = m[2] || '';
|
||||
const tail = m[3] || '';
|
||||
// 若紧跟 [n],则把 [n] 保留到 remainder 中,seg 仅为标识符本体
|
||||
const remainder = (idxPart ? `${idxPart}${tail ? `.${tail}` : ''}` : tail) || '';
|
||||
return { seg, remainder };
|
||||
}
|
||||
@@ -354,9 +353,9 @@ function attachGenericRecordVariables(
|
||||
segmentMap.set(seg, arr);
|
||||
}
|
||||
|
||||
// Build a container sub-context lazily only if any child is record-like
|
||||
// 1) 一层 record:varName.seg 是记录
|
||||
const segEntries = Array.from(segmentMap.entries());
|
||||
const recordChildren = segEntries.filter(([seg]) => {
|
||||
const oneLevelRecordChildren = segEntries.filter(([seg]) => {
|
||||
const idx = parseIndexSegment(seg);
|
||||
const nestedObj =
|
||||
_.get(contextParams, [varName, seg]) ?? (idx ? _.get(contextParams, [varName, idx]) : undefined);
|
||||
@@ -364,31 +363,40 @@ function attachGenericRecordVariables(
|
||||
(contextParams || {})[`${varName}.${seg}`] ?? (idx ? (contextParams || {})[`${varName}.${idx}`] : undefined);
|
||||
return isRecordParams(nestedObj) || isRecordParams(dotted);
|
||||
});
|
||||
if (!recordChildren.length) continue;
|
||||
|
||||
// 2) 深层 record:varName.<a>.<b>[.<c>...] 是记录(如 popup.parent.record / popup.parent.parent.record)
|
||||
type RecordParams = { collection: string; filterByTk: unknown; dataSourceKey?: string };
|
||||
const deepRecordMap = new Map<string, RecordParams>(); // relativePath -> recordParams
|
||||
const cp = contextParams;
|
||||
if (cp && typeof cp === 'object') {
|
||||
const cpRec = cp as Record<string, unknown>;
|
||||
for (const key of Object.keys(cpRec)) {
|
||||
if (!key || (key !== varName && !key.startsWith(`${varName}.`))) continue;
|
||||
if (key === varName) continue;
|
||||
const val = cpRec[key];
|
||||
if (!isRecordParams(val)) continue;
|
||||
const relative = key.slice(varName.length + 1); // e.g. 'parent.record'
|
||||
if (!relative) continue;
|
||||
deepRecordMap.set(relative, val);
|
||||
}
|
||||
}
|
||||
|
||||
if (!oneLevelRecordChildren.length && deepRecordMap.size === 0) continue;
|
||||
|
||||
flowCtx.defineProperty(varName, {
|
||||
get: () => {
|
||||
const subContext = new ServerBaseContext();
|
||||
for (const [seg, remainders] of recordChildren) {
|
||||
const idx = parseIndexSegment(seg);
|
||||
const recordParams =
|
||||
_.get(contextParams, [varName, seg]) ??
|
||||
(idx ? _.get(contextParams, [varName, idx]) : undefined) ??
|
||||
(contextParams || {})[`${varName}.${seg}`] ??
|
||||
(idx ? (contextParams || {})[`${varName}.${idx}`] : undefined);
|
||||
// 优先使用本段的子路径;若解析不到任何子路径,则尝试从原始 usedPaths 回退计算一次(去掉首段 `${seg}.`)
|
||||
let effRemainders = remainders.filter((r) => !!r);
|
||||
if (!effRemainders.length) {
|
||||
const all = usedPaths
|
||||
.map((p) =>
|
||||
p.startsWith(`${seg}.`) ? p.slice(seg.length + 1) : p.startsWith(`${seg}[`) ? p.slice(seg.length) : '',
|
||||
)
|
||||
.filter((x) => !!x);
|
||||
if (all.length) effRemainders = all;
|
||||
}
|
||||
const { generatedAppends, generatedFields } = inferSelectsFromUsage(effRemainders, recordParams);
|
||||
const definitionKey = idx ?? seg; // define numeric index for [0] so lodash.get '[0]' resolves to '0'
|
||||
subContext.defineProperty(definitionKey, {
|
||||
const root = new ServerBaseContext();
|
||||
const definedFirstLevel = new Set<string>();
|
||||
|
||||
// Helper: define a record getter at container with given key
|
||||
const defineRecordGetter = (
|
||||
container: ServerBaseContext,
|
||||
key: string,
|
||||
recordParams: { collection: string; filterByTk: unknown; dataSourceKey?: string },
|
||||
subPaths: string[] = [],
|
||||
) => {
|
||||
const { generatedAppends, generatedFields } = inferSelectsFromUsage(subPaths, recordParams);
|
||||
container.defineProperty(key, {
|
||||
get: async () => {
|
||||
const dataSourceKey = recordParams?.dataSourceKey || 'main';
|
||||
return await fetchRecordWithRequestCache(
|
||||
@@ -402,8 +410,75 @@ function attachGenericRecordVariables(
|
||||
},
|
||||
cache: true,
|
||||
});
|
||||
};
|
||||
|
||||
// Helper: get or create sub container under ctx with given key
|
||||
const subContainers = new Map<ServerBaseContext, Map<string, ServerBaseContext>>();
|
||||
const ensureSubContainer = (parent: ServerBaseContext, key: string): ServerBaseContext => {
|
||||
let map = subContainers.get(parent);
|
||||
if (!map) {
|
||||
map = new Map();
|
||||
subContainers.set(parent, map);
|
||||
}
|
||||
let child = map.get(key);
|
||||
if (!child) {
|
||||
const inst = new ServerBaseContext();
|
||||
parent.defineProperty(key, { get: () => inst.createProxy(), cache: true });
|
||||
map.set(key, inst);
|
||||
child = inst;
|
||||
}
|
||||
return child;
|
||||
};
|
||||
|
||||
// First: handle one-level record children (varName.seg)
|
||||
for (const [seg, remainders] of oneLevelRecordChildren) {
|
||||
const idx = parseIndexSegment(seg);
|
||||
const recordParams =
|
||||
_.get(contextParams, [varName, seg]) ??
|
||||
(idx ? _.get(contextParams, [varName, idx]) : undefined) ??
|
||||
(contextParams || {})[`${varName}.${seg}`] ??
|
||||
(idx ? (contextParams || {})[`${varName}.${idx}`] : undefined);
|
||||
|
||||
let effRemainders = (remainders || []).filter((r) => !!r);
|
||||
if (!effRemainders.length) {
|
||||
const all = usedPaths
|
||||
.map((p) =>
|
||||
p.startsWith(`${seg}.`) ? p.slice(seg.length + 1) : p.startsWith(`${seg}[`) ? p.slice(seg.length) : '',
|
||||
)
|
||||
.filter((x) => !!x);
|
||||
if (all.length) effRemainders = all;
|
||||
}
|
||||
|
||||
defineRecordGetter(root, idx ?? seg, recordParams, effRemainders);
|
||||
definedFirstLevel.add(idx ?? seg);
|
||||
}
|
||||
return subContext.createProxy();
|
||||
|
||||
// Then: handle deep record children (varName.a.b[.c...])
|
||||
for (const [relative, recordParams] of deepRecordMap.entries()) {
|
||||
const segs = String(relative).split('.').filter(Boolean);
|
||||
if (segs.length === 0) continue;
|
||||
const first = segs[0];
|
||||
// Ensure first-level container exists, but avoid overriding previously defined first-level record getters
|
||||
let container: ServerBaseContext;
|
||||
if (definedFirstLevel.has(first)) {
|
||||
// 已定义为 record getter 的一层 key,无法作为容器复用;跳过(由上层 one-level 逻辑覆盖)。
|
||||
continue;
|
||||
} else {
|
||||
container = root;
|
||||
for (let i = 0; i < segs.length - 1; i++) {
|
||||
container = ensureSubContainer(container, segs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const leaf = segs[segs.length - 1];
|
||||
// 计算该记录下的使用子路径(相对 relative)
|
||||
const subPaths = (usedPaths || [])
|
||||
.map((p) => (p === relative ? '' : p.startsWith(relative + '.') ? p.slice(relative.length + 1) : ''))
|
||||
.filter((x) => x !== '');
|
||||
defineRecordGetter(container, leaf, recordParams, subPaths);
|
||||
}
|
||||
|
||||
return root.createProxy();
|
||||
},
|
||||
cache: true,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user