Skip to content

Commit f41e121

Browse files
[WEB-5317] chore: enable multi-select for use case in onboarding flow (#8049)
* chore: update use_case type from string to array * chore: convert use_case field to JSONField with array support * feat: implement multi-select UI for use case in onboarding * chore: code refactor * chore: revert backend changes * chore: code refactor * chore: code refactor * chore: code refactor
1 parent 85daa15 commit f41e121

File tree

4 files changed

+60
-33
lines changed

4 files changed

+60
-33
lines changed

apps/web/core/components/onboarding/header.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ export const OnboardingHeader = observer(function OnboardingHeader(props: Onboar
5656
// derived values
5757
const currentStepNumber = getCurrentStepNumber();
5858
const totalSteps = hasInvitations ? 4 : 5; // 4 if invites available, 5 if not
59-
const userName = user?.display_name ?? `${user?.first_name} ${user?.last_name}` ?? user?.email;
59+
const userName = user?.display_name
60+
? user?.display_name
61+
: user?.first_name
62+
? `${user?.first_name} ${user?.last_name ?? ""}`
63+
: user?.email;
6064

6165
return (
6266
<div className="flex flex-col gap-4 sticky top-0 z-10">

apps/web/core/components/onboarding/profile-setup.tsx

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type { IUser, TUserProfile, TOnboardingSteps } from "@plane/types";
1616
// ui
1717
import { Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
1818
// components
19-
import { getFileURL, getPasswordStrength } from "@plane/utils";
19+
import { cn, getFileURL, getPasswordStrength } from "@plane/utils";
2020
import { UserImageUploadModal } from "@/components/core/modals/user-image-upload-modal";
2121
// constants
2222
// helpers
@@ -33,7 +33,7 @@ type TProfileSetupFormValues = {
3333
password?: string;
3434
confirm_password?: string;
3535
role?: string;
36-
use_case?: string;
36+
use_case?: string[];
3737
};
3838

3939
const defaultValues: Partial<TProfileSetupFormValues> = {
@@ -43,7 +43,7 @@ const defaultValues: Partial<TProfileSetupFormValues> = {
4343
password: undefined,
4444
confirm_password: undefined,
4545
role: undefined,
46-
use_case: undefined,
46+
use_case: [],
4747
};
4848

4949
type Props = {
@@ -139,7 +139,7 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
139139
avatar_url: formData.avatar_url ?? undefined,
140140
};
141141
const profileUpdatePayload: Partial<TUserProfile> = {
142-
use_case: formData.use_case,
142+
use_case: formData.use_case && formData.use_case.length > 0 ? formData.use_case.join(". ") : undefined,
143143
role: formData.role,
144144
};
145145
try {
@@ -151,7 +151,7 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
151151
captureSuccess({
152152
eventName: USER_TRACKER_EVENTS.add_details,
153153
payload: {
154-
use_case: formData.use_case,
154+
use_case: profileUpdatePayload.use_case,
155155
role: formData.role,
156156
},
157157
});
@@ -212,7 +212,7 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
212212

213213
const handleSubmitUserPersonalization = async (formData: TProfileSetupFormValues) => {
214214
const profileUpdatePayload: Partial<TUserProfile> = {
215-
use_case: formData.use_case,
215+
use_case: formData.use_case && formData.use_case.length > 0 ? formData.use_case.join(". ") : undefined,
216216
role: formData.role,
217217
};
218218
try {
@@ -223,7 +223,7 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
223223
captureSuccess({
224224
eventName: USER_TRACKER_EVENTS.add_details,
225225
payload: {
226-
use_case: formData.use_case,
226+
use_case: profileUpdatePayload.use_case,
227227
role: formData.role,
228228
},
229229
});
@@ -519,9 +519,13 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
519519
{USER_ROLE.map((userRole) => (
520520
<div
521521
key={userRole}
522-
className={`flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-custom-background-90 ${
523-
value === userRole ? "border-custom-primary-100" : "border-custom-border-300"
524-
} rounded px-3 py-1.5 text-sm font-medium`}
522+
className={cn(
523+
"flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-custom-background-90 rounded px-3 py-1.5 text-sm font-medium",
524+
{
525+
"border-custom-primary-100": value === userRole,
526+
"border-custom-border-300": value !== userRole,
527+
}
528+
)}
525529
onClick={() => onChange(userRole)}
526530
>
527531
{userRole}
@@ -537,27 +541,38 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
537541
className="text-sm text-custom-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
538542
htmlFor="use_case"
539543
>
540-
What is your domain expertise? Choose one.
544+
What is your domain expertise? Choose one or more.
541545
</label>
542546
<Controller
543547
control={control}
544548
name="use_case"
545549
rules={{
546-
required: "This field is required",
550+
required: "Please select at least one option",
551+
validate: (value) => (value && value.length > 0) || "Please select at least one option",
547552
}}
548553
render={({ field: { value, onChange } }) => (
549554
<div className="flex flex-wrap gap-2 py-2 overflow-auto break-all">
550-
{USER_DOMAIN.map((userDomain) => (
551-
<div
552-
key={userDomain}
553-
className={`flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-custom-background-90 ${
554-
value === userDomain ? "border-custom-primary-100" : "border-custom-border-300"
555-
} rounded px-3 py-1.5 text-sm font-medium`}
556-
onClick={() => onChange(userDomain)}
557-
>
558-
{userDomain}
559-
</div>
560-
))}
555+
{USER_DOMAIN.map((userDomain) => {
556+
const isSelected = value?.includes(userDomain) || false;
557+
return (
558+
<div
559+
key={userDomain}
560+
className={`flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-custom-background-90 ${
561+
isSelected ? "border-custom-primary-100" : "border-custom-border-300"
562+
} rounded px-3 py-1.5 text-sm font-medium`}
563+
onClick={() => {
564+
const currentValue = value || [];
565+
if (isSelected) {
566+
onChange(currentValue.filter((item) => item !== userDomain));
567+
} else {
568+
onChange([...currentValue, userDomain]);
569+
}
570+
}}
571+
>
572+
{userDomain}
573+
</div>
574+
);
575+
})}
561576
</div>
562577
)}
563578
/>

apps/web/core/components/onboarding/steps/profile/root.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export type TProfileSetupFormValues = {
3535
password?: string;
3636
confirm_password?: string;
3737
role?: string;
38-
use_case?: string;
38+
use_case?: string[];
3939
has_marketing_email_consent?: boolean;
4040
};
4141

apps/web/core/components/onboarding/steps/usecase/root.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type Props = {
2222
};
2323

2424
const defaultValues = {
25-
use_case: "",
25+
use_case: [] as string[],
2626
};
2727

2828
export const UseCaseSetupStep = observer(function UseCaseSetupStep({ handleStepChange }: Props) {
@@ -36,15 +36,15 @@ export const UseCaseSetupStep = observer(function UseCaseSetupStep({ handleStepC
3636
} = useForm<TProfileSetupFormValues>({
3737
defaultValues: {
3838
...defaultValues,
39-
use_case: profile?.use_case,
39+
use_case: profile?.use_case ? profile.use_case.split(". ") : [],
4040
},
4141
mode: "onChange",
4242
});
4343

4444
// handle submit
4545
const handleSubmitUserPersonalization = async (formData: TProfileSetupFormValues) => {
4646
const profileUpdatePayload: Partial<TUserProfile> = {
47-
use_case: formData.use_case,
47+
use_case: formData.use_case && formData.use_case.length > 0 ? formData.use_case.join(". ") : undefined,
4848
};
4949
try {
5050
await Promise.all([
@@ -54,7 +54,7 @@ export const UseCaseSetupStep = observer(function UseCaseSetupStep({ handleStepC
5454
captureSuccess({
5555
eventName: USER_TRACKER_EVENTS.add_details,
5656
payload: {
57-
use_case: formData.use_case,
57+
use_case: profileUpdatePayload.use_case,
5858
},
5959
});
6060
setToast({
@@ -100,25 +100,33 @@ export const UseCaseSetupStep = observer(function UseCaseSetupStep({ handleStepC
100100

101101
{/* Use Case Selection */}
102102
<div className="flex flex-col gap-3">
103-
<p className="text-sm font-medium text-custom-text-400">Select any</p>
103+
<p className="text-sm font-medium text-custom-text-400">Select one or more</p>
104104

105105
<Controller
106106
control={control}
107107
name="use_case"
108108
rules={{
109-
required: "This field is required",
109+
required: "Please select at least one option",
110+
validate: (value) => (value && value.length > 0) || "Please select at least one option",
110111
}}
111112
render={({ field: { value, onChange } }) => (
112113
<div className="flex flex-col gap-3">
113114
{USE_CASES.map((useCase) => {
114-
const isSelected = value === useCase;
115+
const isSelected = value?.includes(useCase) || false;
115116
return (
116117
<button
117118
key={useCase}
118119
onClick={(e) => {
119120
e.preventDefault();
120121
e.stopPropagation();
121-
onChange(useCase);
122+
const currentValue = value || [];
123+
if (isSelected) {
124+
// Remove from array
125+
onChange(currentValue.filter((item) => item !== useCase));
126+
} else {
127+
// Add to array
128+
onChange([...currentValue, useCase]);
129+
}
122130
}}
123131
className={`w-full px-3 py-2 rounded-lg border transition-all duration-200 flex items-center gap-2 ${
124132
isSelected

0 commit comments

Comments
 (0)