Skip to content

Commit 8a2a3e4

Browse files
fehmerMiodec
andauthored
impr: add copy details to notification history (@fehmer) (monkeytypegame#7262)
Add details to notifications. If details are available show share icon on hover in the notification history. On click the full content is copied to the clipboard. With this is easier for an user to share the full details of an error on github or discord. <img width="377" height="107" alt="image" src="https://github.com/user-attachments/assets/f22638a1-bafd-4708-8d8a-0ec48db10f1d" /> <img width="377" height="107" alt="image" src="https://github.com/user-attachments/assets/d4a66860-f99f-4ac1-992c-81e31ab13eba" /> ```json { "title": "Error", "message": "Failed to save config", "details": { "status": 422, "validationErrors": [ "Unrecognized key(s) in object: 'invalid'" ] } } ``` --------- Co-authored-by: Jack <[email protected]>
1 parent 484ab1b commit 8a2a3e4

25 files changed

+227
-109
lines changed

frontend/src/styles/popups.scss

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,15 +1863,17 @@ body.darkMode {
18631863
}
18641864
}
18651865
.notificationHistory .list .item {
1866-
grid-template-areas: "indicator title" "indicator body";
1867-
grid-template-columns: 0.25rem calc(100% - 0.25rem);
1866+
grid-template-areas: "indicator title buttons" "indicator body buttons";
18681867
.title {
18691868
font-size: 0.75rem;
18701869
color: var(--sub-color);
18711870
}
18721871
.body {
18731872
opacity: 1;
18741873
}
1874+
.highlight {
1875+
color: var(--main-color) !important;
1876+
}
18751877
}
18761878
.accountAlerts {
18771879
.title {

frontend/src/ts/auth.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,11 @@ async function sendVerificationEmail(): Promise<void> {
4545

4646
Loader.show();
4747
qs(".sendVerificationEmail")?.disable();
48-
const result = await Ape.users.verificationEmail();
48+
const response = await Ape.users.verificationEmail();
4949
qs(".sendVerificationEmail")?.enable();
50-
if (result.status !== 200) {
50+
if (response.status !== 200) {
5151
Loader.hide();
52-
Notifications.add(
53-
"Failed to request verification email: " + result.body.message,
54-
-1,
55-
);
52+
Notifications.add("Failed to request verification email", -1, { response });
5653
} else {
5754
Loader.hide();
5855
Notifications.add("Verification email sent", 1);

frontend/src/ts/db.ts

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ export async function getUserResults(offset?: number): Promise<boolean> {
299299
const response = await Ape.results.get({ query: { offset } });
300300

301301
if (response.status !== 200) {
302-
Notifications.add("Error getting results: " + response.body.message, -1);
302+
Notifications.add("Error getting results", -1, { response });
303303
return false;
304304
}
305305

@@ -357,10 +357,7 @@ export async function addCustomTheme(
357357

358358
const response = await Ape.users.addCustomTheme({ body: { ...theme } });
359359
if (response.status !== 200) {
360-
Notifications.add(
361-
"Error adding custom theme: " + response.body.message,
362-
-1,
363-
);
360+
Notifications.add("Error adding custom theme", -1, { response });
364361
return false;
365362
}
366363

@@ -400,10 +397,7 @@ export async function editCustomTheme(
400397
body: { themeId, theme: newTheme },
401398
});
402399
if (response.status !== 200) {
403-
Notifications.add(
404-
"Error editing custom theme: " + response.body.message,
405-
-1,
406-
);
400+
Notifications.add("Error editing custom theme", -1, { response });
407401
return false;
408402
}
409403

@@ -427,10 +421,7 @@ export async function deleteCustomTheme(themeId: string): Promise<boolean> {
427421

428422
const response = await Ape.users.deleteCustomTheme({ body: { themeId } });
429423
if (response.status !== 200) {
430-
Notifications.add(
431-
"Error deleting custom theme: " + response.body.message,
432-
-1,
433-
);
424+
Notifications.add("Error deleting custom theme", -1, { response });
434425
return false;
435426
}
436427

@@ -923,7 +914,7 @@ export async function saveConfig(config: Partial<Config>): Promise<void> {
923914
if (isAuthenticated()) {
924915
const response = await Ape.configs.save({ body: config });
925916
if (response.status !== 200) {
926-
Notifications.add("Failed to save config: " + response.body.message, -1);
917+
Notifications.add("Failed to save config", -1, { response });
927918
}
928919
}
929920
}
@@ -932,7 +923,7 @@ export async function resetConfig(): Promise<void> {
932923
if (isAuthenticated()) {
933924
const response = await Ape.configs.delete();
934925
if (response.status !== 200) {
935-
Notifications.add("Failed to reset config: " + response.body.message, -1);
926+
Notifications.add("Failed to reset config", -1, { response });
936927
}
937928
}
938929
}
@@ -1055,10 +1046,7 @@ export async function getTestActivityCalendar(
10551046
Loader.show();
10561047
const response = await Ape.users.getTestActivity();
10571048
if (response.status !== 200) {
1058-
Notifications.add(
1059-
"Error getting test activities: " + response.body.message,
1060-
-1,
1061-
);
1049+
Notifications.add("Error getting test activities", -1, { response });
10621050
Loader.hide();
10631051
return undefined;
10641052
}

frontend/src/ts/elements/account-settings/ape-key-table.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ const editApeKey = new SimpleModal({
2727
if (response.status !== 200) {
2828
return {
2929
status: -1,
30-
message: "Failed to update key: " + response.body.message,
30+
message: "Failed to update key",
31+
notificationOptions: { response },
3132
};
3233
}
3334
return {
@@ -53,7 +54,8 @@ const deleteApeKeyModal = new SimpleModal({
5354
if (response.status !== 200) {
5455
return {
5556
status: -1,
56-
message: "Failed to delete key: " + response.body.message,
57+
message: "Failed to delete key",
58+
notificationOptions: { response },
5759
};
5860
}
5961

@@ -128,7 +130,8 @@ const generateApeKey = new SimpleModal({
128130
if (response.status !== 200) {
129131
return {
130132
status: -1,
131-
message: "Failed to generate key: " + response.body.message,
133+
message: "Failed to generate key",
134+
notificationOptions: { response },
132135
};
133136
}
134137

@@ -174,7 +177,7 @@ async function getData(): Promise<boolean> {
174177
void update();
175178
return false;
176179
}
177-
Notifications.add("Error getting ape keys: " + response.body.message, -1);
180+
Notifications.add("Error getting ape keys", -1, { response });
178181
return false;
179182
}
180183

@@ -261,7 +264,7 @@ async function toggleActiveKey(keyId: string): Promise<void> {
261264
});
262265
Loader.hide();
263266
if (response.status !== 200) {
264-
Notifications.add("Failed to update key: " + response.body.message, -1);
267+
Notifications.add("Failed to update key", -1, { response });
265268
return;
266269
}
267270
key.enabled = !key.enabled;

frontend/src/ts/elements/account-settings/blocked-user-table.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@ async function getData(): Promise<boolean> {
2424

2525
if (response.status !== 200) {
2626
blockedUsers = [];
27-
Notifications.add(
28-
"Error getting blocked users: " + response.body.message,
29-
-1,
30-
);
27+
Notifications.add("Error getting blocked users", -1, { response });
3128
return false;
3229
}
3330

frontend/src/ts/elements/alerts.ts

Lines changed: 106 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import * as NotificationEvent from "../observables/notification-event";
66
import * as BadgeController from "../controllers/badge-controller";
77
import * as Notifications from "../elements/notifications";
88
import * as ConnectionState from "../states/connection";
9-
import { escapeHTML } from "../utils/misc";
9+
import {
10+
applyReducedMotion,
11+
createErrorMessage,
12+
escapeHTML,
13+
promiseAnimate,
14+
} from "../utils/misc";
1015
import AnimatedModal from "../utils/animated-modal";
1116
import { updateXp as accountPageUpdateProfile } from "./profile";
1217
import { MonkeyMail } from "@monkeytype/schemas/users";
@@ -29,10 +34,17 @@ let mailToMarkRead: string[] = [];
2934
let mailToDelete: string[] = [];
3035

3136
type State = {
32-
notifications: { message: string; level: number; customTitle?: string }[];
37+
notifications: {
38+
id: string;
39+
title: string;
40+
message: string;
41+
level: number;
42+
details?: string | object;
43+
}[];
3344
psas: { message: string; level: number }[];
3445
};
3546

47+
let notificationId = 0;
3648
const state: State = {
3749
notifications: [],
3850
psas: [],
@@ -289,28 +301,29 @@ function fillNotifications(): void {
289301
} else {
290302
notificationHistoryListEl.empty();
291303
for (const n of state.notifications) {
292-
const { message, level, customTitle } = n;
293-
let title = "Notice";
304+
const { message, level, title } = n;
305+
294306
let levelClass = "sub";
295307
if (level === -1) {
296308
levelClass = "error";
297-
title = "Error";
298309
} else if (level === 1) {
299310
levelClass = "main";
300-
title = "Success";
301-
}
302-
303-
if (customTitle !== undefined) {
304-
title = customTitle;
305311
}
306312

307313
notificationHistoryListEl.prependHtml(`
308-
<div class="item">
314+
<div class="item" data-id="${n.id}">
309315
<div class="indicator ${levelClass}"></div>
310316
<div class="title">${title}</div>
311317
<div class="body">
312318
${escapeHTML(message)}
313319
</div>
320+
<div class="buttons">
321+
${
322+
n.details !== undefined
323+
? `<button class="copyNotification textButton" aria-label="Copy details to clipboard" data-balloon-pos="left"><i class="fas fa-fw fa-clipboard"></i></button>`
324+
: ``
325+
}
326+
</div>
314327
</div>
315328
`);
316329
}
@@ -396,15 +409,89 @@ function updateClaimDeleteAllButton(): void {
396409
}
397410
}
398411

412+
async function copyNotificationToClipboard(target: HTMLElement): Promise<void> {
413+
const id = (target as HTMLElement | null)
414+
?.closest(".item")
415+
?.getAttribute("data-id")
416+
?.toString();
417+
418+
if (id === undefined) {
419+
throw new Error("Notification ID is undefined");
420+
}
421+
const notification = state.notifications.find((it) => it.id === id);
422+
if (notification === undefined) return;
423+
424+
const icon = target.querySelector("i") as HTMLElement;
425+
426+
try {
427+
await navigator.clipboard.writeText(
428+
JSON.stringify(
429+
{
430+
title: notification.title,
431+
message: notification.message,
432+
details: notification.details,
433+
},
434+
null,
435+
4,
436+
),
437+
);
438+
439+
const duration = applyReducedMotion(100);
440+
441+
await promiseAnimate(icon, {
442+
scale: [1, 0.8],
443+
opacity: [1, 0],
444+
duration,
445+
});
446+
icon.classList.remove("fa-clipboard");
447+
icon.classList.add("fa-check", "highlight");
448+
await promiseAnimate(icon, {
449+
scale: [0.8, 1],
450+
opacity: [0, 1],
451+
duration,
452+
});
453+
454+
await promiseAnimate(icon, {
455+
scale: [1, 0.8],
456+
opacity: [1, 0],
457+
delay: 3000,
458+
duration,
459+
});
460+
icon.classList.remove("fa-check", "highlight");
461+
icon.classList.add("fa-clipboard");
462+
463+
await promiseAnimate(icon, {
464+
scale: [0.8, 1],
465+
opacity: [0, 1],
466+
duration,
467+
});
468+
} catch (e: unknown) {
469+
const msg = createErrorMessage(e, "Could not copy to clipboard");
470+
Notifications.add(msg, -1);
471+
}
472+
}
473+
399474
qs("header nav .showAlerts")?.on("click", () => {
400475
void show();
401476
});
402477

403-
NotificationEvent.subscribe((message, level, customTitle) => {
478+
NotificationEvent.subscribe((message, level, options) => {
479+
let title = "Notice";
480+
if (level === -1) {
481+
title = "Error";
482+
} else if (level === 1) {
483+
title = "Success";
484+
}
485+
if (options.customTitle !== undefined) {
486+
title = options.customTitle;
487+
}
488+
404489
state.notifications.push({
490+
id: (notificationId++).toString(),
491+
title,
405492
message,
406493
level,
407-
customTitle,
494+
details: options.details,
408495
});
409496
if (state.notifications.length > 25) {
410497
state.notifications.shift();
@@ -495,5 +582,11 @@ const modal = new AnimatedModal({
495582

496583
markReadAlert(id);
497584
});
585+
586+
alertsPopupEl
587+
.qs(".notificationHistory .list")
588+
?.onChild("click", ".item .buttons .copyNotification", (e) => {
589+
void copyNotificationToClipboard(e.target as HTMLElement);
590+
});
498591
},
499592
});

frontend/src/ts/elements/notifications.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as NotificationEvent from "../observables/notification-event";
55
import { convertRemToPixels } from "../utils/numbers";
66
import { animate } from "animejs";
77
import { qsr } from "../utils/dom";
8+
import { CommonResponsesType } from "@monkeytype/contracts/util/api";
89

910
const notificationCenter = qsr("#notificationCenter");
1011
const notificationCenterHistory = notificationCenter.qsr(".history");
@@ -107,6 +108,7 @@ class Notification {
107108
visibleStickyNotifications++;
108109
updateClearAllButton();
109110
}
111+
110112
notificationCenterHistory.prependHtml(`
111113
<div class="notif ${cls}" id=${this.id} style="opacity: 0;">
112114
<div class="message"><div class="title"><div class="icon">${icon}</div>${title}</div>${this.message}</div>
@@ -270,14 +272,33 @@ export type AddNotificationOptions = {
270272
customIcon?: string;
271273
closeCallback?: () => void;
272274
allowHTML?: boolean;
275+
details?: object | string;
276+
response?: CommonResponsesType;
273277
};
274278

275279
export function add(
276280
message: string,
277281
level = 0,
278282
options: AddNotificationOptions = {},
279283
): void {
280-
NotificationEvent.dispatch(message, level, options.customTitle);
284+
let details = options.details;
285+
286+
if (options.response !== undefined) {
287+
details = {
288+
status: options.response.status,
289+
additionalDetails: options.details,
290+
validationErrors:
291+
options.response.status === 422
292+
? options.response.body.validationErrors
293+
: undefined,
294+
};
295+
message = message + ": " + options.response.body.message;
296+
}
297+
298+
NotificationEvent.dispatch(message, level, {
299+
customTitle: options.customTitle,
300+
details,
301+
});
281302

282303
new Notification(
283304
"notification",

0 commit comments

Comments
 (0)