feat: unify workspace dialogs
This commit is contained in:
@@ -203,14 +203,16 @@ test('makes draft state explicit, protects bulk removal and preserves edits acro
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: /车辆权限/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '清空全部 1 辆车辆权限' }));
|
||||
expect(await screen.findByText('清空 1 辆车辆权限?')).toBeInTheDocument();
|
||||
expect(await screen.findByRole('dialog', { name: '确认清空车辆权限' })).toHaveTextContent('影响对象1 辆当前账号全部车辆权限');
|
||||
expect(screen.getByRole('dialog', { name: '确认清空车辆权限' })).toHaveTextContent('未保存不会影响服务器');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认清空' }));
|
||||
await waitFor(() => expect(screen.queryByRole('button', { name: '移除 粤A11111' })).not.toBeInTheDocument());
|
||||
expect(screen.getByText('已从草稿移除 1 辆车,保存后生效')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭账号详情' }));
|
||||
expect(await screen.findByText('放弃未保存的更改?')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'cancel' }));
|
||||
expect(await screen.findByRole('dialog', { name: '确认放弃未保存的更改' })).toHaveTextContent('放弃未保存的更改?');
|
||||
expect(screen.getByRole('dialog', { name: '确认放弃未保存的更改' })).toHaveTextContent('服务器数据不受影响');
|
||||
fireEvent.click(screen.getByRole('button', { name: '继续编辑' }));
|
||||
expect(document.querySelector('.v2-user-editor-form')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '保存权限' })).toBeEnabled();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { IconChevronRight, IconDelete, IconPlus, IconRefresh, IconSearch } from '@douyinfe/semi-icons';
|
||||
import { Avatar, Button, Card, Checkbox, Collapse, Empty, Input, List, Modal, Popconfirm, Select, Spin, Switch, Tabs, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { Avatar, Button, Card, Checkbox, Collapse, Empty, Input, List, Select, Spin, Switch, Tabs, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { FormEvent, useDeferredValue, useEffect, useMemo, useState } from 'react';
|
||||
import { api } from '../../api/client';
|
||||
import type { AdminUser, CustomerUserInput, CustomerVehicleGrantInput } from '../../api/types';
|
||||
@@ -11,6 +11,7 @@ import { TablePagination } from '../shared/TablePagination';
|
||||
import { VehicleCandidateList } from '../shared/VehicleCandidateList';
|
||||
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
||||
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
||||
import { WorkspaceConfirmDialog } from '../shared/WorkspaceDialog';
|
||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||
import { WorkspaceSideSheet, type WorkspaceSideSheetBadgeColor } from '../shared/WorkspaceSideSheet';
|
||||
import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
|
||||
@@ -147,6 +148,7 @@ export default function UsersPage() {
|
||||
const [vehicleLabels, setVehicleLabels] = useState<Record<string, string>>({});
|
||||
const [feedback, setFeedback] = useState('');
|
||||
const [editingGrantVIN, setEditingGrantVIN] = useState('');
|
||||
const [confirmation, setConfirmation] = useState<'clear-vehicles' | 'discard-editor' | null>(null);
|
||||
const attentionCustomerCount = useMemo(() => customers.filter((user) => customerAccessState(user.status, user.menuKeys.length, user.vehicles.length).attention).length, [customers]);
|
||||
const readyCustomers = useMemo(() => visibleCustomers.filter((user) => customerAccessState(user.status, user.menuKeys.length, user.vehicles.length).key === 'ready').length, [visibleCustomers]);
|
||||
const attentionCustomers = useMemo(() => visibleCustomers.filter((user) => customerAccessState(user.status, user.menuKeys.length, user.vehicles.length).attention).length, [visibleCustomers]);
|
||||
@@ -239,6 +241,7 @@ export default function UsersPage() {
|
||||
setVehicleLabels({});
|
||||
setFeedback('');
|
||||
setEditingGrantVIN('');
|
||||
setConfirmation(null);
|
||||
};
|
||||
const closeEditor = () => {
|
||||
const nextDraft = draftFromUser();
|
||||
@@ -254,20 +257,14 @@ export default function UsersPage() {
|
||||
setVehicleLabels({});
|
||||
setFeedback('');
|
||||
setEditingGrantVIN('');
|
||||
setConfirmation(null);
|
||||
};
|
||||
const requestCloseEditor = () => {
|
||||
if (!hasUnsavedChanges) {
|
||||
closeEditor();
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '放弃未保存的更改?',
|
||||
content: '登录身份、菜单或车辆权限的修改尚未保存,关闭后将恢复到上次保存状态。',
|
||||
okText: '放弃更改',
|
||||
cancelText: '继续编辑',
|
||||
okType: 'danger',
|
||||
onOk: closeEditor
|
||||
});
|
||||
setConfirmation('discard-editor');
|
||||
};
|
||||
const selectCustomer = (user: AdminUser) => {
|
||||
const nextDraft = draftFromUser(user);
|
||||
@@ -309,6 +306,7 @@ export default function UsersPage() {
|
||||
setVehicleComposerOpen(true);
|
||||
setEditingGrantVIN('');
|
||||
setFeedback(`已从草稿移除 ${removedCount} 辆车,保存后生效`);
|
||||
setConfirmation(null);
|
||||
};
|
||||
const updateGrant = (vin: string, patch: Partial<CustomerVehicleGrantInput>) => setDraft((value) => ({
|
||||
...value, vehicleGrants: value.vehicleGrants.map((grant) => grant.vin === vin ? { ...grant, ...patch } : grant)
|
||||
@@ -541,6 +539,7 @@ export default function UsersPage() {
|
||||
}
|
||||
]}
|
||||
onCancel={requestCloseEditor}
|
||||
closeOnEsc={!confirmation}
|
||||
footerNote={<span className="v2-user-save-state" role="status">
|
||||
<Tag color={save.isError ? 'red' : hasUnsavedChanges ? 'amber' : 'green'} type="light" size="small">{save.isError ? '保存失败' : hasUnsavedChanges ? '待保存' : '已同步'}</Tag>
|
||||
<Typography.Text className={`v2-user-feedback${save.isError ? ' is-error' : ''}`} type={save.isError ? 'danger' : 'tertiary'}>{feedback || (hasUnsavedChanges ? '更改仅保存在当前草稿,保存后最多 30 秒生效' : '账号与权限已和服务器同步')}</Typography.Text>
|
||||
@@ -621,17 +620,7 @@ export default function UsersPage() {
|
||||
aria-label={vehicleComposerOpen ? '收起添加车辆' : '添加授权车辆'}
|
||||
onClick={() => setVehicleComposerOpen((value) => !value)}
|
||||
>{vehicleComposerOpen ? '收起' : '添加'}</Button> : null}
|
||||
{draft.vehicleGrants.length ? <Popconfirm
|
||||
title={`清空 ${draft.vehicleGrants.length} 辆车辆权限?`}
|
||||
content="车辆会先从当前草稿中移除,点击底部保存后正式生效。"
|
||||
okText="确认清空"
|
||||
cancelText="取消"
|
||||
okType="danger"
|
||||
position="bottomRight"
|
||||
onConfirm={clearVehicleGrants}
|
||||
>
|
||||
<Button theme="borderless" type="tertiary" size="small" aria-label={`清空全部 ${draft.vehicleGrants.length} 辆车辆权限`}>清空</Button>
|
||||
</Popconfirm> : null}
|
||||
{draft.vehicleGrants.length ? <Button theme="borderless" type="tertiary" size="small" aria-haspopup="dialog" aria-controls="v2-user-clear-vehicles" aria-label={`清空全部 ${draft.vehicleGrants.length} 辆车辆权限`} onClick={() => setConfirmation('clear-vehicles')}>清空</Button> : null}
|
||||
</div> : null}
|
||||
>
|
||||
{vehicleComposerVisible ? <div id="v2-user-vehicle-composer" className="v2-user-vehicle-composer">
|
||||
@@ -723,5 +712,39 @@ export default function UsersPage() {
|
||||
{grantWindowInvalid ? <p role="alert">请设置启用时间,并确保停用时间晚于启用时间。</p> : <p>不设置停用时间时,车辆授权将持续有效。</p>}
|
||||
</div> : null}
|
||||
</WorkspaceSideSheet>
|
||||
<WorkspaceConfirmDialog
|
||||
visible={confirmation === 'clear-vehicles'}
|
||||
ariaLabel="确认清空车辆权限"
|
||||
className="v2-user-confirm-dialog"
|
||||
title={`清空 ${draft.vehicleGrants.length} 辆车辆权限?`}
|
||||
description="车辆会先从当前草稿中移除,只有点击账号详情底部的保存权限后才会正式生效。"
|
||||
summaryItems={[
|
||||
{ label: '影响对象', value: `${draft.vehicleGrants.length} 辆`, detail: '当前账号全部车辆权限', tone: 'danger' },
|
||||
{ label: '当前动作', value: '修改草稿', detail: '确认后仍可继续编辑', tone: 'warning' },
|
||||
{ label: '正式生效', value: '保存权限后', detail: '未保存不会影响服务器', tone: 'success' }
|
||||
]}
|
||||
cancelLabel="返回核对"
|
||||
confirmLabel="确认清空"
|
||||
note="这是草稿操作,仍需再次保存权限。"
|
||||
onCancel={() => setConfirmation(null)}
|
||||
onConfirm={clearVehicleGrants}
|
||||
/>
|
||||
<WorkspaceConfirmDialog
|
||||
visible={confirmation === 'discard-editor'}
|
||||
ariaLabel="确认放弃未保存的更改"
|
||||
className="v2-user-confirm-dialog"
|
||||
title="放弃未保存的更改?"
|
||||
description="登录身份、菜单或车辆权限的修改尚未保存,关闭后将恢复到上次保存状态。"
|
||||
summaryItems={[
|
||||
{ label: '草稿状态', value: '尚未保存', detail: `${draft.menuKeys.length} 菜单 · ${draft.vehicleGrants.length} 辆`, tone: 'warning' },
|
||||
{ label: '关闭结果', value: creating ? '清空新建草稿' : '恢复已保存状态', detail: creating ? '账号不会被创建' : `账号 @${selected?.username || draft.username}`, tone: 'danger' },
|
||||
{ label: '服务器数据', value: '不受影响', detail: '不会发送保存请求', tone: 'success' }
|
||||
]}
|
||||
cancelLabel="继续编辑"
|
||||
confirmLabel="放弃更改"
|
||||
note="放弃后无法恢复当前草稿。"
|
||||
onCancel={() => setConfirmation(null)}
|
||||
onConfirm={closeEditor}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user