@tulip-systems/create-app 0.12.4 → 0.14.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.
Files changed (143) hide show
  1. package/dist/index.mjs +2866 -2949
  2. package/dist/index.mjs.map +1 -1
  3. package/dist/templates/basic/package.json +37 -38
  4. package/dist/templates/basic/src/app/layout.tsx +20 -10
  5. package/dist/templates/code/package.json +80 -72
  6. package/dist/templates/code/public/sw.js +35 -0
  7. package/dist/templates/code/src/app/admin/contacts/_components/grid.tsx +1 -1
  8. package/dist/templates/code/src/app/admin/customers/_config/columns-data.tsx +1 -1
  9. package/dist/templates/code/src/app/admin/employees/_config/columns-data.tsx +1 -1
  10. package/dist/templates/code/src/app/admin/history/[id]/layout.tsx +33 -0
  11. package/dist/templates/code/src/app/admin/history/[id]/loading.tsx +11 -0
  12. package/dist/templates/code/src/app/admin/history/[id]/page.tsx +30 -0
  13. package/dist/templates/code/src/app/admin/history/_components/diff.client.tsx +377 -0
  14. package/dist/templates/code/src/app/admin/history/_components/field-comparison.tsx +153 -0
  15. package/dist/templates/code/src/app/admin/history/_components/tables.tsx +47 -0
  16. package/dist/templates/code/src/app/admin/history/_components/toolbars.tsx +54 -0
  17. package/dist/templates/code/src/app/admin/history/_config/columns-data.tsx +61 -0
  18. package/dist/templates/code/src/app/admin/history/_config/filters.ts +46 -0
  19. package/dist/templates/code/src/app/admin/history/_config/sorting.ts +30 -0
  20. package/dist/templates/code/src/app/admin/history/_lib/helpers.ts +98 -0
  21. package/dist/templates/code/src/app/admin/history/_lib/router.ts +100 -0
  22. package/dist/templates/code/src/app/admin/history/_lib/schema.ts +45 -0
  23. package/dist/templates/code/src/app/admin/history/_lib/service.server.ts +30 -0
  24. package/dist/templates/code/src/app/admin/history/_lib/validators.ts +118 -0
  25. package/dist/templates/code/src/app/admin/initiatives/_config/columns-data.tsx +1 -1
  26. package/dist/templates/code/src/app/admin/notifications/_components/command-toggle-push.tsx +115 -0
  27. package/dist/templates/code/src/app/admin/notifications/_components/tables.tsx +37 -0
  28. package/dist/templates/code/src/app/admin/notifications/_components/toolbars.tsx +21 -0
  29. package/dist/templates/code/src/app/admin/notifications/_config/columns-data.tsx +64 -0
  30. package/dist/templates/code/src/app/admin/notifications/_config/commands.tsx +40 -0
  31. package/dist/templates/code/src/app/admin/notifications/_lib/listeners.server.ts +69 -0
  32. package/dist/templates/code/src/app/admin/notifications/_lib/push.server.ts +80 -0
  33. package/dist/templates/code/src/app/admin/notifications/_lib/router.ts +82 -0
  34. package/dist/templates/code/src/app/admin/notifications/_lib/schema.ts +49 -0
  35. package/dist/templates/code/src/app/admin/notifications/_lib/validators.ts +19 -0
  36. package/dist/templates/code/src/app/admin/notifications/layout.tsx +17 -0
  37. package/dist/templates/code/src/app/admin/notifications/loading.tsx +9 -0
  38. package/dist/templates/code/src/app/admin/notifications/page.tsx +28 -0
  39. package/dist/templates/code/src/app/admin/projects/[id]/(default)/page.tsx +1 -1
  40. package/dist/templates/code/src/app/admin/projects/[id]/timeline/page.tsx +1 -1
  41. package/dist/templates/code/src/app/admin/projects/_config/columns-data.tsx +1 -1
  42. package/dist/templates/code/src/app/admin/projects/_lib/events.ts +64 -0
  43. package/dist/templates/code/src/app/admin/projects/_lib/listeners.server.ts +82 -0
  44. package/dist/templates/code/src/app/admin/projects/_lib/router.ts +145 -20
  45. package/dist/templates/code/src/app/admin/projects/timeline/page.tsx +1 -1
  46. package/dist/templates/code/src/app/admin/prospects/_config/columns-data.tsx +1 -1
  47. package/dist/templates/code/src/app/admin/queue/_components/create-command.tsx +97 -0
  48. package/dist/templates/code/src/app/admin/queue/_components/tables.tsx +37 -0
  49. package/dist/templates/code/src/app/admin/queue/_components/toolbars.tsx +16 -0
  50. package/dist/templates/code/src/app/admin/queue/_config/columns-data.tsx +44 -0
  51. package/dist/templates/code/src/app/admin/queue/_config/commands.tsx +39 -0
  52. package/dist/templates/code/src/app/admin/queue/_lib/queue.ts +27 -0
  53. package/dist/templates/code/src/app/admin/queue/_lib/router.ts +33 -0
  54. package/dist/templates/code/src/app/admin/queue/_lib/schema.ts +12 -0
  55. package/dist/templates/code/src/app/admin/queue/_lib/validators.ts +15 -0
  56. package/dist/templates/code/src/app/admin/quotations/_config/columns-data.tsx +1 -1
  57. package/dist/templates/code/src/app/admin/quotations/_config/commands.tsx +1 -1
  58. package/dist/templates/code/src/app/admin/settings/history/layout.tsx +6 -0
  59. package/dist/templates/code/src/app/admin/settings/history/loading.tsx +5 -0
  60. package/dist/templates/code/src/app/admin/settings/history/page.tsx +13 -0
  61. package/dist/templates/code/src/app/admin/settings/layout.tsx +11 -0
  62. package/dist/templates/code/src/app/admin/settings/queue/loading.tsx +9 -0
  63. package/dist/templates/code/src/app/admin/settings/queue/page.tsx +21 -0
  64. package/dist/templates/code/src/app/admin/tasks/_components/domains-tabs-grid.tsx +1 -1
  65. package/dist/templates/code/src/app/admin/tasks/_config/columns-data.tsx +1 -1
  66. package/dist/templates/code/src/app/admin/tasks/_config/columns-inline.tsx +1 -1
  67. package/dist/templates/code/src/app/admin/tasks/_lib/events.ts +40 -0
  68. package/dist/templates/code/src/app/admin/tasks/_lib/listeners.server.ts +46 -0
  69. package/dist/templates/code/src/app/admin/tasks/_lib/router.ts +222 -90
  70. package/dist/templates/code/src/app/admin/time/_config/columns-data.tsx +1 -1
  71. package/dist/templates/code/src/app/admin/time/_config/columns-inline.tsx +1 -1
  72. package/dist/templates/code/src/app/api/[transport]/route.ts +31 -21
  73. package/dist/templates/code/src/app/api/queue/dispatch/route.ts +4 -0
  74. package/dist/templates/code/src/app/auth/forget-password/page.tsx +1 -1
  75. package/dist/templates/code/src/app/auth/layout.tsx +1 -1
  76. package/dist/templates/code/src/app/auth/reset-password/page.tsx +1 -1
  77. package/dist/templates/code/src/app/layout.tsx +25 -13
  78. package/dist/templates/code/src/env.ts +12 -0
  79. package/dist/templates/code/src/lib/config/paths.tsx +8 -0
  80. package/dist/templates/code/src/server/auth/constants.ts +1 -1
  81. package/dist/templates/code/src/server/auth/init.ts +12 -5
  82. package/dist/templates/code/src/server/auth/permissions.ts +3 -0
  83. package/dist/templates/code/src/server/auth/schema.ts +100 -1
  84. package/dist/templates/code/src/server/db/schema.ts +3 -0
  85. package/dist/templates/code/src/server/events/handlers.ts +17 -0
  86. package/dist/templates/code/src/server/events/init.ts +18 -0
  87. package/dist/templates/code/src/server/events/listeners.ts +17 -0
  88. package/dist/templates/code/src/server/queue/handlers.ts +5 -0
  89. package/dist/templates/code/src/server/queue/init.ts +16 -0
  90. package/dist/templates/code/src/server/router/router.ts +13 -0
  91. package/dist/templates/spark/package.json +64 -65
  92. package/dist/templates/spark/src/app/admin/assignments/[id]/(default)/@dashboard/page.tsx +1 -1
  93. package/dist/templates/spark/src/app/admin/assignments/_config/columns-data.tsx +1 -1
  94. package/dist/templates/spark/src/app/admin/companies/_config/columns-data.tsx +1 -1
  95. package/dist/templates/spark/src/app/admin/contacts/_components/grid.tsx +1 -1
  96. package/dist/templates/spark/src/app/admin/customers/_config/columns-data.tsx +1 -1
  97. package/dist/templates/spark/src/app/admin/drive/_config/columns-data.tsx +1 -1
  98. package/dist/templates/spark/src/app/admin/emails/_components/send-email.tsx +1 -1
  99. package/dist/templates/spark/src/app/admin/emails/_config/columns-data.tsx +1 -1
  100. package/dist/templates/spark/src/app/admin/employees/_config/columns-data.tsx +1 -1
  101. package/dist/templates/spark/src/app/admin/executions/[id]/@dashboard/page.tsx +1 -1
  102. package/dist/templates/spark/src/app/admin/executions/_config/columns-data.tsx +1 -1
  103. package/dist/templates/spark/src/app/admin/products/[id]/(preview)/page.tsx +1 -1
  104. package/dist/templates/spark/src/app/admin/products/_config/columns-data.tsx +1 -1
  105. package/dist/templates/spark/src/app/admin/projects/[id]/(dashboard)/page.tsx +1 -1
  106. package/dist/templates/spark/src/app/admin/projects/[id]/messages/page.tsx +1 -1
  107. package/dist/templates/spark/src/app/admin/projects/[id]/statistics/page.tsx +1 -1
  108. package/dist/templates/spark/src/app/admin/projects/_config/columns-data.tsx +1 -1
  109. package/dist/templates/spark/src/app/admin/purchase-order-items/_config/columns-data.tsx +1 -1
  110. package/dist/templates/spark/src/app/admin/purchase-order-items/_config/columns-inline.tsx +1 -1
  111. package/dist/templates/spark/src/app/admin/purchase-order-items/_config/commands.tsx +1 -1
  112. package/dist/templates/spark/src/app/admin/purchase-orders/[id]/(default)/@dashboard/page.tsx +1 -1
  113. package/dist/templates/spark/src/app/admin/purchase-orders/_config/columns-data.tsx +1 -1
  114. package/dist/templates/spark/src/app/admin/quotation-items/_config/commands.tsx +1 -1
  115. package/dist/templates/spark/src/app/admin/quotations/[id]/(default)/@dashboard/page.tsx +1 -1
  116. package/dist/templates/spark/src/app/admin/quotations/_config/columns-data.tsx +1 -1
  117. package/dist/templates/spark/src/app/admin/run-tasks/_config/columns-data.tsx +1 -1
  118. package/dist/templates/spark/src/app/admin/runs/[id]/@dashboard/page.tsx +1 -1
  119. package/dist/templates/spark/src/app/admin/runs/_config/columns-data.tsx +1 -1
  120. package/dist/templates/spark/src/app/admin/sales-invoices/[id]/@dashboard/page.tsx +1 -1
  121. package/dist/templates/spark/src/app/admin/sales-invoices/_config/columns-data.tsx +1 -1
  122. package/dist/templates/spark/src/app/admin/sales-order-executions/_config/columns-data.tsx +1 -1
  123. package/dist/templates/spark/src/app/admin/sales-order-executions/_config/columns-inline.tsx +1 -1
  124. package/dist/templates/spark/src/app/admin/sales-order-executions/_config/commands.tsx +1 -1
  125. package/dist/templates/spark/src/app/admin/sales-order-items/_config/commands.tsx +1 -1
  126. package/dist/templates/spark/src/app/admin/sales-orders/[id]/(default)/@dashboard/page.tsx +1 -1
  127. package/dist/templates/spark/src/app/admin/sales-orders/[id]/(default)/layout.tsx +1 -1
  128. package/dist/templates/spark/src/app/admin/sales-orders/_config/columns-data.tsx +1 -1
  129. package/dist/templates/spark/src/app/admin/schedule/_components/schedule-card.tsx +1 -1
  130. package/dist/templates/spark/src/app/admin/schedule/_components/schedule-month-view.tsx +1 -1
  131. package/dist/templates/spark/src/app/admin/suppliers/_config/columns-data.tsx +1 -1
  132. package/dist/templates/spark/src/app/admin/tasks/_components/subproject-selector.tsx +1 -1
  133. package/dist/templates/spark/src/app/admin/tasks/_config/columns-data.tsx +1 -1
  134. package/dist/templates/spark/src/app/admin/tasks/_config/columns-inline.tsx +1 -1
  135. package/dist/templates/spark/src/app/admin/tasks/_config/commands.tsx +1 -1
  136. package/dist/templates/spark/src/app/admin/technical-sheets/[id]/page.tsx +1 -1
  137. package/dist/templates/spark/src/app/admin/technical-sheets/_config/columns-data.tsx +1 -1
  138. package/dist/templates/spark/src/app/admin/technical-sheets/_config/commands.tsx +1 -1
  139. package/dist/templates/spark/src/app/admin/templates/_config/columns-data.tsx +1 -1
  140. package/dist/templates/spark/src/app/auth/layout.tsx +1 -1
  141. package/dist/templates/spark/src/app/layout.tsx +25 -10
  142. package/dist/templates/spark/src/server/auth/init.ts +1 -0
  143. package/package.json +6 -6
