chore: optimize locators (#2833)

* test(e2e): better locators for designer buttons

* fix: make test passing

* refactor: remove DesignerControl

* chore: better locators

* fix: should not disable add-menu-item

* chore: better test id for block

* chore: optimize Action

* chore: remove role in BlockItem

* feat: improve locators

* chore: menu & add block

* chore: initializer

* chore: testid -> aria label

* chore: tabs

* chore: designers

* refactor: optimize schemaInitializer

* refactor: rename

* chore: add collectionName

* chore: block item

* chore: action

* fix: avoid crashting

* chore(e2e): add __E2E__

* chore: all dialog

* chore: add aria-label for block menu

* Revert "chore: add aria-label for block menu"

This reverts commit 6a840ef816ee1095484dc268b5dfa1bbe6cd8cbe.

* chore: optimize aria-label of Action

* chore: schema-initializer

* chore(e2e): increase timeout

* chore: schema settings

* chore: optimize table

* chore: workflow

* chore: plugin manager

* chore: collection manager and workflow

* chore: details of workflow

* chore: remove testid of Select

* test: fix unit-tests

* test: fix unit-tests

* test(e2e): passing tests

* test: fix unit test

* chore: should use hover

* test: passing tests

* chore: passing tests

* chore: fix CI

* chore: fix CI

* chore: increase timeout in CI

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
被雨水过滤的空气-Rain 2023-10-27 15:32:17 +08:00 committed by GitHub
parent 5a342bdbc7
commit 29bf187fbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
189 changed files with 1402 additions and 825 deletions

View File

@ -15,6 +15,7 @@ APP_PORT=20000
APP_BASE_URL=http://localhost:20000
APP_KEY=test-key-e2e
SOCKET_PATH=storage/gateway-e2e.sock
__E2E__=true
API_BASE_PATH=/api/
API_BASE_URL=

View File

@ -36,6 +36,7 @@ export function buildEsm(cwd: string, userConfig: UserConfig, sourcemap: boolean
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.__TEST__': false,
'process.env.__E2E__': false,
},
build: {
minify: false,

View File

@ -1,13 +1,15 @@
import fs from 'fs-extra';
import chalk from 'chalk';
import ncc from '@vercel/ncc';
import path from 'path';
import react from '@vitejs/plugin-react';
import chalk from 'chalk';
import fg from 'fast-glob';
import fs from 'fs-extra';
import path from 'path';
import { build as tsupBuild } from 'tsup';
import { build as viteBuild } from 'vite';
import fg from 'fast-glob';
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
import { EsbuildSupportExts, globExcludeFiles } from './constant';
import { PkgLog, UserConfig, getPackageJson } from './utils';
import {
buildCheck,
checkFileSize,
@ -18,8 +20,6 @@ import {
getSourcePackages,
} from './utils/buildPluginUtils';
import { getDepPkgPath, getDepsConfig } from './utils/getDepsConfig';
import { EsbuildSupportExts, globExcludeFiles } from './constant';
import { PkgLog, UserConfig, getPackageJson } from './utils';
const validExts = ['.ts', '.tsx', '.js', '.jsx', '.mjs'];
const serverGlobalFiles: string[] = ['src/**', '!src/client/**', ...globExcludeFiles];
@ -304,6 +304,7 @@ export async function buildPluginClient(cwd: string, userConfig: UserConfig, sou
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.__TEST__': false,
'process.env.__E2E__': false,
},
logLevel: 'warn',
build: {

View File

@ -280,12 +280,12 @@ const config = {
};
// fix https://nocobase.height.app/T-2165
test.skip('BUG: variable labels should be displayed normally', async ({ page, mockPage }) => {
test('BUG: variable labels should be displayed normally', async ({ page, mockPage }) => {
await mockPage(config).goto();
await page.getByTestId('users-resource').hover();
await page.getByTestId('designer-schema-settings').first().hover();
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
await page.getByLabel('block-item-CardItem-users-form').hover();
await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-users').hover();
await page.getByLabel('Linkage rules').click();
await expect(page.getByText('Current form / Nickname')).toBeVisible();
await expect(page.getByText('Current form / Phone')).toBeVisible();

View File

@ -714,9 +714,11 @@ const config = {
test('BUG: should show default value option', async ({ page, mockPage }) => {
await mockPage(config).goto();
await page.getByRole('row', { name: '1 View' }).getByText('View').click();
await page.getByLabel('action-Action.Link-View-view-t_ylz5vtxncxq-table-0').click();
await page.getByText('Single select:').hover();
await page.getByTestId('t_ylz5vtxncxq.f_nr8xi7ezw5t-field').getByTestId('designer-schema-settings').hover();
await page
.getByLabel('designer-schema-settings-CollectionField-FormItem.Designer-t_ylz5vtxncxq-t_ylz5vtxncxq.f_nr8xi7ezw5t')
.hover();
await expect(page.getByRole('menuitem', { name: 'Set default value' })).toBeVisible();
await expect(page.getByLabel('Set default value')).toBeVisible();
});

View File

@ -214,14 +214,12 @@ const config = {
};
// fix https://nocobase.height.app/T-2176
test.skip('BUG: Relationship block issue, menu should not display loading', async ({ page, mockPage }) => {
test('BUG: Relationship block issue, menu should not display loading', async ({ page, mockPage }) => {
await mockPage(config).goto();
await page.getByRole('row', { name: '1 View' }).getByTestId('view-action').click();
await page.getByTestId('action-drawer').getByRole('button', { name: 'plus Add block' }).hover();
await page.getByLabel('action-Action.Link-View-view-roles-table-root').click();
await page.getByLabel('schema-initializer-Grid-RecordBlockInitializers-roles').hover();
await page.waitForTimeout(300);
// TODO: 这里无论是 not.toBeVisible 还是 toBeVisible 都会通过,暂时不知道怎么解决,先跳过
// 这个输入框不应该显示
await expect(page.getByPlaceholder('Search and select collection')).not.toBeVisible();
await expect(page.getByText('Relationship blocks')).not.toBeVisible();
await expect(page.getByPlaceholder('Search and select collection')).toBeHidden();
});

View File

@ -163,16 +163,16 @@ const config = {
// fix https://nocobase.height.app/T-2183
test('BUG: should save conditions', async ({ page, mockPage }) => {
await mockPage(config).goto();
await page.getByTestId('filter-action').click();
await page.getByLabel('action-Filter.Action-Filter-filter-users-table').click();
await page.getByText('Add condition', { exact: true }).click();
await page.getByTestId('antd-cascader').getByLabel('Search').click();
await page.getByTestId('filter-select-field').getByLabel('Search').click();
await page.getByRole('menuitemcheckbox', { name: 'ID' }).click();
await page.getByRole('button', { name: 'Save conditions' }).click();
await page.reload();
await page.getByTestId('filter-action').click();
await page.getByLabel('action-Filter.Action-Filter-filter-users-table').click();
// After refreshing the browser, the set field and operator should still be visible
await expect(page.getByTestId('antd-cascader').getByTitle('ID')).toBeVisible();
await expect(page.getByText('IDis')).toBeVisible();
await expect(page.getByTestId('filter-select-field').getByText('ID')).toBeVisible();
await expect(page.getByTestId('filter-select-operator').getByText('is')).toBeVisible();
});

View File

@ -164,8 +164,8 @@ const config = {
test('BUG(Filter): the input box displayed should correspond to the field type', async ({ page, mockPage }) => {
await mockPage(config).goto();
await page.getByTestId('filter-action').click();
await page.getByTestId('antd-cascader').getByLabel('Search').click();
await page.getByLabel('action-Filter.Action-Filter-filter-users-table').click();
await page.getByTestId('filter-select-field').getByLabel('Search').click();
await page.getByRole('menuitemcheckbox', { name: 'ID' }).click();
// 应该显示数字输入框

View File

@ -362,7 +362,7 @@ test('BUG: in the Duplicate mode, the Roles field should not have a value after
}) => {
await mockPage(config).goto();
await page.getByText('Duplicate').click();
await expect(page.getByTestId('users.nickname-field').getByRole('textbox')).toHaveValue('Super Admin');
await expect(page.getByText('AdminMemberRoot')).not.toBeVisible();
await page.getByLabel('action-Action.Link-Duplicate-duplicate-users-table-0').click();
await expect(page.getByRole('textbox')).toHaveValue('Super Admin');
await expect(page.getByTestId('select-object-multiple')).toHaveText('');
});

View File

@ -341,30 +341,30 @@ const config = {
};
// fix https://nocobase.height.app/T-2200
test.skip('BUG: should be possible to change the value of the association field normally', async ({
page,
mockPage,
}) => {
test('BUG: should be possible to change the value of the association field normally', async ({ page, mockPage }) => {
await mockPage(config).goto();
await page.getByTestId('table-block').getByText('Edit').click();
await expect(page.getByTestId('antd-select').getByText('AdminMemberRoot')).toBeVisible();
await page.getByLabel('action-Action.Link-Edit-update-users-table-0').click();
await expect(page.getByLabel('Admin')).toBeVisible();
await expect(page.getByLabel('Member')).toBeVisible();
await expect(page.getByLabel('Root')).toBeVisible();
await page.getByTestId('antd-select').getByText('AdminMemberRoot').click();
await page.getByTestId('select-object-multiple').click();
await page.getByRole('option', { name: 'Member' }).click();
// 再次点击,关闭下拉框。
await page.getByTestId('select-object-multiple').click();
// 点击空白处,关闭下拉框。如不关闭,会挡住按钮,导致鼠标 hover 无效
await page
.getByTestId('action-drawer')
.locator('div')
.filter({ hasText: 'EditAdd tabRoles:AdminRoot Configure fieldsConfigure actionsAdd block' })
.nth(2)
.click();
await expect(page.getByLabel('Admin')).toBeVisible();
await expect(page.getByLabel('Member')).toBeHidden();
await expect(page.getByLabel('Root')).toBeVisible();
await expect(page.getByTestId('antd-select').getByText('AdminRoot')).toBeVisible();
await page.getByLabel('schema-initializer-Grid-FormItemInitializers-users').hover();
await page.getByLabel('Display collection fields-Nickname').click();
await page.mouse.move(200, 0);
await page.getByTestId('configure-fields-button-of-form-item-users').hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click();
await page.waitForTimeout(200);
await expect(page.getByTestId('antd-select').getByText('AdminRoot')).toBeVisible();
await expect(page.getByLabel('Admin')).toBeVisible();
await expect(page.getByLabel('Member')).toBeHidden();
await expect(page.getByLabel('Root')).toBeVisible();
});

View File

@ -344,9 +344,9 @@ test('check table row', async ({ page, mockPage }) => {
await mockPage(config).goto();
await expect(page.getByText('cur vulpes suppellex')).toBeVisible();
await page.getByTestId('table-index-1').hover();
await page.getByTestId('table-index-1').getByLabel('').click();
await page.getByTestId('destroy-action').click();
await page.getByLabel('table-index-1').hover();
await page.getByRole('checkbox', { name: 'checkbox' }).click();
await page.getByLabel('action-Action-Delete-destroy-t_fhdhd0nk7b9-table').click();
// 显示确认弹窗
await expect(page.getByText('Are you sure you want to delete it?')).toBeVisible();

View File

@ -247,11 +247,11 @@ test.describe('createCollections', () => {
test('quickly create collections', async ({ page, mockPage }) => {
await mockPage(pageConfig).goto();
await page.getByRole('button', { name: 'plus Add block' }).hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
await page.getByLabel('schema-initializer-Grid-BlockInitializers').hover();
await page.getByLabel('dataBlocks-table', { exact: true }).hover();
await expect(page.getByRole('menuitem', { name: 'collection1' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'collection2' })).toBeVisible();
await expect(page.getByLabel('dataBlocks-table-collection1')).toBeVisible();
await expect(page.getByLabel('dataBlocks-table-collection2')).toBeVisible();
});
test('no page, just create collections', async ({ page, createCollections }) => {

View File

@ -4,50 +4,50 @@ test.describe('create data block', () => {
test('table', async ({ page, mockPage }) => {
await mockPage().goto();
await page.getByRole('button', { name: 'plus Add block' }).hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
await page.getByLabel('schema-initializer-Grid-BlockInitializers').hover();
await page.getByLabel('dataBlocks-table', { exact: true }).hover();
await page.getByLabel('dataBlocks-table-Users').click();
await expect(page.getByRole('columnheader', { name: 'setting Configure columns' })).toBeVisible();
await expect(page.getByText('Configure columns')).toBeVisible();
});
test('form', async ({ page, mockPage }) => {
await mockPage().goto();
await page.getByRole('button', { name: 'plus Add block' }).hover();
await page.getByRole('menuitem', { name: 'form Form right' }).first().hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
await page.getByLabel('schema-initializer-Grid-BlockInitializers').hover();
await page.getByLabel('dataBlocks-form', { exact: true }).hover();
await page.getByLabel('dataBlocks-form-Users').click();
await expect(page.getByRole('button', { name: 'setting Configure fields' })).toBeVisible();
await expect(page.getByText('Configure fields')).toBeVisible();
});
test('details', async ({ page, mockPage }) => {
await mockPage().goto();
await page.getByRole('button', { name: 'plus Add block' }).hover();
await page.getByRole('menuitem', { name: 'table Details right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
await page.getByLabel('schema-initializer-Grid-BlockInitializers').hover();
await page.getByLabel('dataBlocks-details', { exact: true }).hover();
await page.getByLabel('dataBlocks-details-Users').click();
await expect(page.getByRole('button', { name: 'setting Configure fields' })).toBeVisible();
await expect(page.getByText('Configure fields')).toBeVisible();
});
test('list', async ({ page, mockPage }) => {
await mockPage().goto();
await page.getByRole('button', { name: 'plus Add block' }).hover();
await page.getByRole('menuitem', { name: 'ordered-list List right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
await page.getByLabel('schema-initializer-Grid-BlockInitializers').hover();
await page.getByLabel('dataBlocks-List', { exact: true }).hover();
await page.getByLabel('dataBlocks-List-Users').click();
await expect(page.getByRole('button', { name: 'setting Configure fields' }).first()).toBeVisible();
await expect(page.getByText('Configure fields').first()).toBeVisible();
});
test('grid card', async ({ page, mockPage }) => {
await mockPage().goto();
await page.getByRole('button', { name: 'plus Add block' }).hover();
await page.getByRole('menuitem', { name: 'ordered-list Grid Card right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
await page.getByLabel('schema-initializer-Grid-BlockInitializers').hover();
await page.getByLabel('dataBlocks-GridCard', { exact: true }).hover();
await page.getByLabel('dataBlocks-GridCard-Users').click();
await expect(page.getByRole('button', { name: 'setting Configure fields' }).first()).toBeVisible();
await expect(page.getByText('Configure fields').first()).toBeVisible();
});
});

View File

@ -5,19 +5,19 @@ test.describe('menu', () => {
await page.goto('/');
await enableToConfig(page);
await page.getByTestId('add-menu-item-button-in-header').hover();
await page.getByRole('menuitem', { name: 'Page' }).click();
await page.getByLabel('schema-initializer-Menu-header').hover();
await page.getByLabel('Page').click();
await page.getByRole('textbox').click();
await page.getByRole('textbox').fill('new page');
await page.getByRole('button', { name: 'OK' }).click();
await page.getByText('new page').click();
await expect(page.getByTestId('add-block-button-in-page')).toBeVisible();
await expect(page.getByLabel('schema-initializer-Grid-BlockInitializers')).toBeVisible();
// 删除页面,避免影响其他测试
await page.getByRole('menu').getByText('new page').hover();
await page.getByTestId('designer-schema-settings').hover();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByLabel('designer-schema-settings-Menu.Item').hover();
await page.getByLabel('Delete').click();
await page.getByRole('button', { name: 'OK' }).click();
});
});

View File

@ -84,7 +84,7 @@ export const StrategyActions = connect((props) => {
render: (scope, action) =>
!action.onNewRecord && (
<Select
data-testid="antd-select"
data-testid="select-data-scope"
popupMatchSelectWidth={false}
size={'small'}
value={scope}

View File

@ -286,29 +286,49 @@ export const RenderChildrenWithAssociationFilter: React.FC<any> = (props) => {
return props.children;
};
export const BlockProvider = (props) => {
const { collection, association } = props;
const BlockContext = createContext<{
/** 用以区分区块的标识 */
name: string;
}>(null);
export const useBlockContext = () => {
return useContext(BlockContext);
};
export const BlockProvider = (props: {
name: string;
resource: any;
collection?: any;
association?: any;
params?: any;
children?: any;
}) => {
const { collection, association, name } = props;
const resource = useResource(props);
const params = useMemo(() => ({ ...props.params }), [props.params]);
const { appends, updateAssociationValues } = useAssociationNames();
const blockValue = useMemo(() => ({ name }), [name]);
if (!Object.keys(params).includes('appends')) {
params['appends'] = appends;
}
return (
<MaybeCollectionProvider collection={collection}>
<BlockAssociationContext.Provider value={association}>
<BlockResourceContext.Provider value={resource}>
<BlockRequestProvider {...props} updateAssociationValues={updateAssociationValues} params={params}>
<SharedFilterProvider {...props} params={params}>
<FilterBlockRecord {...props} params={params}>
<div data-testid={props['data-testid']}>{props.children}</div>
</FilterBlockRecord>
</SharedFilterProvider>
</BlockRequestProvider>
</BlockResourceContext.Provider>
</BlockAssociationContext.Provider>
</MaybeCollectionProvider>
<BlockContext.Provider value={blockValue}>
<MaybeCollectionProvider collection={collection}>
<BlockAssociationContext.Provider value={association}>
<BlockResourceContext.Provider value={resource}>
<BlockRequestProvider {...props} updateAssociationValues={updateAssociationValues} params={params}>
<SharedFilterProvider {...props} params={params}>
<FilterBlockRecord {...props} params={params}>
{props.children}
</FilterBlockRecord>
</SharedFilterProvider>
</BlockRequestProvider>
</BlockResourceContext.Provider>
</BlockAssociationContext.Provider>
</MaybeCollectionProvider>
</BlockContext.Provider>
);
};

View File

@ -48,7 +48,7 @@ const InternalCalendarBlockProvider = (props) => {
export const CalendarBlockProvider = (props) => {
return (
<BlockProvider data-testid="calendar-block" {...props} params={{ ...props.params, paginate: false }}>
<BlockProvider name="calendar" {...props} params={{ ...props.params, paginate: false }}>
<InternalCalendarBlockProvider {...props} />
</BlockProvider>
);

View File

@ -56,7 +56,7 @@ const InternalDetailsBlockProvider = (props) => {
export const DetailsBlockProvider = (props) => {
return (
<BlockProvider data-testid="details-block" {...props}>
<BlockProvider name="details" {...props}>
<InternalDetailsBlockProvider {...props} />
</BlockProvider>
);

View File

@ -3,9 +3,11 @@ import { DatePickerProvider } from '../schema-component';
import { FormBlockProvider } from './FormBlockProvider';
export const FilterFormBlockProvider = (props) => {
const { collection, resource } = props;
return (
<DatePickerProvider value={{ utc: false }}>
<FormBlockProvider data-testid="filter-form-block" {...props}></FormBlockProvider>
<FormBlockProvider name="filter-form" {...props}></FormBlockProvider>
</DatePickerProvider>
);
};

View File

@ -103,7 +103,7 @@ export const FormBlockProvider = (props) => {
(currentCollection.name === (collection?.name || collection) && !isDetailBlock) || !currentCollection.name;
return (
(detailFlag || createFlag || isCusomeizeCreate) && (
<BlockProvider data-testid={props['data-testid'] || 'form-block'} {...props} block={'form'}>
<BlockProvider name={props.name || 'form'} {...props} block={'form'}>
<FormActiveFieldsProvider name="form">
<InternalFormBlockProvider {...props} />
</FormActiveFieldsProvider>

View File

@ -77,7 +77,7 @@ export const WithoutFormFieldResource = createContext(null);
export const FormFieldProvider = (props) => {
return (
<WithoutFormFieldResource.Provider value={false}>
<BlockProvider data-testid="form-field-block" block={'FormField'} {...props}>
<BlockProvider name="form-field" block={'FormField'} {...props}>
<InternalFormFieldProvider {...props} />
</BlockProvider>
</WithoutFormFieldResource.Provider>

View File

@ -73,7 +73,7 @@ const InternalGanttBlockProvider = (props) => {
export const GanttBlockProvider = (props) => {
const params = { filter: props.params.filter, tree: true, paginate: false, sort: props.fieldNames.start };
return (
<BlockProvider data-testid="gantt-block" {...props} params={params}>
<BlockProvider name="gantt" {...props} params={params}>
<TableBlockProvider {...props} params={params}>
<InternalGanttBlockProvider {...props} />
</TableBlockProvider>

View File

@ -108,7 +108,7 @@ export const KanbanBlockProvider = (props) => {
params['appends'] = appends;
}
return (
<BlockProvider data-testid="kanban-block" {...props} params={params}>
<BlockProvider name="kanban" {...props} params={params}>
<InternalKanbanBlockProvider {...props} params={params} />
</BlockProvider>
);

View File

@ -116,7 +116,7 @@ export const TableBlockProvider = (props) => {
return (
<SchemaComponentOptions scope={{ treeTable }}>
<FormContext.Provider value={form}>
<BlockProvider data-testid="table-block" {...props} params={paramsWithFilter} runWhenParamsChanged>
<BlockProvider name={props.name || 'table'} {...props} params={paramsWithFilter} runWhenParamsChanged>
<InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={paramsWithFilter} />
</BlockProvider>
</FormContext.Provider>

View File

@ -126,7 +126,7 @@ export const WithoutTableFieldResource = createContext(null);
export const TableFieldProvider = (props) => {
return (
<WithoutTableFieldResource.Provider value={false}>
<BlockProvider data-testid="table-field-block" block={'TableField'} {...props}>
<BlockProvider name="table-field" block={'TableField'} {...props}>
<InternalTableFieldProvider {...props} />
</BlockProvider>
</WithoutTableFieldResource.Provider>

View File

@ -21,6 +21,7 @@ const TableSelectorParamsContext = createContext<Params>({}); // 用于传递参
type TableSelectorProviderProps = {
params: Record<string, any>;
resource: any;
collection?: string;
dragSort?: boolean;
children?: any;
@ -238,7 +239,7 @@ export const TableSelectorProvider = (props: TableSelectorProviderProps) => {
return (
<SchemaComponentOptions scope={{ treeTable }}>
<BlockProvider data-testid="table-selector-block" {...props} params={params}>
<BlockProvider name="table-selector" {...props} params={params}>
<InternalTableSelectorProvider {...props} params={params} extraFilter={extraFilter} />
</BlockProvider>
</SchemaComponentOptions>

View File

@ -14,6 +14,11 @@ interface ProviderProps extends Partial<FormActiveFieldsProviderValue> {
children: React.ReactNode;
}
/**
*
* @param param0
* @returns
*/
export const FormActiveFieldsProvider = ({
children,
name,

View File

@ -257,7 +257,12 @@ export const AddCollectionAction = (props) => {
type: 'divider',
});
}
result.push({ label: compile(item.title), key: item.name });
result.push({
role: 'button',
'aria-label': `create-collection-${item.name}`,
label: compile(item.title),
key: item.name,
});
});
return result;
}, [collectionTemplates]);
@ -284,7 +289,7 @@ export const AddCollectionAction = (props) => {
<ActionContextProvider value={{ visible, setVisible }}>
<Dropdown getPopupContainer={getContainer} trigger={trigger} align={align} menu={menu}>
{children || (
<Button icon={<PlusOutlined />} type={'primary'}>
<Button aria-label="Create collection" icon={<PlusOutlined />} type={'primary'}>
{t('Create collection')} <DownOutlined />
</Button>
)}

View File

@ -9,11 +9,11 @@ import { useTranslation } from 'react-i18next';
import { useRequest } from '../../api-client';
import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import { useCancelAction } from '../action-hooks';
import { useCollectionManager } from '../hooks';
import useDialect from '../hooks/useDialect';
import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import * as components from './components';
import { getOptions } from './interfaces';
@ -255,6 +255,8 @@ export const AddFieldAction = (props) => {
.filter((child) => ['m2o'].includes(child.name))
.map((child) => {
return {
role: 'button',
'aria-label': `${compile(option.label)}-${child.name}`,
label: compile(child.title),
title: compile(child.title),
key: child.name,
@ -272,6 +274,8 @@ export const AddFieldAction = (props) => {
.filter((child) => !['o2o', 'subTable', 'linkTo'].includes(child.name))
.map((child) => {
return {
role: 'button',
'aria-label': `${compile(option.label)}-${child.name}`,
label: compile(child.title),
title: compile(child.title),
key: child.name,
@ -307,7 +311,7 @@ export const AddFieldAction = (props) => {
<ActionContextProvider value={{ visible, setVisible }}>
<Dropdown getPopupContainer={getContainer} trigger={trigger} align={align} menu={menu}>
{children || (
<Button icon={<PlusOutlined />} type={'primary'}>
<Button aria-label="Add field" icon={<PlusOutlined />} type={'primary'}>
{t('Add field')}
</Button>
)}

View File

@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
import { useCurrentAppInfo } from '../../appInfo';
import { RecordProvider, useRecord } from '../../record-provider';
import { Action, useAttach, useCompile } from '../../schema-component';
import { Input } from '../../schema-component/antd/input';
import {
isDeleteButtonDisabled,
useBulkDestroyActionAndRefreshCM,
@ -24,9 +25,8 @@ import { EditCollectionField } from './EditFieldAction';
import { OverridingCollectionField } from './OverridingCollectionField';
import { collection } from './schemas/collectionFields';
import { SyncFieldsAction } from './SyncFieldsAction';
import { ViewCollectionField } from './ViewInheritedField';
import { SyncSQLFieldsAction } from './SyncSQLFieldsAction';
import { Input } from '../../schema-component/antd/input';
import { ViewCollectionField } from './ViewInheritedField';
const indentStyle = css`
.ant-table {
@ -105,6 +105,7 @@ const CurrentFields = (props) => {
return isTitleField(record) ? (
<Tooltip title={t(titlePrompt)} placement="right" overlayInnerStyle={{ textAlign: 'center' }}>
<Switch
aria-label={`switch-title-field-${record.name}`}
size="small"
loading={record.name === loadingRecord?.name}
checked={record.name === (titleField || 'id')}
@ -136,7 +137,7 @@ const CurrentFields = (props) => {
return (
<RecordProvider record={record}>
<Space>
<EditCollectionField type="primary" />
<EditCollectionField role="button" aria-label={`edit-button-${record.name}`} type="primary" />
<Action.Link {...deleteProps} />
</Space>
</RecordProvider>
@ -153,6 +154,12 @@ const CurrentFields = (props) => {
dataSource={props.fields}
rowSelection={{
type: 'checkbox',
// @ts-ignore
getCheckboxProps(record) {
return {
'aria-label': `checkbox-${record.name}`,
};
},
onChange: (selectedRowKeys) => {
setState((state) => {
return {

View File

@ -191,6 +191,8 @@ export const ConfigurationTabs = () => {
return {
items: [
{
role: 'button',
'aria-label': 'edit-category',
key: 'edit',
label: (
<SchemaComponent
@ -209,6 +211,8 @@ export const ConfigurationTabs = () => {
),
},
{
role: 'button',
'aria-label': 'delete-category',
key: 'delete',
label: compile("{{t('Delete category')}}"),
onClick: () => remove(item.id),
@ -256,7 +260,8 @@ export const ConfigurationTabs = () => {
closable: item.closable,
closeIcon: (
<Dropdown menu={menu(item)}>
<MenuOutlined />
{/* 这里的样式是为了扩大图标的点击范围,以使其更容易 Playwright 录制工具中被点中 */}
<MenuOutlined role="button" aria-label={compile(item.name)} style={{ padding: 8, margin: '-8px' }} />
</Dropdown>
),
children: (

View File

@ -130,7 +130,7 @@ export const EditCollection = (props) => {
};
export const EditCollectionAction = (props) => {
const { scope, getContainer, item: record, children } = props;
const { scope, getContainer, item: record, children, ...otherProps } = props;
const { getTemplate } = useCollectionManager();
const [visible, setVisible] = useState(false);
const [schema, setSchema] = useState({});
@ -141,6 +141,7 @@ export const EditCollectionAction = (props) => {
<RecordProvider record={record}>
<ActionContextProvider value={{ visible, setVisible }}>
<a
{...otherProps}
onClick={async () => {
const templateConf = getTemplate(record.template);
const schema = getSchema(

View File

@ -148,7 +148,7 @@ export const EditCollectionField = (props) => {
};
export const EditFieldAction = (props) => {
const { scope, getContainer, item: record, children } = props;
const { scope, getContainer, item: record, children, ...otherProps } = props;
const { getInterface, collections } = useCollectionManager();
const [visible, setVisible] = useState(false);
const [schema, setSchema] = useState({});
@ -166,10 +166,12 @@ export const EditFieldAction = (props) => {
};
});
}, []);
return (
<RecordProvider record={record}>
<ActionContextProvider value={{ visible, setVisible }}>
<a
{...otherProps}
onClick={async () => {
const { data } = await api.resource('collections.fields', record.collectionName).get({
filterByTk: record.name,

View File

@ -17,7 +17,6 @@ export const SourceForeignKey = observer(
return (
<div>
<Select
data-testid="antd-select"
allowClear
placeholder={'留空时,自动生成 FK 字段'}
disabled={field.disabled}
@ -45,7 +44,6 @@ export const ThroughForeignKey = observer(
return (
<div>
<Select
data-testid="antd-select"
allowClear
popupMatchSelectWidth={false}
placeholder={'留空时,自动生成 FK 字段'}
@ -74,7 +72,6 @@ export const TargetForeignKey = observer(
return (
<div>
<Select
data-testid="antd-select"
allowClear
popupMatchSelectWidth={false}
placeholder={'留空时,自动生成 FK 字段'}
@ -104,7 +101,6 @@ export const SourceCollection = observer(
return (
<div>
<Select
data-testid="antd-select"
disabled
popupMatchSelectWidth={false}
value={collection.name}
@ -120,7 +116,7 @@ export const SourceKey = observer(
() => {
return (
<div>
<Select data-testid="antd-select" disabled value={'id'} options={[{ value: 'id', label: 'ID' }]} />
<Select disabled value={'id'} options={[{ value: 'id', label: 'ID' }]} />
</div>
);
},
@ -131,7 +127,7 @@ export const TargetKey = observer(
() => {
return (
<div>
<Select data-testid="antd-select" disabled value={'id'} options={[{ value: 'id', label: 'ID' }]} />
<Select disabled value={'id'} options={[{ value: 'id', label: 'ID' }]} />
</div>
);
},

View File

@ -218,6 +218,8 @@ export const collectionFieldSchema: ISchema = {
title: '{{ t("Edit") }}',
'x-component': 'EditCollectionField',
'x-component-props': {
role: 'button',
'aria-label': '{{ "edit-button-" + $record.name }}',
type: 'primary',
},
},

View File

@ -301,6 +301,8 @@ export const collectionTableSchema: ISchema = {
title: '{{ t("Edit") }}',
'x-component': 'EditCollection',
'x-component-props': {
role: 'button',
'aria-label': '{{ "edit-button-" + $record.name }}',
type: 'primary',
},
},

View File

@ -148,7 +148,6 @@ const PreviewCom = (props) => {
<Tag>{text}</Tag>
) : (
<Select
data-testid="antd-select"
defaultValue={text}
popupMatchSelectWidth={false}
style={{ width: '100%' }}
@ -174,7 +173,6 @@ const PreviewCom = (props) => {
text
) : (
<Select
data-testid="antd-select"
defaultValue={text}
style={{ width: '100%' }}
popupMatchSelectWidth={false}

View File

@ -17,6 +17,7 @@ const defaultTheme: ThemeConfig = {
colorBgSettingsHover: 'rgba(241, 139, 98, 0.06)',
colorBorderSettingsHover: 'rgba(241, 139, 98, 0.3)',
motionUnit: 0.03,
motion: !process.env.__E2E__,
},
};

View File

@ -27,6 +27,7 @@ function PluginInfo(props: IPluginInfo) {
const [showUploadForm, setShowUploadForm] = useState(false);
const [enabledVal, setEnabledVal] = useState(enabled);
const reload = () => window.location.reload();
const title = displayName || name || packageName;
return (
<>
@ -40,6 +41,8 @@ function PluginInfo(props: IPluginInfo) {
/>
)}
<Card
role="button"
aria-label={title}
size={'small'}
bordered={false}
onClick={() => {
@ -48,7 +51,7 @@ function PluginInfo(props: IPluginInfo) {
headStyle={{ border: 'none', minHeight: 'inherit', paddingTop: 14 }}
bodyStyle={{ paddingTop: 10 }}
// style={{ marginBottom: theme.marginLG }}
title={<div>{displayName || name || packageName}</div>}
title={<div>{title}</div>}
hoverable
className={css`
.ant-card-actions {
@ -127,6 +130,7 @@ function PluginInfo(props: IPluginInfo) {
)}
</Space>,
<Switch
aria-label="enable"
key={'enable'}
size={'small'}
disabled={builtIn || error}

View File

@ -121,6 +121,8 @@ const LocalPlugins = () => {
<Space size={theme.marginXXS} split={<Divider type="vertical" />}>
{filterList.map((item, index) => (
<a
role="button"
aria-label={item.type}
onClick={() => setFilterIndex(index)}
key={item.type}
style={{ fontWeight: filterIndex === index ? 'bold' : 'normal' }}

View File

@ -14,7 +14,7 @@ export const PluginManagerLink = () => {
return (
<Tooltip title={t('Plugin manager')}>
<Button
data-testid={'pm-button'}
data-testid={'plugin-manager-button'}
icon={<ApiOutlined />}
title={t('Plugin manager')}
onClick={() => {
@ -48,11 +48,13 @@ export const SettingsCenterDropdown = () => {
return {
items: [
...bookmarkTabs.map((tab) => ({
role: 'button',
key: `/admin/settings/${tab.path}`,
label: compile(tab.title),
})),
{ type: 'divider' },
{
role: 'button',
key: '/admin/settings',
label: t('All plugin settings'),
},

View File

@ -5,6 +5,7 @@ import React from 'react';
import { OpenSize } from './';
import { useStyles } from './Action.Drawer.style';
import { useActionContext } from './hooks';
import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer';
import { ComposedActionDrawer } from './types';
const openSizeWidthMap = new Map<OpenSize, string>([
@ -26,9 +27,12 @@ export const ActionDrawer: ComposedActionDrawer = observer(
return buf;
});
if (process.env.__E2E__) {
useSetAriaLabelForDrawer(visible);
}
return (
<Drawer
data-testid="action-drawer"
width={openSizeWidthMap.get(openSize)}
title={field.title}
{...others}

View File

@ -4,6 +4,7 @@ import { Modal, ModalProps } from 'antd';
import classNames from 'classnames';
import React from 'react';
import { OpenSize, useActionContext } from '.';
import { useSetAriaLabelForModal } from './hooks/useSetAriaLabelForModal';
import { ComposedActionDrawer } from './types';
const openSizeWidthMap = new Map<OpenSize, string>([
@ -24,6 +25,11 @@ export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
}
return buf;
});
if (process.env.__E2E__) {
useSetAriaLabelForModal(visible);
}
return (
<Modal
data-testid="action-modal"

View File

@ -2,7 +2,7 @@ import { observer, RecursionField, useField, useFieldSchema, useForm } from '@fo
import { isPortalInBody } from '@nocobase/utils/client';
import { App, Button, Popover } from 'antd';
import classnames from 'classnames';
import lodash from 'lodash';
import { default as lodash } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useActionContext } from '../..';
@ -22,6 +22,7 @@ import { ActionPage } from './Action.Page';
import useStyles from './Action.style';
import { ActionContextProvider } from './context';
import { useA } from './hooks';
import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
import { ComposedAction } from './types';
import { linkageAction } from './utils';
@ -62,14 +63,16 @@ export const Action: ComposedAction = observer(
const { modal } = App.useApp();
const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: { values: record } as any });
const { getAriaLabel } = useGetAriaLabelOfAction(title);
let actionTitle = title || compile(fieldSchema.title);
actionTitle = lodash.isString(actionTitle) ? t(actionTitle) : actionTitle;
// fix https://nocobase.height.app/T-2259
const shouldResetRecord = ['create', 'customize:bulkUpdate', 'customize:bulkEdit', 'customize:create'].includes(
fieldSchema['x-action'],
);
let actionTitle = title || compile(fieldSchema.title);
actionTitle = lodash.isString(actionTitle) ? t(actionTitle) : actionTitle;
useEffect(() => {
field.linkageProperty = {};
linkageRules
@ -130,7 +133,8 @@ export const Action: ComposedAction = observer(
return (
<SortableItem
data-testid={`${fieldSchema['x-action'] || fieldSchema.name}-action`}
role="button"
aria-label={getAriaLabel()}
{...others}
loading={field?.data?.loading}
icon={icon ? <Icon type={icon} /> : null}

View File

@ -61,7 +61,7 @@ function TypeSelect(props) {
});
return (
<Select data-testid="antd-select" {...props}>
<Select {...props}>
{Object.keys(types).map((key) => (
<Select.Option key={key} value={key}>
{types[key].title}

View File

@ -0,0 +1,40 @@
import { useFieldSchema } from '@formily/react';
import { useCallback } from 'react';
import { useBlockContext } from '../../../../block-provider/BlockProvider';
import { useCollection } from '../../../../collection-manager';
import { useRecord, useRecordIndex } from '../../../../record-provider';
import { useCompile } from '../../../hooks';
/**
* label = 'action' + x-component + actionTitle + [x-action] + [collectionName] + [blockName] + [record.name || record.title || recordIndex] + [postfix]
* @param title
* @returns
*/
export const useGetAriaLabelOfAction = (title: string) => {
const record = useRecord();
const recordIndex = useRecordIndex();
const fieldSchema = useFieldSchema();
const compile = useCompile();
const component = fieldSchema['x-component'];
let recordName = record?.name || record?.title || (recordIndex != null ? String(recordIndex) : '');
let action = fieldSchema['x-action'];
let { name: collectionName } = useCollection();
let { name: blockName } = useBlockContext() || {};
const actionTitle = title || compile(fieldSchema.title);
collectionName = collectionName ? `-${collectionName}` : '';
blockName = blockName ? `-${blockName}` : '';
action = action ? `-${action}` : '';
recordName = recordName ? `-${recordName}` : '';
const getAriaLabel = useCallback(
(postfix?: string) => {
postfix = postfix ? `-${postfix}` : '';
return `action-${component}-${actionTitle}${action}${collectionName}${blockName}${recordName}${postfix}`;
},
[action, actionTitle, blockName, collectionName, component, recordName],
);
return {
getAriaLabel,
};
};

View File

@ -0,0 +1,28 @@
import { useFieldSchema } from '@formily/react';
import { useCallback } from 'react';
import { useCollection } from '../../../../collection-manager';
import { useCompile } from '../../../hooks';
/**
* label = 'drawer' + x-component + [collectionName] + [title] + [postfix]
* @returns
*/
export const useGetAriaLabelOfDrawer = () => {
const fieldSchema = useFieldSchema();
const component = fieldSchema['x-component'];
const compile = useCompile();
let { name: collectionName } = useCollection();
let title = compile(fieldSchema.title);
collectionName = collectionName ? `-${collectionName}` : '';
title = title ? `-${title}` : '';
const getAriaLabel = useCallback(
(postfix?: string) => {
postfix = postfix ? `-${postfix}` : '';
return `drawer-${component}${collectionName}${title}${postfix}`;
},
[collectionName, component, title],
);
return { getAriaLabel };
};

View File

@ -0,0 +1,28 @@
import { useFieldSchema } from '@formily/react';
import { useCallback } from 'react';
import { useCollection } from '../../../../collection-manager';
import { useCompile } from '../../../hooks';
/**
* label = 'modal' + x-component + [collectionName] + [title] + [postfix]
* @returns
*/
export const useGetAriaLabelOfModal = () => {
const fieldSchema = useFieldSchema();
const component = fieldSchema['x-component'];
const compile = useCompile();
let { name: collectionName } = useCollection();
let title = compile(fieldSchema.title);
collectionName = collectionName ? `-${collectionName}` : '';
title = title ? `-${title}` : '';
const getAriaLabel = useCallback(
(postfix?: string) => {
postfix = postfix ? `-${postfix}` : '';
return `modal-${component}${collectionName}${title}${postfix}`;
},
[collectionName, component, title],
);
return { getAriaLabel };
};

View File

@ -0,0 +1,28 @@
import { useFieldSchema } from '@formily/react';
import { useCallback } from 'react';
import { useCollection } from '../../../../collection-manager';
import { useCompile } from '../../../hooks';
/**
* label = 'popover' + x-component + [collectionName] + [title] + [postfix]
* @returns
*/
export const useGetAriaLabelOfPopover = () => {
const fieldSchema = useFieldSchema();
const component = fieldSchema['x-component'];
const compile = useCompile();
let { name: collectionName } = useCollection();
let title = compile(fieldSchema.title);
collectionName = collectionName ? `-${collectionName}` : '';
title = title ? `-${title}` : '';
const getAriaLabel = useCallback(
(postfix?: string) => {
postfix = postfix ? `-${postfix}` : '';
return `popover-${component}${collectionName}${title}${postfix}`;
},
[collectionName, component, title],
);
return { getAriaLabel };
};

View File

@ -0,0 +1,27 @@
import { useEffect } from 'react';
import { useGetAriaLabelOfDrawer } from './useGetAriaLabelOfDrawer';
export function useSetAriaLabelForDrawer(visible: boolean) {
const { getAriaLabel } = useGetAriaLabelOfDrawer();
// 因 Drawer 设置 aria-label 无效,所以使用下面这种方式设置,方便 e2e 录制工具选中
useEffect(() => {
if (visible) {
const wrappers = [...document.querySelectorAll('.ant-drawer-wrapper-body')];
const masks = [...document.querySelectorAll('.ant-drawer-mask')];
// 如果存在多个 mask最后一个 mask 为当前打开的 maskwrapper 也是同理
const currentMask = masks[masks.length - 1];
const currentWrapper = wrappers[wrappers.length - 1];
if (currentMask) {
currentMask.setAttribute('role', 'button');
currentMask.setAttribute('aria-label', getAriaLabel('mask'));
}
if (currentWrapper) {
currentWrapper.setAttribute('role', 'button');
// 都设置一下,让 e2e 录制工具自己选择
currentWrapper.setAttribute('data-testid', getAriaLabel());
currentWrapper.setAttribute('aria-label', getAriaLabel());
}
}
}, [getAriaLabel, visible]);
}

View File

@ -0,0 +1,29 @@
import { useEffect } from 'react';
import { useGetAriaLabelOfModal } from './useGetAriaLabelOfModal';
export function useSetAriaLabelForModal(visible: boolean) {
const { getAriaLabel } = useGetAriaLabelOfModal();
// 因 Drawer 设置 aria-label 无效,所以使用下面这种方式设置,方便 e2e 录制工具选中
useEffect(() => {
if (visible) {
setTimeout(() => {
const wrappers = [...document.querySelectorAll('.ant-modal-content')];
const masks = [...document.querySelectorAll('.ant-modal-wrap')];
// 如果存在多个 mask最后一个 mask 为当前打开的 maskwrapper 也是同理
const currentMask = masks[masks.length - 1];
const currentWrapper = wrappers[wrappers.length - 1];
if (currentMask) {
currentMask.setAttribute('role', 'button');
currentMask.setAttribute('aria-label', getAriaLabel('mask'));
}
if (currentWrapper) {
currentWrapper.setAttribute('role', 'button');
// 都设置一下,让 e2e 录制工具自己选择
currentWrapper.setAttribute('data-testid', getAriaLabel());
currentWrapper.setAttribute('aria-label', getAriaLabel());
}
});
}
}, [getAriaLabel, visible]);
}

View File

@ -0,0 +1,20 @@
import { useEffect } from 'react';
import { useGetAriaLabelOfPopover } from './useGetAriaLabelOfPopover';
export function useSetAriaLabelForPopover(visible: boolean) {
const { getAriaLabel } = useGetAriaLabelOfPopover();
// 因 Popover 设置 aria-label 无效,所以使用下面这种方式设置,方便 e2e 录制工具选中
useEffect(() => {
if (visible) {
const wrappers = [...document.querySelectorAll('.ant-popover-content')];
const currentWrapper = wrappers[wrappers.length - 1];
if (currentWrapper) {
currentWrapper.setAttribute('role', 'button');
// 都设置一下,让 e2e 录制工具自己选择
currentWrapper.setAttribute('data-testid', getAriaLabel());
currentWrapper.setAttribute('aria-label', getAriaLabel());
}
}
}, [getAriaLabel, visible]);
}

View File

@ -2,3 +2,7 @@ export * from './Action';
export * from './ActionBar';
export * from './context';
export * from './hooks';
export * from './hooks/useGetAriaLabelOfAction';
export * from './hooks/useGetAriaLabelOfDrawer';
export * from './hooks/useGetAriaLabelOfModal';
export * from './hooks/useGetAriaLabelOfPopover';

View File

@ -1,3 +1,4 @@
import { CloseCircleFilled } from '@ant-design/icons';
import { Tag, TreeSelect } from 'antd';
import type { DefaultOptionType } from 'rc-tree-select/es/TreeSelect';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
@ -7,6 +8,7 @@ import { CollectionFieldOptions, useCollectionManager, useCompile } from '../../
export type AppendsTreeSelectProps = {
value: string[] | string;
onChange: (value: string[] | string) => void;
title?: string;
multiple?: boolean;
filter?(field): boolean;
collection?: string;
@ -72,6 +74,7 @@ function getCollectionFieldOptions(this: CallScope, collection, parentNode?): Tr
export const AppendsTreeSelect: React.FC<AppendsTreeSelectProps> = (props) => {
const {
title,
value: propsValue,
onChange,
collection,
@ -202,7 +205,7 @@ export const AppendsTreeSelect: React.FC<AppendsTreeSelectProps> = (props) => {
}
const { fullTitle } = optionsMap[value] ?? {};
return (
<Tag data-testid="antd-tag" closable={closable && !disabled} onClose={onClose}>
<Tag closable={closable && !disabled} onClose={onClose}>
{fullTitle?.join(' / ')}
</Tag>
);
@ -219,12 +222,16 @@ export const AppendsTreeSelect: React.FC<AppendsTreeSelectProps> = (props) => {
return (
<TreeSelect
data-testid="antd-tree-select"
// @ts-ignore
role="button"
data-testid={`select-field${title ? `-${title}` : ''}`}
value={filteredValue}
placeholder={t('Select field')}
showCheckedStrategy={TreeSelect.SHOW_ALL}
treeDefaultExpandedKeys={valueKeys}
allowClear
allowClear={{
clearIcon: <CloseCircleFilled role="button" aria-label="icon-close" />,
}}
treeCheckStrictly={props.multiple}
treeCheckable={props.multiple}
tagRender={TreeTag}

View File

@ -132,7 +132,6 @@ export const InternalPicker = observer(
<Input.Group compact style={{ display: 'flex', lineHeight: '32px' }}>
<div style={{ width: '100%' }}>
<Select
data-testid="antd-select"
style={{ width: '100%' }}
popupMatchSelectWidth={false}
{...others}

View File

@ -4,6 +4,8 @@ import { observer } from '@formily/react';
import React, { useContext, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ActionContext, ActionContextProvider } from '../action/context';
import { useGetAriaLabelOfPopover } from '../action/hooks/useGetAriaLabelOfPopover';
import { useSetAriaLabelForPopover } from '../action/hooks/useSetAriaLabelForPopover';
import { PopoverWithStopPropagation } from '../popover';
import { InternalNester } from './InternalNester';
import { ReadPrettyInternalViewer } from './InternalViewer';
@ -44,6 +46,12 @@ export const InternaPopoverNester = observer(
const modalProps = {
getContainer: getContainer,
};
const { getAriaLabel } = useGetAriaLabelOfPopover();
if (process.env.__E2E__) {
useSetAriaLabelForPopover(visible);
}
return (
<ActionContextProvider value={{ ...ctx, modalProps }}>
<PopoverWithStopPropagation
@ -68,6 +76,8 @@ export const InternaPopoverNester = observer(
</PopoverWithStopPropagation>
{visible && (
<div
role="button"
aria-label={getAriaLabel('mask')}
onClick={() => setVisible(false)}
className={css`
position: fixed;

View File

@ -79,7 +79,6 @@ export const AssociationFilterFilterBlockInitializer = () => {
return (
<SchemaInitializer.Button
data-testid="configure-fields-button-of-association-filter-filter-block"
className={css`
margin-top: 16px;
`}

View File

@ -49,7 +49,6 @@ export const AssociationFilterInitializer = () => {
return (
<SchemaInitializer.Button
data-testid="configure-fields-button-of-association-filter"
className={css`
margin-top: 16px;
`}

View File

@ -1,29 +1,19 @@
import { css } from '@emotion/css';
import { useFieldSchema } from '@formily/react';
import cls from 'classnames';
import React from 'react';
import { SortableItem } from '../../common';
import { useDesigner, useProps } from '../../hooks';
const getTestId = (schema) => {
const resource = schema?.['x-decorator-props']?.['resource'];
if (resource) {
return `${resource}-resource`;
}
const field = schema['x-collection-field'];
if (field) {
return `${field}-field`;
}
return `${schema.name}-item`;
};
import { useGetAriaLabelOfBlockItem } from './hooks/useGetAriaLabelOfBlockItem';
export const BlockItem: React.FC<any> = (props) => {
const { className, children } = useProps(props);
const Designer = useDesigner();
const schema = useFieldSchema();
const { getAriaLabel } = useGetAriaLabelOfBlockItem(props.name);
return (
<SortableItem
data-testid={getTestId(schema)}
role="button"
aria-label={getAriaLabel()}
className={cls(
'nb-block-item',
className,

View File

@ -0,0 +1,36 @@
import { useFieldSchema } from '@formily/react';
import { useCallback } from 'react';
import { useBlockContext } from '../../../../block-provider/BlockProvider';
import { useCollection } from '../../../../collection-manager';
import { useCompile } from '../../../hooks';
/**
* label = 'block-item' + x-component + [collectionName] + [blockName] + [x-collection-field] + [title] + [postfix]
* @returns
*/
export const useGetAriaLabelOfBlockItem = (name?: string) => {
const fieldSchema = useFieldSchema();
const compile = useCompile();
const component = fieldSchema['x-component'];
const collectionField = compile(fieldSchema['x-collection-field']);
let { name: blockName } = useBlockContext() || {};
// eslint-disable-next-line prefer-const
let { name: collectionName, getField } = useCollection();
blockName = name || blockName;
const title = compile(fieldSchema['title']) || compile(getField(fieldSchema.name)?.uiSchema?.title);
const getAriaLabel = useCallback(
(postfix?: string) => {
postfix = postfix ? `-${postfix}` : '';
return ['block-item', component, collectionName, blockName, collectionField, title, postfix]
.filter(Boolean)
.join('-');
},
[component, collectionName, blockName, collectionField, title],
);
return {
getAriaLabel,
};
};

View File

@ -15,7 +15,7 @@ export const ViewSelect = observer(
} = useContext(CalendarToolbarContext);
return (
<div className="ant-btn-group">
<Select data-testid="antd-select" popupMatchSelectWidth={false} value={view} onChange={onView}>
<Select popupMatchSelectWidth={false} value={view} onChange={onView}>
{views.map((name) => (
<Select.Option key={name} value={name}>
{messages[name]}

View File

@ -5,8 +5,15 @@ import { useSchemaTemplate } from '../../../schema-templates';
import { BlockItem } from '../block-item';
import useStyles from './style';
export const CardItem: React.FC = (props) => {
const { children, ...restProps } = props;
interface Props {
children?: React.ReactNode;
/** 区块标识 */
name?: string;
[key: string]: any;
}
export const CardItem = (props: Props) => {
const { children, name, ...restProps } = props;
const template = useSchemaTemplate();
const fieldSchema = useFieldSchema();
const templateKey = fieldSchema['x-template-key'];
@ -14,7 +21,7 @@ export const CardItem: React.FC = (props) => {
return wrapSSR(
templateKey && !template ? null : (
<BlockItem className={`${componentCls} ${hashId} noco-card-item`}>
<BlockItem name={name} className={`${componentCls} ${hashId} noco-card-item`}>
<Card className="card" bordered={false} {...restProps}>
{props.children}
</Card>

View File

@ -45,7 +45,9 @@ export const CollectionSelect = connect(
const { t } = useTranslation();
return (
<Select
data-testid="antd-select"
// @ts-ignore
role="button"
data-testid="select-collection"
placeholder={t('Select collection')}
popupMatchSelectWidth={false}
{...others}

View File

@ -23,7 +23,7 @@ export const ColorSelect = connect(
(props) => {
const compile = useCompile();
return (
<Select data-testid="antd-select" {...props}>
<Select {...props}>
{Object.keys(colors).map((color) => (
<Select.Option value={color}>
<Tag color={color}>{compile(colors[color] || colors.default)}</Tag>

View File

@ -64,7 +64,6 @@ const CronSetInternal = (props: CronSetProps) => {
return (
<fieldset>
<Select
data-testid="antd-select"
popupMatchSelectWidth={false}
allowClear
{...props}

View File

@ -63,16 +63,14 @@ export const DynamicComponent = (props: Props) => {
}, [props.schema]);
return (
<FormContext.Provider value={form}>
<div data-testid="dynamic-component-filter-item">
{component
? React.createElement<DynamicComponentProps>(component, {
value: props.value,
collectionField: props.collectionField,
onChange: props?.onChange,
renderSchemaComponent,
})
: renderSchemaComponent()}
</div>
{component
? React.createElement<DynamicComponentProps>(component, {
value: props.value,
collectionField: props.collectionField,
onChange: props?.onChange,
renderSchemaComponent,
})
: renderSchemaComponent()}
</FormContext.Provider>
);
};

View File

@ -42,7 +42,7 @@ export const FilterGroup = connect((props) => {
}
>
{remove && !mergedDisabled && (
<a data-testid="close-icon-button">
<a role="button" aria-label="icon-close">
<CloseCircleOutlined
style={{
position: 'absolute',
@ -58,7 +58,9 @@ export const FilterGroup = connect((props) => {
<Trans>
{'Meet '}
<Select
data-testid="antd-select"
// @ts-ignore
role="button"
data-testid="filter-select-all-or-any"
style={{ width: 'auto' }}
value={logic}
onChange={(value) => {

View File

@ -55,7 +55,9 @@ export const FilterItem = observer(
<div style={style} className="nc-filter-item">
<Space>
<Cascader
data-testid="antd-cascader"
// @ts-ignore
role="button"
data-testid="filter-select-field"
className={css`
width: 160px;
`}
@ -67,7 +69,9 @@ export const FilterItem = observer(
placeholder={t('Select field')}
/>
<Select
data-testid="antd-select"
// @ts-ignore
role="button"
data-testid="filter-select-operator"
className={css`
min-width: 110px;
`}
@ -81,7 +85,7 @@ export const FilterItem = observer(
<DynamicComponent value={value} schema={schema} collectionField={collectionField} onChange={setValue} />
) : null}
{!props.disabled && (
<a data-testid="close-icon-button">
<a role="button" aria-label="icon-close">
<CloseCircleOutlined onClick={remove} style={removeStyle} />
</a>
)}

View File

@ -391,11 +391,10 @@ FormItem.Designer = function Designer() {
/>
)}
{showModeSelect && (
<SchemaSettings.Item>
<SchemaSettings.Item title="Popup size">
<div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}>
{t('Popup size')}
<Select
data-testid="antd-select"
bordered={false}
options={[
{ label: t('Small'), value: 'small' },

View File

@ -157,7 +157,6 @@ export const Templates = ({ style = {}, form }) => {
<Space wrap>
<label style={labelStyle}>{t('Data template')}: </label>
<Select
data-testid="antd-select"
popupMatchSelectWidth={false}
options={templateOptions}
fieldNames={{ label: 'title', value: 'key' }}

View File

@ -57,7 +57,7 @@ export const GridCardBlockProvider = (props) => {
}, [parsedFilter, params]);
return (
<BlockProvider data-testid="grid-card-block" {...props} params={paramsWithFilter}>
<BlockProvider name="grid-card" {...props} params={paramsWithFilter}>
<InternalGridCardBlockProvider {...props} />
</BlockProvider>
);

View File

@ -29,6 +29,8 @@ export const Input: ComposedInput = Object.assign(
AntdInput.TextArea,
mapProps((props, field) => {
return {
role: 'button',
'aria-label': 'textarea',
autoSize: {
maxRows: 10,
minRows: 3,

View File

@ -1,6 +1,6 @@
import { MenuOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import { ISchema, useField, useFieldSchema } from '@formily/react';
import { ISchema, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared';
import { Space } from 'antd';
import React from 'react';
@ -13,6 +13,7 @@ import {
useFormItemInitializerFields,
} from '../../../schema-initializer/utils';
import { OpenModeSchemaItems } from '../../../schema-items';
import { useGetAriaLabelOfDesigner } from '../../../schema-settings/hooks/useGetAriaLabelOfDesigner';
const titleCss = css`
pointer-events: none;
@ -56,17 +57,17 @@ const gridRowColWrap = (schema: ISchema) => {
// };
export const KanbanCardDesigner = (props: any) => {
const { dn, designable } = useDesignable();
const { designable } = useDesignable();
const { t } = useTranslation();
const api = useAPIClient();
const { refresh } = useDesignable();
const field = useField();
const fieldSchema = useFieldSchema();
const fields = useFormItemInitializerFields({
readPretty: true,
block: 'Kanban',
});
const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Kanban' });
const { getAriaLabel } = useGetAriaLabelOfDesigner();
const items: any = [
{
@ -133,7 +134,13 @@ export const KanbanCardDesigner = (props: any) => {
dn.insertBeforeEnd(schema);
}}
items={items}
component={<MenuOutlined style={{ cursor: 'pointer', fontSize: 12 }} />}
component={
<MenuOutlined
role="button"
aria-label={getAriaLabel('schema-initializer')}
style={{ cursor: 'pointer', fontSize: 12 }}
/>
}
/>
</Space>
</div>

View File

@ -67,7 +67,7 @@ export const ListBlockProvider = (props) => {
}, [parsedFilter, params]);
return (
<BlockProvider data-testid="list-block" {...props} params={paramsWithFilter}>
<BlockProvider name="list" {...props} params={paramsWithFilter}>
<InternalListBlockProvider {...props} />
</BlockProvider>
);

View File

@ -195,10 +195,14 @@ const HeaderMenu = ({
const items = useMemo(() => {
const designerBtn = {
key: 'x-designer-button',
disabled: true,
style: { padding: '0 8px', order: 9999 },
label: render({ 'data-testid': 'add-menu-item-button-in-header', style: { background: 'none' } }),
label: render({
'aria-label': 'schema-initializer-Menu-header',
'aria-disabled': false,
style: { background: 'none' },
}),
notdelete: true,
disabled: true,
};
const result = getMenuItems(() => {
return children;
@ -217,7 +221,12 @@ const HeaderMenu = ({
{...others}
className={headerMenuClass}
onSelect={(info: any) => {
const s = schema.properties[info.key];
const s = schema.properties?.[info.key];
if (!s) {
return;
}
if (mode === 'mix') {
if (s['x-component'] !== 'Menu.SubMenu') {
onSelect?.(info);
@ -282,7 +291,8 @@ const SideMenu = ({
key: 'x-designer-button',
disabled: true,
label: render({
'data-testid': 'add-menu-item-button-in-side',
'aria-label': 'schema-initializer-Menu-side',
'aria-disabled': false,
insert: (s) => {
const dn = createDesignable({
t,
@ -481,7 +491,12 @@ Menu.Item = observer(
label: (
<SchemaContext.Provider value={schema}>
<FieldContext.Provider value={field}>
<SortableItem className={designerCss} removeParentsIfNoChildren={false}>
<SortableItem
role="button"
aria-label={t(field.title)}
className={designerCss}
removeParentsIfNoChildren={false}
>
<Icon type={icon} />
<span
style={{

View File

@ -11,6 +11,7 @@ export const MenuItemInitializers = (props: any) => {
const { t } = useTranslation();
return (
<SchemaInitializer.Button
role="button"
insertPosition={'beforeEnd'}
icon={'PlusOutlined'}
insert={props.insert}

View File

@ -9,11 +9,13 @@ import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom';
import { FormDialog } from '..';
import { useStyles as useAClStyles } from '../../../acl/style';
import { useAppSpin } from '../../../application/hooks/useAppSpin';
import { useDocumentTitle } from '../../../document-title';
import { FilterBlockProvider } from '../../../filter-provider/FilterProvider';
import { useGlobalTheme } from '../../../global-theme';
import { Icon } from '../../../icon';
import { useGetAriaLabelOfSchemaInitializer } from '../../../schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer';
import { DndContext } from '../../common';
import { SortableItem } from '../../common/sortable-item';
import { SchemaComponent, SchemaComponentOptions } from '../../core';
@ -23,7 +25,6 @@ import { ErrorFallback } from '../error-fallback';
import FixedBlock from './FixedBlock';
import { PageDesigner, PageTabDesigner } from './PageTabDesigner';
import { useStyles } from './style';
import { useStyles as useAClStyles } from '../../../acl/style';
export const Page = (props) => {
const { children, ...others } = props;
@ -33,6 +34,7 @@ export const Page = (props) => {
const fieldSchema = useFieldSchema();
const dn = useDesignable();
const { theme } = useGlobalTheme();
const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer();
// react18 tab 动画会卡顿,所以第一个 tab 时,动画禁用,后面的 tab 才启用
const [hasMounted, setHasMounted] = useState(false);
@ -99,6 +101,7 @@ export const Page = (props) => {
tabBarExtraContent={
dn.designable && (
<Button
aria-label={getAriaLabel('tabs')}
icon={<PlusOutlined />}
className={'addTabBtn'}
type={'dashed'}

View File

@ -5,12 +5,14 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { DragHandler, useDesignable } from '../..';
import { SchemaSettings } from '../../../schema-settings';
import { useGetAriaLabelOfDesigner } from '../../../schema-settings/hooks/useGetAriaLabelOfDesigner';
export const PageDesigner = ({ title }) => {
const { dn, designable } = useDesignable();
const { t } = useTranslation();
const field = useField();
const fieldSchema = useFieldSchema();
const { getAriaLabel } = useGetAriaLabelOfDesigner();
const hidePageTitle = fieldSchema['x-component-props']?.hidePageTitle;
const disablePageHeader = fieldSchema['x-component-props']?.disablePageHeader;
if (!designable) {
@ -21,8 +23,13 @@ export const PageDesigner = ({ title }) => {
<div className={'general-schema-designer-icons'}>
<Space size={2} align={'center'}>
<SchemaSettings
data-testid="page-designer-button"
title={<MenuOutlined style={{ cursor: 'pointer', fontSize: 12 }} />}
title={
<MenuOutlined
role="button"
aria-label={getAriaLabel('schema-settings')}
style={{ cursor: 'pointer', fontSize: 12 }}
/>
}
>
<SchemaSettings.SwitchItem
title={t('Enable page header')}
@ -117,6 +124,7 @@ export const PageTabDesigner = ({ schema }) => {
const { dn, designable } = useDesignable();
const { t } = useTranslation();
const { modal } = App.useApp();
const { getAriaLabel } = useGetAriaLabelOfDesigner();
if (!designable) {
return null;
@ -127,9 +135,9 @@ export const PageTabDesigner = ({ schema }) => {
<div className={'general-schema-designer-icons'}>
<Space size={2} align={'center'}>
<DragHandler>
<DragOutlined />
<DragOutlined style={{ marginRight: 0 }} role="button" aria-label={getAriaLabel('drag-handler', 'tab')} />
</DragHandler>
<SchemaSettings title={<MenuOutlined style={{ cursor: 'pointer', fontSize: 12 }} />}>
<SchemaSettings title={<MenuOutlined role="button" aria-label={getAriaLabel('schema-settings', 'tab')} />}>
<SchemaSettings.ModalItem
title={t('Edit')}
schema={
@ -169,6 +177,7 @@ export const PageTabDesigner = ({ schema }) => {
/>
<SchemaSettings.Divider />
<SchemaSettings.Item
title="Delete"
eventKey="remove"
onClick={() => {
modal.confirm({

View File

@ -175,7 +175,6 @@ export const InputRecordPicker: React.FC<any> = (props: IRecordPickerProps) => {
/>
) : (
<Select
data-testid="antd-select"
{...others}
mode={multiple ? 'multiple' : props.mode}
fieldNames={fieldNames}

View File

@ -76,19 +76,27 @@ const InternalRemoteSelect = connect(
const option = targetField.uiSchema.enum.find((i) => i.value === item);
if (option) {
return (
<Tag key={index} color={option.color} style={{ marginRight: 3 }}>
<Tag role="button" key={index} color={option.color} style={{ marginRight: 3 }}>
{option?.label || item}
</Tag>
);
} else {
return <Tag key={item}>{item}</Tag>;
return (
<Tag role="button" key={item}>
{item}
</Tag>
);
}
})
.reverse();
} else {
const item = targetField.uiSchema.enum.find((i) => i.value === label);
if (item) {
label = <Tag color={item.color}>{item.label}</Tag>;
label = (
<Tag role="button" color={item.color}>
{item.label}
</Tag>
);
}
}
}

View File

@ -1,9 +1,9 @@
import { LoadingOutlined } from '@ant-design/icons';
import { CloseCircleFilled, CloseOutlined, LoadingOutlined } from '@ant-design/icons';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { isValid, toArr } from '@formily/shared';
import { isPlainObject } from '@nocobase/utils/client';
import type { SelectProps } from 'antd';
import { Select as AntdSelect, Empty, Spin } from 'antd';
import { Select as AntdSelect, Empty, Spin, Tag } from 'antd';
import React from 'react';
import { ReadPretty } from './ReadPretty';
import { FieldNames, defaultFieldNames, getCurrentOptions } from './utils';
@ -43,10 +43,14 @@ const ObjectSelect = (props: Props) => {
return (
<AntdSelect
data-testid="antd-select"
// @ts-ignore
role="button"
data-testid={`select-object-${mode}`}
value={toValue(value)}
defaultValue={toValue(defaultValue)}
allowClear
allowClear={{
clearIcon: <CloseCircleFilled role="button" aria-label="icon-close-select" />,
}}
labelInValue
notFoundContent={loading ? <Spin /> : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
options={options}
@ -72,6 +76,19 @@ const ObjectSelect = (props: Props) => {
}
}}
mode={mode}
tagRender={(props) => {
return (
// @ts-ignore
<Tag
role="button"
aria-label={props.label}
closeIcon={<CloseOutlined role="button" aria-label="icon-close-tag" />}
{...props}
>
{props.label}
</Tag>
);
}}
{...others}
/>
);
@ -109,14 +126,31 @@ const InternalSelect = connect(
};
return (
<AntdSelect
data-testid="antd-select"
// @ts-ignore
role="button"
data-testid={`select-${mode || 'single'}`}
showSearch
filterOption={filterOption}
allowClear
allowClear={{
clearIcon: <CloseCircleFilled role="button" aria-label="icon-close-select" />,
}}
popupMatchSelectWidth={false}
notFoundContent={loading ? <Spin /> : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
value={toValue(value)}
defaultValue={toValue(defaultValue)}
tagRender={(props) => {
return (
// @ts-ignore
<Tag
role="button"
aria-label={props.label}
closeIcon={<CloseOutlined role="button" aria-label="icon-close-tag" />}
{...props}
>
{props.label}
</Tag>
);
}}
{...others}
onChange={(changed) => {
props.onChange?.(changed === undefined ? null : changed);

View File

@ -27,7 +27,8 @@ describe('Select', () => {
await userEvent.click(screen.getByText('福州'));
await userEvent.click(selector);
fireEvent.click(screen.getByText('浙江'));
expect(container.querySelectorAll('.ant-tag')).toHaveLength(2);
expect(screen.getAllByText('福州')).toHaveLength(3);
expect(screen.getAllByText('浙江')).toHaveLength(3);
});
it('select one & objectValue: true', async () => {

View File

@ -4,16 +4,18 @@ import { Space } from 'antd';
import React from 'react';
import { DragHandler } from '../../../schema-component';
import { useSchemaInitializer } from '../../../schema-initializer';
import { useGetAriaLabelOfDesigner } from '../../../schema-settings/hooks/useGetAriaLabelOfDesigner';
export const TableActionColumnDesigner = (props: any) => {
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
const { getAriaLabel } = useGetAriaLabelOfDesigner();
return (
<div className={'general-schema-designer'}>
<div className={'general-schema-designer-icons'}>
<Space size={2} align={'center'}>
<DragHandler>
<DragOutlined />
<DragOutlined role="button" aria-label={getAriaLabel('drag-handler')} />
</DragHandler>
{render()}
</Space>

View File

@ -1,6 +1,6 @@
import { useField, useFieldSchema } from '@formily/react';
import React, { useLayoutEffect } from 'react';
import { SortableItem, useCollection, useCollectionManager, useCompile, useDesignable, useDesigner } from '../../../';
import { SortableItem, useCollection, useCollectionManager, useCompile, useDesigner } from '../../../';
import { designerCss } from './Table.Column.ActionBar';
import { isCollectionFieldComponent } from './utils';
@ -27,7 +27,6 @@ export const TableColumnDecorator = (props) => {
const Designer = useDesigner();
const field = useField();
const { fieldSchema, uiSchema, collectionField } = useColumnSchema();
const { refresh } = useDesignable();
const compile = useCompile();
useLayoutEffect(() => {
if (field.title) {
@ -44,7 +43,7 @@ export const TableColumnDecorator = (props) => {
<SortableItem className={designerCss}>
<Designer fieldSchema={fieldSchema} uiSchema={uiSchema} collectionField={collectionField} />
{/* <RecursionField name={columnSchema.name} schema={columnSchema}/> */}
{field?.title || compile(uiSchema?.title)}
<div role="button">{field?.title || compile(uiSchema?.title)}</div>
{/* <div
onClick={() => {
field.title = uid();

View File

@ -3,5 +3,5 @@ import React from 'react';
export const TableColumn = (props) => {
const field = useField();
return <div>{field.title}</div>;
return <div role="button">{field.title}</div>;
};

View File

@ -66,11 +66,13 @@ const useTableColumns = (props: { showDel?: boolean; isSubTable?: boolean }) =>
<RecordIndexProvider index={record.__index || index}>
<RecordProvider record={record}>
<ColumnFieldProvider schema={s} basePath={field.address.concat(record.__index || index)}>
<RecursionField
basePath={field.address.concat(record.__index || index)}
schema={s}
onlyRenderProperties
/>
<span role="button">
<RecursionField
basePath={field.address.concat(record.__index || index)}
schema={s}
onlyRenderProperties
/>
</span>
</ColumnFieldProvider>
</RecordProvider>
</RecordIndexProvider>
@ -158,16 +160,17 @@ const SortableRow = (props) => {
};
const SortHandle = (props) => {
const { id, ...otherProps } = props;
const { listeners } = useSortable({
id: props.id,
id,
});
return <MenuOutlined {...listeners} style={{ cursor: 'grab' }} />;
return <MenuOutlined {...otherProps} {...listeners} style={{ cursor: 'grab' }} />;
};
const TableIndex = (props) => {
const { index } = props;
const { index, ...otherProps } = props;
return (
<div className={classNames('nb-table-index')} style={{ padding: '0 8px 0 16px' }}>
<div className={classNames('nb-table-index')} style={{ padding: '0 8px 0 16px' }} {...otherProps}>
{index}
</div>
);
@ -386,6 +389,11 @@ export const Table: any = observer(
setSelectedRowKeys(selectedRowKeys);
onRowSelectionChange?.(selectedRowKeys, selectedRows);
},
getCheckboxProps(record) {
return {
'aria-label': `checkbox`,
};
},
renderCell: (checked, record, index, originNode) => {
if (!dragSort && !showIndex) {
return originNode;
@ -402,7 +410,8 @@ export const Table: any = observer(
}
return (
<div
data-testid={`table-index-${index}`}
role="button"
aria-label={`table-index-${index}`}
className={classNames(
checked ? 'checked' : null,
css`

View File

@ -130,14 +130,14 @@ const useDefDataSource = (options, props) => {
}, options);
};
const SortHandle = () => {
return <MenuOutlined className={'drag-handle'} style={{ cursor: 'grab' }} />;
const SortHandle = (props) => {
return <MenuOutlined className={'drag-handle'} style={{ cursor: 'grab' }} {...props} />;
};
const TableIndex = (props) => {
const { index } = props;
const { index, ...otherProps } = props;
return (
<div className={classNames('nb-table-index')} style={{ padding: '0 8px 0 16px' }}>
<div className={classNames('nb-table-index')} style={{ padding: '0 8px 0 16px' }} {...otherProps}>
{index + 1}
</div>
);
@ -178,6 +178,11 @@ export const TableArray: React.FC<any> = observer(
onChange(selectedRowKeys: any[]) {
setSelectedRowKeys(selectedRowKeys);
},
getCheckboxProps: (record: any) => {
return {
'aria-label': `checkbox-${record.name}`,
};
},
renderCell: (checked, record, index, originNode) => {
const current = props?.pagination?.current;
const pageSize = props?.pagination?.pageSize || 20;
@ -224,8 +229,10 @@ export const TableArray: React.FC<any> = observer(
`,
)}
>
{dragSort && <SortHandle />}
{showIndex && <TableIndex index={index} />}
{dragSort && <SortHandle role="button" aria-label={`sort-handle-${record?.name || index}`} />}
{showIndex && (
<TableIndex role="button" aria-label={`table-index-${record?.name || index}`} index={index} />
)}
</div>
<div
className={classNames(

View File

@ -4,16 +4,18 @@ import { Space } from 'antd';
import React from 'react';
import { DragHandler } from '../../../schema-component';
import { useSchemaInitializer } from '../../../schema-initializer';
import { useGetAriaLabelOfDesigner } from '../../../schema-settings/hooks/useGetAriaLabelOfDesigner';
export const TableRowActionDesigner = (props: any) => {
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
const { getAriaLabel } = useGetAriaLabelOfDesigner();
return (
<div className={'general-schema-designer'}>
<div className={'general-schema-designer-icons'}>
<Space size={2} align={'center'}>
<DragHandler>
<DragOutlined />
<DragOutlined role="button" aria-label={getAriaLabel('drag-handler')} />
</DragHandler>
{render()}
</Space>

View File

@ -40,27 +40,26 @@ const ConstantTypes = {
string: {
label: `{{t("String")}}`,
value: 'string',
component: function StringComponent({ onChange, value }) {
return <AntInput value={value} onChange={(ev) => onChange(ev.target.value)} />;
component: function StringComponent({ onChange, value, ...otherProps }) {
return <AntInput value={value} onChange={(ev) => onChange(ev.target.value)} {...otherProps} />;
},
default: '',
},
number: {
label: '{{t("Number")}}',
value: 'number',
component: function NumberComponent({ onChange, value }) {
return <InputNumber value={value} onChange={onChange} />;
component: function NumberComponent({ onChange, value, ...otherProps }) {
return <InputNumber value={value} onChange={onChange} {...otherProps} />;
},
default: 0,
},
boolean: {
label: `{{t("Boolean")}}`,
value: 'boolean',
component: function BooleanComponent({ onChange, value }) {
component: function BooleanComponent({ onChange, value, ...otherProps }) {
const { t } = useTranslation();
return (
<Select
data-testid="antd-select"
value={value}
onChange={onChange}
placeholder={t('Select')}
@ -68,6 +67,7 @@ const ConstantTypes = {
{ value: true, label: t('True') },
{ value: false, label: t('False') },
]}
{...otherProps}
/>
);
},
@ -76,13 +76,14 @@ const ConstantTypes = {
date: {
label: '{{t("Date")}}',
value: 'date',
component: function DateComponent({ onChange, value }) {
component: function DateComponent({ onChange, value, ...otherProps }) {
return (
<DatePicker
value={dayjs(value)}
onChange={(d) => (d ? onChange(d.toDate()) : null)}
allowClear={false}
showTime
{...otherProps}
/>
);
},
@ -271,7 +272,7 @@ export function Input(props) {
&:hover {
.clear-button {
opacity: 0.8;
display: inline-block;
}
}
@ -293,6 +294,8 @@ export function Input(props) {
)}
>
<div
role="button"
aria-label="variable-tag"
onInput={(e) => e.preventDefault()}
onKeyDown={(e) => {
if (e.key !== 'Backspace') {
@ -311,10 +314,11 @@ export function Input(props) {
</div>
{!disabled ? (
<span
role="button"
aria-label="icon-close"
className={cx('clear-button')}
// eslint-disable-next-line react/no-unknown-property
unselectable="on"
aria-hidden
onClick={() => onChange(null)}
>
<CloseCircleFilled />
@ -322,7 +326,11 @@ export function Input(props) {
) : null}
</div>
) : (
<div style={{ flex: 1 }}>{children ?? <ConstantComponent value={value} onChange={onChange} />}</div>
<div style={{ flex: 1 }}>
{children ?? (
<ConstantComponent role="button" aria-label="variable-constant" value={value} onChange={onChange} />
)}
</div>
)}
{options.length > 1 && !disabled ? (
<Cascader

View File

@ -367,6 +367,8 @@ export function TextArea(props) {
)}
>
<div
role="button"
aria-label="textbox"
onInput={onInput}
onBlur={onBlur}
onKeyDown={onKeyDown}

View File

@ -1,6 +1,4 @@
import { css } from '@emotion/css';
import { Button, ButtonProps } from 'antd';
import { cx } from 'antd-style';
import React, { forwardRef, useMemo } from 'react';
export const XButton = forwardRef((props: ButtonProps, ref: any) => {
@ -12,7 +10,7 @@ export const XButton = forwardRef((props: ButtonProps, ref: any) => {
}, []);
return (
<Button ref={ref} style={style} {...props}>
<Button aria-label="variable-button" ref={ref} style={style} {...props}>
x{props.children}
</Button>
);

View File

@ -61,7 +61,7 @@ export const useStyles = genStyleHook('nb-variable', (token) => {
insetInlineStart: 'auto',
insetInlineEnd: inputPaddingHorizontalBase,
zIndex: 1,
display: 'inline-block',
display: 'none',
width: token.fontSizeIcon,
height: token.fontSizeIcon,
marginTop: -token.fontSizeIcon / 2,
@ -73,7 +73,7 @@ export const useStyles = genStyleHook('nb-variable', (token) => {
textTransform: 'none',
background: token.colorBgContainer,
cursor: 'pointer',
opacity: 0,
opacity: 0.8,
transition: `color ${token.motionDurationMid} ease, opacity ${token.motionDurationSlow} ease`,
textRendering: 'auto',
userSelect: 'none',

View File

@ -38,6 +38,7 @@ export const Sortable = (props: any) => {
return React.createElement(
component || 'div',
{
role: 'none',
...others,
className: cx('nb-sortable-designer', props.className),
ref: setNodeRef,
@ -118,6 +119,7 @@ export const DragHandler = (props) => {
}}
{...listeners}
{...attributes}
role="none"
>
<span style={{ cursor: 'move', fontSize: 14 }}>{props.children}</span>
</div>

View File

@ -5,12 +5,12 @@ import classNames from 'classnames';
// @ts-ignore
import { isEmpty } from 'lodash';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useCollection } from '../collection-manager';
import { useCollectMenuItem, useMenuItem } from '../hooks/useMenuItem';
import { Icon } from '../icon';
import { SchemaComponent, useActionContext } from '../schema-component';
import { useCompile, useDesignable } from '../schema-component/hooks';
import { SearchCollections } from './SearchCollections';
import { useGetAriaLabelOfSchemaInitializer } from './hooks/useGetAriaLabelOfSchemaInitializer';
import { useStyles } from './style';
import {
SchemaInitializerButtonProps,
@ -252,7 +252,7 @@ SchemaInitializer.Button = observer(
const { Component: CollectComponent, getMenuItem, clean } = useMenuItem();
const menuItems = useRef([]);
const { styles } = useStyles();
const { name } = useCollection();
const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer();
const changeMenu = (v: boolean) => {
setVisible(v);
@ -268,10 +268,6 @@ SchemaInitializer.Button = observer(
return null;
}
if (others['data-testid'] && name) {
others['data-testid'] = `${others['data-testid']}-${name}`;
}
const buttonDom = component || (
<Button
type={'dashed'}
@ -281,6 +277,7 @@ SchemaInitializer.Button = observer(
...style,
}}
{...others}
aria-label={others['aria-label'] || getAriaLabel()}
icon={typeof icon === 'string' ? <Icon type={icon as string} /> : icon}
>
{compile(props.children || props.title)}
@ -295,14 +292,20 @@ SchemaInitializer.Button = observer(
}
};
const renderItems = (items: any) => {
const renderItems = (items: any, parentKey: string) => {
return items
.filter((v: any) => {
return v && (v?.visible ? v.visible() : true);
})
?.map((item: any, indexA: number) => {
// 防止 key 属性无限累加
item = { ...item };
if (item.type === 'divider') {
return { type: 'divider', key: item.key || `item-${indexA}` };
return {
type: 'divider',
key: getKey(item.key || `divider-${indexA}`, parentKey),
};
}
if (item.type === 'item' && item.component) {
const Component = findComponent(item.component);
@ -310,9 +313,9 @@ SchemaInitializer.Button = observer(
error(`SchemaInitializer: component "${item.component}" not found`);
return null;
}
if (!item.key) {
item.key = `${item.title}-${indexA}`;
}
item.key = getKey(item.key || compile(item.title) || item.component, parentKey);
return getMenuItem(() => {
return (
<SchemaInitializerItemContext.Provider
@ -338,31 +341,37 @@ SchemaInitializer.Button = observer(
}
if (item.type === 'itemGroup') {
const label = isString(item.title) ? compile(item.title) : item.title;
const key = getKey(item.key || label, parentKey);
return {
type: 'group',
key: item.key || `item-group-${indexA}`,
key,
label,
title: label,
style: item.style,
loadChildren:
isEmpty(item.children) && item.loadChildren
? ({ searchValue } = { searchValue: '' }) => renderItems(item.loadChildren({ searchValue }) || [])
? ({ searchValue } = { searchValue: '' }) =>
renderItems(item.loadChildren({ searchValue }) || [], key)
: null,
children: isEmpty(item.children) ? [] : renderItems(item.children),
children: isEmpty(item.children) ? [] : renderItems(item.children, key),
};
}
if (item.type === 'subMenu') {
const label = compile(item.title);
const label = isString(item.title) ? compile(item.title) : item.title;
const key = getKey(item.key || label, parentKey);
return {
key: item.key || `item-group-${indexA}`,
role: 'button',
'aria-label': item.key || label,
key,
label,
title: label,
popupClassName: styles.nbMenuItemSubMenu,
loadChildren:
isEmpty(item.children) && item.loadChildren
? ({ searchValue } = { searchValue: '' }) => renderItems(item.loadChildren({ searchValue }) || [])
? ({ searchValue } = { searchValue: '' }) =>
renderItems(item.loadChildren({ searchValue }) || [], key)
: null,
children: isEmpty(item.children) ? [] : renderItems(item.children),
children: isEmpty(item.children) ? [] : renderItems(item.children, key),
};
}
})
@ -371,7 +380,7 @@ SchemaInitializer.Button = observer(
if (visible) {
clean();
menuItems.current = renderItems(items);
menuItems.current = renderItems(items, '');
}
const dropdownRender = () => (
@ -425,11 +434,11 @@ SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
}
return items.map((item, indexA) => {
if (item.type === 'divider') {
return { type: 'divider', key: `divider-${indexA}` };
return { type: 'divider', key: getKey(item.key || `divider-${indexA}`, parentKey) };
}
if (item.type === 'itemGroup') {
const label = isString(item.title) ? compile(item.title) : item.title;
const key = `${parentKey}-item-group-${indexA}`;
const key = getKey(item.key || label, parentKey);
return {
type: 'group',
key,
@ -445,9 +454,11 @@ SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
} as MenuProps['items'][0] & { loadChildren?: ({ searchValue }?: { searchValue: string }) => any[] };
}
if (item.type === 'subMenu') {
const label = compile(item.title);
const key = `${parentKey}-sub-menu-${indexA}`;
const label = isString(item.title) ? compile(item.title) : item.title;
const key = getKey(item.key || label, parentKey);
return {
role: 'button',
'aria-label': key,
key,
label,
title: label,
@ -459,9 +470,12 @@ SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
children: isEmpty(item.children) ? [] : renderMenuItem(item.children, key),
} as MenuProps['items'][0] & { loadChildren?: ({ searchValue }?: { searchValue: string }) => any[] };
}
const label = compile(item.title);
const label = isString(item.title) ? compile(item.title) : item.title;
const key = getKey(item.key || label, parentKey);
return {
key: `${parentKey}-${item.title}-${indexA}`,
role: 'button',
'aria-label': key,
key,
label,
title: label,
onClick: (info) => {
@ -476,6 +490,8 @@ SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
};
const item = {
role: 'button',
'aria-label': info.key,
key: info.key,
label: isString(children) ? compile(children) : children,
icon: typeof icon === 'string' ? <Icon type={icon as string} /> : icon,
@ -488,6 +504,8 @@ SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) {
const label = isString(children) ? compile(children) : children;
const item = {
role: 'button',
'aria-label': info.key,
key: info.key,
label,
title: label,
@ -615,3 +633,18 @@ SchemaInitializer.SwitchItem = (props) => {
</SchemaInitializer.Item>
);
};
function getKey(key: string, parentKey: string) {
if (parentKey && key) {
return `${parentKey}-${key}`;
}
if (!parentKey && !key) {
return '';
}
if (!parentKey) {
return key;
}
if (!key) {
return parentKey;
}
}

View File

@ -2,7 +2,6 @@ import { gridRowColWrap } from '../utils';
// 页面里添加区块
export const BlockInitializers = {
'data-testid': 'add-block-button-in-page',
title: '{{t("Add block")}}',
icon: 'PlusOutlined',
wrap: gridRowColWrap,

View File

@ -2,19 +2,13 @@ import { union } from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SchemaInitializer } from '../SchemaInitializer';
import {
gridRowColWrap,
useAssociatedFormItemInitializerFields,
useCustomBulkEditFormItemInitializerFields,
} from '../utils';
import { gridRowColWrap, useCustomBulkEditFormItemInitializerFields } from '../utils';
export const BulkEditFormItemInitializers = (props: any) => {
const { t } = useTranslation();
const { insertPosition, component } = props;
const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' });
return (
<SchemaInitializer.Button
data-testid="configure-fields-button-of-bulk-edit-form-item"
wrap={gridRowColWrap}
icon={'SettingOutlined'}
items={union<any>(

View File

@ -2,7 +2,6 @@ import { useCollection } from '../../';
// 日历的操作配置
export const CalendarActionInitializers = {
'data-testid': 'configure-actions-button-of-calendar-block',
title: '{{t("Configure actions")}}',
icon: 'SettingOutlined',
style: { marginLeft: 8 },

View File

@ -2,7 +2,6 @@ import { useCollection } from '../..';
// 表单的操作配置
export const CalendarFormActionInitializers = {
'data-testid': 'configure-actions-button-of-calendar-form',
title: '{{t("Configure actions")}}',
icon: 'SettingOutlined',
style: {

View File

@ -34,7 +34,6 @@ export const CustomFormItemInitializers = (props: any) => {
}
return (
<SchemaInitializer.Button
data-testid="configure-fields-button-of-custom-form-item"
wrap={gridRowColWrap}
icon={'SettingOutlined'}
items={fieldItems}

View File

@ -1,6 +1,5 @@
// 表单的操作配置
export const DetailsActionInitializers = {
'data-testid': 'configure-actions-button-of-details-block',
title: '{{t("Configure actions")}}',
icon: 'SettingOutlined',
style: {

Some files were not shown because too many files have changed in this diff Show More