Skip to content

Commit a4465c9

Browse files
committed
feat: add success message, error message, move submit logic up into integration class
1 parent d0e8bf6 commit a4465c9

File tree

11 files changed

+451
-134
lines changed

11 files changed

+451
-134
lines changed

packages/feedback/src/index.ts

Lines changed: 127 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
// import { getCurrentHub } from '@sentry/core';
1+
import { getCurrentHub } from '@sentry/core';
22
import type { Integration } from '@sentry/types';
33
import { isNodeEnv } from '@sentry/utils';
44

5-
import type { FeedbackConfigurationWithDefaults } from './types';
5+
import { sendFeedback } from './sendFeedback';
6+
import type { FeedbackConfigurationWithDefaults, FeedbackFormData } from './types';
67
import { sendFeedbackRequest } from './util/sendFeedbackRequest';
78
import { Actor } from './widget/Actor';
89
import { createActorStyles } from './widget/Actor.css';
910
import { Dialog } from './widget/Dialog';
1011
import { createDialogStyles } from './widget/Dialog.css';
12+
import { SuccessMessage } from './widget/SuccessMessage';
1113

1214
export { sendFeedbackRequest };
1315

@@ -29,10 +31,14 @@ const THEME = {
2931
light: {
3032
background: '#ffffff',
3133
foreground: '#2B2233',
34+
success: '#268d75',
35+
error: '#df3338',
3236
},
3337
dark: {
3438
background: '#29232f',
3539
foreground: '#EBE6EF',
40+
success: '#2da98c',
41+
error: '#f55459',
3642
},
3743
};
3844

