feat: optimize block add menu

This commit is contained in:
sealday 2024-04-03 20:02:39 +08:00
parent 93d0e17912
commit b752e1afe0
24 changed files with 1209 additions and 55 deletions

View File

@ -55,7 +55,8 @@ export const SchemaInitializerMenu: FC<MenuProps> = (props) => {
}
.ant-menu-root {
margin: 0 -${token.margin}px;
.ant-menu-submenu-title {
.ant-menu-submenu-title,
.ant-menu-item-only-child {
margin-inline: 0;
margin-block: 0;
width: 100%;

View File

@ -44,10 +44,15 @@ export function useGetSchemaInitializerMenuItems(onClick?: (args: any) => void)
if (!item.key) {
item.key = `${compiledTitle}-${indexA}`;
}
return {
return item.associationField
? {
key: item.key,
label: element,
associationField: item.associationField,
}
: {
key: item.key,
label: element,
};
}
if (item.type === 'itemGroup') {
@ -77,18 +82,27 @@ export function useGetSchemaInitializerMenuItems(onClick?: (args: any) => void)
const label = element || compiledTitle || item.label;
const key = item.key || `${parentKey}-${compiledTitle}-${indexA}`;
return {
key,
label,
associationField: item.associationField,
onClick: (info) => {
const handleClick = (info) => {
info.domEvent.stopPropagation();
if (info.key !== key) return;
if (item.onClick) {
item.onClick({ ...info, item });
} else {
onClick?.({ ...info, item });
}
},
};
return item.associationField
? {
key,
label,
associationField: item.associationField,
onClick: handleClick,
}
: {
key,
label,
onClick: handleClick,
};
});
},

View File

@ -627,6 +627,7 @@
"Current user": "Current user",
"Current role": "Current role",
"Current record": "Current record",
"Associated records": "Associated records",
"Parent record": "Parent record",
"Current time": "Current time",
"System variables": "System variables",

View File

@ -615,6 +615,7 @@
"Current user": "Usuario actual",
"Current role": "Rol actual",
"Current record": "Registro actual",
"Associated records": "Registros asociados",
"Parent record": "Registro padre",
"Current time": "Hora actual",
"System variables": "Variables del sistema",

View File

@ -612,6 +612,7 @@
"Current user": "Utilisateur actuel",
"Current role": "Rôle actuel",
"Current record": "Enregistrement actuel",
"Associated records": "Enregistrements associés",
"Parent record": "Enregistrement parent",
"Current time": "Heure actuelle",
"System variables": "Variables système",

View File

@ -515,6 +515,7 @@
"Current user": "現在のユーザー",
"Current role": "現在の役割",
"Current record": "現在のレコード",
"Associated records": "関連付けられたレコード",
"Popup close method": "ポップアップを閉じる方法",
"Automatic close": "自動で閉じる",
"Manually close": "手動で閉じる",

View File

@ -640,6 +640,7 @@
"Current user": "현재 사용자",
"Current role": "현재 역할",
"Current record": "현재 레코드",
"Associated records": "관련 레코드",
"Parent record": "상위 레코드",
"Current time": "현재 시간",
"Now": "지금",

View File

@ -451,6 +451,7 @@
"Current user": "Текущий пользователь",
"Current role": "Текущая роль",
"Current record": "Текущая запись",
"Associated records": "Связанные записи",
"Parent record": "Родительская запись",
"Popup close method": "Метод закрытия всплывающего окна",
"Automatic close": "Автоматическое закрытие",

View File

@ -450,6 +450,7 @@
"Current user": "Seçili kullanıcı",
"Current role": "Seçili rol",
"Current record": "Seçili kayıt",
"Associated records": "İlişkili kayıtlar",
"Parent record": "Üst kayıt",
"Popup close method": "Açılır pencere kapatma metodu",
"Automatic close": "Otomatik kapat",

View File

@ -632,6 +632,7 @@
"Current user": "Поточний користувач",
"Current role": "Поточна роль",
"Current record": "Поточний запис",
"Associated records": "Пов'язані записи",
"Parent record": "Батьківський запис",
"Current time": "Поточний час",
"System variables": "Системні змінні",

View File

@ -645,6 +645,7 @@
"Current user": "当前用户",
"Current role": "当前角色",
"Current record": "当前记录",
"Associated records": "关联记录",
"Parent record": "上级记录",
"Current time": "当前时间",
"Now": "现在",

View File

@ -640,6 +640,7 @@
"Current user": "當前使用者",
"Current role": "當前角色",
"Current record": "當前記錄",
"Associated records": "關聯記錄",
"Parent record": "上級記錄",
"Current time": "當前時間",
"Now": "現在",

View File

@ -9,7 +9,8 @@ test('basic', async ({ page, mockPage, mockRecord }) => {
await page.getByLabel('action-Action.Link-Edit record-update-collection1-table-0').click();
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
await page.getByRole('menuitem', { name: 'manyToMany -> collection2' }).click();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'manyToMany' }).click();
// 2. Table 中显示 Role UID 字段
await page

View File

@ -9,7 +9,7 @@ test.describe('where single data details block can be added', () => {
await page.getByLabel('action-Action.Link-View-view-general-table-0').click();
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'Details' }).hover();
await page.getByRole('menuitem', { name: 'General' }).click();
await page.getByRole('menuitem', { name: 'Current record' }).click();
await page.mouse.move(300, 0);
await expect(page.getByLabel('block-item-CardItem-general-details')).toBeVisible();
@ -25,7 +25,7 @@ test.describe('configure actions', () => {
await page.getByLabel('action-Action.Link-View-view-general-table-0').click();
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'Details' }).hover();
await page.getByRole('menuitem', { name: 'General' }).click();
await page.getByRole('menuitem', { name: 'Current record' }).click();
await page.mouse.move(300, 0);
// create edit ------------------------------------------------------------------------------------

View File

@ -21,7 +21,8 @@ test.describe('where table block can be added', () => {
// 添加当前表关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
await page.getByRole('menuitem', { name: 'childAssociationField ->' }).click();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'childAssociationField' }).click();
await page
.getByTestId('drawer-Action.Container-childCollection-View record')
.getByLabel('schema-initializer-TableV2-')
@ -31,7 +32,8 @@ test.describe('where table block can be added', () => {
// 添加父表关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
await page.getByRole('menuitem', { name: 'parentAssociationField ->' }).click();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'parentAssociationField' }).click();
await page.getByLabel('schema-initializer-TableV2-table:configureColumns-parentTargetCollection').hover();
await page.getByRole('menuitem', { name: 'parentTargetText' }).click();

View File

@ -6,6 +6,7 @@ import {
oneFormBlockWithRolesFieldBasedUsers,
test,
} from '@nocobase/test/e2e';
import { tableWithRoles, tableWithUsers, tableWithInherit, tableWithInheritWithoutAssociation } from './templatesOfBug';
test.describe('where to open a popup and what can be added to it', () => {
test('add new', async ({ page, mockPage }) => {
@ -77,7 +78,7 @@ test.describe('where to open a popup and what can be added to it', () => {
await expect(page.getByText('test8')).toBeVisible();
// add blocks
await addBlock(['table Details right', 'General']);
await addBlock(['table Details right', 'Current record']);
await addBlock(['form Form (Edit)']);
await addBlock(['Markdown']);
@ -105,16 +106,18 @@ test.describe('where to open a popup and what can be added to it', () => {
await page.getByRole('button', { name: 'OK', exact: true }).click();
// add relationship blocks
await addBlock(['table Details right', 'Many to one']);
await addBlock(['table Details right', 'Associated records', 'Many to one']);
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
await addBlock(['table Table right', 'One to many']);
await addBlock(['table Table right', 'Associated records', 'One to many']);
await expect(page.getByLabel('block-item-CardItem-users-table')).toBeVisible();
async function addBlock(names: string[]) {
await page.getByLabel('schema-initializer-Grid-popup').hover();
for (const name of names) {
await page.getByRole('menuitem', { name }).click();
for (let i = 0; i < names.length - 1; i++) {
const name = names[i];
await page.getByRole('menuitem', { name }).hover();
}
await page.getByRole('menuitem', { name: names[names.length - 1] }).click();
await page.mouse.move(300, 0);
}
});
@ -163,7 +166,7 @@ test.describe('where to open a popup and what can be added to it', () => {
// add blocks
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'Details' }).hover();
await page.getByRole('menuitem', { name: 'General' }).click();
await page.getByRole('menuitem', { name: 'Current record' }).click();
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'form Form (Edit)' }).first().click();
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
@ -188,12 +191,14 @@ test.describe('where to open a popup and what can be added to it', () => {
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'Details' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'Many to one' }).click();
await page.mouse.move(300, 0);
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'One to many' }).click();
await page.mouse.move(300, 0);
await expect(page.getByLabel('block-item-CardItem-users-table')).toBeVisible();
@ -225,3 +230,138 @@ test.describe('where to open a popup and what can be added to it', () => {
}
});
});
test.describe('add blocks to the popup', () => {
test('no inheritance, no views, no association fields', async ({ page, mockPage }) => {
await mockPage(tableWithRoles).goto();
// 打开弹窗
await page.getByLabel('action-Action.Link-View-view-roles-table-root').click();
// 直接点击 Details 选项创建详情区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Details' }).click();
await page.getByLabel('schema-initializer-Grid-details:configureFields-roles').hover();
await page.getByRole('menuitem', { name: 'Role UID' }).click();
await expect(page.getByLabel('block-item-CollectionField-Role').getByText('root')).toBeVisible();
await page.mouse.move(300, 0);
// 直接点击 Form(Edit) 选项创建表单区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'form Form (Edit)' }).click();
await page.getByLabel('schema-initializer-Grid-form:').hover();
await page.getByRole('menuitem', { name: 'Role UID' }).click();
await expect(
page.getByLabel('block-item-CollectionField-roles-form-roles.name-Role UID').getByRole('textbox'),
).toHaveValue('root');
});
test('no inheritance, no views, with association fields', async ({ page, mockPage }) => {
await mockPage(tableWithUsers).goto();
// 打开弹窗
await page.getByLabel('action-Action.Link-View-view-').click();
// 通过点击 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.getByLabel('schema-initializer-Grid-details:configureFields-users').hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click();
await page.mouse.move(300, 0);
await expect(page.getByLabel('block-item-CollectionField-users').getByText('Super Admin')).toBeVisible();
// 通过 Association 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: 'Roles' }).click();
await page.getByLabel('schema-initializer-Grid-details:configureFields-roles').hover();
await page.getByRole('menuitem', { name: 'Role UID' }).click();
await page.mouse.move(300, 0);
await expect(page.getByLabel('block-item-CollectionField-roles').getByText('admin')).toBeVisible();
});
test('with inheritance, with association fields', async ({ page, mockPage, mockRecord }) => {
const nocoPage = await mockPage(tableWithInherit).waitForInit();
const fatherRecord = await mockRecord('father');
await nocoPage.goto();
// 打开弹窗
await page.getByLabel('action-Action.Link-View-view-father-table-0').click();
// 通过 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 right' }).hover();
await page.getByRole('menuitem', { name: 'father' }).click();
await page.mouse.move(-300, 0);
await page.getByLabel('schema-initializer-Grid-details:configureFields-father').hover();
await page.getByRole('menuitem', { name: 'singleLineText' }).click();
await page.mouse.move(300, 0);
await expect(
page.getByLabel('block-item-CollectionField-father').getByText(fatherRecord.singleLineText),
).toBeVisible();
// 通过 Association records 创建关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'manyToMany' }).click();
await page.mouse.move(-300, 0);
await page
.getByTestId('drawer-Action.Container-father-View record')
.getByLabel('schema-initializer-TableV2-')
.hover();
await page.getByRole('menuitem', { name: 'singleLineText' }).click();
await page.mouse.move(300, 0);
await expect(
page
.getByTestId('drawer-Action.Container-father-View record')
.getByLabel('block-item-CardItem-')
.getByRole('row')
.getByText(fatherRecord.manyToMany[0].singleLineText),
).toBeVisible();
});
test('with inheritance, no association fields', async ({ page, mockPage, mockRecord }) => {
const nocoPage = await mockPage(tableWithInheritWithoutAssociation).waitForInit();
const fatherRecord = await mockRecord('father');
await nocoPage.goto();
// 打开弹窗
await page.getByLabel('action-Action.Link-View-view-father-table-0').click();
// 通过 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 right' }).hover();
await page.getByRole('menuitem', { name: 'father' }).click();
await page.getByLabel('schema-initializer-Grid-details:configureFields-father').hover();
await page.getByRole('menuitem', { name: 'singleLineText' }).click();
await page.mouse.move(300, 0);
await expect(
page.getByLabel('block-item-CollectionField-father').getByText(fatherRecord.singleLineText),
).toBeVisible();
});
test('only support association fields', async ({ page, mockPage }) => {
await mockPage(tableWithUsers).goto();
// 打开弹窗
await page.getByLabel('action-Action.Link-View-view-').click();
// 通过 Association records 创建一个关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'Roles' }).click();
await page
.getByTestId('drawer-Action.Container-users-View record')
.getByLabel('schema-initializer-TableV2-')
.hover();
await page.getByRole('menuitem', { name: 'Role UID' }).click();
await page.mouse.move(300, 0);
await expect(page.getByLabel('block-item-CardItem-roles-').getByRole('row').getByText('root')).toBeVisible();
});
});

View File

@ -1473,3 +1473,910 @@ export const T2838: PageConfig = {
'x-async': true,
},
};
export const tableWithRoles: PageConfig = {
pageSchema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Page',
properties: {
kuzmtxw0lbq: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'page:addBlock',
properties: {
'9snoybve3hf': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
uzodd1sblh8: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
t77ozlyo0r4: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TableBlockProvider',
'x-acl-action': 'roles:list',
'x-use-decorator-props': 'useTableBlockDecoratorProps',
'x-decorator-props': {
collection: 'roles',
dataSource: 'main',
action: 'list',
params: {
pageSize: 20,
},
rowKey: 'name',
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': 'w07zcu825vu',
'x-async': false,
'x-index': 1,
},
i8s4er3z53z: {
_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: {
cgxhx0pycze: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
properties: {
l6zuv8kq3lz: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("View") }}',
'x-action': 'view',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:view',
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
},
'x-decorator': 'ACLActionProvider',
'x-designer-props': {
linkageAction: true,
},
properties: {
drawer: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("View record") }}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
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': '2ty04bmiunh',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ymqk4t82osd',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'tpv088lk09n',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'n1mmv9b43s7',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'qh62598r5nz',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ovtpdelg5lo',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '0ck7uj73zdh',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '52a31apruw6',
'x-async': false,
'x-index': 2,
},
},
'x-uid': '8n1731cywxw',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ua6aed1x4ba',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'kplmcq59pey',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'v0qbl3nyzyo',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'nn6bi7v7ae0',
'x-async': true,
'x-index': 1,
},
};
export const tableWithUsers = {
pageSchema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Page',
properties: {
kuzmtxw0lbq: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'page:addBlock',
properties: {
au5iakeqo5e: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
'2p9r34ylza3': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
i0ywe80o2k0: {
_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': [],
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': 'e7ukdkylglc',
'x-async': false,
'x-index': 1,
},
lbh0sin622q: {
_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: {
mp82chprh15: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
properties: {
'68vzmm4j2v7': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("View") }}',
'x-action': 'view',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:view',
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
},
'x-decorator': 'ACLActionProvider',
'x-designer-props': {
linkageAction: true,
},
properties: {
drawer: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("View record") }}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
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': 'v8mceafhtt9',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'xgev5isxh8c',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'zxhc26ocbwt',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '8y380jnrc91',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'fvyqc3l0nlz',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 's9cdwte7qb5',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'e1e3l9aoqlz',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '2eta2rrzfib',
'x-async': false,
'x-index': 2,
},
},
'x-uid': 'vqxmw6sp0dp',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '1hdhsgoac99',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'delawmtj0ku',
'x-async': false,
'x-index': 2,
},
},
'x-uid': 'v0qbl3nyzyo',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'nn6bi7v7ae0',
'x-async': true,
'x-index': 1,
},
};
export const tableWithInherit = {
collections: [
{
name: 'targetCollection',
fields: [
{
type: 'string',
name: 'singleLineText',
interface: 'input',
},
],
},
{
name: 'father',
fields: [
{
type: 'string',
name: 'singleLineText',
interface: 'input',
},
{
name: 'manyToMany',
interface: 'm2m',
target: 'targetCollection',
},
],
},
{
name: 'children',
inherits: ['father'],
},
],
pageSchema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Page',
properties: {
egrrnisc7pq: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'page:addBlock',
properties: {
bb1fa9y5wwh: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
m8lbibphksn: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
xj86s1jfwmk: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TableBlockProvider',
'x-acl-action': 'father:list',
'x-use-decorator-props': 'useTableBlockDecoratorProps',
'x-decorator-props': {
collection: 'father',
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': '6j0p8tmwey7',
'x-async': false,
'x-index': 1,
},
pm3vc0ire0v: {
_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: {
'6izaz7cwt1f': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
properties: {
'9qgblzykl18': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("View") }}',
'x-action': 'view',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:view',
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
},
'x-decorator': 'ACLActionProvider',
'x-designer-props': {
linkageAction: true,
},
properties: {
drawer: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("View record") }}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
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': '9h1njletm7j',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'y9y0jzcb2pp',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '4m51ixkzi6i',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'kt1n14v8q1u',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'tkod1lyrj9z',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'e84lmt2djp7',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '8pydvn47931',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'zyfhdizs6g3',
'x-async': false,
'x-index': 2,
},
},
'x-uid': '4r6xuptmtw8',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '0aqn6al8phn',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'em7wvio4zan',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'd0zbm0ezhp5',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'qdffo69m238',
'x-async': true,
'x-index': 1,
},
};
export const tableWithInheritWithoutAssociation = {
collections: [
{
name: 'father',
fields: [
{
type: 'string',
name: 'singleLineText',
interface: 'input',
},
],
},
{
name: 'children',
inherits: ['father'],
},
],
pageSchema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Page',
properties: {
egrrnisc7pq: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'page:addBlock',
properties: {
bb1fa9y5wwh: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
m8lbibphksn: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
xj86s1jfwmk: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TableBlockProvider',
'x-acl-action': 'father:list',
'x-use-decorator-props': 'useTableBlockDecoratorProps',
'x-decorator-props': {
collection: 'father',
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': '6j0p8tmwey7',
'x-async': false,
'x-index': 1,
},
pm3vc0ire0v: {
_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: {
'6izaz7cwt1f': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
properties: {
'9qgblzykl18': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("View") }}',
'x-action': 'view',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:view',
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
},
'x-decorator': 'ACLActionProvider',
'x-designer-props': {
linkageAction: true,
},
properties: {
drawer: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("View record") }}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
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': '9h1njletm7j',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'y9y0jzcb2pp',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '4m51ixkzi6i',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'kt1n14v8q1u',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'tkod1lyrj9z',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'e84lmt2djp7',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '8pydvn47931',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'zyfhdizs6g3',
'x-async': false,
'x-index': 2,
},
},
'x-uid': '4r6xuptmtw8',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '0aqn6al8phn',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'em7wvio4zan',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'd0zbm0ezhp5',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'qdffo69m238',
'x-async': true,
'x-index': 1,
},
};

