From 0a687d0791d6df567a243a0c5da44cc8afff879c Mon Sep 17 00:00:00 2001 From: Sheldon Guo Date: Tue, 25 Mar 2025 18:18:57 +0800 Subject: [PATCH] feat: add helper type inference --- .../variable/Helpers/observables/index.ts | 9 +-- .../schema-component/antd/variable/Input.tsx | 6 +- .../antd/variable/VariableProvider.tsx | 57 ++++++--------- .../antd/variable/demos/date-scope.tsx | 72 +++++++++++++++++++ .../antd/variable/demos/demo1.tsx | 10 +-- .../client/src/variables/context/index.tsx | 21 ++++-- .../core/json-template-parser/src/index.ts | 7 +- .../core/json-template-parser/src/types.ts | 21 ++++++ .../json-template-parser/src/utils/index.ts | 7 +- .../src/json-template-helpers/index.ts | 18 ++++- 10 files changed, 164 insertions(+), 64 deletions(-) create mode 100644 packages/core/client/src/schema-component/antd/variable/demos/date-scope.tsx create mode 100644 packages/core/json-template-parser/src/types.ts diff --git a/packages/core/client/src/schema-component/antd/variable/Helpers/observables/index.ts b/packages/core/client/src/schema-component/antd/variable/Helpers/observables/index.ts index 5f4424ff1b..a4863fa5cf 100644 --- a/packages/core/client/src/schema-component/antd/variable/Helpers/observables/index.ts +++ b/packages/core/client/src/schema-component/antd/variable/Helpers/observables/index.ts @@ -8,14 +8,11 @@ */ import { observable } from '@formily/reactive'; -import { extractTemplateElements, createJSONTemplateParser } from '@nocobase/json-template-parser'; +import { Helper as _Helper, createJSONTemplateParser, extractTemplateElements } from '@nocobase/json-template-parser'; -type Helper = { - name: string; +type Helper = _Helper & { argsMap: Record; - config: any; - args: string[]; - handler: (value: any, ...args: any[]) => any; + config: _Helper; }; type RawHelper = { 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 4abae27141..d369c0e8dd 100644 --- a/packages/core/client/src/schema-component/antd/variable/Input.tsx +++ b/packages/core/client/src/schema-component/antd/variable/Input.tsx @@ -230,6 +230,7 @@ function _Input(props: VariableInputProps) { const { t } = useTranslation(); const form = useForm(); const [options, setOptions] = React.useState([]); + const [variableType, setVariableType] = React.useState(); const [variableText, setVariableText] = React.useState([]); const [isFieldValue, setIsFieldValue] = React.useState( hideVariableButton || (children && value != null ? true : false), @@ -382,6 +383,9 @@ function _Input(props: VariableInputProps) { return; } onChange(`{{${next.join('.')}}}`, optionPath); + if (Array.isArray(optionPath) && optionPath.length > 0) { + setVariableType(optionPath[optionPath.length - 1]?.type ?? null); + } }, [type, variable, onChange], ); @@ -487,7 +491,7 @@ 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 a45d5d6310..ad036fb329 100644 --- a/packages/core/client/src/schema-component/antd/variable/VariableProvider.tsx +++ b/packages/core/client/src/schema-component/antd/variable/VariableProvider.tsx @@ -8,6 +8,7 @@ */ import { reaction } from '@formily/reactive'; +import { observer } from '@formily/reactive-react'; import { composeTemplate, extractTemplateElements, Helper } from '@nocobase/json-template-parser'; import { get, isArray } from 'lodash'; import minimatch from 'minimatch'; @@ -19,14 +20,15 @@ import { useHelperObservables } from './Helpers/hooks/useHelperObservables'; interface VariableContextValue { value: any; helperObservables?: ReturnType; - variableHelperMapping: VariableHelperMapping; + variableType: string; + valueType: string; variableName: string; } interface VariableProviderProps { variableName: string; + variableType: string | null; children: React.ReactNode; - variableHelperMapping?: VariableHelperMapping; helperObservables?: ReturnType; onVariableTemplateChange?: (val) => void; } @@ -61,27 +63,11 @@ function escapeGlob(str: string): string { * @param mapping The variable helper mapping configuration * @returns boolean indicating if the filter is allowed for the variable */ -export function isHelperAllowedForVariable( - variableName: string, - helperName: string, - mapping?: VariableHelperMapping, -): boolean { - if (!mapping?.rules) { - return true; // If no rules defined, allow all filters +export function isHelperAllowedForVariable(helperName: string, valueType: string): boolean { + if (valueType) { + const matched = minimatch(helperName, `${valueType}.*`); + return matched; } - - // 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 - const matched = minimatch(variableName, rule.variable); - if (matched) { - // Check if filter matches any of the allowed patterns - return rule.helpers.some((pattern) => minimatch(helperName, pattern)); - } - } - // If no matching rule found and strictMode is true, deny the filter return false; } @@ -118,7 +104,8 @@ export function getSupportedFiltersForVariable( const VariableContext = createContext({ variableName: '', value: null, - variableHelperMapping: { rules: [] }, + variableType: null, + valueType: '', }); export function useCurrentVariable(): VariableContextValue { @@ -129,10 +116,10 @@ export function useCurrentVariable(): VariableContextValue { return context; } -export const VariableProvider: React.FC = ({ +const _VariableProvider: React.FC = ({ variableName, children, - variableHelperMapping, + variableType, onVariableTemplateChange, }) => { const [value, setValue] = useState(null); @@ -171,28 +158,30 @@ export const VariableProvider: React.FC = ({ fetchValue(); }, [localVariables, variableName, variables]); + const valueType = + helperObservables.helpersObs.value.length > 0 + ? helperObservables.helpersObs.value[helperObservables.helpersObs.value.length - 1].config.outputType + : variableType; + return ( - + {children} ); }; +export const VariableProvider = observer(_VariableProvider, { displayName: 'VariableProvider' }); + export function useVariable() { const context = useContext(VariableContext); - const { value, variableName, variableHelperMapping } = context; + const { value, variableName, valueType } = context; - const isHelperAllowed = (filterName: string) => { - return isHelperAllowedForVariable(variableName, filterName, variableHelperMapping); - }; - - const getSupportedFilters = (allHelpers: Helper[]) => { - return getSupportedFiltersForVariable(variableName, variableHelperMapping, allHelpers); + const isHelperAllowed = (helperName: string) => { + return isHelperAllowedForVariable(helperName, valueType); }; return { ...context, isHelperAllowed, - getSupportedFilters, }; } diff --git a/packages/core/client/src/schema-component/antd/variable/demos/date-scope.tsx b/packages/core/client/src/schema-component/antd/variable/demos/date-scope.tsx new file mode 100644 index 0000000000..4d7488a758 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/variable/demos/date-scope.tsx @@ -0,0 +1,72 @@ +import { createForm } from '@formily/core'; +import { observer, useField, useForm } from '@formily/react'; +import { AntdSchemaComponentProvider, Plugin, SchemaComponent } from '@nocobase/client'; +import { mockApp } from '@nocobase/client/demo-utils'; +import PluginVariableFiltersClient from '@nocobase/plugin-variable-helpers/client'; +import React from 'react'; +const scope = [ + { label: 'v1', value: 'v1' }, + { label: 'Date', value: '$nDate', children: [{ label: 'Now', value: 'now' }] }, +]; + +const useFormBlockProps = () => { + return { + form: createForm({ + initialValues: {}, + }), + }; +}; + +const schema = { + type: 'object', + 'x-component': 'FormV2', + 'x-use-component-props': 'useFormBlockProps', + properties: { + input: { + type: 'string', + title: `输入项`, + 'x-decorator': 'FormItem', + 'x-component': 'Variable.Input', + 'x-component-props': { + scope, + variableHelperMapping: { + rules: [ + { + variable: '$nDate.*', + helpers: ['date.*'], + }, + ], + }, + }, + }, + output: { + type: 'void', + title: `输出`, + 'x-decorator': 'FormItem', + 'x-component': 'OutPut', + }, + }, +}; + +const OutPut = observer(() => { + const form = useForm(); + return
Current input value: {form.values.input}
; +}); + +const Demo = () => { + return ( + + + + ); +}; + +class DemoPlugin extends Plugin { + async load() { + this.app.router.add('root', { path: '/', Component: Demo }); + } +} + +const app = mockApp({ plugins: [DemoPlugin, PluginVariableFiltersClient] }); + +export default app.getRootComponent(); 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 35eba1e7db..257eddf6ff 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 @@ -7,7 +7,7 @@ import React from 'react'; const scope = [ { label: 'v1', value: 'v1' }, - { label: 'Date', value: '$nDate', children: [{ label: 'Now', value: 'now' }] }, + { label: 'Date', value: '$nDate', children: [{ label: 'Now', value: 'now', type: 'date' }] }, ]; const useFormBlockProps = () => { @@ -30,14 +30,6 @@ const schema = { 'x-component': 'Variable.Input', 'x-component-props': { scope, - variableHelperMapping: { - rules: [ - { - variable: '$nDate.*', - helpers: ['date.*'], - }, - ], - }, }, }, output: { diff --git a/packages/core/client/src/variables/context/index.tsx b/packages/core/client/src/variables/context/index.tsx index 4bd977ed5a..b96c448ae1 100644 --- a/packages/core/client/src/variables/context/index.tsx +++ b/packages/core/client/src/variables/context/index.tsx @@ -7,11 +7,10 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { DateVariableContext } from '../date'; -import { useImmer } from 'use-immer'; -import { Updater } from 'use-immer'; -import React, { createContext, useContext } from 'react'; import merge from 'lodash/merge'; +import React, { createContext, useContext } from 'react'; +import { Updater, useImmer } from 'use-immer'; +import { DateVariableContext } from '../date'; type VariablesEvalContextType = { $dateV2: DateVariableContext; @@ -54,3 +53,17 @@ export const useVariablesContext = () => { } return variablesCtx; }; + +const scopes = [ + { + label: 'date', + value: '$nDate', + children: [ + { + label: 'Now', + value: 'now', + helpers: ['date.*'], + }, + ], + }, +]; diff --git a/packages/core/json-template-parser/src/index.ts b/packages/core/json-template-parser/src/index.ts index 57d6c26583..6d337eadef 100644 --- a/packages/core/json-template-parser/src/index.ts +++ b/packages/core/json-template-parser/src/index.ts @@ -7,7 +7,8 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -export * from './parser'; -export * from './filters'; -export * from './utils'; export * from './escape'; +export * from './filters'; +export * from './parser'; +export * from './types'; +export * from './utils'; diff --git a/packages/core/json-template-parser/src/types.ts b/packages/core/json-template-parser/src/types.ts new file mode 100644 index 0000000000..f966b0028d --- /dev/null +++ b/packages/core/json-template-parser/src/types.ts @@ -0,0 +1,21 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +type ValueType = 'date' | 'string' | 'dateRange' | 'any'; +export type Helper = { + name: string; + title: string; + handler: Function; + group: string; + inputType: ValueType; + outputType: ValueType; + sort: number; + args: string[]; + uiSchema?: any[]; +}; diff --git a/packages/core/json-template-parser/src/utils/index.ts b/packages/core/json-template-parser/src/utils/index.ts index 3d65931d14..348e2a78d1 100644 --- a/packages/core/json-template-parser/src/utils/index.ts +++ b/packages/core/json-template-parser/src/utils/index.ts @@ -9,14 +9,9 @@ import { escape, escapeSpecialChars, revertEscape } from '../escape'; import { createJSONTemplateParser } from '../parser'; - +import { Helper } from '../types'; const parser = createJSONTemplateParser(); const engine = parser.engine; -export type Helper = { - name: string; - handler: any; - args: string[]; -}; export function extractTemplateVariable(template: string): string | null { const escapedTemplate = escape(template ?? ''); diff --git a/packages/plugins/@nocobase/plugin-variable-helpers/src/json-template-helpers/index.ts b/packages/plugins/@nocobase/plugin-variable-helpers/src/json-template-helpers/index.ts index dfc99d72f9..8a50adb9d3 100644 --- a/packages/plugins/@nocobase/plugin-variable-helpers/src/json-template-helpers/index.ts +++ b/packages/plugins/@nocobase/plugin-variable-helpers/src/json-template-helpers/index.ts @@ -7,6 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { Helper } from '@nocobase/json-template-parser'; import { first } from './array'; import { dateAdd, dateFormat, dateSubtract } from './date'; const NAMESPACE = 'variable-helpers'; @@ -15,13 +16,16 @@ function tval(text: string) { return `{{t(${JSON.stringify(text)}, ${JSON.stringify({ ns: NAMESPACE, nsMode: 'fallback' })})}}`; } -export const helpers = [ +export const helpers: Helper[] = [ { name: 'date_format', title: 'format', handler: dateFormat, group: 'date', + inputType: 'date', + outputType: 'string', sort: 1, + args: [], uiSchema: [ { name: 'format', @@ -37,7 +41,10 @@ export const helpers = [ title: 'add', handler: dateAdd, group: 'date', + inputType: 'date', + outputType: 'date', sort: 2, + args: [], uiSchema: [ { name: 'unit', @@ -69,7 +76,10 @@ export const helpers = [ title: 'substract', handler: dateSubtract, group: 'date', + inputType: 'date', + outputType: 'date', sort: 3, + args: [], uiSchema: [ { name: 'unit', @@ -101,14 +111,20 @@ export const helpers = [ title: 'first', handler: first, sort: 4, + args: [], group: 'array', + inputType: 'string', + outputType: 'any', }, { name: 'array_last', title: 'last', sort: 5, + args: [], handler: first, group: 'array', + inputType: 'string', + outputType: 'any', }, ];