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(); + } + }, + );