[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
This commit is contained in:
Anmol Singh Bhatia
2025-12-08 15:48:15 +05:30
committed by GitHub
parent 85daa1572c
commit f41e121e58
4 changed files with 60 additions and 33 deletions

View File

@@ -56,7 +56,11 @@ export const OnboardingHeader = observer(function OnboardingHeader(props: Onboar
// derived values
const currentStepNumber = getCurrentStepNumber();
const totalSteps = hasInvitations ? 4 : 5; // 4 if invites available, 5 if not
const userName = user?.display_name ?? `${user?.first_name} ${user?.last_name}` ?? user?.email;
const userName = user?.display_name
? user?.display_name
: user?.first_name
? `${user?.first_name} ${user?.last_name ?? ""}`
: user?.email;
return (
<div className="flex flex-col gap-4 sticky top-0 z-10">

View File

@@ -16,7 +16,7 @@ import type { IUser, TUserProfile, TOnboardingSteps } from "@plane/types";
// ui
import { Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
// components
import { getFileURL, getPasswordStrength } from "@plane/utils";
import { cn, getFileURL, getPasswordStrength } from "@plane/utils";
import { UserImageUploadModal } from "@/components/core/modals/user-image-upload-modal";
// constants
// helpers
@@ -33,7 +33,7 @@ type TProfileSetupFormValues = {
password?: string;
confirm_password?: string;
role?: string;
use_case?: string;
use_case?: string[];
};
const defaultValues: Partial<TProfileSetupFormValues> = {
@@ -43,7 +43,7 @@ const defaultValues: Partial<TProfileSetupFormValues> = {
password: undefined,
confirm_password: undefined,
role: undefined,
use_case: undefined,
use_case: [],
};
type Props = {
@@ -139,7 +139,7 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
avatar_url: formData.avatar_url ?? undefined,
};
const profileUpdatePayload: Partial<TUserProfile> = {
use_case: formData.use_case,
use_case: formData.use_case && formData.use_case.length > 0 ? formData.use_case.join(". ") : undefined,
role: formData.role,
};
try {
@@ -151,7 +151,7 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
captureSuccess({
eventName: USER_TRACKER_EVENTS.add_details,
payload: {
use_case: formData.use_case,
use_case: profileUpdatePayload.use_case,
role: formData.role,
},
});
@@ -212,7 +212,7 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
const handleSubmitUserPersonalization = async (formData: TProfileSetupFormValues) => {
const profileUpdatePayload: Partial<TUserProfile> = {
use_case: formData.use_case,
use_case: formData.use_case && formData.use_case.length > 0 ? formData.use_case.join(". ") : undefined,
role: formData.role,
};
try {
@@ -223,7 +223,7 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
captureSuccess({
eventName: USER_TRACKER_EVENTS.add_details,
payload: {
use_case: formData.use_case,
use_case: profileUpdatePayload.use_case,
role: formData.role,
},
});
@@ -519,9 +519,13 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
{USER_ROLE.map((userRole) => (
<div
key={userRole}
className={`flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-custom-background-90 ${
value === userRole ? "border-custom-primary-100" : "border-custom-border-300"
} rounded px-3 py-1.5 text-sm font-medium`}
className={cn(
"flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-custom-background-90 rounded px-3 py-1.5 text-sm font-medium",
{
"border-custom-primary-100": value === userRole,
"border-custom-border-300": value !== userRole,
}
)}
onClick={() => onChange(userRole)}
>
{userRole}
@@ -537,27 +541,38 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
className="text-sm text-custom-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
htmlFor="use_case"
>
What is your domain expertise? Choose one.
What is your domain expertise? Choose one or more.
</label>
<Controller
control={control}
name="use_case"
rules={{
required: "This field is required",
required: "Please select at least one option",
validate: (value) => (value && value.length > 0) || "Please select at least one option",
}}
render={({ field: { value, onChange } }) => (
<div className="flex flex-wrap gap-2 py-2 overflow-auto break-all">
{USER_DOMAIN.map((userDomain) => (
<div
key={userDomain}
className={`flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-custom-background-90 ${
value === userDomain ? "border-custom-primary-100" : "border-custom-border-300"
} rounded px-3 py-1.5 text-sm font-medium`}
onClick={() => onChange(userDomain)}
>
{userDomain}
</div>
))}
{USER_DOMAIN.map((userDomain) => {
const isSelected = value?.includes(userDomain) || false;
return (
<div
key={userDomain}
className={`flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-custom-background-90 ${
isSelected ? "border-custom-primary-100" : "border-custom-border-300"
} rounded px-3 py-1.5 text-sm font-medium`}
onClick={() => {
const currentValue = value || [];
if (isSelected) {
onChange(currentValue.filter((item) => item !== userDomain));
} else {
onChange([...currentValue, userDomain]);
}
}}
>
{userDomain}
</div>
);
})}
</div>
)}
/>

View File

@@ -35,7 +35,7 @@ export type TProfileSetupFormValues = {
password?: string;
confirm_password?: string;
role?: string;
use_case?: string;
use_case?: string[];
has_marketing_email_consent?: boolean;
};

View File

@@ -22,7 +22,7 @@ type Props = {
};
const defaultValues = {
use_case: "",
use_case: [] as string[],
};
export const UseCaseSetupStep = observer(function UseCaseSetupStep({ handleStepChange }: Props) {
@@ -36,7 +36,7 @@ export const UseCaseSetupStep = observer(function UseCaseSetupStep({ handleStepC
} = useForm<TProfileSetupFormValues>({
defaultValues: {
...defaultValues,
use_case: profile?.use_case,
use_case: profile?.use_case ? profile.use_case.split(". ") : [],
},
mode: "onChange",
});
@@ -44,7 +44,7 @@ export const UseCaseSetupStep = observer(function UseCaseSetupStep({ handleStepC
// handle submit
const handleSubmitUserPersonalization = async (formData: TProfileSetupFormValues) => {
const profileUpdatePayload: Partial<TUserProfile> = {
use_case: formData.use_case,
use_case: formData.use_case && formData.use_case.length > 0 ? formData.use_case.join(". ") : undefined,
};
try {
await Promise.all([
@@ -54,7 +54,7 @@ export const UseCaseSetupStep = observer(function UseCaseSetupStep({ handleStepC
captureSuccess({
eventName: USER_TRACKER_EVENTS.add_details,
payload: {
use_case: formData.use_case,
use_case: profileUpdatePayload.use_case,
},
});
setToast({
@@ -100,25 +100,33 @@ export const UseCaseSetupStep = observer(function UseCaseSetupStep({ handleStepC
{/* Use Case Selection */}
<div className="flex flex-col gap-3">
<p className="text-sm font-medium text-custom-text-400">Select any</p>
<p className="text-sm font-medium text-custom-text-400">Select one or more</p>
<Controller
control={control}
name="use_case"
rules={{
required: "This field is required",
required: "Please select at least one option",
validate: (value) => (value && value.length > 0) || "Please select at least one option",
}}
render={({ field: { value, onChange } }) => (
<div className="flex flex-col gap-3">
{USE_CASES.map((useCase) => {
const isSelected = value === useCase;
const isSelected = value?.includes(useCase) || false;
return (
<button
key={useCase}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onChange(useCase);
const currentValue = value || [];
if (isSelected) {
// Remove from array
onChange(currentValue.filter((item) => item !== useCase));
} else {
// Add to array
onChange([...currentValue, useCase]);
}
}}
className={`w-full px-3 py-2 rounded-lg border transition-all duration-200 flex items-center gap-2 ${
isSelected