refactor(mobile): Improve mobile component usability (#5590)

* refactor: mobile component

* refactor: mobile datepicker

* refactor: datepicker

* refactor: selectpicker

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: style improve

* fix: bug

* refactor: locale improve

* fix: locale

* fix: bug

* chore: datetime picker

* fix: test

* fix: test

* refactor: make test stable

* refactor: make test stable

* fix: bug

* fix: bug

* fix: bug

* refactor: locale improve

* fix: bug
This commit is contained in:
Katherine 2024-11-14 17:13:40 +08:00 committed by GitHub
parent 73438d505c
commit fd6b63d24f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 244 additions and 18 deletions

View File

@ -2,9 +2,7 @@
"version": "1.4.0-alpha.9",
"npmClient": "yarn",
"useWorkspaces": true,
"npmClientArgs": [
"--ignore-engines"
],
"npmClientArgs": ["--ignore-engines"],
"command": {
"version": {
"forcePublish": true,

View File

@ -106,13 +106,13 @@ test.describe('association fields', () => {
await expect(page.getByLabel('Root')).toBeVisible();
await page.getByTestId('select-object-multiple').click();
await page.getByRole('option', { name: 'Member' }).click();
await page.getByTitle('Member').locator('div').click();
// 再次点击,关闭下拉框。
await page.getByTestId('select-object-multiple').click();
await expect(page.getByLabel('Admin')).toBeVisible();
await expect(page.getByTestId('select-object-multiple').getByLabel('Admin')).toBeVisible();
await expect(page.getByLabel('Member')).toBeHidden();
await expect(page.getByLabel('Root')).toBeVisible();
await expect(page.getByTestId('select-object-multiple').getByLabel('Root')).toBeVisible();
await page.getByLabel('schema-initializer-Grid-form:configureFields-users').hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click();
@ -120,8 +120,8 @@ test.describe('association fields', () => {
await page.mouse.move(200, 0);
await page.waitForTimeout(200);
await expect(page.getByLabel('Admin')).toBeVisible();
await expect(page.getByTestId('select-object-multiple').getByLabel('Admin')).toBeVisible();
await expect(page.getByLabel('Member')).toBeHidden();
await expect(page.getByLabel('Root')).toBeVisible();
await expect(page.getByTestId('select-object-multiple').getByLabel('Root')).toBeVisible();
});
});

View File

@ -21,6 +21,7 @@ test.describe('hide column', () => {
.hover();
await page.getByRole('menuitem', { name: 'Hide column question-circle' }).click();
await page.mouse.move(500, 0);
await page.getByLabel('block-item-CardItem-users-form').click();
// 2. Sub table: hide column
await page.getByRole('button', { name: 'Role name' }).hover();

View File

@ -8,3 +8,4 @@
*/
export * from './DatePicker';
export * from './util';

View File

@ -77,7 +77,7 @@ export const moment2str = (value?: Dayjs | null, options: Moment2strOptions = {}
const handleChangeOnFilter = (value, picker, showTime) => {
const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : getPickerFormat(picker);
if (value) {
return value.format(format);
return dayjs(value).format(format);
}
return value;
};

View File

@ -96,7 +96,7 @@ test.describe('form item & create form', () => {
.getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-oneToOneBelongsTo')
.getByTestId(/select-object/)
.click();
await page.getByRole('option', { name: String(records[0].id), exact: true }).click();
await page.getByTitle(String(records[0].id), { exact: true }).click();
},
expectReadonly: async () => {
await expect(
@ -165,7 +165,7 @@ test.describe('form item & create form', () => {
.getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-oneToOneBelongsTo')
.getByTestId('select-object-single')
.click();
await expect(page.getByRole('option', { name: String(records[0].id), exact: true })).toBeVisible();
await expect(page.getByTitle(String(records[0].id), { exact: true })).toBeVisible();
await expect(page.getByRole('option')).toHaveCount(1);
});

View File

@ -155,7 +155,7 @@ test.describe('form item & edit form', () => {
.getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-oneToOneBelongsTo')
.getByTestId(/select-object/)
.click();
await expect(page.getByRole('option', { name: String(recordsOfUser[0].id), exact: true })).toBeVisible();
await expect(page.getByTitle(String(recordsOfUser[0].id), { exact: true })).toBeVisible();
await expect(page.getByRole('option')).toHaveCount(2);
});

View File

@ -332,7 +332,7 @@ export class CollectionSettings {
private async ['File storage'](value: string) {
await this.page.getByLabel('block-item-RemoteSelect-collections-File storage').getByTestId('select-single').click();
await this.page.getByRole('option', { name: value }).click();
await this.page.getByTitle(value).click();
}
private async ['Collection display name'](value: string) {

View File

@ -32,7 +32,7 @@ export const MobileTabBar: FC<MobileTabBarProps> & {
Page: typeof MobileTabBarPage;
Link: typeof MobileTabBarLink;
} = ({ enableTabBar = true }) => {
const { styles } = useStyles();
const { styles } = useStyles() as any;
const { designable } = useDesignable();
const { routeList, activeTabBarItem, resource, refresh } = useMobileRoutes();
const validRouteList = routeList.filter((item) => item.schemaUid || isInnerLink(item.options?.url));

View File

@ -7,9 +7,25 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { RemoteSchemaComponent } from '@nocobase/client';
import { RemoteSchemaComponent, AssociatedFields } from '@nocobase/client';
import React, { useCallback } from 'react';
import { Outlet, useParams } from 'react-router-dom';
import { Button as MobileButton, Form as MobileForm, List as MobileList, Dialog as MobileDialog } from 'antd-mobile';
import { MobilePicker } from './components/MobilePicker';
import { MobileDateTimePicker } from './components/MobileDatePicker';
const mobileComponents = {
Button: MobileButton,
Select: MobilePicker,
DatePicker: MobileDateTimePicker,
UnixTimestamp: MobileDateTimePicker,
Modal: MobileDialog,
AssociatedFields: (
<div contentEditable="false">
<AssociatedFields />
</div>
),
};
export const MobilePage = () => {
const { pageSchemaUid } = useParams<{ pageSchemaUid: string }>();
@ -37,6 +53,7 @@ export const MobilePage = () => {
NotFoundPage={'MobileNotFoundPage'}
memoized={false}
onPageNotFind={onPageNotFind}
components={mobileComponents}
/>
{/* 用于渲染子页面 */}
<Outlet />

View File

@ -0,0 +1,91 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React, { useState, useCallback } from 'react';
import { DatePicker } from 'antd-mobile';
import { mapDatePicker, DatePicker as NBDatePicker } from '@nocobase/client';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { useTranslation } from 'react-i18next';
const MobileDateTimePicker = connect(
(props) => {
const { t } = useTranslation();
const {
value,
onChange,
dateFormat = 'YYYY-MM-DD',
timeFormat = 'HH:mm',
showTime = false,
picker,
...rest
} = props;
const [visible, setVisible] = useState(false);
// 性能优化:使用 useCallback 缓存函数
const handleConfirm = useCallback(
(value) => {
setVisible(false);
const selectedDateTime = new Date(value);
onChange(selectedDateTime);
},
[showTime, onChange],
);
// 清空选择的日期和时间
const handleClear = useCallback(() => {
setVisible(false);
onChange(null);
}, [onChange]);
const labelRenderer = useCallback((type: string, data: number) => {
switch (type) {
case 'year':
return data;
case 'quarter':
return data;
default:
return data;
}
}, []);
return (
<>
<div contentEditable="false" onClick={() => setVisible(true)}>
<NBDatePicker
onClick={() => setVisible(true)}
value={value}
picker={picker}
{...rest}
popupStyle={{ display: 'none' }}
style={{ pointerEvents: 'none', width: '100%' }}
/>
</div>
<DatePicker
cancelText={t('Cancel')}
confirmText={t('Confirm')}
visible={visible}
title={<a onClick={handleClear}>{t('Clear')}</a>}
onClose={() => {
setVisible(false);
}}
precision={showTime ? 'second' : picker === 'date' ? 'day' : picker}
renderLabel={labelRenderer}
min={new Date(1000, 0, 1)}
max={new Date(9999, 11, 31)}
onConfirm={(val) => {
handleConfirm(val);
}}
/>
</>
);
},
mapProps(mapDatePicker()),
mapReadPretty(NBDatePicker.ReadPretty),
);
export { MobileDateTimePicker };

View File

@ -0,0 +1,112 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React, { useEffect, useMemo, useState } from 'react';
import { Button, CheckList, Popup, SearchBar } from 'antd-mobile';
import { connect, mapProps } from '@formily/react';
import { Select } from '@nocobase/client';
import { useTranslation } from 'react-i18next';
const MobilePicker = connect(
(props) => {
const { value, onChange, options = [], mode } = props;
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [selected, setSelected] = useState(value || []);
const [searchText, setSearchText] = useState(null);
const filteredItems = useMemo(() => {
if (searchText) {
return options.filter((item) => item.label.toLowerCase().includes(searchText.toLowerCase()));
}
return options;
}, [options, searchText]);
const handleConfirm = () => {
onChange(selected);
setVisible(false);
};
useEffect(() => {
!visible && setSearchText(null);
}, [visible]);
return (
<>
<div contentEditable="false" onClick={() => setVisible(true)}>
<Select
placeholder={t('Select')}
value={value}
dropdownStyle={{ display: 'none' }}
multiple={mode === 'multiple'}
onClear={() => {
setVisible(false);
onChange(null);
setSelected(null);
}}
onFocus={(e) => e.preventDefault()}
style={{ pointerEvents: 'none' }}
/>
</div>
<Popup
visible={visible}
onMaskClick={() => {
setVisible(false);
if (!value || value?.length === 0) {
setSelected([]);
}
}}
destroyOnClose
>
<div style={{ margin: '10px' }}>
<SearchBar
placeholder={t('search')}
value={searchText}
onChange={(v) => setSearchText(v)}
showCancelButton
/>
</div>
<div
style={{
maxHeight: '60vh',
overflowY: 'auto',
}}
>
<CheckList
multiple={mode === 'multiple'}
value={Array.isArray(selected) ? selected : [selected] || []}
onChange={(val) => {
if (mode === 'multiple') {
setSelected(val);
} else {
setSelected(val[0]);
onChange(val[0]);
setVisible(false);
}
}}
>
{filteredItems.map((item) => (
<CheckList.Item key={item.value} value={item.value}>
{item.label}
</CheckList.Item>
))}
</CheckList>
</div>
{mode === 'multiple' && (
<Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
{t('Confirm')}
</Button>
)}
</Popup>
</>
);
},
mapProps({ dataSource: 'options' }),
);
export { MobilePicker };

View File

@ -24,5 +24,6 @@
"Settings": "Settings",
"Mobile menu": "Mobile menu",
"No accessible pages found": "No accessible pages found",
"This might be due to permission configuration issues": "This might be due to permission configuration issues"
"This might be due to permission configuration issues": "This might be due to permission configuration issues",
"Select time":"Select time"
}

View File

@ -22,5 +22,6 @@
"Desktop data blocks": "デスクトップデータブロック",
"Other desktop blocks": "他のデスクトップブロック",
"Settings": "設定",
"Fill": "塗りつぶし"
"Fill": "塗りつぶし",
"Select time":"時間の選択"
}

View File

@ -25,5 +25,9 @@
"Settings": "设置",
"Mobile menu": "移动端菜单",
"No accessible pages found": "没有找到你可以访问的页面",
"This might be due to permission configuration issues": "这可能是权限配置的问题"
"This might be due to permission configuration issues": "这可能是权限配置的问题",
"Select time": "选择时间",
"Clear": "清空",
"Confirm": "确认",
"Cancel": "取消"
}