From 12d95f190ff93fe59a3fe7e3b2520dd3d2f2dc92 Mon Sep 17 00:00:00 2001 From: Elise Bach Date: Wed, 10 Jul 2024 07:21:56 +0100 Subject: [PATCH 1/3] Small changes --- app/thread/[threadId]/page.module.css | 19 ++++++++++ app/thread/[threadId]/page.tsx | 50 ++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/app/thread/[threadId]/page.module.css b/app/thread/[threadId]/page.module.css index 81bb47e..06525b3 100644 --- a/app/thread/[threadId]/page.module.css +++ b/app/thread/[threadId]/page.module.css @@ -15,6 +15,9 @@ padding: 24px; background: #fff; border-left: 1px solid #eee; +} + +.threadInfoGrid { display: grid; grid-template-columns: 1fr 2fr; grid-auto-rows: max-content; @@ -22,9 +25,14 @@ } .threadInfoProp { + font-size: 14px; font-weight: bold; } +.threadInfoDesc { + font-size: 14px; +} + .message { padding: 12px; border-bottom: 1px solid #eee; @@ -65,3 +73,14 @@ font-size: 12px; color: var(--text-muted); } + +.title { + font-size: 14px; + font-weight: bold; +} + +.description { + font-size: 12px; + color: var(--text-muted); + margin-bottom: 16px; +} diff --git a/app/thread/[threadId]/page.tsx b/app/thread/[threadId]/page.tsx index 8d4f3ec..9de6808 100644 --- a/app/thread/[threadId]/page.tsx +++ b/app/thread/[threadId]/page.tsx @@ -18,6 +18,23 @@ function getFullname(actor) { } } +function getPriority(priority: 0 | 1 | 2 | 3) { + switch (priority) { + case 0: { + return "Urgent"; + } + case 1: { + return "High"; + } + case 2: { + return "Normal"; + } + case 3: { + return "Low"; + } + } +} + export default async function ThreadPage({ params, }: { @@ -177,10 +194,35 @@ export default async function ThreadPage({
-
Created by:
-
{getFullname(thread.createdBy)}
-
Created at:
-
{thread.createdAt.iso8601}
+
{thread.title}
+
{thread.description}
+ +
+
Opened by:
+
+ {getFullname(thread.createdBy)} +
+ +
Opened:
+
+ {thread.createdAt.iso8601} +
+ +
Last activity:
+
+ {thread.updatedAt.iso8601} +
+ +
Status:
+
+ In {thread.status.toLowerCase()} queue +
+ +
Priority:
+
+ {getPriority(thread.priority)} +
+
From ed631a65f10520c68124c7d01bce84f778715a9a Mon Sep 17 00:00:00 2001 From: Elise Bach Date: Wed, 10 Jul 2024 08:09:00 +0100 Subject: [PATCH 2/3] Added functions to utils --- app/thread/[threadId]/page.tsx | 64 +++++++--------------------------- components/threadRow.tsx | 24 ++++++++----- utils/createdBy.ts | 0 utils/getActorFullName.ts | 13 +++++++ utils/getFormattedDate.ts | 9 +++++ utils/getPriority.ts | 16 +++++++++ 6 files changed, 65 insertions(+), 61 deletions(-) delete mode 100644 utils/createdBy.ts create mode 100644 utils/getActorFullName.ts create mode 100644 utils/getFormattedDate.ts create mode 100644 utils/getPriority.ts diff --git a/app/thread/[threadId]/page.tsx b/app/thread/[threadId]/page.tsx index 3a76ed6..2dba261 100644 --- a/app/thread/[threadId]/page.tsx +++ b/app/thread/[threadId]/page.tsx @@ -1,50 +1,10 @@ import Navigation from "@/components/navigation"; import styles from "./page.module.css"; -import Actor from "@/components/actor"; -import { ActorPartsFragment } from "@team-plain/typescript-sdk"; -import Avatar from "@/components/avatar"; - -function getFullname(actor) { - switch (actor.__typename) { - case "CustomerActor": { - return actor.customer.fullName; - } - case "UserActor": { - return actor.user.fullName; - } - case "MachineUserActor": { - return actor.user.fullName; - } - } -} - -function getPriority(priority: 0 | 1 | 2 | 3) { - switch (priority) { - case 0: { - return "Urgent"; - } - case 1: { - return "High"; - } - case 2: { - return "Normal"; - } - case 3: { - return "Low"; - } - } -} - -export default async function ThreadPage({ - params, -}: { - params: { threadId: string }; -}) { - const apiKey = process.env.PLAIN_API_KEY; - if (!apiKey) { - throw new Error("Please set the `PLAIN_API_KEY` environment variable"); - } +import { getActorFullName } from "@/utils/getActorFullName"; +import { getFormattedDate } from "@/utils/getFormattedDate"; +import { getPriority } from "@/utils/getPriority"; +export default async function ThreadPage() { const data = await fetch("https://core-api.uk.plain.com/graphql/v1", { method: "POST", body: JSON.stringify({ @@ -130,11 +90,11 @@ export default async function ThreadPage({ "Plain-Workspace-Id": "w_01J28VHKDK5PV3DJSZAA01XGAN", Authorization: `Bearer ${process.env.PLAIN_API_KEY}`, }, - }) - .then((res) => res.json()) + }).then((res) => res.json()); const thread = data.data.thread; const timelineEntries = thread.timelineEntries; + return ( <> @@ -156,14 +116,14 @@ export default async function ThreadPage({
- {getFullname(entry.actor)[0].toUpperCase()} + {getActorFullName(entry.actor)[0].toUpperCase()}
- {getFullname(entry.actor)} + {getActorFullName(entry.actor)}
- {entry.timestamp.iso8601} + {getFormattedDate(entry.timestamp.iso8601)}
@@ -197,17 +157,17 @@ export default async function ThreadPage({
Opened by:
- {getFullname(thread.createdBy)} + {getActorFullName(thread.createdBy)}
Opened:
- {thread.createdAt.iso8601} + {getFormattedDate(thread.createdAt.iso8601)}
Last activity:
- {thread.updatedAt.iso8601} + {getFormattedDate(thread.updatedAt.iso8601)}
Status:
diff --git a/components/threadRow.tsx b/components/threadRow.tsx index c2da812..dcdc8c6 100644 --- a/components/threadRow.tsx +++ b/components/threadRow.tsx @@ -1,13 +1,19 @@ import { plainClient } from "@/utils/plainClient"; -import { ThreadPartsFragment } from "@team-plain/typescript-sdk"; -import styles from './threadRow.module.css'; +import type { ThreadPartsFragment } from "@team-plain/typescript-sdk"; +import styles from "./threadRow.module.css"; export async function ThreadRow({ thread }: { thread: ThreadPartsFragment }) { - const customer = await plainClient.getCustomerById({ customerId: thread.customer.id }); + const customer = await plainClient.getCustomerById({ + customerId: thread.customer.id, + }); - return ( - -
{customer.data?.fullName}

{thread.title}

{thread.previewText}
-
- ) -} \ No newline at end of file + return ( + +
{customer.data?.fullName}
+
+

{thread.title}

+
{thread.previewText}
+
+
+ ); +} diff --git a/utils/createdBy.ts b/utils/createdBy.ts deleted file mode 100644 index e69de29..0000000 diff --git a/utils/getActorFullName.ts b/utils/getActorFullName.ts new file mode 100644 index 0000000..e118dd3 --- /dev/null +++ b/utils/getActorFullName.ts @@ -0,0 +1,13 @@ +export function getActorFullName(actor) { + switch (actor.__typename) { + case "CustomerActor": { + return actor.customer.fullName; + } + case "UserActor": { + return actor.user.fullName; + } + case "MachineUserActor": { + return actor.user.fullName; + } + } +} diff --git a/utils/getFormattedDate.ts b/utils/getFormattedDate.ts new file mode 100644 index 0000000..c492aaf --- /dev/null +++ b/utils/getFormattedDate.ts @@ -0,0 +1,9 @@ +export function getFormattedDate(date: string) { + return new Date(date).toLocaleDateString(undefined, { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + }); +} \ No newline at end of file diff --git a/utils/getPriority.ts b/utils/getPriority.ts new file mode 100644 index 0000000..2bead6e --- /dev/null +++ b/utils/getPriority.ts @@ -0,0 +1,16 @@ +export function getPriority(priority: 0 | 1 | 2 | 3) { + switch (priority) { + case 0: { + return "Urgent"; + } + case 1: { + return "High"; + } + case 2: { + return "Normal"; + } + case 3: { + return "Low"; + } + } +} From c73c81e37872647ff522075facff4161aff92fca Mon Sep 17 00:00:00 2001 From: Elise Bach Date: Wed, 10 Jul 2024 11:19:15 +0100 Subject: [PATCH 3/3] Cleaned up + type + use rawRequest --- app/page.tsx | 66 +++++++-------- app/thread/[threadId]/page.tsx | 114 +++++--------------------- components/threadRow.tsx | 2 +- lib/fetchThreadTimelineEntries.ts | 126 +++++++++++++++++++++++++++++ {utils => lib}/getActorFullName.ts | 6 +- {utils => lib}/getFormattedDate.ts | 0 {utils => lib}/getPriority.ts | 5 +- {utils => lib}/plainClient.ts | 0 lib/types.ts | 91 +++++++++++++++++++++ 9 files changed, 281 insertions(+), 129 deletions(-) create mode 100644 lib/fetchThreadTimelineEntries.ts rename {utils => lib}/getActorFullName.ts (60%) rename {utils => lib}/getFormattedDate.ts (100%) rename {utils => lib}/getPriority.ts (66%) rename {utils => lib}/plainClient.ts (100%) create mode 100644 lib/types.ts diff --git a/app/page.tsx b/app/page.tsx index 5b975a9..748849a 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ import Navigation from "@/components/navigation"; import styles from "./page.module.css"; -import { plainClient } from "@/utils/plainClient"; +import { plainClient } from "@/lib/plainClient"; import { ThreadRow } from "@/components/threadRow"; import { PaginationControls } from "@/components/paginationControls"; @@ -10,39 +10,39 @@ export const fetchCache = "force-no-store"; const TENANT_EXTERNAL_ID = "abcd1234"; export default async function Home({ - searchParams, + searchParams, }: { - searchParams: { [key: string]: string | undefined }; + searchParams: { [key: string]: string | undefined }; }) { - const threads = await plainClient.getThreads({ - filters: { - // If you want to only allow customers to view threads they have raised then you can filter by customerIds instead. - // Note that if you provide multiple filters they are combined with AND rather than OR. - // customerIds: ["c_01J28ZQKJX9CVRXVHBMAXNSV5G"], - tenantIdentifiers: [{ externalId: TENANT_EXTERNAL_ID }], - }, - after: searchParams.after as string | undefined, - before: searchParams.before as string | undefined, - }); + const threads = await plainClient.getThreads({ + filters: { + // If you want to only allow customers to view threads they have raised then you can filter by customerIds instead. + // Note that if you provide multiple filters they are combined with AND rather than OR. + // customerIds: ["c_01J28ZQKJX9CVRXVHBMAXNSV5G"], + tenantIdentifiers: [{ externalId: TENANT_EXTERNAL_ID }], + }, + after: searchParams.after as string | undefined, + before: searchParams.before as string | undefined, + }); - return ( - <> - -
-

Support requests

- {threads.data && ( - <> -
- {threads.data?.threads.map((thread) => { - return ( - - ); - })} -
- - - )} -
- - ); + return ( + <> + +
+

Support requests

+ {threads.data && ( + <> +
+ {threads.data?.threads.map((thread) => { + return ( + + ); + })} +
+ + + )} +
+ + ); } diff --git a/app/thread/[threadId]/page.tsx b/app/thread/[threadId]/page.tsx index 2dba261..782fd0b 100644 --- a/app/thread/[threadId]/page.tsx +++ b/app/thread/[threadId]/page.tsx @@ -1,98 +1,28 @@ import Navigation from "@/components/navigation"; import styles from "./page.module.css"; -import { getActorFullName } from "@/utils/getActorFullName"; -import { getFormattedDate } from "@/utils/getFormattedDate"; -import { getPriority } from "@/utils/getPriority"; +import { getActorFullName } from "@/lib/getActorFullName"; +import { getFormattedDate } from "@/lib/getFormattedDate"; +import { getPriority } from "@/lib/getPriority"; +import { fetchThreadTimelineEntries } from "@/lib/fetchThreadTimelineEntries"; +import { plainClient } from "@/lib/plainClient"; -export default async function ThreadPage() { - const data = await fetch("https://core-api.uk.plain.com/graphql/v1", { - method: "POST", - body: JSON.stringify({ - query: `{ - thread(threadId: "th_01J299WQGA3VNQ4FDECV7JK6MC") { - title - description - priority - status - createdAt { - iso8601 - } - createdBy { - __typename - ... on UserActor { - user { - fullName - } - } - ... on CustomerActor { - customer { - fullName - } - } - ... on MachineUserActor { - machineUser { - fullName - } - } - } - updatedAt { - iso8601 - } - timelineEntries { - edges { - node { - id - timestamp { - iso8601 - } - actor { - __typename - ... on UserActor { - user { - fullName - } - } - ... on CustomerActor { - customer { - fullName - } - } - ... on MachineUserActor { - machineUser { - fullName - } - } - } - entry { - __typename - ... on CustomEntry { - title - components { - __typename - ... on ComponentText { - text - } - } - } - ... on ChatEntry { - chatId - text - } - } - } - } - } - } - }`, - }), - headers: { - "Content-Type": "application/json", - "Plain-Workspace-Id": "w_01J28VHKDK5PV3DJSZAA01XGAN", - Authorization: `Bearer ${process.env.PLAIN_API_KEY}`, - }, - }).then((res) => res.json()); +export default async function ThreadPage({ + params, +}: { + params: { threadId: string }; +}) { + const threadId = params.threadId; - const thread = data.data.thread; + const { data } = await fetchThreadTimelineEntries({ + threadId, + first: 100, + }); + + if (!data) { + return null; + } + + const thread = data.thread; const timelineEntries = thread.timelineEntries; return ( @@ -140,7 +70,7 @@ export default async function ThreadPage() { ); } - return
TODO
; + return null; })} {entry.entry.__typename === "ChatEntry" && (
{entry.entry.text}
diff --git a/components/threadRow.tsx b/components/threadRow.tsx index dcdc8c6..670d477 100644 --- a/components/threadRow.tsx +++ b/components/threadRow.tsx @@ -1,4 +1,4 @@ -import { plainClient } from "@/utils/plainClient"; +import { plainClient } from "@/lib/plainClient"; import type { ThreadPartsFragment } from "@team-plain/typescript-sdk"; import styles from "./threadRow.module.css"; diff --git a/lib/fetchThreadTimelineEntries.ts b/lib/fetchThreadTimelineEntries.ts new file mode 100644 index 0000000..5df0d14 --- /dev/null +++ b/lib/fetchThreadTimelineEntries.ts @@ -0,0 +1,126 @@ +import type { PlainSDKError } from "@team-plain/typescript-sdk"; +import { plainClient } from "./plainClient"; +import type { ThreadTimelineResult } from "./types"; + +export async function fetchThreadTimelineEntries({ + threadId, + first, + after, + last, + before, +}: { + threadId: string; + first?: number; + after?: string; + last?: number; + before?: string; +}): Promise<{ + data: ThreadTimelineResult; + error: PlainSDKError | undefined; +}> { + const query = ` + query threadTimeline($threadId: ID!, $first: Int, $after: String, $last: Int, $before: String) { + thread(threadId: $threadId) { + title + description + priority + status + createdAt { + __typename + iso8601 + } + createdBy { + __typename + ... on UserActor { + user { + fullName + } + } + ... on CustomerActor { + customer { + fullName + } + } + ... on MachineUserActor { + machineUser { + fullName + } + } + } + updatedAt { + __typename + iso8601 + } + timelineEntries(first: $first, after: $after, last: $last, before: $before) { + edges { + cursor + node { + id + timestamp { + __typename + iso8601 + } + actor { + __typename + ... on UserActor { + user { + fullName + } + } + ... on CustomerActor { + customer { + fullName + } + } + ... on MachineUserActor { + machineUser { + fullName + } + } + } + entry { + __typename + ... on CustomEntry { + title + components { + __typename + ... on ComponentText { + text + } + } + } + ... on ChatEntry { + chatId + text + } + } + } + } + pageInfo { + __typename + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + } + `; + + const result = plainClient.rawRequest({ + query, + variables: { + threadId, + first, + after, + last, + before, + }, + }); + + const data = (await result).data as ThreadTimelineResult; + const error = (await result).error; + + return { data, error }; +} diff --git a/utils/getActorFullName.ts b/lib/getActorFullName.ts similarity index 60% rename from utils/getActorFullName.ts rename to lib/getActorFullName.ts index e118dd3..76ffd48 100644 --- a/utils/getActorFullName.ts +++ b/lib/getActorFullName.ts @@ -1,4 +1,6 @@ -export function getActorFullName(actor) { +import type { Actor } from "./types"; + +export function getActorFullName(actor: Actor) { switch (actor.__typename) { case "CustomerActor": { return actor.customer.fullName; @@ -7,7 +9,7 @@ export function getActorFullName(actor) { return actor.user.fullName; } case "MachineUserActor": { - return actor.user.fullName; + return actor.machineUser.fullName; } } } diff --git a/utils/getFormattedDate.ts b/lib/getFormattedDate.ts similarity index 100% rename from utils/getFormattedDate.ts rename to lib/getFormattedDate.ts diff --git a/utils/getPriority.ts b/lib/getPriority.ts similarity index 66% rename from utils/getPriority.ts rename to lib/getPriority.ts index 2bead6e..4348f7b 100644 --- a/utils/getPriority.ts +++ b/lib/getPriority.ts @@ -1,4 +1,4 @@ -export function getPriority(priority: 0 | 1 | 2 | 3) { +export function getPriority(priority: number) { switch (priority) { case 0: { return "Urgent"; @@ -12,5 +12,8 @@ export function getPriority(priority: 0 | 1 | 2 | 3) { case 3: { return "Low"; } + default: { + return "Normal"; + } } } diff --git a/utils/plainClient.ts b/lib/plainClient.ts similarity index 100% rename from utils/plainClient.ts rename to lib/plainClient.ts diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..20968e7 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,91 @@ +import type { + DateTimePartsFragment, + PageInfoPartsFragment, + ThreadStatus, +} from "@team-plain/typescript-sdk"; + +export type Actor = + | { + __typename: "UserActor"; + user: { + fullName: string; + }; + } + | { + __typename: "CustomerActor"; + customer: { + fullName: string; + }; + } + | { + __typename: "MachineUserActor"; + machineUser: { + fullName: string; + }; + }; + +export type ComponentText = { + __typename: "ComponentText"; + text: string; +}; + +export type CustomTimelineEntryComponent = ComponentText; + +export type CustomEntry = { + __typename: "CustomEntry"; + title: string; + components: [CustomTimelineEntryComponent]; +}; + +export type ChatEntry = { + __typename: "ChatEntry"; + chatId: string; + text: string; +}; + +export type EmailEntry = { + __typename: "EmailEntry"; +}; + +export type SlackMessageEntry = { + __typename: "SlackMessageEntry"; +}; + +export type SlackReplyEntry = { + __typename: "SlackReplyEntry"; +}; + +export type TimelineEntry = + | ChatEntry + | CustomEntry + | EmailEntry + | SlackMessageEntry + | SlackReplyEntry; + +export type TimelineEntries = { + cursor: string; + node: { + id: string; + timestamp: DateTimePartsFragment; + actor: Actor; + entry: TimelineEntry; + }; +}[]; + +export type ThreadTimeline = { + title: string; + description: string; + priority: number; + status: ThreadStatus; + createdAt: DateTimePartsFragment; + createdBy: Actor; + updatedAt: DateTimePartsFragment; + timelineEntries: { + edges: TimelineEntries; + }; + pageInfo: PageInfoPartsFragment; +}; + +export type ThreadTimelineResult = { + thread: ThreadTimeline; +};