feat: add support for helper functions in variable configuration and update related components

This commit is contained in:
sheldon66 2025-03-23 11:33:05 +08:00
parent 9d5bb0a069
commit 4d88c083bb
7 changed files with 117 additions and 50 deletions

View File

@ -14,12 +14,14 @@ import { Dropdown, Tag } from 'antd';
import React from 'react'; import React from 'react';
import { useApp } from '../../../../application'; import { useApp } from '../../../../application';
import { useCompile } from '../../../hooks'; import { useCompile } from '../../../hooks';
import { isHelperAllowedForVariable, useVariable } from '../VariableProvider';
import { useHelperObservables } from './hooks/useHelperObservables'; import { useHelperObservables } from './hooks/useHelperObservables';
import { allHelpersConfigObs } from './observables'; import { allHelpersConfigObs } from './observables';
export const HelperAddition = observer(() => { export const HelperAddition = observer(() => {
const app = useApp(); const app = useApp();
const helperObservables = useHelperObservables(); const helperObservables = useHelperObservables();
const { isHelperAllowed } = useVariable();
const { addHelper } = helperObservables; const { addHelper } = helperObservables;
const compile = useCompile(); const compile = useCompile();
const filterOptions = app.jsonTemplateParser.filterGroups const filterOptions = app.jsonTemplateParser.filterGroups
@ -28,31 +30,35 @@ export const HelperAddition = observer(() => {
key: group.name, key: group.name,
type: 'group', type: 'group',
label: compile(group.title), label: compile(group.title),
children: group.filters children: group.helpers
.filter(({ name }) => isHelperAllowed([group.name, name].join('.')))
.sort((a, b) => a.sort - b.sort) .sort((a, b) => a.sort - b.sort)
.map((filter) => ({ key: filter.name, label: compile(filter.title) })), .map((filter) => ({ key: filter.name, label: compile(filter.title) })),
})) as MenuProps['items']; }))
.filter((group) => group.children.length > 0) as MenuProps['items'];
const items = allHelpersConfigObs.value.map((helper) => ({ const items = allHelpersConfigObs.value.map((helper) => ({
key: helper.name, key: helper.name,
label: helper.title, label: helper.title,
})); }));
if (filterOptions.length > 0) {
return ( return (
<> <>
<span style={{ color: '#bfbfbf', margin: '0 5px' }}>|</span> <span style={{ color: '#bfbfbf', margin: '0 5px' }}>|</span>
<Dropdown <Dropdown
menu={{ menu={{
items: filterOptions, items: filterOptions,
onClick: ({ key }) => { onClick: ({ key }) => {
addHelper({ name: key }); addHelper({ name: key });
}, },
}} }}
> >
<a onClick={(e) => e.preventDefault()}> <a onClick={(e) => e.preventDefault()}>
<FilterOutlined style={{ color: '#52c41a' }} /> <FilterOutlined style={{ color: '#52c41a' }} />
</a> </a>
</Dropdown> </Dropdown>
</> </>
); );
}
return null;
}); });

View File

@ -51,7 +51,7 @@ export function isFilterAllowedForVariable(
// But we escape the variable name since it's a literal value // But we escape the variable name since it's a literal value
if (minimatch(escapeGlob(variableName), rule.variable)) { if (minimatch(escapeGlob(variableName), rule.variable)) {
// Check if filter matches any of the allowed patterns // Check if filter matches any of the allowed patterns
return rule.filters.some((pattern) => minimatch(filterName, pattern)); return rule.helpers.some((pattern) => minimatch(filterName, pattern));
} }
} }

View File

