diff --git a/env.d.ts b/env.d.ts index bbe913ad..fd80cdd7 100644 --- a/env.d.ts +++ b/env.d.ts @@ -1 +1,2 @@ declare const APP_VERSION: string; +declare module "*.css"; diff --git a/src/App.tsx b/src/App.tsx index 8a285431..bdecc680 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -51,6 +51,7 @@ const App = () => { const context = useContext(ediTDorContext); const editorRef = useRef(null); + const [jsonIndentation, setJsonIndentation] = useState<2 | 4>(2); const [customBreakpointsState, setCustomBreakpointsState] = useState(0); const tdViewerRef = useRef(null); const [pendingTd, setPendingTd] = useState(""); @@ -229,7 +230,10 @@ const App = () => { return (
- +
@@ -249,7 +253,10 @@ const App = () => { />
- +
diff --git a/src/components/App/AppHeader.tsx b/src/components/App/AppHeader.tsx index 2974e581..a096e6de 100644 --- a/src/components/App/AppHeader.tsx +++ b/src/components/App/AppHeader.tsx @@ -49,7 +49,15 @@ const INVALID_TYPE_MESSAGE = const VALIDATION_FAILED_MESSAGE = "The Thing Model did not pass the JSON schema validation Please make sure the Thing Model is valid according to the JSON schema before contributing it to the catalog."; -const AppHeader: React.FC = () => { +interface AppHeaderProps { + jsonIndentation: 2 | 4; + onJsonIndentationChange: (value: 2 | 4) => void; +} + +const AppHeader: React.FC = ({ + jsonIndentation, + onJsonIndentationChange, +}) => { const context = useContext(ediTDorContext); const td: ThingDescription = context.parsedTD; /** States*/ @@ -109,9 +117,8 @@ const AppHeader: React.FC = () => { context.updateOfflineTD(res.td); context.updateIsModified(false); - context.setFileHandle(res.fileHandle || res.fileNname); - context.updateLinkedTd(undefined); - context.addLinkedTd(linkedTd); + context.setFileHandle(res.fileHandle || res.fileName); + context.updateLinkedTd(linkedTd); } catch (error) { const msg = "Opening a new TD was canceled or an error occured."; console.error(msg, error); @@ -341,7 +348,11 @@ const AppHeader: React.FC = () => { - + void; hideTitle?: boolean; className?: string; + jsonIndentation?: 2 | 4; + onJsonIndentationChange?: (value: 2 | 4) => void; } const Settings: React.FC = ({ @@ -43,6 +46,8 @@ const Settings: React.FC = ({ onChange, hideTitle = false, className = "", + jsonIndentation, + onJsonIndentationChange, }) => { const [data, setData] = useState(initialData); const [errors, setErrors] = useState({ @@ -51,10 +56,6 @@ const Settings: React.FC = ({ pathToValue: "", }); - useEffect(() => { - setData(initialData); - }, [initialData]); - useEffect(() => { if (onChange) { const isValid = @@ -127,8 +128,35 @@ const Settings: React.FC = ({ [] ); + const handleJsonIndentationChange = useCallback( + (e: React.ChangeEvent) => { + if (!onJsonIndentationChange) { + return; + } + + const parsed = Number(e.target.value); + const value: 2 | 4 = parsed === 4 ? 4 : 2; + onJsonIndentationChange(value); + }, + [onJsonIndentationChange] + ); + return (
+
+ {!hideTitle &&

JSON Editor

} +
+ +
+
+
{!hideTitle && (

Third Party Service Configuration

diff --git a/src/components/Dialogs/SettingsDialog.tsx b/src/components/Dialogs/SettingsDialog.tsx index 7ab16ad8..cfc108ed 100644 --- a/src/components/Dialogs/SettingsDialog.tsx +++ b/src/components/Dialogs/SettingsDialog.tsx @@ -21,68 +21,85 @@ export interface SettingsDialogRef { close: () => void; } -const SettingsDialog = forwardRef((_, ref) => { - const [display, setDisplay] = useState(false); - const [settingsData, setSettingsData] = useState({ - northboundUrl: "", - southboundUrl: "", - pathToValue: "/", - }); - const [isValid, setIsValid] = useState(true); +interface SettingsDialogProps { + jsonIndentation: 2 | 4; + onJsonIndentationChange: (value: 2 | 4) => void; +} - useImperativeHandle(ref, () => { - return { - openModal: () => open(), - close: () => close(), - }; - }); +const SettingsDialog = forwardRef( + ({ jsonIndentation, onJsonIndentationChange }, ref) => { + const [display, setDisplay] = useState(false); + const [settingsData, setSettingsData] = useState({ + northboundUrl: "", + southboundUrl: "", + pathToValue: "/", + }); + const [draftJsonIndentation, setDraftJsonIndentation] = useState<2 | 4>( + jsonIndentation + ); + const [isValid, setIsValid] = useState(true); - const open = () => { - setDisplay(true); - setSettingsData({ - northboundUrl: getLocalStorage("northbound") || "", - southboundUrl: getLocalStorage("southbound") || "", - pathToValue: getLocalStorage("valuePath") || "/", + useImperativeHandle(ref, () => { + return { + openModal: () => open(), + close: () => close(), + }; }); - }; - const close = async () => { - setDisplay(false); - }; + const open = () => { + setDisplay(true); + setSettingsData({ + northboundUrl: getLocalStorage("northbound") || "", + southboundUrl: getLocalStorage("southbound") || "", + pathToValue: getLocalStorage("valuePath") || "/", + }); + setDraftJsonIndentation(jsonIndentation); + }; - const handleSubmit = () => { - if (isValid) { - setLocalStorage(settingsData.northboundUrl, "northbound"); - setLocalStorage(settingsData.southboundUrl, "southbound"); - setLocalStorage(settingsData.pathToValue, "valuePath"); - close(); - } - }; + const close = async () => { + setDisplay(false); + }; - const handleSettingsChange = (data: SettingsData, valid: boolean) => { - setSettingsData(data); - setIsValid(valid); - }; + const handleSubmit = () => { + if (isValid) { + setLocalStorage(settingsData.northboundUrl, "northbound"); + setLocalStorage(settingsData.southboundUrl, "southbound"); + setLocalStorage(settingsData.pathToValue, "valuePath"); + onJsonIndentationChange(draftJsonIndentation); + close(); + } + }; - if (display) { - return ReactDOM.createPortal( - - - , - document.getElementById("modal-root") as HTMLElement - ); - } + const handleSettingsChange = (data: SettingsData, valid: boolean) => { + setSettingsData(data); + setIsValid(valid); + }; - return null; -}); + if (display) { + return ReactDOM.createPortal( + + + , + document.getElementById("modal-root") as HTMLElement + ); + } + + return null; + } +); SettingsDialog.displayName = "SettingsDialog"; export default SettingsDialog; diff --git a/src/components/Editor/JsonEditor.tsx b/src/components/Editor/JsonEditor.tsx index 9e92eff6..33c16806 100644 --- a/src/components/Editor/JsonEditor.tsx +++ b/src/components/Editor/JsonEditor.tsx @@ -22,21 +22,11 @@ import React, { import ediTDorContext from "../../context/ediTDorContext"; import { changeBetweenTd } from "../../utils/tdOperations"; import { editor } from "monaco-editor"; -import { IValidationMessage } from "../../types/context"; type SchemaMapMessage = Map>; // List of all Options can be found here: https://microsoft.github.io/monaco-editor/docs.html#interfaces/editor.IStandaloneEditorConstructionOptions.html -const editorOptions: editor.IStandaloneEditorConstructionOptions = { - selectOnLineNumbers: true, - automaticLayout: true, - lineDecorationsWidth: 20, - tabSize: 2, - insertSpaces: true, -}; -// delay function that executes the callback once it hasn't been called for -// at least x ms. let timeoutId: ReturnType; const delay = (fn: (text: string) => void, text: string, ms: number) => { clearTimeout(timeoutId); @@ -45,15 +35,18 @@ const delay = (fn: (text: string) => void, text: string, ms: number) => { type JsonEditorProps = { editorRef?: React.MutableRefObject; + jsonIndentation: 2 | 4; }; interface JsonSchemaEntry { schemaUri: string; schema: Record; } -const JsonEditor: React.FC = ({ editorRef }) => { +const JsonEditor: React.FC = ({ + editorRef, + jsonIndentation, +}) => { const context = useContext(ediTDorContext); - const [schemas] = useState([]); const [proxy, setProxy] = useState(undefined); const editorInstance = useRef(null); @@ -85,6 +78,8 @@ const JsonEditor: React.FC = ({ editorRef }) => { ); useEffect(() => { + if (!proxy) return; + const updateMonacoSchemas = (schemaMap: SchemaMapMessage) => { proxy.splice(0, proxy.length); @@ -165,10 +160,8 @@ const JsonEditor: React.FC = ({ editorRef }) => { setProxy(proxy); }; - const onChange: OnChange = async (editorText: string | undefined) => { - if (!editorText) { - return; - } + const onChange: OnChange = (editorText: string | undefined) => { + if (!editorText) return; let validate: IValidationMessage = { report: { json: null, @@ -199,15 +192,19 @@ const JsonEditor: React.FC = ({ editorRef }) => { }, customMessage: "", }; + try { JSON.parse(editorText); - context.updateOfflineTD(editorText); + if (editorText !== context.offlineTD) { + context.updateOfflineTD(editorText); + } + + setLocalTextState(editorText); context.updateValidationMessage(validate); + + delay(messageWorkers, editorText, 500); } catch (error) { - let message: string = - "Invalid JSON: " + - (error instanceof Error ? error.message : String(error)); validate.report.json = "failed"; context.updateValidationMessage(validate); setLocalTextState(editorText); @@ -242,6 +239,19 @@ const JsonEditor: React.FC = ({ editorRef }) => { } }, [context.linkedTd, context.offlineTD]); + useEffect(() => { + try { + const parsed = JSON.parse(context.offlineTD); + const formatted = JSON.stringify(parsed, null, jsonIndentation); + + if (formatted !== context.offlineTD) { + context.updateOfflineTD(formatted); + } + } catch { + // ignore invalid JSON + } + }, [jsonIndentation, context.offlineTD]); + const changeLinkedTd = async () => { let href = (document.getElementById("linkedTd") as HTMLSelectElement).value; changeBetweenTd(context, href); @@ -255,6 +265,18 @@ const JsonEditor: React.FC = ({ editorRef }) => { }); }; + const editorOptions = useMemo( + () => ({ + selectOnLineNumbers: true, + automaticLayout: true, + lineDecorationsWidth: 20, + tabSize: jsonIndentation, + insertSpaces: true, + detectIndentation: false, + }), + [jsonIndentation] + ); + return ( <>
diff --git a/src/components/base/Dropdown.tsx b/src/components/base/Dropdown.tsx index 74d2fe5f..34c04f35 100644 --- a/src/components/base/Dropdown.tsx +++ b/src/components/base/Dropdown.tsx @@ -17,6 +17,8 @@ interface IDropdownProps { id: string; label: string; options: string[]; + value?: string; + onChange?: (event: React.ChangeEvent) => void; className?: string; } @@ -33,6 +35,8 @@ const Dropdown: React.FC = (props) => {