Skip to content

Commit c276706

Browse files
committed
feat: user settings form
1 parent c1fbf50 commit c276706

File tree

8 files changed

+159
-20
lines changed

8 files changed

+159
-20
lines changed

studio/src/components/onboarding/onboarding-navigation.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const OnboardingNavigation = ({
1010
onSkip,
1111
}: {
1212
backHref?: string;
13-
forward: { href: string } | { onClick: () => void; isLoading?: boolean };
13+
forward: { href: string } | { onClick: () => void; isLoading?: boolean; disabled?: boolean };
1414
forwardLabel?: string;
1515
onSkip: () => void;
1616
}) => {
@@ -53,7 +53,7 @@ export const OnboardingNavigation = ({
5353
className="group"
5454
onClick={forward.onClick}
5555
isLoading={forward.isLoading}
56-
disabled={forward.isLoading}
56+
disabled={forward.isLoading || forward.disabled}
5757
>
5858
{forwardLabel}
5959
<ArrowRightIcon className="ml-2 transition-transform group-hover:translate-x-1" />

studio/src/components/onboarding/onboarding-provider.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { useSessionStorage } from '@/hooks/use-session-storage';
1414
type Onboarding = {
1515
finishedAt?: Date;
1616
federatedGraphsCount: number;
17+
slack: boolean;
18+
email: boolean;
1719
};
1820

1921
export interface OnboardingState {

studio/src/components/onboarding/step-1.tsx

Lines changed: 138 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,51 @@ import { useRouter } from 'next/router';
88
import { useCurrentOrganization } from '@/hooks/use-current-organization';
99
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
1010
import { useToast } from '../ui/use-toast';
11+
import { SubmitHandler, useZodForm } from '@/hooks/use-form';
12+
import { Controller, useFieldArray } from 'react-hook-form';
13+
import { z } from 'zod';
14+
import { emailSchema, organizationNameSchema } from '@/lib/schemas';
15+
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '../ui/form';
16+
import { Input } from '../ui/input';
17+
import { Button } from '../ui/button';
18+
import { Checkbox } from '../ui/checkbox';
19+
import { Cross1Icon, PlusIcon } from '@radix-ui/react-icons';
20+
21+
const onboardingSchema = z.object({
22+
organizationName: organizationNameSchema,
23+
members: z.array(
24+
z.object({
25+
email: emailSchema.or(z.literal('')),
26+
}),
27+
),
28+
channels: z.object({
29+
slack: z.boolean(),
30+
email: z.boolean(),
31+
}),
32+
});
33+
34+
type OnboardingFormValues = z.infer<typeof onboardingSchema>;
1135

1236
export const Step1 = () => {
1337
const router = useRouter();
1438
const { toast } = useToast();
1539
const organization = useCurrentOrganization();
16-
const { setStep, setSkipped, setOnboarding } = useOnboarding();
40+
const { setStep, setSkipped, setOnboarding, onboarding } = useOnboarding();
41+
42+
const form = useZodForm<OnboardingFormValues>({
43+
mode: 'onChange',
44+
schema: onboardingSchema,
45+
defaultValues: {
46+
organizationName: organization?.name ?? '',
47+
members: [{ email: '' }],
48+
channels: { slack: onboarding?.slack ?? false, email: onboarding?.email ?? false },
49+
},
50+
});
51+
52+
const { fields, append, remove } = useFieldArray({
53+
control: form.control,
54+
name: 'members',
55+
});
1756

1857
const { mutate, isPending } = useMutation(createOnboarding, {
1958
onSuccess: (d) => {
@@ -25,9 +64,13 @@ export const Step1 = () => {
2564
return;
2665
}
2766

67+
// TODO: read slack + email from CreateOnboarding response once proto is updated
68+
const formValues = form.getValues();
2869
setOnboarding({
2970
federatedGraphsCount: d.federatedGraphsCount,
3071
finishedAt: d.finishedAt ? new Date(d.finishedAt) : undefined,
72+
slack: formValues.channels.slack,
73+
email: formValues.channels.email,
3174
});
3275
router.push('/onboarding/2');
3376
},
@@ -39,26 +82,110 @@ export const Step1 = () => {
3982
},
4083
});
4184

85+
const onSubmit: SubmitHandler<OnboardingFormValues> = (data) => {
86+
const emails = data.members.map((m) => m.email).filter((e) => e.length > 0);
87+
88+
mutate({
89+
organizationName: data.organizationName,
90+
slack: data.channels.slack,
91+
email: data.channels.email,
92+
invititationEmails: emails,
93+
});
94+
};
95+
4296
useEffect(() => {
4397
setStep(1);
4498
}, [setStep]);
4599

46100
return (
47101
<OnboardingContainer>
48-
<h2 className="text-2xl font-semibold tracking-tight">Step 1</h2>
102+
<Form {...form}>
103+
<form onSubmit={form.handleSubmit(onSubmit)} className="w-full space-y-8 text-left">
104+
<FormField
105+
control={form.control}
106+
name="organizationName"
107+
render={({ field }) => (
108+
<FormItem>
109+
<FormLabel>Organization Name</FormLabel>
110+
<FormDescription>This is your organization name. You can always change it later.</FormDescription>
111+
<FormControl>
112+
<Input {...field} />
113+
</FormControl>
114+
<FormMessage />
115+
</FormItem>
116+
)}
117+
/>
118+
119+
<div className="space-y-3 pt-4">
120+
<FormLabel>Invite Members</FormLabel>
121+
<FormDescription>Add team members by email.</FormDescription>
122+
<div className="space-y-2">
123+
{fields.map((field, index) => (
124+
<div key={field.id}>
125+
<div className="flex items-center gap-2">
126+
<Input placeholder="janedoe@example.com" {...form.register(`members.${index}.email`)} />
127+
{fields.length > 1 && (
128+
<Button type="button" variant="ghost" size="icon-sm" onClick={() => remove(index)}>
129+
<Cross1Icon />
130+
</Button>
131+
)}
132+
</div>
133+
{form.formState.errors.members?.[index]?.email && (
134+
<p className="mt-1 text-sm text-destructive">
135+
{form.formState.errors.members[index].email.message}
136+
</p>
137+
)}
138+
</div>
139+
))}
140+
</div>
141+
<Button type="button" variant="outline" size="sm" onClick={() => append({ email: '' })}>
142+
<PlusIcon className="mr-2" /> Add another
143+
</Button>
144+
</div>
145+
146+
<div className="space-y-3 pt-4">
147+
<FormLabel>Preferred way for us to reach you?</FormLabel>
148+
<FormDescription>If you get stuck with your Cosmo setup, we want to be able to help you.</FormDescription>
149+
<div className="space-y-4">
150+
<Controller
151+
control={form.control}
152+
name="channels.slack"
153+
render={({ field }) => (
154+
<label className="flex items-start gap-3">
155+
<Checkbox checked={field.value} onCheckedChange={(checked) => field.onChange(checked === true)} />
156+
<div className="flex flex-col gap-y-1">
157+
<span className="text-sm font-medium leading-none">Slack</span>
158+
<span className="text-[0.8rem] text-muted-foreground">
159+
We automatically create a Slack channel for you
160+
</span>
161+
</div>
162+
</label>
163+
)}
164+
/>
165+
<Controller
166+
control={form.control}
167+
name="channels.email"
168+
render={({ field }) => (
169+
<label className="flex items-start gap-3">
170+
<Checkbox checked={field.value} onCheckedChange={(checked) => field.onChange(checked === true)} />
171+
<div className="flex flex-col gap-y-1">
172+
<span className="text-sm font-medium leading-none">Email</span>
173+
<span className="text-[0.8rem] text-muted-foreground">Receive updates via email</span>
174+
</div>
175+
</label>
176+
)}
177+
/>
178+
</div>
179+
</div>
180+
</form>
181+
</Form>
182+
49183
<OnboardingNavigation
50184
onSkip={setSkipped}
51185
forward={{
52-
onClick: () => {
53-
// TODO: replace with real values in form
54-
mutate({
55-
organizationName: organization?.name ?? '',
56-
slack: true,
57-
email: false,
58-
invititationEmails: [],
59-
});
60-
},
186+
onClick: form.handleSubmit(onSubmit),
61187
isLoading: isPending,
188+
disabled: !form.formState.isValid,
62189
}}
63190
/>
64191
</OnboardingContainer>

studio/src/components/onboarding/step-4.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export const Step4 = () => {
3131
...prev,
3232
finishedAt: new Date(d.finishedAt),
3333
federatedGraphsCount: d.federatedGraphsCount,
34+
slack: Boolean(prev?.slack),
35+
email: Boolean(prev?.email),
3436
}));
3537

3638
setStep(undefined);

studio/src/hooks/use-onboarding-navigation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export const useOnboardingNavigation = () => {
2929
setOnboarding({
3030
finishedAt: data.finishedAt ? new Date(data.finishedAt) : undefined,
3131
federatedGraphsCount: data.federatedGraphsCount,
32+
slack: data.slack,
33+
email: data.email,
3234
});
3335
},
3436
[initialLoadSuccess, data, setOnboarding],

studio/src/lib/schemas.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { z } from 'zod';
2+
3+
export const emailSchema = z.string().email();
4+
5+
export const organizationNameSchema = z
6+
.string()
7+
.trim()
8+
.min(3, { message: 'Organization name must be a minimum of 3 characters' })
9+
.max(32, { message: 'Organization name must be maximum 32 characters' });

studio/src/pages/[organizationSlug]/members.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,15 @@ import { useRouter } from 'next/router';
4747
import { useState } from 'react';
4848
import { useDebounce } from 'use-debounce';
4949
import { z } from 'zod';
50+
import { emailSchema } from '@/lib/schemas';
5051
import { usePaginationParams } from '@/hooks/use-pagination-params';
5152
import { UpdateMemberGroupDialog } from '@/components/members/update-member-group-dialog';
5253
import { useIsAdmin } from '@/hooks/use-is-admin';
5354
import { formatDateTime } from '@/lib/format-date';
5455
import { MultiGroupSelect } from '@/components/multi-group-select';
5556

5657
const emailInputSchema = z.object({
57-
email: z.string().email(),
58+
email: emailSchema,
5859
groups: z.array(z.string().uuid()).min(1),
5960
});
6061

studio/src/pages/[organizationSlug]/settings.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import { useRouter } from 'next/router';
6060
import { Dispatch, SetStateAction, useContext, useEffect, useState } from 'react';
6161
import { FaMagic } from 'react-icons/fa';
6262
import { z } from 'zod';
63+
import { organizationNameSchema } from '@/lib/schemas';
6364
import { DeleteOrganization } from '@/components/settings/delete-organization';
6465
import { RestoreOrganization } from '@/components/settings/restore-organization';
6566

@@ -70,12 +71,7 @@ const OrganizationDetails = () => {
7071
const sessionQueryClient = useContext(SessionClientContext);
7172

7273
const schema = z.object({
73-
organizationName: z
74-
.string()
75-
.min(1, {
76-
message: 'Organization name must be a minimum of 1 character',
77-
})
78-
.max(24, { message: 'Organization name must be maximum 24 characters' }),
74+
organizationName: organizationNameSchema,
7975
organizationSlug: z
8076
.string()
8177
.toLowerCase()

0 commit comments

Comments
 (0)