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