Skip to content

Commit 6dc556b

Browse files
authored
New onboarding question (#1404)
* Updated “Twitter” to be “X (Twitter)” * added Textarea to storybook * Updated textarea styling to match input field * WIP adding new text field to org creation page * Added description to field * Submit feedback to Plain when an org signs up * Formatting improvement * type improvement * removed userId * Moved submitting to Plain into its own file * Change orgName with name * use sendToPlain function for the help & feedback email form * use name not orgName * import cleanup * Downgrading plan form uses sendToPlain * Get the userId from requireUser only * Added whitespace-pre-wrap to the message property on the run page * use requireUserId * Removed old Plain submit code
1 parent b6d1e0d commit 6dc556b

File tree

10 files changed

+188
-181
lines changed

10 files changed

+188
-181
lines changed

apps/webapp/app/components/Feedback.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { conform, useForm } from "@conform-to/react";
22
import { parse } from "@conform-to/zod";
3-
import { EnvelopeIcon, LightBulbIcon } from "@heroicons/react/24/solid";
3+
import { InformationCircleIcon } from "@heroicons/react/20/solid";
4+
import { EnvelopeIcon } from "@heroicons/react/24/solid";
45
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
5-
import { type ReactNode, useState, useEffect } from "react";
6+
import { type ReactNode, useEffect, useState } from "react";
67
import { type FeedbackType, feedbackTypeLabel, schema } from "~/routes/resources.feedback";
78
import { Button } from "./primitives/Buttons";
89
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "./primitives/Dialog";
@@ -16,7 +17,6 @@ import { Label } from "./primitives/Label";
1617
import { Paragraph } from "./primitives/Paragraph";
1718
import { Select, SelectItem } from "./primitives/Select";
1819
import { TextArea } from "./primitives/TextArea";
19-
import { InformationCircleIcon } from "@heroicons/react/20/solid";
2020
import { TextLink } from "./primitives/TextLink";
2121

2222
type FeedbackProps = {

apps/webapp/app/components/primitives/TextArea.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export function TextArea({ className, rows, ...props }: TextAreaProps) {
88
{...props}
99
rows={rows ?? 6}
1010
className={cn(
11-
"placeholder:text-muted-foreground w-full rounded-md border border-tertiary bg-tertiary px-3 text-sm text-text-bright transition focus-custom file:border-0 file:bg-transparent file:text-base file:font-medium hover:border-charcoal-600 focus:border-transparent focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50",
11+
"placeholder:text-muted-foreground w-full rounded border border-charcoal-800 bg-charcoal-750 px-3 text-sm text-text-bright transition focus-custom focus-custom file:border-0 file:bg-transparent file:text-base file:font-medium hover:border-charcoal-600 hover:bg-charcoal-650 disabled:cursor-not-allowed disabled:opacity-50",
1212
className
1313
)}
1414
/>

apps/webapp/app/routes/_app.orgs.new/route.tsx

Lines changed: 82 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { RadioGroup } from "@radix-ui/react-radio-group";
44
import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node";
55
import { json, redirect } from "@remix-run/node";
66
import { Form, useActionData, useNavigation } from "@remix-run/react";
7+
import { uiComponent } from "@team-plain/typescript-sdk";
78
import { typedjson, useTypedLoaderData } from "remix-typedjson";
89
import { z } from "zod";
910
import { MainCenteredContainer } from "~/components/layout/AppLayout";
@@ -17,30 +18,33 @@ import { Input } from "~/components/primitives/Input";
1718
import { InputGroup } from "~/components/primitives/InputGroup";
1819
import { Label } from "~/components/primitives/Label";
1920
import { RadioGroupItem } from "~/components/primitives/RadioButton";
21+
import { TextArea } from "~/components/primitives/TextArea";
2022
import { useFeatures } from "~/hooks/useFeatures";
2123
import { createOrganization } from "~/models/organization.server";
2224
import { NewOrganizationPresenter } from "~/presenters/NewOrganizationPresenter.server";
23-
import { requireUserId } from "~/services/session.server";
25+
import { logger } from "~/services/logger.server";
26+
import { requireUser, requireUserId } from "~/services/session.server";
2427
import { organizationPath, rootPath } from "~/utils/pathBuilder";
28+
import { sendToPlain } from "~/utils/plain.server";
2529

2630
const schema = z.object({
2731
orgName: z.string().min(3).max(50),
2832
companySize: z.string().optional(),
33+
whyUseUs: z.string().optional(),
2934
});
3035

3136
export const loader = async ({ request }: LoaderFunctionArgs) => {
3237
const userId = await requireUserId(request);
3338
const presenter = new NewOrganizationPresenter();
34-
const { hasOrganizations } = await presenter.call({ userId });
39+
const { hasOrganizations } = await presenter.call({ userId: userId });
3540

3641
return typedjson({
3742
hasOrganizations,
3843
});
3944
};
4045

4146
export const action: ActionFunction = async ({ request }) => {
42-
const userId = await requireUserId(request);
43-
47+
const user = await requireUser(request);
4448
const formData = await request.formData();
4549
const submission = parse(formData, { schema });
4650

@@ -51,10 +55,41 @@ export const action: ActionFunction = async ({ request }) => {
5155
try {
5256
const organization = await createOrganization({
5357
title: submission.value.orgName,
54-
userId,
58+
userId: user.id,
5559
companySize: submission.value.companySize ?? null,
5660
});
5761

62+
const whyUseUs = formData.get("whyUseUs");
63+
64+
if (whyUseUs) {
65+
try {
66+
await sendToPlain({
67+
userId: user.id,
68+
email: user.email,
69+
name: user.name ?? user.displayName ?? user.email,
70+
title: "New org feedback",
71+
components: [
72+
uiComponent.text({
73+
text: `${submission.value.orgName} just created a new organization.`,
74+
}),
75+
uiComponent.divider({ spacingSize: "M" }),
76+
uiComponent.text({
77+
size: "L",
78+
color: "NORMAL",
79+
text: "What problem are you trying to solve?",
80+
}),
81+
uiComponent.text({
82+
size: "L",
83+
color: "NORMAL",
84+
text: whyUseUs.toString(),
85+
}),
86+
],
87+
});
88+
} catch (error) {
89+
logger.error("Error sending data to Plain when creating an org:", { error });
90+
}
91+
}
92+
5893
return redirect(organizationPath(organization));
5994
} catch (error: any) {
6095
return json({ errors: { body: error.message } }, { status: 400 });
@@ -97,39 +132,48 @@ export default function NewOrganizationPage() {
97132
<FormError id={orgName.errorId}>{orgName.error}</FormError>
98133
</InputGroup>
99134
{isManagedCloud && (
100-
<InputGroup>
101-
<Label htmlFor={"companySize"}>Number of employees</Label>
102-
<RadioGroup name="companySize" className="flex items-center justify-between gap-2">
103-
<RadioGroupItem
104-
id="employees-1-5"
105-
label="1-5"
106-
value={"1-5"}
107-
variant="button/small"
108-
className="grow"
109-
/>
110-
<RadioGroupItem
111-
id="employees-6-49"
112-
label="6-49"
113-
value={"6-49"}
114-
variant="button/small"
115-
className="grow"
116-
/>
117-
<RadioGroupItem
118-
id="employees-50-99"
119-
label="50-99"
120-
value={"50-99"}
121-
variant="button/small"
122-
className="grow"
123-
/>
124-
<RadioGroupItem
125-
id="employees-100+"
126-
label="100+"
127-
value={"100+"}
128-
variant="button/small"
129-
className="grow"
130-
/>
131-
</RadioGroup>
132-
</InputGroup>
135+
<>
136+
<InputGroup>
137+
<Label htmlFor={"companySize"}>Number of employees</Label>
138+
<RadioGroup name="companySize" className="flex items-center justify-between gap-2">
139+
<RadioGroupItem
140+
id="employees-1-5"
141+
label="1-5"
142+
value={"1-5"}
143+
variant="button/small"
144+
className="grow"
145+
/>
146+
<RadioGroupItem
147+
id="employees-6-49"
148+
label="6-49"
149+
value={"6-49"}
150+
variant="button/small"
151+
className="grow"
152+
/>
153+
<RadioGroupItem
154+
id="employees-50-99"
155+
label="50-99"
156+
value={"50-99"}
157+
variant="button/small"
158+
className="grow"
159+
/>
160+
<RadioGroupItem
161+
id="employees-100+"
162+
label="100+"
163+
value={"100+"}
164+
variant="button/small"
165+
className="grow"
166+
/>
167+
</RadioGroup>
168+
</InputGroup>
169+
<InputGroup>
170+
<Label htmlFor={"whyUseUs"}>What problem are you trying to solve?</Label>
171+
<TextArea name="whyUseUs" rows={4} spellCheck={false} />
172+
<Hint>
173+
Your answer will help us understand your use case and provide better support.
174+
</Hint>
175+
</InputGroup>
176+
</>
133177
)}
134178

135179
<FormButtons

apps/webapp/app/routes/confirm-basic-details.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ export default function Page() {
222222
<Label htmlFor={confirmEmail.id}>How did you hear about us?</Label>
223223
<Input
224224
{...conform.input(referralSource, { type: "text" })}
225-
placeholder="Google, Twitter…?"
225+
placeholder="Google, X (Twitter)…?"
226226
icon="heart"
227227
spellCheck={false}
228228
/>

apps/webapp/app/routes/resources.feedback.ts

Lines changed: 9 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { z } from "zod";
66
import { env } from "~/env.server";
77
import { redirectWithSuccessMessage } from "~/models/message.server";
88
import { requireUser } from "~/services/session.server";
9+
import { sendToPlain } from "~/utils/plain.server";
910

1011
let client: PlainClient | undefined;
1112

@@ -32,7 +33,7 @@ const feedbackType = z.union(
3233
export const schema = z.object({
3334
path: z.string(),
3435
feedbackType,
35-
message: z.string().min(1, "Must be at least 1 character"),
36+
message: z.string().min(10, "Must be at least 10 characters"),
3637
});
3738

3839
export async function action({ request }: ActionFunctionArgs) {
@@ -45,60 +46,12 @@ export async function action({ request }: ActionFunctionArgs) {
4546
return json(submission);
4647
}
4748

49+
const title = feedbackTypeLabel[submission.value.feedbackType as FeedbackType];
4850
try {
49-
if (!env.PLAIN_API_KEY) {
50-
console.error("PLAIN_API_KEY is not set");
51-
submission.error.message = "PLAIN_API_KEY is not set";
52-
return json(submission);
53-
}
54-
55-
client = new PlainClient({
56-
apiKey: env.PLAIN_API_KEY,
57-
});
58-
59-
const upsertCustomerRes = await client.upsertCustomer({
60-
identifier: {
61-
emailAddress: user.email,
62-
},
63-
onCreate: {
64-
externalId: user.id,
65-
fullName: user.name ?? "",
66-
// TODO - Optional: set 'first name' on user
67-
// shortName: ''
68-
email: {
69-
email: user.email,
70-
isVerified: true,
71-
},
72-
},
73-
onUpdate: {
74-
externalId: { value: user.id },
75-
fullName: { value: user.name ?? "" },
76-
// TODO - see above
77-
// shortName: { value: "" },
78-
email: {
79-
email: user.email,
80-
isVerified: true,
81-
},
82-
},
83-
});
84-
85-
if (upsertCustomerRes.error) {
86-
console.error(
87-
inspect(upsertCustomerRes.error, {
88-
showHidden: false,
89-
depth: null,
90-
colors: true,
91-
})
92-
);
93-
submission.error.message = upsertCustomerRes.error.message;
94-
return json(submission);
95-
}
96-
97-
const title = feedbackTypeLabel[submission.value.feedbackType as FeedbackType];
98-
const createThreadRes = await client.createThread({
99-
customerIdentifier: {
100-
customerId: upsertCustomerRes.data.customer.id,
101-
},
51+
await sendToPlain({
52+
userId: user.id,
53+
email: user.email,
54+
name: user.name ?? user.displayName ?? user.email,
10255
title,
10356
components: [
10457
uiComponent.text({
@@ -123,31 +76,15 @@ export async function action({ request }: ActionFunctionArgs) {
12376
text: submission.value.message,
12477
}),
12578
],
126-
// TODO: Optional: set labels on threads here on creation
127-
// labelTypeIds: [],
128-
129-
// TODO: Optional: set the priority (0 is urgent, 3 is low)
130-
// priority: 0,
13179
});
13280

133-
if (createThreadRes.error) {
134-
console.error(
135-
inspect(createThreadRes.error, {
136-
showHidden: false,
137-
depth: null,
138-
colors: true,
139-
})
140-
);
141-
submission.error.message = createThreadRes.error.message;
142-
return json(submission);
143-
}
144-
14581
return redirectWithSuccessMessage(
14682
submission.value.path,
14783
request,
14884
"Thanks for your feedback! We'll get back to you soon."
14985
);
15086
} catch (e) {
151-
return json(e, { status: 400 });
87+
submission.error.message = e instanceof Error ? e.message : "Unknown error";
88+
return json(submission);
15289
}
15390
}

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ function SpanBody({
314314
<Property.Table>
315315
<Property.Item>
316316
<Property.Label>Message</Property.Label>
317-
<Property.Value>{span.message}</Property.Value>
317+
<Property.Value className="whitespace-pre-wrap">{span.message}</Property.Value>
318318
</Property.Item>
319319
{span.triggeredRuns.length > 0 && (
320320
<Property.Item>

0 commit comments

Comments
 (0)