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;
}