View File

@ -76,7 +76,7 @@ function useRecordBlocks() {
return false;
},
onlyCurrentDataSource: true,
// hideSearch: true,
hideSearch: true,
componentType: 'ReadPrettyFormItem',
createBlockSchema,
templateWrap: useCallback(
@ -92,7 +92,6 @@ function useRecordBlocks() {
[templateWrap, templateWrapOfAssociationDetailsWithoutPagination],
),
showAssociationFields: true,
hideChildrenIfSingleCollection: true,
};
},
},
@ -115,12 +114,11 @@ function useRecordBlocks() {
return false;
},
onlyCurrentDataSource: true,
// hideSearch: true,
hideSearch: true,
componentType: 'FormItem',
createBlockSchema: createEditFormBlock,
templateWrap: templateWrap,
showAssociationFields: true,
hideChildrenIfSingleCollection: true,
};
},
useVisible() {
@ -143,7 +141,7 @@ function useRecordBlocks() {
return false;
},
onlyCurrentDataSource: true,
// hideSearch: true,
hideSearch: true,
componentType: 'FormItem',
createBlockSchema: createAssociationFormBlock,
templateWrap: templateWrap,
@ -179,7 +177,7 @@ function useRecordBlocks() {
const { createAssociationTableBlock } = useCreateAssociationTableBlock();
return {
// hideSearch: true,
hideSearch: true,
onlyCurrentDataSource: true,
filterCollections({ associationField }) {
if (associationField) {
@ -210,7 +208,7 @@ function useRecordBlocks() {
const { createAssociationListBlock } = useCreateAssociationListBlock();
return {
// hideSearch: true,
hideSearch: true,
onlyCurrentDataSource: true,
filterCollections({ associationField }) {
if (associationField) {
@ -241,7 +239,7 @@ function useRecordBlocks() {
const { createAssociationGridCardBlock } = useCreateAssociationGridCardBlock();
return {
// hideSearch: true,
hideSearch: true,
onlyCurrentDataSource: true,
filterCollections({ associationField }) {
if (associationField) {

View File

@ -277,6 +277,7 @@ export interface DataBlockInitializerProps {
showAssociationFields?: boolean;
/** 如果只有一项数据表时,不显示 children 列表 */
hideChildrenIfSingleCollection?: boolean;
items?: ReturnType<typeof useCollectionDataSourceItems>[];
}
export const DataBlockInitializer = (props: DataBlockInitializerProps) => {
@ -293,6 +294,7 @@ export const DataBlockInitializer = (props: DataBlockInitializerProps) => {
showAssociationFields,
hideChildrenIfSingleCollection,
filterDataSource,
items: itemsFromProps,
} = props;
const { insert, setVisible } = useSchemaInitializer();
const compile = useCompile();
@ -311,12 +313,16 @@ export const DataBlockInitializer = (props: DataBlockInitializerProps) => {
},
[getTemplateSchemaByMode, insert, onCreateBlockSchema, setVisible, templateWrap],
);
const items = useCollectionDataSourceItems({
const items =
itemsFromProps ||
// eslint-disable-next-line react-hooks/rules-of-hooks
useCollectionDataSourceItems({
componentName: componentType,
filter,
filterDataSource,
onlyCurrentDataSource,
showAssociationFields,
dataBlockInitializerProps: props,
});
const getMenuItems = useGetSchemaInitializerMenuItems(onClick);
const childItems = useMemo(() => {
@ -326,8 +332,7 @@ export const DataBlockInitializer = (props: DataBlockInitializerProps) => {
const searchedChildren = useMenuSearch({ data: childItems, openKeys: openMenuKeys, hideSearch });
const compiledMenuItems = useMemo(() => {
let children = searchedChildren.filter((item) => item.key !== 'search' && item.key !== 'empty');
const hasAssociationField = children.some((item) => item.associationField);
if (hideChildrenIfSingleCollection && !hasAssociationField && children.length === 1) {
if (hideChildrenIfSingleCollection && children.length === 1) {
// 只有一项可选时,直接展开
children = children[0].children;
} else {
@ -345,7 +350,7 @@ export const DataBlockInitializer = (props: DataBlockInitializerProps) => {
children,
},
];
}, [name, compile, title, icon, searchedChildren, onClick, props]);
}, [searchedChildren, hideChildrenIfSingleCollection, name, compile, title, icon, onClick, props]);
if (childItems.length > 1 || (childItems.length === 1 && childItems[0].children?.length > 0)) {
return (

View File

@ -5,6 +5,7 @@ import _ from 'lodash';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
DataBlockInitializer,
DataSource,
SchemaInitializerItemType,
useCollection,
@ -840,12 +841,14 @@ export const useCollectionDataSourceItems = ({
onlyCurrentDataSource = false,
showAssociationFields,
filterDataSource,
dataBlockInitializerProps,
}: {
componentName;
filter?: (options: { collection?: Collection; associationField?: CollectionFieldOptions }) => boolean;
onlyCurrentDataSource?: boolean;
showAssociationFields?: boolean;
filterDataSource?: (dataSource?: DataSource) => boolean;
dataBlockInitializerProps?: any;
}) => {
const { t } = useTranslation();
const dm = useDataSourceManager();
@ -867,7 +870,8 @@ export const useCollectionDataSourceItems = ({
}
const { getTemplatesByCollection } = useSchemaTemplateManager();
const res = useMemo(() => {
const noAssociationMenu = useMemo(() => {
return allCollections.map(({ key, displayName, collections }) => ({
name: key,
label: displayName,
@ -885,12 +889,80 @@ export const useCollectionDataSourceItems = ({
const inherits = _.toArray(collection?.inherits || []);
if (item.name === collection?.name || inherits.some((inheritName) => inheritName === item.name)) return -1;
}),
...associationFields,
],
}));
}, [allCollections, associationFields, componentName, getTemplatesByCollection, t]);
}, [allCollections, collection?.inherits, collection?.name, componentName, getTemplatesByCollection, t]);
return res;
// https://nocobase.height.app/T-3821
// showAssociationFields 的值是固定不变的,所以在 if 语句里使用 hooks 是安全的
if (showAssociationFields) {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useMemo(() => {
const currentRecord = {
name: 'currentRecord',
collectionName: collection.name,
dataSource: collection.dataSource,
Component: DataBlockInitializer,
// 目的是使点击无效
onClick() {},
componentProps: {
...dataBlockInitializerProps,
icon: null,
title: t('Current record'),
name: 'currentRecord',
hideSearch: false,
hideChildrenIfSingleCollection: true,
items: noAssociationMenu,
},
};
const associationRecords = {
name: 'associationRecords',
Component: DataBlockInitializer,
// 目的是使点击无效
onClick() {},
componentProps: {
...dataBlockInitializerProps,
icon: null,
title: t('Associated records'),
name: 'associationRecords',
hideSearch: false,
items: [
{
name: 'associationFields',
label: t('Association fields'),
type: 'subMenu', // 这里套一层 subMenu 是因为 DataBlockInitializer 组件需要这样的数据结构,其实这层 subMenu 最终是不会渲染出来的
children: associationFields,
},
],
},
};
let children;
if (noAssociationMenu[0].children.length && associationFields.length) {
children = [currentRecord, associationRecords];
} else if (noAssociationMenu[0].children.length) {
// 当可选数据表只有一个时,实现只点击一次区块 menu 就能创建区块
if (noAssociationMenu[0].children.length <= 1) {
noAssociationMenu[0].children = (noAssociationMenu[0].children[0]?.children as any) || [];
return noAssociationMenu;
}
children = [currentRecord];
} else {
children = [associationRecords];
}
return [
{
name: 'records',
label: t('Records'),
type: 'subMenu',
children,
},
];
}, [associationFields, collection.dataSource, collection.name, dataBlockInitializerProps, noAssociationMenu, t]);
}
return noAssociationMenu;
};
export const createDetailsBlockSchema = (options: {
@ -1396,8 +1468,7 @@ function useAssociationFields({
.filter((field) => ['linkTo', 'subTable', 'o2m', 'm2m', 'obo', 'oho', 'o2o', 'm2o'].includes(field.interface))
.filter((field) => filterCollections({ associationField: field }))
.map((field, index) => {
const targetCollection = cm.getCollection(field.target);
const title = `${compile(field.uiSchema.title || field.name)} -> ${compile(targetCollection.title)}`;
const title = compile(field.uiSchema.title || field.name);
const templates = getTemplatesByCollection(dataSource, field.target).filter((template) => {
// 针对弹窗中的详情区块
if (componentName === 'ReadPrettyFormItem') {

View File

@ -20,7 +20,6 @@ export const defineConfig = (config?: PlaywrightTestConfig) => {
// Fail the build on CI if you accidentally left test.only in the source code.
forbidOnly: !!process.env.CI,
// Retry on CI only.
retries: 2,
// Opt out of parallel tests on CI.

View File

@ -53,6 +53,7 @@ export interface CollectionSetting {
* @default false
*/
inherit?: boolean;
inherits?: string[];
category?: any[];
hidden?: boolean;
description?: string;
@ -620,6 +621,9 @@ const deleteCollections = async (collectionNames: string[]) => {
const result = await api.post(`/api/collections:destroy?${params}`, {
headers,
params: {
cascade: true,
},
});
if (!result.ok()) {

View File

@ -24,7 +24,8 @@ test.describe('where can be added', () => {
await page.getByLabel('action-Action.Link-View-view-').first().click();
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'form Calendar right' }).hover();
await page.getByRole('menuitem', { name: 'manyToMany -> calendar' }).click();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'manyToMany' }).click();
await page.getByLabel('block-item-Select-Title field').getByTestId('select-single').click();
await page.getByRole('option', { name: 'Repeats' }).click();

View File

@ -51,6 +51,7 @@ export class PluginCalendarClient extends Plugin {
},
createBlockSchema: createAssociationCalendarBlock,
showAssociationFields: true,
hideSearch: true,
};
},
});