Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow users to change the default new submission email recipient #90

Merged
merged 2 commits into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 105 additions & 3 deletions apps/web/src/app/(main)/form/[id]/form-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { toast } from 'sonner';
import { z } from 'zod';

import { type RouterOutputs } from '@formbase/api';
import { type UserInstance } from '@formbase/auth';
import { Button } from '@formbase/ui/primitives/button';
import {
Form,
Expand Down Expand Up @@ -47,17 +48,26 @@ const enableNotificationsSchema = z.object({
enableNotifications: z.boolean().default(true).optional(),
});

const defaultSubmissionEmailSchema = z.object({
defaultSubmissionEmail: z.string().email().optional(),
});

type FormNameSchema = z.infer<typeof formNameSchema>;
type EnableFormSubmissionsSchema = z.infer<typeof enableFormSubmissionsSchema>;
type _EnableSubmissionsRetentionSchema = z.infer<typeof enableRetentionSchema>;
type EnableFormNotificationsSchema = z.infer<typeof enableNotificationsSchema>;
type FormReturnUrlSchema = z.infer<typeof formReturnUrlSchema>;

type DefaultSubmissionEmailSchema = z.infer<
typeof defaultSubmissionEmailSchema
>;

type FormSettingsProps = {
form: RouterOutputs['form']['get'];
user: UserInstance | null;
};

export function FormSettings({ form }: FormSettingsProps) {
export function FormSettings({ form, user }: FormSettingsProps) {
const router = useRouter();

if (!form) {
Expand Down Expand Up @@ -85,6 +95,14 @@ export function FormSettings({ form }: FormSettingsProps) {
enableNotifications={form.enableEmailNotifications}
/>

{/* only show the if the user has enabled email notifications */}
{form.enableEmailNotifications && (
<FormDefaultSubmissionEmailRecipient
formId={form.id}
email={form.defaultSubmissionEmail ?? user?.email ?? ''}
/>
)}

<div className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<Label className="text-base">Delete Form</Label>
Expand Down Expand Up @@ -247,6 +265,90 @@ const ReturnUrlForm = ({
);
};

const FormDefaultSubmissionEmailRecipient = ({
formId,
email,
}: {
formId: string;
email: string;
}) => {
const router = useRouter();

const formDefaultSubmissionEmail = useForm<DefaultSubmissionEmailSchema>({
resolver: zodResolver(defaultSubmissionEmailSchema),
defaultValues: {
defaultSubmissionEmail: email,
},
});

const {
mutateAsync: updateFormDefaultSubmissionEmail,
isPending: isUpdatingFormDefaultSubmissionEmail,
} = api.form.update.useMutation();

async function handleFormDefaultSubmissionEmailSubmit(
data: DefaultSubmissionEmailSchema,
) {
try {
await updateFormDefaultSubmissionEmail({
id: formId,
defaultSubmissionEmail: data.defaultSubmissionEmail,
});

toast('Your form default submission email has been updated', {
icon: <FolderPen className="h-4 w-4" />,
});

router.refresh();
} catch {
toast('Failed to update form', {
description: 'Please try again later',
icon: <FolderX className="h-4 w-4" />,
});
}
}

return (
<Form {...formDefaultSubmissionEmail}>
<form
onSubmit={formDefaultSubmissionEmail.handleSubmit(
handleFormDefaultSubmissionEmailSubmit,
)}
>
<FormField
control={formDefaultSubmissionEmail.control}
name="defaultSubmissionEmail"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">
Default Submission Email
</FormLabel>
<FormDescription>
This is the email address that submission emails will be sent
to
</FormDescription>
</div>
<FormControl>
<div className="flex gap-2">
<Input className="w-[250px]" {...field} min={1} />
<Button
loading={isUpdatingFormDefaultSubmissionEmail}
type="submit"
variant="default"
>
Save
</Button>
</div>
</FormControl>
</FormItem>
)}
/>
</form>
</Form>
);
};

const EnableFormSubmissions = ({
formId,
enableSubmissions,
Expand Down Expand Up @@ -311,7 +413,7 @@ const EnableFormSubmissions = ({
</div>
<FormControl>
<Switch
checked={field.value}
checked={field.value ?? false}
onCheckedChange={async (isChecked) => {
field.onChange(isChecked);
await handleEnableSubmissionsRetentionSubmit({
Expand Down Expand Up @@ -391,7 +493,7 @@ const EnableFormNotifications = ({
</div>
<FormControl>
<Switch
checked={field.value}
checked={field.value ?? false}
onCheckedChange={async (isChecked) => {
field.onChange(isChecked);
await handleEnableSubmissionsNotifications({
Expand Down
26 changes: 12 additions & 14 deletions apps/web/src/app/(main)/form/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { type FormData } from '@formbase/db/schema';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@formbase/ui/primitives/tabs';
import { validateRequest } from "@formbase/auth";
import { FormData } from "@formbase/db/schema";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@formbase/ui/primitives/tabs";

import { CopyButton } from '~/components/copy-button';
import { api } from '~/lib/trpc/server';
import { CopyButton } from "~/components/copy-button";
import { api } from "~/lib/trpc/server";

import { EmptyFormState } from '../../dashboard/_components/empty-state';
import { ExportSubmissionsDropDownButton } from './export-submissions-button';
import { FormSettings } from './form-settings';
import { SubmissionsTable } from './submissions-table';
import { EmptyFormState } from "../../dashboard/_components/empty-state";
import { ExportSubmissionsDropDownButton } from "./export-submissions-button";
import { FormSettings } from "./form-settings";
import { SubmissionsTable } from "./submissions-table";

export default async function FormPage({ params }: { params: { id: string } }) {
const formId = params.id;
Expand All @@ -21,6 +17,8 @@ export default async function FormPage({ params }: { params: { id: string } }) {
api.formData.all({ formId }),
]);

const { user } = await validateRequest();

return (
<div className="space-y-6">
<div>
Expand Down Expand Up @@ -66,7 +64,7 @@ export default async function FormPage({ params }: { params: { id: string } }) {
{/* <TabsContent value="setup">Change your password here.</TabsContent> */}
{/* <TabsContent value="analytics">Look at your analytics here</TabsContent> */}
<TabsContent value="settings" className="my-6">
<FormSettings form={form} />
<FormSettings form={form} user={user} />
</TabsContent>
</Tabs>
</div>
Expand Down
17 changes: 8 additions & 9 deletions apps/web/src/app/api/s/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { userAgent } from 'next/server';
import { Form } from "@formbase/db/schema";
import { env } from "@formbase/env";
import { userAgent } from "next/server";

import { type Form } from '@formbase/db/schema';
import { env } from '@formbase/env';

import { sendMail } from '~/lib/email/mailer';
import { renderNewSubmissionEmail } from '~/lib/email/templates/new-submission';
import { api } from '~/lib/trpc/server';
import { assignFileOrImage, uploadFileFromBlob } from '~/lib/upload-file';
import { sendMail } from "~/lib/email/mailer";
import { renderNewSubmissionEmail } from "~/lib/email/templates/new-submission";
import { api } from "~/lib/trpc/server";
import { assignFileOrImage, uploadFileFromBlob } from "~/lib/upload-file";

type Json = string | number | boolean | null | { [key: string]: Json } | Json[];

Expand Down Expand Up @@ -68,7 +67,7 @@ async function handleEmailNotifications(
if (!user) throw new Error('User not found');

await sendMail({
to: user.email,
to: form.defaultSubmissionEmail ?? user.email,
subject: `New Submission for "${form.title}"`,
body: renderNewSubmissionEmail({
formTitle: form.title,
Expand Down
16 changes: 10 additions & 6 deletions packages/api/routers/form.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { z } from 'zod';
import { drizzlePrimitives } from "@formbase/db";
import { formDatas, forms, onboardingForms } from "@formbase/db/schema";
import { generateId } from "@formbase/utils/generate-id";
import { z } from "zod";

import { drizzlePrimitives } from '@formbase/db';
import { formDatas, forms, onboardingForms } from '@formbase/db/schema';
import { generateId } from '@formbase/utils/generate-id';

import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";

const { and, count, eq } = drizzlePrimitives;

Expand Down Expand Up @@ -57,6 +56,7 @@ export const formRouter = createTRPCRouter({
)
.mutation(async ({ ctx, input }) => {
const id = generateId(15);
const userEmail = ctx.user.email;

await ctx.db.insert(forms).values({
id,
Expand All @@ -68,6 +68,7 @@ export const formRouter = createTRPCRouter({
keys: [''],
enableEmailNotifications: true,
enableSubmissions: true,
defaultSubmissionEmail: userEmail,
});

return { id };
Expand Down Expand Up @@ -114,6 +115,7 @@ export const formRouter = createTRPCRouter({
enableSubmissions: z.boolean().optional(),
enableEmailNotifications: z.boolean().optional(),
returnUrl: z.string().optional(),
defaultSubmissionEmail: z.string().optional(),
}),
)
.mutation(async ({ ctx, input }) => {
Expand All @@ -135,6 +137,8 @@ export const formRouter = createTRPCRouter({
enableEmailNotifications:
input.enableEmailNotifications ?? form.enableEmailNotifications,
returnUrl: input.returnUrl ?? form.returnUrl,
defaultSubmissionEmail:
input.defaultSubmissionEmail ?? form.defaultSubmissionEmail,
})
.where(eq(forms.id, input.id));
}),
Expand Down
15 changes: 10 additions & 5 deletions packages/auth/lucia.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { User } from '@formbase/db/schema';

import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle';
import { Lucia } from 'lucia';
import { db } from "@formbase/db";
import { sessions, users } from "@formbase/db/schema";
import { env } from "@formbase/env";
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import { Lucia } from "lucia";

import { db } from '@formbase/db';
import { sessions, users } from '@formbase/db/schema';
import { env } from '@formbase/env';
import type { User as LuciaUser } from 'lucia';

const adapter = new DrizzlePostgreSQLAdapter(db, sessions, users);

Expand Down Expand Up @@ -36,3 +37,7 @@ declare module 'lucia' {
DatabaseUserAttributes: DatabaseUserAttributes;
}
}


// export lucia user type
export type UserInstance = LuciaUser;
1 change: 1 addition & 0 deletions packages/db/drizzle/0001_military_rictor.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "forms" ADD COLUMN "default_submission_email" varchar(255);
Loading
Loading