Skip to content

Commit 861035c

Browse files
authored
Feat transfer data between front ends (#215)
* feat add postMessage logic Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> * fix enhance commnunication and user experience Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> * feat improve error handling, typescript checks, rendering optimization Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> * feat add component testing for App.tsx Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> * feat add integration test for App.tsx Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> * refactor integration tests, increase coverage Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> * add integration tests, fix bug found on integrationt test Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> * docs add postMessage Api communication instructions to readme Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> * feat remove restrictionin origin Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> * test fix integration tests Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> * fix output of the yarn:test command Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> * test add act for dispatchEvents Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> --------- Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com>
1 parent 5bb241d commit 861035c

10 files changed

Lines changed: 1477 additions & 45 deletions

README.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ Please follow our [contribution guide](./CONTRIBUTING.md).
6060

6161
In the development environment it is possible to use [react scan](https://react-scan.com/) to detect performance issues by analyzing the pop-up on the bottom right corner. The complete documentation is available [here](https://github.com/aidenybai/react-scan#readme).
6262

63+
## Features
64+
6365
### Using the Catalog Contribution Feature
6466

6567
You will need a [Thing Model Catalog](https://github.com/wot-oss/tmc) running somewhere. If you want to host it yourself, use the command-line interface to run one in the terminal using the following instructions:
@@ -85,7 +87,7 @@ A local repository folder will be created inside the tm-catalog directory
8587
tmc repo remove <nameOfCatalog>
8688
```
8789

88-
### Send TD feature
90+
### Using Send TD feature
8991

9092
#### Northbound and Southbound URLs
9193

@@ -122,7 +124,7 @@ Afterwards, if the service proxies the TD, ediTDor can fetch the proxied TD cont
122124
123125
The proxy uses the TD sent to its southbound API endpoint to communicate with a Thing. This way, you can interact with a non-HTTP Thing from your ediTDor.
124126

125-
### Automatically reading URL parameters
127+
### Using URL query parameters feature
126128

127129
The ediTDor has the functionality to automatically set the following list of variables from a URL with query parameters:
128130

@@ -147,6 +149,30 @@ Example of use:
147149

148150
http://localhost:5173/?northbound=http://localhost:8080&southbound=http://github.com&valuePath=/value
149151

152+
### Using postMessage API communication feature
153+
154+
The ediTDor can receive a Thing Description from another web application through the browser `postMessage` API (Documentation [here](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)). When ediTDor is opened by another application in a new window or tab, it sends a readiness message back to the opener:
155+
156+
```json
157+
{ "type": "EDITDOR_READY" }
158+
```
159+
160+
After that, the parent application can send a Thing Description to ediTDor with a message in the following format:
161+
162+
```json
163+
{
164+
"type": "LOAD_TD",
165+
"description": "Imported TD",
166+
"payload": "{ \"@context\": \"https://www.w3.org/ns/wot-next/td\", \"title\": \"MyThing\" }"
167+
}
168+
```
169+
170+
- **type** must be LOAD_TD
171+
- **description** is a string to show in the confirmation dialog, e.g. title, id
172+
- **payload** must be a valid JSON string containing the Thing Description
173+
174+
When a valid message is received, ediTDor shows a confirmation dialog before loading the TD into the editor. If the payload is not valid JSON, an error message is shown instead.
175+
150176
## Implemented Features:
151177

152178
- JSON Editor with JSON Schema support for TD (Autocompletion, JSON Schema Validation)

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"@babel/core": "^7.26.10",
5757
"@babel/eslint-parser": "^7.27.0",
5858
"@babel/preset-react": "^7.26.3",
59+
"@testing-library/dom": "^10.4.1",
5960
"@testing-library/jest-dom": "^6.8.0",
6061
"@testing-library/react": "^16.3.0",
6162
"@types/babel__core": "^7",

src/App.tsx

Lines changed: 102 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,20 @@ import "./App.css";
1919
import AppFooter from "./components/App/AppFooter";
2020
import AppHeader from "./components/App/AppHeader";
2121
import { Container, Section, Bar } from "@column-resizer/react";
22-
import { RefreshCw } from "react-feather";
2322
import { decompressSharedTd } from "./share";
2423
import { editor } from "monaco-editor";
25-
import BaseButton from "./components/TDViewer/base/BaseButton";
2624
import ErrorDialog from "./components/Dialogs/ErrorDialog";
25+
import DialogTemplate from "./components/Dialogs/DialogTemplate";
26+
27+
type ReadyMessage = {
28+
type: "EDITDOR_READY";
29+
};
30+
31+
type LoadTdMessage = {
32+
type: "LOAD_TD";
33+
description: string;
34+
payload: string;
35+
};
2736

2837
const GlobalStateWrapper = () => {
2938
return (
@@ -38,17 +47,15 @@ const BREAKPOINTS = {
3847
SMALL: 850,
3948
};
4049

41-
// The useEffect hook for checking the URI was called twice somehow.
42-
// This variable prevents the callback from being executed twice.
43-
let checkedUrl = false;
44-
45-
const App: React.FC = () => {
50+
const App = () => {
4651
const context = useContext(ediTDorContext);
4752

4853
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
49-
const [doShowJSON, setDoShowJSON] = useState(false);
5054
const [customBreakpointsState, setCustomBreakpointsState] = useState(0);
5155
const tdViewerRef = useRef<HTMLDivElement>(null);
56+
const [pendingTd, setPendingTd] = useState<string>("");
57+
const [pendingTitle, setPendingTitle] = useState<string>("");
58+
const [isOpen, setIsOpen] = useState(false);
5259

5360
const [errorDisplay, setErrorDisplay] = useState<{
5461
state: boolean;
@@ -74,8 +81,18 @@ const App: React.FC = () => {
7481
}
7582
};
7683

77-
const handleToggleJSON = () => {
78-
setDoShowJSON((prev) => !prev);
84+
const isLoadTdMessage = (value: unknown): value is LoadTdMessage => {
85+
if (typeof value !== "object" || value === null) {
86+
return false;
87+
}
88+
89+
const message = value as Record<string, unknown>;
90+
91+
return (
92+
message.type === "LOAD_TD" &&
93+
typeof message.description === "string" &&
94+
typeof message.payload === "string"
95+
);
7996
};
8097

8198
useEffect(() => {
@@ -93,24 +110,25 @@ const App: React.FC = () => {
93110
processedValue = value + "/";
94111
}
95112

96-
localStorage.setItem(param, processedValue);
113+
try {
114+
localStorage.setItem(param, processedValue);
115+
} catch {
116+
showError("Failed to persist URL parameters to local storage.");
117+
}
97118
}
98119
});
99-
}, [window.location.search]);
120+
}, []);
100121

101122
useEffect(() => {
102-
if (
103-
checkedUrl ||
104-
(window.location.search.indexOf("td") <= -1 &&
105-
window.location.search.indexOf("proxyEndpoint") <= -1 &&
106-
window.location.search.indexOf("localstorage") <= -1 &&
107-
window.location.search.indexOf("southboundTdId") <= -1)
108-
) {
123+
const url = new URL(window.location.href);
124+
125+
const hasRelevantParam =
126+
url.searchParams.has("td") || url.searchParams.has("localstorage");
127+
128+
if (!hasRelevantParam) {
109129
return;
110130
}
111-
checkedUrl = true;
112131

113-
const url = new URL(window.location.href);
114132
const compressedTd = url.searchParams.get("td");
115133
if (compressedTd !== null) {
116134
const td = decompressSharedTd(compressedTd);
@@ -125,23 +143,23 @@ const App: React.FC = () => {
125143
}
126144

127145
if (url.searchParams.has("localstorage")) {
128-
let td = localStorage.getItem("td");
129-
if (!td) {
146+
const storedTd = localStorage.getItem("td");
147+
if (!storedTd) {
130148
showError("Request to read TD from local storage failed.");
131149
return;
132150
}
133151

134152
try {
135-
td = JSON.parse(td);
136-
context.updateOfflineTD(JSON.stringify(td, null, 2));
137-
} catch (e) {
138-
context.updateOfflineTD(td);
153+
const parsedTd: ThingDescription = JSON.parse(storedTd);
154+
context.updateOfflineTD(JSON.stringify(parsedTd, null, 2));
155+
} catch (error) {
156+
context.updateOfflineTD(storedTd);
139157
showError(
140-
`Tried to JSON parse the TD from local storage, but failed: ${e}`
158+
`Tried to JSON parse the TD from local storage, but failed: ${error}`
141159
);
142160
}
143161
}
144-
}, [context]);
162+
}, []);
145163

146164
useEffect(() => {
147165
if (!tdViewerRef.current) return;
@@ -163,6 +181,52 @@ const App: React.FC = () => {
163181
return () => resizeObserver.disconnect();
164182
}, []);
165183

184+
useEffect(() => {
185+
const readyMessage: ReadyMessage = {
186+
type: "EDITDOR_READY",
187+
};
188+
189+
const handleMessage = (event: MessageEvent) => {
190+
if (event.source !== window.opener) {
191+
return;
192+
}
193+
194+
if (!isLoadTdMessage(event.data)) {
195+
return;
196+
}
197+
198+
try {
199+
JSON.parse(event.data.payload);
200+
setPendingTitle(event.data.description);
201+
setPendingTd(event.data.payload);
202+
setIsOpen(true);
203+
} catch {
204+
showError("Received invalid JSON from the other application.");
205+
}
206+
};
207+
208+
window.addEventListener("message", handleMessage);
209+
210+
if (window.opener) {
211+
window.opener.postMessage(readyMessage, "*");
212+
}
213+
214+
return () => {
215+
window.removeEventListener("message", handleMessage);
216+
};
217+
}, []);
218+
219+
const onHandleEventRightButton = () => {
220+
context.updateOfflineTD(pendingTd);
221+
setPendingTd("");
222+
setIsOpen(false);
223+
};
224+
225+
const onHandleEventLeftButton = () => {
226+
setPendingTd("");
227+
setIsOpen(false);
228+
};
229+
166230
return (
167231
<main className="flex max-h-screen w-screen flex-col">
168232
<AppHeader></AppHeader>
@@ -187,15 +251,6 @@ const App: React.FC = () => {
187251
<Section className="w-full md:w-5/12">
188252
<JsonEditor editorRef={editorRef} />
189253
</Section>
190-
191-
<BaseButton
192-
type="button"
193-
className="fixed bottom-12 right-2 z-10 rounded-full bg-blue-500 p-4"
194-
onClick={handleToggleJSON}
195-
variant="empty"
196-
>
197-
<RefreshCw color="white" />
198-
</BaseButton>
199254
</Container>
200255
</div>
201256
<div className="fixed bottom-0 w-screen">
@@ -207,6 +262,15 @@ const App: React.FC = () => {
207262
onClose={() => setErrorDisplay({ state: false, message: "" })}
208263
errorMessage={errorDisplay.message}
209264
/>
265+
{isOpen && (
266+
<DialogTemplate
267+
title={`The Thing Description "${pendingTitle}" was received from the other application.`}
268+
description={`Do you wish to open the following TD in the editdor?`}
269+
onHandleEventRightButton={onHandleEventRightButton}
270+
rightButton="Confirm"
271+
onHandleEventLeftButton={onHandleEventLeftButton}
272+
></DialogTemplate>
273+
)}
210274
</main>
211275
);
212276
};

src/context/editorReducers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ const updateOfflineTDReducer = (
9797
try {
9898
parsedTD = JSON.parse(offlineTD);
9999
} catch (e) {
100-
console.error((e as Error).message);
100+
// console.error((e as Error).message);
101101
return {
102102
...state,
103103
offlineTD: offlineTD,

0 commit comments

Comments
 (0)