@carlonicora/nextjs-jsonapi 3.19.1 → 3.20.0
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/{BlockNoteEditor-7VMBZI3V.js → BlockNoteEditor-2WFPGK64.js} +19 -19
- package/dist/{BlockNoteEditor-7VMBZI3V.js.map → BlockNoteEditor-2WFPGK64.js.map} +1 -1
- package/dist/{BlockNoteEditor-FX5RSUPA.mjs → BlockNoteEditor-CTRQ6JWE.mjs} +4 -4
- package/dist/{BlockNoteViewer-SVVAZ6TQ.js → BlockNoteViewer-XCGBX4ST.js} +3 -3
- package/dist/{BlockNoteViewer-SVVAZ6TQ.js.map → BlockNoteViewer-XCGBX4ST.js.map} +1 -1
- package/dist/{BlockNoteViewer-RK2RKOB7.mjs → BlockNoteViewer-ZY4ZF3HO.mjs} +2 -2
- package/dist/billing/index.js +401 -401
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-GNXZNUQV.mjs → chunk-CJHBL5AN.mjs} +1711 -1482
- package/dist/chunk-CJHBL5AN.mjs.map +1 -0
- package/dist/{chunk-YCFPSXLK.mjs → chunk-CK47KSFV.mjs} +6 -1
- package/dist/{chunk-YCFPSXLK.mjs.map → chunk-CK47KSFV.mjs.map} +1 -1
- package/dist/{chunk-RT3TM342.js → chunk-IQMJAYCJ.js} +1162 -933
- package/dist/chunk-IQMJAYCJ.js.map +1 -0
- package/dist/{chunk-TBSMAL5Y.js → chunk-MWZCCOTS.js} +6 -1
- package/dist/chunk-MWZCCOTS.js.map +1 -0
- package/dist/{chunk-ODH2LYWC.js → chunk-SYQMVTIC.js} +7 -7
- package/dist/{chunk-ODH2LYWC.js.map → chunk-SYQMVTIC.js.map} +1 -1
- package/dist/{chunk-IALPADOD.mjs → chunk-U55JQ4NY.mjs} +2 -2
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +24 -2
- package/dist/components/index.d.ts +24 -2
- package/dist/components/index.js +8 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +7 -3
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +3 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.js +101 -101
- package/dist/features/tokenusage/index.mjs +3 -3
- package/dist/index.js +3 -3
- package/dist/index.mjs +2 -2
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -1
- package/src/features/notification/components/containers/NotificationsListContainer.tsx +35 -18
- package/src/features/notification/components/filters/NotificationViewSwitcher.tsx +48 -0
- package/src/features/notification/components/filters/index.ts +1 -0
- package/src/features/notification/components/index.ts +1 -0
- package/src/features/notification/components/lists/NotificationsList.tsx +59 -104
- package/src/features/notification/components/modals/NotificationModal.tsx +97 -27
- package/src/features/notification/components/modals/__tests__/NotificationModal.spec.tsx +15 -4
- package/src/features/notification/components/notifications/Notification.tsx +19 -14
- package/src/features/notification/components/notifications/NotificationIcon.tsx +37 -0
- package/src/features/notification/components/notifications/index.ts +1 -0
- package/src/features/notification/data/notification.fields.ts +4 -0
- package/src/features/notification/hooks/useNotificationTableStructure.tsx +186 -0
- package/src/features/notification/notification.module.ts +2 -0
- package/dist/chunk-GNXZNUQV.mjs.map +0 -1
- package/dist/chunk-RT3TM342.js.map +0 -1
- package/dist/chunk-TBSMAL5Y.js.map +0 -1
- /package/dist/{BlockNoteEditor-FX5RSUPA.mjs.map → BlockNoteEditor-CTRQ6JWE.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-RK2RKOB7.mjs.map → BlockNoteViewer-ZY4ZF3HO.mjs.map} +0 -0
- /package/dist/{chunk-IALPADOD.mjs.map → chunk-U55JQ4NY.mjs.map} +0 -0
|
@@ -46,6 +46,15 @@ vi.mock("../../../../../contexts", () =>
|
|
|
46
46
|
);
|
|
47
47
|
|
|
48
48
|
vi.mock("../../../../../hooks", () => mocks.barrel({ usePageUrlGenerator: () => (p: unknown) => String(p) }));
|
|
49
|
+
|
|
50
|
+
// `Modules` is a Proxy that throws for any module the app has not registered,
|
|
51
|
+
// and the "view all" link reads `Modules.Notification` while rendering. Only
|
|
52
|
+
// that key is replaced — the rest of the barrel stays real, because the data
|
|
53
|
+
// layer imported alongside it still extends `AbstractApiData`.
|
|
54
|
+
vi.mock("../../../../../core", async () => {
|
|
55
|
+
const actual = await vi.importActual<Record<string, unknown>>("../../../../../core");
|
|
56
|
+
return { ...actual, Modules: { Notification: { name: "notifications", pageUrl: "/notifications" } } };
|
|
57
|
+
});
|
|
49
58
|
vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key }));
|
|
50
59
|
|
|
51
60
|
vi.mock("../../../contexts/NotificationContext", () => ({
|
|
@@ -66,15 +75,17 @@ vi.mock("../../../contexts/NotificationContext", () => ({
|
|
|
66
75
|
vi.mock("../../../../../shadcnui", () => {
|
|
67
76
|
const passthrough = ({ children }: { children?: React.ReactNode }) => <div>{children}</div>;
|
|
68
77
|
return mocks.barrel({
|
|
69
|
-
|
|
70
|
-
CardHeader: passthrough,
|
|
71
|
-
CardTitle: passthrough,
|
|
78
|
+
Link: passthrough,
|
|
72
79
|
Popover: passthrough,
|
|
73
80
|
PopoverContent: passthrough,
|
|
81
|
+
PopoverHeader: passthrough,
|
|
82
|
+
PopoverTitle: passthrough,
|
|
74
83
|
PopoverTrigger: passthrough,
|
|
75
84
|
ScrollArea: passthrough,
|
|
76
|
-
Separator: () => null,
|
|
77
85
|
SidebarMenuButton: passthrough,
|
|
86
|
+
Tooltip: passthrough,
|
|
87
|
+
TooltipContent: passthrough,
|
|
88
|
+
TooltipTrigger: passthrough,
|
|
78
89
|
});
|
|
79
90
|
});
|
|
80
91
|
|
|
@@ -4,9 +4,11 @@ import { useTranslations } from "next-intl";
|
|
|
4
4
|
import { ReactElement, useEffect, useState } from "react";
|
|
5
5
|
import { usePageUrlGenerator } from "../../../../hooks";
|
|
6
6
|
import { Link } from "../../../../shadcnui";
|
|
7
|
+
import { formatDate } from "../../../../utils";
|
|
7
8
|
import { UserInterface } from "../../../user";
|
|
8
9
|
import { UserAvatar } from "../../../user/components";
|
|
9
10
|
import { NotificationInterface } from "../../data";
|
|
11
|
+
import { NotificationIcon } from "./NotificationIcon";
|
|
10
12
|
|
|
11
13
|
type TaskCommentedOnProps = {
|
|
12
14
|
notification: NotificationInterface;
|
|
@@ -93,34 +95,37 @@ export function NotificationMenuItem({ notification, closePopover }: TaskComment
|
|
|
93
95
|
const data = generateNotificationData({ notification: notification, generateUrl: generateUrl });
|
|
94
96
|
|
|
95
97
|
const response = (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<p className="text-sm">
|
|
98
|
+
/* Unread is marked with a tinted row + dot, not the muted background the
|
|
99
|
+
row used to carry: grey-on-grey reads as disabled, the opposite of
|
|
100
|
+
"needs your attention". */
|
|
101
|
+
<div
|
|
102
|
+
className={`hover:bg-accent flex w-full flex-row items-start gap-3 px-3 py-2.5 transition-colors ${isRead ? "" : "bg-primary/5"}`}
|
|
103
|
+
>
|
|
104
|
+
<NotificationIcon notificationType={notification.notificationType} />
|
|
105
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
106
|
+
<p className="text-foreground text-sm">
|
|
106
107
|
{t.rich(`notification.${notification.notificationType}.description` as any, {
|
|
107
|
-
strong: (chunks: any) => <strong>{chunks}</strong>,
|
|
108
|
+
strong: (chunks: any) => <strong className="font-medium">{chunks}</strong>,
|
|
108
109
|
actor: data.actor?.name ?? "",
|
|
109
110
|
title: data.title,
|
|
110
111
|
message: notification.message ?? "",
|
|
111
112
|
})}
|
|
112
113
|
</p>
|
|
113
|
-
<div className="text-muted-foreground mt-
|
|
114
|
-
{new Date(notification.createdAt)
|
|
114
|
+
<div className="text-muted-foreground mt-0.5 text-xs">
|
|
115
|
+
{formatDate(new Date(notification.createdAt), "default")}
|
|
115
116
|
</div>
|
|
116
117
|
</div>
|
|
118
|
+
{!isRead && <span className="bg-primary mt-1.5 size-2 shrink-0 rounded-full" aria-hidden />}
|
|
117
119
|
</div>
|
|
118
120
|
);
|
|
119
121
|
|
|
120
122
|
if (!data.url) return response;
|
|
121
123
|
|
|
124
|
+
/* The whole row is the click target, so the package Link's text-primary
|
|
125
|
+
font-medium (typography role 14 — inline navigational text) would paint
|
|
126
|
+
every notification blue. Emphasis inside the sentence comes from weight. */
|
|
122
127
|
return (
|
|
123
|
-
<Link href={data.url} onClick={closePopover}>
|
|
128
|
+
<Link href={data.url} onClick={closePopover} className="text-foreground block font-normal">
|
|
124
129
|
{response}
|
|
125
130
|
</Link>
|
|
126
131
|
);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AudioLinesIcon, BellIcon, LucideIcon } from "lucide-react";
|
|
4
|
+
import { cn } from "../../../../utils";
|
|
5
|
+
|
|
6
|
+
type IconTone = "muted" | "warning" | "destructive";
|
|
7
|
+
|
|
8
|
+
/* Every row carries a glyph in the same 32px circle, which keeps the left rail
|
|
9
|
+
aligned and gives the eye a faster channel than reading the sentence. Types
|
|
10
|
+
not listed here fall back to the bell. */
|
|
11
|
+
const notificationIcons: Record<string, { icon: LucideIcon; tone: IconTone }> = {
|
|
12
|
+
transcript_ready: { icon: AudioLinesIcon, tone: "muted" },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const toneClasses: Record<IconTone, string> = {
|
|
16
|
+
muted: "bg-muted text-muted-foreground",
|
|
17
|
+
warning: "bg-warning/10 text-warning",
|
|
18
|
+
destructive: "bg-destructive/10 text-destructive",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type NotificationIconProps = {
|
|
22
|
+
notificationType: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function NotificationIcon({ notificationType, className }: NotificationIconProps) {
|
|
27
|
+
const { icon: Icon, tone } = notificationIcons[notificationType] ?? { icon: BellIcon, tone: "muted" as IconTone };
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
className={cn("flex size-8 shrink-0 items-center justify-center rounded-full", toneClasses[tone], className)}
|
|
32
|
+
aria-hidden
|
|
33
|
+
>
|
|
34
|
+
<Icon className="size-4" />
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ColumnDef } from "@tanstack/react-table";
|
|
4
|
+
import { ArchiveIcon } from "lucide-react";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import { registerTableGenerator, TableContent, usePageUrlGenerator, UseTableStructureHook } from "../../../hooks";
|
|
8
|
+
import { Button, Tooltip, TooltipContent, TooltipTrigger } from "../../../shadcnui";
|
|
9
|
+
import { formatDate } from "../../../utils";
|
|
10
|
+
import { generateNotificationData } from "../components/notifications/Notification";
|
|
11
|
+
import { useNotificationContext } from "../contexts/NotificationContext";
|
|
12
|
+
import { NotificationIcon } from "../components/notifications/NotificationIcon";
|
|
13
|
+
import { NotificationInterface } from "../data";
|
|
14
|
+
import { NotificationFields } from "../data/notification.fields";
|
|
15
|
+
import { NotificationService } from "../data/notification.service";
|
|
16
|
+
|
|
17
|
+
const NOTIFICATIONS_MODULE_NAME = "notifications";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Columns for the /notifications list.
|
|
21
|
+
*
|
|
22
|
+
* The row shape deliberately mirrors the notification popover row
|
|
23
|
+
* (`Notification.tsx` → `NotificationMenuItem`): the same `NotificationIcon` in
|
|
24
|
+
* a fixed first column, the same `t.rich(...)` sentence with `font-medium`
|
|
25
|
+
* emphasis, the same timestamp. Someone moving from the bell to the full page
|
|
26
|
+
* should recognise the same rows, not a different rendering of them.
|
|
27
|
+
*
|
|
28
|
+
* `context.archived` comes from `ContentListTable`'s `context` prop — the
|
|
29
|
+
* archive control is hidden in the archived tab, where it has nothing to do.
|
|
30
|
+
*/
|
|
31
|
+
export const useNotificationTableStructure: UseTableStructureHook<NotificationInterface, NotificationFields> = (
|
|
32
|
+
params,
|
|
33
|
+
) => {
|
|
34
|
+
const t = useTranslations();
|
|
35
|
+
const generateUrl = usePageUrlGenerator();
|
|
36
|
+
const { loadNotifications } = useNotificationContext();
|
|
37
|
+
|
|
38
|
+
const archived: boolean = params.context?.archived === true;
|
|
39
|
+
|
|
40
|
+
/* The shared context is refetched, not just this list's retriever: the bell
|
|
41
|
+
renders from that context, and the API already hides archived rows from
|
|
42
|
+
it. Dropping the row only here left the popover showing a notification the
|
|
43
|
+
page had just archived until the context's 5-minute cache expired. */
|
|
44
|
+
const archiveNotification = async (notification: NotificationInterface) => {
|
|
45
|
+
await NotificationService.archive({ id: notification.id });
|
|
46
|
+
params.dataRetriever?.removeElement(notification);
|
|
47
|
+
await loadNotifications();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const tableData = useMemo(() => {
|
|
51
|
+
return params.data.map((notification: NotificationInterface) => {
|
|
52
|
+
const entry: TableContent<NotificationInterface> = { jsonApiData: notification };
|
|
53
|
+
entry[NotificationFields.notificationId] = notification.id;
|
|
54
|
+
entry[NotificationFields.createdAt] = notification.createdAt;
|
|
55
|
+
return entry;
|
|
56
|
+
});
|
|
57
|
+
}, [params.data]);
|
|
58
|
+
|
|
59
|
+
const fieldColumnMap: Partial<Record<NotificationFields, () => any>> = {
|
|
60
|
+
[NotificationFields.icon]: () => ({
|
|
61
|
+
id: "icon",
|
|
62
|
+
accessorKey: "icon",
|
|
63
|
+
header: ``,
|
|
64
|
+
meta: { className: "w-20" },
|
|
65
|
+
cell: ({ row }: { row: TableContent<NotificationInterface> }) => {
|
|
66
|
+
const notification: NotificationInterface = row.original.jsonApiData;
|
|
67
|
+
|
|
68
|
+
/* `isRead` here is the value the row was FETCHED with, and it stays
|
|
69
|
+
that way: opening the page marks what it shows as read, so a dot
|
|
70
|
+
that reacted to that would blink out under the reader's eyes. It
|
|
71
|
+
means "was unread when you opened the page", and it leads the row so
|
|
72
|
+
the eye picks up what is new before reading anything else. */
|
|
73
|
+
return (
|
|
74
|
+
<div className="flex items-center gap-2">
|
|
75
|
+
{notification.isRead ? (
|
|
76
|
+
<span className="size-2 shrink-0" aria-hidden />
|
|
77
|
+
) : (
|
|
78
|
+
<Tooltip>
|
|
79
|
+
<TooltipTrigger render={<span className="bg-primary size-2 shrink-0 rounded-full" />}>
|
|
80
|
+
<span className="sr-only">{t(`notification.unread`)}</span>
|
|
81
|
+
</TooltipTrigger>
|
|
82
|
+
<TooltipContent>{t(`notification.unread`)}</TooltipContent>
|
|
83
|
+
</Tooltip>
|
|
84
|
+
)}
|
|
85
|
+
<NotificationIcon notificationType={notification.notificationType} />
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
enableSorting: false,
|
|
90
|
+
enableHiding: false,
|
|
91
|
+
}),
|
|
92
|
+
[NotificationFields.description]: () => ({
|
|
93
|
+
id: "description",
|
|
94
|
+
accessorKey: "description",
|
|
95
|
+
header: t(`notification.description`),
|
|
96
|
+
cell: ({ row }: { row: TableContent<NotificationInterface> }) => {
|
|
97
|
+
const notification: NotificationInterface = row.original.jsonApiData;
|
|
98
|
+
const data = generateNotificationData({ notification: notification, generateUrl: generateUrl });
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<p className="text-foreground text-sm">
|
|
102
|
+
{t.rich(`notification.${notification.notificationType}.description` as any, {
|
|
103
|
+
strong: (chunks: any) => <strong className="font-medium">{chunks}</strong>,
|
|
104
|
+
actor: data.actor?.name ?? "",
|
|
105
|
+
title: data.title,
|
|
106
|
+
message: notification.message ?? "",
|
|
107
|
+
})}
|
|
108
|
+
</p>
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
enableSorting: false,
|
|
112
|
+
enableHiding: false,
|
|
113
|
+
}),
|
|
114
|
+
[NotificationFields.createdAt]: () => ({
|
|
115
|
+
id: "createdAt",
|
|
116
|
+
accessorKey: "createdAt",
|
|
117
|
+
header: t(`notification.createdAt`),
|
|
118
|
+
meta: { className: "w-40" },
|
|
119
|
+
cell: ({ row }: { row: TableContent<NotificationInterface> }) => {
|
|
120
|
+
const notification: NotificationInterface = row.original.jsonApiData;
|
|
121
|
+
|
|
122
|
+
/* Not `cellDate`: that renders the absolute shape, and the popover row
|
|
123
|
+
shows how long ago it arrived. The two must match. */
|
|
124
|
+
return (
|
|
125
|
+
<span className="text-muted-foreground text-xs">
|
|
126
|
+
{formatDate(new Date(notification.createdAt), "default")}
|
|
127
|
+
</span>
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
enableSorting: false,
|
|
131
|
+
enableHiding: false,
|
|
132
|
+
}),
|
|
133
|
+
[NotificationFields.actions]: () => ({
|
|
134
|
+
id: "actions",
|
|
135
|
+
accessorKey: "actions",
|
|
136
|
+
header: ``,
|
|
137
|
+
meta: { className: "w-16" },
|
|
138
|
+
cell: ({ row }: { row: TableContent<NotificationInterface> }) => {
|
|
139
|
+
const notification: NotificationInterface = row.original.jsonApiData;
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div className="flex flex-row items-center justify-end gap-x-1">
|
|
143
|
+
{!archived && (
|
|
144
|
+
<Tooltip>
|
|
145
|
+
<TooltipTrigger
|
|
146
|
+
render={
|
|
147
|
+
<Button
|
|
148
|
+
variant={`link`}
|
|
149
|
+
data-testid="notification-archive-button"
|
|
150
|
+
onClick={(e) => {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
e.stopPropagation();
|
|
153
|
+
archiveNotification(notification);
|
|
154
|
+
}}
|
|
155
|
+
className="text-muted-foreground hover:text-destructive"
|
|
156
|
+
/>
|
|
157
|
+
}
|
|
158
|
+
>
|
|
159
|
+
<ArchiveIcon className="h-4 w-4 cursor-pointer" />
|
|
160
|
+
</TooltipTrigger>
|
|
161
|
+
<TooltipContent>{t(`notification.buttons.archive`)}</TooltipContent>
|
|
162
|
+
</Tooltip>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
enableSorting: false,
|
|
168
|
+
enableHiding: false,
|
|
169
|
+
}),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const columns = useMemo(() => {
|
|
173
|
+
return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
|
|
174
|
+
TableContent<NotificationInterface>
|
|
175
|
+
>[];
|
|
176
|
+
}, [params.fields, fieldColumnMap, t, generateUrl]);
|
|
177
|
+
|
|
178
|
+
return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/* Registered by module NAME, not by `Modules.Notification`: `Modules` is a
|
|
182
|
+
Proxy that throws for any module the app has not bootstrapped yet, and this
|
|
183
|
+
line runs while the module graph is still being evaluated — before
|
|
184
|
+
`configureJsonApi` has had a chance to register anything. The lookup in
|
|
185
|
+
`useTableGenerator` keys off `module.name`, so the two are equivalent. */
|
|
186
|
+
registerTableGenerator(NOTIFICATIONS_MODULE_NAME, useNotificationTableStructure);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BellIcon } from "lucide-react";
|
|
1
2
|
import { ModuleFactory } from "../../permissions";
|
|
2
3
|
import { Notification } from "./data/notification";
|
|
3
4
|
|
|
@@ -6,5 +7,6 @@ export const NotificationModule = (factory: ModuleFactory) =>
|
|
|
6
7
|
pageUrl: "/notifications",
|
|
7
8
|
name: "notifications",
|
|
8
9
|
model: Notification,
|
|
10
|
+
icon: BellIcon,
|
|
9
11
|
moduleId: "9259d704-c670-4e77-a3a1-a728ffc5be3d",
|
|
10
12
|
});
|