@suphark/ui 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +25 -0
- package/package.json +3 -1
- package/src/components/app-shell/app-nav.tsx +1 -1
- package/src/components/app-shell/app-shell.tsx +31 -5
- package/src/components/app-shell/header-breadcrumbs.tsx +2 -1
- package/src/components/app-shell/header-crumb.tsx +1 -1
- package/src/components/app-shell/index.ts +1 -1
- package/src/components/app-shell/sub-nav.tsx +3 -3
- package/src/components/app-shell/top-tabs.tsx +1 -1
- package/src/components/app-shell/user-menu.tsx +1 -1
- package/src/components/data-table/data-table-column-header.tsx +1 -1
- package/src/components/data-table/data-table-row-actions.tsx +1 -3
- package/src/components/data-table/use-data-table.ts +4 -1
- package/src/components/design-system/design-system-showcase.tsx +1 -1
- package/src/components/pwa/install-prompt-store.ts +26 -0
- package/src/components/pwa/pull-to-refresh.tsx +147 -0
- package/src/components/pwa/pwa-install-card.tsx +165 -0
- package/src/components/pwa/pwa-service-worker-registration.tsx +63 -0
- package/src/components/pwa/service-worker-updates.ts +76 -0
- package/src/components/shared/button/confirm-action-button.tsx +15 -1
- package/src/components/shared/button/delete-many-button.tsx +7 -3
- package/src/components/shared/button/refresh-button.tsx +2 -1
- package/src/components/shared/combobox.tsx +2 -1
- package/src/components/shared/confirm-dialog.tsx +1 -0
- package/src/components/shared/dialogs-provider.tsx +270 -0
- package/src/components/shared/form/form-field-combobox.tsx +1 -1
- package/src/components/shared/form/form-field-input.tsx +8 -2
- package/src/components/shared/form/form-field-radio-cards.tsx +1 -1
- package/src/components/shared/form/form-field-select.tsx +9 -5
- package/src/components/shared/layout/error-state.tsx +16 -5
- package/src/components/shared/link/transition-link.tsx +2 -0
- package/src/components/shared/row-actions.tsx +12 -2
- package/src/components/shared/ui-text.tsx +24 -0
- package/src/components/ui/relative-date.tsx +39 -9
- package/src/hooks/use-tracked-state.ts +35 -0
- package/src/index.ts +12 -0
- package/src/kanban.ts +9 -0
- package/src/lib/formatters.ts +5 -3
- package/src/lib/kanban-collision.ts +118 -0
- package/src/pwa.ts +24 -0
- package/src/styles.css +12 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@suphark/ui/components/ui/button";
|
|
4
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
5
|
+
import { Check, Download, MonitorSmartphone, Share, Smartphone } from "lucide-react";
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import {
|
|
8
|
+
type BeforeInstallPromptEvent,
|
|
9
|
+
getInstallPrompt,
|
|
10
|
+
setInstallPrompt,
|
|
11
|
+
subscribeToInstallPrompt,
|
|
12
|
+
} from "./install-prompt-store";
|
|
13
|
+
|
|
14
|
+
export interface PwaInstallCardText {
|
|
15
|
+
/** หัวการ์ด — `{app}` แทนชื่อแอป */
|
|
16
|
+
title: string;
|
|
17
|
+
installed: string;
|
|
18
|
+
canInstall: string;
|
|
19
|
+
iosHint: string;
|
|
20
|
+
iosSteps: string;
|
|
21
|
+
browserMenu: string;
|
|
22
|
+
installButton: string;
|
|
23
|
+
installedButton: string;
|
|
24
|
+
installing: string;
|
|
25
|
+
dismissed: string;
|
|
26
|
+
installedNow: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const PWA_INSTALL_CARD_TEXT_TH: PwaInstallCardText = {
|
|
30
|
+
title: "ติดตั้ง {app}",
|
|
31
|
+
installed: "{app} เปิดเป็นแอปที่ติดตั้งแล้ว",
|
|
32
|
+
canInstall: "ติดตั้ง {app} เพื่อเปิดเป็นหน้าต่างแอปและเริ่มเร็วขึ้น",
|
|
33
|
+
iosHint: "เปิดเมนูแชร์ แล้วเลือก เพิ่มไปยังหน้าจอโฮม",
|
|
34
|
+
iosSteps: "แตะ แชร์ แล้วเลือก เพิ่มไปยังหน้าจอโฮม",
|
|
35
|
+
browserMenu: "ติดตั้ง {app} จากเมนูของเบราว์เซอร์เมื่อมีตัวเลือกติดตั้ง",
|
|
36
|
+
installButton: "ติดตั้งแอป",
|
|
37
|
+
installedButton: "ติดตั้งแล้ว",
|
|
38
|
+
installing: "กำลังติดตั้ง {app}",
|
|
39
|
+
dismissed: "ยกเลิกการติดตั้ง — ติดตั้งภายหลังได้จากเมนูของเบราว์เซอร์",
|
|
40
|
+
installedNow: "ติดตั้ง {app} บนอุปกรณ์นี้แล้ว",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const PWA_INSTALL_CARD_TEXT_EN: PwaInstallCardText = {
|
|
44
|
+
title: "Install {app}",
|
|
45
|
+
installed: "{app} is already running as an installed app.",
|
|
46
|
+
canInstall: "Install {app} for a focused app window and faster launch.",
|
|
47
|
+
iosHint: "Open Share, then choose Add to Home Screen.",
|
|
48
|
+
iosSteps: "Tap Share, then Add to Home Screen.",
|
|
49
|
+
browserMenu: "Use your browser menu to install {app} when the install option appears.",
|
|
50
|
+
installButton: "Install app",
|
|
51
|
+
installedButton: "Installed",
|
|
52
|
+
installing: "{app} is installing.",
|
|
53
|
+
dismissed: "Install dismissed. You can install later from the browser menu.",
|
|
54
|
+
installedNow: "{app} is installed on this device.",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function isStandaloneDisplay() {
|
|
58
|
+
if (typeof window === "undefined") return false;
|
|
59
|
+
return (
|
|
60
|
+
window.matchMedia("(display-mode: standalone)").matches ||
|
|
61
|
+
("standalone" in window.navigator &&
|
|
62
|
+
(window.navigator as Navigator & { standalone?: boolean }).standalone === true)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isIosDevice() {
|
|
67
|
+
if (typeof window === "undefined") return false;
|
|
68
|
+
return /iphone|ipad|ipod/i.test(window.navigator.userAgent);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* การ์ดชวนติดตั้ง PWA — ใช้ `beforeinstallprompt` ที่ `PwaServiceWorkerRegistration` เก็บไว้;
|
|
73
|
+
* บน iOS (ไม่มี prompt) แสดงวิธี Add to Home Screen; ติดตั้งแล้วแสดงสถานะ
|
|
74
|
+
*
|
|
75
|
+
* <PwaInstallCard appName="Pi Done" text={PWA_INSTALL_CARD_TEXT_EN} />
|
|
76
|
+
*/
|
|
77
|
+
export function PwaInstallCard({
|
|
78
|
+
appName,
|
|
79
|
+
text = PWA_INSTALL_CARD_TEXT_TH,
|
|
80
|
+
compact = false,
|
|
81
|
+
className,
|
|
82
|
+
}: {
|
|
83
|
+
appName: string;
|
|
84
|
+
text?: PwaInstallCardText;
|
|
85
|
+
compact?: boolean;
|
|
86
|
+
className?: string;
|
|
87
|
+
}) {
|
|
88
|
+
const t = (key: keyof PwaInstallCardText) => text[key].replaceAll("{app}", appName);
|
|
89
|
+
const [installPrompt, updateInstallPrompt] = useState<BeforeInstallPromptEvent | null>(null);
|
|
90
|
+
const [isInstalled, setIsInstalled] = useState(false);
|
|
91
|
+
const [isIos, setIsIos] = useState(false);
|
|
92
|
+
const [status, setStatus] = useState<"" | "installing" | "dismissed" | "installedNow">("");
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
setIsInstalled(isStandaloneDisplay());
|
|
96
|
+
setIsIos(isIosDevice());
|
|
97
|
+
updateInstallPrompt(getInstallPrompt());
|
|
98
|
+
|
|
99
|
+
const unsubscribe = subscribeToInstallPrompt(updateInstallPrompt);
|
|
100
|
+
const handleAppInstalled = () => {
|
|
101
|
+
setIsInstalled(true);
|
|
102
|
+
setInstallPrompt(null);
|
|
103
|
+
setStatus("installedNow");
|
|
104
|
+
};
|
|
105
|
+
window.addEventListener("appinstalled", handleAppInstalled);
|
|
106
|
+
return () => {
|
|
107
|
+
unsubscribe();
|
|
108
|
+
window.removeEventListener("appinstalled", handleAppInstalled);
|
|
109
|
+
};
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
const canPromptInstall = Boolean(installPrompt) && !isInstalled;
|
|
113
|
+
const hint = isInstalled
|
|
114
|
+
? t("installed")
|
|
115
|
+
: canPromptInstall
|
|
116
|
+
? t("canInstall")
|
|
117
|
+
: isIos
|
|
118
|
+
? t("iosHint")
|
|
119
|
+
: t("browserMenu");
|
|
120
|
+
|
|
121
|
+
const handleInstall = async () => {
|
|
122
|
+
if (!installPrompt) return;
|
|
123
|
+
await installPrompt.prompt();
|
|
124
|
+
const choice = await installPrompt.userChoice;
|
|
125
|
+
setInstallPrompt(null);
|
|
126
|
+
setStatus(choice.outcome === "accepted" ? "installing" : "dismissed");
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const Icon = isInstalled ? Check : isIos ? Smartphone : MonitorSmartphone;
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className={cn("rounded-lg border bg-muted/20", compact ? "p-3" : "p-4", className)}>
|
|
133
|
+
<div className="flex items-start gap-3">
|
|
134
|
+
<div className="flex size-9 shrink-0 items-center justify-center rounded-md border bg-background text-foreground shadow-xs">
|
|
135
|
+
<Icon className="size-4" aria-hidden="true" />
|
|
136
|
+
</div>
|
|
137
|
+
<div className="flex min-w-0 flex-1 flex-col gap-3">
|
|
138
|
+
<div>
|
|
139
|
+
<h4 className="text-sm font-semibold">{t("title")}</h4>
|
|
140
|
+
<p className="mt-0.5 text-xs text-muted-foreground">{hint}</p>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{isIos && !isInstalled && (
|
|
144
|
+
<div className="flex items-start gap-2 rounded-md border bg-background px-3 py-2 text-xs text-muted-foreground">
|
|
145
|
+
<Share className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
|
|
146
|
+
<span>{t("iosSteps")}</span>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
151
|
+
<Button type="button" size="sm" disabled={!canPromptInstall} onClick={handleInstall}>
|
|
152
|
+
<Download data-icon="inline-start" />
|
|
153
|
+
{isInstalled ? t("installedButton") : t("installButton")}
|
|
154
|
+
</Button>
|
|
155
|
+
{status && (
|
|
156
|
+
<span className="text-xs text-muted-foreground" aria-live="polite">
|
|
157
|
+
{t(status)}
|
|
158
|
+
</span>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { type BeforeInstallPromptEvent, setInstallPrompt } from "./install-prompt-store";
|
|
5
|
+
import { watchServiceWorkerUpdates } from "./service-worker-updates";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ลงทะเบียน service worker ของแอป + ติดตามอัปเดต (watchServiceWorkerUpdates) + เก็บ `beforeinstallprompt`
|
|
9
|
+
* ให้ `PwaInstallCard` — วางครั้งเดียวใน layout ที่มี "use client" ได้ (render null)
|
|
10
|
+
*
|
|
11
|
+
* <PwaServiceWorkerRegistration url="/sw.js" />
|
|
12
|
+
*
|
|
13
|
+
* script ฝั่ง worker ควร: เปลี่ยน byte ทุก deployment (ฝัง deployment id/commit), `self.skipWaiting()` ใน install
|
|
14
|
+
* โดยไม่ขึ้นกับผล precache, `self.clients.claim()` ใน activate, และเสิร์ฟด้วย `Cache-Control: no-cache`
|
|
15
|
+
*/
|
|
16
|
+
export function PwaServiceWorkerRegistration({
|
|
17
|
+
url = "/sw.js",
|
|
18
|
+
scope = "/",
|
|
19
|
+
}: {
|
|
20
|
+
url?: string;
|
|
21
|
+
scope?: string;
|
|
22
|
+
}) {
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const handleBeforeInstallPrompt = (event: Event) => {
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
setInstallPrompt(event as BeforeInstallPromptEvent);
|
|
27
|
+
};
|
|
28
|
+
const handleAppInstalled = () => setInstallPrompt(null);
|
|
29
|
+
window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
|
|
30
|
+
window.addEventListener("appinstalled", handleAppInstalled);
|
|
31
|
+
|
|
32
|
+
let stopWatching: (() => void) | null = null;
|
|
33
|
+
let isUnmounted = false;
|
|
34
|
+
const register = async () => {
|
|
35
|
+
if (!("serviceWorker" in navigator)) return;
|
|
36
|
+
try {
|
|
37
|
+
const registration = await navigator.serviceWorker.register(url, {
|
|
38
|
+
scope,
|
|
39
|
+
updateViaCache: "none",
|
|
40
|
+
});
|
|
41
|
+
if (isUnmounted) return;
|
|
42
|
+
stopWatching = watchServiceWorkerUpdates(navigator.serviceWorker, registration);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error("Service worker registration failed:", error);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
if (document.readyState === "complete") {
|
|
48
|
+
void register();
|
|
49
|
+
} else {
|
|
50
|
+
window.addEventListener("load", register, { once: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return () => {
|
|
54
|
+
isUnmounted = true;
|
|
55
|
+
stopWatching?.();
|
|
56
|
+
window.removeEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
|
|
57
|
+
window.removeEventListener("appinstalled", handleAppInstalled);
|
|
58
|
+
window.removeEventListener("load", register);
|
|
59
|
+
};
|
|
60
|
+
}, [url, scope]);
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const UPDATE_CHECK_INTERVAL_MS = 15 * 60_000;
|
|
2
|
+
|
|
3
|
+
const isEditableElement = (element: Element | null) => {
|
|
4
|
+
if (!element) return false;
|
|
5
|
+
const tag = element.tagName;
|
|
6
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
|
|
7
|
+
return (element as HTMLElement).isContentEditable;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** reload ตอนนี้ได้โดยไม่ขัดผู้ใช้ (ไม่มีช่องกรอกที่กำลังพิมพ์) */
|
|
11
|
+
export const canReloadNow = (doc: Document) =>
|
|
12
|
+
doc.visibilityState === "hidden" || !isEditableElement(doc.activeElement);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ทำให้ service worker ที่ติดตั้งอยู่ตาม deployment ล่าสุดเสมอ
|
|
16
|
+
*
|
|
17
|
+
* - ขอให้ browser เช็ค script ใหม่ตอนโหลด, ทุกครั้งที่กลับมาที่แท็บ, และทุก 15 นาที
|
|
18
|
+
* - worker ฝั่งแอปต้อง `skipWaiting()` + `clients.claim()` (ดู README) → ตัวใหม่คุมหน้าเมื่อติดตั้งเสร็จ
|
|
19
|
+
* แล้ว `controllerchange` จะ reload หน้าให้ bundle ใหม่ทำงานทันที — เลื่อน reload เฉพาะขณะผู้ใช้กำลังพิมพ์
|
|
20
|
+
* และไม่ reload ตอนติดตั้งครั้งแรก
|
|
21
|
+
*
|
|
22
|
+
* คืนฟังก์ชัน cleanup
|
|
23
|
+
*/
|
|
24
|
+
export function watchServiceWorkerUpdates(
|
|
25
|
+
container: ServiceWorkerContainer,
|
|
26
|
+
registration: ServiceWorkerRegistration,
|
|
27
|
+
doc: Document = document,
|
|
28
|
+
win: Window = window,
|
|
29
|
+
) {
|
|
30
|
+
const hadController = container.controller !== null;
|
|
31
|
+
let reloadPending = false;
|
|
32
|
+
let reloading = false;
|
|
33
|
+
|
|
34
|
+
const reload = () => {
|
|
35
|
+
if (reloading) return;
|
|
36
|
+
reloading = true;
|
|
37
|
+
win.location.reload();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const tryReload = () => {
|
|
41
|
+
if (!reloadPending) return;
|
|
42
|
+
if (canReloadNow(doc)) reload();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleControllerChange = () => {
|
|
46
|
+
// ติดตั้งครั้งแรก: หน้านี้รัน bundle ปัจจุบันอยู่แล้ว
|
|
47
|
+
if (!hadController) return;
|
|
48
|
+
reloadPending = true;
|
|
49
|
+
tryReload();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const checkForUpdate = () => {
|
|
53
|
+
if (doc.visibilityState !== "visible") return;
|
|
54
|
+
void registration.update().catch(() => undefined);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleVisibilityChange = () => {
|
|
58
|
+
if (doc.visibilityState === "visible") {
|
|
59
|
+
checkForUpdate();
|
|
60
|
+
tryReload();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
container.addEventListener("controllerchange", handleControllerChange);
|
|
65
|
+
doc.addEventListener("visibilitychange", handleVisibilityChange);
|
|
66
|
+
doc.addEventListener("focusout", tryReload);
|
|
67
|
+
const interval = win.setInterval(checkForUpdate, UPDATE_CHECK_INTERVAL_MS);
|
|
68
|
+
checkForUpdate();
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
container.removeEventListener("controllerchange", handleControllerChange);
|
|
72
|
+
doc.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
73
|
+
doc.removeEventListener("focusout", tryReload);
|
|
74
|
+
win.clearInterval(interval);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -9,8 +9,15 @@ import {
|
|
|
9
9
|
type ConfirmSpec,
|
|
10
10
|
normalizeConfirm,
|
|
11
11
|
} from "@suphark/ui/components/shared/confirm-dialog";
|
|
12
|
+
import { useUiText } from "@suphark/ui/components/shared/ui-text";
|
|
12
13
|
import { useRouter } from "next/navigation";
|
|
13
14
|
import { useState, useTransition } from "react";
|
|
15
|
+
import { toast } from "sonner";
|
|
16
|
+
|
|
17
|
+
/** ข้อความจาก error ที่แสดงให้ผู้ใช้ได้ (Error.message ที่ไม่ว่าง ไม่งั้นใช้ fallback) */
|
|
18
|
+
export function errorMessage(err: unknown, fallback: string): string {
|
|
19
|
+
return err instanceof Error && err.message ? err.message : fallback;
|
|
20
|
+
}
|
|
14
21
|
|
|
15
22
|
/**
|
|
16
23
|
* ปุ่มเดี่ยวที่ถามยืนยัน (ConfirmDialog) ก่อนเรียก server action — ใช้แทน `<form action>` + window.confirm
|
|
@@ -33,6 +40,7 @@ export function ConfirmActionButton({
|
|
|
33
40
|
onDone?: () => void;
|
|
34
41
|
}) {
|
|
35
42
|
const router = useRouter();
|
|
43
|
+
const text = useUiText();
|
|
36
44
|
const [open, setOpen] = useState(false);
|
|
37
45
|
const [pending, start] = useTransition();
|
|
38
46
|
const spec = normalizeConfirm(confirm);
|
|
@@ -61,7 +69,13 @@ export function ConfirmActionButton({
|
|
|
61
69
|
const fd = new FormData();
|
|
62
70
|
for (const [k, v] of Object.entries(data ?? {})) fd.set(k, v);
|
|
63
71
|
start(async () => {
|
|
64
|
-
|
|
72
|
+
// action โยน error → แจ้งด้วย toast แล้วคง dialog ไว้ให้ลองใหม่ (ไม่ปล่อยขึ้น error boundary ทั้งหน้า)
|
|
73
|
+
try {
|
|
74
|
+
await action(fd);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
toast.error(errorMessage(err, text.actionFailed ?? "ดำเนินการไม่สำเร็จ"));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
65
79
|
onDone?.();
|
|
66
80
|
setOpen(false);
|
|
67
81
|
router.refresh();
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import { buttonVariants } from "@suphark/ui/components/ui/button";
|
|
16
16
|
import type { Table } from "@tanstack/react-table";
|
|
17
17
|
import { Loader2 } from "lucide-react";
|
|
18
|
-
import { useTransition } from "react";
|
|
18
|
+
import { useState, useTransition } from "react";
|
|
19
19
|
import { toast } from "sonner";
|
|
20
20
|
|
|
21
21
|
interface DeleteManyButtonProps<TData> {
|
|
@@ -34,6 +34,7 @@ export function DeleteManyButton<TData>({
|
|
|
34
34
|
responsive = false,
|
|
35
35
|
}: DeleteManyButtonProps<TData>) {
|
|
36
36
|
const [isPending, startTransition] = useTransition();
|
|
37
|
+
const [open, setOpen] = useState(false);
|
|
37
38
|
const selectedRows = table.getFilteredSelectedRowModel().rows;
|
|
38
39
|
|
|
39
40
|
if (selectedRows.length === 0) {
|
|
@@ -49,6 +50,9 @@ export function DeleteManyButton<TData>({
|
|
|
49
50
|
throw new Error(result.error);
|
|
50
51
|
}
|
|
51
52
|
toast.success(`${ids.length} ${itemDescription} deleted successfully.`);
|
|
53
|
+
// AlertDialogAction ของ Base UI เป็น Button ธรรมดา (ไม่ใช่ Close) → ต้องปิดเอง + ล้างแถวที่เลือก
|
|
54
|
+
setOpen(false);
|
|
55
|
+
table.resetRowSelection();
|
|
52
56
|
onSuccess?.();
|
|
53
57
|
} catch (err: any) {
|
|
54
58
|
toast.error(err.message);
|
|
@@ -57,7 +61,7 @@ export function DeleteManyButton<TData>({
|
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
return (
|
|
60
|
-
<AlertDialog>
|
|
64
|
+
<AlertDialog open={open} onOpenChange={(o) => !isPending && setOpen(o)}>
|
|
61
65
|
<AlertDialogTrigger render={<DeleteButton isLoading={isPending} responsive={responsive} />}>
|
|
62
66
|
Delete ({selectedRows.length})
|
|
63
67
|
</AlertDialogTrigger>
|
|
@@ -69,7 +73,7 @@ export function DeleteManyButton<TData>({
|
|
|
69
73
|
</AlertDialogDescription>
|
|
70
74
|
</AlertDialogHeader>
|
|
71
75
|
<AlertDialogFooter>
|
|
72
|
-
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
76
|
+
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
|
73
77
|
<AlertDialogAction
|
|
74
78
|
onClick={handleDelete}
|
|
75
79
|
disabled={isPending}
|
|
@@ -27,6 +27,7 @@ export function RefreshButton({
|
|
|
27
27
|
onClick,
|
|
28
28
|
className,
|
|
29
29
|
variant,
|
|
30
|
+
isLoading,
|
|
30
31
|
...props
|
|
31
32
|
}: RefreshButtonProps) {
|
|
32
33
|
const router = useRouter();
|
|
@@ -54,7 +55,7 @@ export function RefreshButton({
|
|
|
54
55
|
icon={buttonIcon}
|
|
55
56
|
onClick={handleClick}
|
|
56
57
|
className={cn(className)}
|
|
57
|
-
isLoading={
|
|
58
|
+
isLoading={isLoading || isPending}
|
|
58
59
|
{...props}
|
|
59
60
|
>
|
|
60
61
|
{buttonText}
|
|
@@ -89,7 +89,8 @@ export function Combobox({
|
|
|
89
89
|
{options.map((option) => (
|
|
90
90
|
<CommandItem
|
|
91
91
|
key={option.value}
|
|
92
|
-
value
|
|
92
|
+
// ต่อ value ท้าย label ให้ cmdk แยกรายการที่ label ซ้ำกันได้ (ค้นด้วยรหัสได้ด้วย)
|
|
93
|
+
value={`${option.label} ${option.value}`}
|
|
93
94
|
onSelect={() => {
|
|
94
95
|
onSelect(option.value === value ? "" : option.value);
|
|
95
96
|
setOpen(false);
|
|
@@ -71,6 +71,7 @@ export function ConfirmDialog({
|
|
|
71
71
|
<AlertDialogAction
|
|
72
72
|
variant={destructive ? "destructive" : "default"}
|
|
73
73
|
disabled={pending}
|
|
74
|
+
aria-busy={pending || undefined}
|
|
74
75
|
onClick={onConfirm}
|
|
75
76
|
>
|
|
76
77
|
{pending && <Loader2 className="animate-spin" aria-hidden="true" />}
|