Skip to content

Commit bafe626

Browse files
committed
streamline some props for attachments and createDialog
1 parent c152ee5 commit bafe626

File tree

9 files changed

+108
-90
lines changed

9 files changed

+108
-90
lines changed

packages/feedback2/src/core/integration.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -363,25 +363,15 @@ export class Feedback2 implements Integration {
363363
throw new Error('Not implemented yet');
364364
}
365365

366-
const shadow = this._getShadow(options);
367-
368-
// TODO: some combination stuff when screenshots exists:
369-
const dialog = modalIntegration.createDialog(
366+
const dialog = modalIntegration.createDialog({
367+
shadow: this._getShadow(options),
368+
sendFeedback,
370369
options,
371-
{
372-
onCreate: (dialog: DialogComponent) => {
373-
shadow.appendChild(dialog.style);
374-
shadow.appendChild(dialog.el);
375-
},
376-
onSubmit: sendFeedback,
377-
onDone: (dialog: DialogComponent) => {
378-
shadow.removeChild(dialog.el);
379-
shadow.removeChild(dialog.style);
380-
this._dialog = null;
381-
},
370+
onDone: () => {
371+
this._dialog = null;
382372
},
383373
screenshotIntegration,
384-
);
374+
});
385375
this._dialog = dialog;
386376
return dialog;
387377
}

