mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-08 15:09:27 +08:00
feat: fix render variables
This commit is contained in:
parent
1671484616
commit
89bb4c434a
@ -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',
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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',
|
||||
});
|
||||
|
||||
|
@ -10,3 +10,4 @@
|
||||
export * from './data-wrapping';
|
||||
export * from './extract-client-ip';
|
||||
export { parseVariables } from './parse-variables';
|
||||
export { renderVariables } from './render-variables';
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user