From 31b665a43b285ef51e41723a0aa8a99bf8f06da0 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Fri, 6 Mar 2026 14:58:58 +0000 Subject: [PATCH 1/8] Adds a new isLoading prop that shows a spinner in the middle of the button --- .../app/components/primitives/Buttons.tsx | 133 +++++++++++------- 1 file changed, 82 insertions(+), 51 deletions(-) diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index 5c52a5d95d1..8ba196b5dc2 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -1,10 +1,18 @@ import { Link, type LinkProps, NavLink, type NavLinkProps } from "@remix-run/react"; -import React, { forwardRef, type ReactNode, useImperativeHandle, useRef } from "react"; +import React, { + forwardRef, + type ReactNode, + useEffect, + useImperativeHandle, + useRef, + useState, +} from "react"; import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys"; import { cn } from "~/utils/cn"; import { ShortcutKey } from "./ShortcutKey"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip"; import { Icon, type RenderIcon } from "./Icon"; +import { Spinner } from "./Spinner"; const sizes = { small: { @@ -180,6 +188,7 @@ export type ButtonContentPropsType = { tooltip?: ReactNode; iconSpacing?: string; hideShortcutKey?: boolean; + isLoading?: boolean; }; export function ButtonContent(props: ButtonContentPropsType) { @@ -196,7 +205,19 @@ export function ButtonContent(props: ButtonContentPropsType) { tooltip, iconSpacing, hideShortcutKey, + isLoading, } = props; + + const [showSpinner, setShowSpinner] = useState(false); + useEffect(() => { + if (!isLoading) { + setShowSpinner(false); + return; + } + const timer = setTimeout(() => setShowSpinner(true), 200); + return () => clearTimeout(timer); + }, [isLoading]); + const variation = allVariants.variant[props.variant]; const btnClassName = cn(allVariants.$all, variation.button); @@ -217,56 +238,64 @@ export function ButtonContent(props: ButtonContentPropsType) { const buttonContent = (
-
- {LeadingIcon && ( - - )} +
+
+ {LeadingIcon && ( + + )} - {text && - (typeof text === "string" ? ( - - {text} - - ) : ( - <>{text} - ))} - - {shortcut && - !tooltip && - props.shortcutPosition === "before-trailing-icon" && - renderShortcutKey()} - - {TrailingIcon && ( - - )} + {text && + (typeof text === "string" ? ( + + {text} + + ) : ( + <>{text} + ))} + + {shortcut && + !tooltip && + props.shortcutPosition === "before-trailing-icon" && + renderShortcutKey()} - {shortcut && - !tooltip && - (!props.shortcutPosition || props.shortcutPosition === "after-trailing-icon") && - renderShortcutKey()} + {TrailingIcon && ( + + )} + + {shortcut && + !tooltip && + (!props.shortcutPosition || props.shortcutPosition === "after-trailing-icon") && + renderShortcutKey()} +
+ {showSpinner && ( + + + + )}
); @@ -298,6 +327,8 @@ export const Button = forwardRef( const innerRef = useRef(null); useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement); + const isDisabled = disabled || props.isLoading; + useShortcutKeys({ shortcut: props.shortcut, action: (e) => { @@ -307,14 +338,14 @@ export const Button = forwardRef( e.stopPropagation(); } }, - disabled: disabled || !props.shortcut, + disabled: isDisabled || !props.shortcut, }); return ( } cancelButton={ From f553900f55c8d471cd1708a14b5f962d24faaa33 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Fri, 6 Mar 2026 14:59:27 +0000 Subject: [PATCH 3/8] Use the new spinner loading state button --- apps/webapp/app/routes/_app.orgs.new/route.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.new/route.tsx b/apps/webapp/app/routes/_app.orgs.new/route.tsx index b1d8fd83af3..fb072518c54 100644 --- a/apps/webapp/app/routes/_app.orgs.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.new/route.tsx @@ -220,7 +220,7 @@ export default function NewOrganizationPage() { + } From 4c997518a7d65c048a14d0385fdf003bb5a87efd Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Fri, 6 Mar 2026 14:59:40 +0000 Subject: [PATCH 4/8] Use the new spinner loading state button --- apps/webapp/app/routes/confirm-basic-details.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/confirm-basic-details.tsx b/apps/webapp/app/routes/confirm-basic-details.tsx index fad485e6d17..e9f9cd519e5 100644 --- a/apps/webapp/app/routes/confirm-basic-details.tsx +++ b/apps/webapp/app/routes/confirm-basic-details.tsx @@ -4,7 +4,7 @@ import { ArrowRightIcon, EnvelopeIcon, UserGroupIcon, UserIcon } from "@heroicon import { HandRaisedIcon } from "@heroicons/react/24/solid"; import { RadioGroup } from "@radix-ui/react-radio-group"; import { json, type ActionFunction } from "@remix-run/node"; -import { Form, useActionData } from "@remix-run/react"; +import { Form, useActionData, useNavigation } from "@remix-run/react"; import { motion } from "framer-motion"; import { forwardRef, useEffect, useState } from "react"; import { z } from "zod"; @@ -201,6 +201,8 @@ export default function Page() { const lastSubmission = useActionData(); const [enteredEmail, setEnteredEmail] = useState(user.email ?? ""); const { isManagedCloud } = useFeatures(); + const navigation = useNavigation(); + const isSubmitting = navigation.state === "submitting"; const [selectedReferralSource, setSelectedReferralSource] = useState(); const [selectedRole, setSelectedRole] = useState(""); @@ -384,7 +386,12 @@ export default function Page() { + } From 0d111a289103ffe31b410035184d44aae9edd955 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Fri, 6 Mar 2026 15:13:50 +0000 Subject: [PATCH 5/8] If an custom typed option is in the list, check the item instead --- .../components/onboarding/TechnologyPicker.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/onboarding/TechnologyPicker.tsx b/apps/webapp/app/components/onboarding/TechnologyPicker.tsx index 3d822e4b692..9c9f9a6b24a 100644 --- a/apps/webapp/app/components/onboarding/TechnologyPicker.tsx +++ b/apps/webapp/app/components/onboarding/TechnologyPicker.tsx @@ -210,11 +210,22 @@ export function TechnologyPicker({ const addCustomValue = useCallback(() => { const trimmed = otherInputValue.trim(); - if (trimmed && !customValues.includes(trimmed) && !value.includes(trimmed)) { + if (!trimmed) return; + + const matchedOption = TECHNOLOGY_OPTIONS.find( + (opt) => opt.toLowerCase() === trimmed.toLowerCase() + ); + + if (matchedOption) { + if (!value.includes(matchedOption)) { + onChange([...value, matchedOption]); + } + } else if (!customValues.includes(trimmed) && !value.includes(trimmed)) { onCustomValuesChange([...customValues, trimmed]); - setOtherInputValue(""); } - }, [otherInputValue, customValues, onCustomValuesChange, value]); + + setOtherInputValue(""); + }, [otherInputValue, customValues, onCustomValuesChange, value, onChange]); const handleOtherKeyDown = useCallback( (e: React.KeyboardEvent) => { From cd7619c148f9a42161ec89eb0b8d917f4a24f8ba Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Fri, 6 Mar 2026 15:32:18 +0000 Subject: [PATCH 6/8] Save the position of the randomized options to the DB --- .../app/routes/confirm-basic-details.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/webapp/app/routes/confirm-basic-details.tsx b/apps/webapp/app/routes/confirm-basic-details.tsx index e9f9cd519e5..a32e122132c 100644 --- a/apps/webapp/app/routes/confirm-basic-details.tsx +++ b/apps/webapp/app/routes/confirm-basic-details.tsx @@ -99,6 +99,8 @@ function createSchema( referralSourceOther: z.string().optional(), role: z.string().optional(), roleOther: z.string().optional(), + referralSourcePosition: z.coerce.number().optional(), + rolePosition: z.coerce.number().optional(), }) .refine((value) => value.email === value.confirmEmail, { message: "Emails must match", @@ -141,6 +143,9 @@ export const action: ActionFunction = async ({ request }) => { if (submission.value.referralSource) { onboardingData.referralSource = submission.value.referralSource; + if (submission.value.referralSourcePosition) { + onboardingData.referralSourcePosition = String(submission.value.referralSourcePosition); + } if (submission.value.referralSource === "Other" && submission.value.referralSourceOther) { onboardingData.referralSourceOther = submission.value.referralSourceOther; } @@ -148,6 +153,9 @@ export const action: ActionFunction = async ({ request }) => { if (submission.value.role) { onboardingData.role = submission.value.role; + if (submission.value.rolePosition) { + onboardingData.rolePosition = String(submission.value.rolePosition); + } if (submission.value.role === "Other" && submission.value.roleOther) { onboardingData.roleOther = submission.value.roleOther; } @@ -319,6 +327,15 @@ export default function Page() { name="referralSource" value={selectedReferralSource ?? ""} /> + + value={selectedRole} setValue={setSelectedRole} From e67a202aedc301435f9b29bc1daff9a55d85bb7d Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Fri, 6 Mar 2026 15:32:23 +0000 Subject: [PATCH 7/8] Save the position of the randomized options to the DB --- .../route.tsx | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx index 6000191950e..abe6da43f1d 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx @@ -169,6 +169,8 @@ const schema = z.object({ technologiesOther: z.string().optional(), goals: z.string().optional(), goalsOther: z.string().optional(), + workingOnPositions: z.string().optional(), + goalsPositions: z.string().optional(), }); export const action: ActionFunction = async ({ request, params }) => { @@ -200,10 +202,25 @@ export const action: ActionFunction = async ({ request, params }) => { } } + const numberArraySchema = z.array(z.number()); + function safeParseNumberArray(value: string | undefined): number[] | undefined { + if (!value) return undefined; + try { + const result = numberArraySchema.safeParse(JSON.parse(value)); + return result.success && result.data.length > 0 ? result.data : undefined; + } catch { + return undefined; + } + } + const onboardingData: Record = {}; const workingOn = safeParseStringArray(submission.value.workingOn); - if (workingOn) onboardingData.workingOn = workingOn; + if (workingOn) { + onboardingData.workingOn = workingOn; + const workingOnPositions = safeParseNumberArray(submission.value.workingOnPositions); + if (workingOnPositions) onboardingData.workingOnPositions = workingOnPositions; + } if (submission.value.workingOnOther) { onboardingData.workingOnOther = submission.value.workingOnOther; @@ -216,7 +233,11 @@ export const action: ActionFunction = async ({ request, params }) => { if (technologiesOther) onboardingData.technologiesOther = technologiesOther; const goals = safeParseStringArray(submission.value.goals); - if (goals) onboardingData.goals = goals; + if (goals) { + onboardingData.goals = goals; + const goalsPositions = safeParseNumberArray(submission.value.goalsPositions); + if (goalsPositions) onboardingData.goalsPositions = goalsPositions; + } if (submission.value.goalsOther) { onboardingData.goalsOther = submission.value.goalsOther; @@ -376,6 +397,13 @@ export default function Page() { + shuffledWorkingOn.indexOf(v) + 1) + )} + /> + shuffledGoals.indexOf(v) + 1) + )} + /> Date: Fri, 6 Mar 2026 16:30:11 +0000 Subject: [PATCH 8/8] Fix for devin suggestion --- apps/webapp/app/routes/confirm-basic-details.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/confirm-basic-details.tsx b/apps/webapp/app/routes/confirm-basic-details.tsx index a32e122132c..ed868186552 100644 --- a/apps/webapp/app/routes/confirm-basic-details.tsx +++ b/apps/webapp/app/routes/confirm-basic-details.tsx @@ -210,7 +210,7 @@ export default function Page() { const [enteredEmail, setEnteredEmail] = useState(user.email ?? ""); const { isManagedCloud } = useFeatures(); const navigation = useNavigation(); - const isSubmitting = navigation.state === "submitting"; + const isSubmitting = navigation.state === "submitting" || navigation.state === "loading"; const [selectedReferralSource, setSelectedReferralSource] = useState(); const [selectedRole, setSelectedRole] = useState("");