diff --git a/packages/core/client/src/block-provider/hooks/useDataBlockSourceId.tsx b/packages/core/client/src/block-provider/hooks/useDataBlockSourceId.tsx index d677198b7d..3b2ff81afd 100644 --- a/packages/core/client/src/block-provider/hooks/useDataBlockSourceId.tsx +++ b/packages/core/client/src/block-provider/hooks/useDataBlockSourceId.tsx @@ -5,12 +5,19 @@ import { useCollectionParentRecordData, useCollectionRecordData, } from '../..'; +import { useSourceKey } from '../../modules/blocks/useSourceKey'; /** * @internal + * @deprecated + * 已弃用(该方法现在只是用来兼容旧版 Schema 的),请通过各个区块的 x-use-decorator-props 中获取 sourceId + * * 注意:这里有一个需要更改 schema 才能解决的问题,就是在获取 sourceId 的时候无法确定(在关系字段和当前表同表时) * 是需要从 recordData 还是 parentRecordData 中获取;解决方法是通过更改 schema,在不同类型的关系区块中 * (`通过点击关系字段按钮打开的弹窗中创建的非关系字段区块`和`关系字段区块`)使用不同的 hook。 + * + * 更新:上面所说的“需要更改 schema 才能解决的问题”已在这个任务中更改:https://nocobase.height.app/T-3848/description + * * @param param0 * @returns */ @@ -18,26 +25,19 @@ export const useDataBlockSourceId = ({ association }: { association: string }) = const recordData = useCollectionRecordData(); const parentRecordData = useCollectionParentRecordData(); const cm = useCollectionManager(); - const collectionOutsideBlock = useCollection(); + const currentRecordCollection = useCollection(); + const sourceKey = useSourceKey(association); if (!association) return; - const associationField = cm.getCollectionField(association); - const associationCollection = cm.getCollection(associationField.collectionName); + const sourceCollection = cm.getCollection(association.split('.')[0]); if ( - collectionOutsideBlock.name === associationCollection.name || - collectionOutsideBlock.getParentCollectionsName?.().includes(associationCollection.name) + currentRecordCollection.name === sourceCollection.name || + currentRecordCollection.getParentCollectionsName?.().includes(sourceCollection.name) ) { - return recordData?.[ - associationField.sourceKey || - associationCollection.filterTargetKey || - associationCollection.getPrimaryKey() || - 'id' - ]; + return recordData?.[sourceKey]; } - return parentRecordData?.[ - associationField.sourceKey || associationCollection.filterTargetKey || associationCollection.getPrimaryKey() || 'id' - ]; + return parentRecordData?.[sourceKey]; }; diff --git a/packages/core/client/src/data-source/data-block/DataBlockRequestProvider.tsx b/packages/core/client/src/data-source/data-block/DataBlockRequestProvider.tsx index 032968ab18..a5aa3b153f 100644 --- a/packages/core/client/src/data-source/data-block/DataBlockRequestProvider.tsx +++ b/packages/core/client/src/data-source/data-block/DataBlockRequestProvider.tsx @@ -7,7 +7,7 @@ import { useDataBlockResource } from './DataBlockResourceProvider'; import { useDataSourceHeaders } from '../utils'; import _ from 'lodash'; import { useDataLoadingMode } from '../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem'; -import { useCollectionManager } from '../collection'; +import { useSourceKey } from '../../modules/blocks/useSourceKey'; export const BlockRequestContext = createContext>(null); BlockRequestContext.displayName = 'BlockRequestContext'; @@ -46,21 +46,17 @@ function useCurrentRequest(options: Omit) { function useParentRequest(options: Omit) { const { sourceId, association, parentRecord } = options; const api = useAPIClient(); - const cm = useCollectionManager(); const dataBlockProps = useDataBlockProps(); const headers = useDataSourceHeaders(dataBlockProps.dataSource); + const sourceKey = useSourceKey(association); return useRequest( async () => { if (parentRecord) return Promise.resolve({ data: parentRecord }); - if (!association) return Promise.resolve({ data: undefined }); + if (!association || !sourceKey) return Promise.resolve({ data: undefined }); // "association": "Collection.Field" const arr = association.split('.'); - const field = cm.getCollectionField(association); - const isM2O = field.interface === 'm2o'; - const filterTargetKey = cm.getCollection(arr[0]).getOption('filterTargetKey'); - const filterKey = isM2O ? filterTargetKey : field.sourceKey; // :get?filter[filterKey]=sourceId - const url = `${arr[0]}:get?filter[${filterKey}]=${sourceId}`; + const url = `${arr[0]}:get?filter[${sourceKey}]=${sourceId}`; const res = await api.request({ url, headers }); return res.data; }, diff --git a/packages/core/client/src/index.ts b/packages/core/client/src/index.ts index 7f62193cc2..277c3ef785 100644 --- a/packages/core/client/src/index.ts +++ b/packages/core/client/src/index.ts @@ -59,3 +59,4 @@ export * from './modules/blocks/data-blocks/table'; export * from './modules/blocks/data-blocks/form'; export * from './modules/blocks/data-blocks/table-selector'; export * from './modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem'; +export * from './modules/blocks/useSourceIdCommon'; diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/hooks/useDetailsWithPaginationDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/hooks/useDetailsWithPaginationDecoratorProps.ts index 5d3935fde7..68f9750dc5 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-multi/hooks/useDetailsWithPaginationDecoratorProps.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/hooks/useDetailsWithPaginationDecoratorProps.ts @@ -1,4 +1,4 @@ -import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId'; +import { useSourceIdCommon } from '../../../useSourceIdCommon'; export function useDetailsWithPaginationDecoratorProps(props) { let sourceId; @@ -6,7 +6,7 @@ export function useDetailsWithPaginationDecoratorProps(props) { // association 的值是固定不变的,所以可以在条件中使用 hooks if (props.association) { // eslint-disable-next-line react-hooks/rules-of-hooks - sourceId = useDataBlockSourceId({ association: props.association }); + sourceId = useSourceIdCommon(props.association); } return { diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/RecordReadPrettyFormBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/details-single/RecordReadPrettyFormBlockInitializer.tsx index 6d6127fbb3..b7e09bd9aa 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-single/RecordReadPrettyFormBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/RecordReadPrettyFormBlockInitializer.tsx @@ -69,6 +69,7 @@ export function useCreateSingleDetailsSchema() { ? { association, dataSource: item.dataSource, + isCurrent: true, } : { collectionName: item.collectionName || item.name, diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts index 27d294aaa8..2091767ab2 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts @@ -1,4 +1,5 @@ import { Page, expect, expectSettingsMenu, oneEmptyTableBlockWithActions, test } from '@nocobase/test/e2e'; +import { T3848 } from './templatesOfBug'; test.describe('where single data details block can be added', () => { test('popup', async ({ page, mockPage, mockRecord }) => { @@ -14,6 +15,38 @@ test.describe('where single data details block can be added', () => { await expect(page.getByLabel('block-item-CardItem-general-details')).toBeVisible(); }); + + // https://nocobase.height.app/T-3848/description + test('popup opened by clicking on the button for the relationship field', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(T3848).waitForInit(); + await mockRecord('example'); + await nocoPage.goto(); + + // 1.打开弹窗 + await page.getByRole('button', { name: '2', exact: true }).getByText('2').click(); + + // 2.通过 Current record 创建一个详情区块 + await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.mouse.move(300, 0); + await page.getByLabel('schema-initializer-Grid-details:configureFields-example').hover(); + await page.getByRole('menuitem', { name: 'ID' }).click(); + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CollectionField-').getByText('2')).toBeVisible(); + + // 3.通过 Associated records 创建一个详情区块 + await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Associated records' }).hover(); + await page.getByRole('menuitem', { name: 'manyToOne' }).click(); + await page.mouse.move(300, 0); + await page.getByLabel('schema-initializer-Grid-details:configureFields-example').nth(1).hover(); + await page.getByRole('menuitem', { name: 'ID' }).click(); + await page.mouse.move(300, 0); + // id 为 2 的记录的关系字段对应的是 3。但是如果 mockRecord 的逻辑变更的话,这里可能会有问题 + await expect(page.getByLabel('block-item-CollectionField-').nth(1).getByText('3')).toBeVisible(); + }); }); test.describe('configure actions', () => { diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/templatesOfBug.ts index e69de29bb2..d94bb6d795 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/templatesOfBug.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/templatesOfBug.ts @@ -0,0 +1,248 @@ +import { PageConfig } from '@nocobase/test/e2e'; + +export const T3848: PageConfig = { + collections: [ + { + name: 'example', + fields: [ + { + name: 'manyToOne', + interface: 'm2o', + target: 'example', + }, + { + name: 'singleLineText', + interface: 'input', + }, + ], + }, + ], + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + nzmrteziofg: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + zzpze3c8oge: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + ndgxtl4rvu3: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + wvab22d93e8: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'example:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'example', + 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': [], + 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-uid': '0clfl0oxnwf', + 'x-async': false, + 'x-index': 1, + }, + gnd7uoea2zu: { + _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', + }, + }, + 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', + properties: { + x59rltkm59a: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-uid': 'rggytn82ilc', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '3d6k1i1jx7v', + 'x-async': false, + 'x-index': 1, + }, + uvhs4tg6nwm: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + properties: { + manyToOne: { + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'example.manyToOne', + 'x-component': 'CollectionField', + 'x-component-props': { + fieldNames: { + value: 'id', + label: 'id', + }, + ellipsis: true, + size: 'small', + }, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + properties: { + pt0gyfcniz8: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("View record") }}', + 'x-component': 'AssociationField.Viewer', + 'x-component-props': { + className: 'nb-action-popup', + }, + 'x-index': 1, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'TabPaneInitializers', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + 'x-uid': 'g9v1ckltvex', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'pz1l8oq2ya7', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '58x2e3ugg1q', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'bdpjlroyuum', + 'x-async': false, + }, + }, + 'x-uid': 'ly8hdsl4jjo', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'yu5bgkjqhu8', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'l3onkna9ido', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'ee81h3nx17g', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'l6h63fsseo7', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'zu4jp6j2avg', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'fd3cw6sti4y', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '7c28d1jik0y', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/__tests__/createDetailsBlockWithoutPagingUISchema.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/__tests__/createDetailsBlockWithoutPagingUISchema.test.ts index 5207891603..a8560272e6 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-single/__tests__/createDetailsBlockWithoutPagingUISchema.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/__tests__/createDetailsBlockWithoutPagingUISchema.test.ts @@ -59,4 +59,60 @@ describe('createDetailsBlockWithoutPagingUISchema', () => { } `); }); + + it('should create a valid schema with custom x-use-decorator-props', () => { + const options = { + collectionName: 'users', + dataSource: 'usersDataSource', + isCurrent: true, + }; + + const result = createDetailsUISchema(options); + expect(result).toMatchInlineSnapshot(` + { + "properties": { + "fixed-uid": { + "properties": { + "fixed-uid": { + "properties": {}, + "type": "void", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24, + }, + }, + "x-initializer": "details:configureActions", + }, + "grid": { + "properties": {}, + "type": "void", + "x-component": "Grid", + "x-initializer": "details:configureFields", + }, + }, + "type": "void", + "x-component": "Details", + "x-read-pretty": true, + "x-use-component-props": "useDetailsProps", + }, + }, + "type": "void", + "x-acl-action": "users:get", + "x-component": "CardItem", + "x-decorator": "DetailsBlockProvider", + "x-decorator-props": { + "action": "get", + "association": undefined, + "collection": "users", + "dataSource": "usersDataSource", + "readPretty": true, + }, + "x-is-current": true, + "x-settings": "blockSettings:details", + "x-toolbar": "BlockSchemaToolbar", + "x-use-decorator-props": "useDetailsDecoratorProps", + } + `); + }); }); diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/createDetailsUISchema.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/createDetailsUISchema.ts index 963c489e83..1e211a755f 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-single/createDetailsUISchema.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/createDetailsUISchema.ts @@ -6,9 +6,14 @@ export function createDetailsUISchema(options: { collectionName?: string; association?: string; templateSchema?: ISchema; + /** + * 如果为 true,则表示当前创建的区块 record 就是 useRecord 返回的 record + */ + isCurrent?: boolean; }): ISchema { const { collectionName, dataSource, association, templateSchema } = options; const resourceName = association || collectionName; + const isCurrentObj = options.isCurrent ? { 'x-is-current': true } : {}; if (!dataSource) { throw new Error('dataSource are required'); @@ -29,6 +34,7 @@ export function createDetailsUISchema(options: { 'x-toolbar': 'BlockSchemaToolbar', 'x-settings': 'blockSettings:details', 'x-component': 'CardItem', + ...isCurrentObj, properties: { [uid()]: { type: 'void', diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/hooks/useDetailsDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/hooks/useDetailsDecoratorProps.ts index 71cd7e8707..27562dcdbf 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-single/hooks/useDetailsDecoratorProps.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/hooks/useDetailsDecoratorProps.ts @@ -1,6 +1,16 @@ +import { useFieldSchema } from '@formily/react'; import { useParamsFromRecord } from '../../../../../block-provider/BlockProvider'; -import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId'; +import { + useCollectionParentRecordData, + useCollectionRecordData, +} from '../../../../../data-source/collection-record/CollectionRecordProvider'; +import { useSourceKey } from '../../../useSourceKey'; +/** + * 应用在通过 Current record 选项创建的区块中(弹窗中的 Add block 菜单) + * @param props + * @returns + */ export function useDetailsDecoratorProps(props) { const params = useParamsFromRecord(); let sourceId; @@ -8,7 +18,7 @@ export function useDetailsDecoratorProps(props) { // association 的值是固定不变的,所以可以在条件中使用 hooks if (props.association) { // eslint-disable-next-line react-hooks/rules-of-hooks - sourceId = useDataBlockSourceId({ association: props.association }); + sourceId = useDetailsSourceId(props.association); } return { @@ -16,3 +26,18 @@ export function useDetailsDecoratorProps(props) { sourceId, }; } + +export function useDetailsSourceId(association: string) { + const fieldSchema = useFieldSchema(); + const recordData = useCollectionRecordData(); + const parentRecordData = useCollectionParentRecordData(); + const sourceKey = useSourceKey(association); + + if (!association) return; + + if (fieldSchema['x-is-current']) { + return parentRecordData?.[sourceKey]; + } + + return recordData?.[sourceKey]; +} diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/RecordFormBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/RecordFormBlockInitializer.tsx index c207b7550a..560f34e6ad 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/RecordFormBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/form/RecordFormBlockInitializer.tsx @@ -49,6 +49,7 @@ export function useCreateEditFormBlock() { ? { association, dataSource: item.dataSource, + isCurrent: true, } : { collectionName: item.collectionName || item.name, diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts index e24468c88f..dcd46b64ee 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts @@ -1,4 +1,5 @@ import { expect, oneEmptyTableBlockWithActions, test } from '@nocobase/test/e2e'; +import { T3848 } from '../../../details-single/__e2e__/templatesOfBug'; test.describe('where edit form block can be added', () => { test('popup', async ({ page, mockPage, mockRecord }) => { @@ -13,6 +14,25 @@ test.describe('where edit form block can be added', () => { await expect(page.getByLabel('block-item-CardItem-general-form')).toBeVisible(); }); + + // https://nocobase.height.app/T-3848/description + test('popup opened by clicking on the button for the relationship field', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(T3848).waitForInit(); + await mockRecord('example'); + await nocoPage.goto(); + + // 1.打开弹窗 + await page.getByRole('button', { name: '2', exact: true }).getByText('2').click(); + + // 2.创建一个编辑表单区块 + await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'form Form (Edit)' }).click(); + await page.mouse.move(300, 0); + await page.getByLabel('schema-initializer-Grid-form:').hover(); + await page.getByRole('menuitem', { name: 'ID' }).click(); + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CollectionField-').getByText('2')).toBeVisible(); + }); }); test.describe('configure actions', () => {}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__tests__/createEditFormBlockUISchema.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__tests__/createEditFormBlockUISchema.test.ts index 8082b18d32..8ff9615cd8 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__tests__/createEditFormBlockUISchema.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__tests__/createEditFormBlockUISchema.test.ts @@ -1,4 +1,3 @@ -import { ISchema } from '@formily/react'; import { vi } from 'vitest'; import { createEditFormBlockUISchema } from '../createEditFormBlockUISchema'; @@ -59,4 +58,61 @@ describe('createEditFormBlockUISchema', () => { } `); }); + + it('should create a valid schema with custom x-use-decorator-props', () => { + const options = { + collectionName: 'users', + dataSource: 'usersDataSource', + isCurrent: true, + }; + + const result = createEditFormBlockUISchema(options); + expect(result).toMatchInlineSnapshot(` + { + "properties": { + "uniqueId": { + "properties": { + "grid": { + "properties": {}, + "type": "void", + "x-component": "Grid", + "x-initializer": "form:configureFields", + }, + "uniqueId": { + "type": "void", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + "style": { + "marginTop": 24, + }, + }, + "x-initializer": "editForm:configureActions", + }, + }, + "type": "void", + "x-component": "FormV2", + "x-use-component-props": "useEditFormBlockProps", + }, + }, + "type": "void", + "x-acl-action": "users:update", + "x-acl-action-props": { + "skipScopeCheck": false, + }, + "x-component": "CardItem", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "action": "get", + "association": undefined, + "collection": "users", + "dataSource": "usersDataSource", + }, + "x-is-current": true, + "x-settings": "blockSettings:editForm", + "x-toolbar": "BlockSchemaToolbar", + "x-use-decorator-props": "useEditFormBlockDecoratorProps", + } + `); + }); }); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/createEditFormBlockUISchema.ts b/packages/core/client/src/modules/blocks/data-blocks/form/createEditFormBlockUISchema.ts index 509b0183b9..7ab4b7b1bc 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/createEditFormBlockUISchema.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/createEditFormBlockUISchema.ts @@ -7,11 +7,16 @@ interface EditFormBlockOptions { collectionName?: string; association?: string; templateSchema?: ISchema; + /** + * 如果为 true,则表示当前创建的区块 record 就是 useRecord 返回的 record + */ + isCurrent?: boolean; } export function createEditFormBlockUISchema(options: EditFormBlockOptions): ISchema { const { collectionName, dataSource, association, templateSchema } = options; const resourceName = association || collectionName; + const isCurrentObj = options.isCurrent ? { 'x-is-current': true } : {}; if (!dataSource) { throw new Error('dataSource are required'); @@ -34,6 +39,7 @@ export function createEditFormBlockUISchema(options: EditFormBlockOptions): ISch 'x-toolbar': 'BlockSchemaToolbar', 'x-settings': 'blockSettings:editForm', 'x-component': 'CardItem', + ...isCurrentObj, properties: { [uid()]: { type: 'void', diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useCreateFormBlockDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useCreateFormBlockDecoratorProps.ts index d0e78303c3..87fdb9c640 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useCreateFormBlockDecoratorProps.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useCreateFormBlockDecoratorProps.ts @@ -1,4 +1,4 @@ -import { useFormBlockSourceId } from './useFormBlockSourceId'; +import { useSourceIdCommon } from '../../../useSourceIdCommon'; export function useCreateFormBlockDecoratorProps(props) { let sourceId; @@ -6,7 +6,7 @@ export function useCreateFormBlockDecoratorProps(props) { // association 的值是固定不变的,所以这里可以使用 hooks if (props.association) { // eslint-disable-next-line react-hooks/rules-of-hooks - sourceId = useFormBlockSourceId(props); + sourceId = useSourceIdCommon(props.association); } return { diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockDecoratorProps.ts index af759e6565..47df14192d 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockDecoratorProps.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockDecoratorProps.ts @@ -1,5 +1,5 @@ import { useParamsFromRecord } from '../../../../../block-provider/BlockProvider'; -import { useFormBlockSourceId } from './useFormBlockSourceId'; +import { useDetailsSourceId } from '../../details-single/hooks/useDetailsDecoratorProps'; export function useEditFormBlockDecoratorProps(props) { const params = useFormBlockParams(); @@ -7,8 +7,9 @@ export function useEditFormBlockDecoratorProps(props) { // association 的值是固定不变的,所以这里可以使用 hooks if (props.association) { + // 复用详情区块的 sourceId 获取逻辑 // eslint-disable-next-line react-hooks/rules-of-hooks - sourceId = useFormBlockSourceId(props); + sourceId = useDetailsSourceId(props.association); } return { diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useFormBlockSourceId.ts b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useFormBlockSourceId.ts deleted file mode 100644 index 5863c1d6f9..0000000000 --- a/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useFormBlockSourceId.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId'; - -export function useFormBlockSourceId(props) { - return useDataBlockSourceId(props); -} diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/hooks/useGridCardBlockDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/hooks/useGridCardBlockDecoratorProps.ts index 1dac89feef..12682b7575 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/grid-card/hooks/useGridCardBlockDecoratorProps.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/hooks/useGridCardBlockDecoratorProps.ts @@ -1,4 +1,4 @@ -import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId'; +import { useSourceIdCommon } from '../../../useSourceIdCommon'; import { useGridCardBlockParams } from './useGridCardBlockParams'; export function useGridCardBlockDecoratorProps(props) { @@ -8,7 +8,7 @@ export function useGridCardBlockDecoratorProps(props) { // 因为 association 是固定的,所以可以在条件中使用 hooks if (props.association) { // eslint-disable-next-line react-hooks/rules-of-hooks - sourceId = useDataBlockSourceId({ association: props.association }); + sourceId = useSourceIdCommon(props.association); } return { diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/hooks/useListBlockDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/list/hooks/useListBlockDecoratorProps.ts index c63d2936f4..98825fc061 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/list/hooks/useListBlockDecoratorProps.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/list/hooks/useListBlockDecoratorProps.ts @@ -1,4 +1,4 @@ -import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId'; +import { useSourceIdCommon } from '../../../useSourceIdCommon'; export function useListBlockDecoratorProps(props) { let sourceId; @@ -6,7 +6,7 @@ export function useListBlockDecoratorProps(props) { // 因为 association 的值是固定的,所以这里可以使用 hooks if (props.association) { // eslint-disable-next-line react-hooks/rules-of-hooks - sourceId = useDataBlockSourceId({ association: props.association }); + sourceId = useSourceIdCommon(props.association); } return { diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps.ts index 55650748ab..be810d4817 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps.ts @@ -1,7 +1,7 @@ import { useFieldSchema } from '@formily/react'; import { useParsedFilter } from '../../../../../block-provider/hooks/useParsedFilter'; import { useMemo } from 'react'; -import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId'; +import { useSourceIdCommon } from '../../../useSourceIdCommon'; export const useTableBlockDecoratorProps = (props) => { const params = useTableBlockParams(props); @@ -44,7 +44,7 @@ function useTableBlockSourceId(props) { // 因为 association 是固定不变的,所以在条件中使用 hooks 是安全的 if (props.association) { // eslint-disable-next-line react-hooks/rules-of-hooks - sourceId = useDataBlockSourceId({ association: props.association }); + sourceId = useSourceIdCommon(props.association); } return sourceId; diff --git a/packages/core/client/src/modules/blocks/useSourceIdCommon.ts b/packages/core/client/src/modules/blocks/useSourceIdCommon.ts new file mode 100644 index 0000000000..f3bfa71566 --- /dev/null +++ b/packages/core/client/src/modules/blocks/useSourceIdCommon.ts @@ -0,0 +1,17 @@ +import { useCollectionRecordData } from '../../data-source/collection-record/CollectionRecordProvider'; +import { useSourceKey } from './useSourceKey'; + +/** + * @internal + * 大部分区块(除了详情和编辑表单)都适用的获取 sourceId 的 hook + * @param association + * @returns + */ +export const useSourceIdCommon = (association: string) => { + const recordData = useCollectionRecordData(); + const sourceKey = useSourceKey(association); + + if (!association) return; + + return recordData?.[sourceKey]; +}; diff --git a/packages/core/client/src/modules/blocks/useSourceKey.ts b/packages/core/client/src/modules/blocks/useSourceKey.ts new file mode 100644 index 0000000000..5b63e2ed17 --- /dev/null +++ b/packages/core/client/src/modules/blocks/useSourceKey.ts @@ -0,0 +1,26 @@ +import { InheritanceCollectionMixin } from '../../collection-manager/mixins/InheritanceCollectionMixin'; +import { useCollectionManager } from '../../data-source/collection/CollectionManagerProvider'; + +/** + * 用于获取关系字段的 source collection 的 key + * @param association string + * @returns + */ +export const useSourceKey = (association: string) => { + const cm = useCollectionManager(); + + if (!association) return; + + const associationField = cm.getCollectionField(association); + + if (!associationField) { + return; + } + + const sourceCollection = cm.getCollection(association.split('.')[0]); + + // 1. hasOne 和 hasMany 和 belongsToMany 的字段存在 sourceKey,所以会直接返回 sourceKey; + // 2. belongsTo 不存在 sourceKey,所以会使用 filterTargetKey; + // 3. 后面的主键和 id 是为了保险起见加上的; + return associationField.sourceKey || sourceCollection.filterTargetKey || sourceCollection.getPrimaryKey() || 'id'; +}; diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/hooks/useCalendarBlockDecoratorProps.ts b/packages/plugins/@nocobase/plugin-calendar/src/client/hooks/useCalendarBlockDecoratorProps.ts index edc67ee95a..85baa46beb 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/hooks/useCalendarBlockDecoratorProps.ts +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/hooks/useCalendarBlockDecoratorProps.ts @@ -1,4 +1,4 @@ -import { useDataBlockSourceId } from '@nocobase/client'; +import { useSourceIdCommon } from '@nocobase/client'; import { useCalendarBlockParams } from './useCalendarBlockParams'; export function useCalendarBlockDecoratorProps(props) { @@ -8,7 +8,7 @@ export function useCalendarBlockDecoratorProps(props) { // 因为 association 是一个固定的值,所以可以在 hooks 中直接使用 if (props.association) { // eslint-disable-next-line react-hooks/rules-of-hooks - sourceId = useDataBlockSourceId({ association: props.association }); + sourceId = useSourceIdCommon(props.association); } return {