From de7b75fea71893518df1d27b3faf2e3eb0be8194 Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Tue, 9 Apr 2024 16:07:14 +0800 Subject: [PATCH] fix(BulkForm): should be required when switching to 'Changed to' (#3965) * fix(BulkForm): should be required when switching to 'Changed to' * fix: clear error on value is empty * chore: optimize unit test log --- packages/core/cli/src/commands/test.js | 1 - .../client/src/block-provider/hooks/index.ts | 4 +- .../__e2e__/form-edit/bulkEditForm.test.ts | 62 +++ .../form/__e2e__/form-edit/templatesOfBug.ts | 393 ++++++++++++++++++ .../src/client/component/BulkEditField.tsx | 34 +- .../src/client/utils.tsx | 48 +-- 6 files changed, 497 insertions(+), 45 deletions(-) create mode 100644 packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/bulkEditForm.test.ts diff --git a/packages/core/cli/src/commands/test.js b/packages/core/cli/src/commands/test.js index 56ee55b7b19..2b7b738577e 100644 --- a/packages/core/cli/src/commands/test.js +++ b/packages/core/cli/src/commands/test.js @@ -20,7 +20,6 @@ function addTestCommand(name, cli) { .arguments('[paths...]') .allowUnknownOption() .action(async (paths, opts) => { - process.argv.push('--disable-console-intercept'); if (name === 'test:server') { process.env.TEST_ENV = 'server-side'; } else if (name === 'test:client') { diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 9d6f1d19161..9d0143b2967 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -13,7 +13,6 @@ import { useReactToPrint } from 'react-to-print'; import { AssociationFilter, useCollectionRecord, - useDataLoadingMode, useDataSourceHeaders, useFormActiveFields, useFormBlockContext, @@ -188,7 +187,7 @@ export const useCreateActionProps = () => { const filterByTk = useFilterByTk(); const record = useCollectionRecord(); const form = useForm(); - const { field, resource, __parent } = useBlockRequestContext(); + const { field, resource } = useBlockRequestContext(); const { setVisible, setSubmitted, setFormValueChanged } = useActionContext(); const navigate = useNavigate(); const actionSchema = useFieldSchema(); @@ -200,7 +199,6 @@ export const useCreateActionProps = () => { const collectValues = useCollectValuesToSubmit(); const action = record.isNew ? actionField.componentProps.saveMode || 'create' : 'update'; const filterKeys = actionField.componentProps.filterKeys?.checked || []; - const dataLoadingMode = useDataLoadingMode(); return { async onClick() { diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/bulkEditForm.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/bulkEditForm.test.ts new file mode 100644 index 00000000000..ab9f9df55e3 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/bulkEditForm.test.ts @@ -0,0 +1,62 @@ +import { expect, test } from '@nocobase/test/e2e'; +import { T3924 } from './templatesOfBug'; + +test.describe('bulk edit form', () => { + // https://nocobase.height.app/T-3924/description + test('should be required when switching to "Changed to"', async ({ page, mockPage }) => { + await mockPage(T3924).goto(); + + // 1. 先选中一条记录 + await page.getByLabel('table-index-').hover(); + await page.getByLabel('table-index-').getByLabel('checkbox').check(); + await page.getByLabel('schema-initializer-TableV2-').hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + + // 2. 打开弹窗,显示出批量编辑表单 + await page.getByLabel('action-Action-Bulk edit-').click(); + + // 默认为 "Changed to" 模式,此时应该显示字段是必填的 + await expect(page.getByLabel('block-item-BulkEditField-').getByText('*')).toBeVisible(); + + // 3. 输入值,点击提交 + await page.getByLabel('block-item-BulkEditField-').getByRole('textbox').fill('123'); + await page.getByRole('button', { name: 'Submit' }).click(); + + // 4. Table 中的值应该被改变 + await expect(page.getByRole('button', { name: '123' })).toBeVisible(); + }); + + test('should be success to submit', async ({ page, mockPage }) => { + await mockPage(T3924).goto(); + + // 1. 打开弹窗,显示出批量编辑表单 + await page.getByLabel('action-Action-Bulk edit-').click(); + + // 默认为 "Changed to" 模式,此时应该显示字段是必填的 + await expect(page.getByLabel('block-item-BulkEditField-').getByText('*')).toBeVisible(); + + // 2. 切换为其它模式,此时应该不显示字段是必填的 + await page.getByLabel('block-item-BulkEditField-').locator('.ant-select-selector').click(); + await page.getByRole('option', { name: 'Remains the same' }).click(); + await expect(page.getByLabel('block-item-BulkEditField-').getByText('*')).toBeHidden(); + + // 3. 再切换为 "Changed to" 模式,此时应该显示字段是必填的 + await page.getByLabel('block-item-BulkEditField-').locator('.ant-select-selector').click(); + await page.getByRole('option', { name: 'Changed to' }).click(); + await expect(page.getByLabel('block-item-BulkEditField-').getByText('*')).toBeVisible(); + + // 4. 点击提交按钮,应该提示一个错误 + await page.getByRole('button', { name: 'Submit' }).click(); + await expect(page.getByLabel('block-item-BulkEditField-').getByText('The field value is required')).toBeVisible(); + + // 5. 输入值后,应该不再提示错误 + await page.getByLabel('block-item-BulkEditField-').getByRole('textbox').fill('123'); + await expect(page.getByLabel('block-item-BulkEditField-').getByText('The field value is required')).toBeHidden(); + // 将鼠标指针移除 Submit 按钮,防止显示右上角的图标,不然影响 Locator 的定位 + await page.mouse.move(-300, 0); + await page.getByRole('button', { name: 'Submit' }).click(); + + // 6. 由于没有选中任何记录,应该提示一个错误 + await expect(page.getByText('Please select the records to')).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts index 3f10631b62c..4579fa064ad 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts @@ -697,3 +697,396 @@ export const T3825: PageConfig = { 'x-index': 1, }, }; + +export const T3924: PageConfig = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + 'x-app-version': '0.21.0-alpha.6', + properties: { + ji64zgsn9az: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + 'x-app-version': '0.21.0-alpha.6', + properties: { + '8pbtk3emqu1': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '0.21.0-alpha.6', + properties: { + '9o9in8r57e1': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '0.21.0-alpha.6', + properties: { + tywqnyoxty5: { + _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': '0.21.0-alpha.6', + 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': '0.21.0-alpha.6', + properties: { + ao2lpw4ea7u: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Bulk edit")}}', + 'x-component': 'Action', + 'x-action': 'customize:bulkEdit', + 'x-action-settings': { + updateMode: 'selected', + }, + 'x-component-props': { + openMode: 'drawer', + icon: 'EditOutlined', + }, + 'x-align': 'right', + 'x-decorator': 'ACLActionProvider', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:bulkEdit', + 'x-acl-action': 'update', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-app-version': '0.21.0-alpha.6', + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Bulk edit")}}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + 'x-app-version': '0.21.0-alpha.6', + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + 'x-initializer-props': { + gridInitializer: 'popup:bulkEdit:addBlock', + }, + 'x-app-version': '0.21.0-alpha.6', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Bulk edit")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + 'x-app-version': '0.21.0-alpha.6', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:bulkEdit:addBlock', + 'x-app-version': '0.21.0-alpha.6', + properties: { + gxsipxe2f0w: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '0.21.0-alpha.6', + properties: { + wfhpt9er7cd: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '0.21.0-alpha.6', + properties: { + xftn39tlrt4: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-acl-action': 'users:create', + 'x-decorator': 'FormBlockProvider', + 'x-decorator-props': { + dataSource: 'main', + collection: 'users', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:createForm', + 'x-component': 'CardItem', + 'x-app-version': '0.21.0-alpha.6', + properties: { + fhv7wvpulwo: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useCreateFormBlockProps', + 'x-app-version': '0.21.0-alpha.6', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'bulkEditForm:configureFields', + 'x-app-version': '0.21.0-alpha.6', + properties: { + c0qzdlpwiq7: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '0.21.0-alpha.6', + properties: { + ziysu45gav9: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '0.21.0-alpha.6', + properties: { + nickname: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + title: '{{t("Nickname")}}', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:BulkEditFormItem', + 'x-component': 'BulkEditField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'users.nickname', + 'x-app-version': '0.21.0-alpha.6', + 'x-uid': 't8dfuwjgiqb', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'pew4adjvp82', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'dds031fmtez', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'l5ytkwkmndn', + 'x-async': false, + 'x-index': 1, + }, + '26pk7ncm53y': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'bulkEditForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + marginTop: 24, + }, + }, + 'x-app-version': '0.21.0-alpha.6', + properties: { + uhoi8j6ysns: { + _isJSONSchemaObject: true, + version: '2.0', + title: '{{ t("Submit") }}', + 'x-action': 'submit', + 'x-component': 'Action', + 'x-use-component-props': + 'useCustomizeBulkEditActionProps', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:updateSubmit', + 'x-component-props': { + type: 'primary', + htmlType: 'submit', + }, + 'x-action-settings': {}, + type: 'void', + 'x-app-version': '0.21.0-alpha.6', + 'x-uid': 'wf55hivy0cu', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'zfwfakm8u0o', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'k80pf9stib8', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'bbpv3vcs9lb', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ex2q632qg3y', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '1mj8u6jyhc4', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '4atik12fpe7', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '21i8gbydvu2', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ek87pvawoex', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '8enlqbu6y24', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'czx3xeegqeh', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '0wursfenbt4', + 'x-async': false, + 'x-index': 1, + }, + jp09zgyzczb: { + _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': '0.21.0-alpha.6', + 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-designer': 'TableV2.ActionColumnDesigner', + 'x-initializer': 'table:configureItemActions', + 'x-app-version': '0.21.0-alpha.6', + properties: { + '99d0rkxwutq': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '0.21.0-alpha.6', + 'x-uid': 'pmo5ev6mqo6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'jdvqrqmqw7s', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'j35biuztvcs', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '0lurkpkvtn6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'pf0t5suyqae', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '2dnn5cxni2y', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'jnozuv3l2pv', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'rdps8w891n5', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx index 7f6ec631de0..1f46abf11ce 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx +++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx @@ -4,8 +4,8 @@ import { connect, useField, useFieldSchema } from '@formily/react'; import { merge } from '@formily/shared'; import { CollectionFieldProvider, - useCollection_deprecated, useCollectionField_deprecated, + useCollection_deprecated, useCompile, useComponent, useFormBlockContext, @@ -21,17 +21,12 @@ export const DeletedField = () => { const InternalField: React.FC = (props) => { const field = useField(); const fieldSchema = useFieldSchema(); - const { name, interface: interfaceType, uiSchema } = useCollectionField_deprecated(); + const { uiSchema } = useCollectionField_deprecated(); const component = useComponent(uiSchema?.['x-component']); const compile = useCompile(); const setFieldProps = (key, value) => { field[key] = typeof field[key] === 'undefined' ? value : field[key]; }; - const setRequired = () => { - if (typeof fieldSchema['required'] === 'undefined') { - field.required = !!uiSchema['required']; - } - }; const ctx = useFormBlockContext(); useEffect(() => { @@ -57,7 +52,7 @@ const InternalField: React.FC = (props) => { if (fieldSchema['x-read-pretty'] === true) { field.readPretty = true; } - setRequired(); + field.required = true; // @ts-ignore field.dataSource = uiSchema.enum; const originalProps = compile(uiSchema['x-component-props']) || {}; @@ -97,11 +92,20 @@ export const BulkEditField = (props: any) => { const collectionField = getField(fieldSchema.name) || {}; useEffect(() => { - field.value = { [type]: value }; - }, [type, value]); + field.value = toFormFieldValue({ [type]: value }); + if (field.required) { + if (field.value) { + field.modify(); + field.form.clearErrors(field.address); + } else if (field.modified) { + field.form.validate(field.address); + } + } + }, [field, type, value]); const typeChangeHandler = (val) => { setType(val); + field.required = val === BulkEditFormItemValueType.ChangedTo; }; const valueChangeHandler = (val) => { @@ -134,3 +138,13 @@ export const BulkEditField = (props: any) => { ); }; + +function toFormFieldValue(value: any) { + if (BulkEditFormItemValueType.Clear in value) { + return null; + } else if (BulkEditFormItemValueType.ChangedTo in value) { + return value[BulkEditFormItemValueType.ChangedTo]; + } else if (BulkEditFormItemValueType.RemainsTheSame in value) { + return; + } +} diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/utils.tsx b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/utils.tsx index e0c81be339b..728892a5dd2 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/utils.tsx +++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/utils.tsx @@ -1,23 +1,20 @@ -import { - useCollection_deprecated, - useCollectionManager_deprecated, - useRemoveGridFormItem, - SchemaInitializerItemType, - useBlockRequestContext, - useActionContext, - useCompile, - useTableBlockContext, - TableFieldResource, -} from '@nocobase/client'; -import React, { useContext } from 'react'; -import { App, message } from 'antd'; -import { useNavigate } from 'react-router-dom'; import { SchemaExpressionScopeContext, useField, useForm } from '@formily/react'; -import { cloneDeep } from 'lodash'; +import { + SchemaInitializerItemType, + TableFieldResource, + useActionContext, + useBlockRequestContext, + useCollectionManager_deprecated, + useCollection_deprecated, + useCompile, + useRemoveGridFormItem, + useTableBlockContext, +} from '@nocobase/client'; import { isURL } from '@nocobase/utils/client'; -import { useMemo } from 'react'; +import { App, message } from 'antd'; +import { useContext, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { BulkEditFormItemValueType } from './component/BulkEditField'; +import { useNavigate } from 'react-router-dom'; export const useCustomBulkEditFormItemInitializerFields = (options?: any) => { const { name, fields } = useCollection_deprecated(); @@ -91,27 +88,16 @@ export const useCustomizeBulkEditActionProps = () => { async onClick() { const { onSuccess, skipValidator, updateMode } = actionSchema?.['x-action-settings'] ?? {}; const { filter } = __parent.service.params?.[0] ?? {}; + if (!skipValidator) { await form.submit(); } - const values = cloneDeep(form.values); + actionField.data = field.data || {}; actionField.data.loading = true; - for (const key in values) { - if (Object.prototype.hasOwnProperty.call(values, key)) { - const value = values[key]; - if (BulkEditFormItemValueType.Clear in value) { - values[key] = null; - } else if (BulkEditFormItemValueType.ChangedTo in value) { - values[key] = value[BulkEditFormItemValueType.ChangedTo]; - } else if (BulkEditFormItemValueType.RemainsTheSame in value) { - delete values[key]; - } - } - } try { const updateData: { filter?: any; values: any; forceUpdate: boolean } = { - values, + values: form.values, filter, forceUpdate: false, };