@@ -76,6 +82,11 @@ export class Feedback implements Integration {
7682
*/
7783
private _isDialogOpen: boolean;
7884

85+
/**
86+
* Tracks if dialog has ever been opened at least one time
87+
*/
88+
private _hasDialogOpened: boolean;
89+
7990
public constructor({
8091
showEmail = true,
8192
showName = true,
@@ -97,6 +108,7 @@ export class Feedback implements Integration {
97108
messageLabel = 'Description',
98109
namePlaceholder = 'Your Name',
99110
nameLabel = 'Name',
111+
successMessageText = 'Thank you for your report!',
100112
}: Partial<FeedbackConfigurationWithDefaults> = {}) {
101113
// Initializations
102114
this.name = Feedback.id;
@@ -105,6 +117,7 @@ export class Feedback implements Integration {
105117
this._host = null;
106118
this._shadow = null;
107119
this._isDialogOpen = false;
120+
this._hasDialogOpened = false;
108121

109122
this.options = {
110123
isAnonymous,
@@ -124,6 +137,7 @@ export class Feedback implements Integration {
124137
messagePlaceholder,
125138
nameLabel,
126139
namePlaceholder,
140+
successMessageText,
127141
};
128142

129143
// TOOD: temp for testing;
@@ -164,21 +178,60 @@ export class Feedback implements Integration {
164178
this._shadow = this._createShadowHost();
165179
}
166180

167-
this._shadow.appendChild(createDialogStyles(document, THEME));
168-
this._dialog = Dialog({ onCancel: this.closeDialog, options: this.options });
181+
// Lazy-load until dialog is opened and only inject styles once
182+
if (!this._hasDialogOpened) {
183+
this._shadow.appendChild(createDialogStyles(document, THEME));
184+
}
185+
186+
const userKey = this.options.useSentryUser;
187+
const scope = getCurrentHub().getScope();
188+
const user = scope && scope.getUser();
189+
190+
this._dialog = Dialog({
191+
defaultName: (userKey && user && user[userKey.name]) || '',
192+
defaultEmail: (userKey && user && user[userKey.email]) || '',
193+
onClose: () => {
194+
this.showActor();
195+
},
196+
onCancel: () => {
197+
this.hideDialog();
198+
this.showActor();
199+
},
200+
onSubmit: this._handleFeedbackSubmit,
201+
options: this.options,
202+
});
169203
this._shadow.appendChild(this._dialog.$el);
204+
205+
// Hides the default actor whenever dialog is opened
170206
this._actor && this._actor.hide();
207+
208+
this._hasDialogOpened = true;
171209
}
172210

173211
/**
174-
* Closes the dialog
212+
* Hides the dialog
175213
*/
176-
public closeDialog = (): void => {
214+
public hideDialog = (): void => {
177215
if (this._dialog) {
178216
this._dialog.close();
179217
}
218+
};
219+
220+
/**
221+
* Removes the dialog element from DOM
222+
*/
223+
public removeDialog = (): void => {
224+
if (this._dialog) {
225+
this._dialog.$el.remove();
226+
this._dialog = null;
227+
}
228+
};
180229

181-
// TODO: if has default actor, show the button
230+
/**
231+
* Displays the default actor
232+
*/
233+
public showActor = (): void => {
234+
// TODO: Only show default actor
182235
if (this._actor) {
183236
this._actor.show();
184237
}
@@ -218,7 +271,12 @@ export class Feedback implements Integration {
218271
this._host.id = 'sentry-feedback';
219272

220273
// Create the shadow root
221-
return this._host.attachShadow({ mode: 'open' });
274+
const shadow = this._host.attachShadow({ mode: 'open' });
275+
276+
// Insert styles for actor
277+
shadow.appendChild(createActorStyles(document, THEME));
278+
279+
return shadow;
222280
}
223281

224282
/**
@@ -230,7 +288,6 @@ export class Feedback implements Integration {
230288
return;
231289
}
232290

233-
// Insert styles for actor
234291
this._shadow.appendChild(createActorStyles(document, THEME));
235292

236293
// Create Actor component
@@ -239,6 +296,34 @@ export class Feedback implements Integration {
239296
this._shadow.appendChild(this._actor.$el);
240297
}
241298

299+
/**
300+
* Show the success message for 5 seconds
301+
*/
302+
protected _showSuccessMessage(): void {
303+
if (!this._shadow) {
304+
return;
305+
}
306+
307+
const success = SuccessMessage({
308+
message: this.options.successMessageText,
309+
onRemove: () => {
310+
if (timeoutId) {
311+
clearTimeout(timeoutId);
312+
}
313+
this.showActor();
314+
},
315+
theme: THEME,
316+
});
317+
318+
this._shadow.appendChild(success.$el);
319+
320+
const timeoutId = setTimeout(() => {
321+
if (success) {
322+
success.remove();
323+
}
324+
}, 5000);
325+
}
326+
242327
/**
243328
* Handles when the actor is clicked, opens the dialog modal and calls any
244329
* callbacks.
@@ -254,4 +339,37 @@ export class Feedback implements Integration {
254339
this._actor.hide();
255340
}
256341
};
342+
343+
/**
344+
* Handler for when the feedback form is completed by the user. This will
345+
* create and send the feedback message as an event.
346+
*/
347+
protected _handleFeedbackSubmit = async (feedback: FeedbackFormData): Promise<void> => {
348+
console.log('ahndle feedback submit');
349+
if (!this._dialog) {
350+
// Not sure when this would happen
351+
return;
352+
}
353+
354+
try {
355+
this._dialog.hideError();
356+
this._dialog.setSubmitDisabled();
357+
const resp = await sendFeedback(feedback);
358+
console.log({ resp });
359+
if (resp) {
360+
// Success!
361+
this.removeDialog();
362+
this._showSuccessMessage();
363+
return;
364+
}
365+
366+
// Errored... re-enable submit button
367+
this._dialog.setSubmitEnabled();
368+
this._dialog.showError('There was a problem submitting feedback, please wait and try again.');
369+
} catch {
370+
// Errored... re-enable submit button
371+
this._dialog.setSubmitEnabled();
372+
this._dialog.showError('There was a problem submitting feedback, please wait and try again.');
373+
}
374+
};
257375
}

packages/feedback/src/sendFeedback.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { BrowserClient, Replay } from '@sentry/browser';
22
import { getCurrentHub } from '@sentry/core';
3-
import { GLOBAL_OBJ } from '@sentry/utils';
43

54
import { sendFeedbackRequest } from './util/sendFeedbackRequest';
65

@@ -22,13 +21,13 @@ export function sendFeedback(
2221
{ name, email, message, url = document.location.href }: SendFeedbackParams,
2322
{ includeReplay = true }: SendFeedbackOptions = {},
2423
) {
25-
const replay = includeReplay
26-
? (getCurrentHub()?.getClient<BrowserClient>()?.getIntegrationById('Replay') as Replay | undefined)
27-
: undefined;
24+
const hub = getCurrentHub();
25+
const client = hub && hub.getClient<BrowserClient>();
26+
const replay = includeReplay && client ? (client.getIntegrationById('Replay') as Replay | undefined) : undefined;
2827

2928
// Prepare session replay
30-
replay?.flush();
31-
const replayId = replay?.getReplayId();
29+
replay && replay.flush();
30+
const replayId = replay && replay.getReplayId();
3231

3332
return sendFeedbackRequest({
3433
feedback: {

packages/feedback/src/types/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ export interface FeedbackConfigurationWithDefaults {
109109
* Placeholder text for Feedback name input
110110
*/
111111
namePlaceholder: string;
112+
/**
113+
* Message after feedback was sent successfully
114+
*/
115+
successMessageText: string;
112116
// * End of text customization * //
113117
}
114118

@@ -121,6 +125,14 @@ interface BaseTheme {
121125
* Foreground color (i.e. text color)
122126
*/
123127
foreground: string;
128+
/**
129+
* Success color
130+
*/
131+
success: string;
132+
/**
133+
* Error color
134+
*/
135+
error: string;
124136
}
125137

126138
export interface FeedbackTheme {

packages/feedback/src/widget/Actor.css.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,27 @@ export function createActorStyles(d: Document, theme: FeedbackTheme): HTMLStyleE
1111
right: 1rem;
1212
bottom: 1rem;
1313
font-family: 'Helvetica Neue', Arial, sans-serif;
14+
font-size: 14px;
1415
--bg-color: ${theme.light.background};
1516
--bg-hover-color: #f6f6f7;
1617
--fg-color: ${theme.light.foreground};
18+
--error-color: #df3338;
19+
--success-color: #268d75;
1720
--border: 1.5px solid rgba(41, 35, 47, 0.13);
1821
--box-shadow: 0px 4px 24px 0px rgba(43, 34, 51, 0.12);
1922
}
2023
21-
.__sntry_fdbk_dark:host {
24+
.__dark-mode:host {
2225
--bg-color: ${theme.dark.background};
2326
--bg-hover-color: #352f3b;
2427
--fg-color: ${theme.dark.foreground};
28+
--error-color: #f55459;
29+
--success-color: #2da98c;
2530
--border: 1.5px solid rgba(235, 230, 239, 0.15);
2631
--box-shadow: 0px 4px 24px 0px rgba(43, 34, 51, 0.12);
2732
}
2833
29-
.widget-actor {
34+
.widget__actor {
3035
line-height: 25px;
3136
3237
display: flex;
@@ -49,22 +54,39 @@ export function createActorStyles(d: Document, theme: FeedbackTheme): HTMLStyleE
4954
transition: opacity 0.1s ease-in-out;
5055
}
5156
52-
.widget-actor:hover {
57+
.widget__actor:hover {
5358
background-color: var(--bg-hover-color);
5459
}
5560
56-
.widget-actor svg {
61+
.widget__actor svg {
5762
width: 16px;
5863
height: 16px;
5964
}
6065
61-
.widget-actor.hidden {
66+
.widget__actor--hidden {
6267
opacity: 0;
6368
pointer-events: none;
6469
visibility: hidden;
6570
}
6671
67-
.widget-actor-text {
72+
.widget__actor__text {
73+
}
74+
75+
.success-message {
76+
background-color: var(--bg-color);
77+
border: var(--border);
78+
border-radius: 12px;
79+
box-shadow: var(--box-shadow);
80+
font-weight: 600;
81+
color: var(--success-color);
82+
padding: 12px 24px;
83+
line-height: 25px;
84+
display: grid;
85+
align-items: center;
86+
grid-auto-flow: column;
87+
gap: 6px;
88+
cursor: default;
89+
6890
}
6991
`;
7092

packages/feedback/src/widget/Actor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function Actor({ options, theme, onClick }: Props): ActorComponent {
3535
className: 'widget__actor',
3636
ariaLabel: options.buttonLabel,
3737
},
38-
Icon({ color: theme.light.foreground }),
38+
Icon({ color: theme.light.foreground }).$el,
3939
h(
4040
'span',
4141
{

0 commit comments

Comments
 (0)