From 601cf95e99c1380809c63e92892061f0f4b9cc23 Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Fri, 28 Jul 2023 17:52:24 -0700 Subject: [PATCH] feat(ui): Add copy link + copy markdown dropdown to short ID Looks like This ![clipboard.png](https://i.imgur.com/CwCbHjT.png) When not hovered there's nothing ![clipboard.png](https://i.imgur.com/8nzVGD4.png) --- static/app/views/issueDetails/header.tsx | 41 +----- .../issueDetails/shortIdBreadcrumb.spec.tsx | 46 +++++++ .../views/issueDetails/shortIdBreadcrumb.tsx | 130 ++++++++++++++++++ 3 files changed, 180 insertions(+), 37 deletions(-) create mode 100644 static/app/views/issueDetails/shortIdBreadcrumb.spec.tsx create mode 100644 static/app/views/issueDetails/shortIdBreadcrumb.tsx diff --git a/static/app/views/issueDetails/header.tsx b/static/app/views/issueDetails/header.tsx index afeda800433589..c1633ca4b398b9 100644 --- a/static/app/views/issueDetails/header.tsx +++ b/static/app/views/issueDetails/header.tsx @@ -3,7 +3,6 @@ import styled from '@emotion/styled'; import {LocationDescriptor} from 'history'; import omit from 'lodash/omit'; -import GuideAnchor from 'sentry/components/assistant/guideAnchor'; import Badge from 'sentry/components/badge'; import Breadcrumbs from 'sentry/components/breadcrumbs'; import Count from 'sentry/components/count'; @@ -14,14 +13,11 @@ import EventMessage from 'sentry/components/events/eventMessage'; import InboxReason from 'sentry/components/group/inboxBadges/inboxReason'; import {GroupStatusBadge} from 'sentry/components/group/inboxBadges/statusBadge'; import UnhandledInboxTag from 'sentry/components/group/inboxBadges/unhandledTag'; -import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import * as Layout from 'sentry/components/layouts/thirds'; import Link from 'sentry/components/links/link'; import ReplayCountBadge from 'sentry/components/replays/replayCountBadge'; import useReplaysCount from 'sentry/components/replays/useReplaysCount'; -import ShortId from 'sentry/components/shortId'; import {TabList} from 'sentry/components/tabs'; -import {Tooltip} from 'sentry/components/tooltip'; import {IconChat} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; @@ -33,6 +29,7 @@ import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import GroupActions from './actions'; +import {ShortIdBreadrcumb} from './shortIdBreadcrumb'; import {Tab} from './types'; import {TagAndMessageWrapper} from './unhandledTag'; import {ReprocessingStatus} from './utils'; @@ -227,26 +224,8 @@ function GroupHeader({ const disableActions = !!disabledTabs.length; - const shortIdBreadCrumb = group.shortId && ( - - - - - - - - + const shortIdBreadcrumb = ( + ); return ( @@ -259,7 +238,7 @@ function GroupHeader({ label: 'Issues', to: `/organizations/${organization.slug}/issues/${location.search}`, }, - {label: shortIdBreadCrumb}, + {label: shortIdBreadcrumb}, ]} /> p.theme.text.family}; - font-size: ${p => p.theme.fontSizeMedium}; - line-height: 1; -`; - const HeaderRow = styled('div')` display: flex; gap: ${space(2)}; diff --git a/static/app/views/issueDetails/shortIdBreadcrumb.spec.tsx b/static/app/views/issueDetails/shortIdBreadcrumb.spec.tsx new file mode 100644 index 00000000000000..fac380327d4036 --- /dev/null +++ b/static/app/views/issueDetails/shortIdBreadcrumb.spec.tsx @@ -0,0 +1,46 @@ +import {initializeOrg} from 'sentry-test/initializeOrg'; +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; + +import {ShortIdBreadrcumb} from './shortIdBreadcrumb'; + +describe('ShortIdBreadrcumb', function () { + const {organization, project} = initializeOrg(); + const group = TestStubs.Group({shortId: 'ABC-123'}); + + beforeEach(() => { + Object.assign(navigator, { + clipboard: {writeText: jest.fn().mockResolvedValue('')}, + }); + }); + + it('renders short ID', function () { + render(); + + expect(screen.getByText('ABC-123')).toBeInTheDocument(); + }); + + it('supports copy', async function () { + render(); + + async function clickMenuItem(name: string) { + await userEvent.click(screen.getByRole('button', {name: 'Short-ID copy actions'})); + await userEvent.click(screen.getByRole('menuitemradio', {name})); + } + + // Copy short ID + await clickMenuItem('Copy Short-ID'); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('ABC-123'); + + // Copy short ID URL + await clickMenuItem('Copy Issue URL'); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + 'http://localhost/organizations/org-slug/issues/1/' + ); + + // Copy short ID Markdown + await clickMenuItem('Copy Markdown Link'); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + '[ABC-123](http://localhost/organizations/org-slug/issues/1/)' + ); + }); +}); diff --git a/static/app/views/issueDetails/shortIdBreadcrumb.tsx b/static/app/views/issueDetails/shortIdBreadcrumb.tsx new file mode 100644 index 00000000000000..c5c1212c9e7103 --- /dev/null +++ b/static/app/views/issueDetails/shortIdBreadcrumb.tsx @@ -0,0 +1,130 @@ +import styled from '@emotion/styled'; + +import GuideAnchor from 'sentry/components/assistant/guideAnchor'; +import {DropdownMenu} from 'sentry/components/dropdownMenu'; +import ProjectBadge from 'sentry/components/idBadge/projectBadge'; +import ShortId from 'sentry/components/shortId'; +import {Tooltip} from 'sentry/components/tooltip'; +import {IconChevron} from 'sentry/icons'; +import {t} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import {Group, Organization, Project} from 'sentry/types'; +import useCopyToClipboard from 'sentry/utils/useCopyToClipboard'; +import {normalizeUrl} from 'sentry/utils/withDomainRequired'; + +interface ShortIdBreadrcumbProps { + group: Group; + organization: Organization; + project: Project; +} + +export function ShortIdBreadrcumb({ + organization, + project, + group, +}: ShortIdBreadrcumbProps) { + const {onClick: handleCopyShortId} = useCopyToClipboard({ + text: group.shortId, + successMessage: t('Copied Short-ID to clipboard'), + }); + + const issueUrl = + window.location.origin + + normalizeUrl(`/organizations/${organization.slug}/issues/${group.id}/`); + + const {onClick: handleCopyUrl} = useCopyToClipboard({ + text: issueUrl, + successMessage: t('Copied Issue URL to clipboard'), + }); + + const {onClick: handleCopyMarkdown} = useCopyToClipboard({ + text: `[${group.shortId}](${issueUrl})`, + successMessage: t('Copied Markdown Issue Link to clipboard'), + }); + + if (!group.shortId) { + return null; + } + + return ( + + + + + + + + , + size: 'zero', + borderless: true, + showChevron: false, + }} + position="bottom" + size="xs" + items={[ + { + key: 'copy-url', + label: t('Copy Issue URL'), + onAction: handleCopyUrl, + }, + { + key: 'copy-short-id', + label: t('Copy Short-ID'), + onAction: handleCopyShortId, + }, + { + key: 'copy-markdown-link', + label: t('Copy Markdown Link'), + onAction: handleCopyMarkdown, + }, + ]} + /> + + + + ); +} + +const Wrapper = styled('div')` + display: flex; + gap: ${space(1)}; + align-items: center; +`; + +const StyledShortId = styled(ShortId)` + font-family: ${p => p.theme.text.family}; + font-size: ${p => p.theme.fontSizeMedium}; + line-height: 1; +`; + +const ShortIdCopyable = styled('div')` + display: flex; + gap: ${space(0.25)}; + align-items: center; + + button[aria-haspopup] { + opacity: 0; + transition: opacity 50ms linear; + } + + &:hover button[aria-haspopup], + button[aria-expanded='true'], + button[aria-haspopup].focus-visible { + opacity: 1; + } +`;