@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.
- 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 +80 -72
- 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/_config/columns-data.tsx +1 -1
- package/dist/templates/code/src/app/admin/quotations/_config/commands.tsx +1 -1
- 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 +8 -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 +3 -0
- package/dist/templates/code/src/server/auth/schema.ts +100 -1
- package/dist/templates/code/src/server/db/schema.ts +3 -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 +13 -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,377 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { convertEditorContentToPlainText, type EditorJSONContent } from "@tulip-systems/editor";
|
|
4
|
+
import { Button } from "@tulip-systems/ui/button";
|
|
5
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@tulip-systems/ui/collapsible";
|
|
6
|
+
import {
|
|
7
|
+
Empty,
|
|
8
|
+
EmptyDescription,
|
|
9
|
+
EmptyHeader,
|
|
10
|
+
EmptyMedia,
|
|
11
|
+
EmptyTitle,
|
|
12
|
+
} from "@tulip-systems/ui/empty";
|
|
13
|
+
import { Switch } from "@tulip-systems/ui/switch";
|
|
14
|
+
import { diffLines, diffWordsWithSpace } from "diff";
|
|
15
|
+
import equal from "fast-deep-equal";
|
|
16
|
+
import { ChevronDownIcon, FileDiffIcon } from "lucide-react";
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import { applyHistoryChanges } from "../_lib/helpers";
|
|
19
|
+
import { type HistoryDiff, historyDiffInputSchema, type JSONValue } from "../_lib/validators";
|
|
20
|
+
import {
|
|
21
|
+
FieldComparison,
|
|
22
|
+
FieldComparisonAdded,
|
|
23
|
+
FieldComparisonCell,
|
|
24
|
+
FieldComparisonHeader,
|
|
25
|
+
FieldComparisonHeading,
|
|
26
|
+
FieldComparisonLabel,
|
|
27
|
+
FieldComparisonRemoved,
|
|
28
|
+
FieldComparisonRow,
|
|
29
|
+
} from "./field-comparison";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Convert stored field names to labels without module-specific mappings.
|
|
33
|
+
*/
|
|
34
|
+
function fieldLabel(key: string): string {
|
|
35
|
+
return key
|
|
36
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
37
|
+
.replaceAll("_", " ")
|
|
38
|
+
.replaceAll("-", " ")
|
|
39
|
+
.replace(/\bid\b/gi, "ID")
|
|
40
|
+
.replace(/^./, (letter) => letter.toUpperCase());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Identify nested objects and editor documents without resolving related records.
|
|
45
|
+
*/
|
|
46
|
+
function isObject(value: JSONValue | undefined): value is Record<string, JSONValue> {
|
|
47
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Convert recognized editor documents to readable text.
|
|
52
|
+
*/
|
|
53
|
+
function editorText(value: JSONValue | undefined): string | undefined {
|
|
54
|
+
if (!isObject(value) || value.type !== "doc" || !Array.isArray(value.content)) return undefined;
|
|
55
|
+
return convertEditorContentToPlainText(value as EditorJSONContent);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Render values as content, preserving empty and absent states and grouping array items.
|
|
60
|
+
*/
|
|
61
|
+
function StoredValue({ value }: { value: JSONValue | undefined }) {
|
|
62
|
+
if (value === undefined) return <span className="text-muted-foreground italic">Not present</span>;
|
|
63
|
+
if (value === null) return <span className="text-muted-foreground italic">No value (null)</span>;
|
|
64
|
+
if (value === "") return <span className="text-muted-foreground italic">Empty text</span>;
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
if (!value.length) return <span className="text-muted-foreground italic">Empty list</span>;
|
|
67
|
+
return (
|
|
68
|
+
<ol className="list-inside list-decimal space-y-3">
|
|
69
|
+
{value.map((item, index) => (
|
|
70
|
+
<li key={`${index}:${JSON.stringify(item)}`}>
|
|
71
|
+
<StoredValue value={item} />
|
|
72
|
+
</li>
|
|
73
|
+
))}
|
|
74
|
+
</ol>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (isObject(value)) {
|
|
78
|
+
if (!Object.keys(value).length)
|
|
79
|
+
return <span className="text-muted-foreground italic">Empty object</span>;
|
|
80
|
+
return (
|
|
81
|
+
<dl className="space-y-3">
|
|
82
|
+
{Object.entries(value).map(([key, item]) => (
|
|
83
|
+
<div key={key}>
|
|
84
|
+
<dt className="mb-1 text-muted-foreground text-xs">{fieldLabel(key)}</dt>
|
|
85
|
+
<dd>
|
|
86
|
+
<StoredValue value={item} />
|
|
87
|
+
</dd>
|
|
88
|
+
</div>
|
|
89
|
+
))}
|
|
90
|
+
</dl>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return <span className="whitespace-pre-wrap break-words">{String(value)}</span>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Present formatted fields with a local switch for unchanged values.
|
|
98
|
+
*/
|
|
99
|
+
function FormattedComparison({
|
|
100
|
+
before,
|
|
101
|
+
after,
|
|
102
|
+
}: {
|
|
103
|
+
before: JSONValue | undefined;
|
|
104
|
+
after: JSONValue | undefined;
|
|
105
|
+
}) {
|
|
106
|
+
const [showUnchanged, setShowUnchanged] = useState(false);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="flex flex-col gap-3">
|
|
110
|
+
<FieldComparison>
|
|
111
|
+
<FieldComparisonHeader>
|
|
112
|
+
<FieldComparisonHeading>Before</FieldComparisonHeading>
|
|
113
|
+
<FieldComparisonHeading>After</FieldComparisonHeading>
|
|
114
|
+
</FieldComparisonHeader>
|
|
115
|
+
{!showUnchanged && equal(before, after) ? (
|
|
116
|
+
<p className="px-4 py-3 text-muted-foreground">No fields changed.</p>
|
|
117
|
+
) : (
|
|
118
|
+
<FormattedFields before={before} after={after} showUnchanged={showUnchanged} />
|
|
119
|
+
)}
|
|
120
|
+
</FieldComparison>
|
|
121
|
+
|
|
122
|
+
<label
|
|
123
|
+
htmlFor="history-show-unchanged"
|
|
124
|
+
className="flex items-center gap-2 self-end text-muted-foreground text-sm"
|
|
125
|
+
>
|
|
126
|
+
<Switch
|
|
127
|
+
id="history-show-unchanged"
|
|
128
|
+
checked={showUnchanged}
|
|
129
|
+
onCheckedChange={setShowUnchanged}
|
|
130
|
+
/>
|
|
131
|
+
Show unchanged fields
|
|
132
|
+
</label>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Compare fields in compact rows, using readable paths for nested values.
|
|
139
|
+
*/
|
|
140
|
+
function FormattedFields({
|
|
141
|
+
label,
|
|
142
|
+
before,
|
|
143
|
+
after,
|
|
144
|
+
showUnchanged,
|
|
145
|
+
}: {
|
|
146
|
+
label?: string;
|
|
147
|
+
before: JSONValue | undefined;
|
|
148
|
+
after: JSONValue | undefined;
|
|
149
|
+
showUnchanged: boolean;
|
|
150
|
+
}) {
|
|
151
|
+
const changed = !equal(before, after);
|
|
152
|
+
if (!changed && !showUnchanged) return null;
|
|
153
|
+
const beforeText = editorText(before);
|
|
154
|
+
const afterText = editorText(after);
|
|
155
|
+
const left = beforeText ?? before;
|
|
156
|
+
const right = afterText ?? after;
|
|
157
|
+
|
|
158
|
+
if ((isObject(left) || left === undefined) && (isObject(right) || right === undefined)) {
|
|
159
|
+
const keys = [...new Set([...Object.keys(left ?? {}), ...Object.keys(right ?? {})])].toSorted();
|
|
160
|
+
if (keys.length) {
|
|
161
|
+
return (
|
|
162
|
+
<>
|
|
163
|
+
{keys.map((key) => (
|
|
164
|
+
<FormattedFields
|
|
165
|
+
key={key}
|
|
166
|
+
label={label ? `${label} › ${fieldLabel(key)}` : fieldLabel(key)}
|
|
167
|
+
before={Object.hasOwn(left ?? {}, key) ? left?.[key] : undefined}
|
|
168
|
+
after={Object.hasOwn(right ?? {}, key) ? right?.[key] : undefined}
|
|
169
|
+
showUnchanged={showUnchanged}
|
|
170
|
+
/>
|
|
171
|
+
))}
|
|
172
|
+
</>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const words =
|
|
178
|
+
typeof left === "string" && typeof right === "string" ? diffWordsWithSpace(left, right) : null;
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<FieldComparisonRow aria-label={label ?? "Value"}>
|
|
182
|
+
<FieldComparisonCell>
|
|
183
|
+
<FieldComparisonLabel className="flex flex-wrap items-center gap-2">
|
|
184
|
+
{label ?? "Value"}
|
|
185
|
+
{!changed && <span className="font-normal">Unchanged</span>}
|
|
186
|
+
</FieldComparisonLabel>
|
|
187
|
+
{words && left !== "" ? (
|
|
188
|
+
<span className="whitespace-pre-wrap break-words">
|
|
189
|
+
{words.map((part, index) => {
|
|
190
|
+
if (part.added) return null;
|
|
191
|
+
if (!part.removed) return part.value;
|
|
192
|
+
return (
|
|
193
|
+
<FieldComparisonRemoved key={`${index}:${part.value}`}>
|
|
194
|
+
{part.value}
|
|
195
|
+
</FieldComparisonRemoved>
|
|
196
|
+
);
|
|
197
|
+
})}
|
|
198
|
+
</span>
|
|
199
|
+
) : (
|
|
200
|
+
<div
|
|
201
|
+
className={changed && after === undefined ? "text-destructive line-through" : undefined}
|
|
202
|
+
>
|
|
203
|
+
<StoredValue value={left} />
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</FieldComparisonCell>
|
|
207
|
+
<FieldComparisonCell>
|
|
208
|
+
<FieldComparisonLabel className="flex flex-wrap items-center gap-2">
|
|
209
|
+
{label ?? "Value"}
|
|
210
|
+
{!changed && <span className="font-normal">Unchanged</span>}
|
|
211
|
+
</FieldComparisonLabel>
|
|
212
|
+
{words && right !== "" ? (
|
|
213
|
+
<span className="whitespace-pre-wrap break-words">
|
|
214
|
+
{words.map((part, index) => {
|
|
215
|
+
if (part.removed) return null;
|
|
216
|
+
if (!part.added) return part.value;
|
|
217
|
+
return (
|
|
218
|
+
<FieldComparisonAdded key={`${index}:${part.value}`}>
|
|
219
|
+
{part.value}
|
|
220
|
+
</FieldComparisonAdded>
|
|
221
|
+
);
|
|
222
|
+
})}
|
|
223
|
+
</span>
|
|
224
|
+
) : (
|
|
225
|
+
<div className={changed && before === undefined ? "text-primary" : undefined}>
|
|
226
|
+
<StoredValue value={right} />
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
</FieldComparisonCell>
|
|
230
|
+
{changed && beforeText !== undefined && beforeText === afterText && (
|
|
231
|
+
<p className="col-span-2 px-4 pb-3 text-muted-foreground text-xs">
|
|
232
|
+
Formatting or structure changed. The text is unchanged.
|
|
233
|
+
</p>
|
|
234
|
+
)}
|
|
235
|
+
</FieldComparisonRow>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Show complete before/after JSON snapshots with changed lines highlighted.
|
|
241
|
+
*/
|
|
242
|
+
function TechnicalComparison({ before, after }: { before: JSONValue; after: JSONValue }) {
|
|
243
|
+
const parts = diffLines(
|
|
244
|
+
`${JSON.stringify(before, null, 2)}\n`,
|
|
245
|
+
`${JSON.stringify(after, null, 2)}\n`,
|
|
246
|
+
);
|
|
247
|
+
return (
|
|
248
|
+
<FieldComparison>
|
|
249
|
+
<FieldComparisonHeader>
|
|
250
|
+
<FieldComparisonHeading>Before snapshot</FieldComparisonHeading>
|
|
251
|
+
<FieldComparisonHeading>After snapshot</FieldComparisonHeading>
|
|
252
|
+
</FieldComparisonHeader>
|
|
253
|
+
<div className="py-2">
|
|
254
|
+
{parts.map((part, index) => {
|
|
255
|
+
if (part.added && parts[index - 1]?.removed) return null;
|
|
256
|
+
const addition = part.removed ? parts[index + 1] : part;
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<FieldComparisonRow key={`${index}:${part.value}`} className="not-last:border-b-0">
|
|
260
|
+
<FieldComparisonCell className="py-0 font-mono text-xs leading-6">
|
|
261
|
+
<pre className="whitespace-pre-wrap break-all">
|
|
262
|
+
{part.removed && (
|
|
263
|
+
<FieldComparisonRemoved className="block rounded-none no-underline">
|
|
264
|
+
{part.value}
|
|
265
|
+
</FieldComparisonRemoved>
|
|
266
|
+
)}
|
|
267
|
+
{!part.removed && !part.added && part.value}
|
|
268
|
+
</pre>
|
|
269
|
+
</FieldComparisonCell>
|
|
270
|
+
<FieldComparisonCell className="py-0 font-mono text-xs leading-6">
|
|
271
|
+
<pre className="whitespace-pre-wrap break-all">
|
|
272
|
+
{addition?.added && (
|
|
273
|
+
<FieldComparisonAdded className="block rounded-none">
|
|
274
|
+
{addition.value}
|
|
275
|
+
</FieldComparisonAdded>
|
|
276
|
+
)}
|
|
277
|
+
{!part.removed && !part.added && part.value}
|
|
278
|
+
</pre>
|
|
279
|
+
</FieldComparisonCell>
|
|
280
|
+
</FieldComparisonRow>
|
|
281
|
+
);
|
|
282
|
+
})}
|
|
283
|
+
</div>
|
|
284
|
+
</FieldComparison>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Reconstruct the final resource once, then display field comparisons independently of patch operations.
|
|
290
|
+
*/
|
|
291
|
+
export function HistoryDiffComparison({ diff }: { diff: HistoryDiff | null }) {
|
|
292
|
+
if (!diff) {
|
|
293
|
+
return (
|
|
294
|
+
<Empty className="min-h-[80vh] border py-12">
|
|
295
|
+
<EmptyHeader>
|
|
296
|
+
<EmptyMedia variant="icon">
|
|
297
|
+
<FileDiffIcon aria-hidden="true" />
|
|
298
|
+
</EmptyMedia>
|
|
299
|
+
<EmptyTitle>No diff recorded</EmptyTitle>
|
|
300
|
+
<EmptyDescription>
|
|
301
|
+
Detailed changes were not recorded for this history entry.
|
|
302
|
+
</EmptyDescription>
|
|
303
|
+
</EmptyHeader>
|
|
304
|
+
</Empty>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const parsed = historyDiffInputSchema.safeParse(diff);
|
|
309
|
+
if (!parsed.success) {
|
|
310
|
+
return (
|
|
311
|
+
<Empty className="min-h-[80vh] border py-12">
|
|
312
|
+
<EmptyHeader>
|
|
313
|
+
<EmptyMedia variant="icon">
|
|
314
|
+
<FileDiffIcon aria-hidden="true" />
|
|
315
|
+
</EmptyMedia>
|
|
316
|
+
<EmptyTitle>Diff unavailable</EmptyTitle>
|
|
317
|
+
<EmptyDescription>
|
|
318
|
+
This history entry uses a diff format that cannot be displayed.
|
|
319
|
+
</EmptyDescription>
|
|
320
|
+
</EmptyHeader>
|
|
321
|
+
</Empty>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
const after = applyHistoryChanges(parsed.data.snapshot, parsed.data.changes);
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<div className="flex flex-col gap-3">
|
|
328
|
+
<Collapsible defaultOpen>
|
|
329
|
+
<CollapsibleTrigger asChild>
|
|
330
|
+
<Button variant="outline" className="group w-full">
|
|
331
|
+
Changes
|
|
332
|
+
<ChevronDownIcon className="ml-auto group-data-[state=open]:rotate-180" />
|
|
333
|
+
</Button>
|
|
334
|
+
</CollapsibleTrigger>
|
|
335
|
+
|
|
336
|
+
<CollapsibleContent className="pt-3 pb-8">
|
|
337
|
+
<FormattedComparison
|
|
338
|
+
before={
|
|
339
|
+
parsed.data.changes[0]?.op === "add" && parsed.data.changes[0].path === ""
|
|
340
|
+
? undefined
|
|
341
|
+
: parsed.data.snapshot
|
|
342
|
+
}
|
|
343
|
+
after={after}
|
|
344
|
+
/>
|
|
345
|
+
</CollapsibleContent>
|
|
346
|
+
</Collapsible>
|
|
347
|
+
|
|
348
|
+
<Collapsible>
|
|
349
|
+
<CollapsibleTrigger asChild>
|
|
350
|
+
<Button variant="outline" className="group w-full">
|
|
351
|
+
Technical changes
|
|
352
|
+
<ChevronDownIcon className="ml-auto group-data-[state=open]:rotate-180" />
|
|
353
|
+
</Button>
|
|
354
|
+
</CollapsibleTrigger>
|
|
355
|
+
|
|
356
|
+
<CollapsibleContent className="pt-3 pb-6">
|
|
357
|
+
<TechnicalComparison before={parsed.data.snapshot} after={after} />
|
|
358
|
+
</CollapsibleContent>
|
|
359
|
+
</Collapsible>
|
|
360
|
+
|
|
361
|
+
<Collapsible>
|
|
362
|
+
<CollapsibleTrigger asChild>
|
|
363
|
+
<Button variant="outline" className="group w-full">
|
|
364
|
+
Payload
|
|
365
|
+
<ChevronDownIcon className="ml-auto group-data-[state=open]:rotate-180" />
|
|
366
|
+
</Button>
|
|
367
|
+
</CollapsibleTrigger>
|
|
368
|
+
|
|
369
|
+
<CollapsibleContent className="pt-3 pb-6">
|
|
370
|
+
<pre className="max-h-96 overflow-auto rounded-md border bg-muted/30 p-4 text-xs leading-6">
|
|
371
|
+
{JSON.stringify(parsed.data.changes, null, 2)}
|
|
372
|
+
</pre>
|
|
373
|
+
</CollapsibleContent>
|
|
374
|
+
</Collapsible>
|
|
375
|
+
</div>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { cn } from "@tulip-systems/ui/cn";
|
|
2
|
+
import type { ComponentProps } from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Contain a two-column comparison in a bordered card that scrolls on narrow screens.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <FieldComparison>
|
|
9
|
+
* <FieldComparisonHeader>
|
|
10
|
+
* <FieldComparisonHeading>Before</FieldComparisonHeading>
|
|
11
|
+
* <FieldComparisonHeading>After</FieldComparisonHeading>
|
|
12
|
+
* </FieldComparisonHeader>
|
|
13
|
+
* <FieldComparisonRow>
|
|
14
|
+
* <FieldComparisonCell>
|
|
15
|
+
* <FieldComparisonLabel>Status</FieldComparisonLabel>
|
|
16
|
+
* <FieldComparisonRemoved>Planned</FieldComparisonRemoved>
|
|
17
|
+
* </FieldComparisonCell>
|
|
18
|
+
* <FieldComparisonCell>
|
|
19
|
+
* <FieldComparisonLabel>Status</FieldComparisonLabel>
|
|
20
|
+
* <FieldComparisonAdded>Active</FieldComparisonAdded>
|
|
21
|
+
* </FieldComparisonCell>
|
|
22
|
+
* </FieldComparisonRow>
|
|
23
|
+
* </FieldComparison>
|
|
24
|
+
*/
|
|
25
|
+
function FieldComparison({ className, children, ...props }: ComponentProps<"div">) {
|
|
26
|
+
return (
|
|
27
|
+
<div className={cn("overflow-x-auto rounded-md border border-border", className)} {...props}>
|
|
28
|
+
<div className="min-w-[36rem]">{children}</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Align two column headings above the comparison rows.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* <FieldComparisonHeader>
|
|
38
|
+
* <FieldComparisonHeading>Before</FieldComparisonHeading>
|
|
39
|
+
* <FieldComparisonHeading>After</FieldComparisonHeading>
|
|
40
|
+
* </FieldComparisonHeader>
|
|
41
|
+
*/
|
|
42
|
+
function FieldComparisonHeader({ className, ...props }: ComponentProps<"div">) {
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
className={cn("grid grid-cols-2 border-border border-b bg-muted/50", className)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Label a column, matching the content spacing and center divider below it.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* <FieldComparisonHeading>Before snapshot</FieldComparisonHeading>
|
|
56
|
+
*/
|
|
57
|
+
function FieldComparisonHeading({ className, ...props }: ComponentProps<"div">) {
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
className={cn(
|
|
61
|
+
"min-w-0 border-border px-4 py-2 font-medium text-muted-foreground text-xs even:border-l",
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
64
|
+
{...props}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Align a pair of cells with equal widths and a subtle separator between rows.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* <FieldComparisonRow>
|
|
74
|
+
* <FieldComparisonCell>Planned</FieldComparisonCell>
|
|
75
|
+
* <FieldComparisonCell>Active</FieldComparisonCell>
|
|
76
|
+
* </FieldComparisonRow>
|
|
77
|
+
*/
|
|
78
|
+
function FieldComparisonRow({ className, ...props }: ComponentProps<"div">) {
|
|
79
|
+
return (
|
|
80
|
+
<div className={cn("grid grid-cols-2 border-border not-last:border-b", className)} {...props} />
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Hold arbitrary comparison content with consistent padding and a center divider.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* <FieldComparisonCell>
|
|
89
|
+
* <FieldComparisonLabel>Title</FieldComparisonLabel>
|
|
90
|
+
* Swimwise
|
|
91
|
+
* </FieldComparisonCell>
|
|
92
|
+
*/
|
|
93
|
+
function FieldComparisonCell({ className, ...props }: ComponentProps<"div">) {
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
className={cn(
|
|
97
|
+
"min-w-0 break-words border-border px-4 py-2.5 text-sm leading-relaxed even:border-l",
|
|
98
|
+
className,
|
|
99
|
+
)}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Display a muted field label above a cell's value.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* <FieldComparisonLabel>Settings › Visibility</FieldComparisonLabel>
|
|
110
|
+
*/
|
|
111
|
+
function FieldComparisonLabel({ className, ...props }: ComponentProps<"div">) {
|
|
112
|
+
return (
|
|
113
|
+
<div className={cn("mb-1 font-medium text-muted-foreground text-xs", className)} {...props} />
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Mark removed content with semantic deletion markup and Tulip's destructive color.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* <FieldComparisonRemoved>Planned</FieldComparisonRemoved>
|
|
122
|
+
*/
|
|
123
|
+
function FieldComparisonRemoved({ className, ...props }: ComponentProps<"del">) {
|
|
124
|
+
return (
|
|
125
|
+
<del className={cn("rounded-sm bg-destructive/10 text-destructive", className)} {...props} />
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Mark added content with semantic insertion markup and Tulip's primary color.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* <FieldComparisonAdded>Active</FieldComparisonAdded>
|
|
134
|
+
*/
|
|
135
|
+
function FieldComparisonAdded({ className, ...props }: ComponentProps<"ins">) {
|
|
136
|
+
return (
|
|
137
|
+
<ins
|
|
138
|
+
className={cn("rounded-sm bg-primary/10 text-primary no-underline", className)}
|
|
139
|
+
{...props}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export {
|
|
145
|
+
FieldComparison,
|
|
146
|
+
FieldComparisonAdded,
|
|
147
|
+
FieldComparisonCell,
|
|
148
|
+
FieldComparisonHeader,
|
|
149
|
+
FieldComparisonHeading,
|
|
150
|
+
FieldComparisonLabel,
|
|
151
|
+
FieldComparisonRemoved,
|
|
152
|
+
FieldComparisonRow,
|
|
153
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
4
|
+
import type { TableColumnDef } from "@tulip-systems/data-tables";
|
|
5
|
+
import {
|
|
6
|
+
createTableConfig,
|
|
7
|
+
TableConfigProvider,
|
|
8
|
+
usePaginationStrategy,
|
|
9
|
+
} from "@tulip-systems/data-tables/client";
|
|
10
|
+
import { offsetPagination } from "@tulip-systems/query";
|
|
11
|
+
import { useFilters, usePagination, useSorting } from "@tulip-systems/query/client";
|
|
12
|
+
import { type PropsWithChildren, useDeferredValue, useMemo } from "react";
|
|
13
|
+
import { orpc } from "@/server/router/client";
|
|
14
|
+
import type { HistoryColumn } from "../_config/columns-data";
|
|
15
|
+
import { historyFilters } from "../_config/filters";
|
|
16
|
+
import { historyTableSorts } from "../_config/sorting";
|
|
17
|
+
|
|
18
|
+
export function HistoryPaginationTableProvider({
|
|
19
|
+
columns,
|
|
20
|
+
children,
|
|
21
|
+
}: PropsWithChildren<{ columns: TableColumnDef<HistoryColumn>[] }>) {
|
|
22
|
+
const filtering = useFilters(historyFilters);
|
|
23
|
+
const sorting = useSorting(historyTableSorts);
|
|
24
|
+
const pagination = usePagination(offsetPagination);
|
|
25
|
+
const input = useDeferredValue({
|
|
26
|
+
pagination: pagination.input,
|
|
27
|
+
filters: filtering.input,
|
|
28
|
+
sorts: sorting.input,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const response = useSuspenseQuery(orpc.history.list.queryOptions({ input }));
|
|
32
|
+
const queryData = useMemo(() => response.data.data ?? [], [response.data.data]);
|
|
33
|
+
const { total } = response.data.pagination;
|
|
34
|
+
|
|
35
|
+
const strategy = usePaginationStrategy({ pagination, total });
|
|
36
|
+
|
|
37
|
+
const config = createTableConfig<HistoryColumn>({
|
|
38
|
+
queryData,
|
|
39
|
+
getRowId: (row) => row.id,
|
|
40
|
+
columns,
|
|
41
|
+
strategy,
|
|
42
|
+
filtering,
|
|
43
|
+
sorting,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return <TableConfigProvider config={config}>{children}</TableConfigProvider>;
|
|
47
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
4
|
+
import { TableToolbar, TableToolbarList } from "@tulip-systems/data-tables";
|
|
5
|
+
import { resolveFilterDescriptors } from "@tulip-systems/query";
|
|
6
|
+
import { FilterCombobox, FilterSearch } from "@tulip-systems/query/client";
|
|
7
|
+
import { Suspense } from "react";
|
|
8
|
+
import { orpc } from "@/server/router/client";
|
|
9
|
+
import { historyFilters } from "../_config/filters";
|
|
10
|
+
|
|
11
|
+
const descriptors = resolveFilterDescriptors(historyFilters);
|
|
12
|
+
|
|
13
|
+
export function HistoryTableToolbar() {
|
|
14
|
+
return (
|
|
15
|
+
<TableToolbar>
|
|
16
|
+
<TableToolbarList>
|
|
17
|
+
<FilterSearch
|
|
18
|
+
placeholder="Search resources, actions or actors…"
|
|
19
|
+
filter={descriptors.search}
|
|
20
|
+
/>
|
|
21
|
+
|
|
22
|
+
<Suspense>
|
|
23
|
+
<HistoryResourceFilter />
|
|
24
|
+
<HistoryActionFilter />
|
|
25
|
+
<HistoryActorFilter />
|
|
26
|
+
</Suspense>
|
|
27
|
+
</TableToolbarList>
|
|
28
|
+
</TableToolbar>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Available resource types are supplied by recorded entries.
|
|
34
|
+
*/
|
|
35
|
+
function HistoryResourceFilter() {
|
|
36
|
+
const { data } = useSuspenseQuery(orpc.history.filterResourceTypes.queryOptions());
|
|
37
|
+
return <FilterCombobox title="Resource" filter={descriptors.resourceType} options={data} />;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Action keys remain open so modules can introduce additional actions.
|
|
42
|
+
*/
|
|
43
|
+
function HistoryActionFilter() {
|
|
44
|
+
const { data } = useSuspenseQuery(orpc.history.filterActions.queryOptions());
|
|
45
|
+
return <FilterCombobox title="Action" filter={descriptors.action} options={data} />;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Account IDs identify actors even when their recorded names change.
|
|
50
|
+
*/
|
|
51
|
+
function HistoryActorFilter() {
|
|
52
|
+
const { data } = useSuspenseQuery(orpc.history.filterActors.queryOptions());
|
|
53
|
+
return <FilterCombobox title="Who" filter={descriptors.actorId} options={data} />;
|
|
54
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { formatDateTime } from "@tulip-systems/core";
|
|
4
|
+
import { type TableColumnDef, TableColumnHeader } from "@tulip-systems/data-tables";
|
|
5
|
+
import { TableTextCell } from "@tulip-systems/data-tables/client";
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
import type { HistoryEntry } from "../_lib/validators";
|
|
8
|
+
|
|
9
|
+
export type HistoryColumn = HistoryEntry;
|
|
10
|
+
|
|
11
|
+
export const historyColumns: TableColumnDef<HistoryColumn>[] = [
|
|
12
|
+
{
|
|
13
|
+
id: "summary",
|
|
14
|
+
// Render stored resource facts verbatim; modules own labels and context order.
|
|
15
|
+
accessorFn: (entry) =>
|
|
16
|
+
[entry.resourceType, entry.resourceLabel, entry.action].filter(Boolean).join(" · "),
|
|
17
|
+
enableSorting: false,
|
|
18
|
+
header: ({ column }) => <TableColumnHeader column={column} title="What happened" />,
|
|
19
|
+
cell: ({ getValue, row }) => (
|
|
20
|
+
<TableTextCell className="whitespace-nowrap">
|
|
21
|
+
<Link className="hover:underline" href={`/admin/history/${row.original.id}`}>
|
|
22
|
+
{getValue<string>()}
|
|
23
|
+
|
|
24
|
+
{row.original.context.length > 0 && (
|
|
25
|
+
<span className="text-muted-foreground">
|
|
26
|
+
{" — "}
|
|
27
|
+
{row.original.context.map((item) => `${item.type}: ${item.label}`).join(" › ")}
|
|
28
|
+
</span>
|
|
29
|
+
)}
|
|
30
|
+
</Link>
|
|
31
|
+
</TableTextCell>
|
|
32
|
+
),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
accessorKey: "actorName",
|
|
36
|
+
header: ({ column }) => <TableColumnHeader column={column} title="Who" />,
|
|
37
|
+
cell: ({ row }) => <TableTextCell>{row.getValue("actorName")}</TableTextCell>,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "action",
|
|
41
|
+
accessorFn: (entry) => entry.action,
|
|
42
|
+
header: ({ column }) => <TableColumnHeader column={column} title="Action" />,
|
|
43
|
+
cell: ({ row }) => <TableTextCell>{row.getValue("action")}</TableTextCell>,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
accessorKey: "resourceLabel",
|
|
47
|
+
header: ({ column }) => <TableColumnHeader column={column} title="Resource title" />,
|
|
48
|
+
cell: ({ row }) => (
|
|
49
|
+
<TableTextCell className="whitespace-nowrap">{row.getValue("resourceLabel")}</TableTextCell>
|
|
50
|
+
),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
accessorKey: "occurredAt",
|
|
54
|
+
header: ({ column }) => <TableColumnHeader column={column} title="When" />,
|
|
55
|
+
cell: ({ row }) => (
|
|
56
|
+
<TableTextCell className="whitespace-nowrap">
|
|
57
|
+
{formatDateTime(row.original.occurredAt)}
|
|
58
|
+
</TableTextCell>
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
];
|