diff --git a/apps/webapp/app/components/Feedback.tsx b/apps/webapp/app/components/Feedback.tsx
index 3c9b389750..5a5472a976 100644
--- a/apps/webapp/app/components/Feedback.tsx
+++ b/apps/webapp/app/components/Feedback.tsx
@@ -1,8 +1,9 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
-import { EnvelopeIcon, LightBulbIcon } from "@heroicons/react/24/solid";
+import { InformationCircleIcon } from "@heroicons/react/20/solid";
+import { EnvelopeIcon } from "@heroicons/react/24/solid";
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
-import { type ReactNode, useState, useEffect } from "react";
+import { type ReactNode, useEffect, useState } from "react";
import { type FeedbackType, feedbackTypeLabel, schema } from "~/routes/resources.feedback";
import { Button } from "./primitives/Buttons";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "./primitives/Dialog";
@@ -16,7 +17,6 @@ import { Label } from "./primitives/Label";
import { Paragraph } from "./primitives/Paragraph";
import { Select, SelectItem } from "./primitives/Select";
import { TextArea } from "./primitives/TextArea";
-import { InformationCircleIcon } from "@heroicons/react/20/solid";
import { TextLink } from "./primitives/TextLink";
type FeedbackProps = {
diff --git a/apps/webapp/app/components/primitives/TextArea.tsx b/apps/webapp/app/components/primitives/TextArea.tsx
index 7d543e2ec5..f5350a510b 100644
--- a/apps/webapp/app/components/primitives/TextArea.tsx
+++ b/apps/webapp/app/components/primitives/TextArea.tsx
@@ -8,7 +8,7 @@ export function TextArea({ className, rows, ...props }: TextAreaProps) {
{...props}
rows={rows ?? 6}
className={cn(
- "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",
+ "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",
className
)}
/>
diff --git a/apps/webapp/app/routes/_app.orgs.new/route.tsx b/apps/webapp/app/routes/_app.orgs.new/route.tsx
index d99286dd05..571c6163be 100644
--- a/apps/webapp/app/routes/_app.orgs.new/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.new/route.tsx
@@ -4,6 +4,7 @@ import { RadioGroup } from "@radix-ui/react-radio-group";
import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
+import { uiComponent } from "@team-plain/typescript-sdk";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { MainCenteredContainer } from "~/components/layout/AppLayout";
@@ -17,21 +18,25 @@ import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import { RadioGroupItem } from "~/components/primitives/RadioButton";
+import { TextArea } from "~/components/primitives/TextArea";
import { useFeatures } from "~/hooks/useFeatures";
import { createOrganization } from "~/models/organization.server";
import { NewOrganizationPresenter } from "~/presenters/NewOrganizationPresenter.server";
-import { requireUserId } from "~/services/session.server";
+import { logger } from "~/services/logger.server";
+import { requireUser, requireUserId } from "~/services/session.server";
import { organizationPath, rootPath } from "~/utils/pathBuilder";
+import { sendToPlain } from "~/utils/plain.server";
const schema = z.object({
orgName: z.string().min(3).max(50),
companySize: z.string().optional(),
+ whyUseUs: z.string().optional(),
});
export const loader = async ({ request }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
const presenter = new NewOrganizationPresenter();
- const { hasOrganizations } = await presenter.call({ userId });
+ const { hasOrganizations } = await presenter.call({ userId: userId });
return typedjson({
hasOrganizations,
@@ -39,8 +44,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
};
export const action: ActionFunction = async ({ request }) => {
- const userId = await requireUserId(request);
-
+ const user = await requireUser(request);
const formData = await request.formData();
const submission = parse(formData, { schema });
@@ -51,10 +55,41 @@ export const action: ActionFunction = async ({ request }) => {
try {
const organization = await createOrganization({
title: submission.value.orgName,
- userId,
+ userId: user.id,
companySize: submission.value.companySize ?? null,
});
+ const whyUseUs = formData.get("whyUseUs");
+
+ if (whyUseUs) {
+ try {
+ await sendToPlain({
+ userId: user.id,
+ email: user.email,
+ name: user.name ?? user.displayName ?? user.email,
+ title: "New org feedback",
+ components: [
+ uiComponent.text({
+ text: `${submission.value.orgName} just created a new organization.`,
+ }),
+ uiComponent.divider({ spacingSize: "M" }),
+ uiComponent.text({
+ size: "L",
+ color: "NORMAL",
+ text: "What problem are you trying to solve?",
+ }),
+ uiComponent.text({
+ size: "L",
+ color: "NORMAL",
+ text: whyUseUs.toString(),
+ }),
+ ],
+ });
+ } catch (error) {
+ logger.error("Error sending data to Plain when creating an org:", { error });
+ }
+ }
+
return redirect(organizationPath(organization));
} catch (error: any) {
return json({ errors: { body: error.message } }, { status: 400 });
@@ -97,39 +132,48 @@ export default function NewOrganizationPage() {
{orgName.error}
{isManagedCloud && (
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Your answer will help us understand your use case and provide better support.
+
+
+ >
)}
How did you hear about us?
diff --git a/apps/webapp/app/routes/resources.feedback.ts b/apps/webapp/app/routes/resources.feedback.ts
index 161eed564b..6bf8e012fd 100644
--- a/apps/webapp/app/routes/resources.feedback.ts
+++ b/apps/webapp/app/routes/resources.feedback.ts
@@ -6,6 +6,7 @@ import { z } from "zod";
import { env } from "~/env.server";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { requireUser } from "~/services/session.server";
+import { sendToPlain } from "~/utils/plain.server";
let client: PlainClient | undefined;
@@ -32,7 +33,7 @@ const feedbackType = z.union(
export const schema = z.object({
path: z.string(),
feedbackType,
- message: z.string().min(1, "Must be at least 1 character"),
+ message: z.string().min(10, "Must be at least 10 characters"),
});
export async function action({ request }: ActionFunctionArgs) {
@@ -45,60 +46,12 @@ export async function action({ request }: ActionFunctionArgs) {
return json(submission);
}
+ const title = feedbackTypeLabel[submission.value.feedbackType as FeedbackType];
try {
- if (!env.PLAIN_API_KEY) {
- console.error("PLAIN_API_KEY is not set");
- submission.error.message = "PLAIN_API_KEY is not set";
- return json(submission);
- }
-
- client = new PlainClient({
- apiKey: env.PLAIN_API_KEY,
- });
-
- const upsertCustomerRes = await client.upsertCustomer({
- identifier: {
- emailAddress: user.email,
- },
- onCreate: {
- externalId: user.id,
- fullName: user.name ?? "",
- // TODO - Optional: set 'first name' on user
- // shortName: ''
- email: {
- email: user.email,
- isVerified: true,
- },
- },
- onUpdate: {
- externalId: { value: user.id },
- fullName: { value: user.name ?? "" },
- // TODO - see above
- // shortName: { value: "" },
- email: {
- email: user.email,
- isVerified: true,
- },
- },
- });
-
- if (upsertCustomerRes.error) {
- console.error(
- inspect(upsertCustomerRes.error, {
- showHidden: false,
- depth: null,
- colors: true,
- })
- );
- submission.error.message = upsertCustomerRes.error.message;
- return json(submission);
- }
-
- const title = feedbackTypeLabel[submission.value.feedbackType as FeedbackType];
- const createThreadRes = await client.createThread({
- customerIdentifier: {
- customerId: upsertCustomerRes.data.customer.id,
- },
+ await sendToPlain({
+ userId: user.id,
+ email: user.email,
+ name: user.name ?? user.displayName ?? user.email,
title,
components: [
uiComponent.text({
@@ -123,31 +76,15 @@ export async function action({ request }: ActionFunctionArgs) {
text: submission.value.message,
}),
],
- // TODO: Optional: set labels on threads here on creation
- // labelTypeIds: [],
-
- // TODO: Optional: set the priority (0 is urgent, 3 is low)
- // priority: 0,
});
- if (createThreadRes.error) {
- console.error(
- inspect(createThreadRes.error, {
- showHidden: false,
- depth: null,
- colors: true,
- })
- );
- submission.error.message = createThreadRes.error.message;
- return json(submission);
- }
-
return redirectWithSuccessMessage(
submission.value.path,
request,
"Thanks for your feedback! We'll get back to you soon."
);
} catch (e) {
- return json(e, { status: 400 });
+ submission.error.message = e instanceof Error ? e.message : "Unknown error";
+ return json(submission);
}
}
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx
index 299a5eafec..9db878ea6c 100644
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx
@@ -314,7 +314,7 @@ function SpanBody({
Message
- {span.message}
+ {span.message}
{span.triggeredRuns.length > 0 && (
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx
index b4411761b3..ceb499d331 100644
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx
@@ -8,7 +8,7 @@ import {
import { ArrowDownCircleIcon } from "@heroicons/react/24/outline";
import { Form, useLocation, useNavigation } from "@remix-run/react";
import { ActionFunctionArgs } from "@remix-run/server-runtime";
-import { PlainClient, uiComponent } from "@team-plain/typescript-sdk";
+import { uiComponent } from "@team-plain/typescript-sdk";
import { GitHubLightIcon } from "@trigger.dev/companyicons";
import {
FreePlanDefinition,
@@ -19,7 +19,6 @@ import {
SubscriptionResult,
} from "@trigger.dev/platform/v3";
import React, { useEffect, useState } from "react";
-import { inspect } from "util";
import { z } from "zod";
import { DefinitionTip } from "~/components/DefinitionTooltip";
import { Feedback } from "~/components/Feedback";
@@ -39,12 +38,12 @@ import { Spinner } from "~/components/primitives/Spinner";
import { TextArea } from "~/components/primitives/TextArea";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { prisma } from "~/db.server";
-import { env } from "~/env.server";
import { redirectWithErrorMessage } from "~/models/message.server";
import { logger } from "~/services/logger.server";
import { setPlan } from "~/services/platform.v3.server";
import { requireUser } from "~/services/session.server";
import { cn } from "~/utils/cn";
+import { sendToPlain } from "~/utils/plain.server";
const Params = z.object({
organizationSlug: z.string(),
@@ -88,53 +87,11 @@ export async function action({ request, params }: ActionFunctionArgs) {
switch (form.type) {
case "free": {
try {
- if (!env.PLAIN_API_KEY) {
- throw new Error("PLAIN_API_KEY is not set");
- }
-
- const client = new PlainClient({
- apiKey: env.PLAIN_API_KEY,
- });
-
- const upsertCustomerRes = await client.upsertCustomer({
- identifier: {
- emailAddress: user.email,
- },
- onCreate: {
- externalId: user.id,
- fullName: user.name ?? "",
- email: {
- email: user.email,
- isVerified: true,
- },
- },
- onUpdate: {
- externalId: { value: user.id },
- fullName: { value: user.name ?? "" },
- email: {
- email: user.email,
- isVerified: true,
- },
- },
- });
-
- if (upsertCustomerRes.error) {
- console.error(
- inspect(upsertCustomerRes.error, {
- showHidden: false,
- depth: null,
- colors: true,
- })
- );
- throw redirectWithErrorMessage(form.callerPath, request, upsertCustomerRes.error.message);
- }
-
- // Only create a thread if there are reasons or a message
if (reasons.length > 0 || (message && message.toString().trim() !== "")) {
- const createThreadRes = await client.createThread({
- customerIdentifier: {
- customerId: upsertCustomerRes.data.customer.id,
- },
+ await sendToPlain({
+ userId: user.id,
+ email: user.email,
+ name: user.name ?? "",
title: "Plan cancelation feedback",
components: [
uiComponent.text({
@@ -169,17 +126,6 @@ export async function action({ request, params }: ActionFunctionArgs) {
: []),
],
});
-
- if (createThreadRes.error) {
- console.error(
- inspect(createThreadRes.error, {
- showHidden: false,
- depth: null,
- colors: true,
- })
- );
- throw redirectWithErrorMessage(form.callerPath, request, createThreadRes.error.message);
- }
}
} catch (e) {
logger.error("Failed to submit to Plain the unsubscribe reason", { error: e });
@@ -867,7 +813,7 @@ function TierLimit({ children, href }: { children: React.ReactNode; href?: strin
}
content={
-
View detailed compute pricing information
+
View compute pricing information
}
diff --git a/apps/webapp/app/routes/storybook.textarea/route.tsx b/apps/webapp/app/routes/storybook.textarea/route.tsx
new file mode 100644
index 0000000000..efdcf577d3
--- /dev/null
+++ b/apps/webapp/app/routes/storybook.textarea/route.tsx
@@ -0,0 +1,17 @@
+import { Input } from "~/components/primitives/Input";
+import { TextArea } from "~/components/primitives/TextArea";
+
+export default function Story() {
+ return (
+
+ );
+}
diff --git a/apps/webapp/app/routes/storybook/route.tsx b/apps/webapp/app/routes/storybook/route.tsx
index 7449597481..39cdb19d75 100644
--- a/apps/webapp/app/routes/storybook/route.tsx
+++ b/apps/webapp/app/routes/storybook/route.tsx
@@ -126,16 +126,20 @@ const stories: Story[] = [
slug: "date-fields",
},
{
- name: "Simple form",
- slug: "simple-form",
+ name: "Input fields",
+ slug: "input-fields",
},
{
name: "Search fields",
slug: "search-fields",
},
{
- name: "Input fields",
- slug: "input-fields",
+ name: "Simple form",
+ slug: "simple-form",
+ },
+ {
+ name: "Textarea",
+ slug: "textarea",
},
{
sectionTitle: "Menus",
diff --git a/apps/webapp/app/utils/plain.server.ts b/apps/webapp/app/utils/plain.server.ts
new file mode 100644
index 0000000000..4e8ce63073
--- /dev/null
+++ b/apps/webapp/app/utils/plain.server.ts
@@ -0,0 +1,59 @@
+import { PlainClient, uiComponent } from "@team-plain/typescript-sdk";
+import { env } from "~/env.server";
+
+type Input = {
+ userId: string;
+ email: string;
+ name: string;
+ title: string;
+ components: ReturnType[];
+};
+
+export async function sendToPlain({ userId, email, name, title, components }: Input) {
+ if (!env.PLAIN_API_KEY) {
+ return;
+ }
+
+ const client = new PlainClient({
+ apiKey: env.PLAIN_API_KEY,
+ });
+
+ const upsertCustomerRes = await client.upsertCustomer({
+ identifier: {
+ emailAddress: email,
+ },
+ onCreate: {
+ externalId: userId,
+ fullName: name,
+ email: {
+ email: email,
+ isVerified: true,
+ },
+ },
+ onUpdate: {
+ externalId: { value: userId },
+ fullName: { value: name },
+ email: {
+ email: email,
+ isVerified: true,
+ },
+ },
+ });
+
+ if (upsertCustomerRes.error) {
+ console.error("Failed to upsert customer in Plain", upsertCustomerRes.error);
+ return;
+ }
+
+ const createThreadRes = await client.createThread({
+ customerIdentifier: {
+ customerId: upsertCustomerRes.data.customer.id,
+ },
+ title: title,
+ components: components,
+ });
+
+ if (createThreadRes.error) {
+ console.error("Failed to create thread in Plain", createThreadRes.error);
+ }
+}