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 803802c274..45262eae8f 100644 --- a/packages/core/client/src/schema-component/antd/variable/Input.tsx +++ b/packages/core/client/src/schema-component/antd/variable/Input.tsx @@ -22,6 +22,7 @@ import { Select, Space, Tag, + Tooltip, Typography, } from 'antd'; import useAntdInputStyle from 'antd/es/input/style'; @@ -35,7 +36,7 @@ import { Json } from '../input'; import { HelperAddition, HelperList } from './Helpers'; import { useHelperObservables } from './Helpers/hooks/useHelperObservables'; import { useStyles } from './style'; -import { VariableHelperMapping, VariableProvider } from './VariableProvider'; +import { useVariable, VariableHelperMapping, VariableProvider } from './VariableProvider'; import { XButton } from './XButton'; const { Text } = Typography; @@ -45,6 +46,22 @@ type ParseOptions = { stringToDate?: boolean; }; +const findScopeOption = (options: DefaultOptionType[], path: string[]): DefaultOptionType | null => { + if (!options || !Array.isArray(options)) { + return null; + } + + for (const option of options) { + if (option.value === path[0]) { + if (path.length === 1) { + return option; + } + return findScopeOption(option.children, path.slice(1)); + } + } + return null; +}; + function parseValue(value: any, options: ParseOptions = {}): string | string[] { if (value == null || (Array.isArray(value) && value.length === 0)) { return 'null'; @@ -220,6 +237,7 @@ function _Input(props: VariableInputProps) { } = props; const scope = typeof props.scope === 'function' ? props.scope() : props.scope; + const { wrapSSR, hashId, componentCls, rootPrefixCls } = useStyles({ hideVariableButton }); const helperObservables = useHelperObservables(); @@ -230,8 +248,6 @@ function _Input(props: VariableInputProps) { const { t } = useTranslation(); const form = useForm(); const [options, setOptions] = React.useState([]); - const [variableType, setVariableType] = React.useState(); - const [showLastHelper, setShowLastHelper] = React.useState(false); const [variableText, setVariableText] = React.useState([]); const [isFieldValue, setIsFieldValue] = React.useState( hideVariableButton || (children && value != null ? true : false), @@ -246,17 +262,7 @@ function _Input(props: VariableInputProps) { [value], ); - // useEffect(() => { - // const dispose = reaction( - // () => { - // return composeTemplate({ fullVariable, helpers: helperObservables.helpersObs.value }); - // }, - // (newVal) => { - // onChange(newVal); - // }, - // ); - // return dispose; - // }, [fullVariable, onChange]); + const selectedScopeOption = useMemo(() => findScopeOption(scope, variableSegments), [scope, variableSegments]); const parsed = useMemo(() => parseValue(variableSegments, parseOptions), [parseOptions, variableSegments]); const isConstant = typeof parsed === 'string'; @@ -389,10 +395,6 @@ function _Input(props: VariableInputProps) { const variableName = next.join('.'); const option = optionPath[optionPath.length - 1]; onChange(composeTemplate({ fullVariable: variableName, helpers: option?.helpers ?? [] }), optionPath); - if (Array.isArray(optionPath) && optionPath.length > 0) { - setVariableType(option.type ?? null); - setShowLastHelper(option.showLastHelper ?? false); - } }, [type, variable, onChange], ); @@ -488,21 +490,16 @@ function _Input(props: VariableInputProps) { className={cx('ant-input ant-input-outlined', { 'ant-input-disabled': disabled }, hashId)} > - {variableText.map((item, index) => { - return ( - - {index ? ' / ' : ''} - {item} - - ); - })} + + {variableText.length > 0 && } @@ -561,4 +558,22 @@ function _Input(props: VariableInputProps) { ); } +const VariableTag = ({ variablePath }: { variablePath: string[] }) => { + const { value } = useVariable(); + return ( + + + {variablePath.map((item, index) => { + return ( + + {index ? ' / ' : ''} + {item} + + ); + })} + + + ); +}; + export const Input = observer(_Input, { displayName: 'VariableInput' }); 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 968093e003..34e6e4f785 100644 --- a/packages/core/client/src/schema-component/antd/variable/VariableProvider.tsx +++ b/packages/core/client/src/schema-component/antd/variable/VariableProvider.tsx @@ -27,6 +27,7 @@ interface VariableContextValue { interface VariableProviderProps { variableName: string; + variableExampleValue?: any; variableType: string | null; openLastHelper?: boolean; children: React.ReactNode; @@ -122,6 +123,7 @@ const _VariableProvider: React.FC = ({ variableName, children, variableType, + variableExampleValue, openLastHelper, helperObservables, onVariableTemplateChange, @@ -145,6 +147,10 @@ const _VariableProvider: React.FC = ({ useEffect(() => { async function fetchValue() { try { + if (variableExampleValue !== undefined) { + setValue(variableExampleValue); + return; + } const val = await getValue(variableName); if (val) { setValue(val); @@ -157,7 +163,7 @@ const _VariableProvider: React.FC = ({ } } fetchValue(); - }, [localVariables, variableName, variables, getValue]); + }, [localVariables, variableName, variables, getValue, variableExampleValue]); const valueType = helperObservables.helpersObs.value.length > 0 diff --git a/packages/core/client/src/schema-component/antd/variable/demos/comprehensive-demo.tsx b/packages/core/client/src/schema-component/antd/variable/demos/comprehensive-demo.tsx new file mode 100644 index 0000000000..03c3ee90e0 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/variable/demos/comprehensive-demo.tsx @@ -0,0 +1,194 @@ +import { createForm } from '@formily/core'; +import { observer, useField, useForm } from '@formily/react'; +import { AntdSchemaComponentProvider, FormItem, Plugin, SchemaComponent, Variable } from '@nocobase/client'; +import { mockApp } from '@nocobase/client/demo-utils'; +import { createJSONTemplateParser } from '@nocobase/json-template-parser'; +import PluginVariableFiltersClient from '@nocobase/plugin-variable-helpers/client'; +import { dayjs } from '@nocobase/utils/client'; +import { Form } from 'antd'; +import React, { useEffect, useState } from 'react'; + +const parser = createJSONTemplateParser(); + +// 定义变量作用域 +const scope = [ + { + label: 'Date', + value: '$nDate', + type: 'date', + children: [ + { + label: 'Now', + value: 'now', + type: 'date', + tooltip: '当前时间:2025-03-26 10:00:00', + }, + { + label: 'Today', + value: 'today', + type: 'date', + tooltip: '今天:2025-03-26', + }, + { + label: 'This Quarter', + value: 'thisQuarter', + type: 'date', + tooltip: '本季度:2025Q1', + }, + { + label: 'This Week', + value: 'thisWeek', + type: 'date', + tooltip: '本周:2025w12', + }, + { + label: 'Today (DateOnly)', + value: 'today_dateOnly', + type: 'date', + tooltip: '今天(无时区):2025-03-26', + }, + { + label: 'Today (Without TZ)', + value: 'today_withoutTz', + type: 'date', + tooltip: '今天(无时区时间):2025-03-26 00:00:00', + }, + { + label: 'Today (UTC)', + value: 'today_utc', + type: 'date', + tooltip: '今天(UTC时间):2025-03-26T00:00:00.000Z', + }, + ], + }, +]; + +// 定义表单属性 +const useFormBlockProps = () => { + return { + form: createForm({ + initialValues: { + input1: '{{ $nDate.now }}', + input2: '{{ $nDate.today | date_format: "YYYY-MM-DD" }}', + input3: '{{ $nDate.today | date_add: 7, "day" | date_format: "YYYY-MM-DD" }}', + input4: '{{ $nDate.today_dateOnly }}', + input5: '{{ $nDate.today_withoutTz }}', + input6: '{{ $nDate.today_utc }}', + }, + }), + }; +}; + +// 定义表单 schema +const schema = { + type: 'object', + 'x-component': 'FormV2', + 'x-use-component-props': 'useFormBlockProps', + properties: { + input1: { + type: 'string', + title: '基础变量选择', + 'x-decorator': 'FormItem', + 'x-component': 'Variable.Input', + 'x-component-props': { + scope, + value: '{{ $nDate.now }}', + }, + }, + input2: { + type: 'string', + title: '带格式化的变量', + 'x-decorator': 'FormItem', + 'x-component': 'Variable.Input', + 'x-component-props': { + scope, + value: '{{ $nDate.today | date_format: "YYYY-MM-DD" }}', + }, + }, + input3: { + type: 'string', + title: '带日期偏移的变量', + 'x-decorator': 'FormItem', + 'x-component': 'Variable.Input', + 'x-component-props': { + scope, + value: '{{ $nDate.today | date_add: 7, "day" | date_format: "YYYY-MM-DD" }}', + }, + }, + input4: { + type: 'string', + title: 'DateOnly 格式', + 'x-decorator': 'FormItem', + 'x-component': 'Variable.Input', + 'x-component-props': { + scope, + value: '{{ $nDate.today_dateOnly }}', + }, + }, + input5: { + type: 'string', + title: '无时区时间格式', + 'x-decorator': 'FormItem', + 'x-component': 'Variable.Input', + 'x-component-props': { + scope, + value: '{{ $nDate.today_withoutTz }}', + }, + }, + input6: { + type: 'string', + title: 'UTC 时间格式', + 'x-decorator': 'FormItem', + 'x-component': 'Variable.Input', + 'x-component-props': { + scope, + value: '{{ $nDate.today_utc }}', + }, + }, + }, +}; + +// 独立组件示例 +const Demo = () => { + const [value1, setValue1] = useState('{{$nDate.now}}'); + const [value2, setValue2] = useState('{{$nDate.today | date_format: "YYYY-MM-DD"}}'); + const [value3, setValue3] = useState('{{$nDate.today | date_add: 7, "day" | date_format: "YYYY-MM-DD"}}'); + const [value4, setValue4] = useState('{{$nDate.today_dateOnly}}'); + const [value5, setValue5] = useState('{{$nDate.today_withoutTz}}'); + const [value6, setValue6] = useState('{{$nDate.today_utc}}'); + + 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/date-scope.tsx b/packages/core/client/src/schema-component/antd/variable/demos/date-scope.tsx index 4d7488a758..4b6ded695f 100644 --- 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 @@ -1,6 +1,6 @@ import { createForm } from '@formily/core'; import { observer, useField, useForm } from '@formily/react'; -import { AntdSchemaComponentProvider, Plugin, SchemaComponent } from '@nocobase/client'; +import { AntdSchemaComponentProvider, Plugin, SchemaComponent, Variable } from '@nocobase/client'; import { mockApp } from '@nocobase/client/demo-utils'; import PluginVariableFiltersClient from '@nocobase/plugin-variable-helpers/client'; import React from 'react'; diff --git a/packages/core/client/src/schema-component/antd/variable/demos/form-default-value.tsx b/packages/core/client/src/schema-component/antd/variable/demos/form-default-value.tsx new file mode 100644 index 0000000000..d8b3749fb5 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/variable/demos/form-default-value.tsx @@ -0,0 +1,55 @@ +import { ISchema, Plugin, SchemaComponent, SchemaSettings } from '@nocobase/client'; +import { mockApp } from '@nocobase/client/demo-utils'; +import React from 'react'; + +const simpleSettings = new SchemaSettings({ + name: 'simpleSettings', + items: [ + { + name: 'delete', + type: 'remove', + }, + ], +}); + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DndContext', + 'x-component': 'FormV2', + properties: { + username: { + type: 'string', + title: 'Username', + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-settings': 'simpleSettings', + required: true, + }, + nickname: { + type: 'string', + title: 'Nickname', + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-settings': 'simpleSettings', + }, + }, +}; + +const Demo = () => { + return ; +}; + +class DemoPlugin extends Plugin { + async load() { + this.app.router.add('root', { path: '/', Component: Demo }); + } +} + +const app = mockApp({ + designable: true, + plugins: [DemoPlugin], + schemaSettings: [simpleSettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-component/antd/variable/demos/helper-demo.tsx b/packages/core/client/src/schema-component/antd/variable/demos/helper-demo.tsx new file mode 100644 index 0000000000..cb819e731f --- /dev/null +++ b/packages/core/client/src/schema-component/antd/variable/demos/helper-demo.tsx @@ -0,0 +1,65 @@ +import { createForm } from '@formily/core'; +import { observer, useField, useForm } from '@formily/react'; +import { + AntdSchemaComponentProvider, + FormItem, + Plugin, + SchemaComponent, + useApp, + useRequest, + Variable, + VariableEvaluateProvider, +} from '@nocobase/client'; +import { mockApp } from '@nocobase/client/demo-utils'; +import { createJSONTemplateParser } from '@nocobase/json-template-parser'; +import PluginVariableFiltersClient from '@nocobase/plugin-variable-helpers/client'; +import { dayjs } from '@nocobase/utils/client'; +import { Form } from 'antd'; +import React, { useEffect, useState } from 'react'; + +const scope = [{ label: 'now', value: 'now', type: 'date' }]; + +const Demo = () => { + const app = useApp(); + const [value, setValue] = useState('{{now | date_format: "YYYY-MM-DD"}}'); + const [value2, setValue2] = useState('{{now |date_offset: "add", 1, "week" | date_format: "YYYY-MM-DD"}}'); + const tmplateData = { now: dayjs().format() }; + const { data } = useRequest( + () => { + return app.jsonTemplateParser.render(value, tmplateData); + }, + { + refreshDeps: [value], + }, + ); + const { data: data2 } = useRequest( + () => { + return app.jsonTemplateParser.render(value2); + }, + { + refreshDeps: [value2], + }, + ); + 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/scope.tsx b/packages/core/client/src/schema-component/antd/variable/demos/scope.tsx new file mode 100644 index 0000000000..e68552a333 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/variable/demos/scope.tsx @@ -0,0 +1,39 @@ +import { createForm } from '@formily/core'; +import { observer, useField, useForm } from '@formily/react'; +import { AntdSchemaComponentProvider, FormItem, Plugin, SchemaComponent, Variable } from '@nocobase/client'; +import { mockApp } from '@nocobase/client/demo-utils'; +import { createJSONTemplateParser } from '@nocobase/json-template-parser'; +import PluginVariableFiltersClient from '@nocobase/plugin-variable-helpers/client'; +import { dayjs } from '@nocobase/utils/client'; +import { Form } from 'antd'; +import React, { useEffect, useState } from 'react'; + +const scope = [ + { label: 'v1', value: 'v1' }, + { label: 'now', value: 'now', type: 'date' }, +]; + +const Demo = () => { + const [value, setValue] = useState('{{now}}'); + const [value2, setValue2] = useState('{{v1}}'); + 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/selected-and-disable.tsx b/packages/core/client/src/schema-component/antd/variable/demos/selected-and-disable.tsx new file mode 100644 index 0000000000..1ac5b0f57f --- /dev/null +++ b/packages/core/client/src/schema-component/antd/variable/demos/selected-and-disable.tsx @@ -0,0 +1,74 @@ +import { createForm } from '@formily/core'; +import { observer, useField, useForm } from '@formily/react'; +import { AntdSchemaComponentProvider, FormItem, Plugin, SchemaComponent, Variable } from '@nocobase/client'; +import { mockApp } from '@nocobase/client/demo-utils'; +import { createJSONTemplateParser } from '@nocobase/json-template-parser'; +import PluginVariableFiltersClient from '@nocobase/plugin-variable-helpers/client'; +import { dayjs } from '@nocobase/utils/client'; +import { Form } from 'antd'; +import React, { useEffect, useState } from 'react'; + +const parser = createJSONTemplateParser(); +const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 30, 60, 90]; +const scope = [ + { + label: 'Date', + value: '$nDate', + type: 'date', + children: [{ label: 'now', value: 'now', type: 'date', disabled: true }], + }, +]; + +const useFormBlockProps = () => { + return { + form: createForm({ + initialValues: { + input: '{{ $nDate.now }}', + }, + }), + }; +}; + +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, + value: '{{ $nDate.now }}', + test: '{{ $nDate.now }}', + }, + }, + }, +}; + +const Demo = () => { + const [value, setValue] = useState('{{$nDate.now}}'); + const [value2, setValue2] = useState('{{$nDate.now | date_format: "YYYY-MM-DD"}}'); + 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/variable-value.tsx b/packages/core/client/src/schema-component/antd/variable/demos/variable-value.tsx new file mode 100644 index 0000000000..5660a38d9f --- /dev/null +++ b/packages/core/client/src/schema-component/antd/variable/demos/variable-value.tsx @@ -0,0 +1,53 @@ +import { createForm } from '@formily/core'; +import { observer, useField, useForm } from '@formily/react'; +import { + AntdSchemaComponentProvider, + FormItem, + Plugin, + SchemaComponent, + useApp, + useRequest, + Variable, + VariableEvaluateProvider, +} from '@nocobase/client'; +import { mockApp } from '@nocobase/client/demo-utils'; +import { createJSONTemplateParser } from '@nocobase/json-template-parser'; +import PluginVariableFiltersClient from '@nocobase/plugin-variable-helpers/client'; +import { dayjs } from '@nocobase/utils/client'; +import { Form } from 'antd'; +import React, { useEffect, useState } from 'react'; + +const scope = [ + { label: 'v1', value: 'v1' }, + { label: 'v2', value: 'v2', example: 'example-value-v2' }, +]; + +const Demo = () => { + const app = useApp(); + const [value, setValue] = useState('{{v1}}'); + const [value2, setValue2] = useState('{{v2}}'); + const tmplateData = { v1: 'value-v1' }; + + 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/index.md b/packages/core/client/src/schema-component/antd/variable/index.md index 8e6c65fce7..c980eb583b 100644 --- a/packages/core/client/src/schema-component/antd/variable/index.md +++ b/packages/core/client/src/schema-component/antd/variable/index.md @@ -4,6 +4,42 @@ ## `Variable.Input` +### scope 定义 + +scope 可以定义变量的 label, value , type +变量的 type 会匹配对应的 helper 函数,对变量进行二次处理。目前内置了 date.format , date.offset 的 helpers。也就是说如果变量的类型是 date, 会匹配到对应的过滤器。如果变量是其他类型或者无类型,都无法匹配到过滤器。 + + +```ts +const scope = [ + {label: 'v1', value: 'v1'} + {label: 'now', value: 'now', type: 'date'} +] +``` + + + +### helper 示例 + + +### 变量值如何获取 +1. VariableEvaluateProvider 上下文获取 +提供 VariableEvaluateProvider 上下文,可以注入 data 属性 +2. 从 scope 中的 example 属性获取 +某些变量在前端无法获取到实际值(如服务端环境下的变量),但是有需要提供一个示例值方便应用 helpers,可以在声明 scope 选项时带上示例值,如 `{label: 'v1' , value: 'v1', example: 'example-value-v1'}` + +一下是一个完整的例子,变量 v1 由上下文提供值,变量 v2 由 example 提供值。鼠标悬浮在变量名称上即可查看对应变量的值。 + + +### 应用场景 +#### 默认值 + + +### 变量选中并已被禁用的效果 +当变量被禁用时,不影响已经选中的变量显示。在下面的例子中,now 变量已被禁用: + + + ```ts import type { DefaultOptionType } from 'antd/lib/cascader'; type VariableInputProps = { diff --git a/packages/core/client/src/variables/context/EvaluateContext.tsx b/packages/core/client/src/variables/context/EvaluateContext.tsx index ce843628fe..3c786ecdba 100644 --- a/packages/core/client/src/variables/context/EvaluateContext.tsx +++ b/packages/core/client/src/variables/context/EvaluateContext.tsx @@ -48,9 +48,10 @@ const getValue = async (params: { }): Promise => { const { field, data, context } = params; const path = field.split('.'); - const dataKey = path.slice(1).join('.'); + // Handle scope functions (starting with $) if (path[0].startsWith('$')) { + const dataKey = path.slice(1).join('.'); const scopeKey = path[0]; const scopeFn = data[scopeKey]; if (typeof scopeFn === 'function') { @@ -62,11 +63,10 @@ const getValue = async (params: { return null; } - const value = get(data, dataKey); - - // Handle function values + const value = get(data, field); if (typeof value === 'function') { return value(); } + return value; }; 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 558bf5b68a..1ba17a458d 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 @@ -68,9 +68,11 @@ export class JSONTemplateParser { } registerFilterGroup(group: FilterGroup): void { + if (this._filterGroups.find((g) => g.name === group.name)) return; this._filterGroups.push(group); } registerFilter(filter: Helper): void { + if (this._filters.find((f) => f.name === filter.name)) return; this._filters.push(filter); this._engine.registerFilter(filter.name, filter.handler); } diff --git a/packages/core/json-template-parser/src/types.ts b/packages/core/json-template-parser/src/types.ts index f966b0028d..413082d6ee 100644 --- a/packages/core/json-template-parser/src/types.ts +++ b/packages/core/json-template-parser/src/types.ts @@ -11,7 +11,7 @@ type ValueType = 'date' | 'string' | 'dateRange' | 'any'; export type Helper = { name: string; title: string; - handler: Function; + handler: (...args: any[]) => any; group: string; inputType: ValueType; outputType: ValueType; diff --git a/packages/plugins/@nocobase/plugin-variable-helpers/src/json-template-helpers/date.ts b/packages/plugins/@nocobase/plugin-variable-helpers/src/json-template-helpers/date.ts index daa7a4bc74..840f0cba78 100644 --- a/packages/plugins/@nocobase/plugin-variable-helpers/src/json-template-helpers/date.ts +++ b/packages/plugins/@nocobase/plugin-variable-helpers/src/json-template-helpers/date.ts @@ -70,3 +70,15 @@ export function dateSubtract(initialValue: any, unit: any, number: number) { return handler(initialValue); } } + +export function dateOffset(initialValue: any, action: 'add' | 'subtract', number: number, unit: any) { + const handler = (value: any) => { + if (action === 'add') { + return dayjs.isDayjs(value) ? value.add(number, unit) : dayjs(value).add(number, unit); + } else if (action === 'subtract') { + return dayjs.isDayjs(value) ? value.subtract(number, unit) : dayjs(value).subtract(number, unit); + } + throw new Error('Invalid action'); + }; + return handler(initialValue); +} 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 8a50adb9d3..4ababe495a 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 @@ -9,7 +9,7 @@ import { Helper } from '@nocobase/json-template-parser'; import { first } from './array'; -import { dateAdd, dateFormat, dateSubtract } from './date'; +import { dateAdd, dateFormat, dateOffset, dateSubtract } from './date'; const NAMESPACE = 'variable-helpers'; function tval(text: string) { @@ -37,50 +37,33 @@ export const helpers: Helper[] = [ ], }, { - name: 'date_add', - title: 'add', - handler: dateAdd, - group: 'date', - inputType: 'date', - outputType: 'date', - sort: 2, - args: [], - uiSchema: [ - { - name: 'unit', - title: tval('Unit'), - type: 'string', - required: true, - 'x-component': 'Select', - enum: [ - { label: tval('Year'), value: 'year' }, - { label: tval('Quarter'), value: 'quarter' }, - { label: tval('Week'), value: 'week' }, - { label: tval('Day'), value: 'day' }, - { label: tval('Hour'), value: 'hour' }, - { label: tval('Minute'), value: 'minute' }, - { label: tval('Second'), value: 'second' }, - ], - }, - { - name: 'number', - title: tval('Amount'), - type: 'number', - 'x-component': 'InputNumber', - required: true, - }, - ], - }, - { - name: 'date_subtract', - title: 'substract', - handler: dateSubtract, + name: 'date_offset', + title: 'offset', + handler: dateOffset, group: 'date', inputType: 'date', outputType: 'date', sort: 3, args: [], uiSchema: [ + { + name: 'action', + title: tval('Action'), + type: 'string', + required: true, + 'x-component': 'Select', + enum: [ + { label: tval('Add'), value: 'add' }, + { label: tval('Sbutract'), value: 'subtract' }, + ], + }, + { + name: 'number', + title: tval('Amount'), + type: 'number', + 'x-component': 'InputNumber', + required: true, + }, { name: 'unit', title: tval('Unit'), @@ -97,35 +80,8 @@ export const helpers: Helper[] = [ { label: tval('Second'), value: 'second' }, ], }, - { - name: 'number', - title: tval('Amount'), - type: 'number', - 'x-component': 'InputNumber', - required: true, - }, ], }, - { - name: 'array_first', - 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', - }, ]; export const helperGroups = [