From 7bb7838b30703e41b8aae9fa39f9961ccb216bea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Simon=20K=C3=BCnzel?= <simonk@fsmpi.rwth-aachen.de>
Date: Thu, 17 Apr 2025 01:40:32 +0200
Subject: [PATCH] Add JsonEditor

---
 src/videoag/api/types.ts                      |  2 +-
 src/videoag/form/TypeEditor.tsx               | 80 +++++++++++++++++++
 .../object_management/OMFieldEditor.tsx       | 11 ++-
 3 files changed, 91 insertions(+), 2 deletions(-)

diff --git a/src/videoag/api/types.ts b/src/videoag/api/types.ts
index 80568bd..ac052d6 100644
--- a/src/videoag/api/types.ts
+++ b/src/videoag/api/types.ts
@@ -166,7 +166,7 @@ export interface field_description {
     has_default_value: boolean;
     default_value?: any;
 
-    // type: object, datetime
+    // type: object, datetime, media_process
     may_be_null?: boolean;
     // type: enum
     enums?: string[];
diff --git a/src/videoag/form/TypeEditor.tsx b/src/videoag/form/TypeEditor.tsx
index 60861d3..40ab15c 100644
--- a/src/videoag/form/TypeEditor.tsx
+++ b/src/videoag/form/TypeEditor.tsx
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
 
 import type { int } from "@/videoag/api/types";
 import { useLanguage } from "@/videoag/localization/LanguageProvider";
+import { deepEquals } from "@/videoag/miscellaneous/Util";
 
 export type EditorArgs = {
     value?: any;
@@ -348,3 +349,82 @@ export function ChooserEditor({
         </select>
     );
 }
+
+export function JsonEditor({
+    value,
+    updateValue,
+    disabled,
+    autoFocus,
+    hideErrors,
+    allowUndefined,
+    undefinedAsNull,
+}: EditorArgs & {
+    allowUndefined?: boolean;
+    undefinedAsNull?: boolean;
+}) {
+    // We need to store the default value locally to enable smooth editing. If the input is partially invalid, we just
+    // get nothing and setting that back as the value will cause the whole input to reset
+    const knownValue = useRef<any>(undefined);
+    const [stringValue, setStringValue] = useState<string>("");
+
+    useEffect(() => {
+        if (deepEquals(value, knownValue.current)) return;
+        //Value was changed not by us, update internal state
+        knownValue.current = value;
+
+        let normValue = value;
+        if (undefinedAsNull && normValue === null) {
+            normValue = undefined;
+        }
+        if (allowUndefined && normValue === undefined) {
+            setStringValue("");
+        } else {
+            setStringValue(JSON.stringify(normValue, null, "  "));
+        }
+    }, [knownValue, value, allowUndefined, undefinedAsNull]);
+
+    const getJsonParseResults = (stringValue: string) => {
+        let json;
+        let isValid;
+        let errorMsg = undefined;
+        if (stringValue === "" && allowUndefined === true) {
+            isValid = true;
+            json = undefinedAsNull ? null : undefined;
+        } else {
+            try {
+                json = JSON.parse(stringValue);
+                isValid = true;
+            } catch (e: any) {
+                json = undefined;
+                isValid = false;
+                errorMsg = e.toString();
+            }
+        }
+        return [json, isValid, errorMsg];
+    };
+    const update = (val: any, affectNow: boolean) => {
+        setStringValue(val);
+        const [json, isValid, errorMsg] = getJsonParseResults(val);
+        knownValue.current = json;
+        updateValue(json, isValid, affectNow);
+    };
+    const [json, isValid, errorMsg] = getJsonParseResults(stringValue);
+
+    return (
+        <div className="d-inline-grid w-100">
+            <textarea
+                className={
+                    "form-control w-100 " + (isValid || hideErrors === true ? "" : "is-invalid")
+                }
+                style={{ minHeight: "6em" }}
+                value={stringValue ?? ""}
+                onChange={(e) => update(e.target.value, false)}
+                disabled={disabled}
+                autoFocus={autoFocus}
+            />
+            {errorMsg && (
+                <span className="d-inline invalid-feedback">Invalid JSON: {errorMsg}</span>
+            )}
+        </div>
+    );
+}
diff --git a/src/videoag/object_management/OMFieldEditor.tsx b/src/videoag/object_management/OMFieldEditor.tsx
index 96ce052..0b45fec 100644
--- a/src/videoag/object_management/OMFieldEditor.tsx
+++ b/src/videoag/object_management/OMFieldEditor.tsx
@@ -8,6 +8,7 @@ import {
     IntEditor,
     DatetimeEditor,
     ChooserEditor,
+    JsonEditor,
 } from "@/videoag/form/TypeEditor";
 
 import { UserIdListEditor } from "./ObjectEditor";
@@ -73,7 +74,15 @@ export function OMFieldEditor({
                 </span>
             );
 
-        // TODO media_process
+        case "media_process":
+            return (
+                <JsonEditor
+                    {...args}
+                    allowUndefined={description.may_be_null}
+                    undefinedAsNull={true}
+                />
+            );
+
         // TODO object
 
         default:
-- 
GitLab