feat: add helper type inference

This commit is contained in:
Sheldon Guo 2025-03-25 18:18:57 +08:00
parent 2151143dbf
commit 0a687d0791
10 changed files with 164 additions and 64 deletions

View File

@ -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<string, any>;
config: any;
args: string[];
handler: (value: any, ...args: any[]) => any;
config: _Helper;
};
type RawHelper = {

View File

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

View File

@ -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<typeof useHelperObservables>;
variableHelperMapping: VariableHelperMapping;
variableType: string;
valueType: string;
variableName: string;
}
interface VariableProviderProps {
variableName: string;
variableType: string | null;
children: React.ReactNode;
variableHelperMapping?: VariableHelperMapping;
helperObservables?: ReturnType<typeof useHelperObservables>;
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<VariableContextValue>({
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<VariableProviderProps> = ({
const _VariableProvider: React.FC<VariableProviderProps> = ({
variableName,
children,
variableHelperMapping,
variableType,
onVariableTemplateChange,
}) => {
const [value, setValue] = useState(null);
@ -171,28 +158,30 @@ export const VariableProvider: React.FC<VariableProviderProps> = ({
fetchValue();
}, [localVariables, variableName, variables]);
const valueType =
helperObservables.helpersObs.value.length > 0
? helperObservables.helpersObs.value[helperObservables.helpersObs.value.length - 1].config.outputType
: variableType;
return (
<VariableContext.Provider value={{ variableName, value, helperObservables, variableHelperMapping }}>
<VariableContext.Provider value={{ variableName, value, valueType, helperObservables, variableType }}>
{children}
</VariableContext.Provider>
);
};
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,
};
}

View File

@ -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 <div>Current input value: {form.values.input}</div>;
});
const Demo = () => {
return (
<AntdSchemaComponentProvider>
<SchemaComponent schema={schema} scope={{ useFormBlockProps }} components={{ OutPut }} />
</AntdSchemaComponentProvider>
);
};
class DemoPlugin extends Plugin {
async load() {
this.app.router.add('root', { path: '/', Component: Demo });
}
}
const app = mockApp({ plugins: [DemoPlugin, PluginVariableFiltersClient] });
export default app.getRootComponent();

View File

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

View File

@ -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.*'],
},
],
},
];

View File

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

View File

@ -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[];
};

View File

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

View File

@ -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',
},
];