From 4d88c083bba7ae17ad5ab3a8f6883dfe707d881c Mon Sep 17 00:00:00 2001 From: sheldon66 Date: Sun, 23 Mar 2025 11:33:05 +0800 Subject: [PATCH] feat: add support for helper functions in variable configuration and update related components --- .../antd/variable/Helpers/HelperAddition.tsx | 46 +++++++------ .../variable/Helpers/HelperConfiguator.tsx | 2 +- .../antd/variable/VariableProvider.tsx | 68 ++++++++++++++----- .../antd/variable/demos/demo1.tsx | 10 ++- .../schema-component/antd/variable/index.md | 21 +++++- .../src/parser/json-template-parser.ts | 12 ++-- .../json-template-parser/src/utils/index.ts | 8 +-- 7 files changed, 117 insertions(+), 50 deletions(-) diff --git a/packages/core/client/src/schema-component/antd/variable/Helpers/HelperAddition.tsx b/packages/core/client/src/schema-component/antd/variable/Helpers/HelperAddition.tsx index 44d611fd14..37ba1e8534 100644 --- a/packages/core/client/src/schema-component/antd/variable/Helpers/HelperAddition.tsx +++ b/packages/core/client/src/schema-component/antd/variable/Helpers/HelperAddition.tsx @@ -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 ( - <> - | - { - addHelper({ name: key }); - }, - }} - > - e.preventDefault()}> - - - - - ); + if (filterOptions.length > 0) { + return ( + <> + | + { + addHelper({ name: key }); + }, + }} + > + e.preventDefault()}> + + + + + ); + } + return null; }); diff --git a/packages/core/client/src/schema-component/antd/variable/Helpers/HelperConfiguator.tsx b/packages/core/client/src/schema-component/antd/variable/Helpers/HelperConfiguator.tsx index 38e893f3ac..7cf25c2bb9 100644 --- a/packages/core/client/src/schema-component/antd/variable/Helpers/HelperConfiguator.tsx +++ b/packages/core/client/src/schema-component/antd/variable/Helpers/HelperConfiguator.tsx @@ -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)); } } diff --git a/packages/core/client/src/schema-component/antd/variable/VariableProvider.tsx b/packages/core/client/src/schema-component/antd/variable/VariableProvider.tsx index 3e311593b4..3f2fd95f68 100644 --- a/packages/core/client/src/schema-component/antd/variable/VariableProvider.tsx +++ b/packages/core/client/src/schema-component/antd/variable/VariableProvider.tsx @@ -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; + 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({ value: null }); +const VariableContext = createContext({ + 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 = ({ variableName, children, helperObservables }) => { +export const VariableProvider: React.FC = ({ + variableName, + children, + helperObservables, + variableHelperMapping, +}) => { const [value, setValue] = useState(null); const variables = useVariables(); const localVariables = useLocalVariables(); @@ -134,5 +147,28 @@ export const VariableProvider: React.FC = ({ variableName fetchValue(); }, [localVariables, variableName, variables]); - return {children}; + return ( + + {children} + + ); }; + +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, + }; +} diff --git a/packages/core/client/src/schema-component/antd/variable/demos/demo1.tsx b/packages/core/client/src/schema-component/antd/variable/demos/demo1.tsx index 9c834d3e0b..14e4ef2b57 100644 --- a/packages/core/client/src/schema-component/antd/variable/demos/demo1.tsx +++ b/packages/core/client/src/schema-component/antd/variable/demos/demo1.tsx @@ -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.*'], + }, + ], + }, }, }, }, diff --git a/packages/core/client/src/schema-component/antd/variable/index.md b/packages/core/client/src/schema-component/antd/variable/index.md index 5d6a9d9482..1f70d4b3dc 100644 --- a/packages/core/client/src/schema-component/antd/variable/index.md +++ b/packages/core/client/src/schema-component/antd/variable/index.md @@ -28,10 +28,27 @@ type ParseOptions = { -### `Variable.TextArea` +### 支持 helper 助手函数 +目前变量支持添加助手函数进行二次处理,不同的变量可能支持不同的助手函数,助手函数还支持分组。 +Input 组件支持传入 variableHelperMapping 属性来标记变量支持哪些助手函数。 +例如,日期变量只支持日期相关的助手函数,则可配置 +```ts +const variableHelperMapping = { + rules: [ + { + variable: '$date.*', + helpers: ['date.*'], + }, + ], + }, + +``` + + +## `Variable.TextArea` -### `Variable.JSON` +## `Variable.JSON` diff --git a/packages/core/json-template-parser/src/parser/json-template-parser.ts b/packages/core/json-template-parser/src/parser/json-template-parser.ts index 8af994d9ee..112260a9ef 100644 --- a/packages/core/json-template-parser/src/parser/json-template-parser.ts +++ b/packages/core/json-template-parser/src/parser/json-template-parser.ts @@ -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; scopeFnWrapper: ScopeFnWrapper; sc export class JSONTemplateParser { private _engine: Liquid; private _filterGroups: Array; - private _filters: Array; + private _filters: Array; constructor() { this._engine = new Liquid(); @@ -48,7 +48,7 @@ export class JSONTemplateParser { this._filters = []; } - get filters(): Array { + get filters(): Array { return this._filters; } @@ -58,19 +58,19 @@ export class JSONTemplateParser { get filterGroups(): Array< FilterGroup & { - filters: Array; + helpers: Array; } > { 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); } diff --git a/packages/core/json-template-parser/src/utils/index.ts b/packages/core/json-template-parser/src/utils/index.ts index 7f16a21f86..3d65931d14 100644 --- a/packages/core/json-template-parser/src/utils/index.ts +++ b/packages/core/json-template-parser/src/utils/index.ts @@ -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(',')}` : '' }`;