From cb6af812290ca1c7233c1883066f9b7cd07b26fc Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Tue, 21 Feb 2023 22:37:20 +0000 Subject: [PATCH 01/29] disable rule for better dx - linter will catch --- components/dashboard/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/dashboard/tsconfig.json b/components/dashboard/tsconfig.json index 583b996eae568e..b413f8e11d1ea0 100644 --- a/components/dashboard/tsconfig.json +++ b/components/dashboard/tsconfig.json @@ -13,7 +13,7 @@ "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, + "noUnusedLocals": false, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, From e85350759c7d9da8305c0af794e450d0cc4286a6 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Tue, 21 Feb 2023 22:37:45 +0000 Subject: [PATCH 02/29] add styles for other input types --- components/dashboard/src/index.css | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/dashboard/src/index.css b/components/dashboard/src/index.css index 0a738e04c14a73..9ed055a4c28a4a 100644 --- a/components/dashboard/src/index.css +++ b/components/dashboard/src/index.css @@ -85,6 +85,8 @@ input[type="tel"], input[type="number"], input[type="password"], + input[type="email"], + input[type="url"], select { @apply block w-56 text-gray-600 dark:text-gray-400 dark:bg-gray-800 bg-white rounded-md border border-gray-300 dark:border-gray-500 focus:border-gray-400 dark:focus:border-gray-400 focus:ring-0; } @@ -93,7 +95,9 @@ input[type="tel"]::placeholder, input[type="number"]::placeholder, input[type="search"]::placeholder, - input[type="password"]::placeholder { + input[type="password"]::placeholder, + input[type="email"]::placeholder, + input[type="url"]::placeholder { @apply text-gray-400 dark:text-gray-500; } input[type="text"].error, @@ -101,6 +105,8 @@ input[type="number"].error, input[type="search"].error, input[type="password"].error, + input[type="email"].error, + input[type="url"].error, select.error { @apply border-gitpod-red dark:border-gitpod-red focus:border-gitpod-red dark:focus:border-gitpod-red; } From d4b78323e7ea90fa5a01499f5bd671ab6f554a6a Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Tue, 21 Feb 2023 22:39:10 +0000 Subject: [PATCH 03/29] adding more options to input components --- .../src/components/forms/InputField.tsx | 5 ++- .../src/components/forms/SelectInputField.tsx | 41 +++++++++++++++++-- .../src/components/forms/TextInputField.tsx | 12 ++++-- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/components/dashboard/src/components/forms/InputField.tsx b/components/dashboard/src/components/forms/InputField.tsx index e78758f219fddc..7725591cbc4eee 100644 --- a/components/dashboard/src/components/forms/InputField.tsx +++ b/components/dashboard/src/components/forms/InputField.tsx @@ -12,11 +12,12 @@ type Props = { id?: string; hint?: ReactNode; error?: ReactNode; + className?: string; }; -export const InputField: FunctionComponent = memo(({ label, id, hint, error, children }) => { +export const InputField: FunctionComponent = memo(({ label, id, hint, error, className, children }) => { return ( -
+
); }; export default UserOnboarding; - -// const getInitialNameParts = (user: User) => { -// const name = user.fullName || user.name || ""; -// let firstName = name; -// let lastName = ""; - -// const parts = name.split(" "); -// if (parts.length > 1) { -// firstName = parts.shift() || ""; -// lastName = parts.join(" "); -// } - -// return { firstName, lastName }; -// }; - -// const getInitialProfileState = (user: User): OnboardingProfileDetails => { -// const { firstName, lastName } = getInitialNameParts(user); - -// return { -// firstName, -// lastName, -// emailAddress: user.additionalData?.profile?.emailAddress ?? "", -// companyWebsite: user.additionalData?.profile?.companyWebsite ?? "", -// jobRole: user.additionalData?.profile?.jobRole ?? "", -// jobRoleOther: user.additionalData?.profile?.jobRoleOther ?? "", -// signupGoals: user.additionalData?.profile?.signupGoals || "", -// signupGoalsOther: user.additionalData?.profile?.signupGoalsOther ?? "", -// }; -// }; From 05ebdd788735ec807ed6c01a00d27290c2dc9e7a Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Wed, 22 Feb 2023 04:37:23 +0000 Subject: [PATCH 09/29] removing un-needed code --- .../src/components/forms/SelectInputField.tsx | 36 ++----------------- components/gitpod-protocol/src/protocol.ts | 5 --- 2 files changed, 2 insertions(+), 39 deletions(-) diff --git a/components/dashboard/src/components/forms/SelectInputField.tsx b/components/dashboard/src/components/forms/SelectInputField.tsx index 8355ee3fca19be..65d91c681fb4d6 100644 --- a/components/dashboard/src/components/forms/SelectInputField.tsx +++ b/components/dashboard/src/components/forms/SelectInputField.tsx @@ -12,8 +12,6 @@ import { InputField } from "./InputField"; type Props = { label: ReactNode; value?: string; - multiple?: boolean; - size?: number; id?: string; hint?: ReactNode; error?: ReactNode; @@ -24,20 +22,7 @@ type Props = { }; export const SelectInputField: FunctionComponent = memo( - ({ - label, - value, - multiple, - size, - id, - hint, - error, - disabled = false, - required = false, - children, - onChange, - onBlur, - }) => { + ({ label, value, id, hint, error, disabled = false, required = false, children, onChange, onBlur }) => { const maybeId = useId(); const elementId = id || maybeId; @@ -46,8 +31,6 @@ export const SelectInputField: FunctionComponent = memo( = memo( type SelectInputProps = { value?: string; - multiple?: boolean; - size?: number; className?: string; id?: string; disabled?: boolean; @@ -74,18 +55,7 @@ type SelectInputProps = { }; export const SelectInput: FunctionComponent = memo( - ({ - value, - multiple = false, - size, - className, - id, - disabled = false, - required = false, - children, - onChange, - onBlur, - }) => { + ({ value, className, id, disabled = false, required = false, children, onChange, onBlur }) => { const handleChange = useCallback( (e) => { onChange && onChange(e.target.value); @@ -100,8 +70,6 @@ export const SelectInput: FunctionComponent = memo( id={id} className={classNames("w-full max-w-lg", className)} value={value} - multiple={multiple} - size={size} disabled={disabled} required={required} onChange={handleChange} diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 8eb85872e44602..6d866419dc0aab 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -123,11 +123,6 @@ export namespace User { return !hasPreferredIde(user); } - // TODO: Determine how we want to handle who we show new onboarding flow to, and fix this fn name - export function isOnboardingUserNew(user: User) { - return !hasPreferredIde(user) && !user.additionalData?.profile?.jobRole; - } - export function migrationIDESettings(user: User) { if ( !user?.additionalData?.ideSettings || From f078fcdcfcfcbfa7aa5a74228b5160089da7be7c Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Wed, 22 Feb 2023 04:40:39 +0000 Subject: [PATCH 10/29] Plug in ThemeSelector component --- .../src/user-settings/Preferences.tsx | 76 +------------------ 1 file changed, 3 insertions(+), 73 deletions(-) diff --git a/components/dashboard/src/user-settings/Preferences.tsx b/components/dashboard/src/user-settings/Preferences.tsx index defa8dca7be2b5..3d208466d2979a 100644 --- a/components/dashboard/src/user-settings/Preferences.tsx +++ b/components/dashboard/src/user-settings/Preferences.tsx @@ -5,33 +5,15 @@ */ import { useContext, useState } from "react"; -import SelectableCardSolid from "../components/SelectableCardSolid"; import { getGitpodService } from "../service/service"; -import { ThemeContext } from "../theme-context"; import { UserContext } from "../user-context"; import { trackEvent } from "../Analytics"; import SelectIDE from "./SelectIDE"; import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu"; - -type Theme = "light" | "dark" | "system"; +import { ThemeSelector } from "../components/ThemeSelector"; export default function Preferences() { const { user } = useContext(UserContext); - const { setIsDark } = useContext(ThemeContext); - - const [theme, setTheme] = useState(localStorage.theme || "system"); - const actuallySetTheme = (theme: Theme) => { - if (theme === "dark" || theme === "light") { - localStorage.theme = theme; - } else { - localStorage.removeItem("theme"); - } - const isDark = - localStorage.theme === "dark" || - (localStorage.theme !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches); - setIsDark(isDark); - setTheme(theme); - }; const [dotfileRepo, setDotfileRepo] = useState(user?.additionalData?.dotfileRepo || ""); const actuallySetDotfileRepo = async (value: string) => { @@ -63,60 +45,8 @@ export default function Preferences() {

-

Theme

-

Early bird or night owl? Choose your side.

-
- actuallySetTheme("light")} - > -
- - - -
-
- actuallySetTheme("dark")} - > -
- - - -
-
- actuallySetTheme("system")} - > -
- - - - - -
-
-
+ +

