11import type { CSSProperties } from 'react' ;
2- import { Fragment } from 'react' ;
2+ import { Fragment , useCallback } from 'react' ;
33
44import { Button } from 'sentry/components/core/button' ;
55import { Flex } from 'sentry/components/core/layout' ;
@@ -8,11 +8,14 @@ import {DropdownMenu} from 'sentry/components/dropdownMenu';
88import ErrorBoundary from 'sentry/components/errorBoundary' ;
99import FeedbackAssignedTo from 'sentry/components/feedback/feedbackItem/feedbackAssignedTo' ;
1010import useFeedbackActions from 'sentry/components/feedback/feedbackItem/useFeedbackActions' ;
11- import { IconEllipsis } from 'sentry/icons' ;
11+ import { IconCopy , IconEllipsis } from 'sentry/icons' ;
1212import { t } from 'sentry/locale' ;
1313import type { Event } from 'sentry/types/event' ;
1414import type { Group } from 'sentry/types/group' ;
15+ import { trackAnalytics } from 'sentry/utils/analytics' ;
1516import type { FeedbackIssue } from 'sentry/utils/feedback/types' ;
17+ import useCopyToClipboard from 'sentry/utils/useCopyToClipboard' ;
18+ import useOrganization from 'sentry/utils/useOrganization' ;
1619
1720interface Props {
1821 eventData : Event | undefined ;
@@ -29,6 +32,47 @@ export default function FeedbackActions({
2932 size,
3033 style,
3134} : Props ) {
35+ const organization = useOrganization ( ) ;
36+ const { copy} = useCopyToClipboard ( ) ;
37+ const handleCopyToClipboard = useCallback ( ( ) => {
38+ const summary = feedbackItem . metadata . summary ;
39+ const message =
40+ feedbackItem . metadata . message ?? feedbackItem . metadata . value ?? t ( 'No message' ) ;
41+ const culprit = eventData ?. culprit ?. trim ( ) ;
42+ const viewNames = eventData ?. contexts ?. app ?. view_names ?. filter ( Boolean ) ;
43+
44+ const sourceLines = [ ] ;
45+ if ( culprit ) {
46+ sourceLines . push ( `- ${ culprit } ` ) ;
47+ }
48+ if ( viewNames ?. length ) {
49+ sourceLines . push ( t ( '- View names: %s' , viewNames . join ( ', ' ) ) ) ;
50+ }
51+
52+ const markdown = [
53+ '# User Feedback' ,
54+ '' ,
55+ ...( summary ? [ `**Summary:** ${ summary } ` , '' ] : [ ] ) ,
56+ '## Feedback Message' ,
57+ message ,
58+ ...( sourceLines . length
59+ ? [
60+ '' ,
61+ '## Source (_where user was when feedback was sent_)' ,
62+ sourceLines . join ( '\n' ) ,
63+ ]
64+ : [ ] ) ,
65+ ] . join ( '\n' ) ;
66+
67+ trackAnalytics ( 'feedback.feedback-item-copy-as-markdown' , {
68+ organization,
69+ } ) ;
70+
71+ copy ( markdown , {
72+ successMessage : t ( 'Copied feedback' ) ,
73+ errorMessage : t ( 'Failed to copy feedback' ) ,
74+ } ) ;
75+ } , [ copy , eventData , feedbackItem , organization ] ) ;
3276 if ( ! eventData ) {
3377 return null ;
3478 }
@@ -42,14 +86,35 @@ export default function FeedbackActions({
4286 />
4387 </ ErrorBoundary >
4488
45- { size === 'large' ? < LargeWidth feedbackItem = { feedbackItem } /> : null }
46- { size === 'medium' ? < MediumWidth feedbackItem = { feedbackItem } /> : null }
47- { size === 'small' ? < SmallWidth feedbackItem = { feedbackItem } /> : null }
89+ { size === 'large' ? (
90+ < LargeWidth
91+ feedbackItem = { feedbackItem }
92+ onCopyToClipboard = { handleCopyToClipboard }
93+ />
94+ ) : null }
95+ { size === 'medium' ? (
96+ < MediumWidth
97+ feedbackItem = { feedbackItem }
98+ onCopyToClipboard = { handleCopyToClipboard }
99+ />
100+ ) : null }
101+ { size === 'small' ? (
102+ < SmallWidth
103+ feedbackItem = { feedbackItem }
104+ onCopyToClipboard = { handleCopyToClipboard }
105+ />
106+ ) : null }
48107 </ Flex >
49108 ) ;
50109}
51110
52- function LargeWidth ( { feedbackItem} : { feedbackItem : FeedbackIssue } ) {
111+ function LargeWidth ( {
112+ feedbackItem,
113+ onCopyToClipboard,
114+ } : {
115+ feedbackItem : FeedbackIssue ;
116+ onCopyToClipboard : ( ) => void ;
117+ } ) {
53118 const {
54119 enableDelete,
55120 onDelete,
@@ -82,6 +147,15 @@ function LargeWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
82147 { hasSeen ? t ( 'Mark Unread' ) : t ( 'Mark Read' ) }
83148 </ Button >
84149 </ Tooltip >
150+ < Tooltip title = { t ( 'Copy feedback as markdown' ) } >
151+ < Button
152+ size = "xs"
153+ priority = "default"
154+ icon = { < IconCopy /> }
155+ onClick = { onCopyToClipboard }
156+ aria-label = { t ( 'Copy feedback as markdown' ) }
157+ />
158+ </ Tooltip >
85159 < Tooltip
86160 disabled = { enableDelete }
87161 title = { t ( 'You must be an admin to delete feedback' ) }
@@ -94,7 +168,13 @@ function LargeWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
94168 ) ;
95169}
96170
97- function MediumWidth ( { feedbackItem} : { feedbackItem : FeedbackIssue } ) {
171+ function MediumWidth ( {
172+ feedbackItem,
173+ onCopyToClipboard,
174+ } : {
175+ feedbackItem : FeedbackIssue ;
176+ onCopyToClipboard : ( ) => void ;
177+ } ) {
98178 const {
99179 enableDelete,
100180 onDelete,
@@ -140,6 +220,12 @@ function MediumWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
140220 ? undefined
141221 : t ( 'You must be a member of the project' ) ,
142222 } ,
223+ {
224+ key : 'copy' ,
225+ label : t ( 'Copy as markdown' ) ,
226+ onAction : onCopyToClipboard ,
227+ tooltip : t ( 'Copy feedback as markdown' ) ,
228+ } ,
143229 {
144230 key : 'delete' ,
145231 priority : 'danger' as const ,
@@ -156,7 +242,13 @@ function MediumWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
156242 ) ;
157243}
158244
159- function SmallWidth ( { feedbackItem} : { feedbackItem : FeedbackIssue } ) {
245+ function SmallWidth ( {
246+ feedbackItem,
247+ onCopyToClipboard,
248+ } : {
249+ feedbackItem : FeedbackIssue ;
250+ onCopyToClipboard : ( ) => void ;
251+ } ) {
160252 const {
161253 enableDelete,
162254 onDelete,
@@ -189,6 +281,12 @@ function SmallWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
189281 label : isSpam ? t ( 'Move to Inbox' ) : t ( 'Mark as Spam' ) ,
190282 onAction : onSpamClick ,
191283 } ,
284+ {
285+ key : 'copy' ,
286+ label : t ( 'Copy as markdown' ) ,
287+ onAction : onCopyToClipboard ,
288+ tooltip : t ( 'Copy feedback as markdown' ) ,
289+ } ,
192290 {
193291 key : 'read' ,
194292 label : hasSeen ? t ( 'Mark Unread' ) : t ( 'Mark Read' ) ,
0 commit comments