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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 49 additions & 5 deletions web/ce/components/issues/issue-details/issue-identifier.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { observer } from "mobx-react";
// types
import { IIssueDisplayProperties } from "@plane/types";
// ui
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
Expand All @@ -11,6 +13,7 @@ type TIssueIdentifierBaseProps = {
size?: "xs" | "sm" | "md" | "lg";
textContainerClassName?: string;
displayProperties?: IIssueDisplayProperties | undefined;
enableClickToCopyIdentifier?: boolean;
};

type TIssueIdentifierFromStore = TIssueIdentifierBaseProps & {
Expand All @@ -23,9 +26,48 @@ type TIssueIdentifierWithDetails = TIssueIdentifierBaseProps & {
issueSequenceId: string | number;
};

type TIssueIdentifierProps = TIssueIdentifierFromStore | TIssueIdentifierWithDetails;
export type TIssueIdentifierProps = TIssueIdentifierFromStore | TIssueIdentifierWithDetails;

type TIdentifierTextProps = {
identifier: string;
enableClickToCopyIdentifier?: boolean;
textContainerClassName?: string;
};

export const IdentifierText: React.FC<TIdentifierTextProps> = (props) => {
const { identifier, enableClickToCopyIdentifier = false, textContainerClassName } = props;
// handlers
const handleCopyIssueIdentifier = () => {
if (enableClickToCopyIdentifier) {
navigator.clipboard.writeText(identifier).then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Issue ID copied to clipboard",
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please handle the error scenario with catch?

}
};

return (
<Tooltip tooltipContent="Click to copy" disabled={!enableClickToCopyIdentifier} position="top">
<span
className={cn(
"text-base font-medium text-custom-text-300",
{
"cursor-pointer": enableClickToCopyIdentifier,
},
textContainerClassName
)}
onClick={handleCopyIssueIdentifier}
>
{identifier}
</span>
</Tooltip>
);
};
Comment on lines +51 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance accessibility for interactive elements

The <span> element is being used as an interactive element due to the onClick handler. For better accessibility, it should be focusable and operable via keyboard.

Consider adding role="button" and tabIndex={0} to make the element accessible:

 <span
   className={cn(
     "text-base font-medium text-custom-text-300",
     {
       "cursor-pointer": enableClickToCopyIdentifier,
     },
     textContainerClassName
   )}
+  role={enableClickToCopyIdentifier ? "button" : undefined}
+  tabIndex={enableClickToCopyIdentifier ? 0 : undefined}
   onClick={handleCopyIssueIdentifier}
+  onKeyPress={(e) => {
+    if (enableClickToCopyIdentifier && (e.key === 'Enter' || e.key === ' ')) {
+      handleCopyIssueIdentifier();
+    }
+  }}
 >

This change ensures users who navigate via keyboard can interact with the element.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<Tooltip tooltipContent="Click to copy" disabled={!enableClickToCopyIdentifier} position="top">
<span
className={cn(
"text-base font-medium text-custom-text-300",
{
"cursor-pointer": enableClickToCopyIdentifier,
},
textContainerClassName
)}
onClick={handleCopyIssueIdentifier}
>
{identifier}
</span>
</Tooltip>
);
};
return (
<Tooltip tooltipContent="Click to copy" disabled={!enableClickToCopyIdentifier} position="top">
<span
className={cn(
"text-base font-medium text-custom-text-300",
{
"cursor-pointer": enableClickToCopyIdentifier,
},
textContainerClassName
)}
role={enableClickToCopyIdentifier ? "button" : undefined}
tabIndex={enableClickToCopyIdentifier ? 0 : undefined}
onClick={handleCopyIssueIdentifier}
onKeyPress={(e) => {
if (enableClickToCopyIdentifier && (e.key === 'Enter' || e.key === ' ')) {
handleCopyIssueIdentifier();
}
}}
>
{identifier}
</span>
</Tooltip>
);
};

Comment on lines +37 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for clipboard operations

The navigator.clipboard.writeText method may fail due to browser permissions or security settings, which could leave the user unaware of the failure.

Consider adding error handling to inform the user if copying to the clipboard fails.

Apply this diff to include error handling:

 const handleCopyIssueIdentifier = () => {
   if (enableClickToCopyIdentifier) {
-    navigator.clipboard.writeText(identifier).then(() => {
+    navigator.clipboard.writeText(identifier)
+      .then(() => {
         setToast({
           type: TOAST_TYPE.SUCCESS,
           title: "Issue ID copied to clipboard",
         });
-    });
+      })
+      .catch(() => {
+        setToast({
+          type: TOAST_TYPE.ERROR,
+          title: "Failed to copy Issue ID",
+        });
+      });
   }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const IdentifierText: React.FC<TIdentifierTextProps> = (props) => {
const { identifier, enableClickToCopyIdentifier = false, textContainerClassName } = props;
// handlers
const handleCopyIssueIdentifier = () => {
if (enableClickToCopyIdentifier) {
navigator.clipboard.writeText(identifier).then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Issue ID copied to clipboard",
});
});
}
};
return (
<Tooltip tooltipContent="Click to copy" disabled={!enableClickToCopyIdentifier} position="top">
<span
className={cn(
"text-base font-medium text-custom-text-300",
{
"cursor-pointer": enableClickToCopyIdentifier,
},
textContainerClassName
)}
onClick={handleCopyIssueIdentifier}
>
{identifier}
</span>
</Tooltip>
);
};
export const IdentifierText: React.FC<TIdentifierTextProps> = (props) => {
const { identifier, enableClickToCopyIdentifier = false, textContainerClassName } = props;
// handlers
const handleCopyIssueIdentifier = () => {
if (enableClickToCopyIdentifier) {
navigator.clipboard.writeText(identifier)
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Issue ID copied to clipboard",
});
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Failed to copy Issue ID",
});
});
}
};
return (
<Tooltip tooltipContent="Click to copy" disabled={!enableClickToCopyIdentifier} position="top">
<span
className={cn(
"text-base font-medium text-custom-text-300",
{
"cursor-pointer": enableClickToCopyIdentifier,
},
textContainerClassName
)}
onClick={handleCopyIssueIdentifier}
>
{identifier}
</span>
</Tooltip>
);
};


export const IssueIdentifier: React.FC<TIssueIdentifierProps> = observer((props) => {
const { projectId, textContainerClassName, displayProperties } = props;
const { projectId, textContainerClassName, displayProperties, enableClickToCopyIdentifier = false } = props;
// store hooks
const { getProjectIdentifierById } = useProject();
const {
Expand All @@ -43,9 +85,11 @@ export const IssueIdentifier: React.FC<TIssueIdentifierProps> = observer((props)

return (
<div className="flex items-center space-x-2">
<span className={cn("text-base font-medium text-custom-text-300", textContainerClassName)}>
{projectIdentifier}-{issueSequenceId}
</span>
<IdentifierText
identifier={`${projectIdentifier}-${issueSequenceId}`}
enableClickToCopyIdentifier={enableClickToCopyIdentifier}
textContainerClassName={textContainerClassName}
/>
</div>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ export const IssueTypeSwitcher: React.FC<TIssueTypeSwitcherProps> = observer((pr

if (!issue || !issue.project_id) return <></>;

return <IssueIdentifier issueId={issueId} projectId={issue.project_id} size="md" />;
return <IssueIdentifier issueId={issueId} projectId={issue.project_id} size="md" enableClickToCopyIdentifier />;
});