Added the ability to bookmark the extension, along with some visual adjustments
This commit is contained in:
@@ -44,10 +44,52 @@ function registerBackgroundMessages({
|
||||
requestOpptyProducts,
|
||||
runtimeApi
|
||||
}) {
|
||||
const bookmarkPending = new Map();
|
||||
async function getBookmarkFolders() {
|
||||
const folders = [];
|
||||
function visit(nodes, ancestors = []) {
|
||||
for (const node of nodes) {
|
||||
if (node.url || node.unmodifiable) continue;
|
||||
const path = node.parentId ? [...ancestors, node.title] : ancestors;
|
||||
if (node.parentId) folders.push({ id: node.id, label: path.join(" / ") });
|
||||
visit(node.children || [], path);
|
||||
}
|
||||
}
|
||||
visit(await runtimeApi.bookmarks.getTree());
|
||||
return { ok: true, folders };
|
||||
}
|
||||
function addShortcutBookmark(parentId) {
|
||||
if (bookmarkPending.has(parentId)) return bookmarkPending.get(parentId);
|
||||
const pending = (async () => {
|
||||
const { folders } = await getBookmarkFolders();
|
||||
if (!folders.some(folder => folder.id === parentId)) throw new Error("Select an available bookmarks folder.");
|
||||
const url = "https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome#opportunities-extension";
|
||||
const existing = await runtimeApi.bookmarks.search({ url });
|
||||
if (existing.some(bookmark => bookmark.parentId === parentId)) return { ok: true, existing: true };
|
||||
await runtimeApi.bookmarks.create({ title: "Opportunities Extension", url, parentId });
|
||||
return { ok: true, existing: false };
|
||||
})().finally(() => { bookmarkPending.delete(parentId); });
|
||||
bookmarkPending.set(parentId, pending);
|
||||
return pending;
|
||||
}
|
||||
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (!message || !message.type) {
|
||||
return false;
|
||||
}
|
||||
if (message.type === messageTypes.addShortcutBookmark) {
|
||||
addShortcutBookmark(message.parentId).then(sendResponse).catch(error => sendResponse({ ok: false, error: error.message }));
|
||||
return true;
|
||||
}
|
||||
if (message.type === messageTypes.getBookmarkFolders) {
|
||||
getBookmarkFolders().then(sendResponse).catch(error => sendResponse({ ok: false, error: error.message }));
|
||||
return true;
|
||||
}
|
||||
if (message.type === messageTypes.getShortcutBookmarkStatus) {
|
||||
runtimeApi.bookmarks.search({ url: "https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome#opportunities-extension" })
|
||||
.then(items => sendResponse({ ok: true, bookmarked: items.length > 0 }))
|
||||
.catch(error => sendResponse({ ok: false, error: error.message }));
|
||||
return true;
|
||||
}
|
||||
if ([messageTypes.loadOrganizationChart, messageTypes.saveOrganizationChart].includes(message.type)) {
|
||||
const operation = message.type === messageTypes.loadOrganizationChart ? loadOrganizationChart : saveOrganizationChart;
|
||||
operation(message).then(sendResponse).catch(error => sendResponse({ ok: false, error: error.message }));
|
||||
|
||||
@@ -79,6 +79,15 @@ function createContentApplication(overrides = {}) {
|
||||
environment
|
||||
});
|
||||
const contentModal = createContentModal({
|
||||
getShortcutBookmarkStatus: () => platformContentMessages.sendRuntimeMessage({ type: messageTypes.getShortcutBookmarkStatus }),
|
||||
openBookmarkPicker: () => opportunitiesLinks.chooseBookmarkFolder(
|
||||
() => platformContentMessages.sendRuntimeMessage({ type: messageTypes.getBookmarkFolders }),
|
||||
async parentId => {
|
||||
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.addShortcutBookmark, parentId });
|
||||
if (result?.ok) contentModal.hideBookmarkHint();
|
||||
return result;
|
||||
}
|
||||
),
|
||||
createManageTimeButton: () => manageTime.createButton(),
|
||||
prepareSrSession: () => srSessionPreparation.ensure(),
|
||||
closeActionForm: (...args) => rampProjectionView.close(...args) || manageTime.close(...args) || internalSrForm.close(...args) || opportunityCalendar.closeDetails(...args) || crmSrForm.close(...args) || workloadActionForm.close(...args),
|
||||
@@ -212,7 +221,9 @@ function createContentApplication(overrides = {}) {
|
||||
return result.items;
|
||||
}
|
||||
});
|
||||
const dialogDrag = createDialogDrag({ environment });
|
||||
const contentStyles = createContentStyles({
|
||||
enableDialogDrag: () => dialogDrag.install(),
|
||||
STYLE_ID: contracts.STYLE_ID,
|
||||
redwoodStyles,
|
||||
environment
|
||||
@@ -250,6 +261,7 @@ function createContentApplication(overrides = {}) {
|
||||
environment
|
||||
});
|
||||
const opportunitiesController = createOpportunitiesController({
|
||||
loadServiceRequests: (token, version) => opportunityCrmServiceRequests.load(token, version),
|
||||
ensureCurrentUser: () => currentUser.ensure(),
|
||||
resetWorkloadActions: (...args) => opportunityWorkloadActions.reset(...args),
|
||||
resetWorkloads: () => { rampProjectionView.close(false); productWorkloads.reset(); },
|
||||
@@ -273,7 +285,6 @@ function createContentApplication(overrides = {}) {
|
||||
setOpportunitiesTableState: (...args) => opportunitiesTable.setOpportunitiesTableState(...args)
|
||||
});
|
||||
const fusionOpportunities = createFusionOpportunities({
|
||||
loadServiceRequests: (token, version) => opportunityCrmServiceRequests.load(token, version),
|
||||
resetDebugRequests: () => debugState.reset(),
|
||||
OPPORTUNITIES_PAGE_LIMIT: contracts.OPPORTUNITIES_PAGE_LIMIT,
|
||||
OPPORTUNITIES_QUERY_URL: contracts.OPPORTUNITIES_QUERY_URL,
|
||||
@@ -507,6 +518,7 @@ function createContentApplication(overrides = {}) {
|
||||
});
|
||||
const opportunitiesLinks = createOpportunitiesLinks({
|
||||
MODAL_ID: contracts.MODAL_ID,
|
||||
searchableSelect,
|
||||
environment
|
||||
});
|
||||
const preferencesStorage = createPreferencesStorage({
|
||||
|
||||
71
src/content/dialog-drag.js
Normal file
71
src/content/dialog-drag.js
Normal file
@@ -0,0 +1,71 @@
|
||||
function createDialogDrag({ environment }) {
|
||||
const { document, window } = environment;
|
||||
const panels = ".opportunities-extension-action-form-dialog, .opportunities-extension-preferences-dialog, .opportunities-extension-account-plans-dialog, .oe-org-confirm-panel";
|
||||
const positions = new WeakMap();
|
||||
let installed = false, drag = null;
|
||||
function move(panel, dx, dy) {
|
||||
const rect = panel.getBoundingClientRect();
|
||||
const viewport = window.visualViewport;
|
||||
const left = viewport?.offsetLeft || 0, top = viewport?.offsetTop || 0;
|
||||
const width = viewport?.width || document.documentElement.clientWidth;
|
||||
const height = viewport?.height || document.documentElement.clientHeight;
|
||||
const x = Math.max(left - rect.width * 0.7, Math.min(rect.left + dx, left + width - rect.width * 0.3));
|
||||
const y = Math.max(top - rect.height * 0.7, Math.min(rect.top + dy, top + height - rect.height * 0.3));
|
||||
const previous = positions.get(panel) || { x: 0, y: 0 };
|
||||
const next = { x: previous.x + x - rect.left, y: previous.y + y - rect.top };
|
||||
positions.set(panel, next);
|
||||
panel.style.position = "relative";
|
||||
panel.style.left = `${next.x}px`;
|
||||
panel.style.top = `${next.y}px`;
|
||||
panel.setAttribute("data-dialog-moved", "true");
|
||||
}
|
||||
function stop() {
|
||||
if (!drag) return;
|
||||
const previous = drag;
|
||||
drag = null;
|
||||
previous.panel.removeAttribute("data-dialog-dragging");
|
||||
if (previous.title.hasPointerCapture?.(previous.id)) previous.title.releasePointerCapture(previous.id);
|
||||
}
|
||||
function install() {
|
||||
if (installed || window.__opportunitiesDialogDragInstalled || !document.addEventListener || !window.addEventListener) return;
|
||||
installed = true;
|
||||
window.__opportunitiesDialogDragInstalled = true;
|
||||
document.addEventListener("pointerdown", event => {
|
||||
if (event.button !== 0 || event.isPrimary === false) return;
|
||||
const title = event.target.closest?.("header, h2, h3");
|
||||
const panel = title?.closest(panels);
|
||||
if (!panel || !panel.closest(".opportunities-extension-modal") || panel.closest("[inert], [hidden]") ||
|
||||
event.target.closest("button, a, input, select, textarea, [role='button'], [contenteditable='true']")) return;
|
||||
// Accept the whole title bar, but not headers inside the dialog's content.
|
||||
const heading = panel.querySelector("h2, h3");
|
||||
if (!heading || (heading !== title && !title.contains(heading))) return;
|
||||
const rect = panel.getBoundingClientRect();
|
||||
if (rect.width >= document.documentElement.clientWidth - 2 && rect.height >= document.documentElement.clientHeight - 2) return;
|
||||
stop();
|
||||
event.preventDefault();
|
||||
drag = { title, panel, id: event.pointerId, x: event.clientX, y: event.clientY };
|
||||
panel.setAttribute("data-dialog-dragging", "true");
|
||||
title.setPointerCapture?.(event.pointerId);
|
||||
});
|
||||
document.addEventListener("pointermove", event => {
|
||||
if (!drag || event.pointerId !== drag.id) return;
|
||||
if (!drag.panel.isConnected || drag.panel.closest("[hidden], [inert]")) { stop(); return; }
|
||||
event.preventDefault();
|
||||
move(drag.panel, event.clientX - drag.x, event.clientY - drag.y);
|
||||
drag.x = event.clientX; drag.y = event.clientY;
|
||||
});
|
||||
for (const name of ["pointerup", "pointercancel", "lostpointercapture"]) {
|
||||
document.addEventListener(name, event => { if (drag?.id === event.pointerId) stop(); });
|
||||
}
|
||||
window.addEventListener("blur", stop);
|
||||
function resize() {
|
||||
stop();
|
||||
for (const panel of document.querySelectorAll('[data-dialog-moved="true"]')) {
|
||||
if (panel.getClientRects().length) move(panel, 0, 0);
|
||||
}
|
||||
}
|
||||
window.addEventListener("resize", resize);
|
||||
window.visualViewport?.addEventListener("resize", resize);
|
||||
}
|
||||
return { install };
|
||||
}
|
||||
@@ -9,6 +9,20 @@ function createContentFusionPage({
|
||||
environment
|
||||
}) {
|
||||
const { Node, document, window } = environment;
|
||||
let shortcutOpened = false;
|
||||
function openShortcutIfRequested() {
|
||||
if (shortcutOpened || !isAllowedPage()) return;
|
||||
if (window.top && window.top !== window) return;
|
||||
let pending = false;
|
||||
try {
|
||||
const timestamp = Number(window.sessionStorage.getItem("opportunitiesExtension.shortcutIntent"));
|
||||
pending = timestamp > 0 && Date.now() >= timestamp && Date.now() - timestamp < 10 * 60 * 1000;
|
||||
window.sessionStorage.removeItem("opportunitiesExtension.shortcutIntent");
|
||||
} catch { /* Storage may be disabled. */ }
|
||||
if (!pending && window.location.hash !== "#opportunities-extension") return;
|
||||
shortcutOpened = true;
|
||||
openOpportunitiesModal();
|
||||
}
|
||||
function createOpportunitiesTile() {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "flat-grid-cell";
|
||||
@@ -88,6 +102,9 @@ function createContentFusionPage({
|
||||
}
|
||||
|
||||
function insertTile() {
|
||||
// Opening a bookmarked shortcut must not depend on Sales being expanded
|
||||
// or its tile insertion point being present in Fusion's lazy navigation.
|
||||
openShortcutIfRequested();
|
||||
if (document.getElementById(TILE_ID)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ function startContentApplication({ environment, page, styles, products }) {
|
||||
if (!page.isAllowedPage()) return;
|
||||
styles.ensureExtensionStyles();
|
||||
products.ensureProductsBackgroundMessageHandler();
|
||||
environment.window?.addEventListener?.("hashchange", () => page.insertTile());
|
||||
if (page.insertTile()) return;
|
||||
const observer = new environment.MutationObserver(() => page.insertTile());
|
||||
observer.observe(environment.document.documentElement, { childList: true, subtree: true });
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
function createContentModal({
|
||||
openBookmarkPicker,
|
||||
getShortcutBookmarkStatus,
|
||||
ACCOUNT_PLANS_MODAL_ID,
|
||||
AUTH_STATUS,
|
||||
CUSTOMER_FILTER_BUTTON_ID,
|
||||
@@ -72,6 +74,27 @@ function createContentModal({
|
||||
}) {
|
||||
const { document, window, setTimeout, clearTimeout, matchMedia } = environment;
|
||||
let authCompletion = null;
|
||||
let bookmarkHintTimer = null;
|
||||
let bookmarkHintVersion = 0;
|
||||
function hideBookmarkHint() {
|
||||
bookmarkHintVersion++;
|
||||
if (bookmarkHintTimer !== null) clearTimeout(bookmarkHintTimer);
|
||||
bookmarkHintTimer = null;
|
||||
const hint = document.getElementById("opportunities-extension-bookmark-hint");
|
||||
if (hint) hint.hidden = true;
|
||||
}
|
||||
async function refreshBookmarkHint() {
|
||||
hideBookmarkHint();
|
||||
const version = bookmarkHintVersion;
|
||||
try {
|
||||
const result = await getShortcutBookmarkStatus?.();
|
||||
const modal = document.getElementById(MODAL_ID);
|
||||
const hint = document.getElementById("opportunities-extension-bookmark-hint");
|
||||
if (version !== bookmarkHintVersion || !modal || modal.hidden || !hint || !result?.ok || result.bookmarked) return;
|
||||
hint.hidden = false;
|
||||
bookmarkHintTimer = setTimeout(hideBookmarkHint, 20000);
|
||||
} catch { /* A failed bookmarks check must not interrupt opportunities. */ }
|
||||
}
|
||||
function resetAuthCompletion() {
|
||||
if (!authCompletion) return;
|
||||
clearTimeout(authCompletion.waitTimer);
|
||||
@@ -89,6 +112,7 @@ function createContentModal({
|
||||
|
||||
if (existingModal) {
|
||||
existingModal.hidden = false;
|
||||
refreshBookmarkHint();
|
||||
prepareSrSession?.();
|
||||
existingModal.querySelector("select").focus();
|
||||
document.documentElement.classList.add("opportunities-extension-scroll-lock");
|
||||
@@ -148,7 +172,13 @@ function createContentModal({
|
||||
productsCacheTimestamp.className = "opportunities-extension-products-cache-timestamp";
|
||||
productsCacheTimestamp.setAttribute("role", "status");
|
||||
productsCacheTimestamp.hidden = true;
|
||||
subtitleRow.append(authBadge, productsProgressBadge, productsCacheTimestamp);
|
||||
const bookmarkHint = document.createElement("span");
|
||||
bookmarkHint.id = "opportunities-extension-bookmark-hint";
|
||||
bookmarkHint.className = "opportunities-extension-products-cache-timestamp";
|
||||
bookmarkHint.setAttribute("role", "status");
|
||||
bookmarkHint.textContent = "Save to favorites: Ctrl+Shift+F";
|
||||
bookmarkHint.hidden = true;
|
||||
subtitleRow.append(productsCacheTimestamp, bookmarkHint, authBadge, productsProgressBadge);
|
||||
|
||||
const titleBlock = document.createElement("div");
|
||||
titleBlock.className = "opportunities-extension-title-row";
|
||||
@@ -420,6 +450,7 @@ function createContentModal({
|
||||
shell.append(header, body);
|
||||
overlay.append(shell);
|
||||
document.body.append(overlay);
|
||||
refreshBookmarkHint();
|
||||
document.documentElement.classList.add("opportunities-extension-scroll-lock");
|
||||
ensureEscapeKeyHandler();
|
||||
renderStageFilterButtons();
|
||||
@@ -472,6 +503,7 @@ function createContentModal({
|
||||
|
||||
function closeOpportunitiesModal() {
|
||||
if (closeOrganizationChart()) return;
|
||||
hideBookmarkHint();
|
||||
resetAuthCompletion();
|
||||
closeCrmSrPopover();
|
||||
const modal = document.getElementById(MODAL_ID);
|
||||
@@ -502,10 +534,14 @@ function createContentModal({
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.ctrlKey && event.key.toLowerCase() === "d") {
|
||||
const shortcutKey = event.key.toLowerCase();
|
||||
if (event.ctrlKey && event.shiftKey && !event.altKey && !event.metaKey &&
|
||||
(shortcutKey === "d" || shortcutKey === "f")) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
toggleDebugPanel();
|
||||
if (event.repeat) return;
|
||||
if (shortcutKey === "d") toggleDebugPanel();
|
||||
else openBookmarkPicker();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -580,5 +616,5 @@ function createContentModal({
|
||||
});
|
||||
}
|
||||
|
||||
return { openOpportunitiesModal, updateAuthBadge, closeOpportunitiesModal, ensureEscapeKeyHandler };
|
||||
return { openOpportunitiesModal, updateAuthBadge, closeOpportunitiesModal, ensureEscapeKeyHandler, hideBookmarkHint, refreshBookmarkHint };
|
||||
}
|
||||
|
||||
11
src/content/shortcut-intent.js
Normal file
11
src/content/shortcut-intent.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// Capture the bookmark before Fusion rewrites its navigation URL.
|
||||
(() => {
|
||||
if (window.top !== window) return;
|
||||
const capture = () => {
|
||||
const url = new URL(window.location.href);
|
||||
if (url.hash !== "#opportunities-extension" && url.searchParams.get("opportunitiesExtension") !== "open") return;
|
||||
try { window.sessionStorage.setItem("opportunitiesExtension.shortcutIntent", String(Date.now())); } catch { /* The URL remains a fallback. */ }
|
||||
};
|
||||
capture();
|
||||
window.addEventListener("hashchange", capture);
|
||||
})();
|
||||
@@ -1,10 +1,12 @@
|
||||
function createContentStyles({
|
||||
STYLE_ID,
|
||||
enableDialogDrag,
|
||||
redwoodStyles,
|
||||
environment
|
||||
}) {
|
||||
const { document } = environment;
|
||||
function ensureExtensionStyles() {
|
||||
enableDialogDrag?.();
|
||||
if (document.getElementById(STYLE_ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
/* Modal titles follow the customer Service Requests heading. Keep section and
|
||||
calendar headings independent from dialog typography. */
|
||||
.opportunities-extension-modal :is(.opportunities-extension-action-form-dialog,
|
||||
.opportunities-extension-preferences-dialog, .opportunities-extension-account-plans-dialog,
|
||||
.oe-org-confirm-panel) :is(header, h2, h3) {
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
.opportunities-extension-modal [data-dialog-dragging="true"] :is(header, h2, h3) { cursor: grabbing; }
|
||||
.opportunities-extension-modal #opportunities-extension-bookmark-hint {
|
||||
background: #9a4600;
|
||||
color: #ffffff;
|
||||
border: 1px solid #b95b00;
|
||||
}
|
||||
.opportunities-extension-modal[data-theme="dark"] #opportunities-extension-bookmark-hint {
|
||||
background: #ffca70;
|
||||
color: #3b2400;
|
||||
border-color: #ffd99a;
|
||||
}
|
||||
.opportunities-extension-modal :is(
|
||||
.opportunities-extension-header,
|
||||
.opportunities-extension-account-plans-header,
|
||||
|
||||
@@ -64,7 +64,10 @@ function createActivityFilters({ allowsCrmActivities, getSrSummary, getActionSum
|
||||
function toggle(kind, value) {
|
||||
const selected = selections[kind];
|
||||
if (!selected || !allowsCrmActivities()) return;
|
||||
if (selected.has(value)) selected.delete(value);
|
||||
if (selected.has("matched") && selected.has("missing")) {
|
||||
selected.clear();
|
||||
selected.add(value);
|
||||
} else if (selected.has(value)) selected.delete(value);
|
||||
else {
|
||||
if (value === "under4") selected.clear();
|
||||
else selected.delete("under4");
|
||||
|
||||
@@ -210,6 +210,7 @@ function createOpportunityCalendar({ environment, loadLibrary, getState, getItem
|
||||
viewbar.append(tablist);
|
||||
if (productTypeSelection) {
|
||||
const filters = node("div", "opportunities-product-type-filters");
|
||||
const filterButtons = new Map();
|
||||
filters.setAttribute("role", "group"); filters.setAttribute("aria-label", "Opportunity type");
|
||||
for (const type of ["Booking", "Workload"]) {
|
||||
const button = node("button", "opportunities-product-type-filter", type);
|
||||
@@ -217,10 +218,16 @@ function createOpportunityCalendar({ environment, loadLibrary, getState, getItem
|
||||
button.setAttribute("data-type", type);
|
||||
button.setAttribute("aria-pressed", String(productTypeSelection.has(type)));
|
||||
button.addEventListener("click", () => {
|
||||
if (productTypeSelection.has(type)) productTypeSelection.delete(type); else productTypeSelection.add(type);
|
||||
button.setAttribute("aria-pressed", String(productTypeSelection.has(type)));
|
||||
if (["Booking", "Workload"].every(value => productTypeSelection.has(value))) {
|
||||
productTypeSelection.clear();
|
||||
productTypeSelection.add(type);
|
||||
} else if (productTypeSelection.has(type)) productTypeSelection.delete(type); else productTypeSelection.add(type);
|
||||
for (const [value, filterButton] of filterButtons) {
|
||||
filterButton.setAttribute("aria-pressed", String(productTypeSelection.has(value)));
|
||||
}
|
||||
onProductTypeChange();
|
||||
});
|
||||
filterButtons.set(type, button);
|
||||
filters.append(button);
|
||||
}
|
||||
if (createPrintButton) filters.append(createPrintButton());
|
||||
|
||||
@@ -13,6 +13,7 @@ function createOpportunitiesController({
|
||||
productsContentState,
|
||||
renderProductsProgress,
|
||||
requestOpportunitiesPage,
|
||||
loadServiceRequests = async () => {},
|
||||
resetCrmServiceRequests,
|
||||
resetWorkloadActions,
|
||||
resetWorkloads,
|
||||
@@ -95,6 +96,9 @@ function createOpportunitiesController({
|
||||
status: "ready",
|
||||
message: ""
|
||||
});
|
||||
// Start SR only after all opportunity pages have been displayed.
|
||||
// Its incremental updates run independently of the opportunity request.
|
||||
try { Promise.resolve(loadServiceRequests(token, requestVersion)).catch(() => {}); } catch { /* Keep the displayed opportunities available. */ }
|
||||
requestOpptyProductsInBackground(allItems, token);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -28,12 +28,28 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
|
||||
let items = cached?.items;
|
||||
if (!items) {
|
||||
reportProgress("running", 0);
|
||||
items = await requestCrmServiceRequest(token, version, isActive, (count, total) => {
|
||||
if (isActive()) reportProgress("running", count, total);
|
||||
items = await requestCrmServiceRequest(token, version, isActive, (count, total, partialItems) => {
|
||||
if (!isActive()) return;
|
||||
reportProgress("running", count, total);
|
||||
if (Array.isArray(partialItems)) {
|
||||
updateItems(partialItems);
|
||||
onStateChange();
|
||||
}
|
||||
});
|
||||
if (items && isActive() && cacheOptions) await activityCache.write("sr", "all", cacheOptions.cachedAt, items);
|
||||
}
|
||||
if (!items || !isActive()) return false;
|
||||
updateItems(items);
|
||||
status = "ready";
|
||||
if (!cached) reportProgress("complete", items.length, items.length);
|
||||
onStateChange();
|
||||
return true;
|
||||
} catch {
|
||||
if (isActive()) { status = "error"; reportProgress("error"); onStateChange(); }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function updateItems(items) {
|
||||
const grouped = new Map();
|
||||
for (const item of items) {
|
||||
const number = item?.OpportunityName_Id_c?.OptyNumber;
|
||||
@@ -44,15 +60,6 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
|
||||
}
|
||||
byOpportunity = grouped;
|
||||
loadedItems = items;
|
||||
status = "ready";
|
||||
if (!cached) reportProgress("complete", items.length, items.length);
|
||||
// Refresh SR badges, filters and totals independently of product/action jobs.
|
||||
onStateChange();
|
||||
return true;
|
||||
} catch {
|
||||
if (isActive()) { status = "error"; reportProgress("error"); onStateChange(); }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function getSummary(optyNumber) {
|
||||
if (!allowsCrmActivities() || !optyNumber) return null;
|
||||
@@ -61,22 +68,23 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
|
||||
(typeof value === "number" || (typeof value === "string" && value.trim() !== "")) && Number.isFinite(Number(value))
|
||||
).map(Number);
|
||||
return {
|
||||
state: status === "ready" ? items.length ? "matched" : "missing" : status,
|
||||
state: status === "loading" && items.length ? "matched" : status === "ready" ? items.length ? "matched" : "missing" : status,
|
||||
reportedHours: hours.length === items.length ? hours.reduce((total, value) => total + value, 0) : null,
|
||||
hasUnderFourHours: status === "ready" && hours.some(value => value < 4)
|
||||
hasUnderFourHours: (status === "ready" || status === "loading") && hours.some(value => value < 4)
|
||||
};
|
||||
}
|
||||
function createBadge(optyNumber) {
|
||||
if (!allowsCrmActivities() || !optyNumber || status === "idle" || status === "loading") return null;
|
||||
const badgeStatus = status === "loading" && byOpportunity.get(String(optyNumber))?.length ? "ready" : status;
|
||||
if (!allowsCrmActivities() || !optyNumber || badgeStatus === "idle" || badgeStatus === "loading") return null;
|
||||
const items = byOpportunity.get(String(optyNumber)) || [];
|
||||
const badge = environment.document.createElement(items.length && status === "ready" ? "button" : "span");
|
||||
const badge = environment.document.createElement(items.length && badgeStatus === "ready" ? "button" : "span");
|
||||
badge.className = "opportunities-extension-crm-sr-badge";
|
||||
badge.setAttribute("data-state", status === "error" ? "error" : items.length ? "matched" : "missing");
|
||||
badge.setAttribute("data-state", badgeStatus === "error" ? "error" : items.length ? "matched" : "missing");
|
||||
const summary = getSummary(optyNumber);
|
||||
const reportedHours = summary.reportedHours !== null
|
||||
? new Intl.NumberFormat("en-US", { useGrouping: false, maximumFractionDigits: 10 }).format(summary.reportedHours)
|
||||
: "N/A";
|
||||
setBadgeContent(badge, "sr", status === "error" ? "?" : items.length ? `${reportedHours}${reportedHours === "N/A" ? "" : "h"}` : "0h");
|
||||
setBadgeContent(badge, "sr", badgeStatus === "error" ? "?" : items.length ? `${reportedHours}${reportedHours === "N/A" ? "" : "h"}` : "0h");
|
||||
const display = value => value === null || value === undefined || value === "" ? "N/A" : String(value);
|
||||
const descriptions = items.map(item => [
|
||||
`Title: ${display(item.Title)}`,
|
||||
@@ -87,13 +95,13 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
|
||||
`Email: ${display(item.Owner?.PrimaryEmail?.EmailAddress)}`,
|
||||
`Do Not Call: ${display(item.DoNotCallFlag)}`, `Do Not Email: ${display(item.DoNotEmailFlag)}`
|
||||
].join("\n"));
|
||||
const description = status === "error" ? "Unable to check CRM service requests. See Debug Requests."
|
||||
const description = badgeStatus === "error" ? "Unable to check CRM service requests. See Debug Requests."
|
||||
: !items.length ? "No matching CRM service request with status SVC_PENDING_DELIVERY in your SR team."
|
||||
: items.map((item, index) => `SR Number: ${display(item.SrNumber)}\n${descriptions[index]}`).join("\n\n");
|
||||
badge.setAttribute("aria-label", description);
|
||||
badge.title = "Whether a Service Request has been created for the opportunities.";
|
||||
if (items.length && status === "ready") bindPopover(badge, items, descriptions);
|
||||
if (!items.length && status === "ready") badge.append(createSrButton(String(optyNumber)));
|
||||
if (items.length && badgeStatus === "ready") bindPopover(badge, items, descriptions);
|
||||
if (!items.length && badgeStatus === "ready") badge.append(createSrButton(String(optyNumber)));
|
||||
badge.tabIndex = 0;
|
||||
return badge;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,10 @@ function createOpportunitiesFilters({
|
||||
}
|
||||
|
||||
function toggleStageFilter(stage) {
|
||||
if (opportunitiesFilterState.selectedStages.has(stage)) {
|
||||
if (STAGE_OPTIONS.every(value => opportunitiesFilterState.selectedStages.has(value))) {
|
||||
opportunitiesFilterState.selectedStages.clear();
|
||||
opportunitiesFilterState.selectedStages.add(stage);
|
||||
} else if (opportunitiesFilterState.selectedStages.has(stage)) {
|
||||
opportunitiesFilterState.selectedStages.delete(stage);
|
||||
} else {
|
||||
opportunitiesFilterState.selectedStages.add(stage);
|
||||
@@ -99,7 +102,10 @@ function createOpportunitiesFilters({
|
||||
}
|
||||
|
||||
function toggleStatusFilter(status) {
|
||||
if (opportunitiesFilterState.selectedStatuses.has(status)) {
|
||||
if (STATUS_OPTIONS.every(value => opportunitiesFilterState.selectedStatuses.has(value))) {
|
||||
opportunitiesFilterState.selectedStatuses.clear();
|
||||
opportunitiesFilterState.selectedStatuses.add(status);
|
||||
} else if (opportunitiesFilterState.selectedStatuses.has(status)) {
|
||||
opportunitiesFilterState.selectedStatuses.delete(status);
|
||||
} else {
|
||||
opportunitiesFilterState.selectedStatuses.add(status);
|
||||
@@ -246,7 +252,10 @@ function createOpportunitiesFilters({
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = opportunitiesFilterState.selectedCustomerKeys.has(customer.key);
|
||||
checkbox.addEventListener("change", () => {
|
||||
if (checkbox.checked) {
|
||||
if (getCustomerOptions().every(option => opportunitiesFilterState.selectedCustomerKeys.has(option.key))) {
|
||||
opportunitiesFilterState.selectedCustomerKeys.clear();
|
||||
opportunitiesFilterState.selectedCustomerKeys.add(customer.key);
|
||||
} else if (checkbox.checked) {
|
||||
opportunitiesFilterState.selectedCustomerKeys.add(customer.key);
|
||||
} else {
|
||||
opportunitiesFilterState.selectedCustomerKeys.delete(customer.key);
|
||||
@@ -316,7 +325,10 @@ function createOpportunitiesFilters({
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = opportunitiesFilterState.selectedOwnerKeys.has(owner.key);
|
||||
checkbox.addEventListener("change", () => {
|
||||
if (checkbox.checked) {
|
||||
if (getOwnerOptions().every(option => opportunitiesFilterState.selectedOwnerKeys.has(option.key))) {
|
||||
opportunitiesFilterState.selectedOwnerKeys.clear();
|
||||
opportunitiesFilterState.selectedOwnerKeys.add(owner.key);
|
||||
} else if (checkbox.checked) {
|
||||
opportunitiesFilterState.selectedOwnerKeys.add(owner.key);
|
||||
} else {
|
||||
opportunitiesFilterState.selectedOwnerKeys.delete(owner.key);
|
||||
|
||||
@@ -1,8 +1,140 @@
|
||||
function createOpportunitiesLinks({
|
||||
MODAL_ID,
|
||||
searchableSelect,
|
||||
environment
|
||||
}) {
|
||||
const { document, navigator, window } = environment;
|
||||
function chooseBookmarkFolder(loadFolders, saveBookmark) {
|
||||
const modal = document.getElementById(MODAL_ID);
|
||||
if (!modal || modal.querySelector(".oe-bookmark-picker")) return;
|
||||
const previous = document.activeElement;
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "opportunities-extension-action-form-modal oe-bookmark-picker";
|
||||
const panel = document.createElement("section");
|
||||
panel.className = "opportunities-extension-action-form-dialog";
|
||||
panel.setAttribute("role", "dialog");
|
||||
panel.setAttribute("aria-modal", "true");
|
||||
panel.setAttribute("aria-labelledby", "oe-bookmark-title");
|
||||
const header = document.createElement("header");
|
||||
header.className = "opportunities-extension-account-plans-header";
|
||||
const title = document.createElement("h2");
|
||||
title.id = "oe-bookmark-title";
|
||||
title.textContent = "Add to favorites";
|
||||
const dismiss = document.createElement("button");
|
||||
dismiss.type = "button";
|
||||
dismiss.className = "opportunities-extension-icon-button";
|
||||
dismiss.setAttribute("aria-label", "Close favorites form");
|
||||
const closeIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
closeIcon.setAttribute("viewBox", "0 0 24 24");
|
||||
closeIcon.setAttribute("aria-hidden", "true");
|
||||
const closePath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
closePath.setAttribute("d", "M6 6l12 12M18 6 6 18");
|
||||
closePath.setAttribute("stroke", "currentColor");
|
||||
closePath.setAttribute("stroke-width", "1.75");
|
||||
closeIcon.append(closePath);
|
||||
dismiss.append(closeIcon);
|
||||
header.append(title, dismiss);
|
||||
const body = document.createElement("div");
|
||||
body.className = "opportunities-extension-action-form";
|
||||
const label = document.createElement("label");
|
||||
label.className = "opportunities-extension-action-form-field opportunities-extension-action-form-wide";
|
||||
label.textContent = "Folder (required)";
|
||||
const select = document.createElement("select");
|
||||
select.id = "oe-bookmark-folder";
|
||||
select.required = true;
|
||||
select.setAttribute("aria-describedby", "oe-bookmark-status");
|
||||
select.disabled = true;
|
||||
label.append(select);
|
||||
const status = document.createElement("p");
|
||||
status.id = "oe-bookmark-status";
|
||||
status.className = "opportunities-extension-action-form-status opportunities-extension-action-form-wide";
|
||||
status.setAttribute("role", "status");
|
||||
status.textContent = "Loading bookmarks folders…";
|
||||
const actions = document.createElement("footer");
|
||||
actions.className = "opportunities-extension-action-form-footer";
|
||||
const cancel = document.createElement("button");
|
||||
cancel.type = "button";
|
||||
cancel.textContent = "Cancel";
|
||||
const save = document.createElement("button");
|
||||
save.type = "button";
|
||||
save.className = "opportunities-extension-action-form-save";
|
||||
save.textContent = "Save";
|
||||
save.disabled = true;
|
||||
const autocomplete = searchableSelect(select, label, () => { save.disabled = !select.value; }, { floating: true });
|
||||
autocomplete.input.setAttribute("aria-describedby", "oe-bookmark-status");
|
||||
let saving = false;
|
||||
const siblings = Array.from(modal.children).map(node => [node, node.inert]);
|
||||
siblings.forEach(([node]) => { node.inert = true; });
|
||||
function close() {
|
||||
if (saving) return;
|
||||
autocomplete.destroy();
|
||||
overlay.remove();
|
||||
siblings.forEach(([node, inert]) => { node.inert = inert; });
|
||||
previous?.focus();
|
||||
}
|
||||
cancel.addEventListener("click", close);
|
||||
dismiss.addEventListener("click", close);
|
||||
save.addEventListener("click", async () => {
|
||||
if (!select.value || saving) return;
|
||||
saving = true;
|
||||
save.disabled = cancel.disabled = select.disabled = dismiss.disabled = true;
|
||||
autocomplete.sync();
|
||||
status.setAttribute("role", "status");
|
||||
status.textContent = "Saving favorite…";
|
||||
try {
|
||||
const result = await saveBookmark(select.value);
|
||||
if (!result?.ok) throw new Error(result?.error || "Unable to add favorite.");
|
||||
saving = false;
|
||||
close();
|
||||
showCopyConfirmation(result.existing ? "This shortcut is already in the selected folder." : "Opportunities Extension added to favorites.");
|
||||
} catch (error) {
|
||||
status.setAttribute("role", "alert");
|
||||
status.textContent = error.message;
|
||||
saving = false;
|
||||
save.disabled = cancel.disabled = select.disabled = dismiss.disabled = false;
|
||||
autocomplete.sync();
|
||||
}
|
||||
});
|
||||
overlay.addEventListener("keydown", event => {
|
||||
if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); }
|
||||
if (event.key === "Tab") {
|
||||
const controls = [dismiss, autocomplete.input, cancel, save].filter(control => !control.disabled);
|
||||
event.preventDefault();
|
||||
const index = controls.indexOf(document.activeElement);
|
||||
controls[(index + (event.shiftKey ? -1 : 1) + controls.length) % controls.length]?.focus();
|
||||
}
|
||||
});
|
||||
actions.append(cancel, save);
|
||||
body.append(label, status, actions);
|
||||
panel.append(header, body);
|
||||
overlay.append(panel);
|
||||
modal.append(overlay);
|
||||
cancel.focus();
|
||||
Promise.resolve().then(loadFolders).then(result => {
|
||||
if (!overlay.isConnected) return;
|
||||
if (!result?.ok) throw new Error(result?.error || "Unable to load bookmarks folders.");
|
||||
const placeholder = document.createElement("option");
|
||||
placeholder.value = "";
|
||||
placeholder.textContent = "Select a folder";
|
||||
select.append(placeholder);
|
||||
for (const folder of result.folders) {
|
||||
const option = document.createElement("option");
|
||||
option.value = folder.id;
|
||||
option.textContent = folder.label;
|
||||
select.append(option);
|
||||
}
|
||||
select.disabled = !result.folders.length;
|
||||
status.textContent = result.folders.length ? "Choose where to save Opportunities Extension." : "No writable bookmarks folders available.";
|
||||
select.addEventListener("change", () => { save.disabled = !select.value; });
|
||||
autocomplete.sync();
|
||||
if (!select.disabled) autocomplete.input.focus();
|
||||
}).catch(error => {
|
||||
if (overlay.isConnected) {
|
||||
status.setAttribute("role", "alert");
|
||||
status.textContent = error.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
function createDetailLink(label, href) {
|
||||
if (label === null || label === undefined || label === "") {
|
||||
const placeholder = document.createElement("span");
|
||||
@@ -114,5 +246,5 @@ function createOpportunitiesLinks({
|
||||
return badge;
|
||||
}
|
||||
|
||||
return { createDetailLink, createCopyOpportunityButton, copyOpportunityNumber, showCopyConfirmation, createStageBadge, createStatusBadge };
|
||||
return { chooseBookmarkFolder, createDetailLink, createCopyOpportunityButton, copyOpportunityNumber, showCopyConfirmation, createStageBadge, createStatusBadge };
|
||||
}
|
||||
|
||||
@@ -111,10 +111,14 @@ function createRatecardView({ ACCOUNT_PLANS_MODAL_ID, columns, formatValue, sele
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = selectedOrders.has(value);
|
||||
checkbox.addEventListener("change", () => {
|
||||
if (checkbox.checked) selectedOrders.add(value); else selectedOrders.delete(value);
|
||||
if (orders.every(order => selectedOrders.has(order))) {
|
||||
selectedOrders.clear();
|
||||
selectedOrders.add(value);
|
||||
} else if (checkbox.checked) selectedOrders.add(value); else selectedOrders.delete(value);
|
||||
allOrders.checked = selectedOrders.size === orders.length;
|
||||
allOrders.indeterminate = selectedOrders.size > 0 && !allOrders.checked;
|
||||
orderButton.textContent = allOrders.checked ? "All order numbers" : `${selectedOrders.size} order numbers selected`;
|
||||
updateOrders();
|
||||
renderRows();
|
||||
});
|
||||
label.append(checkbox, element("span", null, value || "Not provided"));
|
||||
|
||||
@@ -67,7 +67,7 @@ function createFusionCrmServiceRequests({ isCurrentPeriodRequest,
|
||||
throw new Error("CRM service request pagination did not advance.");
|
||||
}
|
||||
items.push(...data.items);
|
||||
onProgress(items.length, Number.isFinite(data.totalResults) && data.totalResults >= items.length ? data.totalResults : null);
|
||||
onProgress(items.length, Number.isFinite(data.totalResults) && data.totalResults >= items.length ? data.totalResults : null, items.slice());
|
||||
if (!data.hasMore) return items;
|
||||
offset = current + received;
|
||||
attempt = 1;
|
||||
|
||||
@@ -9,7 +9,6 @@ function createFusionOpportunities({
|
||||
getOpportunityTypeValues,
|
||||
readResponseBodyForDebug,
|
||||
resetDebugRequests = () => {},
|
||||
loadServiceRequests = async () => {},
|
||||
environment
|
||||
}) {
|
||||
const { fetch } = environment;
|
||||
@@ -40,9 +39,6 @@ function createFusionOpportunities({
|
||||
// One history per query, preserving all pages of the same result set.
|
||||
if (page === 1) {
|
||||
resetDebugRequests();
|
||||
// SR queries do not depend on opportunity or product IDs. Keep their
|
||||
// diagnostics in this query's history and reuse the loaded SR state later.
|
||||
try { await loadServiceRequests(token, requestVersion); } catch { /* SR failure must not block opportunities. */ }
|
||||
if (!isCurrentPeriodRequest(requestVersion)) return null;
|
||||
}
|
||||
const requestEntry = logRequestStart({
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/** Stable wire names. All payloads remain plain, serializable objects. */
|
||||
function createMessageTypes() {
|
||||
return Object.freeze({
|
||||
addShortcutBookmark: "opportunitiesExtension.addShortcutBookmark",
|
||||
getBookmarkFolders: "opportunitiesExtension.getBookmarkFolders",
|
||||
getShortcutBookmarkStatus: "opportunitiesExtension.getShortcutBookmarkStatus",
|
||||
loadOrganizationChart: "opportunitiesExtension.loadOrganizationChart",
|
||||
saveOrganizationChart: "opportunitiesExtension.saveOrganizationChart",
|
||||
requestWeekDetails: "opportunitiesExtension.requestWeekDetails",
|
||||
|
||||
Reference in New Issue
Block a user