@@ -13,6 +13,7 @@ import {
13
13
  desc,
14
14
  eq,
15
15
  exists,
16
+ getTableColumns,
16
17
  gte,
17
18
  ilike,
18
19
  inArray,
@@ -22,9 +23,12 @@ import {
22
23
  or,
23
24
  sql,
24
25
  } from "drizzle-orm";
26
+ import { after } from "next/server";
25
27
  import z from "zod";
28
+ import type { JSONValue } from "@/app/admin/history/_lib/validators";
26
29
  import { getTableData } from "@/app/lib/utils/table-data.server";
27
30
  import { customers, initiatives } from "@/server/db/schema";
31
+ import { events } from "@/server/events/init";
28
32
  import { permissionMiddleware } from "@/server/router/middleware";
29
33
  import { protectedProcedure } from "@/server/router/procedures";
30
34
  import type { ProjectColumn } from "../_config/columns-data";
@@ -34,6 +38,12 @@ import {
34
38
  projectTableSortsParser,
35
39
  projectTableSortsSchema,
36
40
  } from "../_config/sorting";
41
+ import {
42
+ projectArchivedEvent,
43
+ projectCreatedEvent,
44
+ projectRestoredEvent,
45
+ projectUpdatedEvent,
46
+ } from "./events";
37
47
  import { ensureProjectDriveFolder } from "./helpers.server";
38
48
  import { projects } from "./schema";
39
49
  import {
@@ -215,50 +225,135 @@ export const projectsRouter = {
215
225
  .use(permissionMiddleware({ project: ["create"] }))
216
226
  .input(createProjectSchema)
217
227
  .handler(async ({ context, input }) => {
218
- const [result] = await context.db.insert(projects).values(input).returning();
219
-
220
- if (!result) {
221
- throw new ORPCError("NOT_FOUND", {
222
- message: "Project kon niet worden aangemaakt",
228
+ const [created] = await context.db
229
+ .insert(projects)
230
+ .values(input)
231
+ .returning({
232
+ ...getTableColumns(projects),
233
+ after: sql<Record<string, JSONValue>>`to_jsonb(NEW)`,
223
234
  });
224
- }
235
+
236
+ if (!created) throw new ORPCError("INTERNAL_SERVER_ERROR");
237
+ const { after: snapshot, ...result } = created;
238
+
239
+ after(async () => {
240
+ await events.publish(
241
+ projectCreatedEvent,
242
+ {
243
+ actor: { id: context.user.id, name: context.user.name },
244
+ projectId: result.id,
245
+ key: result.key,
246
+ title: result.title,
247
+ status: result.status,
248
+ after: snapshot,
249
+ },
250
+ { occurredAt: new Date().toISOString() },
251
+ );
252
+ });
225
253
 
226
254
  return result;
227
255
  }),
228
256
  update: protectedProcedure
229
257
  .use(permissionMiddleware({ project: ["update"] }))
230
258
  .input(z.object({ id: z.string(), data: updateProjectSchema.partial() }))
231
- .output(z.void())
232
259
  .handler(async ({ context, input }) => {
233
260
  const [result] = await context.db
234
261
  .update(projects)
235
262
  .set(input.data)
236
263
  .where(eq(projects.id, input.id))
237
- .returning();
238
-
239
- if (!result) {
240
- throw new ORPCError("NOT_FOUND", {
241
- message: "Project kon niet worden gewijzigd",
264
+ .returning({
265
+ ...getTableColumns(projects),
266
+ before: sql<Record<string, JSONValue>>`to_jsonb(OLD)`,
267
+ after: sql<Record<string, JSONValue>>`to_jsonb(NEW)`,
242
268
  });
243
- }
269
+
270
+ if (!result) return;
271
+ const { before: previous, after: next, ...project } = result;
272
+ after(async () => {
273
+ await events.publish(
274
+ projectUpdatedEvent,
275
+ {
276
+ actor: { id: context.user.id, name: context.user.name },
277
+ projectId: project.id,
278
+ key: project.key,
279
+ title: project.title,
280
+ before: previous,
281
+ after: next,
282
+ },
283
+ { occurredAt: new Date().toISOString() },
284
+ );
285
+ });
286
+ return project;
244
287
  }),
245
288
  archive: protectedProcedure
246
289
  .use(permissionMiddleware({ project: ["archive"] }))
247
290
  .input(bulkActionSchema)
248
291
  .handler(async ({ context, input }) => {
249
- await context.db
292
+ const updated = await context.db
250
293
  .update(projects)
251
294
  .set({ deletedAt: new Date() })
252
- .where(inArray(projects.id, input.ids));
295
+ .where(and(inArray(projects.id, input.ids), isNull(projects.deletedAt)))
296
+ .returning({
297
+ id: projects.id,
298
+ key: projects.key,
299
+ title: projects.title,
300
+ before: sql<Record<string, JSONValue>>`to_jsonb(OLD)`,
301
+ after: sql<Record<string, JSONValue>>`to_jsonb(NEW)`,
302
+ });
303
+
304
+ after(async () => {
305
+ await Promise.all(
306
+ updated.map((project) =>
307
+ events.publish(
308
+ projectArchivedEvent,
309
+ {
310
+ actor: { id: context.user.id, name: context.user.name },
311
+ projectId: project.id,
312
+ key: project.key,
313
+ title: project.title,
314
+ before: project.before,
315
+ after: project.after,
316
+ },
317
+ { occurredAt: new Date().toISOString() },
318
+ ),
319
+ ),
320
+ );
321
+ });
253
322
  }),
254
323
  restore: protectedProcedure
255
324
  .use(permissionMiddleware({ project: ["archive"] }))
256
325
  .input(bulkActionSchema)
257
326
  .handler(async ({ context, input }) => {
258
- await context.db
327
+ const updated = await context.db
259
328
  .update(projects)
260
329
  .set({ deletedAt: null })
261
- .where(inArray(projects.id, input.ids));
330
+ .where(and(inArray(projects.id, input.ids), isNotNull(projects.deletedAt)))
331
+ .returning({
332
+ id: projects.id,
333
+ key: projects.key,
334
+ title: projects.title,
335
+ before: sql<Record<string, JSONValue>>`to_jsonb(OLD)`,
336
+ after: sql<Record<string, JSONValue>>`to_jsonb(NEW)`,
337
+ });
338
+
339
+ after(async () => {
340
+ await Promise.all(
341
+ updated.map((project) =>
342
+ events.publish(
343
+ projectRestoredEvent,
344
+ {
345
+ actor: { id: context.user.id, name: context.user.name },
346
+ projectId: project.id,
347
+ key: project.key,
348
+ title: project.title,
349
+ before: project.before,
350
+ after: project.after,
351
+ },
352
+ { occurredAt: new Date().toISOString() },
353
+ ),
354
+ ),
355
+ );
356
+ });
262
357
  }),
263
358
  delete: protectedProcedure
264
359
  .use(permissionMiddleware({ project: ["delete"] }))
@@ -270,10 +365,40 @@ export const projectsRouter = {
270
365
  .use(permissionMiddleware({ project: ["update"] }))
271
366
  .input(bulkActionSchema.extend({ status: projectStatusSchema }))
272
367
  .handler(async ({ context, input }) => {
273
- await context.db
368
+ const updated = await context.db
274
369
  .update(projects)
275
370
  .set({ status: input.status })
276
- .where(inArray(projects.id, input.ids))
277
- .returning();
371
+ .where(
372
+ and(
373
+ inArray(projects.id, input.ids),
374
+ sql`${projects.status} IS DISTINCT FROM ${input.status}`,
375
+ ),
376
+ )
377
+ .returning({
378
+ id: projects.id,
379
+ key: projects.key,
380
+ title: projects.title,
381
+ before: sql<Record<string, JSONValue>>`to_jsonb(OLD)`,
382
+ after: sql<Record<string, JSONValue>>`to_jsonb(NEW)`,
383
+ });
384
+
385
+ after(async () => {
386
+ await Promise.all(
387
+ updated.map((project) =>
388
+ events.publish(
389
+ projectUpdatedEvent,
390
+ {
391
+ actor: { id: context.user.id, name: context.user.name },
392
+ projectId: project.id,
393
+ key: project.key,
394
+ title: project.title,
395
+ before: project.before,
396
+ after: project.after,
397
+ },
398
+ { occurredAt: new Date().toISOString() },
399
+ ),
400
+ ),
401
+ );
402
+ });
278
403
  }),
279
404
  };
@@ -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 { useState } from "react";
12
12
  import {
@@ -1,12 +1,12 @@
1
1
  "use client";
2
2
 
3
+ import { Link } from "@tulip-systems/core/navigation";
3
4
  import { TableCell, 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 { Prospect } from "../_lib/validators";
12
12
  import { ProspectStatusField } from "./status";
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
4
+ import type { DefaultError, MutationOptions } from "@tanstack/react-query";
5
+ import {
6
+ CommandFormDialog,
7
+ CommandFormDialogCancel,
8
+ CommandFormDialogContent,
9
+ CommandFormDialogFields,
10
+ CommandFormDialogFooter,
11
+ CommandFormDialogHeader,
12
+ CommandFormDialogSubmit,
13
+ CommandFormDialogTitle,
14
+ CommandFormDialogTrigger,
15
+ } from "@tulip-systems/commands/client";
16
+ import { Input } from "@tulip-systems/ui/input";
17
+ import {
18
+ Form,
19
+ FormControl,
20
+ FormField,
21
+ FormItem,
22
+ FormLabel,
23
+ FormMessage,
24
+ } from "@tulip-systems/ui/react-hook-form";
25
+ import type React from "react";
26
+ import { useForm } from "react-hook-form";
27
+ import {
28
+ type EnqueueQueueTaskInput,
29
+ type EnqueueQueueTaskSchema,
30
+ enqueueQueueTaskSchema,
31
+ } from "../_lib/validators";
32
+
33
+ export type EnqueueQueueTaskCommandProps<
34
+ TData = unknown,
35
+ TError = DefaultError,
36
+ TVariables = undefined,
37
+ TOnMutateResult = unknown,
38
+ > = {
39
+ variables: (values: EnqueueQueueTaskSchema) => TVariables;
40
+ mutation: MutationOptions<TData, TError, TVariables, TOnMutateResult>;
41
+ children?: React.ReactNode;
42
+ };
43
+
44
+ export function EnqueueQueueTaskCommand<
45
+ TData = unknown,
46
+ TError = DefaultError,
47
+ TVariables = undefined,
48
+ TOnMutateResult = unknown,
49
+ >({
50
+ variables,
51
+ mutation,
52
+ children,
53
+ }: EnqueueQueueTaskCommandProps<TData, TError, TVariables, TOnMutateResult>) {
54
+ const form = useForm<EnqueueQueueTaskInput>({
55
+ resolver: standardSchemaResolver(enqueueQueueTaskSchema),
56
+ defaultValues: {
57
+ message: "",
58
+ },
59
+ });
60
+
61
+ return (
62
+ <CommandFormDialog>
63
+ {children}
64
+
65
+ <Form {...form}>
66
+ <CommandFormDialogContent variables={variables} mutation={mutation}>
67
+ <CommandFormDialogHeader>
68
+ <CommandFormDialogTitle>Enqueue task</CommandFormDialogTitle>
69
+ </CommandFormDialogHeader>
70
+
71
+ <CommandFormDialogFields>
72
+ <FormField
73
+ control={form.control}
74
+ name="message"
75
+ render={({ field }) => (
76
+ <FormItem>
77
+ <FormLabel>Message</FormLabel>
78
+ <FormControl>
79
+ <Input {...field} placeholder="Enter your message" />
80
+ </FormControl>
81
+ <FormMessage />
82
+ </FormItem>
83
+ )}
84
+ />
85
+ </CommandFormDialogFields>
86
+
87
+ <CommandFormDialogFooter>
88
+ <CommandFormDialogCancel>Cancel</CommandFormDialogCancel>
89
+ <CommandFormDialogSubmit>Enqueue</CommandFormDialogSubmit>
90
+ </CommandFormDialogFooter>
91
+ </CommandFormDialogContent>
92
+ </Form>
93
+ </CommandFormDialog>
94
+ );
95
+ }
96
+
97
+ export const EnqueueQueueTaskCommandTrigger = CommandFormDialogTrigger;
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import { useSuspenseQuery } from "@tanstack/react-query";
4
+ import type { CommandFor } from "@tulip-systems/commands";
5
+ import type { TableColumnDef } from "@tulip-systems/data-tables";
6
+ import {
7
+ createTableConfig,
8
+ TableConfigProvider,
9
+ useLocalStrategy,
10
+ } from "@tulip-systems/data-tables/client";
11
+ import type { PropsWithChildren } from "react";
12
+ import { orpc } from "@/server/router/client";
13
+ import type { QueueExecution } from "../_lib/validators";
14
+
15
+ type QueueExecutionsTableProviderProps = PropsWithChildren<{
16
+ columns: TableColumnDef<QueueExecution>[];
17
+ commands?: CommandFor<QueueExecution[]>[];
18
+ }>;
19
+
20
+ export function QueueExecutionsTableProvider({
21
+ columns,
22
+ commands,
23
+ children,
24
+ }: QueueExecutionsTableProviderProps) {
25
+ const response = useSuspenseQuery(orpc.queue.listExecutions.queryOptions());
26
+ const strategy = useLocalStrategy({ total: response.data.length });
27
+
28
+ const config = createTableConfig<QueueExecution>({
29
+ queryData: response.data,
30
+ getRowId: (row) => row.id,
31
+ columns,
32
+ strategy,
33
+ commands,
34
+ });
35
+
36
+ return <TableConfigProvider config={config}>{children}</TableConfigProvider>;
37
+ }
@@ -0,0 +1,16 @@
1
+ "use client";
2
+
3
+ import { TableToolbar, TableToolbarList } from "@tulip-systems/data-tables";
4
+ import type { PropsWithChildren } from "react";
5
+
6
+ export function QueueExecutionsTableToolbar({ children }: PropsWithChildren) {
7
+ return (
8
+ <TableToolbar>
9
+ <TableToolbarList>
10
+ <h1 className="font-semibold text-lg tracking-tight">Queue executions</h1>
11
+ </TableToolbarList>
12
+
13
+ <TableToolbarList>{children}</TableToolbarList>
14
+ </TableToolbar>
15
+ );
16
+ }
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ import { formatDateTime } from "@tulip-systems/core";
4
+ import { TableCell, type TableColumnDef, TableColumnHeader } from "@tulip-systems/data-tables";
5
+ import { createTableSelectCell } from "@tulip-systems/data-tables/client";
6
+ import type { QueueExecution } from "../_lib/validators";
7
+
8
+ export const queueExecutionColumns: TableColumnDef<QueueExecution>[] = [
9
+ createTableSelectCell(),
10
+ {
11
+ accessorKey: "message",
12
+ header: ({ column }) => <TableColumnHeader column={column} title="Message" />,
13
+ cell: ({ row }) => (
14
+ <TableCell className="max-w-[360px] truncate">{row.original.message}</TableCell>
15
+ ),
16
+ enableSorting: false,
17
+ },
18
+ {
19
+ accessorKey: "task",
20
+ header: ({ column }) => <TableColumnHeader column={column} title="Task" />,
21
+ cell: ({ row }) => <TableCell>{row.original.task}</TableCell>,
22
+ enableSorting: false,
23
+ },
24
+ {
25
+ accessorKey: "jobId",
26
+ header: ({ column }) => <TableColumnHeader column={column} title="Job" />,
27
+ cell: ({ row }) => (
28
+ <TableCell className="max-w-[280px] truncate">{row.original.jobId}</TableCell>
29
+ ),
30
+ enableSorting: false,
31
+ },
32
+ {
33
+ accessorKey: "enqueuedAt",
34
+ header: ({ column }) => <TableColumnHeader column={column} title="Enqueued" />,
35
+ cell: ({ row }) => <TableCell>{formatDateTime(row.original.enqueuedAt)}</TableCell>,
36
+ enableSorting: false,
37
+ },
38
+ {
39
+ accessorKey: "executedAt",
40
+ header: ({ column }) => <TableColumnHeader column={column} title="Executed" />,
41
+ cell: ({ row }) => <TableCell>{formatDateTime(row.original.executedAt)}</TableCell>,
42
+ enableSorting: false,
43
+ },
44
+ ];
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { commandBuilder } from "@tulip-systems/commands";
4
+ import { CommandLabel, DeleteCommand } from "@tulip-systems/commands/client";
5
+ import { toast } from "@tulip-systems/ui/sonner";
6
+ import { ListPlusIcon } from "lucide-react";
7
+ import { orpc } from "@/server/router/client";
8
+ import {
9
+ EnqueueQueueTaskCommand,
10
+ EnqueueQueueTaskCommandTrigger,
11
+ } from "../_components/create-command";
12
+ import type { QueueExecution } from "../_lib/validators";
13
+
14
+ export const queueEnqueueCommand = commandBuilder.render(() => (
15
+ <EnqueueQueueTaskCommand
16
+ variables={(values) => values}
17
+ mutation={orpc.queue.enqueueTask.mutationOptions({
18
+ meta: { invalidates: [orpc.queue.listExecutions.key()] },
19
+ onSuccess: () => toast.success("Task enqueued"),
20
+ onError: () => toast.error("Could not enqueue task"),
21
+ })}
22
+ >
23
+ <EnqueueQueueTaskCommandTrigger label="Enqueue task" hotkey="mod+i">
24
+ <ListPlusIcon className="size-4" />
25
+ <CommandLabel />
26
+ </EnqueueQueueTaskCommandTrigger>
27
+ </EnqueueQueueTaskCommand>
28
+ ));
29
+
30
+ export const queueDeleteCommand = commandBuilder.$type<QueueExecution[]>().render(({ data }) => (
31
+ <DeleteCommand
32
+ variables={{ ids: data.map((item) => item.id) }}
33
+ mutation={orpc.queue.deleteExecutions.mutationOptions({
34
+ meta: { invalidates: [orpc.queue.listExecutions.key()] },
35
+ onSuccess: () => toast.success("Queue executions deleted"),
36
+ onError: () => toast.error("Could not delete queue executions"),
37
+ })}
38
+ />
39
+ ));
@@ -0,0 +1,27 @@
1
+ import "server-cli-only";
2
+
3
+ import { defineTask } from "@tulip-systems/queue";
4
+ import { z } from "zod";
5
+ import { db } from "@/server/db/init";
6
+ import { queueExecutions } from "./schema";
7
+
8
+ export const queueLogTask = defineTask("queue.log", {
9
+ schema: z.object({
10
+ message: z.string().min(1),
11
+ requestedAt: z.string().min(1),
12
+ }),
13
+ });
14
+
15
+ export const queueLogTaskHandler = queueLogTask.handle(
16
+ "record-queue-execution",
17
+ async ({ payload, job }) => {
18
+ await db.insert(queueExecutions).values({
19
+ jobId: job.id,
20
+ task: job.task,
21
+ message: payload.message,
22
+ enqueuedAt: new Date(job.enqueuedAt),
23
+ requestedAt: new Date(payload.requestedAt),
24
+ executedAt: new Date(),
25
+ });
26
+ },
27
+ );
@@ -0,0 +1,33 @@
1
+ import "server-cli-only";
2
+
3
+ import { desc, inArray } from "drizzle-orm";
4
+ import { queue } from "@/server/queue/init";
5
+ import { protectedProcedure } from "@/server/router/procedures";
6
+ import { queueLogTask } from "./queue";
7
+ import { queueExecutions } from "./schema";
8
+ import { deleteQueueExecutionsSchema, enqueueQueueTaskSchema } from "./validators";
9
+
10
+ export const queueRouter = {
11
+ listExecutions: protectedProcedure.handler(async ({ context }) =>
12
+ context.db.query.queueExecutions.findMany({
13
+ orderBy: [desc(queueExecutions.executedAt)],
14
+ limit: 100,
15
+ }),
16
+ ),
17
+ enqueueTask: protectedProcedure.input(enqueueQueueTaskSchema).handler(async ({ input }) => {
18
+ const result = await queue.enqueue(queueLogTask, {
19
+ message: input.message,
20
+ requestedAt: new Date().toISOString(),
21
+ });
22
+
23
+ return {
24
+ adapter: queue.adapter.key,
25
+ enqueued: result,
26
+ };
27
+ }),
28
+ deleteExecutions: protectedProcedure
29
+ .input(deleteQueueExecutionsSchema)
30
+ .handler(async ({ context, input }) => {
31
+ await context.db.delete(queueExecutions).where(inArray(queueExecutions.id, input.ids));
32
+ }),
33
+ };
@@ -0,0 +1,12 @@
1
+ import { pgTable } from "drizzle-orm/pg-core";
2
+ import { baseColumns } from "@/server/db/helpers";
3
+
4
+ export const queueExecutions = pgTable("queue_executions", (t) => ({
5
+ ...baseColumns,
6
+ jobId: t.varchar().notNull(),
7
+ task: t.varchar().notNull(),
8
+ message: t.text().notNull(),
9
+ enqueuedAt: t.timestamp().notNull(),
10
+ requestedAt: t.timestamp().notNull(),
11
+ executedAt: t.timestamp().notNull(),
12
+ }));
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import type { queueExecutions } from "./schema";
3
+
4
+ export type QueueExecution = typeof queueExecutions.$inferSelect;
5
+
6
+ export const enqueueQueueTaskSchema = z.object({
7
+ message: z.string().trim().min(1),
8
+ });
9
+
10
+ export const deleteQueueExecutionsSchema = z.object({
11
+ ids: z.array(z.string()),
12
+ });
13
+
14
+ export type EnqueueQueueTaskInput = z.input<typeof enqueueQueueTaskSchema>;
15
+ export type EnqueueQueueTaskSchema = z.infer<typeof enqueueQueueTaskSchema>;
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
 
3
+ import { Link } from "@tulip-systems/core/navigation";
3
4
  import { TableCell, type TableColumnDef, TableColumnHeader } from "@tulip-systems/data-tables";
4
5
  import { createTableSelectCell } from "@tulip-systems/data-tables/client";
5
6
  import { format } from "date-fns";
6
- import Link from "next/link";
7
7
  import type { Quotation } from "../_lib/validators";
8
8
 
9
9
  export type QuotationColumn = Quotation;
@@ -7,9 +7,9 @@ import {
7
7
  type CommandMutationMeta,
8
8
  DeleteCommand,
9
9
  } from "@tulip-systems/commands/client";
10
+ import { Link } from "@tulip-systems/core/navigation";
10
11
  import { toast } from "@tulip-systems/ui/sonner";
11
12
  import { EyeIcon } from "lucide-react";
12
- import Link from "next/link";
13
13
  import { Icons } from "@/components/icons";
14
14
  import { orpc } from "@/server/router/client";
15
15
  import {
@@ -0,0 +1,6 @@
1
+ import { TableLayout } from "@tulip-systems/data-tables";
2
+ import type { PropsWithChildren } from "react";
3
+
4
+ export default function Layout(props: PropsWithChildren) {
5
+ return <TableLayout className="p-content">{props.children}</TableLayout>;
6
+ }
@@ -0,0 +1,5 @@
1
+ import { TableSkeleton } from "@tulip-systems/data-tables";
2
+
3
+ export default function Loading() {
4
+ return <TableSkeleton />;
5
+ }
@@ -0,0 +1,13 @@
1
+ import { DataTable } from "@tulip-systems/data-tables/client";
2
+ import { HistoryPaginationTableProvider } from "@/app/admin/history/_components/tables";
3
+ import { HistoryTableToolbar } from "@/app/admin/history/_components/toolbars";
4
+ import { historyColumns } from "@/app/admin/history/_config/columns-data";
5
+
6
+ export default function Page() {
7
+ return (
8
+ <HistoryPaginationTableProvider columns={historyColumns}>
9
+ <HistoryTableToolbar />
10
+ <DataTable />
11
+ </HistoryPaginationTableProvider>
12
+ );
13
+ }
@@ -38,6 +38,17 @@ export default function Layout(props: PropsWithChildren) {
38
38
  href: "/admin/settings/theme",
39
39
  segment: "theme",
40
40
  },
41
+ {
42
+ name: "Queue",
43
+ href: "/admin/settings/queue",
44
+ segment: "queue",
45
+ },
46
+ {
47
+ name: "History",
48
+ href: "/admin/settings/history",
49
+ segment: "history",
50
+ permission: { history: ["view"] },
51
+ },
41
52
  ]}
42
53
  />
43
54
  </BottomBar>
@@ -0,0 +1,9 @@
1
+ import { TableSkeleton } from "@tulip-systems/data-tables";
2
+
3
+ export default function Loading() {
4
+ return (
5
+ <div className="p-content">
6
+ <TableSkeleton />
7
+ </div>
8
+ );
9
+ }