feat(tickets): add ticket collections and edit history
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
f1239ad593
commit
dbf7a0f180
2 changed files with 98 additions and 1 deletions
37
src/collections/tickets/TicketVotes.ts
Normal file
37
src/collections/tickets/TicketVotes.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import type { CollectionConfig } from "payload";
|
||||||
|
|
||||||
|
export const TicketVotes: CollectionConfig = {
|
||||||
|
slug: "ticket-votes",
|
||||||
|
admin: {
|
||||||
|
group: "Helpdesk",
|
||||||
|
defaultColumns: ["ticket", "user", "createdAt"],
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: ({ req }) => Boolean(req.user),
|
||||||
|
create: () => false,
|
||||||
|
update: () => false,
|
||||||
|
delete: () => false,
|
||||||
|
},
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ["ticket", "user"],
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "ticket",
|
||||||
|
type: "relationship",
|
||||||
|
relationTo: "tickets",
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user",
|
||||||
|
type: "relationship",
|
||||||
|
relationTo: "users",
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { CollectionConfig } from "payload";
|
import { CollectionConfig } from "payload";
|
||||||
import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
import {
|
import {
|
||||||
CLOSING_STATUSES,
|
CLOSING_STATUSES,
|
||||||
STATUS_LABEL,
|
STATUS_LABEL,
|
||||||
|
|
@ -30,6 +30,20 @@ const systemEntry = (message: string, createdAt?: string) => ({
|
||||||
created_at: createdAt ?? new Date().toISOString(),
|
created_at: createdAt ?? new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function immutableEditValue(value: unknown): string | null {
|
||||||
|
if (!isRecord(value)) return null;
|
||||||
|
return JSON.stringify({
|
||||||
|
before: value.before,
|
||||||
|
after: value.after,
|
||||||
|
editedBy: resolveUserId(value.editedBy),
|
||||||
|
editedAt: value.editedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const Tickets: CollectionConfig = {
|
export const Tickets: CollectionConfig = {
|
||||||
slug: "tickets",
|
slug: "tickets",
|
||||||
admin: {
|
admin: {
|
||||||
|
|
@ -74,6 +88,17 @@ export const Tickets: CollectionConfig = {
|
||||||
// entry appended by the create server action instead.
|
// entry appended by the create server action instead.
|
||||||
if (!prev || !prev.id) return data;
|
if (!prev || !prev.id) return data;
|
||||||
|
|
||||||
|
if (next.editHistory !== undefined) {
|
||||||
|
const previousEdits = Array.isArray(prev.editHistory) ? prev.editHistory : [];
|
||||||
|
const nextEdits = Array.isArray(next.editHistory) ? next.editHistory : [];
|
||||||
|
const existingEditsChanged = previousEdits.some(
|
||||||
|
(entry, index) => immutableEditValue(entry) !== immutableEditValue(nextEdits[index]),
|
||||||
|
);
|
||||||
|
if (nextEdits.length < previousEdits.length || existingEditsChanged) {
|
||||||
|
throw new Error("Ticket edit history is immutable.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const entries = [...((next.activity as unknown[]) ?? [])];
|
const entries = [...((next.activity as unknown[]) ?? [])];
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const changes: string[] = [];
|
const changes: string[] = [];
|
||||||
|
|
@ -230,6 +255,41 @@ export const Tickets: CollectionConfig = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "editHistory",
|
||||||
|
type: "array",
|
||||||
|
admin: {
|
||||||
|
readOnly: true,
|
||||||
|
description: "Immutable before/after snapshots captured when the author edits a ticket.",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "before",
|
||||||
|
type: "json",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "after",
|
||||||
|
type: "json",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "editedBy",
|
||||||
|
type: "relationship",
|
||||||
|
relationTo: "users",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "editedAt",
|
||||||
|
type: "date",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
readOnly: true,
|
||||||
|
date: { pickerAppearance: "dayAndTime" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "closedAt",
|
name: "closedAt",
|
||||||
type: "date",
|
type: "date",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue