@open-mercato/ui 0.6.6-develop.6205.1.109e4b6a84 → 0.6.6-develop.6227.1.2695efff30
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/dist/backend/notifications/useNotificationActions.js +48 -20
- package/dist/backend/notifications/useNotificationActions.js.map +2 -2
- package/dist/primitives/tag-input.js +6 -2
- package/dist/primitives/tag-input.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/notifications/__tests__/useNotificationActions.failedWrites.test.tsx +8 -2
- package/src/backend/notifications/__tests__/useNotificationActions.test.tsx +147 -0
- package/src/backend/notifications/useNotificationActions.ts +58 -20
- package/src/primitives/__tests__/tag-input.test.tsx +7 -0
- package/src/primitives/tag-input.tsx +11 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { apiCall, apiCallOrThrow } from "../utils/apiCall.js";
|
|
4
|
+
import { useGuardedMutation } from "../injection/useGuardedMutation.js";
|
|
5
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
|
+
const NOTIFICATION_ACTIONS_CONTEXT_ID = "notifications-actions";
|
|
4
7
|
function useNotificationActions(notifications, setNotifications, setUnreadCount) {
|
|
5
8
|
const markAsReadRef = React.useRef(async () => {
|
|
6
9
|
});
|
|
@@ -8,27 +11,40 @@ function useNotificationActions(notifications, setNotifications, setUnreadCount)
|
|
|
8
11
|
});
|
|
9
12
|
const [dismissUndo, setDismissUndo] = React.useState(null);
|
|
10
13
|
const dismissUndoTimerRef = React.useRef(null);
|
|
14
|
+
const t = useT();
|
|
15
|
+
const { runMutation, retryLastMutation } = useGuardedMutation({
|
|
16
|
+
contextId: NOTIFICATION_ACTIONS_CONTEXT_ID,
|
|
17
|
+
blockedMessage: t("ui.forms.flash.saveBlocked", "Save blocked by validation")
|
|
18
|
+
});
|
|
11
19
|
const markAsRead = React.useCallback(async (id) => {
|
|
12
|
-
await
|
|
20
|
+
await runMutation({
|
|
21
|
+
operation: () => apiCallOrThrow(`/api/notifications/${id}/read`, { method: "PUT" }),
|
|
22
|
+
context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: "notification", retryLastMutation },
|
|
23
|
+
mutationPayload: { id }
|
|
24
|
+
});
|
|
13
25
|
setNotifications(
|
|
14
26
|
(prev) => prev.map(
|
|
15
27
|
(n) => n.id === id ? { ...n, status: "read", readAt: (/* @__PURE__ */ new Date()).toISOString() } : n
|
|
16
28
|
)
|
|
17
29
|
);
|
|
18
30
|
setUnreadCount((prev) => Math.max(0, prev - 1));
|
|
19
|
-
}, [setNotifications, setUnreadCount]);
|
|
31
|
+
}, [runMutation, retryLastMutation, setNotifications, setUnreadCount]);
|
|
20
32
|
React.useEffect(() => {
|
|
21
33
|
markAsReadRef.current = markAsRead;
|
|
22
34
|
}, [markAsRead]);
|
|
23
35
|
const executeAction = React.useCallback(async (id, actionId) => {
|
|
24
|
-
const result = await
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
const result = await runMutation({
|
|
37
|
+
operation: () => apiCall(
|
|
38
|
+
`/api/notifications/${id}/action`,
|
|
39
|
+
{
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "content-type": "application/json" },
|
|
42
|
+
body: JSON.stringify({ actionId })
|
|
43
|
+
}
|
|
44
|
+
),
|
|
45
|
+
context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: "notification", retryLastMutation },
|
|
46
|
+
mutationPayload: { id, actionId }
|
|
47
|
+
});
|
|
32
48
|
if (result.ok) {
|
|
33
49
|
setNotifications(
|
|
34
50
|
(prev) => prev.map(
|
|
@@ -38,10 +54,14 @@ function useNotificationActions(notifications, setNotifications, setUnreadCount)
|
|
|
38
54
|
setUnreadCount((prev) => Math.max(0, prev - 1));
|
|
39
55
|
}
|
|
40
56
|
return { href: result.result?.href };
|
|
41
|
-
}, [setNotifications, setUnreadCount]);
|
|
57
|
+
}, [runMutation, retryLastMutation, setNotifications, setUnreadCount]);
|
|
42
58
|
const dismiss = React.useCallback(
|
|
43
59
|
async (id) => {
|
|
44
|
-
await
|
|
60
|
+
await runMutation({
|
|
61
|
+
operation: () => apiCallOrThrow(`/api/notifications/${id}/dismiss`, { method: "PUT" }),
|
|
62
|
+
context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: "notification", retryLastMutation },
|
|
63
|
+
mutationPayload: { id }
|
|
64
|
+
});
|
|
45
65
|
const notification = notifications.find((n) => n.id === id);
|
|
46
66
|
setNotifications((prev) => prev.filter((n) => n.id !== id));
|
|
47
67
|
if (notification?.status === "unread") {
|
|
@@ -58,17 +78,21 @@ function useNotificationActions(notifications, setNotifications, setUnreadCount)
|
|
|
58
78
|
}, 6e3);
|
|
59
79
|
}
|
|
60
80
|
},
|
|
61
|
-
[notifications, setNotifications, setUnreadCount]
|
|
81
|
+
[notifications, setNotifications, setUnreadCount, runMutation, retryLastMutation]
|
|
62
82
|
);
|
|
63
83
|
React.useEffect(() => {
|
|
64
84
|
dismissRef.current = dismiss;
|
|
65
85
|
}, [dismiss]);
|
|
66
86
|
const undoDismiss = React.useCallback(async () => {
|
|
67
87
|
if (!dismissUndo) return;
|
|
68
|
-
await
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
88
|
+
await runMutation({
|
|
89
|
+
operation: () => apiCallOrThrow(`/api/notifications/${dismissUndo.notification.id}/restore`, {
|
|
90
|
+
method: "PUT",
|
|
91
|
+
headers: { "content-type": "application/json" },
|
|
92
|
+
body: JSON.stringify({ status: dismissUndo.previousStatus })
|
|
93
|
+
}),
|
|
94
|
+
context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: "notification", retryLastMutation },
|
|
95
|
+
mutationPayload: { id: dismissUndo.notification.id, status: dismissUndo.previousStatus }
|
|
72
96
|
});
|
|
73
97
|
setNotifications((prev) => {
|
|
74
98
|
const next = [
|
|
@@ -90,16 +114,20 @@ function useNotificationActions(notifications, setNotifications, setUnreadCount)
|
|
|
90
114
|
window.clearTimeout(dismissUndoTimerRef.current);
|
|
91
115
|
}
|
|
92
116
|
setDismissUndo(null);
|
|
93
|
-
}, [dismissUndo, setNotifications, setUnreadCount]);
|
|
117
|
+
}, [dismissUndo, setNotifications, setUnreadCount, runMutation, retryLastMutation]);
|
|
94
118
|
const markAllRead = React.useCallback(async () => {
|
|
95
|
-
await
|
|
119
|
+
await runMutation({
|
|
120
|
+
operation: () => apiCallOrThrow("/api/notifications/mark-all-read", { method: "PUT" }),
|
|
121
|
+
context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: "notification", retryLastMutation },
|
|
122
|
+
mutationPayload: {}
|
|
123
|
+
});
|
|
96
124
|
setNotifications(
|
|
97
125
|
(prev) => prev.map(
|
|
98
126
|
(n) => n.status === "unread" ? { ...n, status: "read", readAt: (/* @__PURE__ */ new Date()).toISOString() } : n
|
|
99
127
|
)
|
|
100
128
|
);
|
|
101
129
|
setUnreadCount(0);
|
|
102
|
-
}, [setNotifications, setUnreadCount]);
|
|
130
|
+
}, [runMutation, retryLastMutation, setNotifications, setUnreadCount]);
|
|
103
131
|
React.useEffect(() => {
|
|
104
132
|
return () => {
|
|
105
133
|
if (dismissUndoTimerRef.current) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/notifications/useNotificationActions.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { apiCall, apiCallOrThrow } from '../utils/apiCall'\nimport type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'\n\nexport type NotificationDismissUndoState = {\n notification: NotificationDto\n previousStatus: 'read' | 'unread'\n} | null\n\ntype SetUnreadCount = React.Dispatch<React.SetStateAction<number>>\n\nexport type NotificationActionsResult = {\n markAsRead: (id: string) => Promise<void>\n executeAction: (id: string, actionId: string) => Promise<{ href?: string }>\n dismiss: (id: string) => Promise<void>\n dismissUndo: NotificationDismissUndoState\n undoDismiss: () => Promise<void>\n markAllRead: () => Promise<void>\n markAsReadRef: React.MutableRefObject<(id: string) => Promise<void>>\n dismissRef: React.MutableRefObject<(id: string) => Promise<void>>\n}\n\nexport function useNotificationActions(\n notifications: NotificationDto[],\n setNotifications: React.Dispatch<React.SetStateAction<NotificationDto[]>>,\n setUnreadCount: SetUnreadCount,\n): NotificationActionsResult {\n const markAsReadRef = React.useRef<(id: string) => Promise<void>>(async () => {})\n const dismissRef = React.useRef<(id: string) => Promise<void>>(async () => {})\n const [dismissUndo, setDismissUndo] = React.useState<NotificationDismissUndoState>(null)\n const dismissUndoTimerRef = React.useRef<number | null>(null)\n\n const markAsRead = React.useCallback(async (id: string) => {\n await apiCallOrThrow(`/api/notifications/${id}/read`, { method: 'PUT' })\n setNotifications((prev) =>\n prev.map((n) =>\n n.id === id ? { ...n, status: 'read', readAt: new Date().toISOString() } : n,\n ),\n )\n setUnreadCount((prev) => Math.max(0, prev - 1))\n }, [setNotifications, setUnreadCount])\n\n React.useEffect(() => {\n markAsReadRef.current = markAsRead\n }, [markAsRead])\n\n const executeAction = React.useCallback(async (id: string, actionId: string) => {\n const result = await apiCall<{ ok: boolean; href?: string }>(\n
|
|
5
|
-
"mappings": ";AACA,YAAY,WAAW;AACvB,SAAS,SAAS,sBAAsB;
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { apiCall, apiCallOrThrow } from '../utils/apiCall'\nimport { useGuardedMutation } from '../injection/useGuardedMutation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'\n\nconst NOTIFICATION_ACTIONS_CONTEXT_ID = 'notifications-actions'\n\ntype NotificationMutationContext = {\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n}\n\nexport type NotificationDismissUndoState = {\n notification: NotificationDto\n previousStatus: 'read' | 'unread'\n} | null\n\ntype SetUnreadCount = React.Dispatch<React.SetStateAction<number>>\n\nexport type NotificationActionsResult = {\n markAsRead: (id: string) => Promise<void>\n executeAction: (id: string, actionId: string) => Promise<{ href?: string }>\n dismiss: (id: string) => Promise<void>\n dismissUndo: NotificationDismissUndoState\n undoDismiss: () => Promise<void>\n markAllRead: () => Promise<void>\n markAsReadRef: React.MutableRefObject<(id: string) => Promise<void>>\n dismissRef: React.MutableRefObject<(id: string) => Promise<void>>\n}\n\nexport function useNotificationActions(\n notifications: NotificationDto[],\n setNotifications: React.Dispatch<React.SetStateAction<NotificationDto[]>>,\n setUnreadCount: SetUnreadCount,\n): NotificationActionsResult {\n const markAsReadRef = React.useRef<(id: string) => Promise<void>>(async () => {})\n const dismissRef = React.useRef<(id: string) => Promise<void>>(async () => {})\n const [dismissUndo, setDismissUndo] = React.useState<NotificationDismissUndoState>(null)\n const dismissUndoTimerRef = React.useRef<number | null>(null)\n\n const t = useT()\n const { runMutation, retryLastMutation } = useGuardedMutation<NotificationMutationContext>({\n contextId: NOTIFICATION_ACTIONS_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const markAsRead = React.useCallback(async (id: string) => {\n await runMutation({\n operation: () => apiCallOrThrow(`/api/notifications/${id}/read`, { method: 'PUT' }),\n context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },\n mutationPayload: { id },\n })\n setNotifications((prev) =>\n prev.map((n) =>\n n.id === id ? { ...n, status: 'read', readAt: new Date().toISOString() } : n,\n ),\n )\n setUnreadCount((prev) => Math.max(0, prev - 1))\n }, [runMutation, retryLastMutation, setNotifications, setUnreadCount])\n\n React.useEffect(() => {\n markAsReadRef.current = markAsRead\n }, [markAsRead])\n\n const executeAction = React.useCallback(async (id: string, actionId: string) => {\n const result = await runMutation({\n operation: () =>\n apiCall<{ ok: boolean; href?: string }>(\n `/api/notifications/${id}/action`,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ actionId }),\n },\n ),\n context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },\n mutationPayload: { id, actionId },\n })\n\n if (result.ok) {\n setNotifications((prev) =>\n prev.map((n) =>\n n.id === id ? { ...n, status: 'actioned', actionTaken: actionId } : n,\n ),\n )\n setUnreadCount((prev) => Math.max(0, prev - 1))\n }\n\n return { href: result.result?.href }\n }, [runMutation, retryLastMutation, setNotifications, setUnreadCount])\n\n const dismiss = React.useCallback(\n async (id: string) => {\n await runMutation({\n operation: () => apiCallOrThrow(`/api/notifications/${id}/dismiss`, { method: 'PUT' }),\n context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },\n mutationPayload: { id },\n })\n const notification = notifications.find((n) => n.id === id)\n setNotifications((prev) => prev.filter((n) => n.id !== id))\n if (notification?.status === 'unread') {\n setUnreadCount((prev) => Math.max(0, prev - 1))\n }\n if (notification) {\n const previousStatus = notification.status === 'unread' ? 'unread' : 'read'\n setDismissUndo({ notification, previousStatus })\n if (dismissUndoTimerRef.current) {\n window.clearTimeout(dismissUndoTimerRef.current)\n }\n dismissUndoTimerRef.current = window.setTimeout(() => {\n setDismissUndo(null)\n }, 6000)\n }\n },\n [notifications, setNotifications, setUnreadCount, runMutation, retryLastMutation],\n )\n\n React.useEffect(() => {\n dismissRef.current = dismiss\n }, [dismiss])\n\n const undoDismiss = React.useCallback(async () => {\n if (!dismissUndo) return\n await runMutation({\n operation: () =>\n apiCallOrThrow(`/api/notifications/${dismissUndo.notification.id}/restore`, {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ status: dismissUndo.previousStatus }),\n }),\n context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },\n mutationPayload: { id: dismissUndo.notification.id, status: dismissUndo.previousStatus },\n })\n\n setNotifications((prev) => {\n const next = [\n {\n ...dismissUndo.notification,\n status: dismissUndo.previousStatus,\n readAt:\n dismissUndo.previousStatus === 'unread'\n ? null\n : dismissUndo.notification.readAt ?? new Date().toISOString(),\n },\n ...prev.filter((n) => n.id !== dismissUndo.notification.id),\n ]\n return next.sort(\n (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),\n )\n })\n\n if (dismissUndo.previousStatus === 'unread') {\n setUnreadCount((prev) => prev + 1)\n }\n\n if (dismissUndoTimerRef.current) {\n window.clearTimeout(dismissUndoTimerRef.current)\n }\n setDismissUndo(null)\n }, [dismissUndo, setNotifications, setUnreadCount, runMutation, retryLastMutation])\n\n const markAllRead = React.useCallback(async () => {\n await runMutation({\n operation: () => apiCallOrThrow('/api/notifications/mark-all-read', { method: 'PUT' }),\n context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },\n mutationPayload: {},\n })\n setNotifications((prev) =>\n prev.map((n) =>\n n.status === 'unread'\n ? { ...n, status: 'read', readAt: new Date().toISOString() }\n : n,\n ),\n )\n setUnreadCount(0)\n }, [runMutation, retryLastMutation, setNotifications, setUnreadCount])\n\n React.useEffect(() => {\n return () => {\n if (dismissUndoTimerRef.current) {\n window.clearTimeout(dismissUndoTimerRef.current)\n }\n }\n }, [])\n\n return {\n markAsRead,\n executeAction,\n dismiss,\n dismissUndo,\n undoDismiss,\n markAllRead,\n markAsReadRef,\n dismissRef,\n }\n}\n"],
|
|
5
|
+
"mappings": ";AACA,YAAY,WAAW;AACvB,SAAS,SAAS,sBAAsB;AACxC,SAAS,0BAA0B;AACnC,SAAS,YAAY;AAGrB,MAAM,kCAAkC;AA0BjC,SAAS,uBACd,eACA,kBACA,gBAC2B;AAC3B,QAAM,gBAAgB,MAAM,OAAsC,YAAY;AAAA,EAAC,CAAC;AAChF,QAAM,aAAa,MAAM,OAAsC,YAAY;AAAA,EAAC,CAAC;AAC7E,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAuC,IAAI;AACvF,QAAM,sBAAsB,MAAM,OAAsB,IAAI;AAE5D,QAAM,IAAI,KAAK;AACf,QAAM,EAAE,aAAa,kBAAkB,IAAI,mBAAgD;AAAA,IACzF,WAAW;AAAA,IACX,gBAAgB,EAAE,8BAA8B,4BAA4B;AAAA,EAC9E,CAAC;AAED,QAAM,aAAa,MAAM,YAAY,OAAO,OAAe;AACzD,UAAM,YAAY;AAAA,MAChB,WAAW,MAAM,eAAe,sBAAsB,EAAE,SAAS,EAAE,QAAQ,MAAM,CAAC;AAAA,MAClF,SAAS,EAAE,QAAQ,iCAAiC,cAAc,gBAAgB,kBAAkB;AAAA,MACpG,iBAAiB,EAAE,GAAG;AAAA,IACxB,CAAC;AACD;AAAA,MAAiB,CAAC,SAChB,KAAK;AAAA,QAAI,CAAC,MACR,EAAE,OAAO,KAAK,EAAE,GAAG,GAAG,QAAQ,QAAQ,SAAQ,oBAAI,KAAK,GAAE,YAAY,EAAE,IAAI;AAAA,MAC7E;AAAA,IACF;AACA,mBAAe,CAAC,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,CAAC;AAAA,EAChD,GAAG,CAAC,aAAa,mBAAmB,kBAAkB,cAAc,CAAC;AAErE,QAAM,UAAU,MAAM;AACpB,kBAAc,UAAU;AAAA,EAC1B,GAAG,CAAC,UAAU,CAAC;AAEf,QAAM,gBAAgB,MAAM,YAAY,OAAO,IAAY,aAAqB;AAC9E,UAAM,SAAS,MAAM,YAAY;AAAA,MAC/B,WAAW,MACT;AAAA,QACE,sBAAsB,EAAE;AAAA,QACxB;AAAA,UACE,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,EAAE,SAAS,CAAC;AAAA,QACnC;AAAA,MACF;AAAA,MACF,SAAS,EAAE,QAAQ,iCAAiC,cAAc,gBAAgB,kBAAkB;AAAA,MACpG,iBAAiB,EAAE,IAAI,SAAS;AAAA,IAClC,CAAC;AAED,QAAI,OAAO,IAAI;AACb;AAAA,QAAiB,CAAC,SAChB,KAAK;AAAA,UAAI,CAAC,MACR,EAAE,OAAO,KAAK,EAAE,GAAG,GAAG,QAAQ,YAAY,aAAa,SAAS,IAAI;AAAA,QACtE;AAAA,MACF;AACA,qBAAe,CAAC,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,CAAC;AAAA,IAChD;AAEA,WAAO,EAAE,MAAM,OAAO,QAAQ,KAAK;AAAA,EACrC,GAAG,CAAC,aAAa,mBAAmB,kBAAkB,cAAc,CAAC;AAErE,QAAM,UAAU,MAAM;AAAA,IACpB,OAAO,OAAe;AACpB,YAAM,YAAY;AAAA,QAChB,WAAW,MAAM,eAAe,sBAAsB,EAAE,YAAY,EAAE,QAAQ,MAAM,CAAC;AAAA,QACrF,SAAS,EAAE,QAAQ,iCAAiC,cAAc,gBAAgB,kBAAkB;AAAA,QACpG,iBAAiB,EAAE,GAAG;AAAA,MACxB,CAAC;AACD,YAAM,eAAe,cAAc,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE;AAC1D,uBAAiB,CAAC,SAAS,KAAK,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;AAC1D,UAAI,cAAc,WAAW,UAAU;AACrC,uBAAe,CAAC,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,CAAC;AAAA,MAChD;AACA,UAAI,cAAc;AAChB,cAAM,iBAAiB,aAAa,WAAW,WAAW,WAAW;AACrE,uBAAe,EAAE,cAAc,eAAe,CAAC;AAC/C,YAAI,oBAAoB,SAAS;AAC/B,iBAAO,aAAa,oBAAoB,OAAO;AAAA,QACjD;AACA,4BAAoB,UAAU,OAAO,WAAW,MAAM;AACpD,yBAAe,IAAI;AAAA,QACrB,GAAG,GAAI;AAAA,MACT;AAAA,IACF;AAAA,IACA,CAAC,eAAe,kBAAkB,gBAAgB,aAAa,iBAAiB;AAAA,EAClF;AAEA,QAAM,UAAU,MAAM;AACpB,eAAW,UAAU;AAAA,EACvB,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,cAAc,MAAM,YAAY,YAAY;AAChD,QAAI,CAAC,YAAa;AAClB,UAAM,YAAY;AAAA,MAChB,WAAW,MACT,eAAe,sBAAsB,YAAY,aAAa,EAAE,YAAY;AAAA,QAC1E,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,QAAQ,YAAY,eAAe,CAAC;AAAA,MAC7D,CAAC;AAAA,MACH,SAAS,EAAE,QAAQ,iCAAiC,cAAc,gBAAgB,kBAAkB;AAAA,MACpG,iBAAiB,EAAE,IAAI,YAAY,aAAa,IAAI,QAAQ,YAAY,eAAe;AAAA,IACzF,CAAC;AAED,qBAAiB,CAAC,SAAS;AACzB,YAAM,OAAO;AAAA,QACX;AAAA,UACE,GAAG,YAAY;AAAA,UACf,QAAQ,YAAY;AAAA,UACpB,QACE,YAAY,mBAAmB,WAC3B,OACA,YAAY,aAAa,WAAU,oBAAI,KAAK,GAAE,YAAY;AAAA,QAClE;AAAA,QACA,GAAG,KAAK,OAAO,CAAC,MAAM,EAAE,OAAO,YAAY,aAAa,EAAE;AAAA,MAC5D;AACA,aAAO,KAAK;AAAA,QACV,CAAC,GAAG,MAAM,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ;AAAA,MAC5E;AAAA,IACF,CAAC;AAED,QAAI,YAAY,mBAAmB,UAAU;AAC3C,qBAAe,CAAC,SAAS,OAAO,CAAC;AAAA,IACnC;AAEA,QAAI,oBAAoB,SAAS;AAC/B,aAAO,aAAa,oBAAoB,OAAO;AAAA,IACjD;AACA,mBAAe,IAAI;AAAA,EACrB,GAAG,CAAC,aAAa,kBAAkB,gBAAgB,aAAa,iBAAiB,CAAC;AAElF,QAAM,cAAc,MAAM,YAAY,YAAY;AAChD,UAAM,YAAY;AAAA,MAChB,WAAW,MAAM,eAAe,oCAAoC,EAAE,QAAQ,MAAM,CAAC;AAAA,MACrF,SAAS,EAAE,QAAQ,iCAAiC,cAAc,gBAAgB,kBAAkB;AAAA,MACpG,iBAAiB,CAAC;AAAA,IACpB,CAAC;AACD;AAAA,MAAiB,CAAC,SAChB,KAAK;AAAA,QAAI,CAAC,MACR,EAAE,WAAW,WACT,EAAE,GAAG,GAAG,QAAQ,QAAQ,SAAQ,oBAAI,KAAK,GAAE,YAAY,EAAE,IACzD;AAAA,MACN;AAAA,IACF;AACA,mBAAe,CAAC;AAAA,EAClB,GAAG,CAAC,aAAa,mBAAmB,kBAAkB,cAAc,CAAC;AAErE,QAAM,UAAU,MAAM;AACpB,WAAO,MAAM;AACX,UAAI,oBAAoB,SAAS;AAC/B,eAAO,aAAa,oBAAoB,OAAO;AAAA,MACjD;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,6 +4,9 @@ import * as React from "react";
|
|
|
4
4
|
import { cn } from "@open-mercato/shared/lib/utils";
|
|
5
5
|
import { Input } from "./input.js";
|
|
6
6
|
import { Tag } from "./tag.js";
|
|
7
|
+
function defaultRemoveTagLabel(tag) {
|
|
8
|
+
return `Remove ${tag}`;
|
|
9
|
+
}
|
|
7
10
|
function escapeRegExp(input) {
|
|
8
11
|
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9
12
|
}
|
|
@@ -29,7 +32,8 @@ const TagInput = React.forwardRef(
|
|
|
29
32
|
name,
|
|
30
33
|
"aria-label": ariaLabel,
|
|
31
34
|
"aria-invalid": ariaInvalid,
|
|
32
|
-
rightIcon
|
|
35
|
+
rightIcon,
|
|
36
|
+
removeTagLabel = defaultRemoveTagLabel
|
|
33
37
|
}, ref) => {
|
|
34
38
|
const [internalValue, setInternalValue] = React.useState([]);
|
|
35
39
|
const [inputText, setInputText] = React.useState("");
|
|
@@ -160,7 +164,7 @@ const TagInput = React.forwardRef(
|
|
|
160
164
|
shape: "square",
|
|
161
165
|
disabled,
|
|
162
166
|
onRemove: () => removeTagAt(index),
|
|
163
|
-
removeAriaLabel:
|
|
167
|
+
removeAriaLabel: removeTagLabel(tag),
|
|
164
168
|
children: tag
|
|
165
169
|
},
|
|
166
170
|
`${tag}-${index}`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/tag-input.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Input, type InputProps } from './input'\nimport { Tag } from './tag'\n\nexport type TagInputProps = {\n value?: string[]\n onChange?: (value: string[]) => void\n placeholder?: string\n size?: 'sm' | 'default' | 'lg'\n disabled?: boolean\n maxTags?: number\n validate?: (tag: string) => boolean | string\n separator?: string | RegExp\n allowDuplicates?: boolean\n className?: string\n id?: string\n name?: string\n 'aria-label'?: string\n 'aria-invalid'?: boolean\n /**\n * Optional right-side icon for the input row (e.g. info-circle, search). Mirrors\n * the Figma `Tag Input` Filled state with `Right Icon=true`.\n */\n rightIcon?: InputProps['rightIcon']\n}\n\nfunction escapeRegExp(input: string): string {\n return input.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nfunction buildSplitter(separator: string | RegExp): RegExp {\n if (separator instanceof RegExp) {\n return separator.flags.includes('g')\n ? separator\n : new RegExp(separator.source, `${separator.flags}g`)\n }\n return new RegExp(escapeRegExp(separator), 'g')\n}\n\nexport const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(\n (\n {\n value: valueProp,\n onChange,\n placeholder,\n size = 'default',\n disabled = false,\n maxTags,\n validate,\n separator = ',',\n allowDuplicates = false,\n className,\n id,\n name,\n 'aria-label': ariaLabel,\n 'aria-invalid': ariaInvalid,\n rightIcon,\n },\n ref,\n ) => {\n const [internalValue, setInternalValue] = React.useState<string[]>([])\n const [inputText, setInputText] = React.useState('')\n const [error, setError] = React.useState<string | null>(null)\n\n const isControlled = valueProp !== undefined\n const value = isControlled ? (valueProp as string[]) : internalValue\n\n const commitValue = React.useCallback(\n (next: string[]) => {\n if (!isControlled) setInternalValue(next)\n onChange?.(next)\n },\n [isControlled, onChange],\n )\n\n const appendToList = React.useCallback(\n (current: string[], rawTag: string): { next: string[]; errorText: string | null } | null => {\n const tag = rawTag.trim()\n if (!tag) return null\n if (!allowDuplicates && current.includes(tag)) return null\n if (typeof maxTags === 'number' && current.length >= maxTags) return null\n if (validate) {\n const result = validate(tag)\n if (result === false) return null\n if (typeof result === 'string') return { next: current, errorText: result }\n }\n return { next: [...current, tag], errorText: null }\n },\n [allowDuplicates, maxTags, validate],\n )\n\n const tryAddTag = React.useCallback(\n (rawTag: string): boolean => {\n const outcome = appendToList(value, rawTag)\n if (!outcome) return false\n if (outcome.errorText !== null) {\n setError(outcome.errorText)\n return false\n }\n setError(null)\n if (outcome.next !== value) commitValue(outcome.next)\n return outcome.next !== value\n },\n [appendToList, commitValue, value],\n )\n\n const tryAddManyTags = React.useCallback(\n (rawTags: string[]) => {\n let acc = value\n let pendingError: string | null = null\n for (const raw of rawTags) {\n const outcome = appendToList(acc, raw)\n if (!outcome) continue\n if (outcome.errorText !== null) {\n pendingError = outcome.errorText\n continue\n }\n acc = outcome.next\n }\n if (acc !== value) commitValue(acc)\n if (pendingError !== null) setError(pendingError)\n else if (acc !== value) setError(null)\n },\n [appendToList, commitValue, value],\n )\n\n const removeTagAt = React.useCallback(\n (index: number) => {\n if (index < 0 || index >= value.length) return\n const next = value.slice(0, index).concat(value.slice(index + 1))\n commitValue(next)\n setError(null)\n },\n [commitValue, value],\n )\n\n const splitter = React.useMemo(() => buildSplitter(separator), [separator])\n\n const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n const text = event.target.value\n splitter.lastIndex = 0\n if (splitter.test(text)) {\n splitter.lastIndex = 0\n const parts = text.split(splitter)\n const trailing = parts.pop() ?? ''\n tryAddManyTags(parts)\n setInputText(trailing)\n return\n }\n setInputText(text)\n if (error) setError(null)\n }\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n tryAddTag(inputText)\n setInputText('')\n return\n }\n if (event.key === 'Backspace' && inputText.length === 0 && value.length > 0) {\n event.preventDefault()\n removeTagAt(value.length - 1)\n }\n }\n\n const limitReached = typeof maxTags === 'number' && value.length >= maxTags\n\n return (\n <div className={cn('flex w-full flex-col gap-1', className)} data-slot=\"tag-input\">\n <Input\n ref={ref}\n type=\"text\"\n size={size}\n id={id}\n name={name}\n value={inputText}\n onChange={handleChange}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n disabled={disabled || limitReached}\n aria-label={ariaLabel}\n aria-invalid={ariaInvalid || Boolean(error) || undefined}\n rightIcon={rightIcon}\n data-slot=\"tag-input-field\"\n />\n {value.length > 0 ? (\n <div\n className=\"flex flex-wrap items-center gap-2 pt-1\"\n data-slot=\"tag-input-chips\"\n >\n {value.map((tag, index) => (\n <Tag\n key={`${tag}-${index}`}\n variant=\"default\"\n shape=\"square\"\n disabled={disabled}\n onRemove={() => removeTagAt(index)}\n removeAriaLabel={
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Input, type InputProps } from './input'\nimport { Tag } from './tag'\n\nexport type TagInputProps = {\n value?: string[]\n onChange?: (value: string[]) => void\n placeholder?: string\n size?: 'sm' | 'default' | 'lg'\n disabled?: boolean\n maxTags?: number\n validate?: (tag: string) => boolean | string\n separator?: string | RegExp\n allowDuplicates?: boolean\n className?: string\n id?: string\n name?: string\n 'aria-label'?: string\n 'aria-invalid'?: boolean\n /**\n * Optional right-side icon for the input row (e.g. info-circle, search). Mirrors\n * the Figma `Tag Input` Filled state with `Right Icon=true`.\n */\n rightIcon?: InputProps['rightIcon']\n /**\n * Builds the accessible label for each tag's remove (\u00D7) button. Lets callers\n * localize the action; defaults to the English `Remove {tag}`.\n */\n removeTagLabel?: (tag: string) => string\n}\n\nfunction defaultRemoveTagLabel(tag: string): string {\n return `Remove ${tag}`\n}\n\nfunction escapeRegExp(input: string): string {\n return input.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nfunction buildSplitter(separator: string | RegExp): RegExp {\n if (separator instanceof RegExp) {\n return separator.flags.includes('g')\n ? separator\n : new RegExp(separator.source, `${separator.flags}g`)\n }\n return new RegExp(escapeRegExp(separator), 'g')\n}\n\nexport const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(\n (\n {\n value: valueProp,\n onChange,\n placeholder,\n size = 'default',\n disabled = false,\n maxTags,\n validate,\n separator = ',',\n allowDuplicates = false,\n className,\n id,\n name,\n 'aria-label': ariaLabel,\n 'aria-invalid': ariaInvalid,\n rightIcon,\n removeTagLabel = defaultRemoveTagLabel,\n },\n ref,\n ) => {\n const [internalValue, setInternalValue] = React.useState<string[]>([])\n const [inputText, setInputText] = React.useState('')\n const [error, setError] = React.useState<string | null>(null)\n\n const isControlled = valueProp !== undefined\n const value = isControlled ? (valueProp as string[]) : internalValue\n\n const commitValue = React.useCallback(\n (next: string[]) => {\n if (!isControlled) setInternalValue(next)\n onChange?.(next)\n },\n [isControlled, onChange],\n )\n\n const appendToList = React.useCallback(\n (current: string[], rawTag: string): { next: string[]; errorText: string | null } | null => {\n const tag = rawTag.trim()\n if (!tag) return null\n if (!allowDuplicates && current.includes(tag)) return null\n if (typeof maxTags === 'number' && current.length >= maxTags) return null\n if (validate) {\n const result = validate(tag)\n if (result === false) return null\n if (typeof result === 'string') return { next: current, errorText: result }\n }\n return { next: [...current, tag], errorText: null }\n },\n [allowDuplicates, maxTags, validate],\n )\n\n const tryAddTag = React.useCallback(\n (rawTag: string): boolean => {\n const outcome = appendToList(value, rawTag)\n if (!outcome) return false\n if (outcome.errorText !== null) {\n setError(outcome.errorText)\n return false\n }\n setError(null)\n if (outcome.next !== value) commitValue(outcome.next)\n return outcome.next !== value\n },\n [appendToList, commitValue, value],\n )\n\n const tryAddManyTags = React.useCallback(\n (rawTags: string[]) => {\n let acc = value\n let pendingError: string | null = null\n for (const raw of rawTags) {\n const outcome = appendToList(acc, raw)\n if (!outcome) continue\n if (outcome.errorText !== null) {\n pendingError = outcome.errorText\n continue\n }\n acc = outcome.next\n }\n if (acc !== value) commitValue(acc)\n if (pendingError !== null) setError(pendingError)\n else if (acc !== value) setError(null)\n },\n [appendToList, commitValue, value],\n )\n\n const removeTagAt = React.useCallback(\n (index: number) => {\n if (index < 0 || index >= value.length) return\n const next = value.slice(0, index).concat(value.slice(index + 1))\n commitValue(next)\n setError(null)\n },\n [commitValue, value],\n )\n\n const splitter = React.useMemo(() => buildSplitter(separator), [separator])\n\n const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n const text = event.target.value\n splitter.lastIndex = 0\n if (splitter.test(text)) {\n splitter.lastIndex = 0\n const parts = text.split(splitter)\n const trailing = parts.pop() ?? ''\n tryAddManyTags(parts)\n setInputText(trailing)\n return\n }\n setInputText(text)\n if (error) setError(null)\n }\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n tryAddTag(inputText)\n setInputText('')\n return\n }\n if (event.key === 'Backspace' && inputText.length === 0 && value.length > 0) {\n event.preventDefault()\n removeTagAt(value.length - 1)\n }\n }\n\n const limitReached = typeof maxTags === 'number' && value.length >= maxTags\n\n return (\n <div className={cn('flex w-full flex-col gap-1', className)} data-slot=\"tag-input\">\n <Input\n ref={ref}\n type=\"text\"\n size={size}\n id={id}\n name={name}\n value={inputText}\n onChange={handleChange}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n disabled={disabled || limitReached}\n aria-label={ariaLabel}\n aria-invalid={ariaInvalid || Boolean(error) || undefined}\n rightIcon={rightIcon}\n data-slot=\"tag-input-field\"\n />\n {value.length > 0 ? (\n <div\n className=\"flex flex-wrap items-center gap-2 pt-1\"\n data-slot=\"tag-input-chips\"\n >\n {value.map((tag, index) => (\n <Tag\n key={`${tag}-${index}`}\n variant=\"default\"\n shape=\"square\"\n disabled={disabled}\n onRemove={() => removeTagAt(index)}\n removeAriaLabel={removeTagLabel(tag)}\n >\n {tag}\n </Tag>\n ))}\n </div>\n ) : null}\n {error ? (\n <p\n className=\"text-xs text-status-error-text\"\n role=\"alert\"\n data-slot=\"tag-input-error\"\n >\n {error}\n </p>\n ) : null}\n </div>\n )\n },\n)\n\nTagInput.displayName = 'TagInput'\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6227.1.2695efff30",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,13 +155,13 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
158
|
+
"@open-mercato/shared": "0.6.6-develop.6227.1.2695efff30",
|
|
159
159
|
"react": ">=18.0.0",
|
|
160
160
|
"react-dom": ">=18.0.0",
|
|
161
161
|
"react-is": ">=18.0.0"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
164
|
+
"@open-mercato/shared": "0.6.6-develop.6227.1.2695efff30",
|
|
165
165
|
"@testing-library/dom": "^10.4.1",
|
|
166
166
|
"@testing-library/jest-dom": "^6.9.1",
|
|
167
167
|
"@testing-library/react": "^16.3.1",
|
|
@@ -5,9 +5,14 @@ jest.mock('../../utils/api', () => ({
|
|
|
5
5
|
import { act, renderHook } from '@testing-library/react'
|
|
6
6
|
import * as React from 'react'
|
|
7
7
|
import type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'
|
|
8
|
+
import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
|
|
8
9
|
import { apiFetch } from '../../utils/api'
|
|
9
10
|
import { useNotificationActions } from '../useNotificationActions'
|
|
10
11
|
|
|
12
|
+
function I18nWrapper({ children }: { children: React.ReactNode }) {
|
|
13
|
+
return React.createElement(I18nProvider, { locale: 'en', dict: {} }, children)
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
function makeNotification(id: string): NotificationDto {
|
|
12
17
|
return {
|
|
13
18
|
id,
|
|
@@ -39,8 +44,9 @@ function setFetchResult(status: number) {
|
|
|
39
44
|
function renderActions(notifications: NotificationDto[]) {
|
|
40
45
|
const setNotifications = jest.fn()
|
|
41
46
|
const setUnreadCount = jest.fn()
|
|
42
|
-
const hook = renderHook(
|
|
43
|
-
useNotificationActions(notifications, setNotifications, setUnreadCount),
|
|
47
|
+
const hook = renderHook(
|
|
48
|
+
() => useNotificationActions(notifications, setNotifications, setUnreadCount),
|
|
49
|
+
{ wrapper: I18nWrapper },
|
|
44
50
|
)
|
|
45
51
|
return { hook, setNotifications, setUnreadCount }
|
|
46
52
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { act, renderHook } from '@testing-library/react'
|
|
5
|
+
import type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'
|
|
6
|
+
|
|
7
|
+
const apiCallMock = jest.fn()
|
|
8
|
+
const apiCallOrThrowMock = jest.fn()
|
|
9
|
+
const runMutationMock = jest.fn()
|
|
10
|
+
const retryLastMutation = jest.fn(async () => true)
|
|
11
|
+
|
|
12
|
+
jest.mock('../../utils/apiCall', () => ({
|
|
13
|
+
apiCall: (...args: unknown[]) => apiCallMock(...args),
|
|
14
|
+
apiCallOrThrow: (...args: unknown[]) => apiCallOrThrowMock(...args),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
jest.mock('../../injection/useGuardedMutation', () => ({
|
|
18
|
+
useGuardedMutation: () => ({
|
|
19
|
+
runMutation: (...args: unknown[]) => runMutationMock(...args),
|
|
20
|
+
retryLastMutation,
|
|
21
|
+
}),
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
jest.mock('@open-mercato/shared/lib/i18n/context', () => ({
|
|
25
|
+
useT: () => (key: string, fallback?: string) => fallback ?? key,
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
import { useNotificationActions } from '../useNotificationActions'
|
|
29
|
+
|
|
30
|
+
type RunMutationInput = {
|
|
31
|
+
context: { resourceKind: string; retryLastMutation: () => Promise<boolean> }
|
|
32
|
+
mutationPayload: Record<string, unknown>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeNotification(id: string, status: 'unread' | 'read' = 'unread'): NotificationDto {
|
|
36
|
+
return {
|
|
37
|
+
id,
|
|
38
|
+
type: 'example',
|
|
39
|
+
title: `title-${id}`,
|
|
40
|
+
severity: 'info',
|
|
41
|
+
status,
|
|
42
|
+
actions: [],
|
|
43
|
+
createdAt: new Date().toISOString(),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderActions(initial: NotificationDto[] = []) {
|
|
48
|
+
const setNotifications = jest.fn()
|
|
49
|
+
const setUnreadCount = jest.fn()
|
|
50
|
+
const { result } = renderHook(() =>
|
|
51
|
+
useNotificationActions(initial, setNotifications, setUnreadCount),
|
|
52
|
+
)
|
|
53
|
+
return { result }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function lastRunMutationInput(): RunMutationInput {
|
|
57
|
+
return runMutationMock.mock.calls[runMutationMock.mock.calls.length - 1][0] as RunMutationInput
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('useNotificationActions guarded mutations', () => {
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
apiCallMock.mockReset()
|
|
63
|
+
apiCallMock.mockResolvedValue({ ok: true, result: {} })
|
|
64
|
+
apiCallOrThrowMock.mockReset()
|
|
65
|
+
apiCallOrThrowMock.mockResolvedValue({ ok: true, result: {} })
|
|
66
|
+
runMutationMock.mockReset()
|
|
67
|
+
runMutationMock.mockImplementation(
|
|
68
|
+
async ({ operation }: { operation: () => Promise<unknown> }) => operation(),
|
|
69
|
+
)
|
|
70
|
+
retryLastMutation.mockClear()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('routes markAsRead through the guarded mutation path', async () => {
|
|
74
|
+
const { result } = renderActions([makeNotification('n1')])
|
|
75
|
+
await act(async () => {
|
|
76
|
+
await result.current.markAsRead('n1')
|
|
77
|
+
})
|
|
78
|
+
expect(runMutationMock).toHaveBeenCalledTimes(1)
|
|
79
|
+
const input = lastRunMutationInput()
|
|
80
|
+
expect(input.context.resourceKind).toBe('notification')
|
|
81
|
+
expect(input.context.retryLastMutation).toBe(retryLastMutation)
|
|
82
|
+
expect(input.mutationPayload).toEqual({ id: 'n1' })
|
|
83
|
+
expect(apiCallOrThrowMock).toHaveBeenCalledWith('/api/notifications/n1/read', { method: 'PUT' })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('routes executeAction through the guarded mutation path and returns the href', async () => {
|
|
87
|
+
apiCallMock.mockResolvedValue({ ok: true, result: { href: '/go' } })
|
|
88
|
+
const { result } = renderActions([makeNotification('n1')])
|
|
89
|
+
let returned: { href?: string } = {}
|
|
90
|
+
await act(async () => {
|
|
91
|
+
returned = await result.current.executeAction('n1', 'approve')
|
|
92
|
+
})
|
|
93
|
+
expect(runMutationMock).toHaveBeenCalledTimes(1)
|
|
94
|
+
const input = lastRunMutationInput()
|
|
95
|
+
expect(input.context.resourceKind).toBe('notification')
|
|
96
|
+
expect(input.mutationPayload).toEqual({ id: 'n1', actionId: 'approve' })
|
|
97
|
+
expect(apiCallMock).toHaveBeenCalledWith(
|
|
98
|
+
'/api/notifications/n1/action',
|
|
99
|
+
expect.objectContaining({ method: 'POST' }),
|
|
100
|
+
)
|
|
101
|
+
expect(returned.href).toBe('/go')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('routes dismiss through the guarded mutation path', async () => {
|
|
105
|
+
const { result } = renderActions([makeNotification('n1')])
|
|
106
|
+
await act(async () => {
|
|
107
|
+
await result.current.dismiss('n1')
|
|
108
|
+
})
|
|
109
|
+
expect(runMutationMock).toHaveBeenCalledTimes(1)
|
|
110
|
+
const input = lastRunMutationInput()
|
|
111
|
+
expect(input.context.resourceKind).toBe('notification')
|
|
112
|
+
expect(input.mutationPayload).toEqual({ id: 'n1' })
|
|
113
|
+
expect(apiCallOrThrowMock).toHaveBeenCalledWith('/api/notifications/n1/dismiss', { method: 'PUT' })
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('routes undoDismiss through the guarded mutation path', async () => {
|
|
117
|
+
const { result } = renderActions([makeNotification('n1')])
|
|
118
|
+
await act(async () => {
|
|
119
|
+
await result.current.dismiss('n1')
|
|
120
|
+
})
|
|
121
|
+
runMutationMock.mockClear()
|
|
122
|
+
apiCallMock.mockClear()
|
|
123
|
+
apiCallOrThrowMock.mockClear()
|
|
124
|
+
await act(async () => {
|
|
125
|
+
await result.current.undoDismiss()
|
|
126
|
+
})
|
|
127
|
+
expect(runMutationMock).toHaveBeenCalledTimes(1)
|
|
128
|
+
const input = lastRunMutationInput()
|
|
129
|
+
expect(input.context.resourceKind).toBe('notification')
|
|
130
|
+
expect(input.mutationPayload).toEqual({ id: 'n1', status: 'unread' })
|
|
131
|
+
expect(apiCallOrThrowMock).toHaveBeenCalledWith(
|
|
132
|
+
'/api/notifications/n1/restore',
|
|
133
|
+
expect.objectContaining({ method: 'PUT' }),
|
|
134
|
+
)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('routes markAllRead through the guarded mutation path', async () => {
|
|
138
|
+
const { result } = renderActions([makeNotification('n1')])
|
|
139
|
+
await act(async () => {
|
|
140
|
+
await result.current.markAllRead()
|
|
141
|
+
})
|
|
142
|
+
expect(runMutationMock).toHaveBeenCalledTimes(1)
|
|
143
|
+
const input = lastRunMutationInput()
|
|
144
|
+
expect(input.context.resourceKind).toBe('notification')
|
|
145
|
+
expect(apiCallOrThrowMock).toHaveBeenCalledWith('/api/notifications/mark-all-read', { method: 'PUT' })
|
|
146
|
+
})
|
|
147
|
+
})
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import { apiCall, apiCallOrThrow } from '../utils/apiCall'
|
|
4
|
+
import { useGuardedMutation } from '../injection/useGuardedMutation'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
4
6
|
import type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'
|
|
5
7
|
|
|
8
|
+
const NOTIFICATION_ACTIONS_CONTEXT_ID = 'notifications-actions'
|
|
9
|
+
|
|
10
|
+
type NotificationMutationContext = {
|
|
11
|
+
formId: string
|
|
12
|
+
resourceKind: string
|
|
13
|
+
retryLastMutation: () => Promise<boolean>
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
export type NotificationDismissUndoState = {
|
|
7
17
|
notification: NotificationDto
|
|
8
18
|
previousStatus: 'read' | 'unread'
|
|
@@ -31,29 +41,44 @@ export function useNotificationActions(
|
|
|
31
41
|
const [dismissUndo, setDismissUndo] = React.useState<NotificationDismissUndoState>(null)
|
|
32
42
|
const dismissUndoTimerRef = React.useRef<number | null>(null)
|
|
33
43
|
|
|
44
|
+
const t = useT()
|
|
45
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<NotificationMutationContext>({
|
|
46
|
+
contextId: NOTIFICATION_ACTIONS_CONTEXT_ID,
|
|
47
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
48
|
+
})
|
|
49
|
+
|
|
34
50
|
const markAsRead = React.useCallback(async (id: string) => {
|
|
35
|
-
await
|
|
51
|
+
await runMutation({
|
|
52
|
+
operation: () => apiCallOrThrow(`/api/notifications/${id}/read`, { method: 'PUT' }),
|
|
53
|
+
context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },
|
|
54
|
+
mutationPayload: { id },
|
|
55
|
+
})
|
|
36
56
|
setNotifications((prev) =>
|
|
37
57
|
prev.map((n) =>
|
|
38
58
|
n.id === id ? { ...n, status: 'read', readAt: new Date().toISOString() } : n,
|
|
39
59
|
),
|
|
40
60
|
)
|
|
41
61
|
setUnreadCount((prev) => Math.max(0, prev - 1))
|
|
42
|
-
}, [setNotifications, setUnreadCount])
|
|
62
|
+
}, [runMutation, retryLastMutation, setNotifications, setUnreadCount])
|
|
43
63
|
|
|
44
64
|
React.useEffect(() => {
|
|
45
65
|
markAsReadRef.current = markAsRead
|
|
46
66
|
}, [markAsRead])
|
|
47
67
|
|
|
48
68
|
const executeAction = React.useCallback(async (id: string, actionId: string) => {
|
|
49
|
-
const result = await
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
const result = await runMutation({
|
|
70
|
+
operation: () =>
|
|
71
|
+
apiCall<{ ok: boolean; href?: string }>(
|
|
72
|
+
`/api/notifications/${id}/action`,
|
|
73
|
+
{
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'content-type': 'application/json' },
|
|
76
|
+
body: JSON.stringify({ actionId }),
|
|
77
|
+
},
|
|
78
|
+
),
|
|
79
|
+
context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },
|
|
80
|
+
mutationPayload: { id, actionId },
|
|
81
|
+
})
|
|
57
82
|
|
|
58
83
|
if (result.ok) {
|
|
59
84
|
setNotifications((prev) =>
|
|
@@ -65,11 +90,15 @@ export function useNotificationActions(
|
|
|
65
90
|
}
|
|
66
91
|
|
|
67
92
|
return { href: result.result?.href }
|
|
68
|
-
}, [setNotifications, setUnreadCount])
|
|
93
|
+
}, [runMutation, retryLastMutation, setNotifications, setUnreadCount])
|
|
69
94
|
|
|
70
95
|
const dismiss = React.useCallback(
|
|
71
96
|
async (id: string) => {
|
|
72
|
-
await
|
|
97
|
+
await runMutation({
|
|
98
|
+
operation: () => apiCallOrThrow(`/api/notifications/${id}/dismiss`, { method: 'PUT' }),
|
|
99
|
+
context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },
|
|
100
|
+
mutationPayload: { id },
|
|
101
|
+
})
|
|
73
102
|
const notification = notifications.find((n) => n.id === id)
|
|
74
103
|
setNotifications((prev) => prev.filter((n) => n.id !== id))
|
|
75
104
|
if (notification?.status === 'unread') {
|
|
@@ -86,7 +115,7 @@ export function useNotificationActions(
|
|
|
86
115
|
}, 6000)
|
|
87
116
|
}
|
|
88
117
|
},
|
|
89
|
-
[notifications, setNotifications, setUnreadCount],
|
|
118
|
+
[notifications, setNotifications, setUnreadCount, runMutation, retryLastMutation],
|
|
90
119
|
)
|
|
91
120
|
|
|
92
121
|
React.useEffect(() => {
|
|
@@ -95,10 +124,15 @@ export function useNotificationActions(
|
|
|
95
124
|
|
|
96
125
|
const undoDismiss = React.useCallback(async () => {
|
|
97
126
|
if (!dismissUndo) return
|
|
98
|
-
await
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
127
|
+
await runMutation({
|
|
128
|
+
operation: () =>
|
|
129
|
+
apiCallOrThrow(`/api/notifications/${dismissUndo.notification.id}/restore`, {
|
|
130
|
+
method: 'PUT',
|
|
131
|
+
headers: { 'content-type': 'application/json' },
|
|
132
|
+
body: JSON.stringify({ status: dismissUndo.previousStatus }),
|
|
133
|
+
}),
|
|
134
|
+
context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },
|
|
135
|
+
mutationPayload: { id: dismissUndo.notification.id, status: dismissUndo.previousStatus },
|
|
102
136
|
})
|
|
103
137
|
|
|
104
138
|
setNotifications((prev) => {
|
|
@@ -126,10 +160,14 @@ export function useNotificationActions(
|
|
|
126
160
|
window.clearTimeout(dismissUndoTimerRef.current)
|
|
127
161
|
}
|
|
128
162
|
setDismissUndo(null)
|
|
129
|
-
}, [dismissUndo, setNotifications, setUnreadCount])
|
|
163
|
+
}, [dismissUndo, setNotifications, setUnreadCount, runMutation, retryLastMutation])
|
|
130
164
|
|
|
131
165
|
const markAllRead = React.useCallback(async () => {
|
|
132
|
-
await
|
|
166
|
+
await runMutation({
|
|
167
|
+
operation: () => apiCallOrThrow('/api/notifications/mark-all-read', { method: 'PUT' }),
|
|
168
|
+
context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },
|
|
169
|
+
mutationPayload: {},
|
|
170
|
+
})
|
|
133
171
|
setNotifications((prev) =>
|
|
134
172
|
prev.map((n) =>
|
|
135
173
|
n.status === 'unread'
|
|
@@ -138,7 +176,7 @@ export function useNotificationActions(
|
|
|
138
176
|
),
|
|
139
177
|
)
|
|
140
178
|
setUnreadCount(0)
|
|
141
|
-
}, [setNotifications, setUnreadCount])
|
|
179
|
+
}, [runMutation, retryLastMutation, setNotifications, setUnreadCount])
|
|
142
180
|
|
|
143
181
|
React.useEffect(() => {
|
|
144
182
|
return () => {
|
|
@@ -72,6 +72,13 @@ describe('TagInput primitive', () => {
|
|
|
72
72
|
expect(screen.getByText('stay')).toBeInTheDocument()
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
+
it('localizes the remove button accessible name via removeTagLabel', () => {
|
|
76
|
+
render(<Harness initial={['Call']} removeTagLabel={(tag) => `Usuń ${tag}`} aria-label="tags" />)
|
|
77
|
+
expect(screen.queryByRole('button', { name: 'Remove Call' })).not.toBeInTheDocument()
|
|
78
|
+
fireEvent.click(screen.getByRole('button', { name: 'Usuń Call' }))
|
|
79
|
+
expect(screen.queryByText('Call')).not.toBeInTheDocument()
|
|
80
|
+
})
|
|
81
|
+
|
|
75
82
|
it('blocks further additions when maxTags is reached', () => {
|
|
76
83
|
render(<Harness initial={['a', 'b']} maxTags={2} aria-label="tags" />)
|
|
77
84
|
const input = getInput()
|
|
@@ -25,6 +25,15 @@ export type TagInputProps = {
|
|
|
25
25
|
* the Figma `Tag Input` Filled state with `Right Icon=true`.
|
|
26
26
|
*/
|
|
27
27
|
rightIcon?: InputProps['rightIcon']
|
|
28
|
+
/**
|
|
29
|
+
* Builds the accessible label for each tag's remove (×) button. Lets callers
|
|
30
|
+
* localize the action; defaults to the English `Remove {tag}`.
|
|
31
|
+
*/
|
|
32
|
+
removeTagLabel?: (tag: string) => string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function defaultRemoveTagLabel(tag: string): string {
|
|
36
|
+
return `Remove ${tag}`
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
function escapeRegExp(input: string): string {
|
|
@@ -58,6 +67,7 @@ export const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
|
|
58
67
|
'aria-label': ariaLabel,
|
|
59
68
|
'aria-invalid': ariaInvalid,
|
|
60
69
|
rightIcon,
|
|
70
|
+
removeTagLabel = defaultRemoveTagLabel,
|
|
61
71
|
},
|
|
62
72
|
ref,
|
|
63
73
|
) => {
|
|
@@ -199,7 +209,7 @@ export const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
|
|
199
209
|
shape="square"
|
|
200
210
|
disabled={disabled}
|
|
201
211
|
onRemove={() => removeTagAt(index)}
|
|
202
|
-
removeAriaLabel={
|
|
212
|
+
removeAriaLabel={removeTagLabel(tag)}
|
|
203
213
|
>
|
|
204
214
|
{tag}
|
|
205
215
|
</Tag>
|