diff --git a/packages/core/client/src/schema-component/antd/variable/Helpers/Helper.tsx b/packages/core/client/src/schema-component/antd/variable/Helpers/Helper.tsx index eed5f5ffa0..ad1a375cfb 100644 --- a/packages/core/client/src/schema-component/antd/variable/Helpers/Helper.tsx +++ b/packages/core/client/src/schema-component/antd/variable/Helpers/Helper.tsx @@ -11,8 +11,8 @@ import { Popover } from 'antd'; import React, { useState } from 'react'; import { HelperConfiguator } from './HelperConfiguator'; -const WithPropOver = ({ children, index }) => { - const [open, setOpen] = useState(false); +const WithPropOver = ({ children, index, defaultOpen }) => { + const [open, setOpen] = useState(defaultOpen); const handleOpenChange = (newOpen: boolean) => { setOpen(newOpen); @@ -29,12 +29,14 @@ const WithPropOver = ({ children, index }) => { ); }; -export function Helper({ index, label }: { index: number; label: string }) { +export function Helper({ index, label, defaultOpen }: { index: number; label: string; defaultOpen: boolean }) { const Label =
{label}
; return ( <> | - {Label} + + {Label} + ); } diff --git a/packages/core/client/src/schema-component/antd/variable/Helpers/HelperList.tsx b/packages/core/client/src/schema-component/antd/variable/Helpers/HelperList.tsx index 46335b723f..e94798e9e9 100644 --- a/packages/core/client/src/schema-component/antd/variable/Helpers/HelperList.tsx +++ b/packages/core/client/src/schema-component/antd/variable/Helpers/HelperList.tsx @@ -11,15 +11,22 @@ import { observer } from '@formily/reactive-react'; import React from 'react'; import { useVariable } from '../VariableProvider'; import { Helper } from './Helper'; -import { useHelperObservables } from './hooks/useHelperObservables'; + const _HelperList = () => { - const { helperObservables } = useVariable(); + const { helperObservables, openLastHelper } = useVariable(); const { helpersObs, rawHelpersObs } = helperObservables; - console.log(rawHelpersObs.value); + return ( <> {helpersObs.value.map((helper, index) => { - return ; + return ( + + ); })} ); 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 a4863fa5cf..6e266006b3 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 @@ -57,7 +57,7 @@ export const createHelperObservables = () => { const { fullVariable, helpers, variableSegments } = extractTemplateElements( typeof template === 'string' ? template : '', ); - variableNameObs.value = fullVariable; + variableNameObs.value = variableSegments.join('.'); variableSegmentsObs.value = variableSegments; const computedHelpers = helpers.map((helper: any) => { const config = allHelpersConfigObs.value.find((f) => f.name === helper.name); 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 d369c0e8dd..803802c274 100644 --- a/packages/core/client/src/schema-component/antd/variable/Input.tsx +++ b/packages/core/client/src/schema-component/antd/variable/Input.tsx @@ -231,6 +231,7 @@ function _Input(props: VariableInputProps) { 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), @@ -373,7 +374,10 @@ function _Input(props: VariableInputProps) { if (next[1] !== type) { // setPrevType(next[1]); const newVariable = ConstantTypes[next[1]]?.default?.() ?? null; - onChange(composeTemplate({ fullVariable: newVariable, helpers }), optionPath); + onChange( + composeTemplate({ fullVariable: newVariable, helpers: optionPath[optionPath.length - 1]?.helpers ?? [] }), + optionPath, + ); } } else { if (variable) { @@ -382,9 +386,12 @@ function _Input(props: VariableInputProps) { } return; } - onChange(`{{${next.join('.')}}}`, optionPath); + 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(optionPath[optionPath.length - 1]?.type ?? null); + setVariableType(option.type ?? null); + setShowLastHelper(option.showLastHelper ?? false); } }, [type, variable, onChange], @@ -492,6 +499,8 @@ 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 ad036fb329..968093e003 100644 --- a/packages/core/client/src/schema-component/antd/variable/VariableProvider.tsx +++ b/packages/core/client/src/schema-component/antd/variable/VariableProvider.tsx @@ -13,8 +13,7 @@ import { composeTemplate, extractTemplateElements, Helper } from '@nocobase/json import { get, isArray } from 'lodash'; import minimatch from 'minimatch'; import React, { createContext, useContext, useEffect, useState } from 'react'; -import { useLocalVariables, useVariables } from '../../../variables'; -import { useVariablesContext } from '../../../variables/context'; +import { useLocalVariables, useVariableEvaluateContext, useVariables } from '../../../variables'; import { dateVarsMap } from '../../../variables/date'; import { useHelperObservables } from './Helpers/hooks/useHelperObservables'; interface VariableContextValue { @@ -23,13 +22,15 @@ interface VariableContextValue { variableType: string; valueType: string; variableName: string; + openLastHelper?: boolean; } interface VariableProviderProps { variableName: string; variableType: string | null; + openLastHelper?: boolean; children: React.ReactNode; - helperObservables?: ReturnType; + helperObservables: ReturnType; onVariableTemplateChange?: (val) => void; } @@ -106,6 +107,7 @@ const VariableContext = createContext({ value: null, variableType: null, valueType: '', + openLastHelper: false, }); export function useCurrentVariable(): VariableContextValue { @@ -120,13 +122,15 @@ const _VariableProvider: React.FC = ({ variableName, children, variableType, + openLastHelper, + helperObservables, onVariableTemplateChange, }) => { const [value, setValue] = useState(null); const variables = useVariables(); const localVariables = useLocalVariables(); - const helperObservables = useHelperObservables(); isArray(localVariables) ? localVariables : [localVariables]; + const { getValue } = useVariableEvaluateContext(); useEffect(() => { const dispose = reaction( () => { @@ -137,14 +141,11 @@ const _VariableProvider: React.FC = ({ }, ); return dispose; - }, [variableName, onVariableTemplateChange]); + }, [variableName, onVariableTemplateChange, helperObservables.helpersObs.value]); useEffect(() => { async function fetchValue() { try { - const vars = { - $nDate: dateVarsMap, - }; - const val = get(vars, variableName); + const val = await getValue(variableName); if (val) { setValue(val); } else { @@ -156,7 +157,7 @@ const _VariableProvider: React.FC = ({ } } fetchValue(); - }, [localVariables, variableName, variables]); + }, [localVariables, variableName, variables, getValue]); const valueType = helperObservables.helpersObs.value.length > 0 @@ -164,7 +165,9 @@ const _VariableProvider: React.FC = ({ : variableType; return ( - + {children} ); 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 257eddf6ff..ac2f646761 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 @@ -1,13 +1,98 @@ import { createForm } from '@formily/core'; import { observer, useField, useForm } from '@formily/react'; -import { AntdSchemaComponentProvider, Plugin, SchemaComponent } from '@nocobase/client'; +import { AntdSchemaComponentProvider, Plugin, SchemaComponent, 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 React from 'react'; +import { dayjs } from '@nocobase/utils/client'; +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: 'v1', value: 'v1' }, - { label: 'Date', value: '$nDate', children: [{ label: 'Now', value: 'now', type: 'date' }] }, + { + label: 'Date', + value: '$nDate', + type: 'date', + children: [ + { label: 'Now', value: 'now', type: 'date' }, + { + label: 'before', + value: 'before', + type: 'date', + children: numbers.map((number) => ({ + label: `${number}`, + value: `${number}`, + children: [ + { + label: 'days', + value: 'day', + type: 'date', + helpers: [ + { + name: 'date_format', + args: ['YYYY-MM-DD'], + }, + ], + showLastHelper: true, + }, + { + label: 'weeks', + value: 'week', + type: 'date', + helpers: [ + { + name: 'date_format', + args: ['YYYY-MM-DD'], + }, + ], + showLastHelper: true, + }, + { + label: 'months', + value: 'month', + type: 'date', + helpers: [ + { + name: 'date_format', + args: ['YYYY-MM-DD'], + }, + ], + showLastHelper: true, + }, + { + label: 'years', + value: 'year', + type: 'date', + helpers: [ + { + name: 'date_format', + args: ['YYYY-MM-DD'], + }, + ], + showLastHelper: true, + }, + ], + })), + }, + { + label: 'after', + value: 'after', + type: 'date', + children: numbers.map((number) => ({ + label: `${number}`, + value: `${number}`, + children: [ + { label: 'days', value: 'day', type: 'date' }, + { label: 'weeks', value: 'week', type: 'date' }, + { label: 'months', value: 'month', type: 'date' }, + { label: 'years', value: 'year', type: 'date' }, + ], + })), + }, + ], + }, ]; const useFormBlockProps = () => { @@ -34,22 +119,64 @@ const schema = { }, output: { type: 'void', - title: `输出`, + title: `模板`, 'x-decorator': 'FormItem', 'x-component': 'OutPut', }, + result: { + type: 'void', + title: `值`, + 'x-decorator': 'FormItem', + 'x-component': 'Result', + }, }, }; const OutPut = observer(() => { const form = useForm(); - return
Current input value: {form.values.input}
; + return
{form.values.input}
; }); +const Result = observer(() => { + const form = useForm(); + const [value, setValue] = useState(''); + useEffect(() => { + if (!form.values.input) { + return; + } + parser + .render(form.values.input, { $nDate: dateScopeFn }, {}) + .then((result) => { + setValue(result); + }) + .catch((error) => { + throw error; + }); + }, [form.values.input]); + return
{value.toString()}
; +}); + +const dateScopeFn = ({ fields, data, context }) => { + return { + getValue: ({ field, keys }) => { + const path = field.split('.'); + if (path[0] === 'now') { + return dayjs(); + } else if (path[0] === 'before') { + return dayjs().subtract(parseInt(path[1]), path[2]); + } else if (path[0] === 'after') { + return dayjs().add(parseInt(path[1]), path[2]); + } + return null; + }, + }; +}; const Demo = () => { return ( - + + + ); }; diff --git a/packages/core/client/src/variables/context/EvaluateContext.tsx b/packages/core/client/src/variables/context/EvaluateContext.tsx new file mode 100644 index 0000000000..ce843628fe --- /dev/null +++ b/packages/core/client/src/variables/context/EvaluateContext.tsx @@ -0,0 +1,72 @@ +/** + * 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. + */ + +import { get } from 'lodash'; +import React, { createContext, ReactNode, useContext } from 'react'; + +interface EvaluateContextProps { + data: Record; + context: Record; + getValue: (field: string) => Promise; +} + +const EvaluateContext = createContext({ data: {}, context: {}, getValue: async () => null }); + +export const VariableEvaluateProvider: React.FC<{ + children: ReactNode; + data: Record; + context: Record; +}> = ({ children, data, context }) => { + const getValueFromData = async (field: string): Promise => { + return getValue({ field, data, context }); + }; + return ( + + {children} + + ); +}; + +export const useVariableEvaluateContext = (): EvaluateContextProps => { + const context = useContext(EvaluateContext); + if (!context) { + throw new Error('useEvaluate must be used within an EvaluateProvider'); + } + return context; +}; + +const getValue = async (params: { + field: string; + data: Record; + context?: Record; +}): 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 scopeKey = path[0]; + const scopeFn = data[scopeKey]; + if (typeof scopeFn === 'function') { + const scopeResult = await scopeFn({ fields: [dataKey], data, context }); + if (scopeResult?.getValue) { + return scopeResult.getValue({ field: dataKey, keys: [] }); + } + } + return null; + } + + const value = get(data, dataKey); + + // Handle function values + if (typeof value === 'function') { + return value(); + } + return value; +}; diff --git a/packages/core/client/src/variables/index.ts b/packages/core/client/src/variables/index.ts index c883997108..d701e14347 100644 --- a/packages/core/client/src/variables/index.ts +++ b/packages/core/client/src/variables/index.ts @@ -7,13 +7,15 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -export { default as VariablesProvider, VariablesContext } from './VariablesProvider'; export { default as useBuiltinVariables } from './hooks/useBuiltinVariables'; export { default as useContextVariable } from './hooks/useContextVariable'; export { default as useLocalVariables } from './hooks/useLocalVariables'; export { default as useVariables } from './hooks/useVariables'; export * from './utils/isVariable'; export * from './utils/transformVariableValue'; +export { VariablesContext, default as VariablesProvider } from './VariablesProvider'; export * from './constants'; export type { VariablesContextType } from './types'; + +export * from './context/EvaluateContext'; 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 112260a9ef..558bf5b68a 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 @@ -159,7 +159,7 @@ export class JSONTemplateParser { fieldSet.add(field); } return { - variableName: fullVariables[0], + variableName: variableSegments.join('.'), variableSegments, tokenKind: rawTemplate.token.kind, tokenBegin: rawTemplate.token.begin, diff --git a/packages/core/json-template-parser/src/utils/index.ts b/packages/core/json-template-parser/src/utils/index.ts index 348e2a78d1..7834f360e8 100644 --- a/packages/core/json-template-parser/src/utils/index.ts +++ b/packages/core/json-template-parser/src/utils/index.ts @@ -30,8 +30,8 @@ export function extractTemplateElements(template: string): { } { const escapedTemplate = escape(template ?? ''); try { - const fullVariable = engine.fullVariablesSync(escapedTemplate)[0] ?? ''; const variableSegments = engine.variableSegmentsSync(escapedTemplate)[0] ?? []; + const fullVariable = variableSegments.join('.'); const parsedTemplate = engine.parse(escapedTemplate)[0] ?? {}; const helpers = //@ts-ignore