Skip to content

Commit d794624

Browse files
Add copy button for TD affordances (actions, properties, events) (#181)
* Add copy button for TD affordances (actions, properties, events) Signed-off-by: Ayush <prakashayush414@gmail.com> * Refactor affordance copy logic and improve UX Signed-off-by: Ayush <prakashayush414@gmail.com> * chore: apply prettier formatting * fix: finalize copy affordance logic and tests * refactor: address review UI comments * refactor: restore typing and extract reusable AffordanceButtons component * fix race condition onToggle Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> * fix ts errors and narrowing down ts interfaces Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> * test add integration tests and refactor Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> --------- Signed-off-by: Ayush <prakashayush414@gmail.com> Signed-off-by: Ricardo Silva <rephyrus0877@protonmail.com> Co-authored-by: Ricardo Silva <rephyrus0877@protonmail.com>
1 parent 861035c commit d794624

15 files changed

Lines changed: 1017 additions & 272 deletions

src/components/Dialogs/AddFormDialog.tsx

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@
1010
*
1111
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
1212
********************************************************************************/
13-
import { forwardRef, useContext, useState, useImperativeHandle } from "react";
13+
import React, {
14+
forwardRef,
15+
useContext,
16+
useState,
17+
useImperativeHandle,
18+
} from "react";
1419
import ReactDOM from "react-dom";
1520
import ediTDorContext from "../../context/ediTDorContext";
1621
import { checkIfFormIsInItem } from "../../utils/tdOperations";
1722
import DialogTemplate from "./DialogTemplate";
1823
import AddForm from "../App/AddForm";
24+
import type { IExplicitForm, IInteractionAffordance } from "../../types/form";
1925

2026
export type OperationsType = "property" | "action" | "event" | "thing" | "";
2127
export type OperationsMap = PropertyMap | ActionMap | EventMap | ThingMap;
@@ -36,23 +42,18 @@ type ThingMap =
3642
]
3743
| [];
3844

