{copyMenu}
diff --git a/front_end/src/components/forecast_maker/continuous_input/index.tsx b/front_end/src/components/forecast_maker/continuous_input/index.tsx
index bf69309977..2f555730ec 100644
--- a/front_end/src/components/forecast_maker/continuous_input/index.tsx
+++ b/front_end/src/components/forecast_maker/continuous_input/index.tsx
@@ -23,7 +23,9 @@ import {
} from "@/utils/forecasts/switch_forecast_type";
import { computeQuartilesFromCDF, getCdfBounds } from "@/utils/math";
-import ContinuousInputContainer from "./continuous_input_container";
+import ContinuousInputContainer, {
+ ContinuousInputContainerProps,
+} from "./continuous_input_container";
import ContinuousPredictionChart from "./continuous_prediction_chart";
import ContinuousSlider from "./continuous_slider";
import { validateAllQuantileInputs } from "../helpers";
@@ -55,6 +57,11 @@ type Props = {
predictionMessage?: ReactNode;
menu?: ReactNode;
copyMenu?: ReactNode;
+ clipboardData?: Omit<
+ NonNullable
,
+ "scaling" | "openLowerBound" | "openUpperBound"
+ >;
+ skipModeSyncRef?: React.RefObject;
userPreviousLabel?: string;
userPreviousRowClassName?: string;
hideCurrentUserRow?: boolean;
@@ -83,6 +90,8 @@ const ContinuousInput: FC = ({
predictionMessage,
menu,
copyMenu,
+ clipboardData,
+ skipModeSyncRef,
userPreviousLabel,
userPreviousRowClassName,
hideCurrentUserRow,
@@ -102,6 +111,10 @@ const ContinuousInput: FC = ({
isMounted.current = true;
return;
}
+ if (skipModeSyncRef?.current) {
+ skipModeSyncRef.current = false;
+ return;
+ }
if (
forecastInputMode === ContinuousForecastInputType.Quantile &&
(isDirty ||
@@ -143,6 +156,16 @@ const ContinuousInput: FC = ({
previousForecast={previousForecast}
menu={menu}
copyMenu={copyMenu}
+ clipboardData={
+ clipboardData
+ ? {
+ ...clipboardData,
+ scaling: question.scaling,
+ openLowerBound: !!question.open_lower_bound,
+ openUpperBound: !!question.open_upper_bound,
+ }
+ : undefined
+ }
disabled={disabled || disableInputModeSwitch}
questionType={question.type}
>
diff --git a/front_end/src/components/forecast_maker/forecast_maker_group/continuous_input_wrapper.tsx b/front_end/src/components/forecast_maker/forecast_maker_group/continuous_input_wrapper.tsx
index 91c9b83d44..32f07ca40c 100644
--- a/front_end/src/components/forecast_maker/forecast_maker_group/continuous_input_wrapper.tsx
+++ b/front_end/src/components/forecast_maker/forecast_maker_group/continuous_input_wrapper.tsx
@@ -7,6 +7,7 @@ import React, {
useCallback,
useEffect,
useMemo,
+ useRef,
useState,
} from "react";
@@ -21,6 +22,7 @@ import {
DistributionQuantileComponent,
DistributionSlider,
DistributionSliderComponent,
+ QuantileValue,
} from "@/types/question";
import { TranslationKey } from "@/types/translations";
import cn from "@/utils/core/cn";
@@ -268,6 +270,39 @@ const ContinuousInputWrapper: FC> = ({
handleForecastExpiration(option.id, modalSavedState.forecastExpiration);
}, [handleForecastExpiration, option.id, modalSavedState.forecastExpiration]);
+ const skipModeSyncRef = useRef(false);
+
+ const handleClipboardPaste = useCallback(
+ (
+ type: ContinuousForecastInputType,
+ components: DistributionSliderComponent[] | DistributionQuantileComponent
+ ) => {
+ // Only skip the mode-sync effect if this paste actually changes the
+ // mode — if it fires while already on the target mode, the effect
+ // never runs to consume the flag, and it would wrongly suppress the
+ // next genuine manual mode toggle.
+ if (type !== forecastInputMode) {
+ skipModeSyncRef.current = true;
+ }
+ if (type === ContinuousForecastInputType.Slider) {
+ handleChange(option.id, {
+ type: ContinuousForecastInputType.Slider,
+ components: components as DistributionSliderComponent[],
+ });
+ } else {
+ handleChange(option.id, {
+ type: ContinuousForecastInputType.Quantile,
+ components: (components as QuantileValue[]).map((c) => ({
+ ...c,
+ isDirty: true,
+ })),
+ });
+ }
+ setForecastInputMode(type);
+ },
+ [handleChange, option.id, setForecastInputMode, forecastInputMode]
+ );
+
let SubmitControls: ReactNode = null;
const predictButtonIsDirty =
@@ -432,6 +467,12 @@ const ContinuousInputWrapper: FC> = ({
}
menu={option.menu}
copyMenu={copyMenu}
+ clipboardData={{
+ sliderComponents: option.userSliderForecast,
+ quantileComponents: option.userQuantileForecast,
+ onPaste: handleClipboardPaste,
+ }}
+ skipModeSyncRef={skipModeSyncRef}
userPreviousLabel={showWithdrawnRow ? "(Withdrawn)" : undefined}
userPreviousRowClassName={showWithdrawnRow ? "text-xs" : undefined}
hideCurrentUserRow={showWithdrawnRow}
diff --git a/front_end/src/components/forecast_maker/forecast_maker_question/forecast_maker_continuous.tsx b/front_end/src/components/forecast_maker/forecast_maker_question/forecast_maker_continuous.tsx
index ac1b74675f..5a28f3564e 100644
--- a/front_end/src/components/forecast_maker/forecast_maker_question/forecast_maker_continuous.tsx
+++ b/front_end/src/components/forecast_maker/forecast_maker_question/forecast_maker_continuous.tsx
@@ -2,7 +2,15 @@
import { isNil } from "lodash";
import { usePathname, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
-import React, { FC, ReactNode, useEffect, useMemo, useState } from "react";
+import React, {
+ FC,
+ ReactNode,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
import {
createForecasts,
@@ -22,6 +30,7 @@ import {
DistributionQuantileComponent,
DistributionSlider,
DistributionSliderComponent,
+ QuantileValue,
QuestionWithNumericForecasts,
} from "@/types/question";
import { sendPredictEvent } from "@/utils/analytics";
@@ -83,6 +92,7 @@ const ForecastMakerContinuous: FC = ({
const [isDirty, setIsDirty] = useState(false);
const [submitError, setSubmitError] = useState();
const [isWithdrawModalOpen, setIsWithdrawModalOpen] = useState(false);
+ const skipModeSyncRef = useRef(false);
const previousForecast = question.my_forecasts?.latest;
const activeForecast = isOpenQuestionPredicted(question)
? previousForecast
@@ -463,6 +473,34 @@ const ForecastMakerContinuous: FC = ({
);
}
+ const handleClipboardPaste = useCallback(
+ (
+ type: ContinuousForecastInputType,
+ components: DistributionSliderComponent[] | DistributionQuantileComponent
+ ) => {
+ // Only skip the mode-sync effect if this paste actually changes the
+ // mode — if it fires while already on the target mode, the effect
+ // never runs to consume the flag, and it would wrongly suppress the
+ // next genuine manual mode toggle.
+ if (type !== forecastInputMode) {
+ skipModeSyncRef.current = true;
+ }
+ if (type === ContinuousForecastInputType.Slider) {
+ setSliderDistributionComponents(
+ components as DistributionSliderComponent[]
+ );
+ } else {
+ setQuantileDistributionComponents(
+ (components as QuantileValue[]).map((c) => ({ ...c, isDirty: true }))
+ );
+ }
+ setForecastInputMode(type);
+ setIsDirty(true);
+ setShowSuccessBox(false);
+ },
+ [forecastInputMode]
+ );
+
return (
<>
= ({
submitControls={SubmitControls}
disabled={!canPredict}
predictionMessage={predictionMessage}
+ clipboardData={{
+ sliderComponents: sliderDistributionComponents,
+ quantileComponents: quantileDistributionComponents,
+ onPaste: handleClipboardPaste,
+ }}
+ skipModeSyncRef={skipModeSyncRef}
/>
>
);