diff --git a/server/classes/task.ts b/server/classes/task.ts index daf8fba86..81762c993 100644 --- a/server/classes/task.ts +++ b/server/classes/task.ts @@ -84,6 +84,8 @@ export default class Task { macros: Macro[]; preMacros: Macro[]; assigned: boolean | string; + /** Frozen at creation so random instructions don't re-roll on every read */ + instructions: string | null; constructor(params: Partial = {}) { // The check to see if the task is relevant was already handled // before this task was instantiated @@ -181,6 +183,28 @@ export default class Task { // Task Report Assignment this.assigned = params.assigned || false; + + // Freeze instructions at creation so random generators (Panel Actions, + // reportReplacer tokens) don't re-roll on every GraphQL read. The resolver + // returns this frozen value and falls back to live computation only for + // tasks persisted before this change (where instructions is undefined). + if (params.instructions !== undefined && params.instructions !== null) { + this.instructions = params.instructions; + } else { + try { + this.instructions = definitionObject?.instructions + ? definitionObject.instructions({ + simulator, + requiredValues: this.values, + task: this, + }) + : null; + } catch (e) { + // Some definitions throw if their required system/panel isn't found; + // fall back to live computation in the resolver. + this.instructions = null; + } + } } verify(dismiss) { if (this.verified) return; diff --git a/server/tasks/softwarePanels.js b/server/tasks/softwarePanels.js index 9f86cec64..09936e69c 100644 --- a/server/tasks/softwarePanels.js +++ b/server/tasks/softwarePanels.js @@ -33,7 +33,7 @@ export default [ input: ({simulator}) => simulator ? simulator.panels.map(p => { - const panel = App.softwarePanels.find(pp => (pp.id = p)); + const panel = App.softwarePanels.find(pp => pp.id === p); return {label: panel.name, value: panel.id}; }) : App.softwarePanels.map(panel => ({ diff --git a/server/typeDefs/tasks.ts b/server/typeDefs/tasks.ts index 72d952cc2..06a7c316d 100644 --- a/server/typeDefs/tasks.ts +++ b/server/typeDefs/tasks.ts @@ -111,6 +111,13 @@ const schema = gql` const resolver = { Task: { instructions(task) { + // Return the frozen value stamped at task creation when available. + // This prevents random generators (Panel Actions operations list, + // reportReplacer tokens) from re-rolling on every subscription push. + // The live fallback handles tasks created before this change. + if (task.instructions !== undefined && task.instructions !== null) { + return task.instructions; + } const {simulatorId, values, definition} = task; const simulator = App.simulators.find(s => s.id === simulatorId); const taskDef = taskDefinitions.find(d => d.name === definition); diff --git a/src/containers/FlightDirector/TaskTemplates/DebouncedInput.tsx b/src/containers/FlightDirector/TaskTemplates/DebouncedInput.tsx new file mode 100644 index 000000000..9aa9f8cc8 --- /dev/null +++ b/src/containers/FlightDirector/TaskTemplates/DebouncedInput.tsx @@ -0,0 +1,71 @@ +import React, {useEffect, useRef, useState} from "react"; +import {Input} from "reactstrap"; +import debounce from "helpers/debounce"; + +interface DebouncedInputProps + extends Omit, "onChange"> { + /** Controlled value sourced from the server. Only synced in when the + * input is not focused, so an inbound subscription echo never clobbers + * actively typed text. */ + value: string | number | undefined | null; + /** Called with the latest value after the user pauses typing (~400 ms). */ + onCommit: (value: string) => void; +} + +/** + * A debounced, focus-aware controlled input for the Task Flow config editor. + * + * The task-flow subscription re-pushes the entire taskFlows array on every + * mutation, which previously caused `defaultValue`-based inputs to reset their + * text on every keystroke round-trip. This component fixes that by: + * 1. Holding local state for smooth typing — server value only seeded on mount + * or when the field is not focused. + * 2. Firing `onCommit` (the GraphQL mutation) debounced at 400 ms so the + * subscription does not echo back mid-keystroke. + */ +const DebouncedInput: React.FC = ({ + value, + onCommit, + ...rest +}) => { + const [localValue, setLocalValue] = useState( + value !== undefined && value !== null ? String(value) : "", + ); + const isFocused = useRef(false); + + // Sync from server only when not actively typing. + useEffect(() => { + if (!isFocused.current) { + setLocalValue(value !== undefined && value !== null ? String(value) : ""); + } + }, [value]); + + const debouncedCommit = useRef( + debounce((v: string) => onCommit(v), 400), + ); + + // Rebuild the debounced callback if onCommit identity changes (rare but safe). + useEffect(() => { + debouncedCommit.current = debounce((v: string) => onCommit(v), 400); + }, [onCommit]); + + return ( + ) => { + const v = e.target.value; + setLocalValue(v); + debouncedCommit.current(v); + }} + onFocus={() => { + isFocused.current = true; + }} + onBlur={() => { + isFocused.current = false; + }} + /> + ); +}; + +export default DebouncedInput; diff --git a/src/containers/FlightDirector/TaskTemplates/flowConfig.tsx b/src/containers/FlightDirector/TaskTemplates/flowConfig.tsx index e5a7ad718..8e1dc7795 100644 --- a/src/containers/FlightDirector/TaskTemplates/flowConfig.tsx +++ b/src/containers/FlightDirector/TaskTemplates/flowConfig.tsx @@ -9,7 +9,8 @@ import { useTaskFlowAddStepMutation, useTaskFlowRemoveStepMutation, } from "generated/graphql"; -import {Col, Label, Input, Button} from "reactstrap"; +import {Col, Label, Button} from "reactstrap"; +import DebouncedInput from "./DebouncedInput"; import SortableList from "helpers/SortableList"; const FlowConfig = () => { @@ -39,22 +40,22 @@ const FlowConfig = () => {

Flow Config