From 18850a515632d0e972a94549dd31e826e2358f3f Mon Sep 17 00:00:00 2001
From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com>
Date: Tue, 22 Apr 2025 11:12:37 +0200
Subject: [PATCH] feat: move 'copy flag name' button (#9805)
- moved "copy flag name" action next to the flag name
- refactored this component into a separate file
- added "Ctrl+C" shortcut
---
.../FeatureCopyName/FeatureCopyName.tsx | 61 +++++++++++++++++++
.../feature/FeatureView/FeatureViewHeader.tsx | 30 ++++-----
frontend/src/hooks/useKeyboardCopy.ts | 19 ++++++
3 files changed, 93 insertions(+), 17 deletions(-)
create mode 100644 frontend/src/component/feature/FeatureView/FeatureCopyName/FeatureCopyName.tsx
create mode 100644 frontend/src/hooks/useKeyboardCopy.ts
diff --git a/frontend/src/component/feature/FeatureView/FeatureCopyName/FeatureCopyName.tsx b/frontend/src/component/feature/FeatureView/FeatureCopyName/FeatureCopyName.tsx
new file mode 100644
index 0000000000..1eb13d8240
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView/FeatureCopyName/FeatureCopyName.tsx
@@ -0,0 +1,61 @@
+import { useState, type FC } from 'react';
+import copy from 'copy-to-clipboard';
+import useToast from 'hooks/useToast';
+import { useKeyboardCopy } from 'hooks/useKeyboardCopy';
+import { IconButton, Tooltip } from '@mui/material';
+import Check from '@mui/icons-material/Check';
+import FileCopyOutlined from '@mui/icons-material/FileCopyOutlined';
+
+const iconSize = 18 / 8;
+
+export const FeatureCopyName: FC<{ name: string }> = ({ name }) => {
+ const [isFeatureNameCopied, setIsFeatureNameCopied] = useState(false);
+ const { setToastData } = useToast();
+
+ const handleCopyToClipboard = () => {
+ try {
+ copy(name);
+ setIsFeatureNameCopied(true);
+ const timeout = setTimeout(() => {
+ setIsFeatureNameCopied(false);
+ }, 3000);
+
+ return () => {
+ clearTimeout(timeout);
+ };
+ } catch (error: unknown) {
+ setToastData({
+ type: 'error',
+ text: 'Could not copy feature name',
+ });
+ }
+ };
+
+ const shortcutDescription = useKeyboardCopy(handleCopyToClipboard);
+
+ return (
+
+
+ {isFeatureNameCopied ? (
+ ({
+ fontSize: theme.spacing(iconSize),
+ color: theme.palette.success.main,
+ })}
+ />
+ ) : (
+ ({ fontSize: theme.spacing(iconSize) })}
+ />
+ )}
+
+
+ );
+};
diff --git a/frontend/src/component/feature/FeatureView/FeatureViewHeader.tsx b/frontend/src/component/feature/FeatureView/FeatureViewHeader.tsx
index d5b6872157..d3e8bb59b3 100644
--- a/frontend/src/component/feature/FeatureView/FeatureViewHeader.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureViewHeader.tsx
@@ -12,7 +12,6 @@ import {
import Archive from '@mui/icons-material/Archive';
import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
import FileCopy from '@mui/icons-material/FileCopy';
-import FileCopyOutlined from '@mui/icons-material/FileCopyOutlined';
import Label from '@mui/icons-material/Label';
import WatchLater from '@mui/icons-material/WatchLater';
import WatchLaterOutlined from '@mui/icons-material/WatchLaterOutlined';
@@ -45,6 +44,7 @@ import { ManageTagsDialog } from './FeatureOverview/ManageTagsDialog/ManageTagsD
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
+import { FeatureCopyName } from './FeatureCopyName/FeatureCopyName';
const NewStyledHeader = styled('div')(({ theme }) => ({
backgroundColor: 'none',
@@ -64,7 +64,13 @@ const UpperHeaderRow = styled('div')(({ theme }) => ({
display: 'flex',
flexFlow: 'row wrap',
alignItems: 'center',
- columnGap: theme.spacing(2),
+ columnGap: theme.spacing(1.5),
+}));
+
+const StyledTitle = styled('div')(({ theme }) => ({
+ display: 'flex',
+ alignItems: 'center',
+ columnGap: theme.spacing(0.5),
}));
const LowerHeaderRow = styled(UpperHeaderRow)(({ theme }) => ({
@@ -210,8 +216,6 @@ type HeaderActionsProps = {
feature: IFeatureToggle;
showOnNarrowScreens?: boolean;
onFavorite: () => void;
- handleCopyToClipboard: () => void;
- isFeatureNameCopied: boolean;
openStaleDialog: () => void;
openDeleteDialog: () => void;
};
@@ -220,8 +224,6 @@ const HeaderActionsComponent = ({
showOnNarrowScreens,
feature,
onFavorite,
- handleCopyToClipboard,
- isFeatureNameCopied,
openStaleDialog,
openDeleteDialog,
}: HeaderActionsProps) => (
@@ -233,14 +235,6 @@ const HeaderActionsComponent = ({
>
{feature.favorite ? : }
-
-
- {isFeatureNameCopied ? : }
-
= ({ feature }) => {
showOnNarrowScreens={showOnNarrowScreens}
feature={feature}
onFavorite={onFavorite}
- handleCopyToClipboard={handleCopyToClipboard}
- isFeatureNameCopied={isFeatureNameCopied}
openStaleDialog={() => setOpenStaleDialog(true)}
openDeleteDialog={() => setShowDelDialog(true)}
/>
@@ -386,7 +378,11 @@ export const FeatureViewHeader: FC = ({ feature }) => {
{flagOverviewRedesign ? (
- {feature.name}
+
+ {feature.name}
+
+
+
{feature.stale ? (
) : null}
diff --git a/frontend/src/hooks/useKeyboardCopy.ts b/frontend/src/hooks/useKeyboardCopy.ts
new file mode 100644
index 0000000000..1e9305295e
--- /dev/null
+++ b/frontend/src/hooks/useKeyboardCopy.ts
@@ -0,0 +1,19 @@
+import { useKeyboardShortcut } from './useKeyboardShortcut';
+
+export const useKeyboardCopy = (handler: () => void) =>
+ useKeyboardShortcut(
+ {
+ key: 'c',
+ modifiers: ['ctrl'],
+ preventDefault: false,
+ },
+ () => {
+ const selection = window.getSelection?.();
+ if (
+ selection &&
+ (selection.type === 'None' || selection.type === 'Caret')
+ ) {
+ handler();
+ }
+ },
+ );