mirror of
https://github.com/makeplane/plane.git
synced 2025-12-16 11:57:56 +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
|
// derived values
|
||||||
const currentStepNumber = getCurrentStepNumber();
|
const currentStepNumber = getCurrentStepNumber();
|
||||||
const totalSteps = hasInvitations ? 4 : 5; // 4 if invites available, 5 if not
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-4 sticky top-0 z-10">
|
<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
|
// ui
|
||||||
import { Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
|
import { Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { getFileURL, getPasswordStrength } from "@plane/utils";
|
import { cn, getFileURL, getPasswordStrength } from "@plane/utils";
|
||||||
import { UserImageUploadModal } from "@/components/core/modals/user-image-upload-modal";
|
import { UserImageUploadModal } from "@/components/core/modals/user-image-upload-modal";
|
||||||
// constants
|
// constants
|
||||||
// helpers
|
// helpers
|
||||||
@@ -33,7 +33,7 @@ type TProfileSetupFormValues = {
|
|||||||
password?: string;
|
password?: string;
|
||||||
confirm_password?: string;
|
confirm_password?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
use_case?: string;
|
use_case?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues: Partial<TProfileSetupFormValues> = {
|
const defaultValues: Partial<TProfileSetupFormValues> = {
|
||||||
@@ -43,7 +43,7 @@ const defaultValues: Partial<TProfileSetupFormValues> = {
|
|||||||
password: undefined,
|
password: undefined,
|
||||||
confirm_password: undefined,
|
confirm_password: undefined,
|
||||||
role: undefined,
|
role: undefined,
|
||||||
use_case: undefined,
|
use_case: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -139,7 +139,7 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
|
|||||||
avatar_url: formData.avatar_url ?? undefined,
|
avatar_url: formData.avatar_url ?? undefined,
|
||||||
};
|
};
|
||||||
const profileUpdatePayload: Partial<TUserProfile> = {
|
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,
|
role: formData.role,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
@@ -151,7 +151,7 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
|
|||||||
captureSuccess({
|
captureSuccess({
|
||||||
eventName: USER_TRACKER_EVENTS.add_details,
|
eventName: USER_TRACKER_EVENTS.add_details,
|
||||||
payload: {
|
payload: {
|
||||||
use_case: formData.use_case,
|
use_case: profileUpdatePayload.use_case,
|
||||||
role: formData.role,
|
role: formData.role,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -212,7 +212,7 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
|
|||||||
|
|
||||||
const handleSubmitUserPersonalization = async (formData: TProfileSetupFormValues) => {
|
const handleSubmitUserPersonalization = async (formData: TProfileSetupFormValues) => {
|
||||||
const profileUpdatePayload: Partial<TUserProfile> = {
|
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,
|
role: formData.role,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
@@ -223,7 +223,7 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
|
|||||||
captureSuccess({
|
captureSuccess({
|
||||||
eventName: USER_TRACKER_EVENTS.add_details,
|
eventName: USER_TRACKER_EVENTS.add_details,
|
||||||
payload: {
|
payload: {
|
||||||
use_case: formData.use_case,
|
use_case: profileUpdatePayload.use_case,
|
||||||
role: formData.role,
|
role: formData.role,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -519,9 +519,13 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
|
|||||||
{USER_ROLE.map((userRole) => (
|
{USER_ROLE.map((userRole) => (
|
||||||
<div
|
<div
|
||||||
key={userRole}
|
key={userRole}
|
||||||
className={`flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-custom-background-90 ${
|
className={cn(
|
||||||
value === userRole ? "border-custom-primary-100" : "border-custom-border-300"
|
"flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-custom-background-90 rounded px-3 py-1.5 text-sm font-medium",
|
||||||
} 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)}
|
onClick={() => onChange(userRole)}
|
||||||
>
|
>
|
||||||
{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"
|
className="text-sm text-custom-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
|
||||||
htmlFor="use_case"
|
htmlFor="use_case"
|
||||||
>
|
>
|
||||||
What is your domain expertise? Choose one.
|
What is your domain expertise? Choose one or more.
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="use_case"
|
name="use_case"
|
||||||
rules={{
|
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 } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="flex flex-wrap gap-2 py-2 overflow-auto break-all">
|
<div className="flex flex-wrap gap-2 py-2 overflow-auto break-all">
|
||||||
{USER_DOMAIN.map((userDomain) => (
|
{USER_DOMAIN.map((userDomain) => {
|
||||||
<div
|
const isSelected = value?.includes(userDomain) || false;
|
||||||
key={userDomain}
|
return (
|
||||||
className={`flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-custom-background-90 ${
|
<div
|
||||||
value === userDomain ? "border-custom-primary-100" : "border-custom-border-300"
|
key={userDomain}
|
||||||
} rounded px-3 py-1.5 text-sm font-medium`}
|
className={`flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-custom-background-90 ${
|
||||||
onClick={() => onChange(userDomain)}
|
isSelected ? "border-custom-primary-100" : "border-custom-border-300"
|
||||||
>
|
} rounded px-3 py-1.5 text-sm font-medium`}
|
||||||
{userDomain}
|
onClick={() => {
|
||||||
</div>
|
const currentValue = value || [];
|
||||||
))}
|
if (isSelected) {
|
||||||
|
onChange(currentValue.filter((item) => item !== userDomain));
|
||||||
|
} else {
|
||||||
|
onChange([...currentValue, userDomain]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{userDomain}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export type TProfileSetupFormValues = {
|
|||||||
password?: string;
|
password?: string;
|
||||||
confirm_password?: string;
|
confirm_password?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
use_case?: string;
|
use_case?: string[];
|
||||||
has_marketing_email_consent?: boolean;
|
has_marketing_email_consent?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
use_case: "",
|
use_case: [] as string[],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UseCaseSetupStep = observer(function UseCaseSetupStep({ handleStepChange }: Props) {
|
export const UseCaseSetupStep = observer(function UseCaseSetupStep({ handleStepChange }: Props) {
|
||||||
@@ -36,7 +36,7 @@ export const UseCaseSetupStep = observer(function UseCaseSetupStep({ handleStepC
|
|||||||
} = useForm<TProfileSetupFormValues>({
|
} = useForm<TProfileSetupFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
use_case: profile?.use_case,
|
use_case: profile?.use_case ? profile.use_case.split(". ") : [],
|
||||||
},
|
},
|
||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
});
|
});
|
||||||
@@ -44,7 +44,7 @@ export const UseCaseSetupStep = observer(function UseCaseSetupStep({ handleStepC
|
|||||||
// handle submit
|
// handle submit
|
||||||
const handleSubmitUserPersonalization = async (formData: TProfileSetupFormValues) => {
|
const handleSubmitUserPersonalization = async (formData: TProfileSetupFormValues) => {
|
||||||
const profileUpdatePayload: Partial<TUserProfile> = {
|
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 {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -54,7 +54,7 @@ export const UseCaseSetupStep = observer(function UseCaseSetupStep({ handleStepC
|
|||||||
captureSuccess({
|
captureSuccess({
|
||||||
eventName: USER_TRACKER_EVENTS.add_details,
|
eventName: USER_TRACKER_EVENTS.add_details,
|
||||||
payload: {
|
payload: {
|
||||||
use_case: formData.use_case,
|
use_case: profileUpdatePayload.use_case,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setToast({
|
setToast({
|
||||||
@@ -100,25 +100,33 @@ export const UseCaseSetupStep = observer(function UseCaseSetupStep({ handleStepC
|
|||||||
|
|
||||||
{/* Use Case Selection */}
|
{/* Use Case Selection */}
|
||||||
<div className="flex flex-col gap-3">
|
<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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="use_case"
|
name="use_case"
|
||||||
rules={{
|
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 } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{USE_CASES.map((useCase) => {
|
{USE_CASES.map((useCase) => {
|
||||||
const isSelected = value === useCase;
|
const isSelected = value?.includes(useCase) || false;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={useCase}
|
key={useCase}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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 ${
|
className={`w-full px-3 py-2 rounded-lg border transition-all duration-200 flex items-center gap-2 ${
|
||||||
isSelected
|
isSelected
|
||||||
|
|||||||
Reference in New Issue
Block a user