All files / src/hooks useUndoRedoStore.ts

56.52% Statements 39/69
54.76% Branches 23/42
50% Functions 4/8
62.29% Lines 38/61

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186                                                                                                                  4068x 4068x   4068x     4068x       4068x       4068x 4068x 4068x 4068x 4068x 4068x   4068x 4068x 32584x     4068x 32584x 29423x 4068x 32584x       14x 14x 14x 14x       14x 14x             14x                                         7x   7x             7x       7x           7x         7x   7x             7x       7x           7x         4068x   4068x                
import { useRef, useState } from "react";
import {
  FlowsheetEditOperationRead,
  FlowsheetEditTransition,
  SimulationObjectRetrieveRead,
  useUnitopsEditOperationsListQuery,
  useUnitopsEditOperationsRedoCreateMutation,
  useUnitopsEditOperationsUndoCreateMutation,
  useUnitopsSimulationobjectsListQuery,
} from "@/api/apiStore.gen";
import { getUnitOpVariantFamily } from "@/data/unitOpConfigs";
import { useFlowsheetId } from "./project";
import { useSearchParam } from "./searchParams";
 
/** Find the one active variant that replaces the currently selected object. */
function replacementVariantObjectId({
  selectedObjectId,
  affectedObjectIds,
  objectsBefore,
  objectsAfter,
}: {
  selectedObjectId: string | undefined;
  affectedObjectIds: number[];
  objectsBefore: SimulationObjectRetrieveRead[];
  objectsAfter: SimulationObjectRetrieveRead[];
}): number | undefined {
  const selectedId = Number(selectedObjectId);
  if (!Number.isFinite(selectedId) || !affectedObjectIds.includes(selectedId)) {
    return undefined;
  }
  Iif (objectsAfter.some((object) => object.id === selectedId)) return undefined;
 
  const selectedObject = objectsBefore.find(
    (object) => object.id === selectedId,
  );
  const selectedFamily = getUnitOpVariantFamily(selectedObject?.objectType);
  Iif (!selectedFamily) return undefined;
 
  const affectedIds = new Set(affectedObjectIds);
  const replacements = objectsAfter.filter(
    (object) =>
      affectedIds.has(object.id) &&
      object.componentName === selectedObject?.componentName &&
      getUnitOpVariantFamily(object.objectType)?.familyKey ===
        selectedFamily.familyKey,
  );
  return replacements.length === 1 ? replacements[0].id : undefined;
}
 
/**
 * Exposes the backend-owned edit history for the current flowsheet.
 *
 * The browser no longer stores domain snapshots or reconstructs objects. It
 * selects the next eligible operation and asks Django to apply its atomic
 * forward or inverse transition.
 */
export function useUndoRedoStore() {
  const flowsheetId = useFlowsheetId();
  const [selectedObjectId, setSelectedObjectId] = useSearchParam("object");
  const hasFlowsheet =
    flowsheetId !== undefined &&
    Number.isFinite(flowsheetId) &&
    flowsheetId > 0;
  const operationsQuery = useUnitopsEditOperationsListQuery(
    { flowsheet: flowsheetId! },
    { skip: !hasFlowsheet },
  );
  const simulationObjectsQuery = useUnitopsSimulationobjectsListQuery(
    { flowsheet: flowsheetId! },
    { skip: !hasFlowsheet },
  );
  const [undoOperation] = useUnitopsEditOperationsUndoCreateMutation();
  const [redoOperation] = useUnitopsEditOperationsRedoCreateMutation();
  const transitionInFlight = useRef(false);
  const selectedObjectIdRef = useRef(selectedObjectId);
  selectedObjectIdRef.current = selectedObjectId;
  const [isTransitioning, setIsTransitioning] = useState(false);
 
  const operations = operationsQuery.data ?? [];
  const currentRevision = operations.reduce(
    (revision, operation) => Math.max(revision, operation.last_revision),
    0,
  );
  const nextUndo = operations
    .filter((operation) => operation.state === "applied")
    .sort((left, right) => right.revision - left.revision)[0];
  const nextRedo = operations
    .filter((operation) => operation.state === "undone")
    .sort((left, right) => left.revision - right.revision)[0];
 
  const beginTransition = () => {
    Iif (transitionInFlight.current || operationsQuery.isFetching) return false;
    transitionInFlight.current = true;
    setIsTransitioning(true);
    return true;
  };
 
  const finishTransition = () => {
    transitionInFlight.current = false;
    setIsTransitioning(false);
  };
 
  const reconcileVariantSelection = async (
    operation: FlowsheetEditOperationRead,
    transition: FlowsheetEditTransition,
  ) => {
    if (operation.kind !== "switch_variant") return;
 
    const objectsBefore = simulationObjectsQuery.data ?? [];
    const refreshResult = await simulationObjectsQuery.refetch();
    Iif (!("data" in refreshResult) || !refreshResult.data) return;
 
    const replacementId = replacementVariantObjectId({
      selectedObjectId,
      affectedObjectIds: transition.affected_object_ids,
      objectsBefore,
      objectsAfter: refreshResult.data,
    });
    if (
      replacementId !== undefined &&
      selectedObjectIdRef.current === selectedObjectId
    ) {
      setSelectedObjectId(String(replacementId));
    }
  };
 
  const executeUndo = async (): Promise<boolean> => {
    Iif (!hasFlowsheet || !nextUndo || !beginTransition()) return false;
    try {
      const transition = await undoOperation({
        flowsheet: flowsheetId!,
        id: nextUndo.operation_id,
        flowsheetEditTransitionRequest: {
          expected_revision: currentRevision,
        },
      }).unwrap();
      await Promise.all([
        operationsQuery.refetch(),
        reconcileVariantSelection(nextUndo, transition),
      ]);
      return true;
    } catch (error) {
      console.error("Undo failed", error);
      await operationsQuery.refetch();
      return false;
    } finally {
      finishTransition();
    }
  };
 
  const executeRedo = async (): Promise<boolean> => {
    Iif (!hasFlowsheet || !nextRedo || !beginTransition()) return false;
    try {
      const transition = await redoOperation({
        flowsheet: flowsheetId!,
        id: nextRedo.operation_id,
        flowsheetEditTransitionRequest: {
          expected_revision: currentRevision,
        },
      }).unwrap();
      await Promise.all([
        operationsQuery.refetch(),
        reconcileVariantSelection(nextRedo, transition),
      ]);
      return true;
    } catch (error) {
      console.error("Redo failed", error);
      await operationsQuery.refetch();
      return false;
    } finally {
      finishTransition();
    }
  };
 
  const isBusy =
    isTransitioning || operationsQuery.isLoading || operationsQuery.isFetching;
 
  return {
    canUndo: nextUndo !== undefined && !isBusy,
    canRedo: nextRedo !== undefined && !isBusy,
    isReady: hasFlowsheet && !isBusy,
    executeUndo,
    executeRedo,
  };
}