diff --git a/apps/webapp/app/components/primitives/Checkbox.tsx b/apps/webapp/app/components/primitives/Checkbox.tsx index a00bfee9916..e22be7e712c 100644 --- a/apps/webapp/app/components/primitives/Checkbox.tsx +++ b/apps/webapp/app/components/primitives/Checkbox.tsx @@ -86,7 +86,7 @@ export const CheckboxWithLabel = React.forwardRef { const [isChecked, setIsChecked] = useState(defaultChecked ?? false); - const [isDisabled, setIsDisabled] = useState(disabled ?? false); + const isDisabled = disabled ?? false; const onChangeRef = React.useRef(onChange); const generatedId = React.useId(); const inputId = id ?? generatedId; @@ -102,10 +102,6 @@ export const CheckboxWithLabel = React.forwardRef { - setIsDisabled(disabled ?? false); - }, [disabled]); - useEffect(() => { onChangeRef.current = onChange; }, [onChange]); diff --git a/apps/webapp/app/components/primitives/ClientTabs.tsx b/apps/webapp/app/components/primitives/ClientTabs.tsx index ebe730c174d..459a3e87637 100644 --- a/apps/webapp/app/components/primitives/ClientTabs.tsx +++ b/apps/webapp/app/components/primitives/ClientTabs.tsx @@ -20,18 +20,13 @@ const ClientTabs = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ onValueChange, value: valueProp, defaultValue, ...props }, ref) => { - const [value, setValue] = React.useState(valueProp ?? defaultValue); - - React.useEffect(() => { - if (valueProp !== undefined) { - setValue(valueProp); - } - }, [valueProp]); + const [internalValue, setInternalValue] = React.useState(defaultValue); + const value = valueProp ?? internalValue; const handleValueChange = React.useCallback( (nextValue: string) => { if (valueProp === undefined) { - setValue(nextValue); + setInternalValue(nextValue); } onValueChange?.(nextValue); }, diff --git a/apps/webapp/app/components/runs/v3/RunTagInput.tsx b/apps/webapp/app/components/runs/v3/RunTagInput.tsx index 25d818f402e..f25a1106fa9 100644 --- a/apps/webapp/app/components/runs/v3/RunTagInput.tsx +++ b/apps/webapp/app/components/runs/v3/RunTagInput.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState, useEffect, type KeyboardEvent } from "react"; +import { useCallback, useState, type KeyboardEvent } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { Input } from "~/components/primitives/Input"; import { RunTag } from "./RunTag"; @@ -26,39 +26,30 @@ export function RunTagInput({ maxTagLength = 128, onTagsChange, }: TagInputProps) { - // Use controlled tags if provided, otherwise use default - const initialTags = controlledTags ?? defaultTags; - - const [tags, setTags] = useState(initialTags); + const [internalTags, setInternalTags] = useState(defaultTags); + const tags = controlledTags ?? internalTags; const [inputValue, setInputValue] = useState(""); - // Sync internal state with external tag changes - useEffect(() => { - if (controlledTags !== undefined) { - setTags(controlledTags); - } - }, [controlledTags]); - const addTag = useCallback( (tagText: string) => { const trimmedTag = tagText.trim(); if (trimmedTag && !tags.includes(trimmedTag) && tags.length < maxTags) { const newTags = [...tags, trimmedTag]; - setTags(newTags); + if (controlledTags === undefined) setInternalTags(newTags); onTagsChange?.(newTags); } setInputValue(""); }, - [tags, onTagsChange, maxTags] + [tags, controlledTags, onTagsChange, maxTags] ); const removeTag = useCallback( (tagToRemove: string) => { const newTags = tags.filter((tag) => tag !== tagToRemove); - setTags(newTags); + if (controlledTags === undefined) setInternalTags(newTags); onTagsChange?.(newTags); }, - [tags, onTagsChange] + [tags, controlledTags, onTagsChange] ); const handleKeyDown = useCallback( diff --git a/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx b/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx index 3054a0cdf48..67a142ce36f 100644 --- a/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx +++ b/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx @@ -67,12 +67,16 @@ export function PurchaseSchedulesModal({ const isLoading = fetcher.state !== "idle"; const [open, setOpen] = useState(false); - // Reset the bundle stepper to the user's current extra-schedules count on - // each open. Earlier this only re-synced when `extraSchedules`/`stepSize` - // props changed, so if the user opened the modal, typed a value, cancelled, - // and reopened without purchasing, the stale draft persisted. + // Reset the bundle stepper to the user's current extra-schedules count on each open. + const handleOpenChange = (nextOpen: boolean) => { + if (nextOpen) setBundles(Math.round(extraSchedules / stepSize)); + setOpen(nextOpen); + }; + useEffect(() => { - if (open) setBundles(Math.round(extraSchedules / stepSize)); + if (!open) return; + // oxlint-disable-next-line react/react-compiler -- Keep the open draft aligned with authoritative billing values. + setBundles(Math.round(extraSchedules / stepSize)); }, [open, extraSchedules, stepSize]); useEffect(() => { @@ -113,7 +117,7 @@ export function PurchaseSchedulesModal({ } return ( - + {triggerButton ?? (