diff --git a/.prettierignore b/.prettierignore index 9b11b569..e4d291d3 100644 --- a/.prettierignore +++ b/.prettierignore @@ -14,3 +14,4 @@ build src/external LICENSE.md +LICENSE.md diff --git a/src/components/App/AddForm.tsx b/src/components/App/AddForm.tsx new file mode 100644 index 00000000..15276726 --- /dev/null +++ b/src/components/App/AddForm.tsx @@ -0,0 +1,92 @@ +/******************************************************************************** + * Copyright (c) 2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ +import React from "react"; +import { OperationsMap, OperationsType } from "../Dialogs/AddFormDialog"; + +interface AddFormProps { + type: OperationsType; + onInputChange: (e: React.ChangeEvent) => void; + operations: (type: OperationsType) => OperationsMap; + defaultValue?: string; + error?: string; +} + +const AddForm: React.FC = ({ + type, + onInputChange, + operations, + defaultValue = "", + error = "", +}) => { + const inputClasses = + "w-full rounded-md border-2 bg-gray-600 p-2 text-white focus:outline-none sm:text-sm " + + (error + ? "border-red-400 focus:border-red-400" + : "border-gray-600 focus:border-blue-500"); + + return ( + <> + +
+
+ {operations(type).map((name) => { + const id = `form-${name}`; + const isInvoke = name === "invokeaction"; + return ( +
+ + +
+ ); + })} +
+
+ +
+ + + {error && ( + + {error} + + )} +
+ + ); +}; + +export default AddForm; diff --git a/src/components/App/AppHeader.tsx b/src/components/App/AppHeader.tsx index 4fdb2a14..507d22cb 100644 --- a/src/components/App/AppHeader.tsx +++ b/src/components/App/AppHeader.tsx @@ -35,9 +35,9 @@ import ConvertTmDialog from "../Dialogs/ConvertTmDialog"; import CreateTdDialog from "../Dialogs/CreateTdDialog"; import SettingsDialog from "../Dialogs/SettingsDialog"; import ShareDialog from "../Dialogs/ShareDialog"; -import ContributeToCatalog from "../Dialogs/ContributeToCatalog"; +import ContributeToCatalog from "../Dialogs/ContributeToCatalogDialog"; import ErrorDialog from "../Dialogs/ErrorDialog"; -import Button from "./Button"; +import Button from "../base/Button"; import SendTDDialog from "../Dialogs/SendTDDialog"; import { getLocalStorage } from "../../services/localStorage"; import type { ThingDescription } from "wot-thing-description-types"; diff --git a/src/components/App/Button.tsx b/src/components/App/Button.tsx deleted file mode 100644 index bd175c85..00000000 --- a/src/components/App/Button.tsx +++ /dev/null @@ -1,16 +0,0 @@ -interface IButtonProps { - onClick: () => void; - children: React.ReactNode; -} - -const Button: React.FC = ({ onClick, children }) => { - return ( - - ); -}; - -export default Button; diff --git a/src/components/App/ContributeToCatalog.tsx b/src/components/App/ContributeToCatalog.tsx new file mode 100644 index 00000000..ef4a385d --- /dev/null +++ b/src/components/App/ContributeToCatalog.tsx @@ -0,0 +1,151 @@ +/******************************************************************************** + * Copyright (c) 2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ +import React from "react"; +import FormMetadata from "../App/FormMetadata"; +import FormInteraction from "../App/FormInteraction"; +import FormSubmission from "../App/FormSubmission"; +import type { FormElementBase } from "wot-thing-description-types"; + +interface ContributeToCatalogProps { + currentStep: number; + + // Step 1 - Metadata + metadata: { + model: string; + author: string; + manufacturer: string; + license: string; + copyrightYear: string; + holder: string; + validation: Validation; + errorMessage: string; + copied: boolean; + }; + onChangeModel: (e: React.ChangeEvent) => void; + onChangeAuthor: (e: React.ChangeEvent) => void; + onChangeManufacturer: (e: React.ChangeEvent) => void; + onChangeLicense: (e: React.ChangeEvent) => void; + onChangeCopyrightYear: (e: React.ChangeEvent) => void; + onChangeHolder: (e: React.ChangeEvent) => void; + onClickCatalogValidation: () => void; + onClickCopyThingModel: () => void; + + // Step 2 - Interaction + filteredHeaders: { key: string; text: string }[]; + filteredRows: (FormElementBase & { + id: string; + description: string; + propName: string; + title: string; + })[]; + backgroundTdToSend: ThingDescription; + interaction: ContributionToCatalogState["interaction"]; + dispatch: React.Dispatch; + handleFieldChange: (placeholder: string, value: string) => void; + + // Step 3 - Submission + submission: { + tmCatalogEndpoint: string; + tmCatalogEndpointError: string; + repository: string; + repositoryError: string; + submittedError: string; + submitted: boolean; + id: string; + link: string; + }; + handleTmCatalogEndpointChange: ( + e: React.ChangeEvent + ) => void; + handleRepositoryChange: (e: React.ChangeEvent) => void; + handleSubmit: () => Promise; +} + +const ContributeToCatalog: React.FC = ({ + currentStep, + metadata, + onChangeModel, + onChangeAuthor, + onChangeManufacturer, + onChangeLicense, + onChangeCopyrightYear, + onChangeHolder, + onClickCatalogValidation, + onClickCopyThingModel, + filteredHeaders, + filteredRows, + backgroundTdToSend, + interaction, + dispatch, + handleFieldChange, + submission, + handleTmCatalogEndpointChange, + handleRepositoryChange, + handleSubmit, +}) => { + return ( +
+ {currentStep === 1 && ( + + )} + + {currentStep === 2 && ( + + )} + + {currentStep === 3 && ( + + )} +
+ ); +}; + +export default ContributeToCatalog; diff --git a/src/components/App/ConvertTm.tsx b/src/components/App/ConvertTm.tsx new file mode 100644 index 00000000..2c6f68ae --- /dev/null +++ b/src/components/App/ConvertTm.tsx @@ -0,0 +1,12 @@ +/******************************************************************************** + * Copyright (c) 2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ diff --git a/src/components/Dialogs/base/FormCreateTd.tsx b/src/components/App/CreateTd.tsx similarity index 97% rename from src/components/Dialogs/base/FormCreateTd.tsx rename to src/components/App/CreateTd.tsx index 0dcacff4..e3c7f180 100644 --- a/src/components/Dialogs/base/FormCreateTd.tsx +++ b/src/components/App/CreateTd.tsx @@ -12,13 +12,13 @@ ********************************************************************************/ import React from "react"; import { ChevronDown } from "react-feather"; -import { parseCsv, mapCsvToProperties } from "../../../utils/parser"; -import FormField from "./FormField"; -import BaseButton from "../../TDViewer/base/BaseButton"; +import { parseCsv, mapCsvToProperties } from "../../utils/parser"; +import FormField from "../base/FormField"; +import BaseButton from "../TDViewer/base/BaseButton"; type ThingType = "TD" | "TM"; -interface FormCreateTdProps { +interface CreateTdProps { type: ThingType; onChangeType: (e: React.ChangeEvent) => void; protocol: string; @@ -43,7 +43,7 @@ interface FormCreateTdProps { setThingSecurity: React.Dispatch>; } -const FormCreateTd: React.FC = ({ +const CreateTd: React.FC = ({ type, onChangeType, protocol, @@ -290,4 +290,4 @@ const FormCreateTd: React.FC = ({ ); }; -export default FormCreateTd; +export default CreateTd; diff --git a/src/components/Dialogs/base/FormInteraction.tsx b/src/components/App/FormInteraction.tsx similarity index 88% rename from src/components/Dialogs/base/FormInteraction.tsx rename to src/components/App/FormInteraction.tsx index b8104e58..ab080caf 100644 --- a/src/components/Dialogs/base/FormInteraction.tsx +++ b/src/components/App/FormInteraction.tsx @@ -10,35 +10,31 @@ * * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ -import React, { useRef, useContext, useEffect, useMemo } from "react"; +import React, { useRef, useContext, useEffect, useMemo, useState } from "react"; import type { ThingDescription } from "wot-thing-description-types"; -import { isEqual } from "lodash"; - -import ediTDorContext from "../../../context/ediTDorContext"; -import BaseTable from "../../TDViewer/base/BaseTable"; -import BaseButton from "../../TDViewer/base/BaseButton"; -import { readPropertyWithServient } from "../../../services/form"; -import { extractIndexFromId } from "../../../utils/strings"; -import { getErrorSummary } from "../../../utils/arrays"; -import Settings, { SettingsData } from "../../App/Settings"; +import { isEqual, set } from "lodash"; + +import ediTDorContext from "../../context/ediTDorContext"; +import BaseTable from "../TDViewer/base/BaseTable"; +import BaseButton from "../TDViewer/base/BaseButton"; +import { readPropertyWithServient } from "../../services/form"; +import { extractIndexFromId } from "../../utils/strings"; +import { getErrorSummary } from "../../utils/arrays"; +import Settings, { SettingsData } from "./Settings"; import { ChevronDown, ChevronUp, AlertTriangle, RefreshCw, } from "react-feather"; -import TmInputForm from "../../App/TmInputForm"; -import { prepareTdForSubmission } from "../../../services/operations"; -import { readAllReadablePropertyForms } from "../../../services/thingsApiService"; +import TmInputForm from "../base/TmInputForm"; +import { prepareTdForSubmission } from "../../services/operations"; +import { readAllReadablePropertyForms } from "../../services/thingsApiService"; import { handleHttpRequest, fetchNorthboundTD, -} from "../../../services/thingsApiService"; -import { - ContributionToCatalogState, - ContributionToCatalogAction, -} from "../../../context/ContributeToCatalogState"; -import type { ActiveSection } from "../../../context/ContributeToCatalogState"; +} from "../../services/thingsApiService"; +import { ContributionToCatalogAction } from "../../context/ContributeToCatalogState"; interface IFormInteractionProps { filteredHeaders: { key: string; text: string }[]; @@ -60,6 +56,13 @@ const FormInteraction: React.FC = ({ const context = useContext(ediTDorContext); const td: ThingDescription = context.parsedTD; + const tdWithPlaceholders: ThingDescription = useMemo( + () => ({ ...backgroundTdToSend }), + [backgroundTdToSend] + ); + const [temporaryTdWithoutPlaceholders, setTemporaryTdWithoutPlaceholders] = + useState({} as ThingDescription); + const { activeSection, sectionErrors, @@ -126,26 +129,13 @@ const FormInteraction: React.FC = ({ }); return true; case "GATEWAY": - let gatewayIsValid = - settingsData.northboundUrl.trim() !== "" && - settingsData.southboundUrl.trim() !== "" && - settingsData.pathToValue.trim() !== ""; - if (!gatewayIsValid) { - dispatch({ - type: "SET_INTERACTION_SECTION_ERROR", - section: "gateway", - error: true, - message: "All fields in section Gateway must have values", - }); - return false; - } else { - dispatch({ - type: "SET_INTERACTION_SECTION_ERROR", - section: "gateway", - error: false, - message: "", - }); - } + dispatch({ + type: "SET_INTERACTION_SECTION_ERROR", + section: "gateway", + error: false, + message: "", + }); + return true; case "TABLE": return true; @@ -162,15 +152,13 @@ const FormInteraction: React.FC = ({ if (sectionName === "TABLE") { let preparedTd = {} as ThingDescription; + try { preparedTd = prepareTdForSubmission( - backgroundTdToSend, + tdWithPlaceholders, placeholderValues ); - dispatch({ - type: "SET_BACKGROUND_TD_TO_SEND", - payload: preparedTd, - }); + setTemporaryTdWithoutPlaceholders(preparedTd); } catch (error) { dispatch({ type: "SET_INTERACTION_SECTION_ERROR", @@ -180,16 +168,20 @@ const FormInteraction: React.FC = ({ }); return; } - - if (preparedTd.id) { - requestNorthboundTdVersion(preparedTd.id, preparedTd); - } else { - dispatch({ - type: "SET_INTERACTION_SECTION_ERROR", - section: "table", - error: true, - message: "Cannot interact with the TD: missing ID", - }); + if ( + settingsData.northboundUrl.trim() !== "" && + settingsData.southboundUrl.trim() !== "" + ) { + if (preparedTd.id) { + requestNorthboundTdVersion(preparedTd.id, preparedTd); + } else { + dispatch({ + type: "SET_INTERACTION_SECTION_ERROR", + section: "table", + error: true, + message: "Cannot interact with the TD: missing ID", + }); + } } } @@ -212,7 +204,7 @@ const FormInteraction: React.FC = ({ const tdSource = Object.keys(context.northboundConnection.northboundTd).length > 0 ? (context.northboundConnection.northboundTd as ThingDescription) - : td; + : temporaryTdWithoutPlaceholders; const results = await readAllReadablePropertyForms( tdSource, filteredRows.map((r) => ({ id: r.id, propName: r.propName })), @@ -238,7 +230,7 @@ const FormInteraction: React.FC = ({ const res = await readPropertyWithServient( Object.keys(context.northboundConnection.northboundTd).length > 0 ? (context.northboundConnection.northboundTd as ThingDescription) - : td, + : temporaryTdWithoutPlaceholders, item.propName, { formIndex: index }, settingsData.pathToValue || "" diff --git a/src/components/Dialogs/base/FormMetadata.tsx b/src/components/App/FormMetadata.tsx similarity index 92% rename from src/components/Dialogs/base/FormMetadata.tsx rename to src/components/App/FormMetadata.tsx index 3faaad00..b2cb2823 100644 --- a/src/components/Dialogs/base/FormMetadata.tsx +++ b/src/components/App/FormMetadata.tsx @@ -11,11 +11,11 @@ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ import React from "react"; -import DialogTextField from "./DialogTextField"; -import BaseButton from "../../TDViewer/base/BaseButton"; +import TextField from "../base/TextField"; +import BaseButton from "../TDViewer/base/BaseButton"; import { AlertTriangle, Check, RefreshCw } from "react-feather"; -import InfoIconWrapper from "../../InfoIcon/InfoIconWrapper"; -import { getValidateTMContent } from "../../InfoIcon/TooltipMapper"; +import InfoIconWrapper from "../base/InfoIconWrapper"; +import { getValidateTMContent } from "../../utils/TooltipMapper"; interface IFormMetadataProps { model: string; @@ -66,7 +66,7 @@ const FormMetadata: React.FC = ({ Models.
- = ({ onChange={onChangeModel} autoFocus={true} /> - = ({ onChange={onChangeAuthor} autoFocus={false} /> - = ({ onChange={onChangeManufacturer} autoFocus={false} /> - = ({ onChange={onChangeLicense} autoFocus={false} /> - = ({ onChange={onChangeCopyrightYear} autoFocus={false} /> - = ({
{isValidating ? ( <> - Validating + Validating ) : ( diff --git a/src/components/Dialogs/base/FormSubmission.tsx b/src/components/App/FormSubmission.tsx similarity index 97% rename from src/components/Dialogs/base/FormSubmission.tsx rename to src/components/App/FormSubmission.tsx index 65179ebf..760f3d89 100644 --- a/src/components/Dialogs/base/FormSubmission.tsx +++ b/src/components/App/FormSubmission.tsx @@ -11,8 +11,8 @@ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ import React from "react"; -import DialogTextField from "./DialogTextField"; -import BaseButton from "../../TDViewer/base/BaseButton"; +import TextField from "../base/TextField"; +import BaseButton from "../TDViewer/base/BaseButton"; import { AlertTriangle, Check, Copy, ExternalLink } from "react-feather"; interface IFormSubmissionProps { @@ -59,7 +59,7 @@ const FormSubmission: React.FC = ({ Add the TM Catalog Endpoint and Repository URL
- = ({ {tmCatalogEndpointError}
)} - void; + onUpdate: () => void; + currentTdId: string; +} + +const RequestSuccessful: React.FC<{ message: string }> = ({ message }) => ( +
+ {message} +
+); + +const RequestFailed: React.FC<{ + errorMessage: string; + errorReason: string; +}> = ({ errorMessage, errorReason }) => ( +
+
Request failed
+
{errorMessage}
+ {errorReason && ( +
Reason: {errorReason}
+ )} +
+); + +const SpinnerTemplate: React.FC<{ fullScreen?: boolean }> = ({ + fullScreen, +}) => ( +
+
+
+); + +const CreateTd: React.FC = ({ + isUpdate, + requestSend, + requestUpdate, + onSend, + onUpdate, +}) => { + const renderDialogContent = () => { + const isLoading = requestUpdate.isLoading || requestSend.isLoading; + if (isLoading) return ; + + const displayRequest = + isUpdate && requestUpdate.message + ? requestUpdate + : requestSend.message + ? requestSend + : isUpdate + ? requestUpdate + : requestSend; + + if (!displayRequest || displayRequest.message === "") return <>; + + const operationType = displayRequest === requestSend ? "sent" : "updated"; + return displayRequest.success ? ( + + ) : ( + + ); + }; + + return ( +
+

Operation Details

+
+
+ Action: + + {isUpdate ? "Update Existing TD" : "Send New TD"} + +
+

+ {isUpdate + ? "You are updating an existing Thing Description that was previously sent to the server." + : "You are sending a new Thing Description to the server for the first time."} +

+
+ +
+

Result:

+
+

+ {requestSend.message + ? requestSend.message + : isUpdate + ? requestUpdate.message || "No update request made yet." + : "No send request made yet."} +

+ +
+ {isUpdate ? ( + + ) : ( + + )} +
+ +
{renderDialogContent()}
+
+
+
+ ); +}; + +export default CreateTd; diff --git a/src/components/App/Settings.tsx b/src/components/App/Settings.tsx index ab5d2e10..875708e9 100644 --- a/src/components/App/Settings.tsx +++ b/src/components/App/Settings.tsx @@ -11,8 +11,8 @@ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ import React, { useState, useEffect, useCallback } from "react"; -import InfoIconWrapper from "../InfoIcon/InfoIconWrapper"; -import DialogTextField from "../Dialogs/base/DialogTextField"; +import InfoIconWrapper from "../base/InfoIconWrapper"; +import TextField from "../base/TextField"; import { isValidUrl } from "../../utils/strings"; export interface SettingsData { @@ -141,7 +141,7 @@ const Settings: React.FC = ({ of the initial TD is used to correlate both. - = ({
)} - = ({ {`If the gateway is wrapping the payloads in a JSON object, please provide the path to the value as a JSON pointer. For a JSON like {"foo": {"bar":"baz"}}, where baz is the value according the Data Schema of the TD, you should enter /foo/bar.`} - ((_, ref) => { const children = ( <> - clearErrorMessage()} /> - ((_, ref) => { const children = ( <> - clearErrorMessage()} /> - void; @@ -27,7 +48,7 @@ interface Form { } interface AddFormDialogProps { - type?: string; + type?: OperationsType; interaction?: { forms?: Form[] }; interactionName?: string; } @@ -39,7 +60,10 @@ const AddFormDialog = forwardRef( return false; }); - const type = props.type ?? ""; + const [value, setValue] = React.useState(""); + const [error, setError] = React.useState(""); + + const type: OperationsType = props.type || ""; const name = type && type[0].toUpperCase() + type.slice(1); const interaction = props.interaction ?? {}; const interactionName = props.interactionName ?? ""; @@ -78,101 +102,66 @@ const AddFormDialog = forwardRef( }; const formSelection = operationsSelections(type); - const children = ( - <> - -
{formSelection}
-
- - { - clearErrorMessage(); - }} - /> - -
- - ); + + const onHandleEventRightButton = () => { + const form: Form = { + op: operations(type) + .map((x) => { + const element = document.getElementById( + "form-" + x + ) as HTMLInputElement; + return element?.checked ? element.value : undefined; + }) + .filter((y): y is string => y !== undefined), + href: + ( + document.getElementById("form-href") as HTMLInputElement + )?.value?.trim() || "/", + }; + + if (form.op.length === 0) { + setError("You have to select at least one operation ..."); + } else if (checkIfFormExists(form)) { + setError( + "A Form for one of the selected operations already exists ..." + ); + } else { + setError(""); + context.addForm(typeToJSONKey(type), interactionName, form); + close(); + } + }; if (display) { return ReactDOM.createPortal( { - const form: Form = { - op: operations(type) - .map((x) => { - const element = document.getElementById( - "form-" + x - ) as HTMLInputElement; - return element?.checked ? element.value : undefined; - }) - .filter((y): y is string => y !== undefined), - href: - ( - document.getElementById("form-href") as HTMLInputElement - )?.value?.trim() || "/", - }; - - if (form.op.length === 0) { - showErrorMessage("You have to select at least one operation ..."); - } else if (checkIfFormExists(form)) { - showErrorMessage( - "A Form for one of the selected operations already exists ..." - ); - } else { - context.addForm(typeToJSONKey(type), interactionName, form); - close(); - } - }} - rightButton={"Add"} - children={children} + onHandleEventRightButton={onHandleEventRightButton} + rightButton={"Add Form"} + leftButton="Cancel" + hasSubmit={true} title={`Add ${name} Form`} description={`Tell us how this ${name} can be interfaced by selecting operations below and providing an href.`} - />, + > + { + setValue(e.target.value.trim()); + setError(""); + }} + operations={operations} + error={error} + defaultValue={interaction.forms?.[0].href ?? ""} + > + , document.getElementById("modal-root") as HTMLElement ); } - return null; } ); -const showErrorMessage = (msg) => { - (document.getElementById("form-href-info") as HTMLElement).textContent = msg; - (document.getElementById("form-href") as HTMLInputElement).classList.remove( - "border-gray-600" - ); - (document.getElementById("form-href") as HTMLInputElement).classList.add( - "border-red-400" - ); -}; - -const clearErrorMessage = () => { - (document.getElementById("form-href") as HTMLElement).classList.add( - "border-gray-600" - ); - (document.getElementById("form-href") as HTMLInputElement).classList.remove( - "border-red-400" - ); -}; - -const operations = (type: string): string[] => { +const operations = (type: OperationsType): OperationsMap => { switch (type) { case "property": return [ @@ -199,39 +188,12 @@ const operations = (type: string): string[] => { } }; -const operationsSelections = (type: string): JSX.Element => { +const operationsSelections = (type: OperationsType): JSX.Element => { return (
- {operations(type).map((e) => formCheckbox(e))} -
- ); -}; - -const formCheckbox = (name: string): JSX.Element => { - const id = `form-${name}`; - - return ( -
- {name !== "invokeaction" ? ( - - ) : ( - - )} - + {operations(type).map((e) => ( + + ))}
); }; diff --git a/src/components/Dialogs/AddPropertyDialog.tsx b/src/components/Dialogs/AddPropertyDialog.tsx index 68e48763..cafef313 100644 --- a/src/components/Dialogs/AddPropertyDialog.tsx +++ b/src/components/Dialogs/AddPropertyDialog.tsx @@ -18,10 +18,10 @@ import React, { } from "react"; import ReactDOM from "react-dom"; import ediTDorContext from "../../context/ediTDorContext"; -import DialogCheckbox from "./base/DialogCheckbox"; -import DialogTextArea from "./base/DialogTextArea"; -import DialogTextField from "./base/DialogTextField"; -import DialogDropdown from "./base/DialogDropdown"; +import Checkbox from "../base/Checkbox"; +import TextArea from "../base/TextArea"; +import TextField from "../base/TextField"; +import Dropdown from "../base/Dropdown"; import DialogTemplate from "./DialogTemplate"; @@ -73,19 +73,19 @@ export const AddPropertyDialog = forwardRef( const children = ( <> - clearErrorMessage()} /> - - ( Additional:
- - + +
); diff --git a/src/components/Dialogs/ContributeToCatalog.tsx b/src/components/Dialogs/ContributeToCatalogDialog.tsx similarity index 84% rename from src/components/Dialogs/ContributeToCatalog.tsx rename to src/components/Dialogs/ContributeToCatalogDialog.tsx index 4f0b46ef..a0b3d613 100644 --- a/src/components/Dialogs/ContributeToCatalog.tsx +++ b/src/components/Dialogs/ContributeToCatalogDialog.tsx @@ -25,7 +25,7 @@ import type { } from "wot-thing-description-types"; import Ajv2019 from "ajv/dist/2019"; import addFormats from "ajv-formats"; -import { ProgressBar, Step } from "./base/ProgressBar"; +import { ProgressBar, Step } from "../App/ProgressBar"; import ediTDorContext from "../../context/ediTDorContext"; import { @@ -33,14 +33,14 @@ import { initialState, } from "../../context/ContributeToCatalogState"; import DialogTemplate from "./DialogTemplate"; -import FormMetadata from "./base/FormMetadata"; -import FormSubmission from "./base/FormSubmission"; -import FormInteraction from "./base/FormInteraction"; +import ContributeToCatalog from "../App/ContributeToCatalog"; import { isValidUrl, formatText } from "../../utils/strings"; +import { getJsonLdString } from "../../utils/arrays"; import { requestWeb } from "../../services/thingsApiService"; import { normalizeContext, extractPlaceholders, + generateIdForThingDescription, } from "../../services/operations"; export interface IContributeToCatalogProps { @@ -58,7 +58,7 @@ const VALIDATION_TM_JSON = const VALIDATION_MODBUS = "https://raw.githubusercontent.com/wot-oss/tmc/refs/heads/main/internal/commands/validate/modbus.schema.json"; -const ContributeToCatalog = forwardRef((props, ref) => { +const ContributeToCatalogDialog = forwardRef((props, ref) => { const context = useContext(ediTDorContext); const td: ThingDescription = context.parsedTD; const contributeCatalogData = context.contributeCatalog; @@ -92,21 +92,29 @@ const ContributeToCatalog = forwardRef((props, ref) => { const open = () => { const metadataFromTd = { - model: td["schema:mpn"] || contributeCatalogData.model || "", + model: + getJsonLdString(td, ["schema:mpn"]) ?? + contributeCatalogData.model ?? + "", author: - td["schema:author"]?.["schema:name"] || - contributeCatalogData.author || + getJsonLdString(td, ["schema:author", "schema:name"]) ?? + contributeCatalogData.author ?? "", manufacturer: - td["schema:manufacturer"]?.["schema:name"] || - contributeCatalogData.manufacturer || + getJsonLdString(td, ["schema:manufacturer", "schema:name"]) ?? + contributeCatalogData.manufacturer ?? + "", + license: + getJsonLdString(td, ["schema:license"]) ?? + contributeCatalogData.license ?? "", - license: td["schema:license"] || contributeCatalogData.license || "", copyrightYear: - td["schema:copyrightYear"] || contributeCatalogData.copyrightYear || "", + getJsonLdString(td, ["schema:copyrightYear"]) ?? + contributeCatalogData.copyrightYear ?? + "", holder: ( - td["schema:copyrightHolder"]?.["name"] || - contributeCatalogData.holder || + getJsonLdString(td, ["schema:copyrightHolder", "schema:name"]) ?? + contributeCatalogData.holder ?? "" ).trim(), }; @@ -232,6 +240,8 @@ const ContributeToCatalog = forwardRef((props, ref) => { throw new Error(message); } + const tdTransformed = generateIdForThingDescription(tdCopy); + const ajv = new Ajv2019({ strict: false, allErrors: true, @@ -248,7 +258,7 @@ const ContributeToCatalog = forwardRef((props, ref) => { let schema = await response.json(); let validate = ajv.compile(schema); - let valid = validate(tdCopy); + let valid = validate(tdTransformed); if (!valid) { let message = `Validation failed for ${VALIDATION_TMC_MANDATORY}: ${ validate.errors ? ajv.errorsText(validate.errors) : "" @@ -268,7 +278,7 @@ const ContributeToCatalog = forwardRef((props, ref) => { throw new Error(`Failed to fetch schema from ${VALIDATION_MODBUS}`); schema = await response.json(); validate = ajv.compile(schema); - valid = validate(tdCopy); + valid = validate(tdTransformed); if (!valid) { let message = `Validation failed for ${VALIDATION_MODBUS}: ${ validate.errors ? ajv.errorsText(validate.errors) : "" @@ -276,7 +286,7 @@ const ContributeToCatalog = forwardRef((props, ref) => { throw new Error(message); } - dispatch({ type: "SET_BACKGROUND_TD_TO_SEND", payload: tdCopy }); + dispatch({ type: "SET_BACKGROUND_TD_TO_SEND", payload: tdTransformed }); dispatch({ type: "SET_METADATA_ERROR_MESSAGE", payload: "" }); dispatch({ type: "SET_METADATA_VALIDATION", payload: "VALID" }); dispatch({ @@ -593,59 +603,28 @@ const ContributeToCatalog = forwardRef((props, ref) => {
-
- {state.workflow.currentStep === 1 && ( - <> - - - )} - {state.workflow.currentStep === 2 && ( - - )} - {state.workflow.currentStep === 3 && ( - <> - - - )} -
+
); @@ -682,5 +661,5 @@ const ContributeToCatalog = forwardRef((props, ref) => { return null; }); -ContributeToCatalog.displayName = "ContributeToCatalog"; -export default ContributeToCatalog; +ContributeToCatalogDialog.displayName = "ContributeToCatalogDialog"; +export default ContributeToCatalogDialog; diff --git a/src/components/Dialogs/ConvertTmDialog.tsx b/src/components/Dialogs/ConvertTmDialog.tsx index 14a2f7ab..8ab689fc 100644 --- a/src/components/Dialogs/ConvertTmDialog.tsx +++ b/src/components/Dialogs/ConvertTmDialog.tsx @@ -25,9 +25,8 @@ import { extractPlaceholders, isVersionValid, } from "../../services/operations"; -import TmInputForm from "../App/TmInputForm"; -import DialogTextField from "./base/DialogTextField"; -import type { IEdiTDorContext } from "../../types/context"; +import TmInputForm from "../base/TmInputForm"; +import TextField from "../base/TextField"; export interface ConvertTmDialogRef { openModal: () => void; @@ -130,7 +129,7 @@ const ConvertTmDialog = forwardRef((props, ref) => { /> {!validVersion && ( - ((props, ref) => { placeholder="ex: 1.0.0" value={versionInput} helperText="The Thing Model contains a version without instance key and corresponding value. If you leave this field empty it will automatic generate a instance value." - > + > )}

Select/unselect the interaction affordances you would like to see in diff --git a/src/components/Dialogs/CreateTdDialog.tsx b/src/components/Dialogs/CreateTdDialog.tsx index b9070aac..1e12a1d7 100644 --- a/src/components/Dialogs/CreateTdDialog.tsx +++ b/src/components/Dialogs/CreateTdDialog.tsx @@ -21,7 +21,7 @@ import ReactDOM from "react-dom"; import ediTDorContext from "../../context/ediTDorContext"; import DialogTemplate from "./DialogTemplate"; import ErrorDialog from "./ErrorDialog"; -import FormCreateTd from "./base/FormCreateTd"; +import CreateTd from "../App/CreateTd"; export interface CreateTdDialogRef { openModal: () => void; @@ -145,7 +145,7 @@ const CreateTdDialog = forwardRef((props, ref) => { "To quickly create a basis for your new Thing Description/Thing Model just fill out this little template and we'll get you going." } > - { setThingDescription={setThingDescription} thingSecurity={thingSecurity} setThingSecurity={setThingSecurity} - > + > , document.getElementById("modal-root") as HTMLElement )} diff --git a/src/components/Dialogs/FormCheckbox.tsx b/src/components/Dialogs/FormCheckbox.tsx new file mode 100644 index 00000000..27c15949 --- /dev/null +++ b/src/components/Dialogs/FormCheckbox.tsx @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (c) 2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ +interface FormCheckboxProps { + name: string; +} + +const FormCheckbox: React.FC = ({ name }) => { + const id = `form-${name}`; + const isInvoke = name === "invokeaction"; + + return ( +
+ + +
+ ); +}; +export default FormCheckbox; diff --git a/src/components/Dialogs/SendTDDialog.tsx b/src/components/Dialogs/SendTDDialog.tsx index 7011d236..9af0780a 100644 --- a/src/components/Dialogs/SendTDDialog.tsx +++ b/src/components/Dialogs/SendTDDialog.tsx @@ -21,9 +21,10 @@ import { isSuccessResponse, handleHttpRequest, } from "../../services/thingsApiService"; -import RequestSuccessful from "./base/RequestSuccessful"; -import RequestFailed from "./base/RequestFailed"; +import RequestSuccessful from "../base/RequestSuccessful"; +import RequestFailed from "../base/RequestFailed"; import { fetchNorthboundTD } from "../../services/thingsApiService"; +import SendTD from "../App/SendTD"; export interface SendTDDialogRef { openModal: () => void; @@ -250,85 +251,14 @@ const SendTDDialog = forwardRef( "The Thing Description will be sent to a Third-Party Service located at the endpoint configured in the Settings options under Southbound URL field. The proxied Thing will be interactable over HTTP in the left view." } > -
-

- Current Configuration Details -

-
- - - - - - - - - - - -
- Endpoint: - - {getLocalStorage("southbound") || - "(No endpoint configured)"} -
- TD ID: - - {currentTdId || "(No ID available)"} -
-
-
- -
-

Operation Details

-
-
- Action: - - {isUpdate ? "Update Existing TD" : "Send New TD"} - -
-

- {isUpdate - ? "You are updating an existing Thing Description that was previously sent to the server." - : "You are sending a new Thing Description to the server for the first time."} -

-
-
- -
-

Result:

-
-

- {requestSend.message - ? requestSend.message - : isUpdate - ? requestUpdate.message || "No update request made yet." - : "No send request made yet."} -

- -
- {isUpdate ? ( - - ) : ( - - )} -
-
{renderDialogContent()}
-
-
+ , document.getElementById("modal-root") as HTMLElement ); diff --git a/src/components/TDViewer/TDViewer.tsx b/src/components/TDViewer/TDViewer.tsx index 785b3659..6f9969c9 100644 --- a/src/components/TDViewer/TDViewer.tsx +++ b/src/components/TDViewer/TDViewer.tsx @@ -19,8 +19,8 @@ import { separateForms, } from "../../util"; import AddFormDialog from "../Dialogs/AddFormDialog"; -import InfoIconWrapper from "../InfoIcon/InfoIconWrapper"; -import { getFormsTooltipContent } from "../InfoIcon/TooltipMapper"; +import InfoIconWrapper from "../base/InfoIconWrapper"; +import { getFormsTooltipContent } from "../../utils/TooltipMapper"; import Form from "./components/Form"; import InteractionSection from "./components/InteractionSection"; import RenderedObject from "./components/RenderedObject"; diff --git a/src/components/TDViewer/base/BaseTable.tsx b/src/components/TDViewer/base/BaseTable.tsx index 2c992bc5..0c79e01c 100644 --- a/src/components/TDViewer/base/BaseTable.tsx +++ b/src/components/TDViewer/base/BaseTable.tsx @@ -13,7 +13,7 @@ import React, { useMemo, useState, useEffect } from "react"; import BasePagination from "./BasePagination"; import ButtonSwap from "./ButtonSwap"; -import Icon from "../../InfoIcon/Icon"; +import Icon from "../../base/Icon"; import { Edit, XCircle, diff --git a/src/components/TDViewer/base/DoubleSwapButton.tsx b/src/components/TDViewer/base/DoubleSwapButton.tsx index b9a65173..e0a29645 100644 --- a/src/components/TDViewer/base/DoubleSwapButton.tsx +++ b/src/components/TDViewer/base/DoubleSwapButton.tsx @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ import React from "react"; -import InfoIconWrapper from "../../InfoIcon/InfoIconWrapper"; +import InfoIconWrapper from "../../base/InfoIconWrapper"; import ButtonSwap from "../base/ButtonSwap"; interface IDoubleSwapButtonProps { diff --git a/src/components/TDViewer/base/SingleIncrementButton.tsx b/src/components/TDViewer/base/SingleIncrementButton.tsx index 51966e57..05b16876 100644 --- a/src/components/TDViewer/base/SingleIncrementButton.tsx +++ b/src/components/TDViewer/base/SingleIncrementButton.tsx @@ -12,7 +12,7 @@ ********************************************************************************/ import React from "react"; import IncrementButton from "../base/IncrementButton"; -import InfoIconWrapper from "../../InfoIcon/InfoIconWrapper"; +import InfoIconWrapper from "../../base/InfoIconWrapper"; interface SingleIncrementButtonProps { idIcon: string; diff --git a/src/components/TDViewer/base/SingleSwapButton.tsx b/src/components/TDViewer/base/SingleSwapButton.tsx index 408a38cb..a199ae2d 100644 --- a/src/components/TDViewer/base/SingleSwapButton.tsx +++ b/src/components/TDViewer/base/SingleSwapButton.tsx @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ import React from "react"; -import InfoIconWrapper from "../../InfoIcon/InfoIconWrapper"; +import InfoIconWrapper from "../../base/InfoIconWrapper"; import ButtonSwap from "../base/ButtonSwap"; interface SingleSwapButtonProps { diff --git a/src/components/TDViewer/components/Action.tsx b/src/components/TDViewer/components/Action.tsx index 2343a2ff..39059a34 100644 --- a/src/components/TDViewer/components/Action.tsx +++ b/src/components/TDViewer/components/Action.tsx @@ -15,8 +15,8 @@ import { Trash2 } from "react-feather"; import ediTDorContext from "../../../context/ediTDorContext"; import { buildAttributeListObject, separateForms } from "../../../util.js"; import AddFormDialog from "../../Dialogs/AddFormDialog"; -import InfoIconWrapper from "../../InfoIcon/InfoIconWrapper"; -import { getFormsTooltipContent } from "../../InfoIcon/TooltipMapper"; +import InfoIconWrapper from "../../base/InfoIconWrapper"; +import { getFormsTooltipContent } from "../../../utils/TooltipMapper"; import Form from "./Form"; import AddFormElement from "../base/AddFormElement"; diff --git a/src/components/TDViewer/components/EditProperties.tsx b/src/components/TDViewer/components/EditProperties.tsx index a1c31acb..f52837b5 100644 --- a/src/components/TDViewer/components/EditProperties.tsx +++ b/src/components/TDViewer/components/EditProperties.tsx @@ -16,7 +16,7 @@ import { getAddressOffsetTooltipContent, getEndiannessTooltipContent, getUniIdTooltipContent, -} from "../../InfoIcon/TooltipMapper"; +} from "../../../utils/TooltipMapper"; import type { ThingDescription } from "wot-thing-description-types"; import SingleIncrementButton from "../base/SingleIncrementButton"; import SingleSwapButton from "../base/SingleSwapButton"; diff --git a/src/components/TDViewer/components/Event.tsx b/src/components/TDViewer/components/Event.tsx index 97673134..afb89ded 100644 --- a/src/components/TDViewer/components/Event.tsx +++ b/src/components/TDViewer/components/Event.tsx @@ -15,8 +15,8 @@ import { Trash2 } from "react-feather"; import ediTDorContext from "../../../context/ediTDorContext"; import { buildAttributeListObject, separateForms } from "../../../util.js"; import AddFormDialog from "../../Dialogs/AddFormDialog"; -import InfoIconWrapper from "../../InfoIcon/InfoIconWrapper"; -import { getFormsTooltipContent } from "../../InfoIcon/TooltipMapper"; +import InfoIconWrapper from "../../base/InfoIconWrapper"; +import { getFormsTooltipContent } from "../../../utils/TooltipMapper"; import Form from "./Form"; import AddFormElement from "../base/AddFormElement"; diff --git a/src/components/TDViewer/components/InteractionSection.tsx b/src/components/TDViewer/components/InteractionSection.tsx index 0c523679..ee11ce37 100644 --- a/src/components/TDViewer/components/InteractionSection.tsx +++ b/src/components/TDViewer/components/InteractionSection.tsx @@ -16,8 +16,8 @@ import ediTDorContext from "../../../context/ediTDorContext"; import AddActionDialog from "../../Dialogs/AddActionDialog"; import AddEventDialog from "../../Dialogs/AddEventDialog"; import { AddPropertyDialog } from "../../Dialogs/AddPropertyDialog"; -import InfoIconWrapper from "../../InfoIcon/InfoIconWrapper"; -import { tooltipMapper } from "../../InfoIcon/TooltipMapper"; +import InfoIconWrapper from "../../base/InfoIconWrapper"; +import { tooltipMapper } from "../../../utils/TooltipMapper"; import Action from "./Action"; import Event from "./Event"; import Property from "./Property"; diff --git a/src/components/TDViewer/components/LinkSection.tsx b/src/components/TDViewer/components/LinkSection.tsx index a8207dc2..df772277 100644 --- a/src/components/TDViewer/components/LinkSection.tsx +++ b/src/components/TDViewer/components/LinkSection.tsx @@ -16,8 +16,8 @@ import React, { useContext, useEffect, useRef, useState } from "react"; import ediTDorContext from "../../../context/ediTDorContext"; import { changeBetweenTd } from "../../../util"; import AddLinkTdDialog from "../../Dialogs/AddLinkTdDialog"; -import InfoIconWrapper from "../../InfoIcon/InfoIconWrapper"; -import { getLinksTooltipContent } from "../../InfoIcon/TooltipMapper"; +import InfoIconWrapper from "../../base/InfoIconWrapper"; +import { getLinksTooltipContent } from "../../../utils/TooltipMapper"; import Link from "./Link"; import BaseButton from "../base/BaseButton"; diff --git a/src/components/TDViewer/components/Property.tsx b/src/components/TDViewer/components/Property.tsx index 2756c566..c4e91541 100644 --- a/src/components/TDViewer/components/Property.tsx +++ b/src/components/TDViewer/components/Property.tsx @@ -14,8 +14,8 @@ import React, { useContext, useState, useRef } from "react"; import { Trash2 } from "react-feather"; import ediTDorContext from "../../../context/ediTDorContext"; import { buildAttributeListObject, separateForms } from "../../../util.js"; -import InfoIconWrapper from "../../InfoIcon/InfoIconWrapper"; -import { getFormsTooltipContent } from "../../InfoIcon/TooltipMapper"; +import InfoIconWrapper from "../../base/InfoIconWrapper"; +import { getFormsTooltipContent } from "../../../utils/TooltipMapper"; import Form from "./Form"; import AddFormDialog from "../../Dialogs/AddFormDialog"; import AddFormElement from "../base/AddFormElement"; diff --git a/src/components/base/Button.tsx b/src/components/base/Button.tsx new file mode 100644 index 00000000..53faea6b --- /dev/null +++ b/src/components/base/Button.tsx @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ + +interface IButtonProps { + onClick: () => void; + children: React.ReactNode; +} + +const Button: React.FC = ({ onClick, children }) => { + return ( + + ); +}; + +export default Button; diff --git a/src/components/Dialogs/base/DialogCheckbox.tsx b/src/components/base/Checkbox.tsx similarity index 93% rename from src/components/Dialogs/base/DialogCheckbox.tsx rename to src/components/base/Checkbox.tsx index ea87df36..8e3684b7 100644 --- a/src/components/Dialogs/base/DialogCheckbox.tsx +++ b/src/components/base/Checkbox.tsx @@ -16,7 +16,7 @@ interface ICheckboxProps { readOnly?: boolean; } -const DialogCheckbox: React.FC = (props) => { +const Checkbox: React.FC = (props) => { return (
{(props.readOnly ?? true) ? ( @@ -43,4 +43,4 @@ const DialogCheckbox: React.FC = (props) => { ); }; -export default DialogCheckbox; +export default Checkbox; diff --git a/src/components/Dialogs/base/DialogDropdown.tsx b/src/components/base/Dropdown.tsx similarity index 94% rename from src/components/Dialogs/base/DialogDropdown.tsx rename to src/components/base/Dropdown.tsx index 72903b8f..74d2fe5f 100644 --- a/src/components/Dialogs/base/DialogDropdown.tsx +++ b/src/components/base/Dropdown.tsx @@ -20,7 +20,7 @@ interface IDropdownProps { className?: string; } -const DialogDropdown: React.FC = (props) => { +const Dropdown: React.FC = (props) => { return ( <>