mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-08 15:09:27 +08:00
feat: add helper components and enhance filter handling in VariablesProvider
This commit is contained in:
parent
b51c42c70c
commit
21d45bf59a
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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 React, { useState } from 'react';
|
||||
import { Popover } from 'antd';
|
||||
import { HelperConfiguator } from './HelperConfiguator';
|
||||
|
||||
const WithPropOver = ({ children, index }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
setOpen(newOpen);
|
||||
};
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
content={<HelperConfiguator index={index} onDelete={() => setOpen(false)} />}
|
||||
trigger={'click'}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export function Helper({ configurable, index, label }: { configurable: boolean; index: number; label: string }) {
|
||||
const Label = <div style={{ color: '#52c41a', display: 'inline-block', cursor: 'pointer' }}>{label}</div>;
|
||||
return (
|
||||
<>
|
||||
<span style={{ color: '#bfbfbf', margin: '0 5px' }}>|</span>
|
||||
{configurable ? <WithPropOver index={index}>{Label}</WithPropOver> : Label}
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 React, { useState, useMemo, useContext } from 'react';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown, Popover } from 'antd';
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import { useCompile } from '../../../hooks';
|
||||
import { useApp } from '../../../../application';
|
||||
import { addHelper } from './observables';
|
||||
|
||||
export function HelperAddition() {
|
||||
const app = useApp();
|
||||
const compile = useCompile();
|
||||
const filterOptions = app.jsonTemplateParser.filterGroups
|
||||
.sort((a, b) => a.sort - b.sort)
|
||||
.map((group) => ({
|
||||
key: group.name,
|
||||
label: compile(group.title),
|
||||
children: group.filters
|
||||
.sort((a, b) => a.sort - b.sort)
|
||||
.map((filter) => ({ key: filter.name, label: compile(filter.title) })),
|
||||
})) as MenuProps['items'];
|
||||
return (
|
||||
<>
|
||||
<span style={{ color: '#bfbfbf', margin: '0 5px' }}>|</span>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: filterOptions,
|
||||
onClick: ({ key }) => {
|
||||
addHelper({ name: key });
|
||||
},
|
||||
}}
|
||||
>
|
||||
<a onClick={(e) => e.preventDefault()}>
|
||||
<FilterOutlined style={{ color: '#52c41a' }} />
|
||||
</a>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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 React, { useState, useMemo, useContext } from 'react';
|
||||
import { useForm, FormContext, observer } from '@formily/react';
|
||||
import { createForm, onFormValuesChange, Form } from '@formily/core';
|
||||
import { uid, tval } from '@nocobase/utils/client';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { SchemaComponent } from '../../../core';
|
||||
import { helpersObs, rawHelpersObs, removeHelper } from './observables';
|
||||
import { useVariable } from '../VariableProvider';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
export const HelperConfiguator = observer(
|
||||
({ index, onDelete }: { index: number; onDelete: () => void }) => {
|
||||
const helper = helpersObs.value[index];
|
||||
const rawHelper = rawHelpersObs.value[index];
|
||||
const config = helper.config;
|
||||
const history = createMemoryHistory();
|
||||
const previousHelpers = helpersObs.value.slice(0, index);
|
||||
const { value } = useVariable();
|
||||
const inputValue = previousHelpers.reduce((value, helper) => {
|
||||
return helper.handler(value, ...helper.args);
|
||||
}, value);
|
||||
const outputValue = helpersObs.value.slice(0, index + 1).reduce((value, helper) => {
|
||||
return helper.handler(value, ...helper.args);
|
||||
}, value);
|
||||
|
||||
const useFormBlockProps = () => {
|
||||
return {
|
||||
form: createForm({
|
||||
initialValues: helper.argsMap,
|
||||
effects() {
|
||||
onFormValuesChange(
|
||||
debounce((form) => {
|
||||
rawHelper.argsMap = form.values;
|
||||
}, 500),
|
||||
);
|
||||
},
|
||||
}),
|
||||
layout: 'vertical',
|
||||
};
|
||||
};
|
||||
|
||||
const useDeleteActionProps = () => {
|
||||
const form = useForm();
|
||||
return {
|
||||
type: 'primary',
|
||||
danger: true,
|
||||
onClick: async () => {
|
||||
removeHelper({ index: index });
|
||||
onDelete();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const schema = {
|
||||
'x-uid': uid(),
|
||||
type: 'void',
|
||||
'x-component': 'CardItem',
|
||||
properties: {
|
||||
form: {
|
||||
'x-uid': uid(),
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-use-component-props': 'useFormBlockProps',
|
||||
properties: {
|
||||
'$input-value': {
|
||||
type: 'void',
|
||||
'x-component': 'div',
|
||||
'x-content': '{{ inputValue }}',
|
||||
'x-decorator': 'FormItem',
|
||||
title: tval('Input Value'),
|
||||
},
|
||||
...Object.fromEntries(
|
||||
config.uiSchema.map((param) => [
|
||||
param.name,
|
||||
{
|
||||
...param,
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
]),
|
||||
),
|
||||
'$return-value': {
|
||||
type: 'void',
|
||||
'x-component': 'div',
|
||||
'x-content': '{{ outputValue }}',
|
||||
'x-decorator': 'FormItem',
|
||||
title: tval('Return Value'),
|
||||
},
|
||||
actions: {
|
||||
type: 'void',
|
||||
title: tval('Save'),
|
||||
'x-component': 'ActionBar',
|
||||
properties: {
|
||||
delete: {
|
||||
type: 'void',
|
||||
title: tval('Delete'),
|
||||
'x-component': 'Action',
|
||||
'x-use-component-props': 'useDeleteActionProps',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Router location={history.location} navigator={history}>
|
||||
<SchemaComponent
|
||||
schema={schema}
|
||||
scope={{ useFormBlockProps, useDeleteActionProps, outputValue, inputValue }}
|
||||
basePath={['']}
|
||||
/>
|
||||
</Router>
|
||||
);
|
||||
},
|
||||
{ displayName: 'Helper' },
|
||||
);
|
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 React, { useState, useMemo, useContext } from 'react';
|
||||
import { observer } from '@formily/reactive-react';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown, Popover } from 'antd';
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import { useCompile } from '../../../hooks';
|
||||
import { useApp } from '../../../../application';
|
||||
import { addHelper } from './observables';
|
||||
import { helpersObs } from './observables';
|
||||
import { Helper } from './Helper';
|
||||
const _HelperList = () => {
|
||||
return (
|
||||
<>
|
||||
{helpersObs.value.map((helper, index) => {
|
||||
return (
|
||||
<Helper
|
||||
key={index}
|
||||
index={index}
|
||||
configurable={Boolean(helper.config.uiSchema)}
|
||||
label={helper.config.title}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const HelperList = observer(_HelperList, { displayName: 'HelperList' });
|
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { HelperList } from './HelperList';
|
||||
export { HelperAddition } from './HelperAddition';
|
@ -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 { observable } from '@formily/reactive';
|
||||
import { extractTemplateElements, createJSONTemplateParser } from '@nocobase/json-template-parser';
|
||||
type Helper = {
|
||||
name: string;
|
||||
argsMap: Record<string, any>;
|
||||
config: any;
|
||||
args: string[];
|
||||
handler: (value: any, ...args: any[]) => any;
|
||||
};
|
||||
|
||||
type RawHelper = {
|
||||
name: string;
|
||||
argsMap: Record<string, any>;
|
||||
};
|
||||
|
||||
const parser = createJSONTemplateParser();
|
||||
|
||||
export const rawHelpersObs = observable<{ value: RawHelper[] }>({ value: [] });
|
||||
|
||||
export const allHelpersConfigObs = observable<{ value: any[] }>({ value: parser.filters });
|
||||
|
||||
export const helpersObs = observable.computed(() => {
|
||||
return rawHelpersObs.value.map((helper) => {
|
||||
const config = allHelpersConfigObs.value.find((f) => f.name === helper.name);
|
||||
const args = config.uiSchema.map((param) => helper.argsMap[param.name]);
|
||||
return {
|
||||
...helper,
|
||||
config,
|
||||
args,
|
||||
handler: config.handler,
|
||||
};
|
||||
});
|
||||
}) as { value: Helper[] };
|
||||
|
||||
export const variableNameObs = observable<{ value: string }>({ value: '' });
|
||||
export const variableSegmentsObs = observable<{ value: string[] }>({ value: [] });
|
||||
|
||||
export const addHelper = ({ name }: { name: string }) => {
|
||||
rawHelpersObs.value.push({ name, argsMap: {} });
|
||||
};
|
||||
|
||||
export const removeHelper = ({ index }: { index: number }) => {
|
||||
rawHelpersObs.value.splice(index, 1);
|
||||
};
|
||||
|
||||
export const setHelpersFromTemplateStr = ({ template }: { template: string }) => {
|
||||
const { fullVariable, helpers, variableSegments } = extractTemplateElements(
|
||||
typeof template === 'string' ? template : '',
|
||||
);
|
||||
variableNameObs.value = fullVariable;
|
||||
variableSegmentsObs.value = variableSegments;
|
||||
const computedHelpers = helpers.map((helper: any) => {
|
||||
const config = allHelpersConfigObs.value.find((f) => f.name === helper.name);
|
||||
const argsMap = config.uiSchema
|
||||
? Object.fromEntries(config.uiSchema.map((param, index) => [param.name, helper.args[index]]))
|
||||
: {};
|
||||
return {
|
||||
name: helper.name,
|
||||
argsMap,
|
||||
};
|
||||
});
|
||||
rawHelpersObs.value = computedHelpers;
|
||||
};
|
@ -9,7 +9,8 @@
|
||||
|
||||
import { CloseCircleFilled, FilterOutlined } from '@ant-design/icons';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useForm } from '@formily/react';
|
||||
import { autorun } from '@formily/reactive';
|
||||
import { useForm, observer } from '@formily/react';
|
||||
import { error } from '@nocobase/utils/client';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { extractTemplateElements, composeTemplate } from '@nocobase/json-template-parser';
|
||||
@ -34,8 +35,9 @@ import { useCompile } from '../../hooks';
|
||||
import { XButton } from './XButton';
|
||||
import { useStyles } from './style';
|
||||
import { Json } from '../input';
|
||||
import { Filters, Addition, FilterContext } from './VariableFilters';
|
||||
import { VariableProvider } from './VariableProvider';
|
||||
import { setHelpersFromTemplateStr, helpersObs } from './Helpers/observables';
|
||||
import { HelperList, HelperAddition } from './Helpers';
|
||||
|
||||
const { Text } = Typography;
|
||||
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
|
||||
@ -230,33 +232,20 @@ export function Input(props: VariableInputProps) {
|
||||
hideVariableButton || (children && value != null ? true : false),
|
||||
);
|
||||
|
||||
const { fullVariable, filters, variableSegments } = useMemo(
|
||||
useEffect(() => {
|
||||
setHelpersFromTemplateStr({ template: typeof value === 'string' ? value : '' });
|
||||
}, [value]);
|
||||
const { fullVariable, helpers, variableSegments } = useMemo(
|
||||
() => extractTemplateElements(typeof value === 'string' ? value : ''),
|
||||
[value],
|
||||
);
|
||||
const onFilterAdd = useCallback(
|
||||
(filterName) => {
|
||||
onChange(composeTemplate({ fullVariable, filters: [...filters, { name: filterName, args: [] }] }));
|
||||
},
|
||||
[filters, fullVariable, onChange],
|
||||
);
|
||||
|
||||
const updateFilterParams = useCallback(
|
||||
({ filterId, params }: { filterId: number; params: any[] }) => {
|
||||
const copyedFilters = cloneDeep(filters);
|
||||
copyedFilters[filterId].args = params;
|
||||
onChange(composeTemplate({ fullVariable, filters: copyedFilters }));
|
||||
},
|
||||
[filters, fullVariable, onChange],
|
||||
);
|
||||
|
||||
const deleteFilter = useCallback(
|
||||
({ filterId }: { filterId: number }) => {
|
||||
const newFilters = filters.filter((_, index) => index !== filterId);
|
||||
onChange(composeTemplate({ fullVariable, filters: newFilters }));
|
||||
},
|
||||
[filters, fullVariable, onChange],
|
||||
);
|
||||
useEffect(() => {
|
||||
const dispose = autorun(() => {
|
||||
onChange(composeTemplate({ fullVariable, helpers: helpersObs.value }));
|
||||
});
|
||||
return dispose;
|
||||
}, [onChange, fullVariable, helpers]);
|
||||
|
||||
const parsed = useMemo(() => parseValue(variableSegments, parseOptions), [parseOptions, variableSegments]);
|
||||
const isConstant = typeof parsed === 'string';
|
||||
@ -374,7 +363,7 @@ export function Input(props: VariableInputProps) {
|
||||
if (next[1] !== type) {
|
||||
// setPrevType(next[1]);
|
||||
const newVariable = ConstantTypes[next[1]]?.default?.() ?? null;
|
||||
onChange(composeTemplate({ fullVariable: newVariable, filters }), optionPath);
|
||||
onChange(composeTemplate({ fullVariable: newVariable, helpers }), optionPath);
|
||||
}
|
||||
} else {
|
||||
if (variable) {
|
||||
@ -486,11 +475,8 @@ export function Input(props: VariableInputProps) {
|
||||
);
|
||||
})}
|
||||
<VariableProvider variableName={fullVariable}>
|
||||
<FilterContext.Provider value={{ updateFilterParams, deleteFilter, variableName: fullVariable }}>
|
||||
<Filters filters={filters} onFilterChange={onFilterAdd} />
|
||||
|
||||
{variableText.length > 0 && <Addition variable={fullVariable} onFilterAdd={onFilterAdd} />}
|
||||
</FilterContext.Provider>
|
||||
<HelperList />
|
||||
{variableText.length > 0 && <HelperAddition />}
|
||||
</VariableProvider>
|
||||
</Tag>
|
||||
</div>
|
||||
@ -546,3 +532,5 @@ export function Input(props: VariableInputProps) {
|
||||
</Space.Compact>,
|
||||
);
|
||||
}
|
||||
|
||||
// export const Input = observer(_Input, { displayName: 'VariableInput' });
|
||||
|
@ -288,7 +288,7 @@ const VariablesProvider = ({ children, filterVariables }: any) => {
|
||||
|
||||
const path = getPath(str);
|
||||
const result = await getResult(path, localVariables as VariableOption[], options);
|
||||
if (filters.length > 0) {
|
||||
if (Array.isArray(filters) && filters.length > 0) {
|
||||
result.value = filters.reduce((acc, filter) => filter.handler(...[acc, ...filter.args]), result.value);
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,11 @@ import { createJSONTemplateParser } from '../parser';
|
||||
const parser = createJSONTemplateParser();
|
||||
|
||||
describe('ctx function', () => {
|
||||
it('should handle basic context function', () => {
|
||||
const template = '{{}}';
|
||||
const variable = parser.engine.fullVariablesSync(template);
|
||||
console.log(variable);
|
||||
});
|
||||
it('should handle basic context function with state', () => {
|
||||
const template = '{{$user.id}} - {{$user.name}}';
|
||||
const data = {
|
||||
|
@ -18,27 +18,34 @@ type Filter = {
|
||||
args: string[];
|
||||
};
|
||||
|
||||
export function extractTemplateVariable(template: string) {
|
||||
export function extractTemplateVariable(template: string): string | null {
|
||||
const escapedTemplate = escape(template ?? '');
|
||||
const fullVariable = engine.fullVariablesSync(escapedTemplate)[0] ?? '';
|
||||
return revertEscape(fullVariable);
|
||||
try {
|
||||
const fullVariable = engine.fullVariablesSync(escapedTemplate)[0] ?? '';
|
||||
return revertEscape(fullVariable);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractTemplateElements(template: string) {
|
||||
const escapedTemplate = escape(template ?? '');
|
||||
const fullVariable = engine.fullVariablesSync(escapedTemplate)[0] ?? '';
|
||||
const variableSegments = engine.variableSegmentsSync(escapedTemplate)[0] ?? [];
|
||||
const parsedTemplate = engine.parse(escapedTemplate)[0] ?? {};
|
||||
try {
|
||||
const fullVariable = engine.fullVariablesSync(escapedTemplate)[0] ?? '';
|
||||
const variableSegments = engine.variableSegmentsSync(escapedTemplate)[0] ?? [];
|
||||
const parsedTemplate = engine.parse(escapedTemplate)[0] ?? {};
|
||||
const helpers =
|
||||
//@ts-ignore
|
||||
parsedTemplate?.value?.filters?.map(({ name, handler, args }) => ({
|
||||
name,
|
||||
handler,
|
||||
args: args.map((arg) => arg.content),
|
||||
})) ?? [];
|
||||
|
||||
const filters =
|
||||
//@ts-ignore
|
||||
parsedTemplate?.value?.filters?.map(({ name, handler, args }) => ({
|
||||
name,
|
||||
handler,
|
||||
args: args.map((arg) => arg.content),
|
||||
})) ?? [];
|
||||
|
||||
return revertEscape({ fullVariable, variableSegments, filters });
|
||||
return revertEscape({ fullVariable, variableSegments, helpers });
|
||||
} catch (e) {
|
||||
return { fullVariable: null, variableSegments: [], helpers: [] };
|
||||
}
|
||||
}
|
||||
|
||||
const composeFilterTemplate = (filter: Filter) => {
|
||||
@ -48,8 +55,8 @@ const composeFilterTemplate = (filter: Filter) => {
|
||||
return value;
|
||||
};
|
||||
|
||||
export const composeTemplate = ({ fullVariable, filters }: { fullVariable: string; filters: any[] }) => {
|
||||
const filtersTemplate = filters.map(composeFilterTemplate).join(' | ');
|
||||
export const composeTemplate = ({ fullVariable, helpers }: { fullVariable: string; helpers: any[] }) => {
|
||||
const filtersTemplate = helpers.map(composeFilterTemplate).join(' | ');
|
||||
const innerTemplateStr = [fullVariable, filtersTemplate].filter(Boolean).join(' | ');
|
||||
return `{{${innerTemplateStr}}}`;
|
||||
};
|
||||
|
@ -176,14 +176,14 @@ export const parseFilter = async (filter: any, opts: ParseFilterOptions = {}) =>
|
||||
if (typeof value === 'string') {
|
||||
const match = re.exec(value);
|
||||
if (match) {
|
||||
const { fullVariable: key, filters } = extractTemplateElements(value);
|
||||
const { fullVariable: key, filters: helpers } = extractTemplateElements(value);
|
||||
const val = getValuesByPath(vars, key, null);
|
||||
const field = getField?.(path);
|
||||
if (key.startsWith('$date') || key.startsWith('$nDate')) {
|
||||
const filteredNow = filters.reduce((acc, filter) => filter.handler(...[acc, ...filter.args]), now);
|
||||
const filteredNow = helpers.reduce((acc, filter) => filter.handler(...[acc, ...filter.args]), now);
|
||||
value = typeof val === 'function' ? val?.({ field, operator, timezone, now: filteredNow }) : val;
|
||||
} else {
|
||||
value = filters.reduce((acc, filter) => filter.handler(...[acc, ...filter.args]), value);
|
||||
value = helpers.reduce((acc, filter) => filter.handler(...[acc, ...filter.args]), value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user