packages/feedback2/src/core/sendFeedback.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { prepareFeedbackEvent } from '../util/prepareFeedbackEvent';
99
* Public API to send a Feedback item to Sentry
1010
*/
1111
export async function sendFeedback(
12-
{ name, email, message, attachment, source = FEEDBACK_API_SOURCE, url = getLocationHref() }: SendFeedbackParams,
12+
{ name, email, message, attachments, source = FEEDBACK_API_SOURCE, url = getLocationHref() }: SendFeedbackParams,
1313
{ includeReplay = true }: SendFeedbackOptions = {},
1414
): Promise<TransportMakeRequestResponse> {
1515
if (!message) {
@@ -60,17 +60,12 @@ export async function sendFeedback(
6060
createEventEnvelope(feedbackEvent, dsn, client.getOptions()._metadata, client.getOptions().tunnel),
6161
);
6262

63-
if (attachment) {
64-
const formatted = {
65-
filename: attachment.name,
66-
data: new Uint8Array(await attachment.arrayBuffer()),
67-
contentType: attachment.type,
68-
};
69-
console.log({ formatted });
63+
if (attachments && attachments.length) {
64+
// TODO: https://docs.sentry.io/platforms/javascript/enriching-events/attachments/
7065
await transport.send(
7166
createAttachmentEnvelope(
7267
feedbackEvent,
73-
[formatted],
68+
attachments,
7469
dsn,
7570
client.getOptions()._metadata,
7671
client.getOptions().tunnel,

packages/feedback2/src/modal/components/Form.tsx

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,8 @@ export function Form({
7171
const ScreenshotInput = screenshotWidget && screenshotWidget.input;
7272
const ScreenshotToggle = screenshotWidget && screenshotWidget.toggle;
7373

74-
const validateForm = useCallback(
75-
(form: HTMLFormElement) => {
76-
const formData = new FormData(form);
77-
const data: FeedbackFormData = {
78-
name: retrieveStringValue(formData, 'name'),
79-
email: retrieveStringValue(formData, 'email'),
80-
message: retrieveStringValue(formData, 'message'),
81-
attachment: (formData.get('attachment') as File) || undefined,
82-
};
74+
const hasAllRequiredFields = useCallback(
75+
(data: FeedbackFormData) => {
8376
const missingFields = getMissingFields(data, {
8477
emailLabel,
8578
isEmailRequired,
@@ -94,32 +87,27 @@ export function Form({
9487
setError(null);
9588
}
9689

97-
return { data, missingFields };
90+
return missingFields.length === 0;
9891
},
9992
[emailLabel, isEmailRequired, isNameRequired, messageLabel, nameLabel],
10093
);
10194

102-
const handleFormData = useCallback(
103-
async (e: JSX.TargetedEvent<HTMLFormElement>) => {
104-
if (screenshotWidget && includeScreenshot && 'formData' in e && e.formData instanceof FormData) {
105-
const value = await screenshotWidget.value();
106-
if (value) {
107-
e.formData.set('attachment', value, 'screenshot.png');
108-
}
109-
}
110-
},
111-
[screenshotWidget, includeScreenshot],
112-
);
113-
11495
const handleSubmit = useCallback(
11596
async (e: JSX.TargetedSubmitEvent<HTMLFormElement>) => {
11697
try {
11798
e.preventDefault();
11899
if (!(e.target instanceof HTMLFormElement)) {
119100
return;
120101
}
121-
const { data, missingFields } = validateForm(e.target);
122-
if (missingFields.length > 0) {
102+
const formData = new FormData(e.target);
103+
const attachment = await (screenshotWidget && includeScreenshot ? screenshotWidget.value() : undefined);
104+
const data: FeedbackFormData = {
105+
name: retrieveStringValue(formData, 'name'),
106+
email: retrieveStringValue(formData, 'email'),
107+
message: retrieveStringValue(formData, 'message'),
108+
attachments: attachment ? [attachment] : undefined,
109+
};
110+
if (!hasAllRequiredFields(data)) {
123111
return;
124112
}
125113
try {
@@ -138,7 +126,7 @@ export function Form({
138126
);
139127

140128
return (
141-
<form class="form" onSubmit={handleSubmit} onFormData={handleFormData}>
129+
<form class="form" onSubmit={handleSubmit}>
142130
{error ? <div class="form__error-container">{error}</div> : null}
143131

144132
{ScreenshotInput && includeScreenshot ? <ScreenshotInput initialImage={undefined} /> : null}

packages/feedback2/src/modal/integration.ts

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,28 @@ import { getCurrentScope } from '@sentry/core';
22
import type { Integration, IntegrationFn } from '@sentry/types';
33
import { isBrowser } from '@sentry/utils';
44
import { h } from 'preact';
5+
import type { sendFeedback as sendFeedbackFn } from '../core/sendFeedback';
56
import type { Feedback2Screenshot } from '../screenshot/integration';
6-
import type { FeedbackFormData, FeedbackInternalOptions, SendFeedbackOptions, SendFeedbackParams } from '../types';
7+
import type { FeedbackFormData, FeedbackInternalOptions } from '../types';
78
import { Dialog } from './components/Dialog';
89
import type { DialogComponent } from './components/Dialog';
910

10-
/**
11-
* Internal callbacks for pushing more form event code into the Feedback2Modal integration
12-
*/
13-
export interface DialogLifecycleCallbacks {
14-
/**
15-
* When the dialog is created.
16-
*/
17-
onCreate: (dialog: DialogComponent) => void;
11+
interface CreateDialogProps {
12+
shadow: ShadowRoot;
1813

19-
/**
20-
* When the data is ready to be submitted
21-
*/
22-
onSubmit: (data: SendFeedbackParams, options?: SendFeedbackOptions) => void;
14+
sendFeedback: typeof sendFeedbackFn;
15+
16+
options: FeedbackInternalOptions;
2317

2418
/**
2519
* When the dialog is either closed, or was submitted successfully, and nothing is rendered anymore.
2620
*
2721
* This is called as part of onFormClose and onFormSubmitted
2822
*/
29-
onDone: (dialog: DialogComponent) => void;
23+
onDone: () => void;
24+
25+
// eslint-disable-next-line deprecation/deprecation
26+
screenshotIntegration: Feedback2Screenshot | undefined;
3027
}
3128

3229
export const feedback2ModalIntegration = (() => {
@@ -69,12 +66,13 @@ export class Feedback2Modal implements Integration {
6966
/**
7067
*
7168
*/
72-
public createDialog(
73-
options: FeedbackInternalOptions,
74-
callbacks: DialogLifecycleCallbacks,
75-
// eslint-disable-next-line deprecation/deprecation
76-
screenshotIntegration: Feedback2Screenshot | undefined,
77-
): DialogComponent {
69+
public createDialog({
70+
shadow,
71+
sendFeedback,
72+
options,
73+
onDone,
74+
screenshotIntegration,
75+
}: CreateDialogProps): DialogComponent {
7876
const userKey = options.useSentryUser;
7977
const scope = getCurrentScope();
8078
const user = scope && scope.getUser();
@@ -102,22 +100,30 @@ export class Feedback2Modal implements Integration {
102100
defaultEmail: (userKey && user && user[userKey.email]) || '',
103101
successMessageText: options.successMessageText,
104102
onFormClose: () => {
105-
callbacks.onDone(dialog);
103+
shadow.removeChild(dialog.el);
104+
shadow.removeChild(dialog.style);
105+
screenshotWidget && shadow.removeChild(screenshotWidget.style);
106+
onDone();
106107
options.onFormClose && options.onFormClose();
107108
},
108-
onSubmit: callbacks.onSubmit,
109+
onSubmit: sendFeedback,
109110
onSubmitSuccess: (data: FeedbackFormData) => {
110111
options.onSubmitSuccess && options.onSubmitSuccess(data);
111112
},
112113
onSubmitError: (error: Error) => {
113114
options.onSubmitError && options.onSubmitError(error);
114115
},
115116
onFormSubmitted: () => {
116-
callbacks.onDone(dialog);
117+
shadow.removeChild(dialog.el);
118+
shadow.removeChild(dialog.style);
119+
screenshotWidget && shadow.removeChild(screenshotWidget.style);
120+
onDone();
117121
options.onFormSubmitted && options.onFormSubmitted();
118122
},
119123
});
120-
callbacks.onCreate(dialog);
124+
shadow.appendChild(dialog.style);
125+
screenshotWidget && shadow.appendChild(screenshotWidget.style);
126+
shadow.appendChild(dialog.el);
121127
options.onFormOpen && options.onFormOpen();
122128

123129
return dialog;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { DOCUMENT } from '../../constants';
2+
3+
/**
4+
* Creates <style> element for widget dialog
5+
*/
6+
export function createScreenshotInputStyles(): HTMLStyleElement {
7+
const style = DOCUMENT.createElement('style');
8+
9+
style.textContent = `
10+
.editor {
11+
background: red;
12+
width: 75px;
13+
height: 75px;
14+
}
15+
`;
16+
17+
return style;
18+
}

packages/feedback2/src/screenshot/components/ScreenshotInput.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ComponentType, VNode, h as hType } from 'preact';
2+
import { useEffect, useRef } from 'preact/hooks';
23

34
export interface Props {
45
initialImage: unknown;
@@ -9,9 +10,17 @@ export function makeInput(h: typeof hType, canvasEl: HTMLCanvasElement): Compone
910
return function ScreenshotToggle({ initialImage }: Props): VNode {
1011
console.log({ initialImage, canvasEl }); // eslint-disable-line no-console
1112

13+
const canvasContainerRef = useRef<HTMLDivElement>(null);
14+
useEffect(() => {
15+
const container = canvasContainerRef.current;
16+
container && container.appendChild(canvasEl);
17+
return () => container && container.removeChild(canvasEl);
18+
}, [canvasEl]);
19+
1220
return (
13-
<div style={{ background: 'red', width: '100px', height: '100px' }}>
21+
<div class="editor">
1422
<input type="text" />
23+
<div ref={canvasContainerRef} style={{ display: 'none' }} />
1524
</div>
1625
);
1726
};

packages/feedback2/src/screenshot/integration.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import type { Integration, IntegrationFn } from '@sentry/types';
1+
import type { Attachment, Integration, IntegrationFn } from '@sentry/types';
22
import { isBrowser } from '@sentry/utils';
33
import type { ComponentType, h as hType } from 'preact';
44
import { DOCUMENT } from '../constants';
55
import { makeInput } from './components/ScreenshotInput';
66
import type { Props as ScreenshotInputProps } from './components/ScreenshotInput';
7+
import { createScreenshotInputStyles } from './components/ScreenshotInput.css';
78
import { makeToggle } from './components/ScreenshotToggle';
89
import type { Props as ScreenshotToggleProps } from './components/ScreenshotToggle';
910

@@ -34,7 +35,7 @@ export class Feedback2Screenshot implements Integration {
3435
}
3536

3637
/**
37-
* Setup and initialize feedback container
38+
* Setupand initialize feedback container
3839
*/
3940
public setupOnce(): void {
4041
if (!isBrowser()) {
@@ -48,26 +49,34 @@ export class Feedback2Screenshot implements Integration {
4849
*/
4950
public createWidget(h: typeof hType): ScreenshotWidget {
5051
const canvasEl = DOCUMENT.createElement('canvas');
52+
5153
return {
54+
style: createScreenshotInputStyles(),
5255
input: makeInput(h, canvasEl),
5356
toggle: makeToggle(h),
54-
value: () => {
55-
// TODO: maybe this only returns if the canvas is in the document?
56-
// then handleFormData can be moved into this integration!
57-
return canvasToBlob(canvasEl);
57+
value: async () => {
58+
const blob = await new Promise<Parameters<BlobCallback>[0]>(resolve => {
59+
canvasEl.toBlob(resolve, 'image/png');
60+
});
61+
if (blob) {
62+
const data = new Uint8Array(await blob.arrayBuffer());
63+
const attachment: Attachment = {
64+
data,
65+
filename: 'screenshot.png',
66+
contentType: 'application/png',
67+
// attachmentType?: string;
68+
};
69+
return attachment;
70+
}
71+
return;
5872
},
5973
};
6074
}
6175
}
6276

63-
async function canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob | null> {
64-
return new Promise(resolve => {
65-
canvas.toBlob(resolve);
66-
});
67-
}
68-
6977
export interface ScreenshotWidget {
78+
style: HTMLStyleElement;
7079
input: ComponentType<ScreenshotInputProps>;
7180
toggle: ComponentType<ScreenshotToggleProps>;
72-
value: () => Promise<Blob | null>;
81+
value: () => Promise<Attachment | undefined>;
7382
}

packages/feedback2/src/types/form.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import type { Attachment } from '@sentry/types';
2+
13
export type FeedbackFormData = {
24
name: string;
35
email: string;
46
message: string;
5-
attachment: File | undefined;
7+
attachments: Attachment[] | undefined;
68
};

packages/feedback2/src/types/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Attachment } from '@sentry/types';
12
import type {
23
FeedbackCallbacks,
34
FeedbackGeneralConfiguration,
@@ -43,7 +44,7 @@ export interface SendFeedbackParams {
4344
message: string;
4445
name?: string;
4546
email?: string;
46-
attachment?: File;
47+
attachments?: Attachment[];
4748
url?: string;
4849
source?: string;
4950
}

0 commit comments

Comments
 (0)