From 97ef7a53feccafcad433bfb4b81b2dd0f7054491 Mon Sep 17 00:00:00 2001 From: Steven Sherry Date: Fri, 8 Dec 2023 09:26:24 -0600 Subject: [PATCH] feat: Monaco (#1) * feat: Replace existing editor with monaco * feat: Plugin responses are now pretty printed. * feat: Better dark mode support. * fix: Addressed headers not respecting safe area insets. * fix: Modal was actually being retained. It is now properly disposed. --- package-lock.json | 64 +++++++++-- package.json | 17 +-- src/components/CallMethodModal.tsx | 168 +++++++++++++-------------- src/components/CapacitorPlugins.tsx | 108 ++++++++++-------- src/components/InitialContext.tsx | 25 ++-- src/components/PortalsDebug.tsx | 10 +- src/components/PubSubTest.tsx | 171 ++++++++++++++-------------- src/components/TabPage.tsx | 29 +++++ 8 files changed, 337 insertions(+), 255 deletions(-) create mode 100644 src/components/TabPage.tsx diff --git a/package-lock.json b/package-lock.json index 94c2a61..0dbf8af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,18 +8,20 @@ "name": "portals-debug-web-app", "version": "0.0.1", "dependencies": { - "@capacitor/app": "5.0.6", - "@capacitor/core": "5.2.2", - "@capacitor/haptics": "5.0.6", - "@capacitor/keyboard": "5.0.6", - "@capacitor/status-bar": "5.0.6", + "@capacitor/app": "^5.0.6", + "@capacitor/core": "^5.2.2", + "@capacitor/haptics": "^5.0.6", + "@capacitor/keyboard": "^5.0.6", + "@capacitor/status-bar": "^5.0.6", "@ionic/core": "^7.3.0", "@ionic/portals": "^0.8.1", "@ionic/react": "^7.0.0", "@ionic/react-router": "^7.0.0", + "@monaco-editor/react": "^4.6.0", "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", "ionicons": "^7.0.0", + "monaco-editor": "^0.44.0", "react": "^18.2.0", "react-ace": "^10.1.0", "react-dom": "^18.2.0", @@ -27,7 +29,8 @@ "react-router-dom": "^5.3.4" }, "devDependencies": { - "@capacitor/cli": "5.2.2", + "@capacitor/cli": "^5.2.2", + "@capacitor/ios": "^5.5.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", @@ -1967,9 +1970,9 @@ } }, "node_modules/@capacitor/core": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.2.2.tgz", - "integrity": "sha512-3jKECZC5+YD2rljMZm1e/K3AYyoxUmLDZCyofTPbRYPBSI0wJh5ZCkX+XIGzNM0o/Wokl3Voa1JB8xsLC0MPxA==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.5.0.tgz", + "integrity": "sha512-w59io0ctwnb7JRng7yO2H0YLHG8uz7XARUugRfp5aYTNiG55FqdSmSMOOqGCMPRg4sEnKjJTvAa4ImCYh3Kk1w==", "dependencies": { "tslib": "^2.1.0" } @@ -1982,6 +1985,15 @@ "@capacitor/core": "^5.0.0" } }, + "node_modules/@capacitor/ios": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-5.5.0.tgz", + "integrity": "sha512-kApjblUOlLY91+1OrWIx+vaVfEN1bl1kh1jSgK1/IdGfS9kFs1hxUE/okRoLJGT6tYeSOa6GA/19MLOs64wb6A==", + "dev": true, + "peerDependencies": { + "@capacitor/core": "^5.5.0" + } + }, "node_modules/@capacitor/keyboard": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-5.0.6.tgz", @@ -2857,6 +2869,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -7077,6 +7113,11 @@ "ufo": "^1.1.2" } }, + "node_modules/monaco-editor": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz", + "integrity": "sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -8405,6 +8446,11 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/std-env": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz", diff --git a/package.json b/package.json index 4ab68a4..9bf79b8 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.1", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --host", "build": "tsc && vite build", "preview": "vite preview", "test.e2e": "cypress run", @@ -12,18 +12,20 @@ "lint": "eslint" }, "dependencies": { - "@capacitor/app": "5.0.6", - "@capacitor/core": "5.2.2", - "@capacitor/haptics": "5.0.6", - "@capacitor/keyboard": "5.0.6", - "@capacitor/status-bar": "5.0.6", + "@capacitor/app": "^5.0.6", + "@capacitor/core": "^5.2.2", + "@capacitor/haptics": "^5.0.6", + "@capacitor/keyboard": "^5.0.6", + "@capacitor/status-bar": "^5.0.6", "@ionic/core": "^7.3.0", "@ionic/portals": "^0.8.1", "@ionic/react": "^7.0.0", "@ionic/react-router": "^7.0.0", + "@monaco-editor/react": "^4.6.0", "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", "ionicons": "^7.0.0", + "monaco-editor": "^0.44.0", "react": "^18.2.0", "react-ace": "^10.1.0", "react-dom": "^18.2.0", @@ -31,7 +33,8 @@ "react-router-dom": "^5.3.4" }, "devDependencies": { - "@capacitor/cli": "5.2.2", + "@capacitor/cli": "^5.2.2", + "@capacitor/ios": "^5.5.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", diff --git a/src/components/CallMethodModal.tsx b/src/components/CallMethodModal.tsx index 8cf55b2..abf0932 100644 --- a/src/components/CallMethodModal.tsx +++ b/src/components/CallMethodModal.tsx @@ -1,54 +1,49 @@ import { - IonModal, IonContent, - IonListHeader, IonButton, IonHeader, - IonToolbar, - IonTitle, - IonButtons, IonIcon, IonItem, - IonTextarea, IonLabel, IonList, } from "@ionic/react"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { - chevronBack, alertCircleOutline, checkmarkCircleOutline, } from "ionicons/icons"; -import AceEditor from "react-ace"; -import "ace-builds/src-noconflict/mode-json"; -import "ace-builds/src-noconflict/theme-github"; +import Editor, { useMonaco } from "@monaco-editor/react"; interface CallMethodModalProps { - showModal: boolean; pluginName: string; methodName: string; - onCloseModal: () => void; } +const darkModeMatch = window.matchMedia("(prefers-color-scheme: dark)"); + const CallMethodModal: React.FC = ({ - showModal, pluginName, methodName, - onCloseModal, }) => { - const [code, setCode] = useState(""); + const codeRef = useRef("{}"); + const monaco = useMonaco(); const [result, setResult] = useState(""); const [error, setError] = useState(""); + useEffect(() => { + monaco?.editor.setTheme(darkModeMatch.matches ? "vs-dark" : "vs") + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (_e) => { + monaco?.editor.setTheme(darkModeMatch.matches ? "vs-dark" : "vs") + }); + }, []); + const handleCallMethod = async () => { setResult(""); setError(""); try { // @ts-ignore - const res = await window.Capacitor.Plugins[pluginName][methodName]( - JSON.parse(code || '""') - ); - setResult(JSON.stringify(res)); + const res = await window.Capacitor.Plugins[pluginName][methodName](JSON.parse(codeRef.current)); + setResult(JSON.stringify(res, null, 2)); } catch (error) { if (error instanceof Error) { setError(error.message); @@ -58,82 +53,81 @@ const CallMethodModal: React.FC = ({ } }; - const handleCloseModal = () => { - setCode(""); - setResult(""); - setError(""); - onCloseModal(); - }; - return ( - - - - - - - Back - - - {methodName} - - Execute - - - - - Argument: - - - setCode(e)} - setOptions={{ - showLineNumbers: true, - tabSize: 2, - useWorker: false, - }} - style={{ marginTop: 10 }} - /> + + Argument: + { + monaco.editor.setTheme(darkModeMatch.matches ? "vs-dark" : "vs") + }} + height="200px" + defaultLanguage="json" + defaultValue={codeRef.current} + onChange={(e) => codeRef.current = e ?? ""} + options={{ + accessibilitySupport: "off", + theme: darkModeMatch.matches ? "vs-dark" : "vs", + lineDecorationsWidth: 0, + glyphMargin: false, + inlineSuggest: { + enabled: false + }, + quickSuggestions: false, + suggestOnTriggerCharacters: false, + parameterHints: { + enabled: false + }, + inlayHints: { + enabled: "off" + }, + lineNumbersMinChars: 2, + folding: false, + lineNumbers: "on", + scrollbar: { + vertical: "hidden", + horizontal: "hidden", + }, + minimap: { + enabled: false, + }, + }} + /> + + Execute {methodName} + + + {error && ( + + + + {error} + - - Execute {methodName} - - - Result: - - {error && ( - - - - {error} - - - )} - {result && ( + )} + {result && ( + <> - {result} + Success + + + + {result} + - )} - - - + + )} + + ); }; diff --git a/src/components/CapacitorPlugins.tsx b/src/components/CapacitorPlugins.tsx index 9ff488a..a430cc3 100644 --- a/src/components/CapacitorPlugins.tsx +++ b/src/components/CapacitorPlugins.tsx @@ -1,15 +1,23 @@ import { IonAccordion, IonAccordionGroup, - IonContent, + IonHeader, + IonButton, + IonToolbar, + IonButtons, IonItem, IonLabel, - IonListHeader, - IonPage, + IonModal, IonText, + IonTitle, + IonIcon } from "@ionic/react"; import { useEffect, useState } from "react"; +import { + chevronBack, +} from "ionicons/icons"; import CallMethodModal from "./CallMethodModal"; +import TabPage from "./TabPage"; interface Plugin { name: string; @@ -69,54 +77,60 @@ const CapacitorPlugins = () => { }, []); return ( - - - -

Capacitor Plugins

-
- - {plugins.map((plugin) => ( - - - {plugin.name} - {defaultPlugins.includes(plugin.name) && ( - - {`(Default)`} - - )} - -
- {plugin.methods.map( - (method) => - !ignoredMethods.includes(method.name) && ( - { - setMethodName(method.name), - setPluginName(plugin.name); - setShowModal(true); - }} - > - {method.name} - - ) - )} -
-
- ))} -
+ + + {plugins.map((plugin) => ( + + + {plugin.name} + {defaultPlugins.includes(plugin.name) && ( + + {`(Default)`} + + )} + +
+ {plugin.methods.map( + (method) => + !ignoredMethods.includes(method.name) && ( + { + setMethodName(method.name), + setPluginName(plugin.name); + setShowModal(true); + }} + > + {method.name} + + ) + )} +
+
+ ))} +
+ + + + + setShowModal(false)}> + + Back + + + {methodName} + + setShowModal(false)} /> -
-
+ + ); }; diff --git a/src/components/InitialContext.tsx b/src/components/InitialContext.tsx index b930e80..5ee7246 100644 --- a/src/components/InitialContext.tsx +++ b/src/components/InitialContext.tsx @@ -1,29 +1,24 @@ import { getInitialContext } from "@ionic/portals"; import { IonItem, - IonContent, - IonPage, - IonListHeader, IonLabel, IonList, } from "@ionic/react"; +import TabPage from "./TabPage"; const InitialContext = () => { const initialContext = getInitialContext(); return ( - - - Initial Context - - - - {JSON.stringify(initialContext, null, 2)} - - - - - + + + + + {initialContext ? {JSON.stringify(initialContext, null, 2)} : "No initial context found"} + + + + ); }; diff --git a/src/components/PortalsDebug.tsx b/src/components/PortalsDebug.tsx index 8c847b5..91b45b8 100644 --- a/src/components/PortalsDebug.tsx +++ b/src/components/PortalsDebug.tsx @@ -25,13 +25,17 @@ const PortalsDebug = () => { } + component={InitialContext} + exact={true} + /> + - } exact={true} /> } + component={CapacitorPlugins} exact={true} /> diff --git a/src/components/PubSubTest.tsx b/src/components/PubSubTest.tsx index cf51dcb..76b6088 100644 --- a/src/components/PubSubTest.tsx +++ b/src/components/PubSubTest.tsx @@ -1,17 +1,16 @@ -import { useState } from "react"; +import { PortalMessage, publish, subscribe } from "@ionic/portals"; import { IonAccordion, IonAccordionGroup, IonButton, - IonContent, IonInput, IonItem, IonLabel, IonList, IonListHeader, - IonPage, } from "@ionic/react"; -import { PortalMessage, subscribe, publish } from "@ionic/portals"; +import { useState } from "react"; +import TabPage from "./TabPage"; interface SubscriptionMessage { id: number; @@ -77,91 +76,89 @@ const PubSubTest = () => { }; return ( - - - -

Publish

-
- - - setPublishTopic(e.detail.value!)} - /> - { - setPublishData(e.detail.value!); - }} - /> + + +

