diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts index 3c0d2ab0da..90b836587d 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts @@ -309,6 +309,7 @@ test.describe('configure actions column', () => { await page.getByText('Actions', { exact: true }).hover({ force: true }); await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.mouse.move(500, 0); // await page.getByText('Actions', { exact: true }).hover({ force: true }); // await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); diff --git a/packages/core/client/src/schema-settings/LinkageRules/bindLinkageRulesToFiled.ts b/packages/core/client/src/schema-settings/LinkageRules/bindLinkageRulesToFiled.ts index 0afde24761..2a3cbbeb52 100644 --- a/packages/core/client/src/schema-settings/LinkageRules/bindLinkageRulesToFiled.ts +++ b/packages/core/client/src/schema-settings/LinkageRules/bindLinkageRulesToFiled.ts @@ -274,6 +274,13 @@ function getSubscriber( // 在 FormItem 中有使用这个属性来判断字段是否被隐藏 field.data.hidden = true; + // 如果字段是必填的,并且被隐藏(保留值)了,那么就需要把 required 设置为 false,否则有可能会导致表单验证失败; + // 进而导致点击提交按钮无效的问题。 + if (field.required) { + field.required = false; + field.data.prevRequired = true; + } + requestAnimationFrame(() => { field.setState((state) => { state.display = 'visible'; @@ -295,6 +302,13 @@ function getSubscriber( field.data = field.data || {}; // 在 FormItem 中有使用这个属性来判断字段是否被隐藏 field.data.hidden = false; + + // 当“隐藏(保留值)”的字段再次显示时,恢复“必填”的状态 + if (fieldName === 'display' && lastState?.value === 'visible' && field.data.prevRequired) { + delete field.data.prevRequired; + field.required = true; + } + requestAnimationFrame(() => { field.setState((state) => { state[fieldName] = lastState?.value; diff --git a/packages/core/client/src/schema-settings/__e2e__/linkageRules.test.ts b/packages/core/client/src/schema-settings/__e2e__/linkageRules.test.ts index f52d12eba1..ccf229c420 100644 --- a/packages/core/client/src/schema-settings/__e2e__/linkageRules.test.ts +++ b/packages/core/client/src/schema-settings/__e2e__/linkageRules.test.ts @@ -10,6 +10,7 @@ import { expect, test } from '@nocobase/test/e2e'; import { formFieldDependsOnSubtableFieldsWithLinkageRules, + whenARequiredFieldIsSetToHideRetainValueItShouldBeAbleToSubmitTheFormNormally, whenClearingARelationshipFieldTheValueOfTheAssociatedFieldShouldBeCleared, whenSetToHideRetainedValueItShouldNotImpactTheFieldSDefaultValueVariables, } from './template'; @@ -83,6 +84,28 @@ test.describe('linkage rules', () => { ).toBeVisible(); }); + test('When a required field is set to "Hide (retain value)", it should be able to submit the form normally', async ({ + mockPage, + page, + }) => { + await mockPage(whenARequiredFieldIsSetToHideRetainValueItShouldBeAbleToSubmitTheFormNormally).goto(); + + // 1. 输入昵称 + await page + .getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname') + .getByRole('textbox') + .fill('123456'); + + // 2. 点击提交 + await page.getByLabel('action-Action-Submit-submit-').click(); + + // 3. 应该能正常提交,不应该被拦截 + await page.reload(); + await expect( + page.getByLabel('block-item-CardItem-users-table').getByRole('cell', { name: '123456' }), + ).toBeVisible(); + }); + test('When clearing a relationship field, the value of the associated field should be cleared', async ({ page, mockPage, diff --git a/packages/core/client/src/schema-settings/__e2e__/template.ts b/packages/core/client/src/schema-settings/__e2e__/template.ts index af6bfdde87..d540b5ab2f 100644 --- a/packages/core/client/src/schema-settings/__e2e__/template.ts +++ b/packages/core/client/src/schema-settings/__e2e__/template.ts @@ -2215,3 +2215,376 @@ export const accessControlActionWithTable = { 'x-index': 1, }, }; +export const whenARequiredFieldIsSetToHideRetainValueItShouldBeAbleToSubmitTheFormNormally = { + tabSchema: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + phefo1qp4yu: { + type: 'void', + version: '2.0', + 'x-component': 'Grid.Row', + 'x-app-version': '1.6.21', + _isJSONSchemaObject: true, + properties: { + e94p5oj6num: { + type: 'void', + version: '2.0', + 'x-component': 'Grid.Col', + 'x-app-version': '1.6.21', + _isJSONSchemaObject: true, + properties: { + fpwdszedqqt: { + type: 'void', + version: '2.0', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:createForm', + 'x-component': 'CardItem', + 'x-decorator': 'FormBlockProvider', + 'x-acl-action': 'users:create', + 'x-app-version': '1.6.21', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + }, + 'x-acl-action-props': { + skipScopeCheck: true, + }, + _isJSONSchemaObject: true, + 'x-use-decorator-props': 'useCreateFormBlockDecoratorProps', + properties: { + fy0innr5s9v: { + type: 'void', + version: '2.0', + 'x-component': 'FormV2', + 'x-app-version': '1.6.21', + _isJSONSchemaObject: true, + 'x-use-component-props': 'useCreateFormBlockProps', + properties: { + grid: { + type: 'void', + 'x-uid': '7wwn4d0722h', + version: '2.0', + 'x-component': 'Grid', + 'x-app-version': '1.6.21', + 'x-initializer': 'form:configureFields', + 'x-linkage-rules': [ + { + actions: [ + { + operator: 'hidden', + targetFields: ['username'], + }, + ], + condition: { + $and: [], + }, + }, + ], + _isJSONSchemaObject: true, + properties: { + t9ik3qkj7mv: { + type: 'void', + version: '2.0', + 'x-component': 'Grid.Row', + 'x-app-version': '1.6.21', + _isJSONSchemaObject: true, + properties: { + szfeenblh7q: { + type: 'void', + version: '2.0', + 'x-component': 'Grid.Col', + 'x-app-version': '1.6.21', + _isJSONSchemaObject: true, + properties: { + username: { + type: 'string', + version: '2.0', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-app-version': '1.6.21', + 'x-component-props': {}, + 'x-collection-field': 'users.username', + _isJSONSchemaObject: true, + 'x-uid': 'w7j6pbapyz2', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '4p4e653i4sg', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ojqn0csar3b', + 'x-async': false, + 'x-index': 1, + }, + ij66m020mn5: { + type: 'void', + version: '2.0', + 'x-component': 'Grid.Row', + 'x-app-version': '1.6.21', + _isJSONSchemaObject: true, + properties: { + '95nih4mt2lf': { + type: 'void', + version: '2.0', + 'x-component': 'Grid.Col', + 'x-app-version': '1.6.21', + _isJSONSchemaObject: true, + properties: { + nickname: { + type: 'string', + version: '2.0', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-app-version': '1.6.21', + 'x-component-props': {}, + 'x-collection-field': 'users.nickname', + _isJSONSchemaObject: true, + 'x-uid': 'rpmd8swoor7', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'h0z7u62fwn3', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'b3v741bdvtt', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + o5pser9a4zy: { + type: 'void', + version: '2.0', + 'x-component': 'ActionBar', + 'x-app-version': '1.6.21', + 'x-initializer': 'createForm:configureActions', + 'x-component-props': { + layout: 'one-column', + }, + _isJSONSchemaObject: true, + properties: { + g5aat11143b: { + type: 'void', + title: '{{ t("Submit") }}', + version: '2.0', + 'x-action': 'submit', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:createSubmit', + 'x-component': 'Action', + 'x-app-version': '1.6.21', + 'x-action-settings': {}, + 'x-component-props': { + type: 'primary', + htmlType: 'submit', + }, + _isJSONSchemaObject: true, + 'x-use-component-props': 'useCreateActionProps', + 'x-uid': 'doarbo27x5f', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'n5j6q3dfzst', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '1k05k8tv3fp', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '4bq4fts48ec', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'tgjtas03gh6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'raas2rrgtgq', + 'x-async': false, + 'x-index': 1, + }, + gqnjk39afuj: { + type: 'void', + version: '2.0', + 'x-component': 'Grid.Row', + 'x-app-version': '1.6.21', + _isJSONSchemaObject: true, + properties: { + fvn883pa5lt: { + type: 'void', + version: '2.0', + 'x-component': 'Grid.Col', + 'x-app-version': '1.6.21', + _isJSONSchemaObject: true, + properties: { + '0wlnuaz623s': { + type: 'void', + version: '2.0', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-app-version': '1.6.21', + 'x-filter-targets': [], + 'x-decorator-props': { + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + dragSort: false, + showIndex: true, + collection: 'users', + dataSource: 'main', + }, + _isJSONSchemaObject: true, + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + properties: { + actions: { + type: 'void', + version: '2.0', + 'x-component': 'ActionBar', + 'x-app-version': '1.6.21', + 'x-initializer': 'table:configureActions', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + _isJSONSchemaObject: true, + 'x-uid': 'q7y8z49vkvs', + 'x-async': false, + 'x-index': 1, + }, + e7dmvrnhhhw: { + type: 'array', + version: '2.0', + 'x-component': 'TableV2', + 'x-app-version': '1.6.21', + 'x-initializer': 'table:configureColumns', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + _isJSONSchemaObject: true, + 'x-use-component-props': 'useTableBlockProps', + properties: { + actions: { + type: 'void', + title: '{{ t("Actions") }}', + version: '2.0', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-app-version': '1.6.21', + 'x-initializer': 'table:configureItemActions', + 'x-action-column': 'actions', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + _isJSONSchemaObject: true, + properties: { + wwt3qrk0ro0: { + type: 'void', + version: '2.0', + 'x-component': 'Space', + 'x-decorator': 'DndContext', + 'x-app-version': '1.6.21', + 'x-component-props': { + split: '|', + }, + _isJSONSchemaObject: true, + 'x-uid': '58ymkonaijm', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '46zml84998i', + 'x-async': false, + 'x-index': 1, + }, + d1qruxltt1t: { + type: 'void', + version: '2.0', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-app-version': '1.6.21', + _isJSONSchemaObject: true, + properties: { + nickname: { + version: '2.0', + 'x-component': 'CollectionField', + 'x-decorator': null, + 'x-app-version': '1.6.21', + 'x-read-pretty': true, + 'x-component-props': { + ellipsis: true, + }, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-collection-field': 'users.nickname', + _isJSONSchemaObject: true, + 'x-uid': 'ut8m8l3qhzn', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'c2592dzb9s1', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'nntvcjy39cg', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'o5gizqch1wr', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '8awil00a9wo', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'i1dnlv9n2k0', + 'x-async': false, + 'x-index': 2, + }, + }, + name: 'ri6dthungp3', + 'x-uid': 'b2u59skxpsy', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/core/test/src/e2e/e2eUtils.ts b/packages/core/test/src/e2e/e2eUtils.ts index 05372aeee2..39dd40af23 100644 --- a/packages/core/test/src/e2e/e2eUtils.ts +++ b/packages/core/test/src/e2e/e2eUtils.ts @@ -31,6 +31,21 @@ function getPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }) { }; } +function getPageMenuSchemaWithTabSchema({ tabSchema }) { + if (!tabSchema) { + return null; + } + + return { + type: 'void', + 'x-component': 'Page', + properties: { + [tabSchema.name]: tabSchema, + }, + 'x-uid': uid(), + }; +} + export * from '@playwright/test'; export { defineConfig }; @@ -193,10 +208,17 @@ export interface PageConfig { */ collections?: CollectionSetting[]; /** + * @deprecate 在菜单被重构之后,没有办法直接复制完整的页面 Schema 了。所以这个选项不推荐使用了。 + * 推荐使用 tabSchema,复制一个页面 tab 的 Schema 传给 tabSchema。 + * * 页面整体的 Schema * @default undefined */ pageSchema?: any; + /** + * 页面 Tab 的 Schema。当 pageSchema 和 tabSchema 都存在时,最终显示的会是 tabSchema 的内容 + */ + tabSchema?: any; /** 如果为 true 则表示不会更改 PageSchema 的 uid */ keepUid?: boolean; /** 在 URL 中的 uid,例如:/admin/0ig6xhe03u2 */ @@ -217,6 +239,7 @@ interface CreatePageOptions { url?: PageConfig['url']; name?: string; pageSchema?: any; + tabSchema?: any; /** 如果为 true 则表示不会更改 PageSchema 的 uid */ keepUid?: boolean; /** 在 URL 中的 uid,例如:/admin/0ig6xhe03u2 */ @@ -367,6 +390,7 @@ export class NocoPage { type: this.options?.type, name: this.options?.name, pageSchema: this.options?.pageSchema, + tabSchema: this.options?.tabSchema, url: this.options?.url, keepUid: this.options?.keepUid, pageUid: this.options?.pageUid, @@ -737,13 +761,16 @@ const updateUidOfPageSchema = (uiSchema: any) => { * 在 NocoBase 中创建一个页面 */ const createPage = async (options?: CreatePageOptions) => { - const { type = 'page', url, name, pageSchema, keepUid, pageUid: pageUidFromOptions } = options || {}; + const { type = 'page', url, name, pageSchema, tabSchema, keepUid, pageUid: pageUidFromOptions } = options || {}; const api = await request.newContext({ storageState: process.env.PLAYWRIGHT_AUTH_FILE, }); + + const schema = getPageMenuSchemaWithTabSchema({ tabSchema }) || pageSchema; + const state = await api.storageState(); const headers = getHeaders(state); - const newPageSchema = keepUid ? pageSchema : updateUidOfPageSchema(pageSchema); + const newPageSchema = keepUid ? schema : updateUidOfPageSchema(schema); const pageSchemaUid = newPageSchema?.['x-uid'] || uid(); const newTabSchemaUid = uid(); const newTabSchemaName = uid();