feat: fix render variables

This commit is contained in:
Sheldon Guo 2025-03-18 02:57:53 +08:00
parent 1671484616
commit 89bb4c434a
8 changed files with 96 additions and 57 deletions

View File

@ -10,6 +10,7 @@
import { CloseCircleFilled } from '@ant-design/icons';
import { css, cx } from '@emotion/css';
import { observer, useForm } from '@formily/react';
import { reaction } from '@formily/reactive';
import { composeTemplate, extractTemplateElements } from '@nocobase/json-template-parser';
import { error } from '@nocobase/utils/client';
import {
@ -239,16 +240,22 @@ function _Input(props: VariableInputProps) {
);
const newVal = useMemo(() => composeTemplate({ fullVariable, helpers: helpersObs.value }), [fullVariable]);
useEffect(() => {
if (value && newVal !== value) {
onChange(newVal);
}
}, [newVal, value, onChange]);
const dispose = reaction(
() => {
return composeTemplate({ fullVariable, helpers: helpersObs.value });
},
(newVal) => {
onChange(newVal);
},
);
return dispose;
}, [fullVariable, onChange]);
const parsed = useMemo(() => parseValue(variableSegments, parseOptions), [parseOptions, variableSegments]);
const isConstant = typeof parsed === 'string';
const type = isConstant ? parsed : '';
const variable = isConstant ? null : parsed;
// const [prevType, setPrevType] = React.useState<string>(type);
const names = Object.assign(
{
label: 'label',

View File

@ -25,6 +25,18 @@ type Filter = {
sort: number;
};
type ScopeFnWrapperResult = {
getValue: (params: { field: string[]; keys: string[] }) => any;
afterApplyHelpers: (params: { field: string[]; keys: string[]; value: any }) => any;
};
type ScopeFnWrapper = (params: {
fields: string[];
data: any;
context?: Record<string, any>;
}) => Promise<ScopeFnWrapperResult>;
type ScopeMapValue = { fieldSet: Set<string>; scopeFnWrapper: ScopeFnWrapper; scopeFn?: ScopeFnWrapperResult };
export class JSONTemplateParser {
private _engine: Liquid;
private _filterGroups: Array<FilterGroup>;
@ -64,34 +76,38 @@ export class JSONTemplateParser {
}
async render(template: string, data: any = {}, context?: Record<string, any>): Promise<any> {
const NamespaceMap = new Map<string, { fieldSet: Set<String>; fnWrapper: Function; fn?: any }>();
const NamespaceMap = new Map<string, ScopeMapValue>();
Object.keys(data).forEach((key) => {
if (key.startsWith('$') || typeof data[key] === 'function') {
NamespaceMap.set(escape(key), { fieldSet: new Set<String>(), fnWrapper: data[key] });
NamespaceMap.set(escape(key), { fieldSet: new Set<string>(), scopeFnWrapper: data[key] });
}
});
const parsed = this.parse(template, { NamespaceMap });
const fnPromises = Array.from(NamespaceMap.entries()).map(async ([key, { fieldSet, fnWrapper }]) => {
const fields = Array.from(fieldSet);
if (fields.length === 0) {
return { key, fn: null };
}
const fn = await fnWrapper({ fields: Array.from(fields), data, context });
return { key, fn };
});
const fns = await Promise.all(fnPromises);
fns.forEach(({ key, fn }) => {
const fnPromises = Array.from(NamespaceMap.entries()).map(
async ([key, { fieldSet, scopeFnWrapper: fnWrapper }]): Promise<{
key: string;
scopeFn: ScopeFnWrapperResult;
} | null> => {
const fields = Array.from(fieldSet);
if (fields.length === 0) {
return null;
}
const hooks: Record<'afterApplyHelpers', any> = { afterApplyHelpers: null };
const scopeFn = await fnWrapper({ fields, data, context });
return { key, scopeFn };
},
);
const scopeFns = (await Promise.all(fnPromises)).filter(Boolean);
scopeFns.forEach(({ key, scopeFn }) => {
const NS = NamespaceMap.get(key);
NS.fn = fn;
NS.scopeFn = scopeFn;
});
return parsed(data);
}
parse = (
value: any,
opts?: { NamespaceMap: Map<string, { fieldSet: Set<String>; fnWrapper: Function; fn?: Function }> },
) => {
parse = (value: any, opts?: { NamespaceMap: Map<string, ScopeMapValue> }) => {
const engine = this.engine;
function type(value) {
let valueType: string = typeof value;
@ -177,22 +193,32 @@ export class JSONTemplateParser {
if (opts?.NamespaceMap && opts.NamespaceMap.has(template.variableSegments[0])) {
const scopeKey = template.variableSegments[0];
const NS = opts.NamespaceMap.get(scopeKey);
const fn = NS.fn;
const scopeFn = NS.scopeFn;
const field = getFieldName({
variableName: template.variableName,
variableSegments: template.variableSegments,
});
if (!fn) {
if (!scopeFn?.getValue) {
throw new Error(`fn not found for ${scopeKey}`);
}
return fn(revertEscape(field), preKeys);
value = scopeFn.getValue({ field: revertEscape(field), keys: preKeys });
const appliedHelpersValue = template.filters.reduce(
(acc, filter) => filter.handler(...[acc, ...filter.args]),
typeof value === 'function' ? value() : value,
);
if (scopeFn?.afterApplyHelpers) {
return scopeFn.afterApplyHelpers({ field: preKeys, keys: preKeys, value: appliedHelpersValue });
}
return appliedHelpersValue;
} else if (typeof ctxVal === 'function') {
const ctxVal = get(escapedContext, template.variableName);
value = ctxVal();
} else {
value = get(escapedContext, template.variableName);
}
template.filters.reduce(
return template.filters.reduce(
(acc, filter) => filter.handler(...[acc, ...filter.args]),
typeof value === 'function' ? value() : value,
);

View File

@ -25,6 +25,7 @@ import {
import { ResourceOptions, Resourcer } from '@nocobase/resourcer';
import { Telemetry, TelemetryOptions } from '@nocobase/telemetry';
import { createJSONTemplateParser, JSONTemplateParser } from '@nocobase/json-template-parser';
import { LockManager, LockManagerOptions } from '@nocobase/lock-manager';
import {
applyMixins,
@ -34,7 +35,6 @@ import {
ToposortOptions,
wrapMiddlewareWithLogging,
} from '@nocobase/utils';
import { createJSONTemplateParser, JSONTemplateParser } from '@nocobase/json-template-parser';
import { Command, CommandOptions, ParseOptions } from 'commander';
import { randomUUID } from 'crypto';
import glob from 'glob';
@ -64,7 +64,7 @@ import {
import { ApplicationVersion } from './helpers/application-version';
import { Locale } from './locale';
import { MainDataSource } from './main-data-source';
import { parseVariables } from './middlewares';
import { renderVariables } from './middlewares';
import { dataTemplate } from './middlewares/data-template';
import validateFilterParams from './middlewares/validate-filter-params';
import { Plugin } from './plugin';
@ -1260,8 +1260,8 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
this._dataSourceManager.use(this._authManager.middleware(), { tag: 'auth' });
this._dataSourceManager.use(validateFilterParams, { tag: 'validate-filter-params', before: ['auth'] });
this._dataSourceManager.use(parseVariables, {
group: 'parseVariables',
this._dataSourceManager.use(renderVariables, {
group: 'renderVariables',
after: 'acl',
});

View File

@ -10,3 +10,4 @@
export * from './data-wrapping';
export * from './extract-client-ip';
export { parseVariables } from './parse-variables';
export { renderVariables } from './render-variables';

View File

@ -8,11 +8,11 @@
*/
import { createJSONTemplateParser } from '@nocobase/json-template-parser';
import { getDateVars } from '@nocobase/utils';
import { getDateVars, isDate } from '@nocobase/utils';
import { get } from 'lodash';
function getUser(ctx) {
return async ({ fields }) => {
const getValue = async ({ fields }) => {
const userFields = fields.filter((f) => f && ctx.db.getFieldByPath('users.' + f));
ctx.logger?.info('filter-parse: ', { userFields });
if (!ctx.state.currentUser) {
@ -32,13 +32,9 @@ function getUser(ctx) {
get(user, field);
};
};
return { getValue };
}
function isNumeric(str: any) {
if (typeof str === 'number') return true;
if (typeof str != 'string') return false;
return !isNaN(str as any) && !isNaN(parseFloat(str));
}
const isDateOperator = (op) => {
return [
'$dateOn',
@ -51,10 +47,6 @@ const isDateOperator = (op) => {
].includes(op);
};
function isDate(input) {
return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
}
const dateValueWrapper = (value: any, timezone?: string) => {
if (!value) {
return null;
@ -70,7 +62,7 @@ const dateValueWrapper = (value: any, timezone?: string) => {
}
if (typeof value === 'string') {
if (!timezone || /(\+|-)\d\d:\d\d$/.test(value)) {
if (!timezone || /((\+|-)\d\d:\d\d|Z)$/.test(value)) {
return value;
}
return value + timezone;
@ -84,17 +76,24 @@ const dateValueWrapper = (value: any, timezone?: string) => {
const $date = ({ fields, data, context }) => {
const timezone = context.timezone;
const dateVars = getDateVars();
return (field, keys) => {
const getValue = ({ field, keys }) => {
const value = get(dateVars, field);
return value;
};
const afterApplyHelpers = ({ field, value, keys }) => {
const operator = keys[keys.length - 1];
if (isDateOperator(operator)) {
const field = context?.getField?.(keys);
if (field?.constructor.name === 'DateOnlyField' || field?.constructor.name === 'DatetimeNoTzField') {
const ctxField = context?.getField?.(keys);
if (ctxField?.constructor.name === 'DateOnlyField' || ctxField?.constructor.name === 'DatetimeNoTzField') {
return value;
}
return dateValueWrapper(value, field?.timezone || timezone);
const result = dateValueWrapper(value, ctxField?.timezone || timezone);
return result;
}
return value;
};
return { getValue, afterApplyHelpers };
};
const parser = createJSONTemplateParser();
@ -104,23 +103,25 @@ export async function renderVariables(ctx, next) {
if (!filter) {
return next();
}
ctx.action.params.filter = await parser.render(
const renderedFilter = await parser.render(
filter,
{
$user: getUser(ctx),
$date,
$nDate: $date,
$nRole: ctx.state.currentRole,
$nRole: ctx.state.currentRole === '__union__' ? ctx.state.currentRoles : ctx.state.currentRole,
},
{
timezone: ctx.get('x-timezone'),
now: new Date().toISOString(),
getField: (keys) => {
const fieldPath = keys.filter((p) => !p.startsWith('$') && !isNumeric(p)).join('.');
const fieldPath = keys.filter((p) => typeof p === 'string' && !p.startsWith('$')).join('.');
const { resourceName } = ctx.action;
return ctx.db.getFieldByPath(`${resourceName}.${fieldPath}`);
const field = ctx.db.getFieldByPath(`${resourceName}.${fieldPath}`);
return field;
},
},
);
ctx.action.params.filter = renderedFilter;
await next();
}

View File

@ -235,3 +235,7 @@ export const getDateTimeFormat = (picker, format, showTime, timeFormat) => {
}
return format;
};
export function isDate(input) {
return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]' || dayjs.isDayjs(input);
}

View File

@ -7,11 +7,11 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { extractTemplateElements } from '@nocobase/json-template-parser';
import _ from 'lodash';
import set from 'lodash/set';
import moment from 'moment';
import { extractTemplateElements } from '@nocobase/json-template-parser';
import { offsetFromString } from './date';
import { isDate, offsetFromString } from './date';
import { dayjs } from './dayjs';
import { getValuesByPath } from './getValuesByPath';
@ -100,10 +100,6 @@ const isDateOperator = (op) => {
].includes(op);
};
function isDate(input) {
return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
}
const dateValueWrapper = (value: any, timezone?: string) => {
if (!value) {
return null;

View File

@ -63,8 +63,12 @@ export function dateSubtract(initialValue: any, number: number, unit: any) {
return dayjs.isDayjs(value) ? value.subtract(number, unit) : dayjs(value).subtract(number, unit);
};
if (Array.isArray(initialValue)) {
return initialValue.map(handler);
const results = initialValue.map(handler);
console.log(results[0].toISOString());
return results;
} else {
const result = handler(initialValue);
console.log(result.toISOString());
return handler(initialValue);
}
}