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 new file mode 100644 index 0000000000..3b4f3c8882 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/variable/Helpers/Helper.tsx @@ -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 ( + setOpen(false)} />} + trigger={'click'} + > + {children} + + ); +}; + +export function Helper({ configurable, index, label }: { configurable: boolean; index: number; label: string }) { + const Label =
{label}
; + return ( + <> + | + {configurable ? {Label} : Label} + + ); +} diff --git a/packages/core/client/src/schema-component/antd/variable/Helpers/HelperAddition.tsx b/packages/core/client/src/schema-component/antd/variable/Helpers/HelperAddition.tsx new file mode 100644 index 0000000000..d9105ee4a9 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/variable/Helpers/HelperAddition.tsx @@ -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 ( + <> + | + { + addHelper({ name: key }); + }, + }} + > + e.preventDefault()}> + + + + + ); +} diff --git a/packages/core/client/src/schema-component/antd/variable/Helpers/HelperConfiguator.tsx b/packages/core/client/src/schema-component/antd/variable/Helpers/HelperConfiguator.tsx new file mode 100644 index 0000000000..0ecad3e8e6 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/variable/Helpers/HelperConfiguator.tsx @@ -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 ( + + + + ); + }, + { displayName: 'Helper' }, +); 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 new file mode 100644 index 0000000000..42d743be35 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/variable/Helpers/HelperList.tsx @@ -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 ( + + ); + })} + + ); +}; + +export const HelperList = observer(_HelperList, { displayName: 'HelperList' }); diff --git a/packages/core/client/src/schema-component/antd/variable/Helpers/index.ts b/packages/core/client/src/schema-component/antd/variable/Helpers/index.ts new file mode 100644 index 0000000000..3d4b38e99b --- /dev/null +++ b/packages/core/client/src/schema-component/antd/variable/Helpers/index.ts @@ -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'; 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 new file mode 100644 index 0000000000..825872ca92 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/variable/Helpers/observables/index.ts @@ -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; + config: any; + args: string[]; + handler: (value: any, ...args: any[]) => any; +}; + +type RawHelper = { + name: string; + argsMap: Record; +}; + +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; +}; 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 9f4876af4d..8818fb76f4 100644 --- a/packages/core/client/src/schema-component/antd/variable/Input.tsx +++ b/packages/core/client/src/schema-component/antd/variable/Input.tsx @@ -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) { ); })} - - - - {variableText.length > 0 && } - + + {variableText.length > 0 && } @@ -546,3 +532,5 @@ export function Input(props: VariableInputProps) { , ); } + +// export const Input = observer(_Input, { displayName: 'VariableInput' }); diff --git a/packages/core/client/src/variables/VariablesProvider.tsx b/packages/core/client/src/variables/VariablesProvider.tsx index 4019bb34f8..dff152ddef 100644 --- a/packages/core/client/src/variables/VariablesProvider.tsx +++ b/packages/core/client/src/variables/VariablesProvider.tsx @@ -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); } diff --git a/packages/core/json-template-parser/src/__tests__/ctx-func.test.ts b/packages/core/json-template-parser/src/__tests__/ctx-func.test.ts index 2463cc644d..51c6a7e6e0 100644 --- a/packages/core/json-template-parser/src/__tests__/ctx-func.test.ts +++ b/packages/core/json-template-parser/src/__tests__/ctx-func.test.ts @@ -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 = { diff --git a/packages/core/json-template-parser/src/utils/index.ts b/packages/core/json-template-parser/src/utils/index.ts index efbbb22764..0d8f06cf49 100644 --- a/packages/core/json-template-parser/src/utils/index.ts +++ b/packages/core/json-template-parser/src/utils/index.ts @@ -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}}}`; }; diff --git a/packages/core/utils/src/parse-filter.ts b/packages/core/utils/src/parse-filter.ts index dd568456c8..71d5164d47 100644 --- a/packages/core/utils/src/parse-filter.ts +++ b/packages/core/utils/src/parse-filter.ts @@ -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; }