From 22a9f2bd6e44af8afd100bd420b43c0c34bf1e2a Mon Sep 17 00:00:00 2001 From: sheldon66 Date: Thu, 20 Mar 2025 21:27:54 +0800 Subject: [PATCH] feat: variable supported helpers --- .../variable-filter-mapping.md | 158 ++++++++++++++++++ .../variable/Helpers/HelperConfiguator.tsx | 42 +++++ .../schema-component/antd/variable/Input.tsx | 19 ++- .../antd/variable/VariableProvider.tsx | 2 +- .../VariableInput/VariableInput.tsx | 53 ++++++ 5 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 docs/zh-CN/developer/filter-operators/variable-filter-mapping.md diff --git a/docs/zh-CN/developer/filter-operators/variable-filter-mapping.md b/docs/zh-CN/developer/filter-operators/variable-filter-mapping.md new file mode 100644 index 0000000000..8287ec3a1f --- /dev/null +++ b/docs/zh-CN/developer/filter-operators/variable-filter-mapping.md @@ -0,0 +1,158 @@ +# VariableFilterMapping 使用指南 + +## 概述 + +VariableFilterMapping 是 NocoBase 中一个用于处理变量助手条件映射的工具类。它允许开发者定义变量与助手条件之间的映射关系,使系统能够根据变量值动态生成数据查询条件。 + +## 主要功能 + +- 将变量值转换为数据查询条件 +- 支持多种助手操作符 +- 处理不同类型的变量(字符串、数字、日期等) +- 支持自定义变量处理器 + +## 基本用法 + +### 1. 定义助手条件映射 + +```typescript +import { VariableFilterMapping } from '@nocobase/database'; + +const helperMapping = new VariableFilterMapping({ + 'user.id': '$user.id', // 映射用户ID + 'user.name': '$user.name', // 映射用户名 + 'created_at': '$dateRange', // 映射日期范围 + 'status': '$status' // 映射状态 +}); +``` + +### 2. 使用映射生成助手条件 + +```typescript +// 变量值 +const variables = { + '$user.id': 1, + '$user.name': 'admin', + '$dateRange': ['2023-01-01', '2023-12-31'], + '$status': 'active' +}; + +// 生成助手条件 +const helper = helperMapping.toHelper(variables); +``` + +生成的助手条件类似于: + +```javascript +{ + 'user.id': 1, + 'user.name': 'admin', + 'created_at': { + $between: ['2023-01-01', '2023-12-31'] + }, + 'status': 'active' +} +``` + +## 高级用法 + +### 1. 自定义操作符 + +```typescript +const helperMapping = new VariableFilterMapping({ + 'price': { + name: '$price', + operator: '$gt' // 使用大于操作符 + } +}); + +const helper = helperMapping.toHelper({ + '$price': 100 +}); + +// 生成: { price: { $gt: 100 } } +``` + +### 2. 嵌套映射 + +```typescript +const helperMapping = new VariableFilterMapping({ + 'product': { + 'price': { + name: '$product.price', + operator: '$between' + } + } +}); + +const helper = helperMapping.toHelper({ + '$product.price': [100, 200] +}); + +// 生成: { product: { price: { $between: [100, 200] } } } +``` + +### 3. 使用处理器 + +```typescript +const helperMapping = new VariableFilterMapping({ + 'tags': { + name: '$tags', + operator: '$match', + processor: (value) => { + // 将逗号分隔的标签转换为数组 + return value.split(','); + } + } +}); + +const helper = helperMapping.toHelper({ + '$tags': 'javascript,typescript,react' +}); + +// 生成: { tags: { $match: ['javascript', 'typescript', 'react'] } } +``` + +## 实际应用场景 + +1. **数据表格筛选**:根据用户界面中的筛选选项动态生成查询条件 +2. **权限控制**:基于当前用户信息生成数据访问限制 +3. **报表生成**:使用用户选择的参数动态生成报表数据查询条件 +4. **工作流触发条件**:定义基于变量的工作流触发条件 + +## 注意事项 + +- 变量名必须以 `$` 开头 +- 确保变量值与期望的操作符兼容(例如,$between 操作符需要数组值) +- 注意处理变量值为 null 或 undefined 的情况 +- 复杂的助手逻辑可能需要使用处理器函数进行转换 + +## API 参考 + +### VariableFilterMapping 构造函数 + +```typescript +constructor(options: Record) +``` + +### MappingOptions 接口 + +```typescript +interface MappingOptions { + name: string; // 变量名 + operator?: string; // 操作符名称 + processor?: (value) => any; // 值处理器 +} +``` + +### toHelper 方法 + +```typescript +toHelper(variables: Record): Record +``` + +将变量值转换为助手条件。 + +## 总结 + +VariableFilterMapping 是 NocoBase 中强大的查询条件生成工具,它通过定义变量与助手条件的映射关系,使应用能够基于动态变量生成灵活的数据查询条件。掌握这一工具可以帮助开发者更有效地实现动态数据筛选、权限控制和自定义业务逻辑。 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 15008abcb2..ffaed4e985 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 @@ -19,6 +19,48 @@ import { useApp } from '../../../../application'; import { SchemaComponent } from '../../../core/SchemaComponent'; import { useVariable } from '../VariableProvider'; import { helpersObs, rawHelpersObs, removeHelper } from './observables'; +import { VariableHelperMapping } from '../Input'; +import minimatch from 'minimatch'; + +/** + * Escapes special glob characters in a string + * @param str The string to escape + * @returns The escaped string + */ +function escapeGlob(str: string): string { + return str.replace(/[?*[\](){}!|+@\\]/g, '\\$&'); +} + +/** + * 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 mapping The variable helper mapping configuration + * @returns boolean indicating if the filter is allowed for the variable + */ +export function isFilterAllowedForVariable( + variableName: string, + filterName: string, + mapping?: VariableHelperMapping, +): boolean { + if (!mapping?.rules) { + return true; // If no rules defined, allow all filters + } + + // Check each rule + for (const rule of mapping.rules) { + // 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.variables)) { + // Check if filter matches any of the allowed patterns + return rule.filters.some((pattern) => minimatch(filterName, pattern)); + } + } + + // If no matching rule found and strictMode is true, deny the filter + return !mapping.strictMode; +} export const HelperConfiguator = observer( ({ index, close }: { index: number; close: () => void }) => { diff --git a/packages/core/client/src/schema-component/antd/variable/Input.tsx b/packages/core/client/src/schema-component/antd/variable/Input.tsx index 8818fb76f4..fa4004a5e2 100644 --- a/packages/core/client/src/schema-component/antd/variable/Input.tsx +++ b/packages/core/client/src/schema-component/antd/variable/Input.tsx @@ -12,7 +12,6 @@ import { css, cx } from '@emotion/css'; import { autorun } from '@formily/reactive'; import { useForm, observer } from '@formily/react'; import { error } from '@nocobase/utils/client'; -import { cloneDeep } from 'lodash'; import { extractTemplateElements, composeTemplate } from '@nocobase/json-template-parser'; import { Input as AntInput, @@ -46,6 +45,23 @@ type ParseOptions = { stringToDate?: boolean; }; +/** + * Configuration for mapping variables to their allowed filter functions + */ +interface VariableHelperRule { + /** Pattern to match variables, supports glob patterns */ + variables: string; + /** Array of allowed filter patterns, supports glob patterns */ + filters: string[]; +} + +interface VariableHelperMapping { + /** Array of rules defining which filters are allowed for which variables */ + rules: VariableHelperRule[]; + /** Optional flag to determine if unlisted combinations should be allowed */ + strictMode?: boolean; +} + function parseValue(value: any, options: ParseOptions = {}): string | string[] { if (value == null || (Array.isArray(value) && value.length === 0)) { return 'null'; @@ -200,6 +216,7 @@ export type VariableInputProps = { className?: string; parseOptions?: ParseOptions; hideVariableButton?: boolean; + variableHelperMapping?: VariableHelperMapping; }; export function Input(props: VariableInputProps) { 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 03f1a405ee..f14d75727c 100644 --- a/packages/core/client/src/schema-component/antd/variable/VariableProvider.tsx +++ b/packages/core/client/src/schema-component/antd/variable/VariableProvider.tsx @@ -14,7 +14,7 @@ interface VariableContextValue { value: any; } -interface eProviderProps { +interface VariableProviderProps { variableName: string; children: React.ReactNode; } diff --git a/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx b/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx index 6c263799f5..492329183a 100644 --- a/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx +++ b/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx @@ -25,6 +25,23 @@ import { useCurrentUserVariable } from './hooks/useUserVariable'; import { useVariableOptions } from './hooks/useVariableOptions'; import { Option } from './type'; +/** + * Configuration for mapping variables to their allowed filter functions + */ +interface VariableFilterRule { + /** Pattern to match variables, supports glob patterns */ + variables: string; + /** Array of allowed filter patterns, supports glob patterns */ + filters: string[]; +} + +interface VariableFilterMapping { + /** Array of rules defining which filters are allowed for which variables */ + rules: VariableFilterRule[]; + /** Optional flag to determine if unlisted combinations should be allowed */ + strictMode?: boolean; +} + interface GetShouldChangeProps { collectionField: CollectionFieldOptions_deprecated; variables: VariablesContextType; @@ -43,6 +60,8 @@ type Props = { onChange: (value: any, optionPath?: any[]) => void; renderSchemaComponent: (props: RenderSchemaComponentProps) => any; schema?: any; + /** Configuration for mapping variables to their allowed filter functions */ + variableFilterMapping?: VariableFilterMapping; /** 消费变量值的字段 */ targetFieldSchema?: Schema; children?: any; @@ -344,3 +363,37 @@ export function useCompatOldVariables(props: { return { compatOldVariables }; } + +/** + * Check if a variable and filter combination is allowed according to the mapping rules + */ +function isFilterAllowedForVariable( + variable: string, + filter: string, + rules: VariableFilterRule[], + strictMode = false, +): boolean { + // If no rules defined and not in strict mode, allow everything + if (!rules?.length && !strictMode) { + return true; + } + + for (const rule of rules) { + const variablePattern = new RegExp('^' + rule.variables.replace(/\*/g, '.*') + '$'); + if (variablePattern.test(variable)) { + // If no filters defined for this rule, allow all helpers + if (!rule.filters?.length) { + return true; + } + + // Check if any of the filter patterns match the helper + return rule.filters.some((filter) => { + const filterPattern = new RegExp('^' + filter.replace(/\*/g, '.*') + '$'); + return filterPattern.test(filter); + }); + } + } + + // If no matching rules found, return !strictMode + return !strictMode; +}