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

View File

@ -51,7 +51,7 @@ export function isFilterAllowedForVariable(
// But we escape the variable name since it's a literal value
if (minimatch(escapeGlob(variableName), rule.variable)) {
// 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.
*/
import { Helper } from '@nocobase/json-template-parser';
import { isArray } from 'lodash';
import minimatch from 'minimatch';
import React, { createContext, useContext, useEffect, useState } from 'react';
@ -15,6 +16,8 @@ import { useHelperObservables } from './Helpers/hooks/useHelperObservables';
interface VariableContextValue {
value: any;
helperObservables?: ReturnType<typeof useHelperObservables>;
variableHelperMapping: VariableHelperMapping;
variableName: string;
}
interface VariableProviderProps {
@ -28,7 +31,7 @@ export interface VariableHelperRule {
/** Pattern to match variables, supports glob patterns */
variable: string;
/** Array of allowed filter patterns, supports glob patterns */
filters: string[];
helpers: string[];
}
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
* @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
* @returns boolean indicating if the filter is allowed for the variable
*/
export function isFilterAllowedForVariable(
export function isHelperAllowedForVariable(
variableName: string,
filterName: string,
helperName: string,
mapping?: VariableHelperMapping,
): boolean {
if (!mapping?.rules) {
@ -68,30 +71,31 @@ export function isFilterAllowedForVariable(
// Check if variable matches the 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
if (minimatch(escapeGlob(variableName), rule.variable)) {
const matched = minimatch(variableName, rule.variable);
if (matched) {
// 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
return !mapping.strictMode;
return false;
}
/**
* Gets all supported filters for a given variable based on the mapping rules
* @param variableName The name of the variable to check
* @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
*/
export function getSupportedFiltersForVariable(
variableName: string,
mapping?: VariableHelperMapping,
allFilters: string[] = [],
): string[] {
allHelpers: Helper[] = [],
): Helper[] {
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
@ -100,14 +104,18 @@ export function getSupportedFiltersForVariable(
if (!matchingRule) {
// If no matching rule and strictMode is true, return empty array
// Otherwise return all filters
return mapping.strictMode ? [] : allFilters;
return allHelpers;
}
// 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 {
const context = useContext(VariableContext);
@ -117,7 +125,12 @@ export function useCurrentVariable(): VariableContextValue {
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 variables = useVariables();
const localVariables = useLocalVariables();
@ -134,5 +147,28 @@ export const VariableProvider: React.FC<VariableProviderProps> = ({ variableName
fetchValue();
}, [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: {
input: {
type: 'string',
title: `替换模式`,
title: `输入项`,
'x-decorator': 'FormItem',
'x-component': 'Variable.Input',
'x-component-props': {
scope,
variableHelperMapping: {
rules: [
{
variable: '$date.*',
helpers: ['date.*'],
},
],
},
},
},
},

View File

@ -28,10 +28,27 @@ type ParseOptions = {
<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>
### `Variable.JSON`
## `Variable.JSON`
<code src="./demos/demo3.tsx"></code>

View File

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

View File

@ -7,12 +7,12 @@
* 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';
const parser = createJSONTemplateParser();
const engine = parser.engine;
type Filter = {
export type Helper = {
name: string;
handler: any;
args: string[];
@ -31,7 +31,7 @@ export function extractTemplateVariable(template: string): string | null {
export function extractTemplateElements(template: string): {
fullVariable: string | null;
variableSegments: string[];
helpers: Filter[];
helpers: Helper[];
} {
const escapedTemplate = escape(template ?? '');
try {
@ -52,7 +52,7 @@ export function extractTemplateElements(template: string): {
}
}
const composeFilterTemplate = (filter: Filter) => {
const composeFilterTemplate = (filter: Helper) => {
const value = `${filter.name}${
filter.args.length ? `:${filter.args.map((val) => JSON.stringify(val)).join(',')}` : ''
}`;