@ -7,6 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { Helper } from '@nocobase/json-template-parser';
import { isArray } from 'lodash'; import { isArray } from 'lodash';
import minimatch from 'minimatch'; import minimatch from 'minimatch';
import React, { createContext, useContext, useEffect, useState } from 'react'; import React, { createContext, useContext, useEffect, useState } from 'react';
@ -15,6 +16,8 @@ import { useHelperObservables } from './Helpers/hooks/useHelperObservables';
interface VariableContextValue { interface VariableContextValue {
value: any; value: any;
helperObservables?: ReturnType<typeof useHelperObservables>; helperObservables?: ReturnType<typeof useHelperObservables>;
variableHelperMapping: VariableHelperMapping;
variableName: string;
} }
interface VariableProviderProps { interface VariableProviderProps {
@ -28,7 +31,7 @@ export interface VariableHelperRule {
/** Pattern to match variables, supports glob patterns */ /** Pattern to match variables, supports glob patterns */
variable: string; variable: string;
/** Array of allowed filter patterns, supports glob patterns */ /** Array of allowed filter patterns, supports glob patterns */
filters: string[]; helpers: string[];
} }
export interface VariableHelperMapping { export interface VariableHelperMapping {
@ -50,13 +53,13 @@ function escapeGlob(str: string): string {
/** /**
* Tests if a filter is allowed for a given variable based on the variableHelperMapping configuration * Tests if a filter is allowed for a given variable based on the variableHelperMapping configuration
* @param variableName The name of the variable to test * @param variableName The name of the variable to test
* @param filterName The name of the filter to test * @param helperName The name of the filter to test
* @param mapping The variable helper mapping configuration * @param mapping The variable helper mapping configuration
* @returns boolean indicating if the filter is allowed for the variable * @returns boolean indicating if the filter is allowed for the variable
*/ */
export function isFilterAllowedForVariable( export function isHelperAllowedForVariable(
variableName: string, variableName: string,
filterName: string, helperName: string,
mapping?: VariableHelperMapping, mapping?: VariableHelperMapping,
): boolean { ): boolean {
if (!mapping?.rules) { if (!mapping?.rules) {
@ -68,30 +71,31 @@ export function isFilterAllowedForVariable(
// Check if variable matches the pattern // Check if variable matches the pattern
// We don't escape the pattern since it's meant to be a glob pattern // We don't escape the pattern since it's meant to be a glob pattern
// But we escape the variable name since it's a literal value // But we escape the variable name since it's a literal value
if (minimatch(escapeGlob(variableName), rule.variable)) { const matched = minimatch(variableName, rule.variable);
if (matched) {
// Check if filter matches any of the allowed patterns // Check if filter matches any of the allowed patterns
return rule.filters.some((pattern) => minimatch(filterName, pattern)); return rule.helpers.some((pattern) => minimatch(helperName, pattern));
} }
} }
// If no matching rule found and strictMode is true, deny the filter // If no matching rule found and strictMode is true, deny the filter
return !mapping.strictMode; return false;
} }
/** /**
* Gets all supported filters for a given variable based on the mapping rules * Gets all supported filters for a given variable based on the mapping rules
* @param variableName The name of the variable to check * @param variableName The name of the variable to check
* @param mapping The variable helper mapping configuration * @param mapping The variable helper mapping configuration
* @param allFilters Array of all available filter names * @param allHelpers Array of all available filter names
* @returns Array of filter names that are allowed for the variable * @returns Array of filter names that are allowed for the variable
*/ */
export function getSupportedFiltersForVariable( export function getSupportedFiltersForVariable(
variableName: string, variableName: string,
mapping?: VariableHelperMapping, mapping?: VariableHelperMapping,
allFilters: string[] = [], allHelpers: Helper[] = [],
): string[] { ): Helper[] {
if (!mapping?.rules) { if (!mapping?.rules) {
return allFilters; // If no rules defined, all filters are allowed return allHelpers; // If no rules defined, all filters are allowed
} }
// Find matching rule for the variable // Find matching rule for the variable
@ -100,14 +104,18 @@ export function getSupportedFiltersForVariable(
if (!matchingRule) { if (!matchingRule) {
// If no matching rule and strictMode is true, return empty array // If no matching rule and strictMode is true, return empty array
// Otherwise return all filters // Otherwise return all filters
return mapping.strictMode ? [] : allFilters; return allHelpers;
} }
// Filter the allFilters array based on the matching rule's filter patterns // Filter the allFilters array based on the matching rule's filter patterns
return allFilters.filter((filterName) => matchingRule.filters.some((pattern) => minimatch(filterName, pattern))); return allHelpers.filter(({ name }) => matchingRule.helpers.some((pattern) => minimatch(name, pattern)));
} }
const VariableContext = createContext<VariableContextValue>({ value: null }); const VariableContext = createContext<VariableContextValue>({
variableName: '',
value: null,
variableHelperMapping: { rules: [] },
});
export function useCurrentVariable(): VariableContextValue { export function useCurrentVariable(): VariableContextValue {
const context = useContext(VariableContext); const context = useContext(VariableContext);
@ -117,7 +125,12 @@ export function useCurrentVariable(): VariableContextValue {
return context; return context;
} }
export const VariableProvider: React.FC<VariableProviderProps> = ({ variableName, children, helperObservables }) => { export const VariableProvider: React.FC<VariableProviderProps> = ({
variableName,
children,
helperObservables,
variableHelperMapping,
}) => {
const [value, setValue] = useState(null); const [value, setValue] = useState(null);
const variables = useVariables(); const variables = useVariables();
const localVariables = useLocalVariables(); const localVariables = useLocalVariables();
@ -134,5 +147,28 @@ export const VariableProvider: React.FC<VariableProviderProps> = ({ variableName
fetchValue(); fetchValue();
}, [localVariables, variableName, variables]); }, [localVariables, variableName, variables]);
return <VariableContext.Provider value={{ value, helperObservables }}>{children}</VariableContext.Provider>; return (
<VariableContext.Provider value={{ variableName, value, helperObservables, variableHelperMapping }}>
{children}
</VariableContext.Provider>
);
}; };
export function useVariable() {
const context = useContext(VariableContext);
const { value, variableName, variableHelperMapping } = context;
const isHelperAllowed = (filterName: string) => {
return isHelperAllowedForVariable(variableName, filterName, variableHelperMapping);
};
const getSupportedFilters = (allHelpers: Helper[]) => {
return getSupportedFiltersForVariable(variableName, variableHelperMapping, allHelpers);
};
return {
...context,
isHelperAllowed,
getSupportedFilters,
};
}

View File

@ -13,11 +13,19 @@ const schema = {
properties: { properties: {
input: { input: {
type: 'string', type: 'string',
title: `替换模式`, title: `输入项`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Variable.Input', 'x-component': 'Variable.Input',
'x-component-props': { 'x-component-props': {
scope, scope,
variableHelperMapping: {
rules: [
{
variable: '$date.*',
helpers: ['date.*'],
},
],
},
}, },
}, },
}, },

View File

@ -28,10 +28,27 @@ type ParseOptions = {
<code src="./demos/demo1.tsx"></code> <code src="./demos/demo1.tsx"></code>
### `Variable.TextArea` ### 支持 helper 助手函数
目前变量支持添加助手函数进行二次处理,不同的变量可能支持不同的助手函数,助手函数还支持分组。
Input 组件支持传入 variableHelperMapping 属性来标记变量支持哪些助手函数。
例如,日期变量只支持日期相关的助手函数,则可配置
```ts
const variableHelperMapping = {
rules: [
{
variable: '$date.*',
helpers: ['date.*'],
},
],
},
```
<code src="./demos/demo1.tsx"></code>
## `Variable.TextArea`
<code src="./demos/demo2.tsx"></code> <code src="./demos/demo2.tsx"></code>
### `Variable.JSON` ## `Variable.JSON`
<code src="./demos/demo3.tsx"></code> <code src="./demos/demo3.tsx"></code>

View File

@ -16,7 +16,7 @@ type FilterGroup = {
sort: number; sort: number;
}; };
type Filter = { type Helper = {
name: string; name: string;
title: string; title: string;
handler: (...args: any[]) => any; handler: (...args: any[]) => any;
@ -40,7 +40,7 @@ type ScopeMapValue = { fieldSet: Set<string>; scopeFnWrapper: ScopeFnWrapper; sc
export class JSONTemplateParser { export class JSONTemplateParser {
private _engine: Liquid; private _engine: Liquid;
private _filterGroups: Array<FilterGroup>; private _filterGroups: Array<FilterGroup>;
private _filters: Array<Filter>; private _filters: Array<Helper>;
constructor() { constructor() {
this._engine = new Liquid(); this._engine = new Liquid();
@ -48,7 +48,7 @@ export class JSONTemplateParser {
this._filters = []; this._filters = [];
} }
get filters(): Array<Filter> { get filters(): Array<Helper> {
return this._filters; return this._filters;
} }
@ -58,19 +58,19 @@ export class JSONTemplateParser {
get filterGroups(): Array< get filterGroups(): Array<
FilterGroup & { FilterGroup & {
filters: Array<Filter>; helpers: Array<Helper>;
} }
> { > {
return this._filterGroups.map((group) => ({ return this._filterGroups.map((group) => ({
...group, ...group,
filters: this._filters.filter((filter) => filter.group === group.name), helpers: this._filters.filter((filter) => filter.group === group.name),
})); }));
} }
registerFilterGroup(group: FilterGroup): void { registerFilterGroup(group: FilterGroup): void {
this._filterGroups.push(group); this._filterGroups.push(group);
} }
registerFilter(filter: Filter): void { registerFilter(filter: Helper): void {
this._filters.push(filter); this._filters.push(filter);
this._engine.registerFilter(filter.name, filter.handler); this._engine.registerFilter(filter.name, filter.handler);
} }

View File

@ -7,12 +7,12 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { escapeSpecialChars, escape, revertEscape } from '../escape'; import { escape, escapeSpecialChars, revertEscape } from '../escape';
import { createJSONTemplateParser } from '../parser'; import { createJSONTemplateParser } from '../parser';
const parser = createJSONTemplateParser(); const parser = createJSONTemplateParser();
const engine = parser.engine; const engine = parser.engine;
type Filter = { export type Helper = {
name: string; name: string;
handler: any; handler: any;
args: string[]; args: string[];
@ -31,7 +31,7 @@ export function extractTemplateVariable(template: string): string | null {
export function extractTemplateElements(template: string): { export function extractTemplateElements(template: string): {
fullVariable: string | null; fullVariable: string | null;
variableSegments: string[]; variableSegments: string[];
helpers: Filter[]; helpers: Helper[];
} { } {
const escapedTemplate = escape(template ?? ''); const escapedTemplate = escape(template ?? '');
try { try {
@ -52,7 +52,7 @@ export function extractTemplateElements(template: string): {
} }
} }
const composeFilterTemplate = (filter: Filter) => { const composeFilterTemplate = (filter: Helper) => {
const value = `${filter.name}${ const value = `${filter.name}${
filter.args.length ? `:${filter.args.map((val) => JSON.stringify(val)).join(',')}` : '' filter.args.length ? `:${filter.args.map((val) => JSON.stringify(val)).join(',')}` : ''
}`; }`;