Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions server/classes/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Task> = {}) {
// The check to see if the task is relevant was already handled
// before this task was instantiated
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion server/tasks/softwarePanels.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof. That's a silly bug.

return {label: panel.name, value: panel.id};
})
: App.softwarePanels.map(panel => ({
Expand Down
7 changes: 7 additions & 0 deletions server/typeDefs/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
71 changes: 71 additions & 0 deletions src/containers/FlightDirector/TaskTemplates/DebouncedInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React, {useEffect, useRef, useState} from "react";
import {Input} from "reactstrap";
import debounce from "helpers/debounce";

interface DebouncedInputProps
extends Omit<React.ComponentPropsWithoutRef<typeof Input>, "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<DebouncedInputProps> = ({
value,
onCommit,
...rest
}) => {
const [localValue, setLocalValue] = useState<string>(
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 (
<Input
{...rest}
value={localValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const v = e.target.value;
setLocalValue(v);
debouncedCommit.current(v);
}}
onFocus={() => {
isFocused.current = true;
}}
onBlur={() => {
isFocused.current = false;
}}
/>
);
};

export default DebouncedInput;
19 changes: 10 additions & 9 deletions src/containers/FlightDirector/TaskTemplates/flowConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -39,22 +40,22 @@ const FlowConfig = () => {
<h2>Flow Config</h2>
<Label>
Name:
<Input
<DebouncedInput
type="text"
defaultValue={taskFlow?.name}
onChange={e =>
rename({variables: {id: flowId || "", name: e.target.value || ""}})
value={taskFlow?.name}
onCommit={name =>
rename({variables: {id: flowId || "", name: name || ""}})
}
/>
</Label>
<Label>
Category:
<Input
<DebouncedInput
type="text"
defaultValue={taskFlow?.category}
onChange={e =>
value={taskFlow?.category}
onCommit={category =>
setCategory({
variables: {id: flowId || "", category: e.target.value || ""},
variables: {id: flowId || "", category: category || ""},
})
}
/>
Expand Down
17 changes: 9 additions & 8 deletions src/containers/FlightDirector/TaskTemplates/flowStepConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {css} from "@emotion/core";
import React from "react";
import {Col, Input, Label, ListGroup, ListGroupItem, Button} from "reactstrap";
import {useParams, useNavigate, useMatch, Outlet} from "react-router";
import DebouncedInput from "./DebouncedInput";
import {
useTaskFlowsConfigSubscription,
useTaskFlowRenameStepMutation,
Expand Down Expand Up @@ -59,32 +60,32 @@ const StepConfig = () => {
<h2>Step Config</h2>
<Label>
Name:
<Input
<DebouncedInput
type="text"
defaultValue={step.name}
onChange={e =>
value={step.name}
onCommit={name =>
rename({
variables: {
id: flowId || "",
stepId: stepId || "",
name: e.target.value || "",
name: name || "",
},
})
}
/>
</Label>
<Label>
Delay (ms)
<Input
<DebouncedInput
type="number"
min={0}
defaultValue={step.delay}
onChange={e =>
value={step.delay ?? 0}
onCommit={v =>
setDelay({
variables: {
id: flowId || "",
stepId: stepId || "",
delay: parseInt(e.target.value, 10),
delay: parseInt(v, 10),
},
})
}
Expand Down
Loading