mirror of
https://github.com/makeplane/plane.git
synced 2025-12-16 03:47:54 +01:00
[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:
committed by
GitHub
parent
85daa1572c
commit
f41e121e58
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user