Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ Please follow our [contribution guide](./CONTRIBUTING.md).

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).

## Features

### Using the Catalog Contribution Feature

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:
Expand All @@ -85,7 +87,7 @@ A local repository folder will be created inside the tm-catalog directory
tmc repo remove <nameOfCatalog>
```

### Send TD feature
### Using Send TD feature

#### Northbound and Southbound URLs

Expand Down Expand Up @@ -122,7 +124,7 @@ Afterwards, if the service proxies the TD, ediTDor can fetch the proxied TD cont

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.

### Automatically reading URL parameters
### Using URL query parameters feature

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

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

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

### Using postMessage API communication feature

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:

```json
{ "type": "EDITDOR_READY" }
```

After that, the parent application can send a Thing Description to ediTDor with a message in the following format:

```json
{
"type": "LOAD_TD",
"description": "Imported TD",
"payload": "{ \"@context\": \"https://www.w3.org/ns/wot-next/td\", \"title\": \"MyThing\" }"
}
```

- **type** must be LOAD_TD
- **description** is a string to show in the confirmation dialog, e.g. title, id
- **payload** must be a valid JSON string containing the Thing Description

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.

## Implemented Features:

- JSON Editor with JSON Schema support for TD (Autocompletion, JSON Schema Validation)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@babel/core": "^7.26.10",
"@babel/eslint-parser": "^7.27.0",
"@babel/preset-react": "^7.26.3",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@types/babel__core": "^7",
Expand Down
140 changes: 102 additions & 38 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,20 @@ import "./App.css";
import AppFooter from "./components/App/AppFooter";
import AppHeader from "./components/App/AppHeader";
import { Container, Section, Bar } from "@column-resizer/react";
import { RefreshCw } from "react-feather";
import { decompressSharedTd } from "./share";
import { editor } from "monaco-editor";
import BaseButton from "./components/TDViewer/base/BaseButton";
import ErrorDialog from "./components/Dialogs/ErrorDialog";
import DialogTemplate from "./components/Dialogs/DialogTemplate";

type ReadyMessage = {
type: "EDITDOR_READY";
};

type LoadTdMessage = {
type: "LOAD_TD";
description: string;
payload: string;
};

const GlobalStateWrapper = () => {
return (
Expand All @@ -38,17 +47,15 @@ const BREAKPOINTS = {
SMALL: 850,
};

// The useEffect hook for checking the URI was called twice somehow.
// This variable prevents the callback from being executed twice.
let checkedUrl = false;

const App: React.FC = () => {
const App = () => {
const context = useContext(ediTDorContext);

const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const [doShowJSON, setDoShowJSON] = useState(false);
const [customBreakpointsState, setCustomBreakpointsState] = useState(0);
const tdViewerRef = useRef<HTMLDivElement>(null);
const [pendingTd, setPendingTd] = useState<string>("");
const [pendingTitle, setPendingTitle] = useState<string>("");
const [isOpen, setIsOpen] = useState(false);

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

const handleToggleJSON = () => {
setDoShowJSON((prev) => !prev);
const isLoadTdMessage = (value: unknown): value is LoadTdMessage => {
if (typeof value !== "object" || value === null) {
return false;
}

const message = value as Record<string, unknown>;

return (
message.type === "LOAD_TD" &&
typeof message.description === "string" &&
typeof message.payload === "string"
);
};

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

localStorage.setItem(param, processedValue);
try {
localStorage.setItem(param, processedValue);
} catch {
showError("Failed to persist URL parameters to local storage.");
}
}
});
}, [window.location.search]);
}, []);

useEffect(() => {
if (
checkedUrl ||
(window.location.search.indexOf("td") <= -1 &&
window.location.search.indexOf("proxyEndpoint") <= -1 &&
window.location.search.indexOf("localstorage") <= -1 &&
window.location.search.indexOf("southboundTdId") <= -1)
) {
const url = new URL(window.location.href);

const hasRelevantParam =
url.searchParams.has("td") || url.searchParams.has("localstorage");

if (!hasRelevantParam) {
return;
}
checkedUrl = true;

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

if (url.searchParams.has("localstorage")) {
let td = localStorage.getItem("td");
if (!td) {
const storedTd = localStorage.getItem("td");
if (!storedTd) {
showError("Request to read TD from local storage failed.");
return;
}

try {
td = JSON.parse(td);
context.updateOfflineTD(JSON.stringify(td, null, 2));
} catch (e) {
context.updateOfflineTD(td);
const parsedTd: ThingDescription = JSON.parse(storedTd);
context.updateOfflineTD(JSON.stringify(parsedTd, null, 2));
} catch (error) {
context.updateOfflineTD(storedTd);
showError(
`Tried to JSON parse the TD from local storage, but failed: ${e}`
`Tried to JSON parse the TD from local storage, but failed: ${error}`
);
}
}
}, [context]);
}, []);

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

useEffect(() => {
const readyMessage: ReadyMessage = {
type: "EDITDOR_READY",
};

const handleMessage = (event: MessageEvent) => {
if (event.source !== window.opener) {
return;
}

if (!isLoadTdMessage(event.data)) {
return;
}

try {
JSON.parse(event.data.payload);
setPendingTitle(event.data.description);
setPendingTd(event.data.payload);
setIsOpen(true);
} catch {
showError("Received invalid JSON from the other application.");
}
};

window.addEventListener("message", handleMessage);

if (window.opener) {
window.opener.postMessage(readyMessage, "*");
}

return () => {
window.removeEventListener("message", handleMessage);
};
}, []);

const onHandleEventRightButton = () => {
context.updateOfflineTD(pendingTd);
setPendingTd("");
setIsOpen(false);
};

const onHandleEventLeftButton = () => {
setPendingTd("");
setIsOpen(false);
};

return (
<main className="flex max-h-screen w-screen flex-col">
<AppHeader></AppHeader>
Expand All @@ -187,15 +251,6 @@ const App: React.FC = () => {
<Section className="w-full md:w-5/12">
<JsonEditor editorRef={editorRef} />
</Section>

<BaseButton
type="button"
className="fixed bottom-12 right-2 z-10 rounded-full bg-blue-500 p-4"
onClick={handleToggleJSON}
variant="empty"
>
<RefreshCw color="white" />
</BaseButton>
</Container>
</div>
<div className="fixed bottom-0 w-screen">
Expand All @@ -207,6 +262,15 @@ const App: React.FC = () => {
onClose={() => setErrorDisplay({ state: false, message: "" })}
errorMessage={errorDisplay.message}
/>
{isOpen && (
<DialogTemplate
title={`The Thing Description "${pendingTitle}" was received from the other application.`}
description={`Do you wish to open the following TD in the editdor?`}
onHandleEventRightButton={onHandleEventRightButton}
rightButton="Confirm"
onHandleEventLeftButton={onHandleEventLeftButton}
></DialogTemplate>
)}
</main>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/context/editorReducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const updateOfflineTDReducer = (
try {
parsedTD = JSON.parse(offlineTD);
} catch (e) {
console.error((e as Error).message);
// console.error((e as Error).message);
return {
...state,
offlineTD: offlineTD,
Expand Down
Loading
Loading