Dotfiles

Customize workspaces using dotfiles.

From aa05a794fae780b08e4b39692949620b89b0b8dc Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Wed, 22 Feb 2023 04:45:46 +0000 Subject: [PATCH 11/29] cleanup --- .../dashboard/src/components/forms/SelectInputField.tsx | 4 ++-- components/dashboard/src/onboarding/OnboardingStep.tsx | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/components/dashboard/src/components/forms/SelectInputField.tsx b/components/dashboard/src/components/forms/SelectInputField.tsx index 65d91c681fb4d6..6f476b60d4c833 100644 --- a/components/dashboard/src/components/forms/SelectInputField.tsx +++ b/components/dashboard/src/components/forms/SelectInputField.tsx @@ -11,7 +11,7 @@ import { InputField } from "./InputField"; type Props = { label: ReactNode; - value?: string; + value: string; id?: string; hint?: ReactNode; error?: ReactNode; @@ -45,7 +45,7 @@ export const SelectInputField: FunctionComponent = memo( ); type SelectInputProps = { - value?: string; + value: string; className?: string; id?: string; disabled?: boolean; diff --git a/components/dashboard/src/onboarding/OnboardingStep.tsx b/components/dashboard/src/onboarding/OnboardingStep.tsx index 59bbf38adfdc5d..c488582b89dd0f 100644 --- a/components/dashboard/src/onboarding/OnboardingStep.tsx +++ b/components/dashboard/src/onboarding/OnboardingStep.tsx @@ -35,7 +35,6 @@ export const OnboardingStep: FC = ({ title, subtitle, isValid, children, ); return ( - // flex classes here to account for an upcoming image on the right

{title}

{subtitle}

From 4ec1fd7c2e513a98c61d0d37b2d9940ee3ec56c2 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Wed, 22 Feb 2023 17:01:02 +0000 Subject: [PATCH 12/29] update onboarding logic --- components/dashboard/src/app/AppRoutes.tsx | 24 +++++----- .../src/onboarding/OnboardingStep.tsx | 2 +- .../dashboard/src/onboarding/StepUserInfo.tsx | 3 +- .../src/onboarding/UserOnboarding.tsx | 44 +++++++++++++------ 4 files changed, 47 insertions(+), 26 deletions(-) diff --git a/components/dashboard/src/app/AppRoutes.tsx b/components/dashboard/src/app/AppRoutes.tsx index e2e1211bf4a026..f10a1f4e132bf8 100644 --- a/components/dashboard/src/app/AppRoutes.tsx +++ b/components/dashboard/src/app/AppRoutes.tsx @@ -46,6 +46,7 @@ import { OrgRequiredRoute } from "./OrgRequiredRoute"; import { WebsocketClients } from "./WebsocketClients"; import { StartWorkspaceOptions } from "../start/start-workspace-options"; import { useFeatureFlags } from "../contexts/FeatureFlagContext"; +import { FORCE_ONBOARDING_PARAM, FORCE_ONBOARDING_PARAM_VALUE } from "../onboarding/UserOnboarding"; const Setup = React.lazy(() => import(/* webpackPrefetch: true */ "../Setup")); const Workspaces = React.lazy(() => import(/* webpackPrefetch: true */ "../workspaces/Workspaces")); @@ -104,12 +105,6 @@ export const AppRoutes: FunctionComponent = ({ user, teams }) => const { newSignupFlow } = useFeatureFlags(); const search = new URLSearchParams(location.search); - // Prefix with `/#referrer` will specify an IDE for workspace - // We don't need to show IDE preference in this case - const [showUserIdePreference, setShowUserIdePreference] = useState( - User.isOnboardingUser(user) && !hash.startsWith(ContextURL.REFERRER_PREFIX), - ); - // TODO: Add a Route for this instead of inspecting location manually if (location.pathname.startsWith("/blocked")) { return ; @@ -124,19 +119,26 @@ export const AppRoutes: FunctionComponent = ({ user, teams }) => return setWhatsNewShown(false)} />; } - // Placeholder for new signup flow - check we make here tbd still - if (newSignupFlow && search.get("onboarding") === "1") { + // Show new signup flow if: + // * feature flag enabled + // * User is onboarding (no ide selected yet) OR query param `onboarding=force` is set + const showNewSignupFlow = + newSignupFlow && + (User.isOnboardingUser(user) || search.get(FORCE_ONBOARDING_PARAM) === FORCE_ONBOARDING_PARAM_VALUE); + if (showNewSignupFlow) { return ; } // TODO: Try and encapsulate this in a route for "/" (check for hash in route component, render or redirect accordingly) const isCreation = location.pathname === "/" && hash !== ""; if (isCreation) { - if (showUserIdePreference) { + // Prefix with `/#referrer` will specify an IDE for workspace + // After selection is saved, user will be updated, and this condition will be false + const showIDESelection = User.isOnboardingUser(user) && !hash.startsWith(ContextURL.REFERRER_PREFIX); + if (showIDESelection) { return ( - {/* TODO: ensure we don't show this after new onboarding flow */} - setShowUserIdePreference(false)} /> + ); } else if (new URLSearchParams(location.search).has("showOptions") || newCreateWsPage) { diff --git a/components/dashboard/src/onboarding/OnboardingStep.tsx b/components/dashboard/src/onboarding/OnboardingStep.tsx index c488582b89dd0f..20c678646ab307 100644 --- a/components/dashboard/src/onboarding/OnboardingStep.tsx +++ b/components/dashboard/src/onboarding/OnboardingStep.tsx @@ -39,7 +39,7 @@ export const OnboardingStep: FC = ({ title, subtitle, isValid, children,

{title}

{subtitle}

-
+ {/* Form contents provided as children */} {children} diff --git a/components/dashboard/src/onboarding/StepUserInfo.tsx b/components/dashboard/src/onboarding/StepUserInfo.tsx index 56e6859e134fbb..9e341a4e13a046 100644 --- a/components/dashboard/src/onboarding/StepUserInfo.tsx +++ b/components/dashboard/src/onboarding/StepUserInfo.tsx @@ -20,7 +20,7 @@ export const StepUserInfo: FC = ({ user, onComplete }) => { const [firstName, setFirstName] = useState(first); const [lastName, setLastName] = useState(last); - const [emailAddress, setEmailAddress] = useState(user.additionalData?.profile?.emailAddress ?? ""); + const [emailAddress, setEmailAddress] = useState(User.getPrimaryEmail(user) ?? ""); const prepareUpdates = useCallback(() => { const additionalData = user.additionalData || {}; @@ -86,6 +86,7 @@ export const StepUserInfo: FC = ({ user, onComplete }) => { ); }; +// Intentionally not using User.getName() here to avoid relying on identity.authName (likely not user's real name) const getInitialNameParts = (user: User) => { const name = user.fullName || user.name || ""; let first = name; diff --git a/components/dashboard/src/onboarding/UserOnboarding.tsx b/components/dashboard/src/onboarding/UserOnboarding.tsx index 124e17769d9c2b..30b14c832dfbde 100644 --- a/components/dashboard/src/onboarding/UserOnboarding.tsx +++ b/components/dashboard/src/onboarding/UserOnboarding.tsx @@ -5,15 +5,20 @@ */ import { User } from "@gitpod/gitpod-protocol"; -import { FunctionComponent, useContext, useState } from "react"; +import { FunctionComponent, useCallback, useContext, useState } from "react"; import gitpodIcon from "../icons/gitpod.svg"; import Separator from "../components/Separator"; -import { useHistory } from "react-router"; +import { useHistory, useLocation } from "react-router"; import { StepUserInfo } from "./StepUserInfo"; import { UserContext } from "../user-context"; import { StepOrgInfo } from "./StepOrgInfo"; import { StepPersonalize } from "./StepPersonalize"; +// This param is optionally present to force an onboarding flow +// Can be used if other conditions aren't true, i.e. if user has already onboarded, but we want to force the flow again +export const FORCE_ONBOARDING_PARAM = "onboarding"; +export const FORCE_ONBOARDING_PARAM_VALUE = "force"; + const STEPS = { ONE: "one", TWO: "two", @@ -24,10 +29,32 @@ type Props = { }; const UserOnboarding: FunctionComponent = ({ user }) => { const history = useHistory(); + const location = useLocation(); const [step, setStep] = useState(STEPS.ONE); // TODO: Remove this once current user is behind react-query const { setUser } = useContext(UserContext); + const onboardingComplete = useCallback( + (updatedUser: User) => { + // Ideally this state update results in the onboarding flow being dismissed, we done. + setUser(updatedUser); + + // Look for the `onboarding=force` query param, and remove if present + const queryParams = new URLSearchParams(location.search); + if (queryParams.get(FORCE_ONBOARDING_PARAM) === FORCE_ONBOARDING_PARAM_VALUE) { + queryParams.delete(FORCE_ONBOARDING_PARAM); + history.replace({ + pathname: location.pathname, + search: queryParams.toString(), + hash: location.hash, + }); + } + // TODO: should be able to remove this once state that shows this flow is updated + // history.push("/workspaces"); + }, + [history, location.hash, location.pathname, location.search, setUser], + ); + return (
@@ -46,24 +73,15 @@ const UserOnboarding: FunctionComponent = ({ user }) => { /> )} {step === STEPS.TWO && ( - { - setUser(updatedUser); - setStep(STEPS.THREE); - }} - /> - )} - {step === STEPS.THREE && ( { setUser(updatedUser); - // TODO: should be able to remove this once state that shows this flow is updated - history.push("/workspaces"); + setStep(STEPS.THREE); }} /> )} + {step === STEPS.THREE && }
From 02b3cd3d293a49bb2278a81e1fc04206c294aede Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Wed, 22 Feb 2023 17:01:26 +0000 Subject: [PATCH 13/29] disable or members query if no current org present --- .../dashboard/src/data/organizations/org-members-query.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/dashboard/src/data/organizations/org-members-query.ts b/components/dashboard/src/data/organizations/org-members-query.ts index 7c9ee98c2787bc..179a9ff7ba0c9e 100644 --- a/components/dashboard/src/data/organizations/org-members-query.ts +++ b/components/dashboard/src/data/organizations/org-members-query.ts @@ -26,6 +26,8 @@ export const useOrgMembers = () => { return publicApiTeamMembersToProtocol(resp.team?.members || []); }, + // If no current org is set, disable query + enabled: !!organization, }); }; From 0f12ff518e25ddc967412580c85233977d800568 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Wed, 22 Feb 2023 23:45:26 +0000 Subject: [PATCH 14/29] adjusting where we save onboarding data --- .../src/onboarding/OnboardingStep.tsx | 33 ++++--- .../dashboard/src/onboarding/StepOrgInfo.tsx | 29 ++++-- .../src/onboarding/StepPersonalize.tsx | 26 ++---- .../dashboard/src/onboarding/StepUserInfo.tsx | 21 +++-- .../src/onboarding/UserOnboarding.tsx | 90 +++++++++++++++---- .../dashboard/src/workspaces/Workspaces.tsx | 2 + components/gitpod-protocol/src/protocol.ts | 2 + 7 files changed, 138 insertions(+), 65 deletions(-) diff --git a/components/dashboard/src/onboarding/OnboardingStep.tsx b/components/dashboard/src/onboarding/OnboardingStep.tsx index 20c678646ab307..b3780da4e168d8 100644 --- a/components/dashboard/src/onboarding/OnboardingStep.tsx +++ b/components/dashboard/src/onboarding/OnboardingStep.tsx @@ -4,34 +4,33 @@ * See License.AGPL.txt in the project root for license information. */ -import { User } from "@gitpod/gitpod-protocol"; import { FC, FormEvent, useCallback } from "react"; import Alert from "../components/Alert"; -import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutation"; type Props = { title: string; subtitle: string; isValid: boolean; - onUpdated(user: User): void; - prepareUpdates(): Partial; + isLoading?: boolean; + error?: string; + onSubmit(): void; }; -export const OnboardingStep: FC = ({ title, subtitle, isValid, children, prepareUpdates, onUpdated }) => { - const updateUser = useUpdateCurrentUserMutation(); - +export const OnboardingStep: FC = ({ + title, + subtitle, + isValid, + isLoading = false, + error, + children, + onSubmit, +}) => { const handleSubmit = useCallback( async (e: FormEvent) => { e.preventDefault(); - const updates = prepareUpdates(); - try { - const updatedUser = await updateUser.mutateAsync(updates); - onUpdated(updatedUser); - } catch (e) { - console.error(e); - } + onSubmit(); }, - [onUpdated, prepareUpdates, updateUser], + [onSubmit], ); return ( @@ -43,10 +42,10 @@ export const OnboardingStep: FC = ({ title, subtitle, isValid, children, {/* Form contents provided as children */} {children} - {updateUser.isError && There was a problem updating your profile} + {error && {error}}
-
diff --git a/components/dashboard/src/onboarding/StepOrgInfo.tsx b/components/dashboard/src/onboarding/StepOrgInfo.tsx index ae1c8d73fb6c3f..4046a6ec0868c3 100644 --- a/components/dashboard/src/onboarding/StepOrgInfo.tsx +++ b/components/dashboard/src/onboarding/StepOrgInfo.tsx @@ -8,6 +8,7 @@ import { User } from "@gitpod/gitpod-protocol"; import { FC, useCallback, useMemo, useState } from "react"; import { SelectInputField } from "../components/forms/SelectInputField"; import { TextInputField } from "../components/forms/TextInputField"; +import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutation"; import { useOnBlurError } from "../hooks/use-onblur-error"; import { OnboardingStep } from "./OnboardingStep"; @@ -16,6 +17,7 @@ type Props = { onComplete(user: User): void; }; export const StepOrgInfo: FC = ({ user, onComplete }) => { + const updateUser = useUpdateCurrentUserMutation(); const jobRoleOptions = useMemo(getJobRoleOptions, []); const signupGoalsOptions = useMemo(getSignupGoalsOptions, []); @@ -25,11 +27,11 @@ export const StepOrgInfo: FC = ({ user, onComplete }) => { const [signupGoalsOther, setSignupGoalsOther] = useState(user.additionalData?.profile?.signupGoalsOther ?? ""); const [companyWebsite, setCompanyWebsite] = useState(user.additionalData?.profile?.companyWebsite ?? ""); - const prepareUpdates = useCallback(() => { + const handleSubmit = useCallback(async () => { const additionalData = user.additionalData || {}; const profile = additionalData.profile || {}; - return { + const updates = { additionalData: { ...additionalData, profile: { @@ -42,7 +44,23 @@ export const StepOrgInfo: FC = ({ user, onComplete }) => { }, }, }; - }, [companyWebsite, jobRole, jobRoleOther, signupGoals, signupGoalsOther, user.additionalData]); + + try { + const updatedUser = await updateUser.mutateAsync(updates); + onComplete(updatedUser); + } catch (e) { + console.error(e); + } + }, [ + companyWebsite, + jobRole, + jobRoleOther, + onComplete, + signupGoals, + signupGoalsOther, + updateUser, + user.additionalData, + ]); const jobRoleError = useOnBlurError("Please select one", !!jobRole); const jobRoleOtherError = useOnBlurError( @@ -61,9 +79,10 @@ export const StepOrgInfo: FC = ({ user, onComplete }) => { = ({ user, onComplete }) => { const [ide, setIDE] = useState(user?.additionalData?.ideSettings?.defaultIde || "code"); const [useLatest, setUseLatest] = useState(user?.additionalData?.ideSettings?.useLatestVersion ?? false); - const prepareUpdates = useCallback(() => { - const additionalData = user.additionalData || {}; - const ideSettings = additionalData.ideSettings || {}; - - return { - additionalData: { - ...additionalData, - ideSettings: { - ...ideSettings, - settingVersion: "2.0", - defaultIde: ide, - useLatestVersion: useLatest, - }, - }, - }; - }, [ide, useLatest, user.additionalData]); + // This step doesn't save the ide selection yet (happens at the end), just passes them along + const handleSubmitted = useCallback(() => { + onComplete(ide, useLatest); + }, [ide, onComplete, useLatest]); const isValid = !!ide; @@ -42,8 +31,7 @@ export const StepPersonalize: FC = ({ user, onComplete }) => { title="Personalize Gitpod" subtitle="Cusomize your experience" isValid={isValid} - prepareUpdates={prepareUpdates} - onUpdated={onComplete} + onSubmit={handleSubmitted} >

Choose an editor

You can change this later in your user preferences.

diff --git a/components/dashboard/src/onboarding/StepUserInfo.tsx b/components/dashboard/src/onboarding/StepUserInfo.tsx index 9e341a4e13a046..cf7bc2a40429b6 100644 --- a/components/dashboard/src/onboarding/StepUserInfo.tsx +++ b/components/dashboard/src/onboarding/StepUserInfo.tsx @@ -7,6 +7,7 @@ import { User } from "@gitpod/gitpod-protocol"; import { FC, useCallback, useState } from "react"; import { TextInputField } from "../components/forms/TextInputField"; +import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutation"; import { useOnBlurError } from "../hooks/use-onblur-error"; import { OnboardingStep } from "./OnboardingStep"; @@ -15,6 +16,7 @@ type Props = { onComplete(user: User): void; }; export const StepUserInfo: FC = ({ user, onComplete }) => { + const updateUser = useUpdateCurrentUserMutation(); // attempt to split provided name for default input values const { first, last } = getInitialNameParts(user); @@ -22,11 +24,11 @@ export const StepUserInfo: FC = ({ user, onComplete }) => { const [lastName, setLastName] = useState(last); const [emailAddress, setEmailAddress] = useState(User.getPrimaryEmail(user) ?? ""); - const prepareUpdates = useCallback(() => { + const handleSubmit = useCallback(async () => { const additionalData = user.additionalData || {}; const profile = additionalData.profile || {}; - return { + const updates = { // we only split these out currently for form collection, but combine in the db fullName: `${firstName} ${lastName}`, additionalData: { @@ -34,10 +36,18 @@ export const StepUserInfo: FC = ({ user, onComplete }) => { profile: { ...profile, emailAddress, + lastUpdatedDetailsNudge: new Date().toISOString(), }, }, }; - }, [emailAddress, firstName, lastName, user.additionalData]); + + try { + const updatedUser = await updateUser.mutateAsync(updates); + onComplete(updatedUser); + } catch (e) { + console.error(e); + } + }, [emailAddress, firstName, lastName, onComplete, updateUser, user.additionalData]); const firstNameError = useOnBlurError("Please enter a value", !!firstName); const lastNameError = useOnBlurError("Please enter a value", !!lastName); @@ -49,9 +59,10 @@ export const StepUserInfo: FC = ({ user, onComplete }) => {
= ({ user }) => { const history = useHistory(); const location = useLocation(); - const [step, setStep] = useState(STEPS.ONE); - // TODO: Remove this once current user is behind react-query const { setUser } = useContext(UserContext); + const updateUser = useUpdateCurrentUserMutation(); + + const [step, setStep] = useState(STEPS.ONE); + const [completingError, setCompletingError] = useState(""); + // We track this state here so we can persist it at the end of the flow instead of when it's selected + // This is because setting the ide is how we indicate a user has onboarded, and want to defer that until the end + // even though we may ask for it earlier in the flow. The tradeoff is a potential error state at the end of the flow when updating the IDE + const [ideOptions, setIDEOptions] = useState({ ide: "code", useLatest: false }); + + // TODO: This logic can be simplified in the future if we put existing users through onboarding and track the onboarded timestamp + // When onboarding is complete (last step finished), we do the following + // * Update the user's IDE selection (current logic relies on this for considering a user onboarded, so we wait until the end) + // * Set an user's onboarded timestamp + // * Update the `user` context w/ the latest user, which will close out this onboarding flow const onboardingComplete = useCallback( - (updatedUser: User) => { - // Ideally this state update results in the onboarding flow being dismissed, we done. - setUser(updatedUser); + async (updatedUser: User) => { + try { + const additionalData = updatedUser.additionalData || {}; + const profile = additionalData.profile || {}; + const ideSettings = additionalData.ideSettings || {}; + + const updates = { + additionalData: { + ...additionalData, + profile: { + ...profile, + onboardedTimestamp: new Date().toISOString(), + }, + ideSettings: { + ...ideSettings, + settingVersion: "2.0", + defaultIde: ideOptions.ide, + useLatestVersion: ideOptions.useLatest, + }, + }, + }; - // Look for the `onboarding=force` query param, and remove if present - const queryParams = new URLSearchParams(location.search); - if (queryParams.get(FORCE_ONBOARDING_PARAM) === FORCE_ONBOARDING_PARAM_VALUE) { - queryParams.delete(FORCE_ONBOARDING_PARAM); - history.replace({ - pathname: location.pathname, - search: queryParams.toString(), - hash: location.hash, - }); + try { + const onboardedUser = await updateUser.mutateAsync(updates); + setUser(onboardedUser); + + // Look for the `onboarding=force` query param, and remove if present + const queryParams = new URLSearchParams(location.search); + if (queryParams.get(FORCE_ONBOARDING_PARAM) === FORCE_ONBOARDING_PARAM_VALUE) { + queryParams.delete(FORCE_ONBOARDING_PARAM); + history.replace({ + pathname: location.pathname, + search: queryParams.toString(), + hash: location.hash, + }); + } + } catch (e) { + console.error(e); + setCompletingError("There was a problem completing your onboarding"); + } + } catch (e) { + console.error(e); } - // TODO: should be able to remove this once state that shows this flow is updated - // history.push("/workspaces"); }, - [history, location.hash, location.pathname, location.search, setUser], + [ + history, + ideOptions.ide, + ideOptions.useLatest, + location.hash, + location.pathname, + location.search, + setUser, + updateUser, + ], ); return ( @@ -75,13 +125,15 @@ const UserOnboarding: FunctionComponent = ({ user }) => { {step === STEPS.TWO && ( { - setUser(updatedUser); + onComplete={(ide, useLatest) => { + setIDEOptions({ ide, useLatest }); setStep(STEPS.THREE); }} /> )} {step === STEPS.THREE && } + + {!!completingError && {completingError}}
diff --git a/components/dashboard/src/workspaces/Workspaces.tsx b/components/dashboard/src/workspaces/Workspaces.tsx index 91eda460d65c08..83d46aaa19f418 100644 --- a/components/dashboard/src/workspaces/Workspaces.tsx +++ b/components/dashboard/src/workspaces/Workspaces.tsx @@ -96,8 +96,10 @@ const WorkspacesPage: FunctionComponent = () => { /> )} + {/* TODO: can remove this once newSignupFlow flag is enabled */} {isOnboardingUser && !newSignupFlow && } + {/* TODO: can remove this once newSignupFlow flag is enabled */} {!isOnboardingUser && !newSignupFlow && } {!isLoading && diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 6d866419dc0aab..0bfbd3fb787d5e 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -264,6 +264,8 @@ export interface ProfileDetails { signupGoals?: string; // freeform entry for signup goals (when signupGoals is "other") signupGoalsOther?: string; + // Set after a user completes the onboarding flow + onboardedTimestamp?: string; } export interface EmailNotificationSettings { From 273525e39f4798aa18c4464910e8dc792300d195 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 23 Feb 2023 00:57:26 +0000 Subject: [PATCH 15/29] make label optional --- .../src/components/forms/InputField.tsx | 22 ++++++++++--------- .../src/components/forms/SelectInputField.tsx | 2 +- .../src/components/forms/TextInputField.tsx | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/components/dashboard/src/components/forms/InputField.tsx b/components/dashboard/src/components/forms/InputField.tsx index 7725591cbc4eee..71948045ab9891 100644 --- a/components/dashboard/src/components/forms/InputField.tsx +++ b/components/dashboard/src/components/forms/InputField.tsx @@ -8,7 +8,7 @@ import classNames from "classnames"; import { FunctionComponent, memo, ReactNode } from "react"; type Props = { - label: ReactNode; + label?: ReactNode; id?: string; hint?: ReactNode; error?: ReactNode; @@ -18,15 +18,17 @@ type Props = { export const InputField: FunctionComponent = memo(({ label, id, hint, error, className, children }) => { return (
- + {label && ( + + )} {children} {error && {error}} {hint && {hint}} diff --git a/components/dashboard/src/components/forms/SelectInputField.tsx b/components/dashboard/src/components/forms/SelectInputField.tsx index 6f476b60d4c833..8463f20457a4a8 100644 --- a/components/dashboard/src/components/forms/SelectInputField.tsx +++ b/components/dashboard/src/components/forms/SelectInputField.tsx @@ -10,7 +10,7 @@ import { useId } from "../../hooks/useId"; import { InputField } from "./InputField"; type Props = { - label: ReactNode; + label?: ReactNode; value: string; id?: string; hint?: ReactNode; diff --git a/components/dashboard/src/components/forms/TextInputField.tsx b/components/dashboard/src/components/forms/TextInputField.tsx index 24c3816b4dda5d..2d7f0d80cc297f 100644 --- a/components/dashboard/src/components/forms/TextInputField.tsx +++ b/components/dashboard/src/components/forms/TextInputField.tsx @@ -13,7 +13,7 @@ type TextInputFieldTypes = "text" | "password" | "email" | "url"; type Props = { type?: TextInputFieldTypes; - label: ReactNode; + label?: ReactNode; value: string; id?: string; hint?: ReactNode; From 189542b9fdf90c3e0f02966904b0dec421b93eee Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 23 Feb 2023 00:57:54 +0000 Subject: [PATCH 16/29] change signup goals to an array --- .../src/onboarding/OnboardingStep.tsx | 2 +- .../dashboard/src/onboarding/StepOrgInfo.tsx | 154 +++++++++--------- .../dashboard/src/onboarding/job-roles.ts | 24 +++ .../dashboard/src/onboarding/signup-goals.ts | 26 +++ components/gitpod-protocol/src/protocol.ts | 2 +- 5 files changed, 131 insertions(+), 77 deletions(-) create mode 100644 components/dashboard/src/onboarding/job-roles.ts create mode 100644 components/dashboard/src/onboarding/signup-goals.ts diff --git a/components/dashboard/src/onboarding/OnboardingStep.tsx b/components/dashboard/src/onboarding/OnboardingStep.tsx index b3780da4e168d8..8010bc8109fa40 100644 --- a/components/dashboard/src/onboarding/OnboardingStep.tsx +++ b/components/dashboard/src/onboarding/OnboardingStep.tsx @@ -38,7 +38,7 @@ export const OnboardingStep: FC = ({

{title}

{subtitle}

- + {/* Form contents provided as children */} {children} diff --git a/components/dashboard/src/onboarding/StepOrgInfo.tsx b/components/dashboard/src/onboarding/StepOrgInfo.tsx index 4046a6ec0868c3..3c1e66865dc767 100644 --- a/components/dashboard/src/onboarding/StepOrgInfo.tsx +++ b/components/dashboard/src/onboarding/StepOrgInfo.tsx @@ -6,11 +6,14 @@ import { User } from "@gitpod/gitpod-protocol"; import { FC, useCallback, useMemo, useState } from "react"; +import { InputField } from "../components/forms/InputField"; import { SelectInputField } from "../components/forms/SelectInputField"; import { TextInputField } from "../components/forms/TextInputField"; import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutation"; import { useOnBlurError } from "../hooks/use-onblur-error"; +import { getJobRoleOptions, JOB_ROLE_OTHER } from "./job-roles"; import { OnboardingStep } from "./OnboardingStep"; +import { getSignupGoalsOptions, SIGNUP_GOALS_OTHER } from "./signup-goals"; type Props = { user: User; @@ -23,10 +26,35 @@ export const StepOrgInfo: FC = ({ user, onComplete }) => { const [jobRole, setJobRole] = useState(user.additionalData?.profile?.jobRole ?? ""); const [jobRoleOther, setJobRoleOther] = useState(user.additionalData?.profile?.jobRoleOther ?? ""); - const [signupGoals, setSignupGoals] = useState(user.additionalData?.profile?.signupGoals ?? ""); + const [signupGoals, setSignupGoals] = useState(user.additionalData?.profile?.signupGoals ?? []); const [signupGoalsOther, setSignupGoalsOther] = useState(user.additionalData?.profile?.signupGoalsOther ?? ""); const [companyWebsite, setCompanyWebsite] = useState(user.additionalData?.profile?.companyWebsite ?? ""); + const addSignupGoal = useCallback( + (goal: string) => { + if (!signupGoals.includes(goal)) { + setSignupGoals([...signupGoals, goal]); + } + }, + [signupGoals], + ); + + const removeSignupGoal = useCallback( + (goal: string) => { + if (signupGoals.includes(goal)) { + const idx = signupGoals.indexOf(goal); + const newGoals = [...signupGoals]; + newGoals.splice(idx, 1); + setSignupGoals(newGoals); + } + // clear out freeform other if removing option + if (goal === SIGNUP_GOALS_OTHER) { + setSignupGoalsOther(""); + } + }, + [signupGoals], + ); + const handleSubmit = useCallback(async () => { const additionalData = user.additionalData || {}; const profile = additionalData.profile || {}; @@ -38,7 +66,7 @@ export const StepOrgInfo: FC = ({ user, onComplete }) => { ...profile, jobRole, jobRoleOther, - signupGoals, + signupGoals: signupGoals.filter(Boolean), signupGoalsOther, companyWebsite, }, @@ -63,17 +91,7 @@ export const StepOrgInfo: FC = ({ user, onComplete }) => { ]); const jobRoleError = useOnBlurError("Please select one", !!jobRole); - const jobRoleOtherError = useOnBlurError( - "Please provide a description", - (jobRole === "other" && !!jobRoleOther) || jobRole !== "other", - ); - const goalsError = useOnBlurError("Please select one", !!signupGoals); - const goalsOtherError = useOnBlurError( - "Please provide a description", - (signupGoals === "other" && !!signupGoalsOther) || signupGoals !== "other", - ); - - const isValid = [jobRoleError, jobRoleOtherError, goalsError, goalsOtherError].every((e) => e.isValid); + const isValid = jobRoleError.isValid && signupGoals.length > 0; return ( = ({ user, onComplete }) => { { + if (val !== "other") { + setJobRoleOther(""); + } + setJobRole(val); + }} + hint={ + jobRole !== JOB_ROLE_OTHER + ? "Please select the role that best describes the type of work you'll use Gitpod for" + : "" + } error={jobRoleError.message} onBlur={jobRoleError.onBlur} > @@ -99,78 +126,55 @@ export const StepOrgInfo: FC = ({ user, onComplete }) => { ))} - {jobRole === "other" && ( + {jobRole === JOB_ROLE_OTHER && ( )} - - {signupGoalsOptions.map((o) => ( - - ))} - - {signupGoals === "other" && ( - - )} - - - ); -}; -// TODO: pull values into constants -const getJobRoleOptions = () => { - return [ - { label: "Please select one", value: "" }, - { label: "Backend", value: "backend" }, - { label: "Frontend", value: "frontend" }, - { label: "Fullstack", value: "fullstack" }, - { label: "Data / analytics", value: "data / analytics" }, - { label: "DevOps / devX / platform", value: "devops / devx / platform" }, - { label: "Product / design", value: "product / design" }, - { label: "Customer engineering", value: "customer engineering" }, - { label: "DevRel", value: "devrel" }, - { label: "Open source", value: "open source" }, - { label: "Academia / student", value: "academia / student" }, - { label: "Other / prefer not to say: please specify", value: "other" }, - ]; -}; + +
+ {signupGoalsOptions.map((o) => ( +
+ { + if (e.target.checked) { + addSignupGoal(o.value); + } else { + removeSignupGoal(o.value); + } + }} + /> + +
+ ))} +
-const getSignupGoalsOptions = () => { - return [ - { label: "Please select one", value: "" }, - { - label: "Replace remote/containerized development (VDI, VM based, Docker Desktop..)", - value: "replace remote/containerized development (vdi, vm based, docker desktop..)", - }, - { label: "More powerful dev resources", value: "more powerful dev resources" }, - { label: "Just exploring CDEs", value: "just exploring cdes" }, - { label: "Faster onboarding", value: "faster onboarding" }, - { label: "More secure dev process", value: "more secure dev process" }, - { label: "Dev efficiency & collaboration", value: "dev efficiency & collaboration" }, - { label: "Contribute to open source", value: "contribute to open source" }, - { label: "Work from any device (iPad,…)", value: "work from any device (ipad,…)" }, - { label: "Solve “works on my machine issue”", value: "solve “works on my machine issue”" }, - { label: "Work on hobby projects", value: "work on hobby projects" }, - { label: "other / prefer not to say: please specify", value: "other" }, - ]; + {signupGoals.includes(SIGNUP_GOALS_OTHER) && ( + + )} + + ); }; diff --git a/components/dashboard/src/onboarding/job-roles.ts b/components/dashboard/src/onboarding/job-roles.ts new file mode 100644 index 00000000000000..3f6474775bf74f --- /dev/null +++ b/components/dashboard/src/onboarding/job-roles.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2023 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +export const JOB_ROLE_OTHER = "other"; + +export const getJobRoleOptions = () => { + return [ + { label: "Please select one", value: "" }, + { label: "Backend", value: "backend" }, + { label: "Frontend", value: "frontend" }, + { label: "Fullstack", value: "fullstack" }, + { label: "Data / analytics", value: "data_analytics" }, + { label: "DevOps / devX / platform", value: "devops_devx_platform" }, + { label: "Product / design", value: "product_design" }, + { label: "Customer engineering", value: "customer_engineering" }, + { label: "DevRel", value: "devrel" }, + { label: "Open source", value: "open_source" }, + { label: "Academia / student", value: "academia" }, + { label: "Other / prefer not to say", value: JOB_ROLE_OTHER }, + ]; +}; diff --git a/components/dashboard/src/onboarding/signup-goals.ts b/components/dashboard/src/onboarding/signup-goals.ts new file mode 100644 index 00000000000000..9bf12b382898b9 --- /dev/null +++ b/components/dashboard/src/onboarding/signup-goals.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2023 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +export const SIGNUP_GOALS_OTHER = "other"; + +export const getSignupGoalsOptions = () => { + return [ + { + label: "Replace remote/containerized development (VDI, VM based, Docker Desktop..)", + value: "replace_remote_containerized_dev", + }, + { label: "More powerful dev resources", value: "powerful_dev_resources" }, + { label: "Just exploring CDEs", value: "exploring_cdes" }, + { label: "Faster onboarding", value: "faster_onboarding" }, + { label: "More secure dev process", value: "secure_dev_process" }, + { label: "Dev efficiency & collaboration", value: "dev_efficiency_collaboration" }, + { label: "Contribute to open source", value: "open_source" }, + { label: "Work from any device (iPad,…)", value: "work_from_any_device" }, + { label: "Solve “works on my machine issue”", value: "works_on_my_machine" }, + { label: "Work on hobby projects", value: "hobby" }, + { label: "Other / prefer not to say", value: SIGNUP_GOALS_OTHER }, + ]; +}; diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 0bfbd3fb787d5e..2662b58ecd46ca 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -261,7 +261,7 @@ export interface ProfileDetails { // freeform entry for job role user works in (when jobRole is "other") jobRoleOther?: string; // what user hopes to accomplish when they signed up - signupGoals?: string; + signupGoals?: string[]; // freeform entry for signup goals (when signupGoals is "other") signupGoalsOther?: string; // Set after a user completes the onboarding flow From f2d868e7eaa111bf6308cb7c4acc085e7c3775e7 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 23 Feb 2023 01:03:49 +0000 Subject: [PATCH 17/29] change to company --- components/dashboard/src/onboarding/StepOrgInfo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/dashboard/src/onboarding/StepOrgInfo.tsx b/components/dashboard/src/onboarding/StepOrgInfo.tsx index 3c1e66865dc767..16d01c84de42cc 100644 --- a/components/dashboard/src/onboarding/StepOrgInfo.tsx +++ b/components/dashboard/src/onboarding/StepOrgInfo.tsx @@ -141,7 +141,7 @@ export const StepOrgInfo: FC = ({ user, onComplete }) => { Date: Thu, 23 Feb 2023 18:14:04 +0000 Subject: [PATCH 18/29] adjust spacing/layout --- components/dashboard/src/components/ThemeSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/dashboard/src/components/ThemeSelector.tsx b/components/dashboard/src/components/ThemeSelector.tsx index f4abc2d26f567b..d1f0cae1e64cef 100644 --- a/components/dashboard/src/components/ThemeSelector.tsx +++ b/components/dashboard/src/components/ThemeSelector.tsx @@ -39,7 +39,7 @@ export const ThemeSelector: FC = ({ className }) => {

Theme

Early bird or night owl? Choose your side.

-
+
Date: Thu, 23 Feb 2023 18:14:22 +0000 Subject: [PATCH 19/29] Add additional comments for context --- .../dashboard/src/data/organizations/org-members-query.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/dashboard/src/data/organizations/org-members-query.ts b/components/dashboard/src/data/organizations/org-members-query.ts index 179a9ff7ba0c9e..df1ba801980c3f 100644 --- a/components/dashboard/src/data/organizations/org-members-query.ts +++ b/components/dashboard/src/data/organizations/org-members-query.ts @@ -26,7 +26,11 @@ export const useOrgMembers = () => { return publicApiTeamMembersToProtocol(resp.team?.members || []); }, - // If no current org is set, disable query + /** + * If no current org is set, disable query + * This is to prevent making a request to the API when there is no organization selected. + * This happens if the user has their personal account selected, or when first loggin in + */ enabled: !!organization, }); }; From a6764535aa9d7a6ec38cd644adcd674015a88349 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 23 Feb 2023 18:16:40 +0000 Subject: [PATCH 20/29] rename isLoading to isSaving for clarity --- .../dashboard/src/onboarding/OnboardingStep.tsx | 11 +++++++---- components/dashboard/src/onboarding/StepOrgInfo.tsx | 2 +- components/dashboard/src/onboarding/StepUserInfo.tsx | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/components/dashboard/src/onboarding/OnboardingStep.tsx b/components/dashboard/src/onboarding/OnboardingStep.tsx index 8010bc8109fa40..3c888a77aa1963 100644 --- a/components/dashboard/src/onboarding/OnboardingStep.tsx +++ b/components/dashboard/src/onboarding/OnboardingStep.tsx @@ -11,7 +11,7 @@ type Props = { title: string; subtitle: string; isValid: boolean; - isLoading?: boolean; + isSaving?: boolean; error?: string; onSubmit(): void; }; @@ -19,7 +19,7 @@ export const OnboardingStep: FC = ({ title, subtitle, isValid, - isLoading = false, + isSaving = false, error, children, onSubmit, @@ -27,10 +27,13 @@ export const OnboardingStep: FC = ({ const handleSubmit = useCallback( async (e: FormEvent) => { e.preventDefault(); + if (isSaving) { + return; + } onSubmit(); }, - [onSubmit], + [isSaving, onSubmit], ); return ( @@ -45,7 +48,7 @@ export const OnboardingStep: FC = ({ {error && {error}}
-
diff --git a/components/dashboard/src/onboarding/StepOrgInfo.tsx b/components/dashboard/src/onboarding/StepOrgInfo.tsx index 16d01c84de42cc..65d02f007dd7fa 100644 --- a/components/dashboard/src/onboarding/StepOrgInfo.tsx +++ b/components/dashboard/src/onboarding/StepOrgInfo.tsx @@ -99,7 +99,7 @@ export const StepOrgInfo: FC = ({ user, onComplete }) => { subtitle="Let us know what brought you here." error={updateUser.isError ? "There was a problem saving your answers" : ""} isValid={isValid} - isLoading={updateUser.isLoading} + isSaving={updateUser.isLoading} onSubmit={handleSubmit} > = ({ user, onComplete }) => { subtitle="Fill in the name and email you want to use to author commits." error={updateUser.isError ? "There was a problem updating your profile" : undefined} isValid={isValid} - isLoading={updateUser.isLoading} + isSaving={updateUser.isLoading} onSubmit={handleSubmit} >
From cae16ddbc2afb16515231b58c94020cec7075980 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 23 Feb 2023 18:16:50 +0000 Subject: [PATCH 21/29] fix typo --- components/dashboard/src/onboarding/StepPersonalize.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/dashboard/src/onboarding/StepPersonalize.tsx b/components/dashboard/src/onboarding/StepPersonalize.tsx index d276606a6b3d42..59e2e4359dee80 100644 --- a/components/dashboard/src/onboarding/StepPersonalize.tsx +++ b/components/dashboard/src/onboarding/StepPersonalize.tsx @@ -8,7 +8,6 @@ import { User } from "@gitpod/gitpod-protocol"; import { FC, useCallback, useState } from "react"; import SelectIDEComponent from "../components/SelectIDEComponent"; import { ThemeSelector } from "../components/ThemeSelector"; -import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutation"; import { OnboardingStep } from "./OnboardingStep"; type Props = { @@ -29,7 +28,7 @@ export const StepPersonalize: FC = ({ user, onComplete }) => { return ( From 1cc94828ef63a9d754b63cae258af42bca795200 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 23 Feb 2023 18:27:06 +0000 Subject: [PATCH 22/29] set type on button, don't submit if invalid --- components/dashboard/src/onboarding/OnboardingStep.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/dashboard/src/onboarding/OnboardingStep.tsx b/components/dashboard/src/onboarding/OnboardingStep.tsx index 3c888a77aa1963..327301f10a602e 100644 --- a/components/dashboard/src/onboarding/OnboardingStep.tsx +++ b/components/dashboard/src/onboarding/OnboardingStep.tsx @@ -27,13 +27,13 @@ export const OnboardingStep: FC = ({ const handleSubmit = useCallback( async (e: FormEvent) => { e.preventDefault(); - if (isSaving) { + if (isSaving || !isValid) { return; } onSubmit(); }, - [isSaving, onSubmit], + [isSaving, isValid, onSubmit], ); return ( @@ -48,7 +48,7 @@ export const OnboardingStep: FC = ({ {error && {error}}
-
From 3cb0b60c3f9e7f71de71c31a133e8179cc554b49 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 23 Feb 2023 18:27:32 +0000 Subject: [PATCH 23/29] adding required on required fields --- components/dashboard/src/onboarding/StepUserInfo.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/dashboard/src/onboarding/StepUserInfo.tsx b/components/dashboard/src/onboarding/StepUserInfo.tsx index 4fce3804dadff6..3356eea48d477f 100644 --- a/components/dashboard/src/onboarding/StepUserInfo.tsx +++ b/components/dashboard/src/onboarding/StepUserInfo.tsx @@ -72,6 +72,7 @@ export const StepUserInfo: FC = ({ user, onComplete }) => { error={firstNameError.message} onBlur={firstNameError.onBlur} onChange={setFirstName} + required /> = ({ user, onComplete }) => { error={lastNameError.message} onBlur={lastNameError.onBlur} onChange={setLastName} + required />
@@ -92,6 +94,7 @@ export const StepUserInfo: FC = ({ user, onComplete }) => { error={emailError.message} onBlur={emailError.onBlur} onChange={setEmailAddress} + required /> ); From c019664e3b311608e6007a5b2985d898fe0088cc Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 23 Feb 2023 18:30:16 +0000 Subject: [PATCH 24/29] fix typos --- components/dashboard/src/onboarding/job-roles.ts | 2 +- components/dashboard/src/onboarding/signup-goals.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/dashboard/src/onboarding/job-roles.ts b/components/dashboard/src/onboarding/job-roles.ts index 3f6474775bf74f..77353155d88452 100644 --- a/components/dashboard/src/onboarding/job-roles.ts +++ b/components/dashboard/src/onboarding/job-roles.ts @@ -11,7 +11,7 @@ export const getJobRoleOptions = () => { { label: "Please select one", value: "" }, { label: "Backend", value: "backend" }, { label: "Frontend", value: "frontend" }, - { label: "Fullstack", value: "fullstack" }, + { label: "Full Stack", value: "fullstack" }, { label: "Data / analytics", value: "data_analytics" }, { label: "DevOps / devX / platform", value: "devops_devx_platform" }, { label: "Product / design", value: "product_design" }, diff --git a/components/dashboard/src/onboarding/signup-goals.ts b/components/dashboard/src/onboarding/signup-goals.ts index 9bf12b382898b9..f1cdcfac01236c 100644 --- a/components/dashboard/src/onboarding/signup-goals.ts +++ b/components/dashboard/src/onboarding/signup-goals.ts @@ -19,7 +19,7 @@ export const getSignupGoalsOptions = () => { { label: "Dev efficiency & collaboration", value: "dev_efficiency_collaboration" }, { label: "Contribute to open source", value: "open_source" }, { label: "Work from any device (iPad,…)", value: "work_from_any_device" }, - { label: "Solve “works on my machine issue”", value: "works_on_my_machine" }, + { label: `Solve "works on my machine issue"`, value: "works_on_my_machine" }, { label: "Work on hobby projects", value: "hobby" }, { label: "Other / prefer not to say", value: SIGNUP_GOALS_OTHER }, ]; From 9f526a824536752120207db15c4f7443d9d294ad Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 23 Feb 2023 19:04:39 +0000 Subject: [PATCH 25/29] Adjusting titles, styles and adding avatar --- .../dashboard/src/onboarding/OnboardingStep.tsx | 7 ++++--- .../dashboard/src/onboarding/StepPersonalize.tsx | 4 ++-- components/dashboard/src/onboarding/StepUserInfo.tsx | 12 +++++++++--- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/components/dashboard/src/onboarding/OnboardingStep.tsx b/components/dashboard/src/onboarding/OnboardingStep.tsx index 327301f10a602e..72aa75e235841c 100644 --- a/components/dashboard/src/onboarding/OnboardingStep.tsx +++ b/components/dashboard/src/onboarding/OnboardingStep.tsx @@ -38,10 +38,11 @@ export const OnboardingStep: FC = ({ return (
-

{title}

-

{subtitle}

+ {/* TODO: Fix our base heading styles so we don't have to override */} +

{title}

+

{subtitle}

- + {/* Form contents provided as children */} {children} diff --git a/components/dashboard/src/onboarding/StepPersonalize.tsx b/components/dashboard/src/onboarding/StepPersonalize.tsx index 59e2e4359dee80..8eef8c36942299 100644 --- a/components/dashboard/src/onboarding/StepPersonalize.tsx +++ b/components/dashboard/src/onboarding/StepPersonalize.tsx @@ -27,8 +27,8 @@ export const StepPersonalize: FC = ({ user, onComplete }) => { return ( diff --git a/components/dashboard/src/onboarding/StepUserInfo.tsx b/components/dashboard/src/onboarding/StepUserInfo.tsx index 3356eea48d477f..6b7db25bf32758 100644 --- a/components/dashboard/src/onboarding/StepUserInfo.tsx +++ b/components/dashboard/src/onboarding/StepUserInfo.tsx @@ -57,13 +57,19 @@ export const StepUserInfo: FC = ({ user, onComplete }) => { return ( + {user.avatarUrl && ( +
+ {user.fullName +
+ )} +
= ({ user, onComplete }) => { value={emailAddress} label="Email" type="email" - hint="We suggest using your work email" + hint="We recommend using a work email address." error={emailError.message} onBlur={emailError.onBlur} onChange={setEmailAddress} From 3be00abb689a5ffa0465a040b09fe9a0fd256bbe Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 23 Feb 2023 19:17:17 +0000 Subject: [PATCH 26/29] account for new profile fields for tracking --- components/gitpod-protocol/src/protocol.ts | 24 +++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 2662b58ecd46ca..5cc491152d2370 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -153,12 +153,20 @@ export namespace User { user.additionalData.ideSettings = newIDESettings; } + // TODO: make it more explicit that these field names are relied for our tracking purposes + // and decouple frontend from relying on them - instead use user.additionalData.profile object directly in FE export function getProfile(user: User): Profile { return { name: User.getName(user!) || "", email: User.getPrimaryEmail(user!) || "", company: user?.additionalData?.profile?.companyName, avatarURL: user?.avatarUrl, + companyWebsite: user?.additionalData?.profile?.companyName, + jobRole: user?.additionalData?.profile?.jobRole, + jobRoleOther: user?.additionalData?.profile?.jobRoleOther, + signupGoals: user?.additionalData?.profile?.signupGoals, + signupGoalsOther: user?.additionalData?.profile?.signupGoalsOther, + onboardedTimestamp: user?.additionalData?.profile?.onboardedTimestamp, }; } @@ -190,12 +198,20 @@ export namespace User { return AttributionId.create(user); } + // TODO: refactor where this is referenced so it's more clearly tied to just analytics-tracking + // Let other places rely on the ProfileDetails type since that's what we store // The actual Profile of a User export interface Profile { name: string; email: string; company?: string; avatarURL?: string; + companyWebsite?: string; + jobRole?: string; + jobRoleOther?: string; + signupGoals?: string[]; + signupGoalsOther?: string; + onboardedTimestamp?: string; } export namespace Profile { export function hasChanges(before: Profile, after: Profile) { @@ -203,7 +219,13 @@ export namespace User { before.name !== after.name || before.email !== after.email || before.company !== after.company || - before.avatarURL !== after.avatarURL + before.avatarURL !== after.avatarURL || + before.companyWebsite !== after.companyWebsite || + before.jobRole !== after.jobRole || + before.jobRoleOther !== after.jobRoleOther || + before.signupGoals !== after.signupGoals || + before.signupGoalsOther !== after.signupGoalsOther || + before.onboardedTimestamp !== after.onboardedTimestamp ); } } From 6a9e7633732f05b2c95df71a77619cec3c9e4d4e Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 23 Feb 2023 20:05:19 +0000 Subject: [PATCH 27/29] remove check for signupGoals --- components/gitpod-protocol/src/protocol.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 5cc491152d2370..c1ee74a7d68e90 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -223,7 +223,7 @@ export namespace User { before.companyWebsite !== after.companyWebsite || before.jobRole !== after.jobRole || before.jobRoleOther !== after.jobRoleOther || - before.signupGoals !== after.signupGoals || + // not checking signupGoals atm as it's an array - need to check deep equality before.signupGoalsOther !== after.signupGoalsOther || before.onboardedTimestamp !== after.onboardedTimestamp ); From f49cd2f1fbd10e8327a99b5c3700b4fafb23e4e1 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 23 Feb 2023 21:02:46 +0000 Subject: [PATCH 28/29] updating options --- .../dashboard/src/onboarding/job-roles.ts | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/components/dashboard/src/onboarding/job-roles.ts b/components/dashboard/src/onboarding/job-roles.ts index 77353155d88452..d3378b3494b5cc 100644 --- a/components/dashboard/src/onboarding/job-roles.ts +++ b/components/dashboard/src/onboarding/job-roles.ts @@ -8,17 +8,14 @@ export const JOB_ROLE_OTHER = "other"; export const getJobRoleOptions = () => { return [ - { label: "Please select one", value: "" }, - { label: "Backend", value: "backend" }, - { label: "Frontend", value: "frontend" }, - { label: "Full Stack", value: "fullstack" }, - { label: "Data / analytics", value: "data_analytics" }, - { label: "DevOps / devX / platform", value: "devops_devx_platform" }, - { label: "Product / design", value: "product_design" }, - { label: "Customer engineering", value: "customer_engineering" }, - { label: "DevRel", value: "devrel" }, - { label: "Open source", value: "open_source" }, - { label: "Academia / student", value: "academia" }, - { label: "Other / prefer not to say", value: JOB_ROLE_OTHER }, + { value: "", label: "Please select one" }, + { value: "software-eng", label: "Software Engineer" }, + { value: "data", label: "Data / Analytics" }, + { value: "academics", label: "Academic (Student, Researcher)" }, + { value: "enabling", label: "Enabling team (Platform, Developer Experience)" }, + { value: "team-lead", label: "Team / Function Lead" }, + { value: "devrel", label: "DevRel" }, + { value: "product-design", label: "Product (PM, Designer)" }, + { value: JOB_ROLE_OTHER, label: "Other - please specify / prefer not to say" }, ]; }; From 452874b21508ed8586d2c54afe07ade772419538 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 23 Feb 2023 21:41:20 +0000 Subject: [PATCH 29/29] Adding exploration reasons question --- .../dashboard/src/onboarding/StepOrgInfo.tsx | 63 ++++++++++++++++++- .../src/onboarding/exploration-reasons.ts | 16 +++++ .../dashboard/src/onboarding/signup-goals.ts | 19 ++---- components/gitpod-protocol/src/protocol.ts | 6 +- 4 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 components/dashboard/src/onboarding/exploration-reasons.ts diff --git a/components/dashboard/src/onboarding/StepOrgInfo.tsx b/components/dashboard/src/onboarding/StepOrgInfo.tsx index 65d02f007dd7fa..d6fc0f49c2d543 100644 --- a/components/dashboard/src/onboarding/StepOrgInfo.tsx +++ b/components/dashboard/src/onboarding/StepOrgInfo.tsx @@ -11,6 +11,7 @@ import { SelectInputField } from "../components/forms/SelectInputField"; import { TextInputField } from "../components/forms/TextInputField"; import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutation"; import { useOnBlurError } from "../hooks/use-onblur-error"; +import { getExplorationReasons } from "./exploration-reasons"; import { getJobRoleOptions, JOB_ROLE_OTHER } from "./job-roles"; import { OnboardingStep } from "./OnboardingStep"; import { getSignupGoalsOptions, SIGNUP_GOALS_OTHER } from "./signup-goals"; @@ -22,10 +23,14 @@ type Props = { export const StepOrgInfo: FC = ({ user, onComplete }) => { const updateUser = useUpdateCurrentUserMutation(); const jobRoleOptions = useMemo(getJobRoleOptions, []); + const explorationReasonsOptions = useMemo(getExplorationReasons, []); const signupGoalsOptions = useMemo(getSignupGoalsOptions, []); const [jobRole, setJobRole] = useState(user.additionalData?.profile?.jobRole ?? ""); const [jobRoleOther, setJobRoleOther] = useState(user.additionalData?.profile?.jobRoleOther ?? ""); + const [explorationReasons, setExplorationReasons] = useState( + user.additionalData?.profile?.explorationReasons ?? [], + ); const [signupGoals, setSignupGoals] = useState(user.additionalData?.profile?.signupGoals ?? []); const [signupGoalsOther, setSignupGoalsOther] = useState(user.additionalData?.profile?.signupGoalsOther ?? ""); const [companyWebsite, setCompanyWebsite] = useState(user.additionalData?.profile?.companyWebsite ?? ""); @@ -55,10 +60,37 @@ export const StepOrgInfo: FC = ({ user, onComplete }) => { [signupGoals], ); + const addExplorationReason = useCallback( + (reason: string) => { + if (!explorationReasons.includes(reason)) { + setExplorationReasons([...explorationReasons, reason]); + } + }, + [explorationReasons], + ); + + const removeExplorationReason = useCallback( + (reason: string) => { + if (explorationReasons.includes(reason)) { + const idx = explorationReasons.indexOf(reason); + const newReasons = [...explorationReasons]; + newReasons.splice(idx, 1); + setExplorationReasons(newReasons); + } + }, + [explorationReasons], + ); + const handleSubmit = useCallback(async () => { const additionalData = user.additionalData || {}; const profile = additionalData.profile || {}; + // Filter out any values not present in options + const filteredReasons = explorationReasons.filter((val) => + explorationReasonsOptions.find((o) => o.value === val), + ); + const filteredGoals = signupGoals.filter((val) => signupGoalsOptions.find((o) => o.value === val)); + const updates = { additionalData: { ...additionalData, @@ -66,7 +98,8 @@ export const StepOrgInfo: FC = ({ user, onComplete }) => { ...profile, jobRole, jobRoleOther, - signupGoals: signupGoals.filter(Boolean), + explorationReasons: filteredReasons, + signupGoals: filteredGoals, signupGoalsOther, companyWebsite, }, @@ -81,10 +114,13 @@ export const StepOrgInfo: FC = ({ user, onComplete }) => { } }, [ companyWebsite, + explorationReasons, + explorationReasonsOptions, jobRole, jobRoleOther, onComplete, signupGoals, + signupGoalsOptions, signupGoalsOther, updateUser, user.additionalData, @@ -147,6 +183,31 @@ export const StepOrgInfo: FC = ({ user, onComplete }) => { onChange={setCompanyWebsite} /> + +
+ {explorationReasonsOptions.map((o) => ( +
+ { + if (e.target.checked) { + addExplorationReason(o.value); + } else { + removeExplorationReason(o.value); + } + }} + /> + +
+ ))} +
+
{signupGoalsOptions.map((o) => ( diff --git a/components/dashboard/src/onboarding/exploration-reasons.ts b/components/dashboard/src/onboarding/exploration-reasons.ts new file mode 100644 index 00000000000000..ed315edcf8c19d --- /dev/null +++ b/components/dashboard/src/onboarding/exploration-reasons.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2023 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +export const getExplorationReasons = () => { + return [ + { value: "explore-professional", label: "For work" }, + { value: "explore-personal", label: "For personal projects, or open-source" }, + { + value: "replace-remote-dev", + label: "To replace remote/containerized development (VDI, VM based, Docker Desktop,...)", + }, + ]; +}; diff --git a/components/dashboard/src/onboarding/signup-goals.ts b/components/dashboard/src/onboarding/signup-goals.ts index f1cdcfac01236c..2904ec5f35f5d5 100644 --- a/components/dashboard/src/onboarding/signup-goals.ts +++ b/components/dashboard/src/onboarding/signup-goals.ts @@ -8,19 +8,10 @@ export const SIGNUP_GOALS_OTHER = "other"; export const getSignupGoalsOptions = () => { return [ - { - label: "Replace remote/containerized development (VDI, VM based, Docker Desktop..)", - value: "replace_remote_containerized_dev", - }, - { label: "More powerful dev resources", value: "powerful_dev_resources" }, - { label: "Just exploring CDEs", value: "exploring_cdes" }, - { label: "Faster onboarding", value: "faster_onboarding" }, - { label: "More secure dev process", value: "secure_dev_process" }, - { label: "Dev efficiency & collaboration", value: "dev_efficiency_collaboration" }, - { label: "Contribute to open source", value: "open_source" }, - { label: "Work from any device (iPad,…)", value: "work_from_any_device" }, - { label: `Solve "works on my machine issue"`, value: "works_on_my_machine" }, - { label: "Work on hobby projects", value: "hobby" }, - { label: "Other / prefer not to say", value: SIGNUP_GOALS_OTHER }, + { value: "efficiency-collab", label: "Dev efficiency & collaboration" }, + { value: "onboarding", label: "Faster onboarding" }, + { value: "powerful-resources", label: "More powerful dev resources" }, + { value: "security", label: "More secure dev process" }, + { value: SIGNUP_GOALS_OTHER, label: "Other - please specify / prefer not to say" }, ]; }; diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index c1ee74a7d68e90..665b018fb9c886 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -164,6 +164,7 @@ export namespace User { companyWebsite: user?.additionalData?.profile?.companyName, jobRole: user?.additionalData?.profile?.jobRole, jobRoleOther: user?.additionalData?.profile?.jobRoleOther, + explorationReasons: user?.additionalData?.profile?.explorationReasons, signupGoals: user?.additionalData?.profile?.signupGoals, signupGoalsOther: user?.additionalData?.profile?.signupGoalsOther, onboardedTimestamp: user?.additionalData?.profile?.onboardedTimestamp, @@ -209,6 +210,7 @@ export namespace User { companyWebsite?: string; jobRole?: string; jobRoleOther?: string; + explorationReasons?: string[]; signupGoals?: string[]; signupGoalsOther?: string; onboardedTimestamp?: string; @@ -223,7 +225,7 @@ export namespace User { before.companyWebsite !== after.companyWebsite || before.jobRole !== after.jobRole || before.jobRoleOther !== after.jobRoleOther || - // not checking signupGoals atm as it's an array - need to check deep equality + // not checking explorationReasons or signupGoals atm as it's an array - need to check deep equality before.signupGoalsOther !== after.signupGoalsOther || before.onboardedTimestamp !== after.onboardedTimestamp ); @@ -282,6 +284,8 @@ export interface ProfileDetails { jobRole?: string; // freeform entry for job role user works in (when jobRole is "other") jobRoleOther?: string; + // Reasons user is exploring Gitpod when they signed up + explorationReasons?: string[]; // what user hopes to accomplish when they signed up signupGoals?: string[]; // freeform entry for signup goals (when signupGoals is "other")