From 6c0ba3c3819c5a004ed05630f447997de06a0703 Mon Sep 17 00:00:00 2001
From: Dorian Koch <doriank@fsmpi.rwth-aachen.de>
Date: Wed, 11 Sep 2024 14:32:46 +0200
Subject: [PATCH] Fix Player title not updating after navigating to next
 lecture

---
 src/components/OMConfigComponent.tsx | 55 ++++++++++++++++------------
 src/components/StopNavigation.tsx    | 24 ++++++++++++
 2 files changed, 55 insertions(+), 24 deletions(-)
 create mode 100644 src/components/StopNavigation.tsx

diff --git a/src/components/OMConfigComponent.tsx b/src/components/OMConfigComponent.tsx
index 45a3e1e..451cc9f 100644
--- a/src/components/OMConfigComponent.tsx
+++ b/src/components/OMConfigComponent.tsx
@@ -20,6 +20,7 @@ import type {
     int,
 } from "@/api/api_v1_types";
 import { StylizedText } from "./StylizedText";
+import StopNavigation from "./StopNavigation";
 
 const markdownSupportingFields = [
     "announcement.text",
@@ -132,29 +133,31 @@ export function EmbeddedOMFieldComponent({
     const [isValid, setIsValid] = useState(true);
     const [isEditing, setIsEditing] = useState(false);
     const [field, setField] = useState<field_value>();
-    //We need an additional references since a state is not updated immediately and sometimes the API call would fail
+    // We need an additional references since a state is not updated immediately and sometimes the API call would fail
     // due to an unexpected current value. (Only using a ref does not work, since the rendering needs this to indicate
     // if changes have been made)
-    const fieldValueRef = useRef<field_value>();
+    const serverSideFieldValueRef = useRef<field_value>(); // This is what we expect the value to be on the server
     const updatedSinceLastReloadRef = useRef<boolean>(false);
-    const beforeEditingValue = useRef(initialValue);
+    const beforeEditingValue = useRef();
     const { editMode } = useEditMode();
 
-    //These types can be edited without loading the configuration or starting editing (e.g. isEditing is ignored)
-    //The description of these types may have no additional fields (only id, type, default_value)
+    // These types can be edited without loading the configuration or starting editing (e.g. isEditing is ignored)
+    // The description of these types may have no additional fields (only id, type, default_value)
     const isInstantEdit = ["boolean"].includes(field_type);
 
     useEffect(() => {
-        if (!isInstantEdit) return;
-        setField({
-            description: {
-                id: field_id,
-                type: field_type,
-                //Default value does not matter
-            },
-            value: initialValue,
-        });
-        fieldValueRef.current = initialValue;
+        if (isInstantEdit) {
+            setField({
+                description: {
+                    id: field_id,
+                    type: field_type,
+                    //Default value does not matter
+                },
+                value: initialValue,
+            });
+        }
+
+        serverSideFieldValueRef.current = initialValue;
         setValue(initialValue);
         // eslint-disable-next-line react-hooks/exhaustive-deps
     }, [isInstantEdit, field_id, field_type, initialValue]);
@@ -162,6 +165,7 @@ export function EmbeddedOMFieldComponent({
     useEffect(() => {
         if (!isEditing) return;
 
+        // TODO: disable editing while config is still loading, this is especially annoying with high latency
         api.getOMConfiguration(object_type, object_id)
             .then((config) => {
                 const field = config.fields?.find((field) => field.description.id === field_id);
@@ -171,7 +175,7 @@ export function EmbeddedOMFieldComponent({
                     return;
                 }
                 setField(field);
-                fieldValueRef.current = field.value;
+                serverSideFieldValueRef.current = field.value;
                 setValue(field.value);
             })
             .catch((error) => {
@@ -180,18 +184,18 @@ export function EmbeddedOMFieldComponent({
             });
     }, [api, isEditing, object_type, object_id, field_id]);
 
-    const [deferred, now] = useDebounceUpdateData<any, void>(
+    const [deferredSendValueUpdate, nowSendValueUpdate] = useDebounceUpdateData<any, void>(
         async (newValue: any) => {
             if (field === undefined) return;
-            if (deepEquals(newValue, fieldValueRef.current)) return;
+            if (deepEquals(newValue, serverSideFieldValueRef.current)) return;
 
             return api
                 .updateOMObject(object_type, object_id, {
                     updates: { [field_id]: newValue },
-                    expected_current_values: { [field_id]: fieldValueRef.current },
+                    expected_current_values: { [field_id]: serverSideFieldValueRef.current },
                 })
                 .then(() => {
-                    fieldValueRef.current = newValue;
+                    serverSideFieldValueRef.current = newValue;
                     setField(field === undefined ? undefined : { ...field, value: newValue });
                     if (isInstantEdit) {
                         reloadFunc();
@@ -209,7 +213,7 @@ export function EmbeddedOMFieldComponent({
     const disableEditing = () => {
         if (!isValid) return;
         if (!isEditing) return;
-        now(value).then(() => {
+        nowSendValueUpdate(value).then(() => {
             setIsEditing(false);
             if (updatedSinceLastReloadRef.current) {
                 reloadFunc();
@@ -222,7 +226,7 @@ export function EmbeddedOMFieldComponent({
         if (!isEditing) return;
         setValue(beforeEditingValue.current);
         setIsValid(true);
-        deferred(beforeEditingValue.current);
+        deferredSendValueUpdate(beforeEditingValue.current);
     };
 
     useEffect(() => {
@@ -276,9 +280,9 @@ export function EmbeddedOMFieldComponent({
             setIsValid(isValid);
             if (isValid) {
                 if (affectNow) {
-                    now(newValue);
+                    nowSendValueUpdate(newValue);
                 } else {
-                    deferred(newValue);
+                    deferredSendValueUpdate(newValue);
                 }
             }
         },
@@ -301,6 +305,9 @@ export function EmbeddedOMFieldComponent({
             onBlur={disableEditing}
             onSubmit={disableEditing}
         >
+            {isEditing && hasChanged && (
+                <StopNavigation warningText="You have unsaved changes. Are you sure you want to leave?" />
+            )}
             <div
                 className={"vr mx-1 " + (hasChanged ? "opacity-25" : "opacity-0")}
                 style={{ color: "orange", width: "4px", verticalAlign: "baseline" }}
diff --git a/src/components/StopNavigation.tsx b/src/components/StopNavigation.tsx
new file mode 100644
index 0000000..47c92f0
--- /dev/null
+++ b/src/components/StopNavigation.tsx
@@ -0,0 +1,24 @@
+import { useEffect } from "react";
+import { useRouter } from "next/router";
+
+export default function StopNavigation({ warningText }: { warningText: string }) {
+    const router = useRouter();
+    useEffect(() => {
+        const handleWindowClose = (e: BeforeUnloadEvent) => {
+            e.preventDefault();
+            return warningText;
+        };
+        const handleBrowseAway = () => {
+            if (window.confirm(warningText)) return;
+            router.events.emit("routeChangeError");
+            throw "routeChange aborted.";
+        };
+        window.addEventListener("beforeunload", handleWindowClose);
+        router.events.on("routeChangeStart", handleBrowseAway);
+        return () => {
+            window.removeEventListener("beforeunload", handleWindowClose);
+            router.events.off("routeChangeStart", handleBrowseAway);
+        };
+    }, [router, warningText]);
+    return null;
+}
-- 
GitLab