Skip to content

Commit 6d38a7c

Browse files
feat(mobile-screenshots): Add visualization modal (#27350)
1 parent 75ef01f commit 6d38a7c

File tree

10 files changed

+275
-150
lines changed

10 files changed

+275
-150
lines changed

static/app/components/actions/actionLink.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export default function ActionLink({
4949
children,
5050
shouldConfirm,
5151
confirmPriority,
52+
header,
5253
...props
5354
}: Props) {
5455
const actionCommonProps = {
@@ -74,6 +75,7 @@ export default function ActionLink({
7475
priority={confirmPriority}
7576
disabled={disabled}
7677
message={message}
78+
header={header}
7779
confirmText={confirmLabel}
7880
onConfirm={onAction}
7981
stopPropagation={disabled}

static/app/components/actions/confirmableAction.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ type ConfirmProps = React.ComponentProps<typeof Confirm>;
66
type Props = {
77
children: React.ReactNode | ConfirmProps['children'];
88
shouldConfirm?: boolean;
9-
} & Partial<Pick<ConfirmProps, 'confirmText' | 'priority' | 'stopPropagation'>> &
9+
} & Partial<
10+
Pick<ConfirmProps, 'confirmText' | 'priority' | 'stopPropagation' | 'header'>
11+
> &
1012
Pick<ConfirmProps, 'message' | 'disabled' | 'confirmText' | 'onConfirm'>;
1113

1214
export default function ConfirmableAction({shouldConfirm, children, ...props}: Props) {

static/app/components/events/contexts/utils.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ export function getSourcePlugin(pluginContexts: Array<any>, contextType: string)
4040

4141
export function getRelativeTimeFromEventDateCreated(
4242
eventDateCreated: string,
43-
timestamp?: string
43+
timestamp?: string,
44+
showTimestamp = true
4445
) {
4546
if (!defined(timestamp)) {
4647
return timestamp;
@@ -56,6 +57,10 @@ export function getRelativeTimeFromEventDateCreated(
5657
'before this event'
5758
)})`;
5859

60+
if (!showTimestamp) {
61+
return <RelativeTime>{relativeTime}</RelativeTime>;
62+
}
63+
5964
return (
6065
<Fragment>
6166
{timestamp}

static/app/components/events/eventTagsAndScreenshot/dataSection.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ const Title = styled('h3')`
5151
const StyledEventDataSection = styled(EventDataSection)`
5252
${SectionContents} {
5353
flex: 1;
54-
overflow: hidden;
5554
}
5655
5756
@media (min-width: ${p => p.theme.breakpoints[0]}) {

static/app/components/events/eventTagsAndScreenshot/screenshot/emptyState.tsx

Lines changed: 0 additions & 64 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import styled from '@emotion/styled';
2+
3+
import ImageViewer from 'app/components/events/attachmentViewers/imageViewer';
4+
5+
const ImageVisualization = styled(ImageViewer)`
6+
padding: 0;
7+
height: 100%;
8+
img {
9+
width: auto;
10+
height: 100%;
11+
object-fit: cover;
12+
flex: 1;
13+
}
14+
`;
15+
16+
export default ImageVisualization;

static/app/components/events/eventTagsAndScreenshot/screenshot/index.tsx

Lines changed: 77 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import {Fragment, useEffect, useState} from 'react';
22
import styled from '@emotion/styled';
33

4+
import {openModal} from 'app/actionCreators/modal';
45
import {Client} from 'app/api';
56
import Role from 'app/components/acl/role';
67
import MenuItemActionLink from 'app/components/actions/menuItemActionLink';
78
import Button from 'app/components/button';
89
import ButtonBar from 'app/components/buttonBar';
910
import DropdownLink from 'app/components/dropdownLink';
10-
import ImageViewer from 'app/components/events/attachmentViewers/imageViewer';
11-
import LoadingIndicator from 'app/components/loadingIndicator';
1211
import {Panel, PanelBody, PanelFooter} from 'app/components/panels';
13-
import {IconDownload, IconEllipsis} from 'app/icons';
12+
import {IconEllipsis} from 'app/icons';
1413
import {t} from 'app/locale';
1514
import space from 'app/styles/space';
1615
import {EventAttachment, Organization, Project} from 'app/types';
@@ -19,8 +18,8 @@ import withApi from 'app/utils/withApi';
1918

2019
import DataSection from '../dataSection';
2120

22-
import EmptyState from './emptyState';
23-
import {platformsMobileWithAttachmentsFeature} from './utils';
21+
import ImageVisualization from './imageVisualization';
22+
import Modal, {modalCss} from './modal';
2423

2524
type Props = {
2625
event: Event;
@@ -33,7 +32,6 @@ function Screenshot({event, api, organization, projectSlug}: Props) {
3332
const [attachments, setAttachments] = useState<EventAttachment[]>([]);
3433
const [isLoading, setIsLoading] = useState(false);
3534
const orgSlug = organization.slug;
36-
const eventPlatform = event.platform;
3735

3836
useEffect(() => {
3937
fetchData();
@@ -59,43 +57,71 @@ function Screenshot({event, api, organization, projectSlug}: Props) {
5957
}
6058
}
6159

62-
function hasPreview(attachment: EventAttachment) {
63-
switch (attachment.mimetype) {
64-
case 'image/jpeg':
65-
case 'image/png':
66-
case 'image/gif':
67-
return true;
68-
default:
69-
return false;
70-
}
60+
function hasScreenshot(attachment: EventAttachment) {
61+
const {mimetype} = attachment;
62+
return mimetype === 'image/jpeg' || mimetype === 'image/png';
7163
}
7264

73-
function renderContent() {
74-
if (isLoading) {
75-
return <LoadingIndicator mini />;
76-
}
77-
78-
const firstAttachmenteWithPreview = attachments.find(hasPreview);
65+
async function handleDelete(screenshotAttachmentId: string, downloadUrl: string) {
66+
try {
67+
await api.requestPromise(downloadUrl.split('/api/0')[1], {
68+
method: 'DELETE',
69+
});
7970

80-
if (!firstAttachmenteWithPreview) {
81-
return <EmptyState platform={eventPlatform} />;
71+
setAttachments(
72+
attachments.filter(attachment => attachment.id !== screenshotAttachmentId)
73+
);
74+
} catch (_err) {
75+
// TODO: Error-handling
8276
}
77+
}
8378

84-
const downloadUrl = `/api/0/projects/${organization.slug}/${projectSlug}/events/${event.id}/attachments/${firstAttachmenteWithPreview.id}/`;
79+
function handleOpenVisualizationModal(
80+
eventAttachment: EventAttachment,
81+
downloadUrl: string
82+
) {
83+
openModal(
84+
modalProps => (
85+
<Modal
86+
{...modalProps}
87+
event={event}
88+
orgSlug={orgSlug}
89+
projectSlug={projectSlug}
90+
eventAttachment={eventAttachment}
91+
downloadUrl={downloadUrl}
92+
onDelete={() => handleDelete(eventAttachment.id, downloadUrl)}
93+
/>
94+
),
95+
{modalCss}
96+
);
97+
}
98+
99+
function renderContent(screenshotAttachment: EventAttachment) {
100+
const downloadUrl = `/api/0/projects/${organization.slug}/${projectSlug}/events/${event.id}/attachments/${screenshotAttachment.id}/`;
85101

86102
return (
87103
<Fragment>
88104
<StyledPanelBody>
89-
<StyledImageViewer
90-
attachment={firstAttachmenteWithPreview}
105+
<ImageVisualization
106+
attachment={screenshotAttachment}
91107
orgId={orgSlug}
92108
projectId={projectSlug}
93109
event={event}
94110
/>
95111
</StyledPanelBody>
96112
<StyledPanelFooter>
97113
<StyledButtonbar gap={1}>
98-
<Button size="xsmall">{t('View screenshot')}</Button>
114+
<Button
115+
size="xsmall"
116+
onClick={() =>
117+
handleOpenVisualizationModal(
118+
screenshotAttachment,
119+
`${downloadUrl}?download=1`
120+
)
121+
}
122+
>
123+
{t('View screenshot')}
124+
</Button>
99125
<DropdownLink
100126
caret={false}
101127
customTitle={
@@ -109,43 +135,46 @@ function Screenshot({event, api, organization, projectSlug}: Props) {
109135
>
110136
<MenuItemActionLink
111137
shouldConfirm={false}
112-
icon={<IconDownload size="xs" />}
113138
title={t('Download')}
114139
href={`${downloadUrl}?download=1`}
115140
>
116141
{t('Download')}
117142
</MenuItemActionLink>
143+
<MenuItemActionLink
144+
shouldConfirm
145+
title={t('Delete')}
146+
onAction={() => handleDelete(screenshotAttachment.id, downloadUrl)}
147+
header={t(
148+
'Screenshots help identify what the user saw when the event happened'
149+
)}
150+
message={t('Are you sure you wish to delete this screenshot?')}
151+
>
152+
{t('Delete')}
153+
</MenuItemActionLink>
118154
</DropdownLink>
119155
</StyledButtonbar>
120156
</StyledPanelFooter>
121157
</Fragment>
122158
);
123159
}
124160

125-
// the UI should only render the screenshots feature in events with platforms that support screenshots
126-
if (
127-
!eventPlatform ||
128-
!platformsMobileWithAttachmentsFeature.includes(eventPlatform as any)
129-
) {
130-
return null;
131-
}
132-
133161
return (
134162
<Role role={organization.attachmentsRole}>
135163
{({hasRole}) => {
136-
if (!hasRole) {
137-
// if the user has no access to the attachments,
138-
// the UI shall not display the screenshot section
164+
const screenshotAttachment = attachments.find(hasScreenshot);
165+
166+
if (!hasRole || isLoading || !screenshotAttachment) {
139167
return null;
140168
}
169+
141170
return (
142171
<DataSection
143172
title={t('Screenshots')}
144173
description={t(
145-
'Screenshots help identify what the user saw when the exception happened'
174+
'Screenshots help identify what the user saw when the event happened'
146175
)}
147176
>
148-
<StyledPanel>{renderContent()}</StyledPanel>
177+
<StyledPanel>{renderContent(screenshotAttachment)}</StyledPanel>
149178
</DataSection>
150179
);
151180
}}
@@ -161,14 +190,19 @@ const StyledPanel = styled(Panel)`
161190
justify-content: center;
162191
align-items: center;
163192
margin-bottom: 0;
164-
min-width: 175px;
165193
min-height: 200px;
194+
min-width: 175px;
166195
`;
167196

168197
const StyledPanelBody = styled(PanelBody)`
169198
height: 175px;
170-
width: 100%;
171199
overflow: hidden;
200+
border: 1px solid ${p => p.theme.border};
201+
border-radius: ${p => p.theme.borderRadius};
202+
margin: -1px;
203+
width: calc(100% + 2px);
204+
border-bottom-left-radius: 0;
205+
border-bottom-right-radius: 0;
172206
`;
173207

174208
const StyledPanelFooter = styled(PanelFooter)`
@@ -182,14 +216,3 @@ const StyledButtonbar = styled(ButtonBar)`
182216
height: 24px;
183217
}
184218
`;
185-
186-
const StyledImageViewer = styled(ImageViewer)`
187-
padding: 0;
188-
height: 100%;
189-
img {
190-
width: auto;
191-
height: 100%;
192-
object-fit: cover;
193-
flex: 1;
194-
}
195-
`;

0 commit comments

Comments
 (0)