feat: update variable handling to use joined segments and enhance helper visibility

This commit is contained in:
Sheldon Guo 2025-03-26 14:15:03 +08:00
parent 0a687d0791
commit ddb1340699
10 changed files with 254 additions and 32 deletions

View File

@ -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 = <div style={{ color: '#52c41a', display: 'inline-block', cursor: 'pointer' }}>{label}</div>;
return (
<>
<span style={{ color: '#bfbfbf', margin: '0 5px' }}>|</span>
<WithPropOver index={index}>{Label}</WithPropOver>
<WithPropOver index={index} defaultOpen={defaultOpen}>
{Label}
</WithPropOver>
</>
);
}

View File

@ -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 <Helper key={index} index={index} label={helper.config.title} />;
return (
<Helper
key={index}
index={index}
defaultOpen={helpersObs.value.length === index + 1 ? openLastHelper : false}
label={helper.config.title}
/>
);
})}
</>
);

View File

@ -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);

View File

@ -231,6 +231,7 @@ function _Input(props: VariableInputProps) {
const form = useForm();
const [options, setOptions] = React.useState<DefaultOptionType[]>([]);
const [variableType, setVariableType] = React.useState<string>();
const [showLastHelper, setShowLastHelper] = React.useState<boolean>(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) {
<VariableProvider
variableName={fullVariable}
variableType={variableType}
openLastHelper={showLastHelper}
helperObservables={helperObservables}
onVariableTemplateChange={onChange}
>
<HelperList />

View File

@ -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<typeof useHelperObservables>;
helperObservables: ReturnType<typeof useHelperObservables>;
onVariableTemplateChange?: (val) => void;
}
@ -106,6 +107,7 @@ const VariableContext = createContext<VariableContextValue>({
value: null,
variableType: null,
valueType: '',
openLastHelper: false,
});
export function useCurrentVariable(): VariableContextValue {
@ -120,13 +122,15 @@ const _VariableProvider: React.FC<VariableProviderProps> = ({
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<VariableProviderProps> = ({
},
);
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<VariableProviderProps> = ({
}
}
fetchValue();
}, [localVariables, variableName, variables]);
}, [localVariables, variableName, variables, getValue]);
const valueType =
helperObservables.helpersObs.value.length > 0
@ -164,7 +165,9 @@ const _VariableProvider: React.FC<VariableProviderProps> = ({
: variableType;
return (
<VariableContext.Provider value={{ variableName, value, valueType, helperObservables, variableType }}>
<VariableContext.Provider
value={{ variableName, value, valueType, helperObservables, variableType, openLastHelper }}
>
{children}
</VariableContext.Provider>
);

View File

@ -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 <div>Current input value: {form.values.input}</div>;
return <div>{form.values.input}</div>;
});
const Result = observer(() => {
const form = useForm();
const [value, setValue] = useState<string>('');
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 <div>{value.toString()}</div>;
});
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 (
<AntdSchemaComponentProvider>
<SchemaComponent schema={schema} scope={{ useFormBlockProps }} components={{ OutPut }} />
<VariableEvaluateProvider data={{ $nDate: dateScopeFn }} context={{}}>
<SchemaComponent schema={schema} scope={{ useFormBlockProps }} components={{ OutPut, Result }} />
</VariableEvaluateProvider>
</AntdSchemaComponentProvider>
);
};

View File

@ -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<string, any>;
context: Record<string, any>;
getValue: (field: string) => Promise<any>;
}
const EvaluateContext = createContext<EvaluateContextProps>({ data: {}, context: {}, getValue: async () => null });
export const VariableEvaluateProvider: React.FC<{
children: ReactNode;
data: Record<string, any>;
context: Record<string, any>;
}> = ({ children, data, context }) => {
const getValueFromData = async (field: string): Promise<any> => {
return getValue({ field, data, context });
};
return (
<EvaluateContext.Provider value={{ data, context, getValue: getValueFromData }}>
{children}
</EvaluateContext.Provider>
);
};
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<string, any>;
context?: Record<string, any>;
}): Promise<any> => {
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;
};

View File

@ -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';

View File

@ -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,

View File

@ -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