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:
parent
5a342bdbc7
commit
29bf187fbf
@ -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=
|
||||
|
@ -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,
|
||||
|
@ -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: {
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
|
||||
// 应该显示数字输入框
|
||||
|
@ -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('');
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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 }) => {
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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}
|
||||
|
@ -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 (
|
||||
<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}>
|
||||
<div data-testid={props['data-testid']}>{props.children}</div>
|
||||
{props.children}
|
||||
</FilterBlockRecord>
|
||||
</SharedFilterProvider>
|
||||
</BlockRequestProvider>
|
||||
</BlockResourceContext.Provider>
|
||||
</BlockAssociationContext.Provider>
|
||||
</MaybeCollectionProvider>
|
||||
</BlockContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -14,6 +14,11 @@ interface ProviderProps extends Partial<FormActiveFieldsProviderValue> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于提供获取和更新当前表单区块中已显示出来的字段
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export const FormActiveFieldsProvider = ({
|
||||
children,
|
||||
name,
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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 {
|
||||
|
@ -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: (
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
|
@ -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}
|
||||
|
@ -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__,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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' }}
|
||||
|
@ -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'),
|
||||
},
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
@ -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 };
|
||||
};
|
@ -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 };
|
||||
};
|
@ -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 };
|
||||
};
|
@ -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 为当前打开的 mask;wrapper 也是同理
|
||||
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]);
|
||||
}
|
@ -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 为当前打开的 mask;wrapper 也是同理
|
||||
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]);
|
||||
}
|
@ -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]);
|
||||
}
|
@ -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';
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
`}
|
||||
|
@ -49,7 +49,6 @@ export const AssociationFilterInitializer = () => {
|
||||
|
||||
return (
|
||||
<SchemaInitializer.Button
|
||||
data-testid="configure-fields-button-of-association-filter"
|
||||
className={css`
|
||||
margin-top: 16px;
|
||||
`}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
@ -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]}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -64,7 +64,6 @@ const CronSetInternal = (props: CronSetProps) => {
|
||||
return (
|
||||
<fieldset>
|
||||
<Select
|
||||
data-testid="antd-select"
|
||||
popupMatchSelectWidth={false}
|
||||
allowClear
|
||||
{...props}
|
||||
|
@ -63,7 +63,6 @@ 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,
|
||||
@ -72,7 +71,6 @@ export const DynamicComponent = (props: Props) => {
|
||||
renderSchemaComponent,
|
||||
})
|
||||
: renderSchemaComponent()}
|
||||
</div>
|
||||
</FormContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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' },
|
||||
|
@ -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' }}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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={{
|
||||
|
@ -11,6 +11,7 @@ export const MenuItemInitializers = (props: any) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<SchemaInitializer.Button
|
||||
role="button"
|
||||
insertPosition={'beforeEnd'}
|
||||
icon={'PlusOutlined'}
|
||||
insert={props.insert}
|
||||
|
@ -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'}
|
||||
|
@ -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({
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 () => {
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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>;
|
||||
};
|
||||
|
@ -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)}>
|
||||
<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`
|
||||
|
@ -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(
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -367,6 +367,8 @@ export function TextArea(props) {
|
||||
)}
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
aria-label="textbox"
|
||||
onInput={onInput}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>(
|
||||
|
@ -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 },
|
||||
|
@ -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: {
|
||||
|
@ -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}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user