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
114 changes: 106 additions & 8 deletions static/app/components/feedback/feedbackItem/feedbackActions.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {CSSProperties} from 'react';
import {Fragment} from 'react';
import {Fragment, useCallback} from 'react';

import {Button} from 'sentry/components/core/button';
import {Flex} from 'sentry/components/core/layout';
Expand All @@ -8,11 +8,14 @@ import {DropdownMenu} from 'sentry/components/dropdownMenu';
import ErrorBoundary from 'sentry/components/errorBoundary';
import FeedbackAssignedTo from 'sentry/components/feedback/feedbackItem/feedbackAssignedTo';
import useFeedbackActions from 'sentry/components/feedback/feedbackItem/useFeedbackActions';
import {IconEllipsis} from 'sentry/icons';
import {IconCopy, IconEllipsis} from 'sentry/icons';
import {t} from 'sentry/locale';
import type {Event} from 'sentry/types/event';
import type {Group} from 'sentry/types/group';
import {trackAnalytics} from 'sentry/utils/analytics';
import type {FeedbackIssue} from 'sentry/utils/feedback/types';
import useCopyToClipboard from 'sentry/utils/useCopyToClipboard';
import useOrganization from 'sentry/utils/useOrganization';

interface Props {
eventData: Event | undefined;
Expand All @@ -29,6 +32,47 @@ export default function FeedbackActions({
size,
style,
}: Props) {
const organization = useOrganization();
const {copy} = useCopyToClipboard();
const handleCopyToClipboard = useCallback(() => {
const summary = feedbackItem.metadata.summary;
const message =
feedbackItem.metadata.message ?? feedbackItem.metadata.value ?? t('No message');
const culprit = eventData?.culprit?.trim();
const viewNames = eventData?.contexts?.app?.view_names?.filter(Boolean);

const sourceLines = [];
if (culprit) {
sourceLines.push(`- ${culprit}`);
}
if (viewNames?.length) {
sourceLines.push(t('- View names: %s', viewNames.join(', ')));
}

const markdown = [
'# User Feedback',
'',
...(summary ? [`**Summary:** ${summary}`, ''] : []),
'## Feedback Message',
message,
...(sourceLines.length
? [
'',
'## Source (_where user was when feedback was sent_)',
sourceLines.join('\n'),
]
: []),
Comment on lines +58 to +64
Copy link
Member

Choose a reason for hiding this comment

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

should we also include tags? I feel like those are useful to us internally, we use them almost everywhere now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looking at a few examples, I think many of the tags are superfluous to implementing a feedback fix/ticket. Going to merge initial version without and we can easily add in if we fill they'd be a benefit

].join('\n');

trackAnalytics('feedback.feedback-item-copy-as-markdown', {
organization,
});

copy(markdown, {
successMessage: t('Copied feedback'),
errorMessage: t('Failed to copy feedback'),
});
}, [copy, eventData, feedbackItem, organization]);
if (!eventData) {
return null;
}
Expand All @@ -42,14 +86,35 @@ export default function FeedbackActions({
/>
</ErrorBoundary>

{size === 'large' ? <LargeWidth feedbackItem={feedbackItem} /> : null}
{size === 'medium' ? <MediumWidth feedbackItem={feedbackItem} /> : null}
{size === 'small' ? <SmallWidth feedbackItem={feedbackItem} /> : null}
{size === 'large' ? (
<LargeWidth
feedbackItem={feedbackItem}
onCopyToClipboard={handleCopyToClipboard}
/>
) : null}
{size === 'medium' ? (
<MediumWidth
feedbackItem={feedbackItem}
onCopyToClipboard={handleCopyToClipboard}
/>
) : null}
{size === 'small' ? (
<SmallWidth
feedbackItem={feedbackItem}
onCopyToClipboard={handleCopyToClipboard}
/>
) : null}
</Flex>
);
}

function LargeWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
function LargeWidth({
feedbackItem,
onCopyToClipboard,
}: {
feedbackItem: FeedbackIssue;
onCopyToClipboard: () => void;
}) {
const {
enableDelete,
onDelete,
Expand Down Expand Up @@ -82,6 +147,15 @@ function LargeWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
{hasSeen ? t('Mark Unread') : t('Mark Read')}
</Button>
</Tooltip>
<Tooltip title={t('Copy feedback as markdown')}>
<Button
size="xs"
priority="default"
icon={<IconCopy />}
onClick={onCopyToClipboard}
aria-label={t('Copy feedback as markdown')}
/>
</Tooltip>
<Tooltip
disabled={enableDelete}
title={t('You must be an admin to delete feedback')}
Expand All @@ -94,7 +168,13 @@ function LargeWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
);
}

function MediumWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
function MediumWidth({
feedbackItem,
onCopyToClipboard,
}: {
feedbackItem: FeedbackIssue;
onCopyToClipboard: () => void;
}) {
const {
enableDelete,
onDelete,
Expand Down Expand Up @@ -140,6 +220,12 @@ function MediumWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
? undefined
: t('You must be a member of the project'),
},
{
key: 'copy',
label: t('Copy as markdown'),
onAction: onCopyToClipboard,
tooltip: t('Copy feedback as markdown'),
},
{
key: 'delete',
priority: 'danger' as const,
Expand All @@ -156,7 +242,13 @@ function MediumWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
);
}

function SmallWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
function SmallWidth({
feedbackItem,
onCopyToClipboard,
}: {
feedbackItem: FeedbackIssue;
onCopyToClipboard: () => void;
}) {
const {
enableDelete,
onDelete,
Expand Down Expand Up @@ -189,6 +281,12 @@ function SmallWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) {
label: isSpam ? t('Move to Inbox') : t('Mark as Spam'),
onAction: onSpamClick,
},
{
key: 'copy',
label: t('Copy as markdown'),
onAction: onCopyToClipboard,
tooltip: t('Copy feedback as markdown'),
},
{
key: 'read',
label: hasSeen ? t('Mark Unread') : t('Mark Read'),
Expand Down
2 changes: 2 additions & 0 deletions static/app/utils/analytics/feedbackAnalyticsEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type FeedbackEventParameters = {
'feedback.details-integration-issue-clicked': {
integration_key: string;
};
'feedback.feedback-item-copy-as-markdown': Record<string, unknown>;
'feedback.feedback-item-not-found': {feedbackId: string};
'feedback.feedback-item-rendered': Record<string, unknown>;
'feedback.index-setup-viewed': Record<string, unknown>;
Expand All @@ -28,6 +29,7 @@ export type FeedbackEventParameters = {
type FeedbackEventKey = keyof FeedbackEventParameters;

export const feedbackEventMap: Record<FeedbackEventKey, string | null> = {
'feedback.feedback-item-copy-as-markdown': 'Copied Feedback Item as Markdown',
'feedback.feedback-item-not-found': 'Feedback item not found',
'feedback.feedback-item-rendered': 'Loaded and rendered a feedback item',
'feedback.index-setup-viewed': 'Viewed Feedback Onboarding Setup',
Expand Down
Loading