Merge branch 'next' into develop

This commit is contained in:
nocobase[bot] 2025-04-30 10:30:06 +00:00
commit 461525353b
11 changed files with 132 additions and 80 deletions

View File

@ -72,7 +72,7 @@ describe('SchemaInitializerDivider', () => {
expect(onSubmit).toBeCalled();
});
it('item mode', async () => {
it.skip('item mode', async () => {
const onSubmit = vitest.fn();
const Demo = () => {
return (

View File

@ -52,6 +52,7 @@ import { useBlockRequestContext, useFilterByTk, useParamsFromRecord } from '../B
import { useOperators } from '../CollectOperators';
import { useDetailsBlockContext } from '../DetailsBlockProvider';
import { TableFieldResource } from '../TableFieldProvider';
import { getVariableValue } from '../../common/getVariableValue';
export * from './useBlockHeightProps';
export * from './useDataBlockParentRecord';
@ -232,12 +233,6 @@ export function useCollectValuesToSubmit() {
]);
}
export function interpolateVariables(str: string, scope: Record<string, any>): string {
return str.replace(/\{\{\s*([a-zA-Z0-9_$.-]+?)\s*\}\}/g, (_, key) => {
return scope[key] !== undefined ? String(scope[key]) : '';
});
}
export const useCreateActionProps = () => {
const filterByTk = useFilterByTk();
const record = useCollectionRecord();
@ -287,11 +282,10 @@ export const useCreateActionProps = () => {
});
let redirectTo = rawRedirectTo;
if (rawRedirectTo) {
const { exp, scope: expScope } = await replaceVariables(rawRedirectTo, {
redirectTo = await getVariableValue(rawRedirectTo, {
variables,
localVariables: [...localVariables, { name: '$record', ctx: new Proxy(data?.data?.data, {}) }],
});
redirectTo = interpolateVariables(exp, expScope);
}
if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) {
@ -686,11 +680,11 @@ export const useCustomizeUpdateActionProps = () => {
let redirectTo = rawRedirectTo;
if (rawRedirectTo) {
const { exp, scope: expScope } = await replaceVariables(rawRedirectTo, {
// eslint-disable-next-line react-hooks/rules-of-hooks
redirectTo = await getVariableValue(rawRedirectTo, {
variables,
localVariables: [...localVariables, { name: '$record', ctx: new Proxy(result?.data?.data?.[0], {}) }],
localVariables: [...localVariables, { name: '$record', ctx: new Proxy(result?.data?.data, {}) }],
});
redirectTo = interpolateVariables(exp, expScope);
}
if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) {
@ -1050,11 +1044,11 @@ export const useUpdateActionProps = () => {
}
let redirectTo = rawRedirectTo;
if (rawRedirectTo) {
const { exp, scope: expScope } = await replaceVariables(rawRedirectTo, {
// eslint-disable-next-line react-hooks/rules-of-hooks
redirectTo = await getVariableValue(rawRedirectTo, {
variables,
localVariables: [...localVariables, { name: '$record', ctx: new Proxy(result?.data?.data?.[0], {}) }],
localVariables: [...localVariables, { name: '$record', ctx: new Proxy(result?.data?.data, {}) }],
});
redirectTo = interpolateVariables(exp, expScope);
}
if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) {

View File

@ -0,0 +1,23 @@
/**
* 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 { evaluators } from '@nocobase/evaluators/client';
import { replaceVariables } from '../schema-settings/LinkageRules/bindLinkageRulesToFiled';
export const getVariableValue = async (text: string, scopes) => {
if (!text) {
return text;
}
const { evaluate } = evaluators.get('string');
const { exp, scope: expScope } = await replaceVariables(text, scopes);
const result = evaluate(exp, { now: () => new Date().toString(), ...expScope });
return result;
};

View File

@ -10,3 +10,4 @@
export * from './AppNotFound';
export * from './SelectWithTitle';
export * from './useFieldComponentName';
export * from './getVariableValue';

View File

@ -31,7 +31,11 @@ test.describe('Link', () => {
await expect(
page.getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:link-users' }),
).toHaveCount(1);
await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:link-users' }).hover();
await page
.getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:link-users' })
.first()
.hover();
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:link-users').first().hover();
await page.getByRole('menuitem', { name: 'Edit link' }).click();
await page.getByLabel('block-item-users-URL').getByLabel('textbox').click();
await page
@ -106,7 +110,7 @@ test.describe('Link', () => {
await page.getByLabel('block-item-users-URL').getByLabel('textbox').fill(otherPageUrl);
await page.getByRole('button', { name: 'OK', exact: true }).click();
await page.getByLabel('action-Action.Link-Link-').click();
await page.getByLabel('action-Action.Link-Link-').first().click();
expect(page.url().endsWith(otherPageUrl)).toBe(true);
// 开启 “Open in new window” 选项后,点击链接按钮会在新窗口打开

View File

@ -23,7 +23,7 @@ test.describe('options of Select field in linkage rule', () => {
// 去掉联动规则恢复选项
await page.getByLabel('block-item-CardItem-general-').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:createForm-general').hover();
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
await page.getByRole('menuitem', { name: 'Field linkage rules' }).click();
await page.getByRole('switch', { name: 'On Off' }).click();
await page.getByRole('button', { name: 'OK' }).click();
await page.reload();

View File

@ -19,7 +19,8 @@ test('tabs', async ({ page, mockPage }) => {
await page.getByText('tab 1').hover();
await page.getByRole('button', { name: 'designer-drag-handler-Page-tab' }).dragTo(page.getByText('tab 2'));
await expect(page.getByText('tab 1')).toBeVisible();
await page.waitForTimeout(500);
await expect(page.getByText('tab 2')).toBeVisible();
tab1Box = await page.getByText('tab 1').boundingBox();
tab2Box = await page.getByText('tab 2').boundingBox();

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { ISchema, useField, useFieldSchema } from '@formily/react';
import { ISchema, useField, useFieldSchema, useForm } from '@formily/react';
import { isValid, uid } from '@formily/shared';
import { ModalProps, Select } from 'antd';
import React, { useCallback, useMemo } from 'react';
@ -18,7 +18,12 @@ import { useGlobalVariable } from '../../../application/hooks/useGlobalVariable'
import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings';
import { useSchemaToolbar } from '../../../application/schema-toolbar';
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager';
import { highlightBlock, startScrollEndTracking, stopScrollEndTracking, unhighlightBlock } from '../../../filter-provider/highlightBlock';
import {
highlightBlock,
startScrollEndTracking,
stopScrollEndTracking,
unhighlightBlock,
} from '../../../filter-provider/highlightBlock';
import { FlagProvider } from '../../../flag-provider';
import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
@ -38,6 +43,8 @@ import { useAllDataBlocks } from '../page/AllDataBlocksProvider';
import { useLinkageAction } from './hooks';
import { useAfterSuccessOptions } from './hooks/useGetAfterSuccessVariablesOptions';
import { requestSettingsSchema } from './utils';
import { useVariableOptions } from '../../../schema-settings/VariableInput/hooks/useVariableOptions';
import { useCollectionRecordData } from '../../../data-source';
const MenuGroup = (props) => {
return props.children;
@ -307,7 +314,7 @@ const hideDialog = (dialogClassName: string) => {
dialogWrap.style.opacity = '0';
dialogWrap.style.transition = 'opacity 0.5s ease';
}
}
};
const showDialog = (dialogClassName: string) => {
const dialogMask = document.querySelector<HTMLElement>(`.${dialogClassName} > .ant-modal-mask`);
@ -320,7 +327,7 @@ const showDialog = (dialogClassName: string) => {
dialogWrap.style.opacity = '1';
dialogWrap.style.transition = 'opacity 0.5s ease';
}
}
};
export const BlocksSelector = (props) => {
const { getAllDataBlocks } = useAllDataBlocks();
@ -330,7 +337,8 @@ export const BlocksSelector = (props) => {
// 转换 allDataBlocks 为 Select 选项
const options = useMemo(() => {
return allDataBlocks.map(block => {
return allDataBlocks
.map((block) => {
// 防止列表中出现已关闭的弹窗中的区块
if (!block.dom?.isConnected) {
return null;
@ -352,9 +360,10 @@ export const BlocksSelector = (props) => {
showDialog('dialog-after-successful-submission');
stopScrollEndTracking(block.dom);
unhighlightBlock();
}
}
}).filter(Boolean);
},
};
})
.filter(Boolean);
}, [allDataBlocks, t]);
return (
@ -367,7 +376,7 @@ export const BlocksSelector = (props) => {
onChange={props.onChange}
/>
);
}
};
export function AfterSuccess() {
const { dn } = useDesignable();
@ -380,7 +389,7 @@ export function AfterSuccess() {
return (
<SchemaSettingsModalItem
dialogRootClassName='dialog-after-successful-submission'
dialogRootClassName="dialog-after-successful-submission"
width={700}
title={t('After successful submission')}
initialValues={
@ -704,6 +713,20 @@ export const actionSettingsItems: SchemaSettingOptions['items'] = [
],
},
];
const useSecondConFirmVariables = () => {
const fieldSchema = useFieldSchema();
const form = useForm();
const record = useCollectionRecordData();
const scope = useVariableOptions({
collectionField: { uiSchema: fieldSchema },
form,
record,
uiSchema: fieldSchema,
noDisabled: true,
});
return scope;
};
export function SecondConFirm() {
const { dn } = useDesignable();
const fieldSchema = useFieldSchema();
@ -738,9 +761,11 @@ export function SecondConFirm() {
},
title: {
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component': 'Variable.RawTextArea',
title: t('Title'),
'x-component-props': {
scope: useSecondConFirmVariables,
},
'x-reactions': {
dependencies: ['enable'],
fulfill: {
@ -752,8 +777,11 @@ export function SecondConFirm() {
},
content: {
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component': 'Variable.RawTextArea',
title: t('Content'),
'x-component-props': {
scope: useSecondConFirmVariables,
},
'x-reactions': {
dependencies: ['enable'],
fulfill: {

View File

@ -50,6 +50,7 @@ import { ActionContextProps, ActionProps, ComposedAction } from './types';
import { linkageAction, setInitialActionState } from './utils';
import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant';
import { BlockContext } from '../../../block-provider/BlockProvider';
import { getVariableValue } from '../../../common/getVariableValue';
// 这个要放到最下面,否则会导致前端单测失败
import { useApp } from '../../../application';
@ -450,18 +451,24 @@ const RenderButton = ({
const { t } = useTranslation();
const { isPopupVisibleControlledByURL } = usePopupSettings();
const { openPopup } = usePopupUtils();
const variables = useVariables();
const localVariables = useLocalVariables();
const openPopupRef = useRef(null);
openPopupRef.current = openPopup;
const scopes = {
variables,
localVariables,
};
const handleButtonClick = useCallback(
(e: React.MouseEvent, checkPortal = true) => {
async (e: React.MouseEvent, checkPortal = true) => {
if (checkPortal && isPortalInBody(e.target as Element)) {
return;
}
e.preventDefault();
e.stopPropagation();
const resultTitle = await getVariableValue(confirm?.title, scopes);
const resultContent = await getVariableValue(confirm?.content, scopes);
if (!disabled && aclCtx) {
const onOk = () => {
if (onClick) {
@ -489,8 +496,8 @@ const RenderButton = ({
};
if (confirm?.enable !== false && confirm?.content) {
modal.confirm({
title: t(confirm.title, { title: confirmTitle || title || field?.title }),
content: t(confirm.content, { title: confirmTitle || title || field?.title }),
title: t(resultTitle, { title: confirmTitle || title || field?.title }),
content: t(resultContent, { title: confirmTitle || title || field?.title }),
onOk,
});
} else {

View File

@ -46,6 +46,7 @@ export function AccessControl() {
'x-decorator': 'ACLActionProvider',
},
});
fieldSchema['x-decorator'] = 'ACLActionProvider';
dn.refresh();
}
}, []);

View File

@ -20,8 +20,7 @@ import {
useContextVariable,
useLocalVariables,
useVariables,
replaceVariables,
interpolateVariables,
getVariableValue,
} from '@nocobase/client';
import { isURL } from '@nocobase/utils/client';
import { App } from 'antd';
@ -84,19 +83,13 @@ export const useCustomizeRequestActionProps = () => {
},
responseType: fieldSchema['x-response-type'] === 'stream' ? 'blob' : 'json',
});
try {
const { exp, scope: expScope } = await replaceVariables(successMessage, {
successMessage = await getVariableValue(successMessage, {
variables,
localVariables: [
...localVariables,
{ name: '$nResponse', ctx: new Proxy({ ...res?.data?.data, ...res?.data }, {}) },
],
});
successMessage = interpolateVariables(exp, expScope);
} catch (error) {
console.log(error);
}
if (res.headers['content-disposition']) {
const contentDisposition = res.headers['content-disposition'];
const utf8Match = contentDisposition.match(/filename\*=utf-8''([^;]+)/i);