mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 11:12:20 +08:00
fix: get the correct sourceId (#3897)
* fix(withDynamicSchemaProps): replace recursive merge with simple merge * refactor: add useDetailsByAssociationRecordDecoratorProps * chore: update comment * fix: get the correct sourceId * test: add e2e * refactor: extract to template * refactor: add x-is-current * refactor: optimize code * Revert "refactor: optimize code" This reverts commit b2b03c3c0cd42d4027936cebceb8b0c6ba43edd8. * refactor: add useSourceIdCommon hook and update usage in multiple files * fix(EditForm): refactor form block sourceId logic * chore: add test for form block * chore: update comment * fix: fix filterKey assignment in useParentRequest function * refactor: refactor useParentRequest function to use fieldCollection for filterTargetKey * refactor: extract to useSourceKey * refactor: optimize code
This commit is contained in:
parent
ac655f6866
commit
768dfc624a
@ -5,12 +5,19 @@ import {
|
|||||||
useCollectionParentRecordData,
|
useCollectionParentRecordData,
|
||||||
useCollectionRecordData,
|
useCollectionRecordData,
|
||||||
} from '../..';
|
} from '../..';
|
||||||
|
import { useSourceKey } from '../../modules/blocks/useSourceKey';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
* @deprecated
|
||||||
|
* 已弃用(该方法现在只是用来兼容旧版 Schema 的),请通过各个区块的 x-use-decorator-props 中获取 sourceId
|
||||||
|
*
|
||||||
* 注意:这里有一个需要更改 schema 才能解决的问题,就是在获取 sourceId 的时候无法确定(在关系字段和当前表同表时)
|
* 注意:这里有一个需要更改 schema 才能解决的问题,就是在获取 sourceId 的时候无法确定(在关系字段和当前表同表时)
|
||||||
* 是需要从 recordData 还是 parentRecordData 中获取;解决方法是通过更改 schema,在不同类型的关系区块中
|
* 是需要从 recordData 还是 parentRecordData 中获取;解决方法是通过更改 schema,在不同类型的关系区块中
|
||||||
* (`通过点击关系字段按钮打开的弹窗中创建的非关系字段区块`和`关系字段区块`)使用不同的 hook。
|
* (`通过点击关系字段按钮打开的弹窗中创建的非关系字段区块`和`关系字段区块`)使用不同的 hook。
|
||||||
|
*
|
||||||
|
* 更新:上面所说的“需要更改 schema 才能解决的问题”已在这个任务中更改:https://nocobase.height.app/T-3848/description
|
||||||
|
*
|
||||||
* @param param0
|
* @param param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
@ -18,26 +25,19 @@ export const useDataBlockSourceId = ({ association }: { association: string }) =
|
|||||||
const recordData = useCollectionRecordData();
|
const recordData = useCollectionRecordData();
|
||||||
const parentRecordData = useCollectionParentRecordData();
|
const parentRecordData = useCollectionParentRecordData();
|
||||||
const cm = useCollectionManager();
|
const cm = useCollectionManager();
|
||||||
const collectionOutsideBlock = useCollection<InheritanceCollectionMixin>();
|
const currentRecordCollection = useCollection<InheritanceCollectionMixin>();
|
||||||
|
const sourceKey = useSourceKey(association);
|
||||||
|
|
||||||
if (!association) return;
|
if (!association) return;
|
||||||
|
|
||||||
const associationField = cm.getCollectionField(association);
|
const sourceCollection = cm.getCollection<InheritanceCollectionMixin>(association.split('.')[0]);
|
||||||
const associationCollection = cm.getCollection<InheritanceCollectionMixin>(associationField.collectionName);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
collectionOutsideBlock.name === associationCollection.name ||
|
currentRecordCollection.name === sourceCollection.name ||
|
||||||
collectionOutsideBlock.getParentCollectionsName?.().includes(associationCollection.name)
|
currentRecordCollection.getParentCollectionsName?.().includes(sourceCollection.name)
|
||||||
) {
|
) {
|
||||||
return recordData?.[
|
return recordData?.[sourceKey];
|
||||||
associationField.sourceKey ||
|
|
||||||
associationCollection.filterTargetKey ||
|
|
||||||
associationCollection.getPrimaryKey() ||
|
|
||||||
'id'
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return parentRecordData?.[
|
return parentRecordData?.[sourceKey];
|
||||||
associationField.sourceKey || associationCollection.filterTargetKey || associationCollection.getPrimaryKey() || 'id'
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@ import { useDataBlockResource } from './DataBlockResourceProvider';
|
|||||||
import { useDataSourceHeaders } from '../utils';
|
import { useDataSourceHeaders } from '../utils';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useDataLoadingMode } from '../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
|
import { useDataLoadingMode } from '../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
|
||||||
import { useCollectionManager } from '../collection';
|
import { useSourceKey } from '../../modules/blocks/useSourceKey';
|
||||||
|
|
||||||
export const BlockRequestContext = createContext<UseRequestResult<any>>(null);
|
export const BlockRequestContext = createContext<UseRequestResult<any>>(null);
|
||||||
BlockRequestContext.displayName = 'BlockRequestContext';
|
BlockRequestContext.displayName = 'BlockRequestContext';
|
||||||
@ -46,21 +46,17 @@ function useCurrentRequest<T>(options: Omit<AllDataBlockProps, 'type'>) {
|
|||||||
function useParentRequest<T>(options: Omit<AllDataBlockProps, 'type'>) {
|
function useParentRequest<T>(options: Omit<AllDataBlockProps, 'type'>) {
|
||||||
const { sourceId, association, parentRecord } = options;
|
const { sourceId, association, parentRecord } = options;
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const cm = useCollectionManager();
|
|
||||||
const dataBlockProps = useDataBlockProps();
|
const dataBlockProps = useDataBlockProps();
|
||||||
const headers = useDataSourceHeaders(dataBlockProps.dataSource);
|
const headers = useDataSourceHeaders(dataBlockProps.dataSource);
|
||||||
|
const sourceKey = useSourceKey(association);
|
||||||
return useRequest<T>(
|
return useRequest<T>(
|
||||||
async () => {
|
async () => {
|
||||||
if (parentRecord) return Promise.resolve({ data: parentRecord });
|
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"
|
// "association": "Collection.Field"
|
||||||
const arr = association.split('.');
|
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;
|
|
||||||
// <collection>:get?filter[filterKey]=sourceId
|
// <collection>: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 });
|
const res = await api.request({ url, headers });
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
|
@ -59,3 +59,4 @@ export * from './modules/blocks/data-blocks/table';
|
|||||||
export * from './modules/blocks/data-blocks/form';
|
export * from './modules/blocks/data-blocks/form';
|
||||||
export * from './modules/blocks/data-blocks/table-selector';
|
export * from './modules/blocks/data-blocks/table-selector';
|
||||||
export * from './modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
|
export * from './modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
|
||||||
|
export * from './modules/blocks/useSourceIdCommon';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId';
|
import { useSourceIdCommon } from '../../../useSourceIdCommon';
|
||||||
|
|
||||||
export function useDetailsWithPaginationDecoratorProps(props) {
|
export function useDetailsWithPaginationDecoratorProps(props) {
|
||||||
let sourceId;
|
let sourceId;
|
||||||
@ -6,7 +6,7 @@ export function useDetailsWithPaginationDecoratorProps(props) {
|
|||||||
// association 的值是固定不变的,所以可以在条件中使用 hooks
|
// association 的值是固定不变的,所以可以在条件中使用 hooks
|
||||||
if (props.association) {
|
if (props.association) {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
sourceId = useDataBlockSourceId({ association: props.association });
|
sourceId = useSourceIdCommon(props.association);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -69,6 +69,7 @@ export function useCreateSingleDetailsSchema() {
|
|||||||
? {
|
? {
|
||||||
association,
|
association,
|
||||||
dataSource: item.dataSource,
|
dataSource: item.dataSource,
|
||||||
|
isCurrent: true,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
collectionName: item.collectionName || item.name,
|
collectionName: item.collectionName || item.name,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Page, expect, expectSettingsMenu, oneEmptyTableBlockWithActions, test } from '@nocobase/test/e2e';
|
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.describe('where single data details block can be added', () => {
|
||||||
test('popup', async ({ page, mockPage, mockRecord }) => {
|
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();
|
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', () => {
|
test.describe('configure actions', () => {
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
@ -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",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -6,9 +6,14 @@ export function createDetailsUISchema(options: {
|
|||||||
collectionName?: string;
|
collectionName?: string;
|
||||||
association?: string;
|
association?: string;
|
||||||
templateSchema?: ISchema;
|
templateSchema?: ISchema;
|
||||||
|
/**
|
||||||
|
* 如果为 true,则表示当前创建的区块 record 就是 useRecord 返回的 record
|
||||||
|
*/
|
||||||
|
isCurrent?: boolean;
|
||||||
}): ISchema {
|
}): ISchema {
|
||||||
const { collectionName, dataSource, association, templateSchema } = options;
|
const { collectionName, dataSource, association, templateSchema } = options;
|
||||||
const resourceName = association || collectionName;
|
const resourceName = association || collectionName;
|
||||||
|
const isCurrentObj = options.isCurrent ? { 'x-is-current': true } : {};
|
||||||
|
|
||||||
if (!dataSource) {
|
if (!dataSource) {
|
||||||
throw new Error('dataSource are required');
|
throw new Error('dataSource are required');
|
||||||
@ -29,6 +34,7 @@ export function createDetailsUISchema(options: {
|
|||||||
'x-toolbar': 'BlockSchemaToolbar',
|
'x-toolbar': 'BlockSchemaToolbar',
|
||||||
'x-settings': 'blockSettings:details',
|
'x-settings': 'blockSettings:details',
|
||||||
'x-component': 'CardItem',
|
'x-component': 'CardItem',
|
||||||
|
...isCurrentObj,
|
||||||
properties: {
|
properties: {
|
||||||
[uid()]: {
|
[uid()]: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { useParamsFromRecord } from '../../../../../block-provider/BlockProvider';
|
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) {
|
export function useDetailsDecoratorProps(props) {
|
||||||
const params = useParamsFromRecord();
|
const params = useParamsFromRecord();
|
||||||
let sourceId;
|
let sourceId;
|
||||||
@ -8,7 +18,7 @@ export function useDetailsDecoratorProps(props) {
|
|||||||
// association 的值是固定不变的,所以可以在条件中使用 hooks
|
// association 的值是固定不变的,所以可以在条件中使用 hooks
|
||||||
if (props.association) {
|
if (props.association) {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
sourceId = useDataBlockSourceId({ association: props.association });
|
sourceId = useDetailsSourceId(props.association);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -16,3 +26,18 @@ export function useDetailsDecoratorProps(props) {
|
|||||||
sourceId,
|
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];
|
||||||
|
}
|
||||||
|
@ -49,6 +49,7 @@ export function useCreateEditFormBlock() {
|
|||||||
? {
|
? {
|
||||||
association,
|
association,
|
||||||
dataSource: item.dataSource,
|
dataSource: item.dataSource,
|
||||||
|
isCurrent: true,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
collectionName: item.collectionName || item.name,
|
collectionName: item.collectionName || item.name,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { expect, oneEmptyTableBlockWithActions, test } from '@nocobase/test/e2e';
|
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.describe('where edit form block can be added', () => {
|
||||||
test('popup', async ({ page, mockPage, mockRecord }) => {
|
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();
|
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', () => {});
|
test.describe('configure actions', () => {});
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { ISchema } from '@formily/react';
|
|
||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
import { createEditFormBlockUISchema } from '../createEditFormBlockUISchema';
|
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",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,11 +7,16 @@ interface EditFormBlockOptions {
|
|||||||
collectionName?: string;
|
collectionName?: string;
|
||||||
association?: string;
|
association?: string;
|
||||||
templateSchema?: ISchema;
|
templateSchema?: ISchema;
|
||||||
|
/**
|
||||||
|
* 如果为 true,则表示当前创建的区块 record 就是 useRecord 返回的 record
|
||||||
|
*/
|
||||||
|
isCurrent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createEditFormBlockUISchema(options: EditFormBlockOptions): ISchema {
|
export function createEditFormBlockUISchema(options: EditFormBlockOptions): ISchema {
|
||||||
const { collectionName, dataSource, association, templateSchema } = options;
|
const { collectionName, dataSource, association, templateSchema } = options;
|
||||||
const resourceName = association || collectionName;
|
const resourceName = association || collectionName;
|
||||||
|
const isCurrentObj = options.isCurrent ? { 'x-is-current': true } : {};
|
||||||
|
|
||||||
if (!dataSource) {
|
if (!dataSource) {
|
||||||
throw new Error('dataSource are required');
|
throw new Error('dataSource are required');
|
||||||
@ -34,6 +39,7 @@ export function createEditFormBlockUISchema(options: EditFormBlockOptions): ISch
|
|||||||
'x-toolbar': 'BlockSchemaToolbar',
|
'x-toolbar': 'BlockSchemaToolbar',
|
||||||
'x-settings': 'blockSettings:editForm',
|
'x-settings': 'blockSettings:editForm',
|
||||||
'x-component': 'CardItem',
|
'x-component': 'CardItem',
|
||||||
|
...isCurrentObj,
|
||||||
properties: {
|
properties: {
|
||||||
[uid()]: {
|
[uid()]: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useFormBlockSourceId } from './useFormBlockSourceId';
|
import { useSourceIdCommon } from '../../../useSourceIdCommon';
|
||||||
|
|
||||||
export function useCreateFormBlockDecoratorProps(props) {
|
export function useCreateFormBlockDecoratorProps(props) {
|
||||||
let sourceId;
|
let sourceId;
|
||||||
@ -6,7 +6,7 @@ export function useCreateFormBlockDecoratorProps(props) {
|
|||||||
// association 的值是固定不变的,所以这里可以使用 hooks
|
// association 的值是固定不变的,所以这里可以使用 hooks
|
||||||
if (props.association) {
|
if (props.association) {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
sourceId = useFormBlockSourceId(props);
|
sourceId = useSourceIdCommon(props.association);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useParamsFromRecord } from '../../../../../block-provider/BlockProvider';
|
import { useParamsFromRecord } from '../../../../../block-provider/BlockProvider';
|
||||||
import { useFormBlockSourceId } from './useFormBlockSourceId';
|
import { useDetailsSourceId } from '../../details-single/hooks/useDetailsDecoratorProps';
|
||||||
|
|
||||||
export function useEditFormBlockDecoratorProps(props) {
|
export function useEditFormBlockDecoratorProps(props) {
|
||||||
const params = useFormBlockParams();
|
const params = useFormBlockParams();
|
||||||
@ -7,8 +7,9 @@ export function useEditFormBlockDecoratorProps(props) {
|
|||||||
|
|
||||||
// association 的值是固定不变的,所以这里可以使用 hooks
|
// association 的值是固定不变的,所以这里可以使用 hooks
|
||||||
if (props.association) {
|
if (props.association) {
|
||||||
|
// 复用详情区块的 sourceId 获取逻辑
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
sourceId = useFormBlockSourceId(props);
|
sourceId = useDetailsSourceId(props.association);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId';
|
|
||||||
|
|
||||||
export function useFormBlockSourceId(props) {
|
|
||||||
return useDataBlockSourceId(props);
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId';
|
import { useSourceIdCommon } from '../../../useSourceIdCommon';
|
||||||
import { useGridCardBlockParams } from './useGridCardBlockParams';
|
import { useGridCardBlockParams } from './useGridCardBlockParams';
|
||||||
|
|
||||||
export function useGridCardBlockDecoratorProps(props) {
|
export function useGridCardBlockDecoratorProps(props) {
|
||||||
@ -8,7 +8,7 @@ export function useGridCardBlockDecoratorProps(props) {
|
|||||||
// 因为 association 是固定的,所以可以在条件中使用 hooks
|
// 因为 association 是固定的,所以可以在条件中使用 hooks
|
||||||
if (props.association) {
|
if (props.association) {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
sourceId = useDataBlockSourceId({ association: props.association });
|
sourceId = useSourceIdCommon(props.association);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId';
|
import { useSourceIdCommon } from '../../../useSourceIdCommon';
|
||||||
|
|
||||||
export function useListBlockDecoratorProps(props) {
|
export function useListBlockDecoratorProps(props) {
|
||||||
let sourceId;
|
let sourceId;
|
||||||
@ -6,7 +6,7 @@ export function useListBlockDecoratorProps(props) {
|
|||||||
// 因为 association 的值是固定的,所以这里可以使用 hooks
|
// 因为 association 的值是固定的,所以这里可以使用 hooks
|
||||||
if (props.association) {
|
if (props.association) {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
sourceId = useDataBlockSourceId({ association: props.association });
|
sourceId = useSourceIdCommon(props.association);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useFieldSchema } from '@formily/react';
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { useParsedFilter } from '../../../../../block-provider/hooks/useParsedFilter';
|
import { useParsedFilter } from '../../../../../block-provider/hooks/useParsedFilter';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId';
|
import { useSourceIdCommon } from '../../../useSourceIdCommon';
|
||||||
|
|
||||||
export const useTableBlockDecoratorProps = (props) => {
|
export const useTableBlockDecoratorProps = (props) => {
|
||||||
const params = useTableBlockParams(props);
|
const params = useTableBlockParams(props);
|
||||||
@ -44,7 +44,7 @@ function useTableBlockSourceId(props) {
|
|||||||
// 因为 association 是固定不变的,所以在条件中使用 hooks 是安全的
|
// 因为 association 是固定不变的,所以在条件中使用 hooks 是安全的
|
||||||
if (props.association) {
|
if (props.association) {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
sourceId = useDataBlockSourceId({ association: props.association });
|
sourceId = useSourceIdCommon(props.association);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sourceId;
|
return sourceId;
|
||||||
|
17
packages/core/client/src/modules/blocks/useSourceIdCommon.ts
Normal file
17
packages/core/client/src/modules/blocks/useSourceIdCommon.ts
Normal file
@ -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];
|
||||||
|
};
|
26
packages/core/client/src/modules/blocks/useSourceKey.ts
Normal file
26
packages/core/client/src/modules/blocks/useSourceKey.ts
Normal file
@ -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<InheritanceCollectionMixin>(association.split('.')[0]);
|
||||||
|
|
||||||
|
// 1. hasOne 和 hasMany 和 belongsToMany 的字段存在 sourceKey,所以会直接返回 sourceKey;
|
||||||
|
// 2. belongsTo 不存在 sourceKey,所以会使用 filterTargetKey;
|
||||||
|
// 3. 后面的主键和 id 是为了保险起见加上的;
|
||||||
|
return associationField.sourceKey || sourceCollection.filterTargetKey || sourceCollection.getPrimaryKey() || 'id';
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { useDataBlockSourceId } from '@nocobase/client';
|
import { useSourceIdCommon } from '@nocobase/client';
|
||||||
import { useCalendarBlockParams } from './useCalendarBlockParams';
|
import { useCalendarBlockParams } from './useCalendarBlockParams';
|
||||||
|
|
||||||
export function useCalendarBlockDecoratorProps(props) {
|
export function useCalendarBlockDecoratorProps(props) {
|
||||||
@ -8,7 +8,7 @@ export function useCalendarBlockDecoratorProps(props) {
|
|||||||
// 因为 association 是一个固定的值,所以可以在 hooks 中直接使用
|
// 因为 association 是一个固定的值,所以可以在 hooks 中直接使用
|
||||||
if (props.association) {
|
if (props.association) {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
sourceId = useDataBlockSourceId({ association: props.association });
|
sourceId = useSourceIdCommon(props.association);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user