ESDP - List Parser - Notification fixes 0.0.5
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -52,3 +52,5 @@ override.tf.json
|
||||
.terraformrc
|
||||
terraform.rc
|
||||
/.eslintcache
|
||||
/docs
|
||||
/.idea
|
||||
@@ -19,6 +19,10 @@ asarUnpack:
|
||||
win:
|
||||
executableName: EMSDP
|
||||
icon: resources/icon.png
|
||||
protocols:
|
||||
- name: EMS Data Pump
|
||||
schemes:
|
||||
- esdp
|
||||
azureSignOptions:
|
||||
endpoint: https://eus.codesigning.azure.net
|
||||
certificateProfileName: ImEXRPS
|
||||
@@ -38,6 +42,7 @@ mac:
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
- NSUserNotificationAlertStyle: alert
|
||||
- CFBundleURLTypes:
|
||||
- CFBundleTypeRole: Viewer # More specific role for protocol handling
|
||||
CFBundleURLName: com.imex.esdp
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "esdp",
|
||||
"productName": "EMS Uploader",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"description": "EMS Uploader",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "ImEX Systems Inc.",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
!macro customInstall
|
||||
; Register imexmedia protocol
|
||||
WriteRegStr HKCR "imexmedia" "" "URL:ImEX Shop Partner Protocol"
|
||||
WriteRegStr HKCR "imexmedia" "URL Protocol" ""
|
||||
WriteRegStr HKCR "imexmedia\\shell" "" ""
|
||||
WriteRegStr HKCR "imexmedia\\shell\\open" "" ""
|
||||
WriteRegStr HKCR "imexmedia\\shell\\open\\command" "" '"$INSTDIR\ShopPartner.exe" "%1"'
|
||||
; Register esdp protocol
|
||||
WriteRegStr HKCR "esdp" "" "URL:EMS Data Pump Protocol"
|
||||
WriteRegStr HKCR "esdp" "URL Protocol" ""
|
||||
WriteRegStr HKCR "esdp\\shell" "" ""
|
||||
WriteRegStr HKCR "esdp\\shell\\open" "" ""
|
||||
WriteRegStr HKCR "esdp\\shell\\open\\command" "" '"$INSTDIR\EMSDP.exe" "%1"'
|
||||
!macroend
|
||||
|
||||
!macro customUnInstall
|
||||
; Remove imexmedia protocol
|
||||
DeleteRegKey HKCR "imexmedia"
|
||||
!macroend
|
||||
; Remove esdp protocol
|
||||
DeleteRegKey HKCR "esdp"
|
||||
!macroend
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { platform } from "@electron-toolkit/utils";
|
||||
import { UUID } from "crypto";
|
||||
import { app } from "electron";
|
||||
import log from "electron-log/main";
|
||||
import fs from "fs";
|
||||
import _ from "lodash";
|
||||
@@ -40,6 +41,65 @@ import getMainWindow from "../../util/getMainWindow";
|
||||
import newWindow from "../../util/newWindow";
|
||||
import { createNotification, showNotification } from "../util/notification";
|
||||
|
||||
const protocol = "esdp";
|
||||
|
||||
function escapeXml(value: string): string {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function createProtocolUrl(
|
||||
route: string,
|
||||
params: Record<string, string>,
|
||||
): string {
|
||||
const searchParams = new URLSearchParams(params);
|
||||
return `${protocol}://${route}?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
function createScrubbedToastXml({
|
||||
title,
|
||||
body,
|
||||
scrubHistoryJobId,
|
||||
scrubPdfURL,
|
||||
}: {
|
||||
title: string;
|
||||
body: string;
|
||||
scrubHistoryJobId?: string;
|
||||
scrubPdfURL?: string;
|
||||
}): string {
|
||||
const viewInAppUrl = scrubHistoryJobId
|
||||
? createProtocolUrl("scrub-history", { jobId: scrubHistoryJobId })
|
||||
: undefined;
|
||||
const openPdfUrl = scrubPdfURL
|
||||
? createProtocolUrl("open-pdf", { url: scrubPdfURL })
|
||||
: undefined;
|
||||
|
||||
const actions = [
|
||||
viewInAppUrl
|
||||
? `<action content="View in App" activationType="protocol" arguments="${escapeXml(viewInAppUrl)}" />`
|
||||
: "",
|
||||
openPdfUrl
|
||||
? `<action content="View PDF" activationType="protocol" arguments="${escapeXml(openPdfUrl)}" />`
|
||||
: "",
|
||||
].join("");
|
||||
|
||||
return `<toast activationType="protocol" launch="${escapeXml(
|
||||
viewInAppUrl ?? openPdfUrl ?? `${protocol}://`,
|
||||
)}">
|
||||
<visual>
|
||||
<binding template="ToastGeneric">
|
||||
<text>${escapeXml(title)}</text>
|
||||
<text>${escapeXml(body)}</text>
|
||||
</binding>
|
||||
</visual>
|
||||
${actions ? `<actions>${actions}</actions>` : ""}
|
||||
</toast>`;
|
||||
}
|
||||
|
||||
async function ImportJob(filepath: string): Promise<void> {
|
||||
const parsedFilePath = path.parse(filepath);
|
||||
const extensionlessFilePath = path.join(
|
||||
@@ -140,6 +200,11 @@ async function ImportJob(filepath: string): Promise<void> {
|
||||
|
||||
//Scrub the estimate
|
||||
const scrubResult = await ScrubEstimate({ job: jobObject });
|
||||
if (!scrubResult) {
|
||||
setAppProgressbar(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
const scrubPdfURL = scrubResult?.pdfUrl;
|
||||
const scrubHistoryJobId = scrubResult?.jobId;
|
||||
|
||||
@@ -154,15 +219,6 @@ async function ImportJob(filepath: string): Promise<void> {
|
||||
});
|
||||
setAppProgressbar(-1);
|
||||
|
||||
const uploadNotification = createNotification({
|
||||
title: "Job Scrubbed",
|
||||
body: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}.`,
|
||||
actions: [
|
||||
{ text: "View in App", type: "button" as const },
|
||||
...(scrubPdfURL ? [{ text: "View PDF", type: "button" as const }] : []),
|
||||
],
|
||||
});
|
||||
|
||||
const openScrubHistoryItem = (): void => {
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
@@ -170,7 +226,9 @@ async function ImportJob(filepath: string): Promise<void> {
|
||||
mainWindow.restore();
|
||||
}
|
||||
mainWindow.show();
|
||||
mainWindow.moveTop();
|
||||
mainWindow.focus();
|
||||
app.focus({ steal: true });
|
||||
if (scrubHistoryJobId) {
|
||||
mainWindow.webContents.send(ipcTypes.toRenderer.scrub.openHistoryItem, {
|
||||
jobId: scrubHistoryJobId,
|
||||
@@ -178,16 +236,48 @@ async function ImportJob(filepath: string): Promise<void> {
|
||||
}
|
||||
};
|
||||
|
||||
const notificationTitle = "Job Scrubbed";
|
||||
const notificationBody = `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}.`;
|
||||
const notificationActionHandlers: Array<() => void> = [];
|
||||
const notificationActions: Electron.NotificationAction[] = [];
|
||||
|
||||
if (scrubHistoryJobId) {
|
||||
notificationActions.push({
|
||||
text: "View in App",
|
||||
type: "button" as const,
|
||||
});
|
||||
notificationActionHandlers.push(openScrubHistoryItem);
|
||||
}
|
||||
|
||||
if (scrubPdfURL) {
|
||||
notificationActions.push({ text: "View PDF", type: "button" as const });
|
||||
notificationActionHandlers.push(() => newWindow(scrubPdfURL));
|
||||
}
|
||||
|
||||
const uploadNotification = createNotification(
|
||||
platform.isWindows
|
||||
? {
|
||||
title: notificationTitle,
|
||||
body: notificationBody,
|
||||
toastXml: createScrubbedToastXml({
|
||||
title: notificationTitle,
|
||||
body: notificationBody,
|
||||
scrubHistoryJobId,
|
||||
scrubPdfURL,
|
||||
}),
|
||||
}
|
||||
: {
|
||||
title: notificationTitle,
|
||||
body: notificationBody,
|
||||
actions: notificationActions,
|
||||
},
|
||||
);
|
||||
|
||||
uploadNotification.on("click", openScrubHistoryItem);
|
||||
|
||||
uploadNotification.on("action", (e) => {
|
||||
if (e.actionIndex === 0) {
|
||||
openScrubHistoryItem();
|
||||
} else if (e.actionIndex === 1) {
|
||||
if (scrubPdfURL) {
|
||||
newWindow(scrubPdfURL);
|
||||
}
|
||||
}
|
||||
uploadNotification.on("action", (event, actionIndex) => {
|
||||
const selectedActionIndex = event.actionIndex ?? actionIndex;
|
||||
notificationActionHandlers[selectedActionIndex]?.();
|
||||
});
|
||||
|
||||
uploadNotification.show();
|
||||
|
||||
@@ -92,15 +92,19 @@ async function ScrubEstimate({
|
||||
const esApiKey = store.get("settings.esApiKey") as string;
|
||||
|
||||
if (!esApiKey || esApiKey.trim() === "") {
|
||||
const message =
|
||||
"You must have a valid and active API key set in order to scrub estimates.";
|
||||
|
||||
showNotification({
|
||||
title: "No API Key Set",
|
||||
body: "You must have a valid and active API key set in order to scrub estimates.",
|
||||
body: message,
|
||||
});
|
||||
|
||||
mainWindow?.webContents.send(ipcTypes.toRenderer.scrub.scrubError, {
|
||||
message:
|
||||
"You must have a valid and active API key set in order to scrub estimates.",
|
||||
message,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!job) {
|
||||
@@ -183,24 +187,21 @@ async function ScrubEstimate({
|
||||
: undefined;
|
||||
const scrubErrorMessage = getErrorMessage(responseMessage);
|
||||
|
||||
if (status === 400) {
|
||||
mainWindow?.webContents.send(ipcTypes.toRenderer.scrub.scrubError, {
|
||||
message:
|
||||
scrubErrorMessage ||
|
||||
"Error encountered sending estimate to Estimate Scrubber.",
|
||||
});
|
||||
} else if (status === 401) {
|
||||
showNotification({
|
||||
title: "Error scrubbing estimate",
|
||||
body:
|
||||
scrubErrorMessage || "Authentication with Estimate Scrubber failed.",
|
||||
});
|
||||
const message =
|
||||
scrubErrorMessage ||
|
||||
(status === 401
|
||||
? "Authentication with Estimate Scrubber failed."
|
||||
: "Error encountered sending estimate to Estimate Scrubber.");
|
||||
|
||||
showNotification({
|
||||
title: "Error scrubbing estimate",
|
||||
body: status ? `${message} (${status})` : message,
|
||||
});
|
||||
|
||||
mainWindow?.webContents.send(ipcTypes.toRenderer.scrub.scrubError, {
|
||||
message,
|
||||
});
|
||||
|
||||
mainWindow?.webContents.send(ipcTypes.toRenderer.scrub.scrubError, {
|
||||
message:
|
||||
scrubErrorMessage || "Authentication with Estimate Scrubber failed.",
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@ import {
|
||||
} from "electron";
|
||||
import log from "electron-log/main";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { execFile } from "node:child_process";
|
||||
import path, { join } from "path";
|
||||
import "source-map-support/register";
|
||||
import imexAppIcon from "../../resources/icon.png?asset";
|
||||
import newWindow from "../util/newWindow";
|
||||
|
||||
import {
|
||||
default as ErrorTypeCheck,
|
||||
@@ -54,7 +56,9 @@ log.transports.console.format =
|
||||
"[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [PID:{processId}] {text}";
|
||||
log.transports.file.maxSize = 50 * 1024 * 1024; // 50 MB
|
||||
const isMac: boolean = process.platform === "darwin";
|
||||
const appId = "com.imex.esdp";
|
||||
const protocol: string = "esdp";
|
||||
const protocolDescription = "EMS Data Pump Protocol";
|
||||
let isAppQuitting = false; //Needed on Mac as an override to allow us to fully quit the app.
|
||||
let isKeepAliveLaunch = false; // Track if launched via keep-alive
|
||||
// Initialize the server
|
||||
@@ -68,7 +72,7 @@ if (!gotTheLock) {
|
||||
app.quit(); // Quit the app if another instance is already running
|
||||
}
|
||||
|
||||
function createWindow(): void {
|
||||
function createWindow(): BrowserWindow {
|
||||
// Create the browser window.
|
||||
const { width, height, x, y } = store.get("app.windowBounds") as {
|
||||
width: number;
|
||||
@@ -459,6 +463,8 @@ function createWindow(): void {
|
||||
if (import.meta.env.DEV) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
@@ -471,7 +477,8 @@ app.whenReady().then(async () => {
|
||||
log.debug("App is ready, initializing shortcuts and protocol handlers.");
|
||||
|
||||
if (platform.isWindows) {
|
||||
app.setAppUserModelId("esdp");
|
||||
app.setAppUserModelId(appId);
|
||||
await repairWindowsProtocolRegistration();
|
||||
}
|
||||
|
||||
app.on("browser-window-created", (_, window) => {
|
||||
@@ -483,14 +490,16 @@ app.whenReady().then(async () => {
|
||||
// remove so we can register each time as we run the app.
|
||||
app.removeAsDefaultProtocolClient(protocol);
|
||||
|
||||
const protocolLaunchArgs = getProtocolLaunchArgs();
|
||||
|
||||
// If we are running a non-packaged version of the app && on windows
|
||||
if (process.env.NODE_ENV === "development" && process.platform === "win32") {
|
||||
if (platform.isWindows && protocolLaunchArgs) {
|
||||
// Set the path of electron.exe and your app.
|
||||
// These two additional parameters are only available on windows.
|
||||
isDefaultProtocolClient = app.setAsDefaultProtocolClient(
|
||||
protocol,
|
||||
process.execPath,
|
||||
[path.resolve(process.argv[1])],
|
||||
protocolLaunchArgs,
|
||||
);
|
||||
} else {
|
||||
isDefaultProtocolClient = app.setAsDefaultProtocolClient(protocol);
|
||||
@@ -609,7 +618,13 @@ app.whenReady().then(async () => {
|
||||
});
|
||||
}
|
||||
//The update itself will run when the bodyshop record is queried to know what release channel to use.
|
||||
openMainWindow();
|
||||
const mainWindow = openMainWindow();
|
||||
const launchProtocolUrl = args.find((arg) =>
|
||||
arg.startsWith(`${protocol}://`),
|
||||
);
|
||||
if (launchProtocolUrl) {
|
||||
handleProtocolUrl(launchProtocolUrl, mainWindow);
|
||||
}
|
||||
|
||||
app.on("activate", function () {
|
||||
openMainWindow();
|
||||
@@ -618,29 +633,14 @@ app.whenReady().then(async () => {
|
||||
|
||||
app.on("open-url", (event: Electron.Event, url: string) => {
|
||||
event.preventDefault();
|
||||
if (url.startsWith(`${protocol}://keep-alive`)) {
|
||||
log.info("Keep-alive protocol received.");
|
||||
// Do nothing, whether app is running or not
|
||||
return;
|
||||
} else {
|
||||
openInExplorer(url);
|
||||
}
|
||||
handleProtocolUrl(url);
|
||||
});
|
||||
|
||||
// Add this event handler for second instance
|
||||
app.on("second-instance", (_event: Electron.Event, argv: string[]) => {
|
||||
const url = argv.find((arg) => arg.startsWith(`${protocol}://`));
|
||||
if (url) {
|
||||
if (url.startsWith(`${protocol}://keep-alive`)) {
|
||||
log.info(
|
||||
"Keep-alive protocol received, app is already running. Nothing to do.",
|
||||
);
|
||||
// Do nothing if already running
|
||||
return;
|
||||
} else {
|
||||
log.info("Received Media URL: ", url);
|
||||
openInExplorer(url);
|
||||
}
|
||||
handleProtocolUrl(url);
|
||||
}
|
||||
// No action taken if no URL is provided
|
||||
});
|
||||
@@ -682,13 +682,183 @@ function preQuitMethods(): void {
|
||||
isAppQuitting = true;
|
||||
}
|
||||
|
||||
function openMainWindow(): void {
|
||||
function runRegCommand(args: string[]): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(
|
||||
"reg.exe",
|
||||
args,
|
||||
{ windowsHide: true },
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(
|
||||
new Error(
|
||||
`reg.exe ${args.join(" ")} failed: ${stderr || stdout || error.message}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getProtocolLaunchArgs(): string[] | null {
|
||||
if (!is.dev && !process.defaultApp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const launchArgs = process.argv
|
||||
.slice(1)
|
||||
.filter((arg) => !arg.startsWith(`${protocol}://`));
|
||||
|
||||
if (launchArgs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return launchArgs.map((arg) =>
|
||||
arg.startsWith("-") ? arg : path.resolve(arg),
|
||||
);
|
||||
}
|
||||
|
||||
function quoteCommandArg(arg: string): string {
|
||||
return `"${arg.replaceAll('"', '\\"')}"`;
|
||||
}
|
||||
|
||||
function getProtocolCommand(): string {
|
||||
const protocolLaunchArgs = getProtocolLaunchArgs();
|
||||
const commandParts = [process.execPath, ...(protocolLaunchArgs ?? []), "%1"];
|
||||
|
||||
return commandParts.map(quoteCommandArg).join(" ");
|
||||
}
|
||||
|
||||
async function repairWindowsProtocolRegistration(): Promise<void> {
|
||||
const protocolRoot = `HKCU\\Software\\Classes\\${protocol}`;
|
||||
const protocolCommand = getProtocolCommand();
|
||||
|
||||
try {
|
||||
await runRegCommand([
|
||||
"add",
|
||||
protocolRoot,
|
||||
"/ve",
|
||||
"/d",
|
||||
`URL:${protocolDescription}`,
|
||||
"/f",
|
||||
]);
|
||||
await runRegCommand([
|
||||
"add",
|
||||
protocolRoot,
|
||||
"/v",
|
||||
"URL Protocol",
|
||||
"/t",
|
||||
"REG_SZ",
|
||||
"/d",
|
||||
"",
|
||||
"/f",
|
||||
]);
|
||||
await runRegCommand([
|
||||
"add",
|
||||
`${protocolRoot}\\DefaultIcon`,
|
||||
"/ve",
|
||||
"/d",
|
||||
`"${process.execPath}",0`,
|
||||
"/f",
|
||||
]);
|
||||
await runRegCommand([
|
||||
"add",
|
||||
`${protocolRoot}\\shell\\open\\command`,
|
||||
"/ve",
|
||||
"/d",
|
||||
protocolCommand,
|
||||
"/f",
|
||||
]);
|
||||
log.info("Windows protocol registry repaired.", {
|
||||
protocol,
|
||||
command: protocolCommand,
|
||||
});
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
"Failed to repair Windows protocol registry.",
|
||||
errorTypeCheck(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function openMainWindow(): BrowserWindow {
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
mainWindow.show();
|
||||
} else {
|
||||
createWindow();
|
||||
mainWindow.moveTop();
|
||||
mainWindow.focus();
|
||||
app.focus({ steal: true });
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
return createWindow();
|
||||
}
|
||||
|
||||
function openScrubHistoryItem(
|
||||
jobId: string,
|
||||
targetWindow?: BrowserWindow,
|
||||
): void {
|
||||
const mainWindow = targetWindow ?? openMainWindow();
|
||||
const sendOpenHistoryItem = (): void => {
|
||||
mainWindow.webContents.send(ipcTypes.toRenderer.scrub.openHistoryItem, {
|
||||
jobId,
|
||||
});
|
||||
};
|
||||
|
||||
if (mainWindow.webContents.isLoading()) {
|
||||
mainWindow.webContents.once("did-finish-load", sendOpenHistoryItem);
|
||||
} else {
|
||||
sendOpenHistoryItem();
|
||||
}
|
||||
}
|
||||
|
||||
function handleProtocolUrl(url: string, targetWindow?: BrowserWindow): void {
|
||||
if (url.startsWith(`${protocol}://keep-alive`)) {
|
||||
log.info("Keep-alive protocol received.");
|
||||
return;
|
||||
}
|
||||
|
||||
let parsedUrl: URL;
|
||||
try {
|
||||
parsedUrl = new URL(url);
|
||||
} catch (error) {
|
||||
log.warn("Invalid protocol URL received:", url, errorTypeCheck(error));
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedUrl.protocol !== `${protocol}:`) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedUrl.hostname === "scrub-history") {
|
||||
const jobId =
|
||||
parsedUrl.searchParams.get("jobId") ||
|
||||
decodeURIComponent(parsedUrl.pathname.replace(/^\/+/, ""));
|
||||
if (jobId) {
|
||||
openScrubHistoryItem(jobId, targetWindow);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedUrl.hostname === "open-pdf") {
|
||||
const pdfUrl = parsedUrl.searchParams.get("url");
|
||||
if (pdfUrl) {
|
||||
newWindow(pdfUrl).catch((error) => {
|
||||
log.error("Failed to open notification PDF:", errorTypeCheck(error));
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Received legacy protocol URL: ", url);
|
||||
openInExplorer(url);
|
||||
}
|
||||
|
||||
function openInExplorer(url: string): void {
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import { Notification, type NotificationConstructorOptions } from "electron";
|
||||
import log from "electron-log/main";
|
||||
|
||||
const retainedNotifications = new Set<Notification>();
|
||||
const DEFAULT_RETENTION_MS = 30 * 60 * 1000;
|
||||
|
||||
function createNotification(
|
||||
options: NotificationConstructorOptions,
|
||||
context = options.title,
|
||||
): Notification {
|
||||
const notification = new Notification(options);
|
||||
retainedNotifications.add(notification);
|
||||
|
||||
const retentionTimer = setTimeout(() => {
|
||||
retainedNotifications.delete(notification);
|
||||
}, DEFAULT_RETENTION_MS);
|
||||
retentionTimer.unref?.();
|
||||
|
||||
notification.on("failed", (_event, error) => {
|
||||
retainedNotifications.delete(notification);
|
||||
log.warn(`Notification failed${context ? ` (${context})` : ""}:`, error);
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,15 @@ import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import { FC, UIEvent, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
FC,
|
||||
UIEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import { selectWatcherStatus } from "@renderer/redux/app.slice";
|
||||
@@ -104,6 +112,7 @@ const Home: FC = () => {
|
||||
null,
|
||||
);
|
||||
const [loadingMore, setLoadingMore] = useState<boolean>(false);
|
||||
const historyItemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const routeSelectedJobId = searchParams.get("jobId")?.trim() || null;
|
||||
const selectedJobId = routeSelectedJobId ?? manualSelectedJobId;
|
||||
|
||||
@@ -245,6 +254,27 @@ const Home: FC = () => {
|
||||
await refresh(loadedPage + 1, true);
|
||||
}, [hasMoreHistory, loadedPage, loading, loadingMore, refresh]);
|
||||
|
||||
const setHistoryItemRef = useCallback(
|
||||
(jobId: string, element: HTMLDivElement | null) => {
|
||||
if (element) {
|
||||
historyItemRefs.current.set(jobId, element);
|
||||
} else {
|
||||
historyItemRefs.current.delete(jobId);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedJobId || loading) return;
|
||||
|
||||
const selectedElement = historyItemRefs.current.get(selectedJobId);
|
||||
if (!selectedElement) return;
|
||||
|
||||
selectedElement.scrollIntoView({ block: "nearest" });
|
||||
selectedElement.focus({ preventScroll: true });
|
||||
}, [history, loading, selectedJobId]);
|
||||
|
||||
const handleHistoryScroll = useCallback(
|
||||
(event: UIEvent<HTMLDivElement>) => {
|
||||
const target = event.currentTarget;
|
||||
@@ -576,6 +606,9 @@ const Home: FC = () => {
|
||||
return (
|
||||
<List.Item style={{ padding: "0 0 8px" }}>
|
||||
<div
|
||||
ref={(element) =>
|
||||
setHistoryItemRef(record.id, element)
|
||||
}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => selectJob(record.id)}
|
||||
|
||||
Reference in New Issue
Block a user