From 27d3a43c598a020f9d98e5f5e0d4352c07b5934a Mon Sep 17 00:00:00 2001 From: Mathias Vagni Date: Tue, 17 Oct 2023 09:54:57 +0200 Subject: [PATCH] Migrate all API calls to threads --- README.md | 10 +- package-lock.json | 100 +----------- package.json | 2 +- pages/api/contact-form.ts | 84 +++++----- pages/index.tsx | 2 +- src/{components => }/contactForm.module.css | 0 src/{components => }/contactForm.tsx | 161 ++++++++++++++++---- src/custom-timeline-entry.ts | 137 ----------------- src/{issue.ts => label.ts} | 20 +-- src/thread.ts | 57 +++++++ 10 files changed, 252 insertions(+), 321 deletions(-) rename src/{components => }/contactForm.module.css (100%) rename src/{components => }/contactForm.tsx (63%) delete mode 100644 src/custom-timeline-entry.ts rename src/{issue.ts => label.ts} (51%) create mode 100644 src/thread.ts diff --git a/README.md b/README.md index 97f36cf..e45a70b 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,11 @@ You will then need to make a file called `.env.local` file with the following d PLAIN_API_KEY=plainApiKey_XXX # The issue type ids you created: -NEXT_PUBLIC_PLAIN_ISSUE_TYPE_ID_BUG=it_XXX -NEXT_PUBLIC_PLAIN_ISSUE_TYPE_ID_DEMO=it_XXX -NEXT_PUBLIC_PLAIN_ISSUE_TYPE_ID_FEATURE=it_XXX -NEXT_PUBLIC_PLAIN_ISSUE_TYPE_ID_SECURITY=it_XXX -NEXT_PUBLIC_PLAIN_ISSUE_TYPE_ID_QUESTION=it_XXX +NEXT_PUBLIC_PLAIN_LABEL_TYPE_ID_BUG=it_XXX +NEXT_PUBLIC_PLAIN_LABEL_TYPE_ID_DEMO=it_XXX +NEXT_PUBLIC_PLAIN_LABEL_TYPE_ID_FEATURE=it_XXX +NEXT_PUBLIC_PLAIN_LABEL_TYPE_ID_SECURITY=it_XXX +NEXT_PUBLIC_PLAIN_LABEL_TYPE_ID_QUESTION=it_XXX ``` After that you can run `npm install` followed by `npm run dev` to run the NextJS app and try it out! diff --git a/package-lock.json b/package-lock.json index 2a3c15b..41a6090 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@radix-ui/react-checkbox": "1.0.3", "@radix-ui/react-icons": "1.3.0", "@radix-ui/react-select": "1.2.1", - "@team-plain/typescript-sdk": "2.11.0", + "@team-plain/typescript-sdk": "2.18.0", "@types/lodash": "4.14.194", "@types/ua-parser-js": "0.7.36", "assert-ts": "0.3.4", @@ -755,13 +755,11 @@ } }, "node_modules/@team-plain/typescript-sdk": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@team-plain/typescript-sdk/-/typescript-sdk-2.11.0.tgz", - "integrity": "sha512-abNYIOD50WvUCwo6TS1jFgTm40Y3mkAGTo4DcoVnOdGT6hVR3+/Iygmt2AkLJ6E0SMirFd7DDnEFilLl7dz/rw==", - "hasInstallScript": true, + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@team-plain/typescript-sdk/-/typescript-sdk-2.18.0.tgz", + "integrity": "sha512-rvCncKjRieddv0+sAOTxZITi5H4KdBe5N/96Rqs7JGfv4VExv5NHJ0kg49hmvlRZSbWFQWb8ey3LrIW6K7OdWg==", "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", - "axios": "^1.4.0", "graphql": "^16.6.0", "zod": "^3.21.4" } @@ -1109,11 +1107,6 @@ "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==", "dev": true }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -1135,16 +1128,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/axobject-query": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", @@ -1309,17 +1292,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1465,14 +1437,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -2228,25 +2192,6 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -2256,19 +2201,6 @@ "is-callable": "^1.1.3" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3192,25 +3124,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -3691,11 +3604,6 @@ "react-is": "^16.13.1" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", diff --git a/package.json b/package.json index c007109..0fbe72d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@radix-ui/react-checkbox": "1.0.3", "@radix-ui/react-icons": "1.3.0", "@radix-ui/react-select": "1.2.1", - "@team-plain/typescript-sdk": "2.11.0", + "@team-plain/typescript-sdk": "2.18.0", "@types/lodash": "4.14.194", "@types/ua-parser-js": "0.7.36", "assert-ts": "0.3.4", diff --git a/pages/api/contact-form.ts b/pages/api/contact-form.ts index 43142d2..09cf940 100644 --- a/pages/api/contact-form.ts +++ b/pages/api/contact-form.ts @@ -1,6 +1,12 @@ import { inspect } from 'util'; -import { PlainClient, UpsertCustomTimelineEntryInput } from '@team-plain/typescript-sdk'; +import { + uiComponent, + CreateThreadInput, + PlainClient, + PlainSDKError, +} from '@team-plain/typescript-sdk'; import type { NextApiRequest, NextApiResponse } from 'next'; +import UAParser from 'ua-parser-js'; const apiKey = process.env.PLAIN_API_KEY; @@ -17,32 +23,36 @@ export type ResponseData = { }; export type RequestBody = { - customer: { - name: string; - email: string; - }; - customeTimelineEntry: { - title: string; - components: UpsertCustomTimelineEntryInput['components']; - }; - issue: { - issueTypeId: string; - priority: number | null; - }; + name: string; + email: string; + title: string; + components: CreateThreadInput['components']; + labelTypeIds: string[]; + priority: 0 | 1 | 2 | 3 | number; }; +function logError(err: PlainSDKError) { + // This ensures the full error is logged + console.error(inspect(err, { showHidden: false, depth: null, colors: true })); +} + + + +/** + * The API handler, this is what accepts our contact form request and submits it to Plain + */ export default async function handler(req: NextApiRequest, res: NextApiResponse) { // In production validation of the request body might be necessary. - const body = JSON.parse(req.body) as RequestBody; + const reqBody = JSON.parse(req.body) as RequestBody; const upsertCustomerRes = await client.upsertCustomer({ identifier: { - emailAddress: body.customer.email, + emailAddress: reqBody.email, }, onCreate: { - fullName: body.customer.name, + fullName: reqBody.name, email: { - email: body.customer.email, + email: reqBody.email, isVerified: true, }, }, @@ -50,42 +60,28 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< }); if (upsertCustomerRes.error) { - console.error( - inspect(upsertCustomerRes.error, { showHidden: false, depth: null, colors: true }) - ); + logError(upsertCustomerRes.error); return res.status(500).json({ error: upsertCustomerRes.error.message }); } console.log(`Customer upserted ${upsertCustomerRes.data.customer.id}`); - const upsertTimelineEntryRes = await client.upsertCustomTimelineEntry({ - customerId: upsertCustomerRes.data.customer.id, - title: body.customeTimelineEntry.title, - components: body.customeTimelineEntry.components, - changeCustomerStatusToActive: true, - }); - - if (upsertTimelineEntryRes.error) { - console.error( - inspect(upsertTimelineEntryRes.error, { showHidden: false, depth: null, colors: true }) - ); - return res.status(500).json({ error: upsertTimelineEntryRes.error.message }); - } - - console.log(`Custom timeline entry upserted ${upsertTimelineEntryRes.data.timelineEntry.id}.`); - - const createIssueRes = await client.createIssue({ - customerId: upsertCustomerRes.data.customer.id, - issueTypeId: body.issue.issueTypeId, - priorityValue: body.issue.priority, + const createThreadRes = await client.createThread({ + customerIdentifier: { + customerId: upsertCustomerRes.data.customer.id, + }, + title: reqBody.title, + components: reqBody.components, + labelTypeIds: reqBody.labelTypeIds, + priority: reqBody.priority, }); - if (createIssueRes.error) { - console.error(inspect(createIssueRes.error, { showHidden: false, depth: null, colors: true })); - return res.status(500).json({ error: createIssueRes.error.message }); + if (createThreadRes.error) { + logError(createThreadRes.error); + return res.status(500).json({ error: createThreadRes.error.message }); } - console.log(`Issue created ${createIssueRes.data.id}`); + console.log(`Thread created ${createThreadRes.data.id}`); res.status(200).json({ error: null }); } diff --git a/pages/index.tsx b/pages/index.tsx index d61aac8..a642f8f 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,6 +1,6 @@ import type { NextPage } from 'next'; import Head from 'next/head'; -import { ContactForm } from '../src/components/contactForm'; +import { ContactForm } from '../src/contactForm'; import { Layout } from '../src/components/layout'; const Home: NextPage = () => { diff --git a/src/components/contactForm.module.css b/src/contactForm.module.css similarity index 100% rename from src/components/contactForm.module.css rename to src/contactForm.module.css diff --git a/src/components/contactForm.tsx b/src/contactForm.tsx similarity index 63% rename from src/components/contactForm.tsx rename to src/contactForm.tsx index 784e3dd..6f90d9d 100644 --- a/src/components/contactForm.tsx +++ b/src/contactForm.tsx @@ -1,22 +1,127 @@ -import { useEffect, useState } from 'react'; -import { SelectInput } from './selectInput'; -import { FormField } from './formField'; -import { TextInput } from './textInput'; +import { useState } from 'react'; +import { SelectInput } from './components/selectInput'; +import { FormField } from './components/formField'; +import { TextInput } from './components/textInput'; import styles from './contactForm.module.css'; -import { Textarea } from './textarea'; -import { Checkbox } from './checkbox'; -import { Button } from './button'; +import { Textarea } from './components/textarea'; +import { Checkbox } from './components/checkbox'; +import { Button } from './components/button'; import { toast } from 'react-hot-toast'; -import { RequestBody } from '../../pages/api/contact-form'; -import { - customTimelineEntryForBug, - customTimelineEntryForDemo, - customTimelineEntryForFeatureRequest, - customTimelineEntryForQuestion, - customTimelineEntryForSecurityReport, -} from '../custom-timeline-entry'; -import { getIssue } from '../issue'; +import { RequestBody } from '../pages/api/contact-form'; + +import { CreateThreadInput, uiComponent } from '@team-plain/typescript-sdk'; +import UAParser from 'ua-parser-js'; + +export function componentsForBug(bugDescription: string): CreateThreadInput['components'] { + const parser = new UAParser(window.navigator.userAgent); + const browser = parser.getBrowser(); + + return [ + uiComponent.text({ text: bugDescription }), + uiComponent.spacer({ spacingSize: 'S' }), + uiComponent.text({ + text: `Reported on ${window.location.href} using ${browser.name} (${browser.version})`, + size: 'S', + color: 'MUTED', + }), + ]; +} + +export function componentsForFeatureRequest(featureRequest: string) { + return [uiComponent.text({ text: featureRequest })]; +} + +export function componentsForQuestion(question: string) { + return [uiComponent.text({ text: question })]; +} + +export function componentsForSecurityTreport(securityIssue: string) { + return [uiComponent.text({ text: securityIssue })]; +} + +export function componentsForDemoRequest( + demoMessage: string, + currentProvider: string, + expectedVolume: string +) { + return [ + ...(demoMessage + ? [uiComponent.text({ text: demoMessage }), uiComponent.spacer({ spacingSize: 'S' })] + : []), + uiComponent.row({ + mainContent: [uiComponent.text({ text: 'Current provider', color: 'MUTED' })], + asideContent: [ + uiComponent.text({ + text: currentProvider, + }), + ], + }), + uiComponent.row({ + mainContent: [uiComponent.text({ text: 'Expected volume', color: 'MUTED' })], + asideContent: [ + uiComponent.text({ + text: expectedVolume, + }), + ], + }), + ]; +} + +function requiredEnvVar(key: string): string { + const envVar = process.env[key]; + + if (!envVar) { + throw new Error(`Environment variable "${key}" not set.`); + } + + return envVar; +} + +function getLabelTypeIds(formType: FormType): string[] { + if (formType === null) { + return []; + } + + const formTypeIdsMap: Record, string[]> = { + bug: [requiredEnvVar('NEXT_PUBLIC_PLAIN_LABEL_TYPE_ID_BUG')], + demo: [requiredEnvVar('NEXT_PUBLIC_PLAIN_LABEL_TYPE_ID_DEMO')], + feature: [requiredEnvVar('NEXT_PUBLIC_PLAIN_LABEL_TYPE_ID_FEATURE')], + question: [requiredEnvVar('NEXT_PUBLIC_PLAIN_LABEL_TYPE_ID_QUESTION')], + security: [requiredEnvVar('NEXT_PUBLIC_PLAIN_LABEL_TYPE_ID_SECURITY')], + }; + return formTypeIdsMap[formType]; +} + +function getPriority(formType: FormType, bugIsBlocking: boolean): 0 | 1 | 2 | 3 { + // We want to treat any security issues reported to us as urgent + if (formType === 'security') { + return 0; + } + + // We want to treat urgent bug reports as being high priority + if (formType === 'bug' && bugIsBlocking) { + return 1; + } + + // Default to a normal priority + return 2; +} + +function getTitle(formType: FormType): string { + if (formType === null) { + return 'Contact form'; + } + + const titleMap: Record, string> = { + bug: 'Bug report', + demo: 'Demo request', + feature: 'Feature suggestion', + question: 'Question', + security: 'Security report', + }; + return titleMap[formType]; +} const formOptions = [ { @@ -103,22 +208,22 @@ export function ContactForm() { setIsProcessing(false); } - function getCustomTimelineEntry(): RequestBody['customeTimelineEntry'] { + function getComponentsInput(): RequestBody['components'] { if (!formType) { throw new Error('form not set'); } switch (formType) { case 'bug': - return customTimelineEntryForBug(bugDescription); + return componentsForBug(bugDescription); case 'feature': - return customTimelineEntryForFeatureRequest(featureRequest); + return componentsForFeatureRequest(featureRequest); case 'question': - return customTimelineEntryForQuestion(question); + return componentsForQuestion(question); case 'security': - return customTimelineEntryForSecurityReport(securityIssue); + return componentsForSecurityTreport(securityIssue); case 'demo': - return customTimelineEntryForDemo( + return componentsForDemoRequest( demoMessage, demoCurrentProviderOptions.find((o) => o.value === demoCurrentProvider)?.label || '', demoExpectedVolumeOptions.find((o) => o.value === demoExpectedVolume)?.label || '' @@ -131,12 +236,12 @@ export function ContactForm() { setIsProcessing(true); const body: RequestBody = { - customer: { - name, - email, - }, - customeTimelineEntry: getCustomTimelineEntry(), - issue: getIssue(formType, bugIsBlocking), + name, + email, + title: getTitle(formType), + components: getComponentsInput(), + labelTypeIds: getLabelTypeIds(formType), + priority: getPriority(formType, bugIsBlocking), }; try { diff --git a/src/custom-timeline-entry.ts b/src/custom-timeline-entry.ts deleted file mode 100644 index 164db73..0000000 --- a/src/custom-timeline-entry.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { - ComponentSpacerSize, - ComponentTextColor, - ComponentTextSize, -} from '@team-plain/typescript-sdk'; -import UAParser from 'ua-parser-js'; - -export function customTimelineEntryForBug(bugDescription: string) { - const parser = new UAParser(window.navigator.userAgent); - const browser = parser.getBrowser(); - - return { - title: 'Bug report', - components: [ - { - componentText: { - text: bugDescription, - }, - }, - { - componentSpacer: { - spacerSize: ComponentSpacerSize.S, - }, - }, - { - componentText: { - text: `Reported on ${window.location.href} using ${browser.name} (${browser.version})`, - textSize: ComponentTextSize.S, - textColor: ComponentTextColor.Muted, - }, - }, - ], - }; -} - -export function customTimelineEntryForFeatureRequest(featureRequest: string) { - return { - title: 'Feature request', - components: [ - { - componentText: { - text: featureRequest, - }, - }, - ], - }; -} - -export function customTimelineEntryForQuestion(question: string) { - return { - title: 'General question', - components: [ - { - componentText: { - text: question, - }, - }, - ], - }; -} - -export function customTimelineEntryForSecurityReport(securityIssue: string) { - return { - title: 'Security report', - components: [ - { - componentText: { - text: securityIssue, - }, - }, - ], - }; -} - -export function customTimelineEntryForDemo( - demoMessage: string, - currentProvider: string, - expectedVolume: string -) { - return { - title: 'Demo request', - components: [ - ...(demoMessage - ? [ - { - componentText: { - text: demoMessage, - }, - }, - { - componentSpacer: { - spacerSize: ComponentSpacerSize.S, - }, - }, - ] - : []), - { - componentRow: { - rowMainContent: [ - { - componentText: { - text: 'Current provider', - color: ComponentTextColor.Muted, - }, - }, - ], - rowAsideContent: [ - { - componentText: { - text: currentProvider, - }, - }, - ], - }, - }, - { - componentRow: { - rowMainContent: [ - { - componentText: { - text: 'Expected volume', - color: ComponentTextColor.Muted, - }, - }, - ], - rowAsideContent: [ - { - componentText: { - text: expectedVolume, - }, - }, - ], - }, - }, - ], - }; -} diff --git a/src/issue.ts b/src/label.ts similarity index 51% rename from src/issue.ts rename to src/label.ts index 64ef2c2..b754b12 100644 --- a/src/issue.ts +++ b/src/label.ts @@ -1,16 +1,18 @@ -import { RequestBody } from '../pages/api/contact-form'; -import { FormType } from './components/contactForm'; +import { FormType } from './contactForm'; -export function getIssue(formType: FormType, bugIsBlocking: boolean): RequestBody['issue'] { +export function getLabelId( + formType: FormType, + bugIsBlocking: boolean +): { labelTypeId: string; priority: number | null } { if (!formType) { throw new Error('form not set'); } const issueTypeIds = { - bug: process.env.NEXT_PUBLIC_PLAIN_ISSUE_TYPE_ID_BUG || '', - demo: process.env.NEXT_PUBLIC_PLAIN_ISSUE_TYPE_ID_DEMO || '', - feature: process.env.NEXT_PUBLIC_PLAIN_ISSUE_TYPE_ID_FEATURE || '', - security: process.env.NEXT_PUBLIC_PLAIN_ISSUE_TYPE_ID_SECURITY || '', - question: process.env.NEXT_PUBLIC_PLAIN_ISSUE_TYPE_ID_QUESTION || '', + bug: process.env.NEXT_PUBLIC_PLAIN_LABEL_TYPE_ID_BUG || '', + demo: process.env.NEXT_PUBLIC_PLAIN_LABEL_TYPE_ID_DEMO || '', + feature: process.env.NEXT_PUBLIC_PLAIN_LABEL_TYPE_ID_FEATURE || '', + security: process.env.NEXT_PUBLIC_PLAIN_LABEL_TYPE_ID_SECURITY || '', + question: process.env.NEXT_PUBLIC_PLAIN_LABEL_TYPE_ID_QUESTION || '', } as const; const issueTypeId = issueTypeIds[formType]; @@ -20,7 +22,7 @@ export function getIssue(formType: FormType, bugIsBlocking: boolean): RequestBod } return { - issueTypeId, + labelTypeId: issueTypeId, // In this example contact form if an issue is blocking the user than we want // to make sure the created issue is urgent. Otherwise we're ok with the default // which is set per issue type in the Plain settings. diff --git a/src/thread.ts b/src/thread.ts new file mode 100644 index 0000000..d74c7e4 --- /dev/null +++ b/src/thread.ts @@ -0,0 +1,57 @@ +import { CreateThreadInput, uiComponent } from '@team-plain/typescript-sdk'; +import UAParser from 'ua-parser-js'; + +export function componentsForBug(bugDescription: string): CreateThreadInput['components'] { + const parser = new UAParser(window.navigator.userAgent); + const browser = parser.getBrowser(); + + return [ + uiComponent.text({ text: bugDescription }), + uiComponent.spacer({ spacingSize: 'S' }), + uiComponent.text({ + text: `Reported on ${window.location.href} using ${browser.name} (${browser.version})`, + size: 'S', + color: 'MUTED', + }), + ]; +} + +export function componentsForFeatureRequest(featureRequest: string) { + return [uiComponent.text({ text: featureRequest })]; +} + +export function componentsForQuestion(question: string) { + return [uiComponent.text({ text: question })]; +} + +export function componentsForSecurityTreport(securityIssue: string) { + return [uiComponent.text({ text: securityIssue })]; +} + +export function componentsForDemoRequest( + demoMessage: string, + currentProvider: string, + expectedVolume: string +) { + return [ + ...(demoMessage + ? [uiComponent.text({ text: demoMessage }), uiComponent.spacer({ spacingSize: 'S' })] + : []), + uiComponent.row({ + mainContent: [uiComponent.text({ text: 'Current provider', color: 'MUTED' })], + asideContent: [ + uiComponent.text({ + text: currentProvider, + }), + ], + }), + uiComponent.row({ + mainContent: [uiComponent.text({ text: 'Expected volume', color: 'MUTED' })], + asideContent: [ + uiComponent.text({ + text: expectedVolume, + }), + ], + }), + ]; +}