@tulip-systems/create-app 0.12.3 → 0.13.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/index.mjs +2866 -2949
- package/dist/index.mjs.map +1 -1
- package/dist/templates/basic/package.json +37 -38
- package/dist/templates/basic/src/app/layout.tsx +20 -10
- package/dist/templates/code/package.json +81 -71
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-black-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-black.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-light-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-light.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin.otf +0 -0
- package/dist/templates/code/public/images/preview/hero-home.webp +0 -0
- package/dist/templates/code/public/sw.js +35 -0
- package/dist/templates/code/src/app/admin/contacts/_components/grid.tsx +1 -1
- package/dist/templates/code/src/app/admin/customers/_config/columns-data.tsx +1 -1
- package/dist/templates/code/src/app/admin/employees/_config/columns-data.tsx +1 -1
- package/dist/templates/code/src/app/admin/history/[id]/layout.tsx +33 -0
- package/dist/templates/code/src/app/admin/history/[id]/loading.tsx +11 -0
- package/dist/templates/code/src/app/admin/history/[id]/page.tsx +30 -0
- package/dist/templates/code/src/app/admin/history/_components/diff.client.tsx +377 -0
- package/dist/templates/code/src/app/admin/history/_components/field-comparison.tsx +153 -0
- package/dist/templates/code/src/app/admin/history/_components/tables.tsx +47 -0
- package/dist/templates/code/src/app/admin/history/_components/toolbars.tsx +54 -0
- package/dist/templates/code/src/app/admin/history/_config/columns-data.tsx +61 -0
- package/dist/templates/code/src/app/admin/history/_config/filters.ts +46 -0
- package/dist/templates/code/src/app/admin/history/_config/sorting.ts +30 -0
- package/dist/templates/code/src/app/admin/history/_lib/helpers.ts +98 -0
- package/dist/templates/code/src/app/admin/history/_lib/router.ts +100 -0
- package/dist/templates/code/src/app/admin/history/_lib/schema.ts +45 -0
- package/dist/templates/code/src/app/admin/history/_lib/service.server.ts +30 -0
- package/dist/templates/code/src/app/admin/history/_lib/validators.ts +118 -0
- package/dist/templates/code/src/app/admin/initiatives/_config/columns-data.tsx +1 -1
- package/dist/templates/code/src/app/admin/notifications/_components/command-toggle-push.tsx +115 -0
- package/dist/templates/code/src/app/admin/notifications/_components/tables.tsx +37 -0
- package/dist/templates/code/src/app/admin/notifications/_components/toolbars.tsx +21 -0
- package/dist/templates/code/src/app/admin/notifications/_config/columns-data.tsx +64 -0
- package/dist/templates/code/src/app/admin/notifications/_config/commands.tsx +40 -0
- package/dist/templates/code/src/app/admin/notifications/_lib/listeners.server.ts +69 -0
- package/dist/templates/code/src/app/admin/notifications/_lib/push.server.ts +80 -0
- package/dist/templates/code/src/app/admin/notifications/_lib/router.ts +82 -0
- package/dist/templates/code/src/app/admin/notifications/_lib/schema.ts +49 -0
- package/dist/templates/code/src/app/admin/notifications/_lib/validators.ts +19 -0
- package/dist/templates/code/src/app/admin/notifications/layout.tsx +17 -0
- package/dist/templates/code/src/app/admin/notifications/loading.tsx +9 -0
- package/dist/templates/code/src/app/admin/notifications/page.tsx +28 -0
- package/dist/templates/code/src/app/admin/projects/[id]/(default)/page.tsx +1 -1
- package/dist/templates/code/src/app/admin/projects/[id]/timeline/page.tsx +1 -1
- package/dist/templates/code/src/app/admin/projects/_config/columns-data.tsx +1 -1
- package/dist/templates/code/src/app/admin/projects/_lib/events.ts +64 -0
- package/dist/templates/code/src/app/admin/projects/_lib/listeners.server.ts +82 -0
- package/dist/templates/code/src/app/admin/projects/_lib/router.ts +145 -20
- package/dist/templates/code/src/app/admin/projects/timeline/page.tsx +1 -1
- package/dist/templates/code/src/app/admin/prospects/_config/columns-data.tsx +1 -1
- package/dist/templates/code/src/app/admin/queue/_components/create-command.tsx +97 -0
- package/dist/templates/code/src/app/admin/queue/_components/tables.tsx +37 -0
- package/dist/templates/code/src/app/admin/queue/_components/toolbars.tsx +16 -0
- package/dist/templates/code/src/app/admin/queue/_config/columns-data.tsx +44 -0
- package/dist/templates/code/src/app/admin/queue/_config/commands.tsx +39 -0
- package/dist/templates/code/src/app/admin/queue/_lib/queue.ts +27 -0
- package/dist/templates/code/src/app/admin/queue/_lib/router.ts +33 -0
- package/dist/templates/code/src/app/admin/queue/_lib/schema.ts +12 -0
- package/dist/templates/code/src/app/admin/queue/_lib/validators.ts +15 -0
- package/dist/templates/code/src/app/admin/quotations/(list)/layout.tsx +24 -0
- package/dist/templates/code/src/app/admin/quotations/(list)/page.tsx +23 -0
- package/dist/templates/code/src/app/admin/quotations/[id]/(dashboard)/layout.tsx +40 -0
- package/dist/templates/code/src/app/admin/quotations/[id]/(dashboard)/page.tsx +56 -0
- package/dist/templates/code/src/app/admin/quotations/[id]/preview/page.tsx +17 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/commands.tsx +24 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/hero/config.ts +14 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/hero/create.tsx +152 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/hero/inline.tsx +90 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/hero/preview.tsx +90 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/registry.ts +13 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/render.tsx +190 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/styles.ts +24 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/summary/config.ts +13 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/summary/create.tsx +175 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/summary/inline.tsx +93 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/summary/preview.tsx +25 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/totals/config.ts +18 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/totals/create.tsx +177 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/totals/inline.tsx +96 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/totals/preview.tsx +32 -0
- package/dist/templates/code/src/app/admin/quotations/_components/create-command.tsx +97 -0
- package/dist/templates/code/src/app/admin/quotations/_components/tables.tsx +68 -0
- package/dist/templates/code/src/app/admin/quotations/_components/toolbars.tsx +18 -0
- package/dist/templates/code/src/app/admin/quotations/_config/columns-data.tsx +35 -0
- package/dist/templates/code/src/app/admin/quotations/_config/commands.tsx +71 -0
- package/dist/templates/code/src/app/admin/quotations/_config/filters.ts +22 -0
- package/dist/templates/code/src/app/admin/quotations/_config/sorting.ts +23 -0
- package/dist/templates/code/src/app/admin/quotations/_lib/router.ts +260 -0
- package/dist/templates/code/src/app/admin/quotations/_lib/schema.ts +42 -0
- package/dist/templates/code/src/app/admin/quotations/_lib/validators.ts +20 -0
- package/dist/templates/code/src/app/admin/settings/history/layout.tsx +6 -0
- package/dist/templates/code/src/app/admin/settings/history/loading.tsx +5 -0
- package/dist/templates/code/src/app/admin/settings/history/page.tsx +13 -0
- package/dist/templates/code/src/app/admin/settings/layout.tsx +11 -0
- package/dist/templates/code/src/app/admin/settings/queue/loading.tsx +9 -0
- package/dist/templates/code/src/app/admin/settings/queue/page.tsx +21 -0
- package/dist/templates/code/src/app/admin/tasks/_components/domains-tabs-grid.tsx +1 -1
- package/dist/templates/code/src/app/admin/tasks/_config/columns-data.tsx +1 -1
- package/dist/templates/code/src/app/admin/tasks/_config/columns-inline.tsx +1 -1
- package/dist/templates/code/src/app/admin/tasks/_lib/events.ts +40 -0
- package/dist/templates/code/src/app/admin/tasks/_lib/listeners.server.ts +46 -0
- package/dist/templates/code/src/app/admin/tasks/_lib/router.ts +222 -90
- package/dist/templates/code/src/app/admin/time/_config/columns-data.tsx +1 -1
- package/dist/templates/code/src/app/admin/time/_config/columns-inline.tsx +1 -1
- package/dist/templates/code/src/app/api/[transport]/route.ts +31 -21
- package/dist/templates/code/src/app/api/queue/dispatch/route.ts +4 -0
- package/dist/templates/code/src/app/auth/forget-password/page.tsx +1 -1
- package/dist/templates/code/src/app/auth/layout.tsx +1 -1
- package/dist/templates/code/src/app/auth/reset-password/page.tsx +1 -1
- package/dist/templates/code/src/app/layout.tsx +25 -13
- package/dist/templates/code/src/env.ts +12 -0
- package/dist/templates/code/src/lib/config/paths.tsx +16 -0
- package/dist/templates/code/src/lib/utils/fonts.ts +102 -0
- package/dist/templates/code/src/server/auth/constants.ts +1 -1
- package/dist/templates/code/src/server/auth/init.ts +12 -5
- package/dist/templates/code/src/server/auth/permissions.ts +7 -0
- package/dist/templates/code/src/server/auth/schema.ts +100 -1
- package/dist/templates/code/src/server/db/schema.ts +4 -0
- package/dist/templates/code/src/server/events/handlers.ts +17 -0
- package/dist/templates/code/src/server/events/init.ts +18 -0
- package/dist/templates/code/src/server/events/listeners.ts +17 -0
- package/dist/templates/code/src/server/queue/handlers.ts +5 -0
- package/dist/templates/code/src/server/queue/init.ts +16 -0
- package/dist/templates/code/src/server/router/router.ts +23 -0
- package/dist/templates/spark/package.json +64 -65
- package/dist/templates/spark/src/app/admin/assignments/[id]/(default)/@dashboard/page.tsx +1 -1
- package/dist/templates/spark/src/app/admin/assignments/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/companies/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/contacts/_components/grid.tsx +1 -1
- package/dist/templates/spark/src/app/admin/customers/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/drive/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/emails/_components/send-email.tsx +1 -1
- package/dist/templates/spark/src/app/admin/emails/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/employees/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/executions/[id]/@dashboard/page.tsx +1 -1
- package/dist/templates/spark/src/app/admin/executions/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/products/[id]/(preview)/page.tsx +1 -1
- package/dist/templates/spark/src/app/admin/products/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/projects/[id]/(dashboard)/page.tsx +1 -1
- package/dist/templates/spark/src/app/admin/projects/[id]/messages/page.tsx +1 -1
- package/dist/templates/spark/src/app/admin/projects/[id]/statistics/page.tsx +1 -1
- package/dist/templates/spark/src/app/admin/projects/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/purchase-order-items/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/purchase-order-items/_config/columns-inline.tsx +1 -1
- package/dist/templates/spark/src/app/admin/purchase-order-items/_config/commands.tsx +1 -1
- package/dist/templates/spark/src/app/admin/purchase-orders/[id]/(default)/@dashboard/page.tsx +1 -1
- package/dist/templates/spark/src/app/admin/purchase-orders/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/quotation-items/_config/commands.tsx +1 -1
- package/dist/templates/spark/src/app/admin/quotations/[id]/(default)/@dashboard/page.tsx +1 -1
- package/dist/templates/spark/src/app/admin/quotations/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/run-tasks/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/runs/[id]/@dashboard/page.tsx +1 -1
- package/dist/templates/spark/src/app/admin/runs/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/sales-invoices/[id]/@dashboard/page.tsx +1 -1
- package/dist/templates/spark/src/app/admin/sales-invoices/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/sales-order-executions/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/sales-order-executions/_config/columns-inline.tsx +1 -1
- package/dist/templates/spark/src/app/admin/sales-order-executions/_config/commands.tsx +1 -1
- package/dist/templates/spark/src/app/admin/sales-order-items/_config/commands.tsx +1 -1
- package/dist/templates/spark/src/app/admin/sales-orders/[id]/(default)/@dashboard/page.tsx +1 -1
- package/dist/templates/spark/src/app/admin/sales-orders/[id]/(default)/layout.tsx +1 -1
- package/dist/templates/spark/src/app/admin/sales-orders/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/schedule/_components/schedule-card.tsx +1 -1
- package/dist/templates/spark/src/app/admin/schedule/_components/schedule-month-view.tsx +1 -1
- package/dist/templates/spark/src/app/admin/suppliers/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/tasks/_components/subproject-selector.tsx +1 -1
- package/dist/templates/spark/src/app/admin/tasks/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/tasks/_config/columns-inline.tsx +1 -1
- package/dist/templates/spark/src/app/admin/tasks/_config/commands.tsx +1 -1
- package/dist/templates/spark/src/app/admin/technical-sheets/[id]/page.tsx +1 -1
- package/dist/templates/spark/src/app/admin/technical-sheets/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/admin/technical-sheets/_config/commands.tsx +1 -1
- package/dist/templates/spark/src/app/admin/templates/_config/columns-data.tsx +1 -1
- package/dist/templates/spark/src/app/auth/layout.tsx +1 -1
- package/dist/templates/spark/src/app/layout.tsx +25 -10
- package/dist/templates/spark/src/server/auth/init.ts +1 -0
- package/package.json +6 -6
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { formatDateTime } from "@tulip-systems/core";
|
|
4
|
+
import { Link } from "@tulip-systems/core/navigation";
|
|
5
|
+
import { TableCell, type TableColumnDef, TableColumnHeader } from "@tulip-systems/data-tables";
|
|
6
|
+
import { createTableSelectCell } from "@tulip-systems/data-tables/client";
|
|
7
|
+
import type { Notification } from "../_lib/validators";
|
|
8
|
+
|
|
9
|
+
export const notificationColumns: TableColumnDef<Notification>[] = [
|
|
10
|
+
createTableSelectCell(),
|
|
11
|
+
{
|
|
12
|
+
accessorKey: "title",
|
|
13
|
+
header: ({ column }) => <TableColumnHeader column={column} title="Title" />,
|
|
14
|
+
cell: ({ row }) => (
|
|
15
|
+
<TableCell>
|
|
16
|
+
<div className="font-medium">{row.original.title}</div>
|
|
17
|
+
<div className="max-w-[520px] truncate text-muted-foreground text-xs">
|
|
18
|
+
{row.original.body}
|
|
19
|
+
</div>
|
|
20
|
+
</TableCell>
|
|
21
|
+
),
|
|
22
|
+
enableSorting: false,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
accessorKey: "eventName",
|
|
26
|
+
header: ({ column }) => <TableColumnHeader column={column} title="Event" />,
|
|
27
|
+
cell: ({ row }) => <TableCell>{row.original.eventName}</TableCell>,
|
|
28
|
+
enableSorting: false,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
accessorKey: "listenerKey",
|
|
32
|
+
header: ({ column }) => <TableColumnHeader column={column} title="Listener" />,
|
|
33
|
+
cell: ({ row }) => <TableCell>{row.original.listenerKey}</TableCell>,
|
|
34
|
+
enableSorting: false,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
accessorKey: "sourceHref",
|
|
38
|
+
header: ({ column }) => <TableColumnHeader column={column} title="Source" />,
|
|
39
|
+
cell: ({ row }) => (
|
|
40
|
+
<TableCell>
|
|
41
|
+
{row.original.sourceHref ? (
|
|
42
|
+
<Link href={row.original.sourceHref} className="hover:underline">
|
|
43
|
+
{row.original.source}
|
|
44
|
+
</Link>
|
|
45
|
+
) : (
|
|
46
|
+
row.original.source
|
|
47
|
+
)}
|
|
48
|
+
</TableCell>
|
|
49
|
+
),
|
|
50
|
+
enableSorting: false,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
accessorKey: "occurredAt",
|
|
54
|
+
header: ({ column }) => <TableColumnHeader column={column} title="Occurred" />,
|
|
55
|
+
cell: ({ row }) => <TableCell>{formatDateTime(row.original.occurredAt)}</TableCell>,
|
|
56
|
+
enableSorting: false,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
accessorKey: "readAt",
|
|
60
|
+
header: ({ column }) => <TableColumnHeader column={column} title="Status" />,
|
|
61
|
+
cell: ({ row }) => <TableCell>{row.original.readAt ? "Read" : "Unread"}</TableCell>,
|
|
62
|
+
enableSorting: false,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { commandBuilder } from "@tulip-systems/commands";
|
|
4
|
+
import { CommandClick, CommandLabel, DeleteCommand } from "@tulip-systems/commands/client";
|
|
5
|
+
import { toast } from "@tulip-systems/ui/sonner";
|
|
6
|
+
import { CheckIcon } from "lucide-react";
|
|
7
|
+
import { orpc } from "@/server/router/client";
|
|
8
|
+
import { CommandTogglePush } from "../_components/command-toggle-push";
|
|
9
|
+
import type { Notification } from "../_lib/validators";
|
|
10
|
+
|
|
11
|
+
export const notificationTogglePushCommand = commandBuilder.render(() => <CommandTogglePush />);
|
|
12
|
+
|
|
13
|
+
export const notificationMarkAsReadCommand = commandBuilder
|
|
14
|
+
.$type<Notification[]>()
|
|
15
|
+
.visibleWhen(({ data }) => data.some((item) => !item.readAt))
|
|
16
|
+
.render(({ data }) => (
|
|
17
|
+
<CommandClick
|
|
18
|
+
label="Mark as read"
|
|
19
|
+
variables={{ ids: data.map((item) => item.id) }}
|
|
20
|
+
mutation={orpc.notifications.markAsRead.mutationOptions({
|
|
21
|
+
meta: { invalidates: [orpc.notifications.list.key()] },
|
|
22
|
+
onSuccess: () => toast.success("Notifications marked as read"),
|
|
23
|
+
})}
|
|
24
|
+
>
|
|
25
|
+
<CheckIcon className="size-4" />
|
|
26
|
+
<CommandLabel />
|
|
27
|
+
</CommandClick>
|
|
28
|
+
));
|
|
29
|
+
|
|
30
|
+
export const notificationDeleteCommand = commandBuilder
|
|
31
|
+
.$type<Notification[]>()
|
|
32
|
+
.render(({ data }) => (
|
|
33
|
+
<DeleteCommand
|
|
34
|
+
variables={{ ids: data.map((item) => item.id) }}
|
|
35
|
+
mutation={orpc.notifications.delete.mutationOptions({
|
|
36
|
+
meta: { invalidates: [orpc.notifications.list.key()] },
|
|
37
|
+
onSuccess: () => toast.success("Notifications deleted"),
|
|
38
|
+
})}
|
|
39
|
+
/>
|
|
40
|
+
));
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import "server-cli-only";
|
|
2
|
+
|
|
3
|
+
import { projectCreatedEvent, projectUpdatedEvent } from "@/app/admin/projects/_lib/events";
|
|
4
|
+
import { taskCreatedEvent } from "@/app/admin/tasks/_lib/events";
|
|
5
|
+
import { db } from "@/server/db/init";
|
|
6
|
+
import { sendPushNotification } from "./push.server";
|
|
7
|
+
import { notifications } from "./schema";
|
|
8
|
+
|
|
9
|
+
export const notificationEventListeners = {
|
|
10
|
+
projectCreated: projectCreatedEvent.listen(async ({ payload, event }) => {
|
|
11
|
+
const [notification] = await db
|
|
12
|
+
.insert(notifications)
|
|
13
|
+
.values({
|
|
14
|
+
title: "Project created",
|
|
15
|
+
body: `${payload.title} was created with status ${payload.status}.`,
|
|
16
|
+
eventId: event.id,
|
|
17
|
+
eventName: event.name,
|
|
18
|
+
eventVersion: event.version,
|
|
19
|
+
listenerKey: "notifications:projectCreated",
|
|
20
|
+
source: "project",
|
|
21
|
+
sourceId: payload.projectId,
|
|
22
|
+
sourceHref: `/admin/projects/${payload.projectId}`,
|
|
23
|
+
occurredAt: new Date(event.occurredAt),
|
|
24
|
+
})
|
|
25
|
+
.returning();
|
|
26
|
+
|
|
27
|
+
if (notification) await sendPushNotification(notification);
|
|
28
|
+
}),
|
|
29
|
+
projectStatusChanged: projectUpdatedEvent.listen(async ({ payload, event }) => {
|
|
30
|
+
if (payload.before.status === payload.after.status) return;
|
|
31
|
+
|
|
32
|
+
const [notification] = await db
|
|
33
|
+
.insert(notifications)
|
|
34
|
+
.values({
|
|
35
|
+
title: "Project status changed",
|
|
36
|
+
body: `${payload.title} moved from ${payload.before.status} to ${payload.after.status}.`,
|
|
37
|
+
eventId: event.id,
|
|
38
|
+
eventName: event.name,
|
|
39
|
+
eventVersion: event.version,
|
|
40
|
+
listenerKey: "notifications:projectStatusChanged",
|
|
41
|
+
source: "project",
|
|
42
|
+
sourceId: payload.projectId,
|
|
43
|
+
sourceHref: `/admin/projects/${payload.projectId}`,
|
|
44
|
+
occurredAt: new Date(event.occurredAt),
|
|
45
|
+
})
|
|
46
|
+
.returning();
|
|
47
|
+
|
|
48
|
+
if (notification) await sendPushNotification(notification);
|
|
49
|
+
}),
|
|
50
|
+
taskCreated: taskCreatedEvent.listen(async ({ payload, event }) => {
|
|
51
|
+
const [notification] = await db
|
|
52
|
+
.insert(notifications)
|
|
53
|
+
.values({
|
|
54
|
+
title: "Task created",
|
|
55
|
+
body: `${payload.code} ${payload.title} was added to ${payload.projectTitle}.`,
|
|
56
|
+
eventId: event.id,
|
|
57
|
+
eventName: event.name,
|
|
58
|
+
eventVersion: event.version,
|
|
59
|
+
listenerKey: "notifications:taskCreated",
|
|
60
|
+
source: "task",
|
|
61
|
+
sourceId: payload.taskId,
|
|
62
|
+
sourceHref: `/admin/tasks/${payload.taskId}`,
|
|
63
|
+
occurredAt: new Date(event.occurredAt),
|
|
64
|
+
})
|
|
65
|
+
.returning();
|
|
66
|
+
|
|
67
|
+
if (notification) await sendPushNotification(notification);
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import "server-cli-only";
|
|
2
|
+
|
|
3
|
+
import { eq } from "drizzle-orm";
|
|
4
|
+
import webpush from "web-push";
|
|
5
|
+
import { env } from "@/env";
|
|
6
|
+
import { db } from "@/server/db/init";
|
|
7
|
+
import { notificationPushSubscriptions } from "./schema";
|
|
8
|
+
import type { Notification } from "./validators";
|
|
9
|
+
|
|
10
|
+
const publicKey = env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY;
|
|
11
|
+
const privateKey = env.WEB_PUSH_PRIVATE_KEY;
|
|
12
|
+
const subject = env.WEB_PUSH_SUBJECT;
|
|
13
|
+
|
|
14
|
+
const isWebPushConfigured = Boolean(publicKey && privateKey && subject);
|
|
15
|
+
|
|
16
|
+
if (publicKey && privateKey && subject) {
|
|
17
|
+
webpush.setVapidDetails(subject, publicKey, privateKey);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns the browser-facing Web Push configuration.
|
|
22
|
+
*
|
|
23
|
+
* The client uses this to know whether push is available in the current
|
|
24
|
+
* environment and which public VAPID key to pass to the browser subscription.
|
|
25
|
+
*/
|
|
26
|
+
export function getWebPushConfig() {
|
|
27
|
+
return {
|
|
28
|
+
publicKey: publicKey ?? null,
|
|
29
|
+
configured: isWebPushConfigured,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Sends a notification row to every stored browser push subscription.
|
|
35
|
+
*
|
|
36
|
+
* This is a best-effort side effect: if VAPID keys are not configured, it no-ops
|
|
37
|
+
* so database notifications keep working. Expired browser subscriptions are
|
|
38
|
+
* deleted when the push service reports them as gone.
|
|
39
|
+
*/
|
|
40
|
+
export async function sendPushNotification(notification: Notification) {
|
|
41
|
+
if (!isWebPushConfigured) return;
|
|
42
|
+
|
|
43
|
+
const subscriptions = await db.query.notificationPushSubscriptions.findMany();
|
|
44
|
+
if (subscriptions.length === 0) return;
|
|
45
|
+
|
|
46
|
+
const payload = JSON.stringify({
|
|
47
|
+
title: notification.title,
|
|
48
|
+
body: notification.body,
|
|
49
|
+
url: notification.sourceHref ?? "/admin/notifications",
|
|
50
|
+
notificationId: notification.id,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await Promise.all(
|
|
54
|
+
subscriptions.map(async (subscription) => {
|
|
55
|
+
try {
|
|
56
|
+
await webpush.sendNotification(
|
|
57
|
+
{
|
|
58
|
+
endpoint: subscription.endpoint,
|
|
59
|
+
keys: {
|
|
60
|
+
p256dh: subscription.p256dh,
|
|
61
|
+
auth: subscription.auth,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
payload,
|
|
65
|
+
);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const statusCode = (error as { statusCode?: number }).statusCode;
|
|
68
|
+
|
|
69
|
+
if (statusCode === 404 || statusCode === 410) {
|
|
70
|
+
await db
|
|
71
|
+
.delete(notificationPushSubscriptions)
|
|
72
|
+
.where(eq(notificationPushSubscriptions.endpoint, subscription.endpoint));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.error("[notifications] Failed to send Web Push notification", error);
|
|
77
|
+
}
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import "server-cli-only";
|
|
2
|
+
|
|
3
|
+
import { ORPCError } from "@orpc/server";
|
|
4
|
+
import { and, desc, eq, inArray } from "drizzle-orm";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { protectedProcedure } from "@/server/router/procedures";
|
|
7
|
+
import { getWebPushConfig } from "./push.server";
|
|
8
|
+
import { notificationPushSubscriptions, notifications } from "./schema";
|
|
9
|
+
import { pushSubscriptionSchema } from "./validators";
|
|
10
|
+
|
|
11
|
+
export const notificationsRouter = {
|
|
12
|
+
list: protectedProcedure.handler(async ({ context }) =>
|
|
13
|
+
context.db.query.notifications.findMany({
|
|
14
|
+
orderBy: [desc(notifications.createdAt)],
|
|
15
|
+
limit: 100,
|
|
16
|
+
}),
|
|
17
|
+
),
|
|
18
|
+
getPushConfig: protectedProcedure.handler(() => getWebPushConfig()),
|
|
19
|
+
subscribePush: protectedProcedure
|
|
20
|
+
.input(pushSubscriptionSchema)
|
|
21
|
+
.handler(async ({ context, input }) => {
|
|
22
|
+
const config = getWebPushConfig();
|
|
23
|
+
|
|
24
|
+
if (!config.configured) {
|
|
25
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
26
|
+
message: "Web Push is not configured for this environment",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const now = new Date();
|
|
31
|
+
const expiresAt = input.expirationTime ? new Date(input.expirationTime) : null;
|
|
32
|
+
|
|
33
|
+
await context.db
|
|
34
|
+
.insert(notificationPushSubscriptions)
|
|
35
|
+
.values({
|
|
36
|
+
userId: context.user.id,
|
|
37
|
+
endpoint: input.endpoint,
|
|
38
|
+
p256dh: input.keys.p256dh,
|
|
39
|
+
auth: input.keys.auth,
|
|
40
|
+
userAgent: context.headers.get("user-agent"),
|
|
41
|
+
lastSeenAt: now,
|
|
42
|
+
expiresAt,
|
|
43
|
+
})
|
|
44
|
+
.onConflictDoUpdate({
|
|
45
|
+
target: notificationPushSubscriptions.endpoint,
|
|
46
|
+
set: {
|
|
47
|
+
userId: context.user.id,
|
|
48
|
+
p256dh: input.keys.p256dh,
|
|
49
|
+
auth: input.keys.auth,
|
|
50
|
+
userAgent: context.headers.get("user-agent"),
|
|
51
|
+
lastSeenAt: now,
|
|
52
|
+
expiresAt,
|
|
53
|
+
updatedAt: now,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}),
|
|
57
|
+
unsubscribePush: protectedProcedure
|
|
58
|
+
.input(z.object({ endpoint: z.url() }))
|
|
59
|
+
.handler(async ({ context, input }) => {
|
|
60
|
+
await context.db
|
|
61
|
+
.delete(notificationPushSubscriptions)
|
|
62
|
+
.where(
|
|
63
|
+
and(
|
|
64
|
+
eq(notificationPushSubscriptions.endpoint, input.endpoint),
|
|
65
|
+
eq(notificationPushSubscriptions.userId, context.user.id),
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
}),
|
|
69
|
+
markAsRead: protectedProcedure
|
|
70
|
+
.input(z.object({ ids: z.array(z.string()) }))
|
|
71
|
+
.handler(async ({ context, input }) => {
|
|
72
|
+
await context.db
|
|
73
|
+
.update(notifications)
|
|
74
|
+
.set({ readAt: new Date() })
|
|
75
|
+
.where(inArray(notifications.id, input.ids));
|
|
76
|
+
}),
|
|
77
|
+
delete: protectedProcedure
|
|
78
|
+
.input(z.object({ ids: z.array(z.string()) }))
|
|
79
|
+
.handler(async ({ context, input }) => {
|
|
80
|
+
await context.db.delete(notifications).where(inArray(notifications.id, input.ids));
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { relations } from "drizzle-orm";
|
|
2
|
+
import { pgTable } from "drizzle-orm/pg-core";
|
|
3
|
+
import { users } from "@/server/auth/schema";
|
|
4
|
+
import { baseColumns } from "@/server/db/helpers";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Notifications
|
|
8
|
+
*/
|
|
9
|
+
export const notifications = pgTable("notifications", (t) => ({
|
|
10
|
+
...baseColumns,
|
|
11
|
+
title: t.varchar().notNull(),
|
|
12
|
+
body: t.text().notNull(),
|
|
13
|
+
eventId: t.varchar().notNull(),
|
|
14
|
+
eventName: t.varchar().notNull(),
|
|
15
|
+
eventVersion: t.integer().notNull(),
|
|
16
|
+
listenerKey: t.varchar().notNull(),
|
|
17
|
+
source: t.varchar().notNull(),
|
|
18
|
+
sourceId: t.uuid().notNull(),
|
|
19
|
+
sourceHref: t.varchar(),
|
|
20
|
+
occurredAt: t.timestamp().notNull(),
|
|
21
|
+
readAt: t.timestamp(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Notification Push Subscriptions
|
|
26
|
+
*/
|
|
27
|
+
export const notificationPushSubscriptions = pgTable("notification_push_subscriptions", (t) => ({
|
|
28
|
+
...baseColumns,
|
|
29
|
+
userId: t
|
|
30
|
+
.uuid()
|
|
31
|
+
.notNull()
|
|
32
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
33
|
+
endpoint: t.text().notNull().unique(),
|
|
34
|
+
p256dh: t.text().notNull(),
|
|
35
|
+
auth: t.text().notNull(),
|
|
36
|
+
userAgent: t.text(),
|
|
37
|
+
lastSeenAt: t.timestamp().defaultNow().notNull(),
|
|
38
|
+
expiresAt: t.timestamp(),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
export const notificationPushSubscriptionsRelations = relations(
|
|
42
|
+
notificationPushSubscriptions,
|
|
43
|
+
({ one }) => ({
|
|
44
|
+
user: one(users, {
|
|
45
|
+
fields: [notificationPushSubscriptions.userId],
|
|
46
|
+
references: [users.id],
|
|
47
|
+
}),
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { notifications } from "./schema";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Notification
|
|
6
|
+
*/
|
|
7
|
+
export type Notification = typeof notifications.$inferSelect;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Push Subscription Schema
|
|
11
|
+
*/
|
|
12
|
+
export const pushSubscriptionSchema = z.object({
|
|
13
|
+
endpoint: z.url(),
|
|
14
|
+
expirationTime: z.number().nullable().optional(),
|
|
15
|
+
keys: z.object({
|
|
16
|
+
p256dh: z.string().min(1),
|
|
17
|
+
auth: z.string().min(1),
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Header, Topbar, TopbarTitle } from "@tulip-systems/app/components/client";
|
|
2
|
+
import { TableLayout } from "@tulip-systems/data-tables";
|
|
3
|
+
import type { PropsWithChildren } from "react";
|
|
4
|
+
|
|
5
|
+
export default function Layout({ children }: PropsWithChildren) {
|
|
6
|
+
return (
|
|
7
|
+
<>
|
|
8
|
+
<Header>
|
|
9
|
+
<Topbar>
|
|
10
|
+
<TopbarTitle breadcrumbs={[{ label: "Notifications" }]} />
|
|
11
|
+
</Topbar>
|
|
12
|
+
</Header>
|
|
13
|
+
|
|
14
|
+
<TableLayout className="p-content">{children}</TableLayout>
|
|
15
|
+
</>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { InlineCommandMenu, InlineCommandMenuLoading } from "@tulip-systems/commands/client";
|
|
2
|
+
import { DataTable } from "@tulip-systems/data-tables/client";
|
|
3
|
+
import { Suspense } from "react";
|
|
4
|
+
import { NotificationTableProvider } from "./_components/tables";
|
|
5
|
+
import { NotificationTableToolbar } from "./_components/toolbars";
|
|
6
|
+
import { notificationColumns } from "./_config/columns-data";
|
|
7
|
+
import {
|
|
8
|
+
notificationDeleteCommand,
|
|
9
|
+
notificationMarkAsReadCommand,
|
|
10
|
+
notificationTogglePushCommand,
|
|
11
|
+
} from "./_config/commands";
|
|
12
|
+
|
|
13
|
+
export default function Page() {
|
|
14
|
+
return (
|
|
15
|
+
<NotificationTableProvider
|
|
16
|
+
columns={notificationColumns}
|
|
17
|
+
commands={[notificationMarkAsReadCommand, notificationDeleteCommand]}
|
|
18
|
+
>
|
|
19
|
+
<NotificationTableToolbar>
|
|
20
|
+
<Suspense fallback={<InlineCommandMenuLoading />}>
|
|
21
|
+
<InlineCommandMenu data={null} commands={[notificationTogglePushCommand]} />
|
|
22
|
+
</Suspense>
|
|
23
|
+
</NotificationTableToolbar>
|
|
24
|
+
|
|
25
|
+
<DataTable />
|
|
26
|
+
</NotificationTableProvider>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useSuspenseQueries, useSuspenseQuery } from "@tanstack/react-query";
|
|
4
|
+
import { Link } from "@tulip-systems/core/navigation";
|
|
4
5
|
import { EditorContent, EditorMenuFixed } from "@tulip-systems/editor/client";
|
|
5
6
|
import {
|
|
6
7
|
InlineComboboxDropdown,
|
|
@@ -20,7 +21,6 @@ import {
|
|
|
20
21
|
DataStackTitle,
|
|
21
22
|
DataStackValue,
|
|
22
23
|
} from "@tulip-systems/ui/data-stack";
|
|
23
|
-
import Link from "next/link";
|
|
24
24
|
import { notFound } from "next/navigation";
|
|
25
25
|
import { use } from "react";
|
|
26
26
|
import { z } from "zod";
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { Temporal } from "@js-temporal/polyfill";
|
|
4
4
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
5
|
+
import { Link } from "@tulip-systems/core/navigation";
|
|
5
6
|
import { Button } from "@tulip-systems/ui/button";
|
|
6
7
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@tulip-systems/ui/select";
|
|
7
8
|
import { findStatus } from "@tulip-systems/ui/status";
|
|
8
9
|
import { MinusIcon, PlusIcon } from "lucide-react";
|
|
9
|
-
import Link from "next/link";
|
|
10
10
|
import { parseAsStringEnum, useQueryState } from "nuqs";
|
|
11
11
|
import { use, useState } from "react";
|
|
12
12
|
import { initiativeStatusConfig } from "@/app/admin/initiatives/_config/status";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { Link } from "@tulip-systems/core/navigation";
|
|
3
4
|
import { type TableColumnDef, TableColumnHeader } from "@tulip-systems/data-tables";
|
|
4
5
|
import {
|
|
5
6
|
createTableSelectCell,
|
|
6
7
|
TableStatusCell,
|
|
7
8
|
TableTextCell,
|
|
8
9
|
} from "@tulip-systems/data-tables/client";
|
|
9
|
-
import Link from "next/link";
|
|
10
10
|
import type { Customer } from "../../customers/_lib/validators";
|
|
11
11
|
import type { Project } from "../_lib/validators";
|
|
12
12
|
import { ProjectStatusField } from "./status";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defineEvent } from "@tulip-systems/events";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { eventHistoryMetadata, historyJSONSchema } from "@/app/admin/history/_lib/validators";
|
|
4
|
+
import { projectStatusSchema } from "./validators";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Published after a project is created. Carries the actor, project identity, initial status and complete inserted row snapshot.
|
|
8
|
+
*/
|
|
9
|
+
export const projectCreatedEvent = defineEvent("project.created", {
|
|
10
|
+
version: 2,
|
|
11
|
+
schema: z.object({
|
|
12
|
+
...eventHistoryMetadata,
|
|
13
|
+
projectId: z.uuid(),
|
|
14
|
+
key: z.string(),
|
|
15
|
+
title: z.string(),
|
|
16
|
+
status: projectStatusSchema,
|
|
17
|
+
after: z.record(z.string(), historyJSONSchema),
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Published after a project update. Carries actor, identity and complete before/after row snapshots.
|
|
23
|
+
*/
|
|
24
|
+
export const projectUpdatedEvent = defineEvent("project.updated", {
|
|
25
|
+
version: 1,
|
|
26
|
+
schema: z.object({
|
|
27
|
+
...eventHistoryMetadata,
|
|
28
|
+
projectId: z.uuid(),
|
|
29
|
+
key: z.string(),
|
|
30
|
+
title: z.string(),
|
|
31
|
+
before: z.record(z.string(), historyJSONSchema),
|
|
32
|
+
after: z.record(z.string(), historyJSONSchema),
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Published for each active project that is archived. Carries actor, identity and complete before/after row snapshots.
|
|
38
|
+
*/
|
|
39
|
+
export const projectArchivedEvent = defineEvent("project.archived", {
|
|
40
|
+
version: 2,
|
|
41
|
+
schema: z.object({
|
|
42
|
+
...eventHistoryMetadata,
|
|
43
|
+
projectId: z.uuid(),
|
|
44
|
+
key: z.string(),
|
|
45
|
+
title: z.string(),
|
|
46
|
+
before: z.record(z.string(), historyJSONSchema),
|
|
47
|
+
after: z.record(z.string(), historyJSONSchema),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Published for each archived project that is restored. Carries actor, identity and complete before/after row snapshots.
|
|
53
|
+
*/
|
|
54
|
+
export const projectRestoredEvent = defineEvent("project.restored", {
|
|
55
|
+
version: 2,
|
|
56
|
+
schema: z.object({
|
|
57
|
+
...eventHistoryMetadata,
|
|
58
|
+
projectId: z.uuid(),
|
|
59
|
+
key: z.string(),
|
|
60
|
+
title: z.string(),
|
|
61
|
+
before: z.record(z.string(), historyJSONSchema),
|
|
62
|
+
after: z.record(z.string(), historyJSONSchema),
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import "server-cli-only";
|
|
2
|
+
import { createHistoryDiff } from "@/app/admin/history/_lib/helpers";
|
|
3
|
+
import { createHistoryEntry } from "@/app/admin/history/_lib/service.server";
|
|
4
|
+
import {
|
|
5
|
+
projectArchivedEvent,
|
|
6
|
+
projectCreatedEvent,
|
|
7
|
+
projectRestoredEvent,
|
|
8
|
+
projectUpdatedEvent,
|
|
9
|
+
} from "./events";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Projects supply historical resource labels for overview entries.
|
|
13
|
+
*/
|
|
14
|
+
export const projectHistoryEventListeners = {
|
|
15
|
+
projectCreated: projectCreatedEvent.listen(async ({ event, payload: p }) => {
|
|
16
|
+
await createHistoryEntry({
|
|
17
|
+
eventId: event.id,
|
|
18
|
+
eventName: event.name,
|
|
19
|
+
eventVersion: event.version,
|
|
20
|
+
occurredAt: event.occurredAt,
|
|
21
|
+
actorId: p.actor.id,
|
|
22
|
+
actorName: p.actor.name,
|
|
23
|
+
resourceType: "projects",
|
|
24
|
+
resourceId: p.projectId,
|
|
25
|
+
resourceLabel: `${p.key} · ${p.title}`,
|
|
26
|
+
context: [],
|
|
27
|
+
action: "created",
|
|
28
|
+
diff: createHistoryDiff(null, p.after),
|
|
29
|
+
});
|
|
30
|
+
}),
|
|
31
|
+
projectUpdated: projectUpdatedEvent.listen(async ({ event, payload: p }) => {
|
|
32
|
+
const diff = createHistoryDiff(p.before, p.after);
|
|
33
|
+
if (!diff.changes.some((change) => change.path !== "/updated_at")) return;
|
|
34
|
+
|
|
35
|
+
await createHistoryEntry({
|
|
36
|
+
eventId: event.id,
|
|
37
|
+
eventName: event.name,
|
|
38
|
+
eventVersion: event.version,
|
|
39
|
+
occurredAt: event.occurredAt,
|
|
40
|
+
actorId: p.actor.id,
|
|
41
|
+
actorName: p.actor.name,
|
|
42
|
+
resourceType: "projects",
|
|
43
|
+
resourceId: p.projectId,
|
|
44
|
+
resourceLabel: `${p.key} · ${p.title}`,
|
|
45
|
+
context: [],
|
|
46
|
+
action: "updated",
|
|
47
|
+
diff,
|
|
48
|
+
});
|
|
49
|
+
}),
|
|
50
|
+
projectArchived: projectArchivedEvent.listen(async ({ event, payload: p }) => {
|
|
51
|
+
await createHistoryEntry({
|
|
52
|
+
eventId: event.id,
|
|
53
|
+
eventName: event.name,
|
|
54
|
+
eventVersion: event.version,
|
|
55
|
+
occurredAt: event.occurredAt,
|
|
56
|
+
actorId: p.actor.id,
|
|
57
|
+
actorName: p.actor.name,
|
|
58
|
+
resourceType: "projects",
|
|
59
|
+
resourceId: p.projectId,
|
|
60
|
+
resourceLabel: `${p.key} · ${p.title}`,
|
|
61
|
+
context: [],
|
|
62
|
+
action: "archived",
|
|
63
|
+
diff: createHistoryDiff(p.before, p.after),
|
|
64
|
+
});
|
|
65
|
+
}),
|
|
66
|
+
projectRestored: projectRestoredEvent.listen(async ({ event, payload: p }) => {
|
|
67
|
+
await createHistoryEntry({
|
|
68
|
+
eventId: event.id,
|
|
69
|
+
eventName: event.name,
|
|
70
|
+
eventVersion: event.version,
|
|
71
|
+
occurredAt: event.occurredAt,
|
|
72
|
+
actorId: p.actor.id,
|
|
73
|
+
actorName: p.actor.name,
|
|
74
|
+
resourceType: "projects",
|
|
75
|
+
resourceId: p.projectId,
|
|
76
|
+
resourceLabel: `${p.key} · ${p.title}`,
|
|
77
|
+
context: [],
|
|
78
|
+
action: "restored",
|
|
79
|
+
diff: createHistoryDiff(p.before, p.after),
|
|
80
|
+
});
|
|
81
|
+
}),
|
|
82
|
+
};
|