Publish

+
+ + + setPublishTopic(e.detail.value!)} + /> + { + setPublishData(e.detail.value!); + }} + /> + + + Publish + + + +

Subscribe

+
+ + + setSubscribeTopic(e.detail.value!)} + /> + + + Subscribe + + + + + + Subscriptions - - Publish - -
- -

Subscribe

-
- - - setSubscribeTopic(e.detail.value!)} - /> +
+ {subscriptions.map((subscription) => ( + + + {subscription.topic} + + + ))} +
+ + + + Subscription Messages - - Subscribe - -
- - - - Subscriptions - -
- {subscriptions.map((subscription) => ( - - - {subscription.topic} - - - ))} -
-
- - - Subscription Messages - -
- {subscriptionMessages.map((message) => ( - - - {message.date} | topic: {message.portalMessage.topic}, data:{" "} - {message.portalMessage.data} - - - ))} -
-
-
-
-
+
+ {subscriptionMessages.map((message) => ( + + + {message.date} | topic: {message.portalMessage.topic}, data:{" "} + {message.portalMessage.data} + + + ))} +
+ + + ); }; diff --git a/src/components/TabPage.tsx b/src/components/TabPage.tsx new file mode 100644 index 0000000..55cdee1 --- /dev/null +++ b/src/components/TabPage.tsx @@ -0,0 +1,29 @@ +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import { ReactNode } from 'react'; + +type TabPageProps = { + title: string; + children: ReactNode; +} + +const TabPage = ({ title, children }: TabPageProps) => { + return ( + + + + {title} + + + + + + {title} + + + {children} + + + ); +}; + +export default TabPage;