39-
export interface AddFormDialogRef {
45+
export interface IAddFormDialogRef {
4046
openModal: () => void;
4147
close: () => void;
4248
}
4349

44-
export interface ExplicitForm {
45-
op: string[] | string;
46-
href: string;
47-
}
48-
49-
interface AddFormDialogProps {
50+
interface IAddFormDialogProps {
5051
type?: OperationsType;
51-
interaction?: { forms?: ExplicitForm[]; type?: string };
52+
interaction?: IInteractionAffordance;
5253
interactionName?: string;
5354
}
5455

55-
const AddFormDialog = forwardRef<AddFormDialogRef, AddFormDialogProps>(
56+
const AddFormDialog = forwardRef<IAddFormDialogRef, IAddFormDialogProps>(
5657
(props, ref) => {
5758
const context: IEdiTDorContext = useContext(ediTDorContext);
5859
const [display, setDisplay] = useState<boolean>(() => {
@@ -64,7 +65,7 @@ const AddFormDialog = forwardRef<AddFormDialogRef, AddFormDialogProps>(
6465

6566
const type: OperationsType = props.type || "";
6667
const name = type && type[0].toUpperCase() + type.slice(1);
67-
const interaction = props.interaction ?? {};
68+
const interaction = props.interaction;
6869
const interactionName = props.interactionName ?? "";
6970

7071
useImperativeHandle(ref, () => {
@@ -111,10 +112,10 @@ const AddFormDialog = forwardRef<AddFormDialogRef, AddFormDialogProps>(
111112
}
112113
};
113114

114-
const checkDuplicates = (form: ExplicitForm): boolean => {
115+
const checkDuplicates = (form: IExplicitForm): boolean => {
115116
const isDuplicate: boolean =
116-
interaction.forms !== undefined
117-
? checkIfFormIsInItem(form, interaction as { forms: ExplicitForm[] })
117+
interaction?.forms !== undefined
118+
? checkIfFormIsInItem(form, { forms: interaction.forms })
118119
: false;
119120
return isDuplicate;
120121
};
@@ -131,7 +132,7 @@ const AddFormDialog = forwardRef<AddFormDialogRef, AddFormDialogProps>(
131132
};
132133

133134
const onHandleEventRightButton = () => {
134-
const form: ExplicitForm = {
135+
const form: IExplicitForm = {
135136
op: operations(type)
136137
.map((x) => {
137138
const element = document.getElementById(

src/components/Dialogs/AddPropertyDialog.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ import DialogTemplate from "./DialogTemplate";
2727

2828
const NO_TYPE = "undefined";
2929

30-
export interface AddPropertyDialogRef {
30+
export interface IAddPropertyDialogRef {
3131
openModal: () => void;
3232
close?: () => void;
3333
}
3434

35-
interface Property {
35+
interface IProperty {
3636
title: string;
3737
description?: string;
3838
type?: string;
@@ -43,7 +43,7 @@ interface Property {
4343
properties?: Record<string, any>;
4444
}
4545

46-
export const AddPropertyDialog = forwardRef<AddPropertyDialogRef, {}>(
46+
export const AddPropertyDialog = forwardRef<IAddPropertyDialogRef, {}>(
4747
(_, ref) => {
4848
const context = useContext(ediTDorContext);
4949
const [display, setDisplay] = React.useState<boolean>(() => {
@@ -115,7 +115,7 @@ export const AddPropertyDialog = forwardRef<AddPropertyDialogRef, {}>(
115115
return;
116116
}
117117

118-
const property: Property = {
118+
const property: IProperty = {
119119
title: (document.getElementById(`${type}-title`) as HTMLInputElement)
120120
.value,
121121
observable: (

src/components/TDViewer/components/Action.tsx

Lines changed: 68 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
*
1111
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
1212
********************************************************************************/
13-
import React, { useContext, useState } from "react";
14-
import { Trash2 } from "react-feather";
13+
import React, { useContext, useRef } from "react";
1514
import ediTDorContext from "../../../context/ediTDorContext";
1615
import {
1716
buildAttributeListObject,
@@ -22,102 +21,103 @@ import InfoIconWrapper from "../../base/InfoIconWrapper";
2221
import { getFormsTooltipContent } from "../../../utils/TooltipMapper";
2322
import Form from "./Form";
2423
import AddFormElement from "../base/AddFormElement";
25-
24+
import AffordanceButtons from "./AffordanceButtons";
25+
import type { IInteractionAffordance } from "../../../types/form";
26+
import { useCopiedAffordanceFocus } from "../../../hooks/useCopiedAffordanceFocus";
2627
const alreadyRenderedKeys = ["title", "forms", "description"];
2728

28-
const Action: React.FC<any> = (props) => {
29+
interface IAction {
30+
action: IInteractionAffordance;
31+
actionName: string;
32+
copiedToken?: number;
33+
onCopy: () => void;
34+
}
35+
const Action: React.FC<IAction> = ({
36+
action,
37+
actionName,
38+
copiedToken,
39+
onCopy,
40+
}) => {
2941
const context = useContext(ediTDorContext);
30-
31-
const [isExpanded, setIsExpanded] = useState(false);
32-
33-
const addFormDialog = React.useRef();
34-
const handleOpenAddFormDialog = () => {
35-
addFormDialog.current.openModal();
36-
};
37-
38-
if (
39-
Object.keys(props.action).length === 0 &&
40-
props.action.constructor !== Object
41-
) {
42-
return (
43-
<div className="text-3xl text-white">
44-
Action could not be rendered because mandatory fields are missing.
45-
</div>
46-
);
47-
}
48-
49-
const action = props.action;
50-
const forms = separateForms(props.action.forms);
51-
42+
const addFormDialog = useRef<{ openModal: () => void; close: () => void }>(
43+
null
44+
);
45+
const { containerRef, isExpanded, isHighlighted, setIsExpanded } =
46+
useCopiedAffordanceFocus({ copiedToken });
47+
const forms = separateForms(action.forms);
5248
const attributeListObject = buildAttributeListObject(
53-
{ name: props.actionName },
54-
props.action,
49+
{ name: actionName },
50+
action,
5551
alreadyRenderedKeys
5652
);
57-
const attributes = Object.keys(attributeListObject).map((x) => {
58-
return (
59-
<li key={x}>
60-
{x} : {JSON.stringify(attributeListObject[x])}
61-
</li>
62-
);
63-
});
64-
65-
const handleDeleteAction = () => {
66-
context.removeOneOfAKindReducer("actions", props.actionName);
53+
const handleDelete = () => {
54+
context.removeOneOfAKindReducer("actions", actionName);
6755
};
6856

6957
return (
7058
<details
71-
className="mb-1"
59+
ref={containerRef}
60+
id={`action-${actionName}`}
61+
className={`mb-2 rounded-lg transition-all ${isExpanded ? "overflow-hidden bg-gray-500" : ""} ${isHighlighted ? "border-2 border-green-400 ring-2 ring-green-300/70" : ""}`}
7262
open={isExpanded}
73-
onToggle={() => setIsExpanded(!isExpanded)}
63+
onToggle={(e) => setIsExpanded(e.currentTarget.open)}
7464
>
75-
<summary
76-
className={`flex cursor-pointer items-center rounded-t-lg pl-2 text-xl font-bold text-white ${isExpanded ? "bg-gray-500" : ""}`}
77-
>
78-
<h3 className="flex-grow px-2">{action.title ?? props.actionName}</h3>
65+
<summary className="flex cursor-pointer items-center py-1 pl-2 text-xl font-bold text-white">
66+
<h3 className="flex-grow px-2">{action.title ?? actionName}</h3>
67+
7968
{isExpanded && (
80-
<button
81-
className="flex h-10 w-10 items-center justify-center self-stretch rounded-bl-md rounded-tr-md bg-gray-400 text-base"
82-
onClick={handleDeleteAction}
83-
>
84-
<Trash2 size={16} color="white" />
85-
</button>
69+
<AffordanceButtons
70+
copyTitle="Copy action"
71+
deleteTitle="Delete action"
72+
onCopy={(e) => {
73+
e.preventDefault();
74+
e.stopPropagation();
75+
onCopy();
76+
}}
77+
onDelete={(e) => {
78+
e.preventDefault();
79+
e.stopPropagation();
80+
handleDelete();
81+
}}
82+
/>
8683
)}
8784
</summary>
8885

89-
<div className="mb-4 rounded-b-lg bg-gray-500 px-2 pb-4">
86+
<div className="px-2 pb-4">
9087
{action.description && (
9188
<div className="px-2 pb-2 text-lg text-gray-400">
9289
{action.description}
9390
</div>
9491
)}
95-
<ul className="list-disc pl-6 text-base text-gray-300">{attributes}</ul>
9692

97-
<div className="flex items-center justify-start pb-2 pt-2">
98-
<InfoIconWrapper
99-
className="flex-grow"
100-
tooltip={getFormsTooltipContent()}
101-
id="actions"
102-
>
103-
<h4 className="pr-1 text-lg font-bold text-white">Forms</h4>
104-
</InfoIconWrapper>
105-
</div>
93+
<ul className="list-disc pl-6 text-base text-gray-300">
94+
{Object.entries(attributeListObject).map(([k, v]) => (
95+
<li key={k}>
96+
{k}: {JSON.stringify(v)}
97+
</li>
98+
))}
99+
</ul>
100+
101+
<InfoIconWrapper tooltip={getFormsTooltipContent()} id="actions">
102+
<h4 className="text-lg font-bold text-white">Forms</h4>
103+
</InfoIconWrapper>
104+
105+
<AddFormElement onClick={() => addFormDialog.current?.openModal()} />
106106

107-
<AddFormElement onClick={handleOpenAddFormDialog} />
108107
<AddFormDialog
109-
type={"action"}
108+
type="action"
110109
interaction={action}
111-
interactionName={props.actionName}
110+
interactionName={actionName}
112111
ref={addFormDialog}
113112
/>
113+
114114
{forms.map((form, i) => (
115115
<Form
116-
key={`${i}-${form.href}`}
116+
key={`${i}-${form?.href}`}
117117
form={form}
118-
propName={props.actionName}
119-
interactionType={"action"}
120-
></Form>
118+
propName={actionName}
119+
interactionType="action"
120+
/>
121121
))}
122122
</div>
123123
</details>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/********************************************************************************
2+
* Copyright (c) 2025 Contributors to the Eclipse Foundation
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License v. 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
10+
*
11+
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
12+
********************************************************************************/
13+
import React from "react";
14+
import { Copy, Trash2 } from "react-feather";
15+
16+
interface IProps {
17+
onCopy: (e: React.MouseEvent) => void;
18+
onDelete: (e: React.MouseEvent) => void;
19+
copyTitle: string;
20+
deleteTitle: string;
21+
}
22+
23+
const AffordanceButtons: React.FC<IProps> = ({
24+
onCopy,
25+
onDelete,
26+
copyTitle,
27+
deleteTitle,
28+
}) => {
29+
return (
30+
<div className="flex self-stretch">
31+
<button
32+
aria-label={copyTitle}
33+
className="flex w-14 items-center justify-center bg-gray-400 transition-colors hover:bg-gray-500"
34+
title={copyTitle}
35+
onClick={onCopy}
36+
>
37+
<Copy size={20} color="white" />
38+
</button>
39+
40+
<button
41+
aria-label={deleteTitle}
42+
className="flex w-14 items-center justify-center rounded-tr-lg border-l border-gray-500 bg-gray-400 transition-colors hover:bg-gray-500"
43+
title={deleteTitle}
44+
onClick={onDelete}
45+
>
46+
<Trash2 size={20} color="white" />
47+
</button>
48+
</div>
49+
);
50+
};
51+
52+
export default AffordanceButtons;

0 commit comments

Comments
 (0)