refactor(client): migrate SharedLinks + guest-convo delete to the unified ConfirmDialog (UI unification infra)

Replace OGDialog/OGDialogTemplate delete confirmations in SharedLinks and the standalone guest-convo item with useConfirm; touch up the ConfirmDialog + Modal gallery sections.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kinyoo
2026-07-09 15:30:38 +08:00
co-authored by Claude Opus 4.8
parent 0836db4b6f
commit 1166f52aa1
4 changed files with 330 additions and 169 deletions
@@ -14,13 +14,11 @@ import {
Label,
} from '~/components/ui';
import { useDeleteSharedLinkMutation, useSharedLinksQuery } from '~/hooks/queries/data-provider';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useLocalize, usePrefersMobileLayout } from '~/hooks';
import DataTable from '~/components/ui/DataTable';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { useToastContext, useConfirm } from '~/Providers';
import { formatDate } from '~/utils';
import { Spinner } from '~/components/svg';
const PAGE_SIZE = 25;
@@ -37,7 +35,6 @@ export default function SharedLinks() {
const { showToast } = useToastContext();
const isSmallScreen = usePrefersMobileLayout();
const [queryParams, setQueryParams] = useState<SharedLinksListParams>(DEFAULT_PARAMS);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isLoading } =
@@ -86,8 +83,6 @@ export default function SharedLinks() {
const deleteMutation = useDeleteSharedLinkMutation({
onSuccess: async () => {
setIsDeleteOpen(false);
setDeleteRow(null);
await refetch();
},
onError: (error) => {
@@ -144,14 +139,22 @@ export default function SharedLinks() {
await fetchNextPage();
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
const [deleteRow, setDeleteRow] = useState<SharedLinkItem | null>(null);
const confirmDelete = useCallback(() => {
if (deleteRow) {
handleDelete([deleteRow]);
}
setIsDeleteOpen(false);
}, [deleteRow, handleDelete]);
const confirm = useConfirm();
const handleDeleteClick = useCallback(
async (row: SharedLinkItem) => {
const ok = await confirm({
variant: 'destructive',
title: localize('com_ui_delete_shared_link'),
description: `${localize('com_ui_delete_confirm')} "${row.title}"`,
confirmText: localize('com_ui_delete'),
});
if (!ok) {
return;
}
handleDelete([row]);
},
[confirm, localize, handleDelete],
);
const columns = useMemo(
() => [
@@ -247,10 +250,7 @@ export default function SharedLinks() {
<Button
variant="ghost"
className="h-8 w-8 p-0 hover:bg-surface-hover"
onClick={() => {
setDeleteRow(row.original);
setIsDeleteOpen(true);
}}
onClick={() => handleDeleteClick(row.original)}
title={localize('com_ui_delete')}
>
<TrashIcon className="size-4" />
@@ -261,7 +261,7 @@ export default function SharedLinks() {
),
},
],
[isSmallScreen, localize],
[isSmallScreen, localize, handleDeleteClick],
);
return (
@@ -294,31 +294,6 @@ export default function SharedLinks() {
/>
</OGDialogContent>
</OGDialog>
<OGDialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>
<OGDialogTemplate
showCloseButton={false}
title={localize('com_ui_delete_shared_link')}
className="max-w-[450px]"
main={
<>
<div className="flex w-full flex-col items-center gap-2">
<div className="grid w-full items-center gap-2">
<Label htmlFor="dialog-confirm-delete" className="text-left text-sm font-medium">
{localize('com_ui_delete_confirm')} <strong>{deleteRow?.title}</strong>
</Label>
</div>
</div>
</>
}
selection={{
selectHandler: confirmDelete,
selectClasses: `bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white ${
deleteMutation.isLoading ? 'cursor-not-allowed opacity-80' : ''
}`,
selectText: deleteMutation.isLoading ? <Spinner /> : localize('com_ui_delete'),
}}
/>
</OGDialog>
</div>
);
}
@@ -109,8 +109,9 @@ export function ConfirmDialogSection() {
subtitle={
<>
/ / {' '}
<code>OGDialogTemplate selection</code> 14 <code>useConfirm()</code>
24 8 <b></b>B C 9 selectClasses
<code>OGDialogTemplate selection</code> 13 7 UI + 6 {' '}
<code>useConfirm()</code>26 10
<b></b> UI SidePanel + Chat/Header Modal <b></b>B C 9 selectClasses
danger / primary
9 <code>AlertDialog</code> Modal
</>
@@ -126,7 +127,7 @@ export function ConfirmDialogSection() {
<>
<code>OGDialogTemplate</code> + <code>selection</code>
</>,
'14(原 21,迁移中',
'13(原 21;剩余全是死 UI 或表单',
'旧页面(会话/书签/Agent/设置/Prompt…LibreChat 血统)',
'差 · 确认按钮 9 种写法',
],
@@ -135,7 +136,7 @@ export function ConfirmDialogSection() {
<>
<code>useConfirm()</code>ConfirmContext + AlertDialog
</>,
'24(收敛目标,含已迁入 8 处)',
'26(收敛完成,含已迁入 10 处)',
'新页面(知识空间 / 订阅频道 / 权限)',
'好 · 样式集中在一个文件,destructive/default 两档',
],
@@ -151,20 +152,20 @@ export function ConfirmDialogSection() {
[
'1',
<code key="c">bg-red-700 dark:bg-red-600 hover:bg-red-800 </code>,
'删除(书签/工具/分享链接…)· 删会话 2 处已迁 C',
'6(原 8',
'可达的 4 处已迁 C;剩 4 处全是死 UI(书签/分享弹窗/两个工具移除)',
'4(原 8· 全死 UI',
],
[
'2',
<code key="c">bg-red-600 hover:bg-red-700 dark:hover:bg-red-800</code>,
'删除Agent / Assistant)· Prompt 组已迁 C',
'2(原 3',
'删除 Agent / Assistant —— 死 UISidePanel 被注释)',
'2(原 3· 全死 UI',
],
[
'3',
<code key="c">bg-red-600 hover:bg-red-700 dark:hover:bg-red-600</code>,
'清空预设',
'1',
'清空预设 —— 死 UIChat/Header 无人引用)',
'1 · 死 UI',
],
[
'4',
@@ -292,7 +293,7 @@ export function ConfirmDialogSection() {
/>
<ConfirmDemo
label="Loading 态(isLoading: true"
note="模板内置 Spinner · 自塞 Spinner 的只剩 SharedLinks 1 处(原 4 处,3 处已迁 C"
note="模板内置 Spinner · 各页自塞 Spinner 的写法已随迁移清零(原 4 处"
title="删除会话"
body="确认按钮处于加载中。"
selectText="删除"
@@ -1,10 +1,13 @@
/**
* Modal / Dialog gallery — DEV-ONLY. See docs-ui-refactor/组件-Modal弹窗.md.
*
* Renders BiSheng's two parallel dialog families side by side so the difference
* (overlay darkness / blur / z-index) is visible by opening them.
* Fresh survey (2026-07-09): FIVE coexisting modal populations. The same demo
* content (title + description + input + cancel/confirm footer) is mounted into
* each shell so the shell differences (overlay / radius / padding / title /
* buttons) are the only variable when opening them side by side.
*/
import { Button } from '~/components/ui/Button';
import { Input } from '~/components/ui/Input';
import {
Dialog,
DialogTrigger,
@@ -23,16 +26,46 @@ import {
OGDialogTitle,
OGDialogDescription,
} from '~/components/ui/OriginalDialog';
import {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
} from '~/components/ui/AlertDialog';
import DialogTemplate from '~/components/ui/DialogTemplate';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useConfirm } from '~/Providers';
import { Section, Demo, DemoGrid, CompareTable } from '../components/kit';
const sampleBody = (
<p className="text-sm text-text-primary">
</p>
/** Identical body for every shell so only the shell itself differs. */
const demoBody = (
<div className="flex flex-col gap-3">
<p className="text-sm text-text-primary">
/
</p>
<Input placeholder="示例输入框" />
</div>
);
/** Small reference demo of the finalized confirm dialog, for shell comparison. */
function ConfirmReferenceDemo() {
const confirm = useConfirm();
return (
<Button
variant="outline"
onClick={() =>
confirm({
variant: 'destructive',
title: '确认删除',
description: '弹窗壳视觉参照:圆角16 / p-5 / 灰底毛玻璃遮罩。',
confirmText: '确认删除',
})
}
>
</Button>
);
}
export function ModalSection() {
return (
<Section
@@ -40,94 +73,263 @@ export function ModalSection() {
title="Modal 弹窗"
subtitle={
<>
<b></b>A <code>Dialog</code> z-100 B {' '}
<code>OriginalDialog / OG</code> z-50
2026-07-09 <b>5 64 </b>
B C A 22 + 3
AlertDialog7
</>
}
>
{/* Difference table */}
<div className="mb-6">
{/* ① Population overview */}
<h3 className="mb-3 mt-2 text-base font-semibold text-text-primary">
ui/
</h3>
<div className="mb-8">
<CompareTable
head={['维度', 'A 套 Dialog', 'B 套 OriginalDialog / OG']}
head={['体系', '实现', '业务文件数', '用在哪 / 备注']}
rows={[
['遮罩颜色', 'bg-black/40(浅)', 'bg-black/80(深)'],
['毛玻璃模糊', '有 backdrop-blur-md', '无'],
['层级 z-index', 'z-[100]', 'z-50'],
['便捷模板', 'DialogTemplate~4 处)', 'OGDialogTemplate~25 处 · 用得最多)'],
[
'A 套 · 原语直接拼',
'Dialog + DialogContent 手拼',
<b key="a1">22</b>,
'新页面为主:知识库 8、订阅 2、审批/通知/账号/分享/appChat…(含 MainLayout 全局弹窗)',
],
[
'A 套 · 模板',
'DialogTemplate',
'3',
'EditPresetDialog、PresetItems、ContextButton(后者在 SidePanel 死树)',
],
[
'B 套 · 模板',
'OGDialogTemplate',
'16(原 25,确认迁移后)',
'书签/导出/SetKey/归档/Agent 面板…(其中 SidePanel 死树约 6 处);壳已对齐 C 套',
],
[
'B 套 · 原语直接拼',
'OGDialog + OGDialogContent 手拼',
'16',
'设置(账号/数据)、Prompts、文件预览、ShareAgent…;壳同上(已对齐 C 套)',
],
[
'手拼 AlertDialog',
'AlertDialogContent + 自拼头尾',
'7',
'频道成员 2、爬取系 4、灵思 TaskModeInput(部分带确认性质,本期一并处理)',
],
[
'C 套 · useConfirm(参照)',
'ConfirmContextAlertDialog 底层)',
'26(已收敛 ✅)',
'二次确认已定稿的视觉基准:圆角16 / p-5 / 灰底毛玻璃 —— Modal 壳的天然候选',
],
]}
/>
</div>
<DemoGrid cols={4}>
{/* A-set raw primitives */}
<Demo label="A 套 · Dialog 原语" note="ui/Dialog.tsx · 毛玻璃遮罩">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline"></Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog </DialogTitle>
<DialogDescription> Header / Footer </DialogDescription>
</DialogHeader>
{sampleBody}
<DialogFooter>
<DialogClose asChild>
{/* ② Shell anatomy */}
<h3 className="mb-3 text-base font-semibold text-text-primary">
</h3>
<div className="mb-8">
<CompareTable
head={['维度', 'A 套 Dialog', 'B 套 OriginalDialog(已对齐 C 套)', 'AlertDialog(手拼底座)']}
rows={[
[
'遮罩',
'bg-black/40 + blur(浅黑毛玻璃)',
'bg-gray-500/90 + blur(灰白毛玻璃)',
'bg-gray-500/90 + blur(同 B',
],
['层级 z-index', 'z-[100]', 'z-50', 'z-[110]'],
['圆角', 'sm:rounded-lg8px,移动端直角)', 'rounded-2xl16px', 'sm:rounded-lg8px,移动端直角)'],
['内边距', 'p-520px', 'p-520px', 'p-624px'],
[
'边框 / 阴影',
'border + shadow-lg',
'border #ebebeb + 淡投影',
'无边框、无阴影',
],
[
'标题',
'text-base font-semibold',
'text-base font-medium',
'组件是 text-lg semibold,但各页多自拼标题行',
],
[
'关闭按钮 ×',
'内置右上(可关)',
'内置右上(可关)',
'无内置,各页自拼',
],
[
'深色模式底色',
'dark:bg-[#303134](写死)',
'bg-background(跟主题)',
'dark:bg-gray-900',
],
[
'移动端行为',
'居中缩放出现',
'居中缩放出现',
'从底部滑入、贴底',
],
[
'footer 按钮',
'各页自拼(多为 Button outline + default',
'模板:取消白底描边 + 确认 danger/primary 档;直接拼的各页自理',
'各页自拼(红 #F53F3F 等)',
],
]}
/>
</div>
{/* ③ Side-by-side demos — identical content, different shells */}
<h3 className="mb-3 text-base font-semibold text-text-primary">
</h3>
<div className="mb-8">
<DemoGrid cols={3}>
{/* A-set raw primitives — the largest population */}
<Demo label="A 套 · 原语直接拼(22 处)" note="ui/Dialog.tsx · 浅黑毛玻璃 · 圆角8 · p-5">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline"></Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{demoBody}
<DialogFooter>
<DialogClose asChild>
<Button variant="outline"></Button>
</DialogClose>
<Button></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Demo>
{/* A-set template — legacy default black confirm button */}
<Demo label="A 套 · DialogTemplate3 处)" note="旧模板 · 默认黑底确认钮 · 正文额外 px-6">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline"></Button>
</DialogTrigger>
<DialogTemplate
title="弹窗标题"
description="标题下的说明文字。"
main={demoBody}
selection={{ selectHandler: () => null, selectText: '确定' }}
/>
</Dialog>
</Demo>
{/* B-set template — shell already aligned with C-set */}
<Demo label="B 套 · OGDialogTemplate16 处)" note="壳已对齐 C 套 · 圆角16 · 取消/确认已统一">
<OGDialog>
<OGDialogTrigger asChild>
<Button variant="outline"></Button>
</OGDialogTrigger>
<OGDialogTemplate
title="弹窗标题"
description="标题下的说明文字。"
className="max-w-md"
main={demoBody}
selection={{
selectHandler: () => null,
selectVariant: 'primary',
selectText: '确定',
}}
/>
</OGDialog>
</Demo>
{/* B-set raw primitives — same shell, hand-rolled body/footer */}
<Demo label="B 套 · OG 原语直接拼(16 处)" note="壳同左 · 头尾各页自拼(设置/Prompts 老页面)">
<OGDialog>
<OGDialogTrigger asChild>
<Button variant="outline"></Button>
</OGDialogTrigger>
<OGDialogContent className="max-w-md">
<OGDialogHeader>
<OGDialogTitle></OGDialogTitle>
<OGDialogDescription></OGDialogDescription>
</OGDialogHeader>
{demoBody}
<div className="flex justify-end gap-2">
<Button variant="outline"></Button>
</DialogClose>
<Button></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Demo>
<Button></Button>
</div>
</OGDialogContent>
</OGDialog>
</Demo>
{/* A-set template */}
<Demo label="A 套 · DialogTemplate" note="ui/DialogTemplate.tsx">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline"></Button>
</DialogTrigger>
<DialogTemplate
title="DialogTemplate"
description="传 title / main / buttons 的便捷版。"
main={sampleBody}
buttons={<Button></Button>}
/>
</Dialog>
</Demo>
{/* Hand-rolled AlertDialog population */}
<Demo
label="手拼 AlertDialog7 处)"
note="p-6 · 圆角8 · 无边框阴影 · z-110 · 移动端贴底滑入"
>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline"></Button>
</AlertDialogTrigger>
<AlertDialogContent className="max-w-md">
{/* Business pages hand-roll header/footer like this (ChannelMemberDialog etc.) */}
<h3 className="text-base font-medium text-text-primary"></h3>
{demoBody}
<div className="flex justify-end gap-2">
<Button variant="outline"></Button>
<Button></Button>
</div>
</AlertDialogContent>
</AlertDialog>
</Demo>
{/* B-set raw primitives */}
<Demo label="B 套 · OG 原语" note="ui/OriginalDialog.tsx · 纯深色遮罩">
<OGDialog>
<OGDialogTrigger asChild>
<Button variant="outline"></Button>
</OGDialogTrigger>
<OGDialogContent className="w-11/12 max-w-lg bg-background text-foreground">
<OGDialogHeader>
<OGDialogTitle>OG </OGDialogTitle>
<OGDialogDescription>OriginalDialog </OGDialogDescription>
</OGDialogHeader>
<div className="py-2">{sampleBody}</div>
</OGDialogContent>
</OGDialog>
</Demo>
{/* C-set reference — the finalized confirm shell */}
<Demo
label="C 套 · useConfirm 参照(已定稿)"
note="二次确认基准壳:圆角16 / p-5 / 灰底毛玻璃 —— Modal 壳候选"
>
<ConfirmReferenceDemo />
</Demo>
</DemoGrid>
</div>
{/* B-set template — the most used one */}
<Demo label="B 套 · OGDialogTemplate" note="用得最多 · 基准候选">
<OGDialog>
<OGDialogTrigger asChild>
<Button></Button>
</OGDialogTrigger>
<OGDialogTemplate
title="OGDialogTemplate"
description="全站用得最多的便捷模板,收敛基准候选。"
className="max-w-lg"
main={sampleBody}
buttons={<Button></Button>}
/>
</OGDialog>
</Demo>
</DemoGrid>
{/* ④ Decision checklist */}
<h3 className="mb-3 text-base font-semibold text-text-primary"> </h3>
<div className="rounded-xl border border-border-light bg-muted/20 p-5 text-sm leading-7 text-text-primary">
<ol className="list-decimal space-y-1 pl-5">
<li>
<b></b>A black/40+blur vs B/C gray-500/90+blur
black/80
</li>
<li>
<b></b>8pxA / AlertDialog vs 16pxB/C /
</li>
<li>
<b></b>p-520pxA/B/C vs p-624pxAlertDialogheader/body/footer gap-4
</li>
<li>
<b></b>font-semiboldA vs font-mediumB/C
</li>
<li>
<b> ×</b> A/B ×AlertDialog
</li>
<li>
<b>footer </b>/ Button outline+default C
+ danger/primary gap-2 vs gap-3
</li>
<li>
<b></b>z-50 / z-[100] / z-[110] Drawer/Sheet/Popover
</li>
<li>
<b></b>A 22 A B
</li>
</ol>
</div>
</Section>
);
}
@@ -7,8 +7,8 @@ import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
import type { AppConversation } from '~/@types/app';
import { DropdownPopup, OGDialog, Label } from '~/components';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { DropdownPopup } from '~/components';
import { useConfirm } from '~/Providers';
import TodayItemIcon from '~/components/ui/icon/TodayItem';
type GuestConvoItemProps = {
@@ -29,7 +29,7 @@ export function GuestConvoItem({ conv, isActive, onClick, onRename, onDelete }:
const [isPopoverActive, setIsPopoverActive] = useState(false);
const [renaming, setRenaming] = useState(false);
const [titleInput, setTitleInput] = useState(conv.title);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const confirm = useConfirm();
const inputRef = useRef<HTMLInputElement>(null);
const menuId = useId();
@@ -83,10 +83,18 @@ export function GuestConvoItem({ conv, isActive, onClick, onRename, onDelete }:
[conv.title],
);
const confirmDelete = useCallback(() => {
const handleDeleteClick = useCallback(async () => {
const ok = await confirm({
variant: 'destructive',
title: localize('com_ui_delete_conversation'),
description: `${localize('com_ui_delete_confirm')} "${conv.title}"`,
confirmText: localize('com_ui_delete'),
});
if (!ok) {
return;
}
onDelete(conv.id);
setShowDeleteDialog(false);
}, [conv.id, onDelete]);
}, [confirm, localize, conv.title, conv.id, onDelete]);
return (
<div
@@ -96,7 +104,7 @@ export function GuestConvoItem({ conv, isActive, onClick, onRename, onDelete }:
renaming ? 'bg-[#EEE]' : '',
)}
onClick={(e) => {
if (renaming || isPopoverActive || showDeleteDialog) return;
if (renaming || isPopoverActive) return;
onClick();
}}
>
@@ -174,7 +182,7 @@ export function GuestConvoItem({ conv, isActive, onClick, onRename, onDelete }:
label: localize('com_ui_delete'),
onClick: () => {
setIsPopoverActive(false);
setShowDeleteDialog(true);
handleDeleteClick();
},
icon: <Trash className="icon-sm mr-2 text-text-primary" />,
hideOnClick: false,
@@ -184,31 +192,6 @@ export function GuestConvoItem({ conv, isActive, onClick, onRename, onDelete }:
]}
menuId={menuId}
/>
{/* Delete confirmation dialog */}
{showDeleteDialog && (
<OGDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog} triggerRef={deleteButtonRef}>
<OGDialogTemplate
showCloseButton={false}
title={localize('com_ui_delete_conversation')}
className="max-w-[450px]"
main={
<div className="flex w-full flex-col items-center gap-2">
<div className="grid w-full items-center gap-2">
<Label className="text-left text-sm font-medium">
{localize('com_ui_delete_confirm')} <strong>{conv.title}</strong>
</Label>
</div>
</div>
}
selection={{
selectHandler: confirmDelete,
selectClasses: 'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white',
selectText: localize('com_ui_delete'),
}}
/>
</OGDialog>
)}
</>
)}
</div>