From a01d1e60367c8e70ade46c67db10d5756a49c3a7 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Fri, 31 Oct 2025 14:00:34 +0530 Subject: [PATCH 1/5] chore: update use_case type from string to array --- packages/types/src/current-user/profile.ts | 2 +- packages/types/src/users.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/types/src/current-user/profile.ts b/packages/types/src/current-user/profile.ts index 64a25a93cf8..b15d793267e 100644 --- a/packages/types/src/current-user/profile.ts +++ b/packages/types/src/current-user/profile.ts @@ -18,7 +18,7 @@ export type TUserProfile = { is_onboarded: boolean; is_tour_completed: boolean; - use_case: string | undefined; + use_case: string[] | undefined; billing_address_country: string | undefined; billing_address: string | undefined; diff --git a/packages/types/src/users.ts b/packages/types/src/users.ts index 170492dc683..061445746c4 100644 --- a/packages/types/src/users.ts +++ b/packages/types/src/users.ts @@ -72,7 +72,7 @@ export type TUserProfile = { onboarding_step: TOnboardingSteps; is_onboarded: boolean; is_tour_completed: boolean; - use_case: string | undefined; + use_case: string[] | undefined; billing_address_country: string | undefined; billing_address: string | undefined; has_billing_address: boolean; From 24b427c7c7ab94d1692b7be078e5d50e679b1b6e Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Fri, 31 Oct 2025 14:02:53 +0530 Subject: [PATCH 2/5] chore: convert use_case field to JSONField with array support --- ...8_convert_profile_use_case_to_jsonfield.py | 36 +++++++++++++++++++ apps/api/plane/db/models/user.py | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 apps/api/plane/db/migrations/0108_convert_profile_use_case_to_jsonfield.py diff --git a/apps/api/plane/db/migrations/0108_convert_profile_use_case_to_jsonfield.py b/apps/api/plane/db/migrations/0108_convert_profile_use_case_to_jsonfield.py new file mode 100644 index 00000000000..0ef1aa11251 --- /dev/null +++ b/apps/api/plane/db/migrations/0108_convert_profile_use_case_to_jsonfield.py @@ -0,0 +1,36 @@ +# Generated manually for converting use_case from TextField to JSONField + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0107_migrate_filters_to_rich_filters"), + ] + + operations = [ + # Convert TextField to JSONField with data transformation in a single atomic operation + # Uses PostgreSQL's json_build_array to wrap existing text strings in JSON arrays + migrations.RunSQL( + # Forward: Convert TextField → JSONField, wrapping strings in arrays + sql=""" + ALTER TABLE profiles + ALTER COLUMN use_case TYPE jsonb + USING CASE + WHEN use_case IS NULL OR use_case = '' THEN '[]'::jsonb + ELSE json_build_array(use_case)::jsonb + END; + """, + # Reverse: Convert JSONField → TextField, extracting first array element + reverse_sql=""" + ALTER TABLE profiles + ALTER COLUMN use_case TYPE text + USING CASE + WHEN use_case IS NULL THEN NULL + WHEN jsonb_array_length(use_case) > 0 THEN use_case->>0 + ELSE NULL + END; + """, + ), + ] diff --git a/apps/api/plane/db/models/user.py b/apps/api/plane/db/models/user.py index c9f0df9b0d6..e432ab01e2b 100644 --- a/apps/api/plane/db/models/user.py +++ b/apps/api/plane/db/models/user.py @@ -192,7 +192,7 @@ class Profile(TimeAuditModel): # Onboarding is_tour_completed = models.BooleanField(default=False) onboarding_step = models.JSONField(default=get_default_onboarding) - use_case = models.TextField(blank=True, null=True) + use_case = models.JSONField(default=list, blank=True, null=True) role = models.CharField(max_length=300, null=True, blank=True) # job role is_onboarded = models.BooleanField(default=False) # Last visited workspace From 1c6c26701d02f990058ed91cc54ea2bb91cae4f0 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Fri, 31 Oct 2025 14:03:55 +0530 Subject: [PATCH 3/5] feat: implement multi-select UI for use case in onboarding --- .../components/onboarding/profile-setup.tsx | 41 ++++++++++++------- .../onboarding/steps/profile/root.tsx | 2 +- .../onboarding/steps/usecase/root.tsx | 18 +++++--- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/apps/web/core/components/onboarding/profile-setup.tsx b/apps/web/core/components/onboarding/profile-setup.tsx index 1dc8c716c3c..c818ba33c52 100644 --- a/apps/web/core/components/onboarding/profile-setup.tsx +++ b/apps/web/core/components/onboarding/profile-setup.tsx @@ -35,7 +35,7 @@ type TProfileSetupFormValues = { password?: string; confirm_password?: string; role?: string; - use_case?: string; + use_case?: string[]; }; const defaultValues: Partial = { @@ -45,7 +45,7 @@ const defaultValues: Partial = { password: undefined, confirm_password: undefined, role: undefined, - use_case: undefined, + use_case: [], }; type Props = { @@ -539,27 +539,38 @@ export const ProfileSetup: React.FC = observer((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. (value && value.length > 0) || "Please select at least one option", }} render={({ field: { value, onChange } }) => (
- {USER_DOMAIN.map((userDomain) => ( -
onChange(userDomain)} - > - {userDomain} -
- ))} + {USER_DOMAIN.map((userDomain) => { + const isSelected = value?.includes(userDomain) || false; + return ( +
{ + const currentValue = value || []; + if (isSelected) { + onChange(currentValue.filter((item) => item !== userDomain)); + } else { + onChange([...currentValue, userDomain]); + } + }} + > + {userDomain} +
+ ); + })}
)} /> diff --git a/apps/web/core/components/onboarding/steps/profile/root.tsx b/apps/web/core/components/onboarding/steps/profile/root.tsx index e2ad3f3a0dd..f38bcc12042 100644 --- a/apps/web/core/components/onboarding/steps/profile/root.tsx +++ b/apps/web/core/components/onboarding/steps/profile/root.tsx @@ -36,7 +36,7 @@ export type TProfileSetupFormValues = { password?: string; confirm_password?: string; role?: string; - use_case?: string; + use_case?: string[]; has_marketing_email_consent?: boolean; }; diff --git a/apps/web/core/components/onboarding/steps/usecase/root.tsx b/apps/web/core/components/onboarding/steps/usecase/root.tsx index 19ba706d015..248aa4ff479 100644 --- a/apps/web/core/components/onboarding/steps/usecase/root.tsx +++ b/apps/web/core/components/onboarding/steps/usecase/root.tsx @@ -24,7 +24,7 @@ type Props = { }; const defaultValues = { - use_case: "", + use_case: [] as string[], }; export const UseCaseSetupStep: FC = observer(({ handleStepChange }) => { @@ -102,25 +102,33 @@ export const UseCaseSetupStep: FC = observer(({ handleStepChange }) => { {/* Use Case Selection */}
-

Select any

+

Select one or more

(value && value.length > 0) || "Please select at least one option", }} render={({ field: { value, onChange } }) => (
{USE_CASES.map((useCase) => { - const isSelected = value === useCase; + const isSelected = value?.includes(useCase) || false; return (