From a54eefc43f8c85b67e4e65f5eb8e1ec98a413969 Mon Sep 17 00:00:00 2001 From: Ayush Date: Fri, 9 Jan 2026 19:04:10 +0530 Subject: [PATCH 1/9] Add copy button for TD affordances (actions, properties, events) Signed-off-by: Ayush --- src/components/TDViewer/components/Action.tsx | 100 +++++++++++++---- src/components/TDViewer/components/Event.tsx | 98 +++++++++++----- .../TDViewer/components/Property.tsx | 106 +++++++++++++----- 3 files changed, 229 insertions(+), 75 deletions(-) diff --git a/src/components/TDViewer/components/Action.tsx b/src/components/TDViewer/components/Action.tsx index 6d4323dd..910e3163 100644 --- a/src/components/TDViewer/components/Action.tsx +++ b/src/components/TDViewer/components/Action.tsx @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ import React, { useContext, useState } from "react"; -import { Trash2 } from "react-feather"; +import { Trash2, Copy } from "react-feather"; import ediTDorContext from "../../../context/ediTDorContext"; import { buildAttributeListObject, @@ -27,10 +27,9 @@ const alreadyRenderedKeys = ["title", "forms", "description"]; const Action: React.FC = (props) => { const context = useContext(ediTDorContext); - const [isExpanded, setIsExpanded] = useState(false); - const addFormDialog = React.useRef(); + const addFormDialog = React.useRef(); const handleOpenAddFormDialog = () => { addFormDialog.current.openModal(); }; @@ -54,18 +53,43 @@ const Action: React.FC = (props) => { props.action, alreadyRenderedKeys ); - const attributes = Object.keys(attributeListObject).map((x) => { - return ( -
  • - {x} : {JSON.stringify(attributeListObject[x])} -
  • - ); - }); + + const attributes = Object.keys(attributeListObject).map((x) => ( +
  • + {x} : {JSON.stringify(attributeListObject[x])} +
  • + )); const handleDeleteAction = () => { context.removeOneOfAKindReducer("actions", props.actionName); }; + const handleCopyAction = () => { + const parsedTD = context.parsedTD; + if (!parsedTD || !parsedTD.actions) { + console.error("parsedTD or actions missing", parsedTD); + return; + } + const originalName = props.actionName; + let newName = `${originalName}_copy`; + let counter = 1; + while (parsedTD.actions[newName]) { + newName = `${originalName}_copy_${counter++}`; + } + const copiedAction = structuredClone(action); + if (copiedAction.title) { + copiedAction.title = `${copiedAction.title} copy`; + } + const updatedParsedTD = { + ...parsedTD, + actions: { + ...parsedTD.actions, + [newName]: copiedAction, + }, + }; + context.updateOfflineTD(JSON.stringify(updatedParsedTD, null, 2)); + }; + return (
    = (props) => { onToggle={() => setIsExpanded(!isExpanded)} > -

    {action.title ?? props.actionName}

    +

    + {action.title ?? props.actionName} +

    + {isExpanded && ( - - )} + <> + + + + +)} +
    @@ -92,7 +141,10 @@ const Action: React.FC = (props) => { {action.description}
    )} -
      {attributes}
    + +
      + {attributes} +
    = (props) => {
    + + {forms.map((form, i) => (
    + interactionType="action" + /> ))}
    diff --git a/src/components/TDViewer/components/Event.tsx b/src/components/TDViewer/components/Event.tsx index e47fa950..ff5da47e 100644 --- a/src/components/TDViewer/components/Event.tsx +++ b/src/components/TDViewer/components/Event.tsx @@ -10,8 +10,8 @@ * * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ -import React, { useContext, useState } from "react"; -import { Trash2 } from "react-feather"; +import React, { useContext, useState, useRef } from "react"; +import { Trash2, Copy } from "react-feather"; import ediTDorContext from "../../../context/ediTDorContext"; import { buildAttributeListObject, @@ -27,12 +27,11 @@ const alreadyRenderedKeys = ["title", "forms", "description"]; const Event: React.FC = (props) => { const context = useContext(ediTDorContext); - const [isExpanded, setIsExpanded] = useState(false); - const addFormDialog = React.useRef(); + const addFormDialog = useRef<{ openModal: () => void }>(null); const handleOpenAddFormDialog = () => { - addFormDialog.current.openModal(); + addFormDialog.current?.openModal(); }; if ( @@ -48,22 +47,47 @@ const Event: React.FC = (props) => { const event = props.event; const forms = separateForms(props.event.forms); + const attributeListObject = buildAttributeListObject( { name: props.eventName }, props.event, alreadyRenderedKeys ); - const attributes = Object.keys(attributeListObject).map((x) => { - return ( -
  • - {x} : {JSON.stringify(attributeListObject[x])} -
  • - ); - }); + const attributes = Object.keys(attributeListObject).map((x) => ( +
  • + {x} : {JSON.stringify(attributeListObject[x])} +
  • + )); + const handleDeleteEventClicked = () => { context.removeOneOfAKindReducer("events", props.eventName); }; + const handleCopyEvent = () => { + const parsedTD = context.parsedTD; + if (!parsedTD || !parsedTD.events) { + console.error("parsedTD or events missing", parsedTD); + return; + } + const originalName = props.eventName; + let newName = `${originalName}_copy`; + let counter = 1; + while (parsedTD.events[newName]) { + newName = `${originalName}_copy_${counter++}`; + } + const copiedEvent = structuredClone(event); + const baseTitle = event.title ?? props.eventName; + copiedEvent.title = `${baseTitle} copy`; + const updatedParsedTD = { + ...parsedTD, + events: { + ...parsedTD.events, + [newName]: copiedEvent, + }, + }; + + context.updateOfflineTD(JSON.stringify(updatedParsedTD, null, 2)); + }; return (
    = (props) => { onToggle={() => setIsExpanded(!isExpanded)} > -
    {event.title ?? props.eventName}
    +
    + {event.title ?? props.eventName} +
    + {isExpanded && ( - + <> + + + )}
    @@ -91,7 +138,9 @@ const Event: React.FC = (props) => { {event.description} )} -
      {attributes}
    +
      + {attributes} +
    = (props) => {

    Forms

    - = (props) => { key={`${i}-${form.href}`} form={form} propName={props.eventName} - interactionType={"event"} - > + interactionType="event" + /> ))}
    diff --git a/src/components/TDViewer/components/Property.tsx b/src/components/TDViewer/components/Property.tsx index b75a086d..abe5ef46 100644 --- a/src/components/TDViewer/components/Property.tsx +++ b/src/components/TDViewer/components/Property.tsx @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ import React, { useContext, useState, useRef } from "react"; -import { Trash2 } from "react-feather"; +import { Trash2, Copy } from "react-feather"; import ediTDorContext from "../../../context/ediTDorContext"; import { buildAttributeListObject, @@ -39,9 +39,9 @@ interface IProperty { }; propName: string; } + const Property: React.FC = (props) => { const context = useContext(ediTDorContext); - const [isExpanded, setIsExpanded] = useState(false); const addFormDialog = useRef<{ openModal: () => void }>(null); @@ -62,31 +62,57 @@ const Property: React.FC = (props) => { const property = props.prop; - let forms: { - href: string; - op: string; - contentType: string; - actualIndex: number; - [key: string]: any; - }[] = separateForms(structuredClone(props.prop.forms)); + const forms = separateForms(structuredClone(props.prop.forms)); const attributeListObject = buildAttributeListObject( { name: props.propName }, props.prop, alreadyRenderedKeys ); - const attributes = Object.keys(attributeListObject).map((x) => { - return ( -
  • - {x} : {JSON.stringify(attributeListObject[x])} -
  • - ); - }); + + const attributes = Object.keys(attributeListObject).map((x) => ( +
  • + {x} : {JSON.stringify(attributeListObject[x])} +
  • + )); const handleDeletePropertyClicked = () => { context.removeOneOfAKindReducer("properties", props.propName); }; + const handleCopyProperty = () => { + const parsedTD = context.parsedTD; + + if (!parsedTD || !parsedTD.properties) { + console.error("parsedTD or properties missing", parsedTD); + return; + } + + const originalName = props.propName; + + let newName = `${originalName}_copy`; + let counter = 1; + + while (parsedTD.properties[newName]) { + newName = `${originalName}_copy_${counter++}`; + } + + const copiedProperty = structuredClone(property); + + const baseTitle = property.title ?? props.propName; + copiedProperty.title = `${baseTitle} copy`; + + const updatedParsedTD = { + ...parsedTD, + properties: { + ...parsedTD.properties, + [newName]: copiedProperty, + }, + }; + + context.updateOfflineTD(JSON.stringify(updatedParsedTD, null, 2)); + }; + return (
    = (props) => { onToggle={() => setIsExpanded(!isExpanded)} > -

    {property.title ?? props.propName}

    +

    + {property.title ?? props.propName} +

    + {isExpanded && ( - + <> + + + )}
    @@ -113,7 +162,10 @@ const Property: React.FC = (props) => { {property.description} )} -
      {attributes}
    + +
      + {attributes} +
    @@ -123,7 +175,7 @@ const Property: React.FC = (props) => { = (props) => { key={`${i}-${form.href}`} propName={props.propName} form={form} - interactionType={"property"} + interactionType="property" /> ))}
    From 647a87c8bfb820e9b46c32c6e93fa80342062033 Mon Sep 17 00:00:00 2001 From: Ayush Date: Mon, 12 Jan 2026 16:03:44 +0530 Subject: [PATCH 2/9] Refactor affordance copy logic and improve UX Signed-off-by: Ayush --- src/components/TDViewer/components/Action.tsx | 101 +++++++++--------- src/components/TDViewer/components/Event.tsx | 52 +++++---- .../TDViewer/components/Property.tsx | 54 +++++----- src/utils/copyAffordance.ts | 69 ++++++++++++ 4 files changed, 172 insertions(+), 104 deletions(-) create mode 100644 src/utils/copyAffordance.ts diff --git a/src/components/TDViewer/components/Action.tsx b/src/components/TDViewer/components/Action.tsx index 910e3163..f4758c43 100644 --- a/src/components/TDViewer/components/Action.tsx +++ b/src/components/TDViewer/components/Action.tsx @@ -22,6 +22,7 @@ import InfoIconWrapper from "../../base/InfoIconWrapper"; import { getFormsTooltipContent } from "../../../utils/TooltipMapper"; import Form from "./Form"; import AddFormElement from "../base/AddFormElement"; +import { copyAffordance } from "../../../utils/copyAffordance"; const alreadyRenderedKeys = ["title", "forms", "description"]; @@ -31,7 +32,7 @@ const Action: React.FC = (props) => { const addFormDialog = React.useRef(); const handleOpenAddFormDialog = () => { - addFormDialog.current.openModal(); + addFormDialog.current?.openModal(); }; if ( @@ -64,34 +65,33 @@ const Action: React.FC = (props) => { context.removeOneOfAKindReducer("actions", props.actionName); }; + const handleCopyAction = () => { - const parsedTD = context.parsedTD; - if (!parsedTD || !parsedTD.actions) { - console.error("parsedTD or actions missing", parsedTD); - return; - } - const originalName = props.actionName; - let newName = `${originalName}_copy`; - let counter = 1; - while (parsedTD.actions[newName]) { - newName = `${originalName}_copy_${counter++}`; + try { + const { updatedTD, newName } = copyAffordance({ + parsedTD: context.parsedTD, + section: "actions", + originalName: props.actionName, + affordance: action, + }); + + context.updateOfflineTD(JSON.stringify(updatedTD, null, 2)); + + setIsExpanded(true); + + setTimeout(() => { + document + .getElementById(`action-${newName}`) + ?.scrollIntoView({ behavior: "smooth", block: "center" }); + }, 100); + } catch (e) { + console.error(e); } - const copiedAction = structuredClone(action); - if (copiedAction.title) { - copiedAction.title = `${copiedAction.title} copy`; - } - const updatedParsedTD = { - ...parsedTD, - actions: { - ...parsedTD.actions, - [newName]: copiedAction, - }, - }; - context.updateOfflineTD(JSON.stringify(updatedParsedTD, null, 2)); }; - + return (
    setIsExpanded(!isExpanded)} @@ -106,33 +106,32 @@ const Action: React.FC = (props) => { {isExpanded && ( - <> - - - - -)} - + <> + + + + + )}
    diff --git a/src/components/TDViewer/components/Event.tsx b/src/components/TDViewer/components/Event.tsx index ff5da47e..53b450a8 100644 --- a/src/components/TDViewer/components/Event.tsx +++ b/src/components/TDViewer/components/Event.tsx @@ -22,6 +22,7 @@ import InfoIconWrapper from "../../base/InfoIconWrapper"; import { getFormsTooltipContent } from "../../../utils/TooltipMapper"; import Form from "./Form"; import AddFormElement from "../base/AddFormElement"; +import { copyAffordance } from "../../../utils/copyAffordance"; const alreadyRenderedKeys = ["title", "forms", "description"]; @@ -59,38 +60,37 @@ const Event: React.FC = (props) => { {x} : {JSON.stringify(attributeListObject[x])} )); - + const handleDeleteEventClicked = () => { context.removeOneOfAKindReducer("events", props.eventName); }; + const handleCopyEvent = () => { - const parsedTD = context.parsedTD; - if (!parsedTD || !parsedTD.events) { - console.error("parsedTD or events missing", parsedTD); - return; - } - const originalName = props.eventName; - let newName = `${originalName}_copy`; - let counter = 1; - while (parsedTD.events[newName]) { - newName = `${originalName}_copy_${counter++}`; + try { + const { updatedTD, newName } = copyAffordance({ + parsedTD: context.parsedTD, + section: "events", + originalName: props.eventName, + affordance: event, + }); + + context.updateOfflineTD(JSON.stringify(updatedTD, null, 2)); + + setIsExpanded(true); + + setTimeout(() => { + document + .getElementById(`event-${newName}`) + ?.scrollIntoView({ behavior: "smooth", block: "center" }); + }, 100); + } catch (e) { + console.error(e); } - const copiedEvent = structuredClone(event); - const baseTitle = event.title ?? props.eventName; - copiedEvent.title = `${baseTitle} copy`; - const updatedParsedTD = { - ...parsedTD, - events: { - ...parsedTD.events, - [newName]: copiedEvent, - }, - }; - - context.updateOfflineTD(JSON.stringify(updatedParsedTD, null, 2)); }; return (
    setIsExpanded(!isExpanded)} @@ -106,6 +106,7 @@ const Event: React.FC = (props) => { {isExpanded && ( <> + {/* COPY BUTTON */} + + {/* DELETE BUTTON */}
    )} +
      {attributes}
    @@ -151,6 +155,7 @@ const Event: React.FC = (props) => {

    Forms

    + = (props) => { interactionName={props.eventName} ref={addFormDialog} /> + {forms.map((form, i) => (
    = (props) => { } const property = props.prop; - const forms = separateForms(structuredClone(props.prop.forms)); const attributeListObject = buildAttributeListObject( @@ -81,40 +81,31 @@ const Property: React.FC = (props) => { }; const handleCopyProperty = () => { - const parsedTD = context.parsedTD; - - if (!parsedTD || !parsedTD.properties) { - console.error("parsedTD or properties missing", parsedTD); - return; - } - - const originalName = props.propName; - - let newName = `${originalName}_copy`; - let counter = 1; - - while (parsedTD.properties[newName]) { - newName = `${originalName}_copy_${counter++}`; + try { + const { updatedTD, newName } = copyAffordance({ + parsedTD: context.parsedTD, + section: "properties", + originalName: props.propName, + affordance: property, + }); + + context.updateOfflineTD(JSON.stringify(updatedTD, null, 2)); + + setIsExpanded(true); + + setTimeout(() => { + document + .getElementById(`property-${newName}`) + ?.scrollIntoView({ behavior: "smooth", block: "center" }); + }, 100); + } catch (e) { + console.error(e); } - - const copiedProperty = structuredClone(property); - - const baseTitle = property.title ?? props.propName; - copiedProperty.title = `${baseTitle} copy`; - - const updatedParsedTD = { - ...parsedTD, - properties: { - ...parsedTD.properties, - [newName]: copiedProperty, - }, - }; - - context.updateOfflineTD(JSON.stringify(updatedParsedTD, null, 2)); }; return (
    setIsExpanded(!isExpanded)} @@ -130,6 +121,7 @@ const Property: React.FC = (props) => { {isExpanded && ( <> + {/* COPY BUTTON */} + + {/* DELETE BUTTON */} -
    ); }; - -export default Action; +export default Action; \ No newline at end of file diff --git a/src/components/TDViewer/components/Event.tsx b/src/components/TDViewer/components/Event.tsx index 1b489478..dc0e87ea 100644 --- a/src/components/TDViewer/components/Event.tsx +++ b/src/components/TDViewer/components/Event.tsx @@ -31,36 +31,20 @@ const Event: React.FC = (props) => { const [isExpanded, setIsExpanded] = useState(false); const addFormDialog = useRef<{ openModal: () => void }>(null); + const handleOpenAddFormDialog = () => { addFormDialog.current?.openModal(); }; - if ( - Object.keys(props.event).length === 0 && - props.event.constructor !== Object - ) { - return ( -
    - Event could not be rendered because mandatory fields are missing. -
    - ); - } - const event = props.event; - const forms = separateForms(props.event.forms); + const forms = separateForms(event.forms); const attributeListObject = buildAttributeListObject( { name: props.eventName }, - props.event, + event, alreadyRenderedKeys ); - const attributes = Object.keys(attributeListObject).map((x) => ( -
  • - {x} : {JSON.stringify(attributeListObject[x])} -
  • - )); - const handleDeleteEventClicked = () => { context.removeOneOfAKindReducer("events", props.eventName); }; @@ -75,7 +59,6 @@ const Event: React.FC = (props) => { }); context.updateOfflineTD(JSON.stringify(updatedTD, null, 2)); - setIsExpanded(true); setTimeout(() => { @@ -104,9 +87,8 @@ const Event: React.FC = (props) => { {isExpanded && ( <> - {/* COPY BUTTON */} - {/* DELETE BUTTON */} - {/* DELETE BUTTON */} - - + { + e.preventDefault(); + e.stopPropagation(); + handleCopy(); + }} + onDelete={(e) => { + e.preventDefault(); + e.stopPropagation(); + handleDelete(); + }} + /> )} -
    +
    {action.description && (
    {action.description}
    )} -
      {attributes}
    +
      + {Object.entries(attributeListObject).map(([k, v]) => ( +
    • + {k}: {JSON.stringify(v)} +
    • + ))} +
    -
    - -

    Forms

    -
    -
    + +

    Forms

    +
    - + addFormDialog.current?.openModal()} /> @@ -164,7 +118,7 @@ const Action: React.FC = (props) => { ))} @@ -172,4 +126,5 @@ const Action: React.FC = (props) => {
    ); }; -export default Action; \ No newline at end of file + +export default Action; diff --git a/src/components/TDViewer/components/AffordanceButtons.tsx b/src/components/TDViewer/components/AffordanceButtons.tsx new file mode 100644 index 00000000..a48fad42 --- /dev/null +++ b/src/components/TDViewer/components/AffordanceButtons.tsx @@ -0,0 +1,50 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +import React from "react"; +import { Copy, Trash2 } from "react-feather"; + +interface Props { + onCopy: (e: React.MouseEvent) => void; + onDelete: (e: React.MouseEvent) => void; + copyTitle: string; + deleteTitle: string; +} + +const AffordanceButtons: React.FC = ({ + onCopy, + onDelete, + copyTitle, + deleteTitle, +}) => { + return ( +
    + + + +
    + ); +}; + +export default AffordanceButtons; diff --git a/src/components/TDViewer/components/Event.tsx b/src/components/TDViewer/components/Event.tsx index dc0e87ea..cd26bab5 100644 --- a/src/components/TDViewer/components/Event.tsx +++ b/src/components/TDViewer/components/Event.tsx @@ -11,7 +11,6 @@ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ import React, { useContext, useState, useRef } from "react"; -import { Trash2, Copy } from "react-feather"; import ediTDorContext from "../../../context/ediTDorContext"; import { buildAttributeListObject, @@ -23,104 +22,73 @@ import { getFormsTooltipContent } from "../../../utils/TooltipMapper"; import Form from "./Form"; import AddFormElement from "../base/AddFormElement"; import { copyAffordance } from "../../../utils/copyAffordance"; - +import AffordanceButtons from "./AffordanceButtons"; const alreadyRenderedKeys = ["title", "forms", "description"]; - -const Event: React.FC = (props) => { +interface IEvent { + event: any; + eventName: string; +} +const Event: React.FC = ({ event, eventName }) => { const context = useContext(ediTDorContext); const [isExpanded, setIsExpanded] = useState(false); - const addFormDialog = useRef<{ openModal: () => void }>(null); - - const handleOpenAddFormDialog = () => { - addFormDialog.current?.openModal(); - }; - - const event = props.event; const forms = separateForms(event.forms); - const attributeListObject = buildAttributeListObject( - { name: props.eventName }, + { name: eventName }, event, alreadyRenderedKeys ); - - const handleDeleteEventClicked = () => { - context.removeOneOfAKindReducer("events", props.eventName); + const handleDelete = () => { + context.removeOneOfAKindReducer("events", eventName); }; - - const handleCopyEvent = () => { - try { - const { updatedTD, newName } = copyAffordance({ - parsedTD: context.parsedTD, - section: "events", - originalName: props.eventName, - affordance: event, - }); - - context.updateOfflineTD(JSON.stringify(updatedTD, null, 2)); - setIsExpanded(true); - - setTimeout(() => { - document - .getElementById(`event-${newName}`) - ?.scrollIntoView({ behavior: "smooth", block: "center" }); - }, 100); - } catch (e) { - console.error(e); - } + const handleCopy = () => { + const { updatedTD, newName } = copyAffordance({ + parsedTD: context.parsedTD, + section: "events", + originalName: eventName, + affordance: event, + }); + context.updateOfflineTD(JSON.stringify(updatedTD, null, 2)); + setIsExpanded(true); + setTimeout(() => { + document + .getElementById(`event-${newName}`) + ?.scrollIntoView({ behavior: "smooth", block: "center" }); + }, 100); }; return (
    setIsExpanded(!isExpanded)} > - -
    {event.title ?? props.eventName}
    - + +
    {event.title ?? eventName}
    {isExpanded && ( - <> - - - - + { + e.preventDefault(); + e.stopPropagation(); + handleCopy(); + }} + onDelete={(e) => { + e.preventDefault(); + e.stopPropagation(); + handleDelete(); + }} + /> )}
    - -
    +
    {event.description && (
    {event.description}
    )} -
      {Object.entries(attributeListObject).map(([k, v]) => (
    • @@ -128,25 +96,21 @@ const Event: React.FC = (props) => {
    • ))}
    -

    Forms

    - - - + addFormDialog.current?.openModal()} /> - {forms.map((form, i) => ( ))} diff --git a/src/components/TDViewer/components/Property.tsx b/src/components/TDViewer/components/Property.tsx index 5317c4db..9f65a88b 100644 --- a/src/components/TDViewer/components/Property.tsx +++ b/src/components/TDViewer/components/Property.tsx @@ -11,7 +11,6 @@ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ import React, { useContext, useState, useRef } from "react"; -import { Trash2, Copy } from "react-feather"; import ediTDorContext from "../../../context/ediTDorContext"; import { buildAttributeListObject, @@ -23,100 +22,73 @@ import Form from "./Form"; import AddFormDialog from "../../Dialogs/AddFormDialog"; import AddFormElement from "../base/AddFormElement"; import { copyAffordance } from "../../../utils/copyAffordance"; - +import AffordanceButtons from "./AffordanceButtons"; const alreadyRenderedKeys = ["title", "forms", "description"]; - -const Property: React.FC = (props) => { +interface IProperty { + prop: any; + propName: string; +} +const Property: React.FC = ({ prop, propName }) => { const context = useContext(ediTDorContext); const [isExpanded, setIsExpanded] = useState(false); - const addFormDialog = useRef<{ openModal: () => void }>(null); - - const property = props.prop; - const forms = separateForms(structuredClone(property.forms)); - + const forms = separateForms(structuredClone(prop.forms)); const attributeListObject = buildAttributeListObject( - { name: props.propName }, - property, + { name: propName }, + prop, alreadyRenderedKeys ); - - const handleDeletePropertyClicked = () => { - context.removeOneOfAKindReducer("properties", props.propName); + const handleDeleteProperty = () => { + context.removeOneOfAKindReducer("properties", propName); }; - const handleCopyProperty = () => { - try { - const { updatedTD, newName } = copyAffordance({ - parsedTD: context.parsedTD, - section: "properties", - originalName: props.propName, - affordance: property, - }); - - context.updateOfflineTD(JSON.stringify(updatedTD, null, 2)); - setIsExpanded(true); - - setTimeout(() => { - document - .getElementById(`property-${newName}`) - ?.scrollIntoView({ behavior: "smooth", block: "center" }); - }, 100); - } catch (e) { - console.error(e); - } + const { updatedTD, newName } = copyAffordance({ + parsedTD: context.parsedTD, + section: "properties", + originalName: propName, + affordance: prop, + }); + context.updateOfflineTD(JSON.stringify(updatedTD, null, 2)); + setIsExpanded(true); + setTimeout(() => { + document + .getElementById(`property-${newName}`) + ?.scrollIntoView({ behavior: "smooth", block: "center" }); + }, 100); }; return (
    setIsExpanded(!isExpanded)} > - -

    {property.title ?? props.propName}

    - + +

    {prop.title ?? propName}

    {isExpanded && ( - <> - - - - + { + e.preventDefault(); + e.stopPropagation(); + handleCopyProperty(); + }} + onDelete={(e) => { + e.preventDefault(); + e.stopPropagation(); + handleDeleteProperty(); + }} + /> )}
    - -
    - {property.description && ( +
    + {prop.description && (
    - {property.description} + {prop.description}
    )} -
      {Object.entries(attributeListObject).map(([k, v]) => (
    • @@ -124,24 +96,20 @@ const Property: React.FC = (props) => {
    • ))}
    -

    Forms

    - addFormDialog.current?.openModal()} /> - - {forms.map((form, i) => ( diff --git a/src/utils/copyAffordance.ts b/src/utils/copyAffordance.ts index cff087ed..4d855106 100644 --- a/src/utils/copyAffordance.ts +++ b/src/utils/copyAffordance.ts @@ -10,8 +10,6 @@ * * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ - -import test from "node:test"; import type { InteractionKey } from "../types/global"; interface CopyAffordanceParams { From 644516680a8b6d8b5579149ee29622aa8e2e2ede Mon Sep 17 00:00:00 2001 From: Ricardo Silva Date: Fri, 24 Apr 2026 15:49:09 +0100 Subject: [PATCH 7/9] fix race condition onToggle Signed-off-by: Ricardo Silva --- src/components/TDViewer/components/Action.tsx | 2 +- src/components/TDViewer/components/Event.tsx | 2 +- .../TDViewer/components/Property.tsx | 19 ++++++++++++++++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/components/TDViewer/components/Action.tsx b/src/components/TDViewer/components/Action.tsx index d77319b4..151cbcf7 100644 --- a/src/components/TDViewer/components/Action.tsx +++ b/src/components/TDViewer/components/Action.tsx @@ -63,7 +63,7 @@ const Action: React.FC = ({ action, actionName }) => { id={`action-${actionName}`} className={`mb-2 ${isExpanded ? "overflow-hidden rounded-lg bg-gray-500" : ""}`} open={isExpanded} - onToggle={() => setIsExpanded(!isExpanded)} + onToggle={(e) => setIsExpanded(e.currentTarget.open)} >

    {action.title ?? actionName}

    diff --git a/src/components/TDViewer/components/Event.tsx b/src/components/TDViewer/components/Event.tsx index cd26bab5..421cf5b1 100644 --- a/src/components/TDViewer/components/Event.tsx +++ b/src/components/TDViewer/components/Event.tsx @@ -62,7 +62,7 @@ const Event: React.FC = ({ event, eventName }) => { id={`event-${eventName}`} className={`mb-2 ${isExpanded ? "overflow-hidden rounded-lg bg-gray-500" : ""}`} open={isExpanded} - onToggle={() => setIsExpanded(!isExpanded)} + onToggle={(e) => setIsExpanded(e.currentTarget.open)} >
    {event.title ?? eventName}
    diff --git a/src/components/TDViewer/components/Property.tsx b/src/components/TDViewer/components/Property.tsx index 9f65a88b..26c71bb8 100644 --- a/src/components/TDViewer/components/Property.tsx +++ b/src/components/TDViewer/components/Property.tsx @@ -23,11 +23,24 @@ import AddFormDialog from "../../Dialogs/AddFormDialog"; import AddFormElement from "../base/AddFormElement"; import { copyAffordance } from "../../../utils/copyAffordance"; import AffordanceButtons from "./AffordanceButtons"; -const alreadyRenderedKeys = ["title", "forms", "description"]; + interface IProperty { - prop: any; + prop: { + title: string; + forms: Array<{ + href: string; + contentType?: string; + op?: string | string[]; + [key: string]: any; + }>; + description?: string; + [key: string]: any; + }; propName: string; } + +const alreadyRenderedKeys = ["title", "forms", "description"]; + const Property: React.FC = ({ prop, propName }) => { const context = useContext(ediTDorContext); const [isExpanded, setIsExpanded] = useState(false); @@ -62,7 +75,7 @@ const Property: React.FC = ({ prop, propName }) => { id={`property-${propName}`} className={`mb-2 ${isExpanded ? "overflow-hidden rounded-lg bg-gray-500" : ""}`} open={isExpanded} - onToggle={() => setIsExpanded(!isExpanded)} + onToggle={(e) => setIsExpanded(e.currentTarget.open)} >

    {prop.title ?? propName}

    From fec6f3a7f28ae5ea483a98b6d337255962fc6b55 Mon Sep 17 00:00:00 2001 From: Ricardo Silva Date: Fri, 24 Apr 2026 17:06:05 +0100 Subject: [PATCH 8/9] fix ts errors and narrowing down ts interfaces Signed-off-by: Ricardo Silva --- src/components/Dialogs/AddFormDialog.tsx | 31 ++++++++++--------- src/components/Dialogs/AddPropertyDialog.tsx | 8 ++--- src/components/TDViewer/components/Action.tsx | 9 ++++-- .../TDViewer/components/AffordanceButtons.tsx | 4 +-- src/components/TDViewer/components/Event.tsx | 7 +++-- src/components/TDViewer/components/Form.tsx | 9 ++---- .../TDViewer/components/Property.tsx | 17 +++------- src/types/form.d.ts | 27 +++++++++++++--- src/utils/tdOperations.ts | 28 ++++++++++------- 9 files changed, 79 insertions(+), 61 deletions(-) diff --git a/src/components/Dialogs/AddFormDialog.tsx b/src/components/Dialogs/AddFormDialog.tsx index 4c2c3658..954ded0b 100644 --- a/src/components/Dialogs/AddFormDialog.tsx +++ b/src/components/Dialogs/AddFormDialog.tsx @@ -10,12 +10,18 @@ * * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ -import { forwardRef, useContext, useState, useImperativeHandle } from "react"; +import React, { + forwardRef, + useContext, + useState, + useImperativeHandle, +} from "react"; import ReactDOM from "react-dom"; import ediTDorContext from "../../context/ediTDorContext"; import { checkIfFormIsInItem } from "../../utils/tdOperations"; import DialogTemplate from "./DialogTemplate"; import AddForm from "../App/AddForm"; +import type { IExplicitForm, IInteractionAffordance } from "../../types/form"; export type OperationsType = "property" | "action" | "event" | "thing" | ""; export type OperationsMap = PropertyMap | ActionMap | EventMap | ThingMap; @@ -36,23 +42,18 @@ type ThingMap = ] | []; -export interface AddFormDialogRef { +export interface IAddFormDialogRef { openModal: () => void; close: () => void; } -export interface ExplicitForm { - op: string[] | string; - href: string; -} - -interface AddFormDialogProps { +interface IAddFormDialogProps { type?: OperationsType; - interaction?: { forms?: ExplicitForm[]; type?: string }; + interaction?: IInteractionAffordance; interactionName?: string; } -const AddFormDialog = forwardRef( +const AddFormDialog = forwardRef( (props, ref) => { const context: IEdiTDorContext = useContext(ediTDorContext); const [display, setDisplay] = useState(() => { @@ -64,7 +65,7 @@ const AddFormDialog = forwardRef( const type: OperationsType = props.type || ""; const name = type && type[0].toUpperCase() + type.slice(1); - const interaction = props.interaction ?? {}; + const interaction = props.interaction; const interactionName = props.interactionName ?? ""; useImperativeHandle(ref, () => { @@ -111,10 +112,10 @@ const AddFormDialog = forwardRef( } }; - const checkDuplicates = (form: ExplicitForm): boolean => { + const checkDuplicates = (form: IExplicitForm): boolean => { const isDuplicate: boolean = - interaction.forms !== undefined - ? checkIfFormIsInItem(form, interaction as { forms: ExplicitForm[] }) + interaction?.forms !== undefined + ? checkIfFormIsInItem(form, { forms: interaction.forms }) : false; return isDuplicate; }; @@ -131,7 +132,7 @@ const AddFormDialog = forwardRef( }; const onHandleEventRightButton = () => { - const form: ExplicitForm = { + const form: IExplicitForm = { op: operations(type) .map((x) => { const element = document.getElementById( diff --git a/src/components/Dialogs/AddPropertyDialog.tsx b/src/components/Dialogs/AddPropertyDialog.tsx index cafef313..8abc9d4a 100644 --- a/src/components/Dialogs/AddPropertyDialog.tsx +++ b/src/components/Dialogs/AddPropertyDialog.tsx @@ -27,12 +27,12 @@ import DialogTemplate from "./DialogTemplate"; const NO_TYPE = "undefined"; -export interface AddPropertyDialogRef { +export interface IAddPropertyDialogRef { openModal: () => void; close?: () => void; } -interface Property { +interface IProperty { title: string; description?: string; type?: string; @@ -43,7 +43,7 @@ interface Property { properties?: Record; } -export const AddPropertyDialog = forwardRef( +export const AddPropertyDialog = forwardRef( (_, ref) => { const context = useContext(ediTDorContext); const [display, setDisplay] = React.useState(() => { @@ -115,7 +115,7 @@ export const AddPropertyDialog = forwardRef( return; } - const property: Property = { + const property: IProperty = { title: (document.getElementById(`${type}-title`) as HTMLInputElement) .value, observable: ( diff --git a/src/components/TDViewer/components/Action.tsx b/src/components/TDViewer/components/Action.tsx index 151cbcf7..35712b61 100644 --- a/src/components/TDViewer/components/Action.tsx +++ b/src/components/TDViewer/components/Action.tsx @@ -23,16 +23,19 @@ import Form from "./Form"; import AddFormElement from "../base/AddFormElement"; import { copyAffordance } from "../../../utils/copyAffordance"; import AffordanceButtons from "./AffordanceButtons"; +import type { IInteractionAffordance } from "../../../types/form"; const alreadyRenderedKeys = ["title", "forms", "description"]; interface IAction { - action: any; + action: IInteractionAffordance; actionName: string; } const Action: React.FC = ({ action, actionName }) => { const context = useContext(ediTDorContext); const [isExpanded, setIsExpanded] = useState(false); - const addFormDialog = useRef<{ openModal: () => void }>(null); + const addFormDialog = useRef<{ openModal: () => void; close: () => void }>( + null + ); const forms = separateForms(action.forms); const attributeListObject = buildAttributeListObject( { name: actionName }, @@ -116,7 +119,7 @@ const Action: React.FC = ({ action, actionName }) => { {forms.map((form, i) => ( void; onDelete: (e: React.MouseEvent) => void; copyTitle: string; deleteTitle: string; } -const AffordanceButtons: React.FC = ({ +const AffordanceButtons: React.FC = ({ onCopy, onDelete, copyTitle, diff --git a/src/components/TDViewer/components/Event.tsx b/src/components/TDViewer/components/Event.tsx index 421cf5b1..8c9cc0b9 100644 --- a/src/components/TDViewer/components/Event.tsx +++ b/src/components/TDViewer/components/Event.tsx @@ -23,15 +23,18 @@ import Form from "./Form"; import AddFormElement from "../base/AddFormElement"; import { copyAffordance } from "../../../utils/copyAffordance"; import AffordanceButtons from "./AffordanceButtons"; +import type { InteractionAffordance } from "../../../types/form"; const alreadyRenderedKeys = ["title", "forms", "description"]; interface IEvent { - event: any; + event: InteractionAffordance; eventName: string; } const Event: React.FC = ({ event, eventName }) => { const context = useContext(ediTDorContext); const [isExpanded, setIsExpanded] = useState(false); - const addFormDialog = useRef<{ openModal: () => void }>(null); + const addFormDialog = useRef<{ openModal: () => void; close: () => void }>( + null + ); const forms = separateForms(event.forms); const attributeListObject = buildAttributeListObject( { name: eventName }, diff --git a/src/components/TDViewer/components/Form.tsx b/src/components/TDViewer/components/Form.tsx index 59eeedf7..b1937fb6 100644 --- a/src/components/TDViewer/components/Form.tsx +++ b/src/components/TDViewer/components/Form.tsx @@ -15,7 +15,7 @@ import ediTDorContext from "../../../context/ediTDorContext"; import FormDetails from "../base/FormDetails"; import UndefinedForm from "../base/UndefinedForm"; import { formConfigurations } from "../../../services/form"; -import type { OpKeys } from "../../../types/form"; +import type { FormProps, OpKeys } from "../../../types/form"; const typeToJSONKey = (type: string): string => { const typeToJSONKey: Record = { @@ -29,12 +29,7 @@ const typeToJSONKey = (type: string): string => { }; interface IFormComponentProps { - form: { - href: string; - contentType: string; - op: string; - actualIndex: number; - }; + form: FormProps; propName: string; interactionType: "thing" | "property" | "action" | "event"; } diff --git a/src/components/TDViewer/components/Property.tsx b/src/components/TDViewer/components/Property.tsx index 26c71bb8..1d7b3bd5 100644 --- a/src/components/TDViewer/components/Property.tsx +++ b/src/components/TDViewer/components/Property.tsx @@ -23,19 +23,10 @@ import AddFormDialog from "../../Dialogs/AddFormDialog"; import AddFormElement from "../base/AddFormElement"; import { copyAffordance } from "../../../utils/copyAffordance"; import AffordanceButtons from "./AffordanceButtons"; +import type { IInteractionAffordance } from "../../../types/form"; interface IProperty { - prop: { - title: string; - forms: Array<{ - href: string; - contentType?: string; - op?: string | string[]; - [key: string]: any; - }>; - description?: string; - [key: string]: any; - }; + prop: IInteractionAffordance; propName: string; } @@ -44,7 +35,9 @@ const alreadyRenderedKeys = ["title", "forms", "description"]; const Property: React.FC = ({ prop, propName }) => { const context = useContext(ediTDorContext); const [isExpanded, setIsExpanded] = useState(false); - const addFormDialog = useRef<{ openModal: () => void }>(null); + const addFormDialog = useRef<{ openModal: () => void; close: () => void }>( + null + ); const forms = separateForms(structuredClone(prop.forms)); const attributeListObject = buildAttributeListObject( { name: propName }, diff --git a/src/types/form.d.ts b/src/types/form.d.ts index ab2619f1..f6b49378 100644 --- a/src/types/form.d.ts +++ b/src/types/form.d.ts @@ -9,7 +9,7 @@ * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and * * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 - ********************************************************************************/ ß; + ********************************************************************************/ import { ThingDescription } from "wot-thing-description-types"; type ServientCallback = ( @@ -26,14 +26,31 @@ export interface IFormConfigurations { callback: ServientCallback | null; } -interface IFormProps { +export interface IInteractionForm { href: string; + op?: string | string[]; + contentType?: string; + [key: string]: unknown; +} + +export interface IExplicitForm extends IInteractionForm { op: string | string[]; - propName: string; - actualIndex: number; } -type OpKeys = +export interface IInteractionAffordance { + title?: string; + description?: string; + type?: string; + forms?: IInteractionForm[]; + [key: string]: unknown; +} + +export type IFormProps = IInteractionForm & { + actualIndex: number; + [key: string]: unknown; +}; + +export type OpKeys = | "readproperty" | "writeproperty" | "observeproperty" diff --git a/src/utils/tdOperations.ts b/src/utils/tdOperations.ts index 28b08f9a..6a167649 100644 --- a/src/utils/tdOperations.ts +++ b/src/utils/tdOperations.ts @@ -11,8 +11,8 @@ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ -import { ExplicitForm } from "components/Dialogs/AddFormDialog"; import { direction } from "direction"; +import type { ExplicitForm, FormProps, InteractionForm } from "types/form"; /** * @param {Object} td @@ -69,26 +69,32 @@ export const buildAttributeListObject = ( * Converts Forms that have an array as the "op" value into multiple separate Forms * which only have a string as "op" value. */ -export const separateForms = (forms) => { - if (forms === undefined && !forms) { +export const separateForms = ( + forms: InteractionForm[] | undefined +): FormProps[] => { + if (forms === undefined || forms === null) { return []; } - const newForms = []; + const newForms: FormProps[] = []; for (let i = 0; i < forms.length; i++) { const form = forms[i]; if (!Array.isArray(form.op)) { - form.actualIndex = i; - newForms.push(form); + newForms.push({ + ...form, + actualIndex: i, + }); continue; } for (let j = 0; j < form.op.length; j++) { - const temp = { ...form }; - temp.op = form.op[j]; - temp.actualIndex = i; + const temp: FormProps = { + ...form, + op: form.op[j], + actualIndex: i, + }; newForms.push(temp); } } @@ -110,7 +116,7 @@ export const checkIfLinkIsInItem = (link, itemToCheck) => { export const checkIfFormIsInItem = ( form: ExplicitForm, - itemToCheck: { forms: ExplicitForm[] } + itemToCheck: { forms: InteractionForm[] } ): boolean => { for (const element of itemToCheck.forms) { if (typeof element.op === "undefined") { @@ -148,7 +154,7 @@ export const checkIfFormIsInItem = ( export const checkIfFormIsInElement = ( form: ExplicitForm & { op: string }, - element: ExplicitForm + element: InteractionForm ): boolean => { if (typeof element.op === "undefined") { return false; From a32b15bb9e26cfbc4cf5c4d413e8c29e681d72b5 Mon Sep 17 00:00:00 2001 From: Ricardo Silva Date: Fri, 24 Apr 2026 18:24:04 +0100 Subject: [PATCH 9/9] test add integration tests and refactor Signed-off-by: Ricardo Silva --- src/components/TDViewer/components/Action.tsx | 36 +- .../TDViewer/components/AffordanceButtons.tsx | 2 + src/components/TDViewer/components/Event.tsx | 35 +- .../components/InteractionSection.tsx | 85 ++++- .../TDViewer/components/Property.tsx | 36 +- src/hooks/useCopiedAffordanceFocus.ts | 66 ++++ .../InteractionSection.integration.test.tsx | 315 ++++++++++++++++++ 7 files changed, 505 insertions(+), 70 deletions(-) create mode 100644 src/hooks/useCopiedAffordanceFocus.ts create mode 100644 src/tests/InteractionSection.integration.test.tsx diff --git a/src/components/TDViewer/components/Action.tsx b/src/components/TDViewer/components/Action.tsx index 35712b61..93f52009 100644 --- a/src/components/TDViewer/components/Action.tsx +++ b/src/components/TDViewer/components/Action.tsx @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ -import React, { useContext, useState, useRef } from "react"; +import React, { useContext, useRef } from "react"; import ediTDorContext from "../../../context/ediTDorContext"; import { buildAttributeListObject, @@ -21,21 +21,29 @@ import InfoIconWrapper from "../../base/InfoIconWrapper"; import { getFormsTooltipContent } from "../../../utils/TooltipMapper"; import Form from "./Form"; import AddFormElement from "../base/AddFormElement"; -import { copyAffordance } from "../../../utils/copyAffordance"; import AffordanceButtons from "./AffordanceButtons"; import type { IInteractionAffordance } from "../../../types/form"; +import { useCopiedAffordanceFocus } from "../../../hooks/useCopiedAffordanceFocus"; const alreadyRenderedKeys = ["title", "forms", "description"]; interface IAction { action: IInteractionAffordance; actionName: string; + copiedToken?: number; + onCopy: () => void; } -const Action: React.FC = ({ action, actionName }) => { +const Action: React.FC = ({ + action, + actionName, + copiedToken, + onCopy, +}) => { const context = useContext(ediTDorContext); - const [isExpanded, setIsExpanded] = useState(false); const addFormDialog = useRef<{ openModal: () => void; close: () => void }>( null ); + const { containerRef, isExpanded, isHighlighted, setIsExpanded } = + useCopiedAffordanceFocus({ copiedToken }); const forms = separateForms(action.forms); const attributeListObject = buildAttributeListObject( { name: actionName }, @@ -45,26 +53,12 @@ const Action: React.FC = ({ action, actionName }) => { const handleDelete = () => { context.removeOneOfAKindReducer("actions", actionName); }; - const handleCopy = () => { - const { updatedTD, newName } = copyAffordance({ - parsedTD: context.parsedTD, - section: "actions", - originalName: actionName, - affordance: action, - }); - context.updateOfflineTD(JSON.stringify(updatedTD, null, 2)); - setIsExpanded(true); - setTimeout(() => { - document - .getElementById(`action-${newName}`) - ?.scrollIntoView({ behavior: "smooth", block: "center" }); - }, 100); - }; return (
    setIsExpanded(e.currentTarget.open)} > @@ -78,7 +72,7 @@ const Action: React.FC = ({ action, actionName }) => { onCopy={(e) => { e.preventDefault(); e.stopPropagation(); - handleCopy(); + onCopy(); }} onDelete={(e) => { e.preventDefault(); diff --git a/src/components/TDViewer/components/AffordanceButtons.tsx b/src/components/TDViewer/components/AffordanceButtons.tsx index 837b7e0d..349e924b 100644 --- a/src/components/TDViewer/components/AffordanceButtons.tsx +++ b/src/components/TDViewer/components/AffordanceButtons.tsx @@ -29,6 +29,7 @@ const AffordanceButtons: React.FC = ({ return (