fix: refresh issues (#5838)

* fix(associationField): fix the issue where the configuration popup does not show when reopened

* test: add e2e test

* refactor: extract template code

* chore(e2e): fix e2e error
This commit is contained in:
Zeke Zhang 2024-12-10 00:07:34 +08:00 committed by GitHub
parent 3a2abcb9a3
commit 3140edf798
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 364 additions and 30 deletions

View File

@ -86,5 +86,6 @@ export {
CollectionFieldUISchemaProvider,
IsInNocoBaseRecursionFieldContext,
NocoBaseRecursionField,
RefreshComponentProvider,
useRefreshFieldSchema,
} from './formily/NocoBaseRecursionField';

View File

@ -141,6 +141,7 @@ test.describe('configure actions', () => {
await expect(
page.getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname').getByRole('textbox'),
).toBeHidden();
await page.mouse.move(600, 0);
// 再次点击添加按钮,默认值应该正常显示出来
await page.locator('.nb-sub-table-addNew').click();

View File

@ -8,7 +8,10 @@
*/
import { expect, test } from '@nocobase/test/e2e';
import { differentURL_DifferentPopupContent } from './templatesOfBug';
import {
differentURL_DifferentPopupContent,
popupConfigurationShouldPersistAcrossDifferentRowsInTheSameColumn,
} from './templatesOfBug';
test.describe('popup opened by clicking the association field', async () => {
test('different URL, different popup content', async ({ page, mockPage, mockRecord }) => {
@ -28,4 +31,91 @@ test.describe('popup opened by clicking the association field', async () => {
await page.goto(prevURL);
await expect(page.getByLabel('block-item-CollectionField-')).toHaveText('Role name:Admin');
});
test('popup configuration should persist across different rows in the same column', async ({
page,
mockPage,
mockRecord,
}) => {
const nocoPage = await mockPage(popupConfigurationShouldPersistAcrossDifferentRowsInTheSameColumn).waitForInit();
await mockRecord('users', { roles: [{ name: 'member', title: 'Member' }], nickname: 'test popup' });
await nocoPage.goto();
// 1. 新建一个 View 按钮
await page.getByRole('button', { name: 'Actions', exact: true }).hover();
await page.getByLabel('designer-schema-initializer-TableV2.Column-fieldSettings:TableColumn-users').hover();
await page.getByRole('menuitem', { name: 'View' }).click();
// 2. 点击第一行的 View 按钮,打开弹窗,然后增加一个 Markdown 区块
await page.getByLabel('action-Action.Link-View-view-users-table-0').click();
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'form Markdown' }).click();
await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible();
// 3. 关闭弹窗,然后点击第二行的 View 按钮,打开弹窗,弹窗中的 Markdown 区块应该和第一行的一样
await page.getByLabel('drawer-Action.Container-users-View record-mask').click();
await page.getByLabel('action-Action.Link-View-view-users-table-1').click();
await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible();
// 4. 关闭弹窗,然后再点击第一行的 View 按钮,打开弹窗,弹窗中的 Markdown 区块依然存在
await page.getByLabel('drawer-Action.Container-users-View record-mask').click();
await page.getByLabel('action-Action.Link-View-view-users-table-0').click();
await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible();
await page.getByLabel('drawer-Action.Container-users-View record-mask').click();
// -----------------------------------------------------------------------------------------
// 1. 新建一个关系字段Roles
await page.getByLabel('schema-initializer-TableV2-').hover();
await page.getByRole('menuitem', { name: 'Roles' }).click();
await page.mouse.move(300, 0);
// 2. 点击第一行的 Roles 字段,打开弹窗,然后增加一个 Markdown 区块
await page.getByText('root').click();
await page.getByLabel('schema-initializer-Grid-popup').click();
await page.getByRole('menuitem', { name: 'form Markdown' }).click();
await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible();
// 3. 关闭弹窗,然后点击第二行的 Roles 字段,打开弹窗,弹窗中的 Markdown 区块应该和第一行的一样
await page.getByLabel('drawer-AssociationField.Viewer-roles-View record-mask').click();
await page.getByText('member').nth(1).click();
await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible();
// 4. 关闭弹窗,然后再点击第一行的 Roles 字段,打开弹窗,弹窗中的 Markdown 区块依然存在
await page.getByLabel('drawer-AssociationField.Viewer-roles-View record-mask').click();
await page.getByText('root').click();
await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible();
await page.getByLabel('drawer-AssociationField.Viewer-roles-View record-mask').click();
// ---------------------------------------------------------------------------------------------------
// 1. 新建一个单行文本字段nickname并开启 Enable link
await page.getByLabel('schema-initializer-TableV2-').hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click();
await page.mouse.move(300, 0);
// 开启 Enable link
await page.getByRole('button', { name: 'Nickname' }).hover();
await page
.getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-users' })
.hover();
await page.getByRole('menuitem', { name: 'Enable link' }).click();
await page.mouse.move(300, 0);
// 2. 点击第一行的 nickname 字段,打开弹窗,然后增加一个 Markdown 区块
await page.getByRole('button', { name: 'Super Admin' }).locator('a').click();
await page.getByLabel('schema-initializer-Grid-popup').click();
await page.getByRole('menuitem', { name: 'form Markdown' }).click();
await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible();
// 3. 关闭弹窗,然后点击第二行的 nickname 字段,打开弹窗,弹窗中的 Markdown 区块应该和第一行的一样
await page.getByLabel('drawer-Action.Container-users-View record-mask').click();
await page.getByRole('button', { name: 'test popup' }).locator('a').click();
await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible();
// 4. 关闭弹窗,然后再点击第一行的 nickname 字段,打开弹窗,弹窗中的 Markdown 区块依然存在
await page.getByLabel('drawer-Action.Container-users-View record-mask').click();
await page.getByRole('button', { name: 'Super Admin' }).locator('a').click();
await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible();
await page.getByLabel('drawer-Action.Container-users-View record-mask').click();
});
});

View File

@ -8682,3 +8682,152 @@ export const hideColumnBasic = {
'x-index': 1,
},
};
export const popupConfigurationShouldPersistAcrossDifferentRowsInTheSameColumn = {
pageSchema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Page',
properties: {
rccgypv911m: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'page:addBlock',
properties: {
githmw0vywe: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '1.6.0-alpha.3',
properties: {
k76036oekiy: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '1.6.0-alpha.3',
properties: {
w6wzrye5ub7: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TableBlockProvider',
'x-acl-action': 'users:list',
'x-use-decorator-props': 'useTableBlockDecoratorProps',
'x-decorator-props': {
collection: 'users',
dataSource: 'main',
action: 'list',
params: {
pageSize: 20,
},
rowKey: 'id',
showIndex: true,
dragSort: false,
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:table',
'x-component': 'CardItem',
'x-filter-targets': [],
'x-app-version': '1.6.0-alpha.3',
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'table:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
'x-app-version': '1.6.0-alpha.3',
'x-uid': 'cr9fvkuyyq2',
'x-async': false,
'x-index': 1,
},
'4c9c8525dks': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'array',
'x-initializer': 'table:configureColumns',
'x-component': 'TableV2',
'x-use-component-props': 'useTableBlockProps',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
},
'x-app-version': '1.6.0-alpha.3',
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("Actions") }}',
'x-action-column': 'actions',
'x-decorator': 'TableV2.Column.ActionBar',
'x-component': 'TableV2.Column',
'x-toolbar': 'TableColumnSchemaToolbar',
'x-initializer': 'table:configureItemActions',
'x-settings': 'fieldSettings:TableColumn',
'x-toolbar-props': {
initializer: 'table:configureItemActions',
},
'x-app-version': '1.6.0-alpha.3',
properties: {
b0umb64pa7m: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
'x-app-version': '1.6.0-alpha.3',
'x-uid': 'uti3lm42fk5',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'r69a6dptvyb',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'v3yjph96rjs',
'x-async': false,
'x-index': 2,
},
},
'x-uid': '6nuwmd062mf',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '4b9z6v33m91',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'wri4ylqnqw3',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '5e7ud75szwt',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '38mzn4lv8by',
'x-async': true,
'x-index': 1,
},
};

View File

@ -51,5 +51,6 @@ test.describe('tree table block schema settings', () => {
async function showSettingsMenu(page: Page) {
await page.getByLabel('block-item-CardItem-treeCollection-table').hover();
await page.getByLabel('designer-schema-settings-CardItem-TableBlockDesigner-treeCollection').hover();
// hover 方法有时会失效,所以这里使用 click 方法。原因未知
await page.getByLabel('designer-schema-settings-CardItem-TableBlockDesigner-treeCollection').click();
}

View File

@ -9,7 +9,7 @@
import { observer, useFieldSchema } from '@formily/react';
import { toArr } from '@formily/shared';
import React, { Fragment, useRef } from 'react';
import React, { Fragment, useCallback, useRef } from 'react';
import { useDesignable } from '../../';
import { useCollectionManager_deprecated } from '../../../collection-manager';
import { useCollectionRecordData } from '../../../data-source/collection-record/CollectionRecordProvider';
@ -50,6 +50,13 @@ const ButtonTabList: React.FC<ButtonListProps> = observer(
const isTreeCollection = targetCollection?.template === 'tree';
const { openPopup } = usePopupUtils();
const recordData = useCollectionRecordData();
const needWaitForFieldSchemaUpdatedRef = useRef(false);
const fieldSchemaRef = useRef(fieldSchema);
fieldSchemaRef.current = fieldSchema;
const getCustomActionSchema = useCallback(() => {
return fieldSchemaRef.current;
}, []);
const renderRecords = () =>
toArr(props.value).map((record, index, arr) => {
@ -77,13 +84,28 @@ const ButtonTabList: React.FC<ButtonListProps> = observer(
props.setBtnHover(true);
e.stopPropagation();
e.preventDefault();
if (designable) {
if (designable && !fieldSchema.properties) {
insertViewer(schema.Viewer);
needWaitForFieldSchemaUpdatedRef.current = true;
}
if (needWaitForFieldSchemaUpdatedRef.current) {
// When first inserting, the fieldSchema instance will be updated to a new instance.
// We need to wait for the instance update before opening the popup to prevent configuration loss.
setTimeout(() => {
openPopup({
recordData: record,
parentRecordData: recordData,
customActionSchema: getCustomActionSchema(),
});
needWaitForFieldSchemaUpdatedRef.current = false;
});
} else {
openPopup({
recordData: record,
parentRecordData: recordData,
});
}
ellipsisWithTooltipRef?.current?.setPopoverVisible(false);
}}
>

View File

@ -10,7 +10,7 @@
import { useField, useFieldSchema } from '@formily/react';
import { toArr } from '@formily/shared';
import _ from 'lodash';
import React, { FC, Fragment, useEffect, useMemo, useRef, useState } from 'react';
import React, { FC, Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDesignable } from '../../';
import { WithoutTableFieldResource } from '../../../block-provider';
import { CollectionRecordProvider, useCollectionManager, useCollectionRecordData } from '../../../data-source';
@ -86,6 +86,13 @@ const RenderRecord = React.memo(
}) => {
const [loading, setLoading] = useState(true);
const [result, setResult] = useState<React.ReactNode[]>([]);
const needWaitForFieldSchemaUpdatedRef = useRef(false);
const fieldSchemaRef = useRef(fieldSchema);
fieldSchemaRef.current = fieldSchema;
const getCustomActionSchema = useCallback(() => {
return fieldSchemaRef.current;
}, []);
// The map method here maybe quite time-consuming, especially in table blocks.
// Therefore, we use an asynchronous approach to render the list,
@ -122,11 +129,23 @@ const RenderRecord = React.memo(
setBtnHover(true);
e.stopPropagation();
e.preventDefault();
if (designable) {
if (designable && !fieldSchema.properties) {
insertViewer(schema.Viewer);
needWaitForFieldSchemaUpdatedRef.current = true;
}
if (fieldSchema.properties) {
if (needWaitForFieldSchemaUpdatedRef.current) {
// When first inserting, the fieldSchema instance will be updated to a new instance.
// We need to wait for the instance update before opening the popup to prevent configuration loss.
setTimeout(() => {
openPopup({
recordData: record,
parentRecordData: recordData,
customActionSchema: getCustomActionSchema(),
});
});
needWaitForFieldSchemaUpdatedRef.current = false;
} else if (fieldSchema.properties) {
openPopup({
recordData: record,
parentRecordData: recordData,
@ -164,6 +183,7 @@ const RenderRecord = React.memo(
setBtnHover,
snapshot,
value,
getCustomActionSchema,
]);
if (loading) {

View File

@ -226,6 +226,7 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginat
width: columnHidden && !designable ? 0 : columnSchema['x-component-props']?.width || 100,
render: (value, record, index) => {
return (
<RefreshComponentProvider refresh={refresh}>
<TableCellRender
record={record}
columnSchema={columnSchema}
@ -235,6 +236,7 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginat
field={field}
index={index}
/>
</RefreshComponentProvider>
);
},
onCell: (record, rowIndex) => {

View File

@ -9,7 +9,7 @@
import { useField, useFieldSchema } from '@formily/react';
import { cloneDeep } from 'lodash';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import { ActionContextProvider, SchemaComponentOptions, useActionContext, useDesignable } from '../../';
import { PopupVisibleProvider } from '../../antd/page/PagePopups';
import { usePopupUtils } from '../../antd/page/pagePopupUtils';
@ -55,14 +55,35 @@ function withPopupWrapper<T>(WrappedComponent: React.ComponentType<T>) {
const { enableLink, openMode, openSize } = fieldSchema?.['x-component-props'] || {};
const { visibleWithURL, setVisibleWithURL } = usePopupUtils();
const { openPopup } = usePopupUtils();
const needWaitForFieldSchemaUpdatedRef = useRef(false);
const fieldSchemaRef = useRef(fieldSchema);
fieldSchemaRef.current = fieldSchema;
const getCustomActionSchema = useCallback(() => {
return fieldSchemaRef.current;
}, []);
const handleClick = useCallback(() => {
if (!fieldSchema.properties) {
insertPopup(popupSchema);
needWaitForFieldSchemaUpdatedRef.current = true;
}
if (needWaitForFieldSchemaUpdatedRef.current) {
// When first inserting, the fieldSchema instance will be updated to a new instance.
// We need to wait for the instance update before opening the popup to prevent configuration loss.
setTimeout(() => {
openPopup({
customActionSchema: getCustomActionSchema(),
});
});
needWaitForFieldSchemaUpdatedRef.current = false;
// Only open the popup when the popup schema exists
if (fieldSchema.properties) {
} else if (fieldSchema.properties) {
openPopup();
}
}, [fieldSchema, insertPopup, openPopup]);
}, [fieldSchema, insertPopup, openPopup, getCustomActionSchema]);
const { setSubmitted } = ctx;
const handleVisibleChange = useCallback(

View File

@ -16,6 +16,7 @@ import {
NocoBaseRecursionField,
PopupSettingsProvider,
RecordProvider,
RefreshComponentProvider,
TabsContextProvider,
fetchTemplateData,
useACLActionParamsContext,
@ -30,6 +31,7 @@ import {
useRecord,
} from '@nocobase/client';
import { App, Button } from 'antd';
import _ from 'lodash';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -209,7 +211,9 @@ export const DuplicateAction = observer(
<RecordProvider record={{ ...parentRecordData, __collection: duplicateCollection || __collection }}>
<ActionContextProvider value={{ ...ctx, visible, setVisible }}>
<PopupSettingsProvider enableURL={false}>
<RefreshComponentProvider refresh={_.noop}>
<NocoBaseRecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties />
</RefreshComponentProvider>
</PopupSettingsProvider>
</ActionContextProvider>
</RecordProvider>

View File

@ -58,6 +58,25 @@ test.describe('direct duplicate & copy into the form and continue to fill in', (
await page.getByLabel('schema-initializer-ActionBar-createForm:configureActions-general').click();
await page.getByRole('menuitem', { name: 'Submit' }).click();
await page.getByLabel('drawer-Action.Container-general-Duplicate-mask').click();
// 再次打开弹窗,刚配置的字段应该还在
await page.getByLabel('action-Action.Link-Duplicate-duplicate-general-table-0').click();
await expect(
page.getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText'),
).toBeVisible();
await expect(
page.getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-oneToOneBelongsTo'),
).toBeVisible();
await expect(
page.getByLabel('block-item-CollectionField-general-form-general.oneToOneHasOne-oneToOneHasOne'),
).toBeVisible();
await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne-manyToOne')).toBeVisible();
await expect(
page.getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany'),
).toBeVisible();
await expect(page.getByLabel('action-Action-Submit-submit-general-form')).toBeVisible();
await page.getByLabel('drawer-Action.Container-general-Duplicate-mask').click();
//同步表单字段
await page.getByLabel('action-Action.Link-Duplicate-duplicate-general-table-0').hover();
await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-Action.Designer-general' }).hover();

View File

@ -18,7 +18,8 @@ test.describe('form item & filter form', () => {
page,
showMenu: async () => {
await page.getByLabel('block-item-CollectionField-general-filter-form-general.manyToMany-manyToMany').hover();
await page.getByRole('button', { name: 'designer-schema-settings-CollectionField' }).hover();
// hover 方法有时会失效,所以这里使用 click 方法。原因未知
await page.getByRole('button', { name: 'designer-schema-settings-CollectionField' }).click();
},
supportedOptions: [
'Edit field title',

View File

@ -27,7 +27,8 @@ test.describe('form item & filter form', () => {
page,
showMenu: async () => {
await page.getByLabel('block-item-CollectionField-general-filter-form-general.manyToOne-manyToOne').hover();
await page.getByRole('button', { name: 'designer-schema-settings-CollectionField' }).hover();
// hover 方法有时会失效,所以这里使用 click 方法。原因未知
await page.getByRole('button', { name: 'designer-schema-settings-CollectionField' }).click();
},
supportedOptions: [
'Edit field title',
@ -48,7 +49,8 @@ test.describe('form item & filter form', () => {
page,
showMenu: async () => {
await page.getByLabel('block-item-CollectionField-general-filter-form-general.manyToOne-manyToOne').hover();
await page.getByRole('button', { name: 'designer-schema-settings-CollectionField' }).hover();
// hover 方法有时会失效,所以这里使用 click 方法。原因未知
await page.getByRole('button', { name: 'designer-schema-settings-CollectionField' }).click();
},
supportedOptions: [
'Edit field title',

View File

@ -125,7 +125,8 @@ test.describe('form item & filter form', () => {
page,
showMenu: async () => {
await page.getByLabel('block-item-CollectionField-general-filter-form-general.oneToMany-oneToMany').hover();
await page.getByRole('button', { name: 'designer-schema-settings-CollectionField' }).hover();
// hover 方法有时会失效,所以这里使用 click 方法。原因未知
await page.getByRole('button', { name: 'designer-schema-settings-CollectionField' }).click();
},
supportedOptions: [
'Edit field title',