Compare commits
6 Commits
db382b63f2
...
6f8b2beea3
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f8b2beea3 | |||
| 911da359e3 | |||
| 9c6c5ff690 | |||
| 9cb58809c8 | |||
| dfc84c64ee | |||
| 0b854e23d2 |
46
README.md
46
README.md
@@ -1,42 +1,50 @@
|
|||||||
# Opportunities Extension
|
# Opportunities Extension 1.0.0
|
||||||
|
|
||||||
Extensao de navegador para adicionar o atalho "Opportunities Extension" nas paginas Oracle Fusion permitidas.
|
Browser extension that adds the "Opportunities Extension" shortcut to the permitted Oracle Fusion pages, retrieves opportunities, and displays them in a filterable table.
|
||||||
|
|
||||||
## Estrutura de desenvolvimento
|
## Development Structure
|
||||||
|
|
||||||
- `src/content.js`: script unico mantido no projeto.
|
- `src/content.js`: single source file maintained by the project.
|
||||||
- `scripts/build.mjs`: gera os pacotes especificos para cada navegador.
|
- `scripts/build.mjs`: generates browser-specific packages.
|
||||||
- `dist/chromium/`: output para Google Chrome e Microsoft Edge.
|
- `dist/chromium/`: output for Google Chrome and Microsoft Edge.
|
||||||
- `dist/firefox/`: output para Firefox.
|
- `dist/firefox/`: output for Firefox.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
Use o Node.js:
|
Use Node.js:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
O build recria `dist/chromium` e `dist/firefox` a partir da mesma fonte.
|
The build recreates `dist/chromium` and `dist/firefox` from the same source.
|
||||||
|
|
||||||
## Como carregar em modo desenvolvimento
|
## Release 1.0.0
|
||||||
|
|
||||||
|
- Paginated opportunity retrieval with filters for reference, customer, owner, status, stage, and text search.
|
||||||
|
- Column sorting, links to opportunities and accounts, opportunity number copying, and stage indicators.
|
||||||
|
- Opportunity type view preferences persisted in the browser.
|
||||||
|
- Light and dark themes based on Oracle Redwood.
|
||||||
|
- Separate outputs for Chromium/Edge and Firefox.
|
||||||
|
|
||||||
|
## Loading in Development Mode
|
||||||
|
|
||||||
### Chrome ou Microsoft Edge
|
### Chrome ou Microsoft Edge
|
||||||
|
|
||||||
1. Abra `chrome://extensions` ou `edge://extensions`.
|
1. Open `chrome://extensions` or `edge://extensions`.
|
||||||
2. Ative o modo de desenvolvedor.
|
2. Enable Developer mode.
|
||||||
3. Clique em "Carregar sem compactacao".
|
3. Click **Load unpacked**.
|
||||||
4. Selecione a pasta `dist/chromium`.
|
4. Select the `dist/chromium` folder.
|
||||||
|
|
||||||
### Firefox
|
### Firefox
|
||||||
|
|
||||||
1. Abra `about:debugging#/runtime/this-firefox`.
|
1. Open `about:debugging#/runtime/this-firefox`.
|
||||||
2. Clique em "Carregar extensao temporaria".
|
2. Click **Load Temporary Add-on**.
|
||||||
3. Selecione o arquivo `dist/firefox/manifest.json`.
|
3. Select the `dist/firefox/manifest.json` file.
|
||||||
|
|
||||||
## Paginas atendidas
|
## Supported Pages
|
||||||
|
|
||||||
- `https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome`
|
- `https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome`
|
||||||
- `https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome`
|
- `https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome`
|
||||||
|
|
||||||
Ao encontrar o grupo `#yourapps_groupNode_sales`, a extensao adiciona o tile antes do item `.flat-grid-cell.flat-grid-cell-addicon`. Ao clicar no tile, exibe o alerta `Deu certo`.
|
When it finds the `#yourapps_groupNode_sales` group, the extension adds the tile before the `.flat-grid-cell.flat-grid-cell-addicon` item. Clicking the tile opens the opportunities interface.
|
||||||
|
|||||||
186
dist/chromium/background.js
vendored
186
dist/chromium/background.js
vendored
@@ -10,37 +10,181 @@
|
|||||||
];
|
];
|
||||||
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
|
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
|
||||||
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-";
|
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-";
|
||||||
|
const OPPORTUNITIES_LIST_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-list";
|
||||||
|
const COOKIE_REFRESH_TIMEOUT_MS = 15000;
|
||||||
|
const COOKIE_REFRESH_POLL_MS = 250;
|
||||||
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||||
|
|
||||||
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
if (!message || message.type !== "opportunitiesExtension.getXsrfToken") {
|
if (!message || !message.type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getXsrfTokenCookie()
|
if (message.type === "opportunitiesExtension.getXsrfToken") {
|
||||||
.then((result) => {
|
getXsrfTokenCookie()
|
||||||
sendResponse({
|
.then((result) => {
|
||||||
ok: true,
|
sendResponse({
|
||||||
cookieName: result.cookie ? result.cookie.name : "",
|
ok: true,
|
||||||
token: result.cookie ? result.cookie.value : "",
|
cookieName: result.cookie ? result.cookie.name : "",
|
||||||
matchedCookieNames: result.matchedCookieNames,
|
token: result.cookie ? result.cookie.value : "",
|
||||||
lookupDetails: result.lookupDetails
|
matchedCookieNames: result.matchedCookieNames,
|
||||||
|
lookupDetails: result.lookupDetails
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
cookieName: "",
|
||||||
|
token: "",
|
||||||
|
matchedCookieNames: [],
|
||||||
|
lookupDetails: [],
|
||||||
|
error: error.message || "Unable to read cookies."
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
sendResponse({
|
|
||||||
ok: false,
|
|
||||||
cookieName: "",
|
|
||||||
token: "",
|
|
||||||
matchedCookieNames: [],
|
|
||||||
lookupDetails: [],
|
|
||||||
error: error.message || "Unable to read cookies."
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "opportunitiesExtension.refreshXsrfCookie") {
|
||||||
|
refreshXsrfCookie(sender)
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
error: error.message || "Unable to refresh XSRF cookie."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function refreshXsrfCookie(sender) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const previous = (await getXsrfTokenCookie()).cookie;
|
||||||
|
const createProperties = {
|
||||||
|
url: OPPORTUNITIES_LIST_URL,
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sender && sender.tab && Number.isInteger(sender.tab.windowId)) {
|
||||||
|
createProperties.windowId = sender.tab.windowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsFirefoxCookieStoreTabs() && sender && sender.tab && sender.tab.cookieStoreId) {
|
||||||
|
createProperties.cookieStoreId = sender.tab.cookieStoreId;
|
||||||
|
}
|
||||||
|
|
||||||
|
let refreshTab;
|
||||||
|
|
||||||
|
try {
|
||||||
|
refreshTab = await tabsCreate(createProperties);
|
||||||
|
const current = await waitForXsrfCookieChange(previous);
|
||||||
|
const refreshed = hasCookieChanged(previous, current);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: Boolean(current),
|
||||||
|
refreshed,
|
||||||
|
cookieName: current ? current.name : "",
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
error: refreshed ? "" : "XSRF cookie was not created or changed before timeout."
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
if (refreshTab && Number.isInteger(refreshTab.id)) {
|
||||||
|
try {
|
||||||
|
await tabsRemove(refreshTab.id);
|
||||||
|
} catch (error) {
|
||||||
|
// The temporary tab may already have been closed.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForXsrfCookieChange(previousCookie) {
|
||||||
|
const deadline = Date.now() + COOKIE_REFRESH_TIMEOUT_MS;
|
||||||
|
let currentCookie = null;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
currentCookie = (await getXsrfTokenCookie()).cookie;
|
||||||
|
|
||||||
|
if (hasCookieChanged(previousCookie, currentCookie)) {
|
||||||
|
return currentCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
await wait(COOKIE_REFRESH_POLL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCookieChanged(previousCookie, currentCookie) {
|
||||||
|
if (!currentCookie || !currentCookie.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previousCookie) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return previousCookie.name !== currentCookie.name
|
||||||
|
|| previousCookie.value !== currentCookie.value
|
||||||
|
|| previousCookie.expirationDate !== currentCookie.expirationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportsFirefoxCookieStoreTabs() {
|
||||||
|
return Boolean(
|
||||||
|
runtimeApi.runtime
|
||||||
|
&& typeof runtimeApi.runtime.getBrowserInfo === "function"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabsCreate(details) {
|
||||||
|
if (runtimeApi.tabs.create.length <= 1) {
|
||||||
|
return runtimeApi.tabs.create(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
runtimeApi.tabs.create(details, (tab) => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
reject(new Error(lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(tab);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabsRemove(tabId) {
|
||||||
|
if (runtimeApi.tabs.remove.length <= 1) {
|
||||||
|
return runtimeApi.tabs.remove(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
runtimeApi.tabs.remove(tabId, () => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
reject(new Error(lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function wait(durationMs) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, durationMs));
|
||||||
|
}
|
||||||
|
|
||||||
async function getXsrfTokenCookie() {
|
async function getXsrfTokenCookie() {
|
||||||
const lookupDetails = [];
|
const lookupDetails = [];
|
||||||
const allCookies = [];
|
const allCookies = [];
|
||||||
|
|||||||
741
dist/chromium/content.js
vendored
741
dist/chromium/content.js
vendored
@@ -26,6 +26,8 @@
|
|||||||
const STAGE_DASHBOARD_ID = "opportunities-extension-stage-dashboard";
|
const STAGE_DASHBOARD_ID = "opportunities-extension-stage-dashboard";
|
||||||
const FILTER_PREFERENCES_STORAGE_KEY = "opportunities-extension-filter-preferences";
|
const FILTER_PREFERENCES_STORAGE_KEY = "opportunities-extension-filter-preferences";
|
||||||
const THEME_STORAGE_KEY = "opportunities-extension-theme";
|
const THEME_STORAGE_KEY = "opportunities-extension-theme";
|
||||||
|
const OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY = "opportunities-extension-opportunity-type-preferences";
|
||||||
|
const PREFERENCES_MODAL_ID = "opportunities-extension-preferences-modal";
|
||||||
const OPPORTUNITY_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?puid=";
|
const OPPORTUNITY_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?puid=";
|
||||||
const ACCOUNT_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/accounts/accounts-detail?id=";
|
const ACCOUNT_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/accounts/accounts-detail?id=";
|
||||||
const DEBUG_BODY_LIMIT = 12000;
|
const DEBUG_BODY_LIMIT = 12000;
|
||||||
@@ -40,6 +42,24 @@
|
|||||||
};
|
};
|
||||||
const STAGE_OPTIONS = ["SQL", "PIPELINE", "UPSIDE", "FORECAST", "WON"];
|
const STAGE_OPTIONS = ["SQL", "PIPELINE", "UPSIDE", "FORECAST", "WON"];
|
||||||
const STATUS_OPTIONS = ["OPEN", "CLOSED", "LOST", "WON"];
|
const STATUS_OPTIONS = ["OPEN", "CLOSED", "LOST", "WON"];
|
||||||
|
const OPPORTUNITY_TYPE_OPTIONS = [
|
||||||
|
{ label: "All records I can see", value: "ORA_ALLOPTIES" },
|
||||||
|
{ label: "I am credit receiver", value: "ORA_CREDITRECEIVER_ISME" },
|
||||||
|
{ label: "I am on the team", value: "ORA_MYSALESTEAMOPTIES" },
|
||||||
|
{ label: "I or my subordinate is a credit receiver", value: "ORA_CREDITRECEIVER_MYORG" },
|
||||||
|
{ label: "I own", value: "ORA_MYOPTIES" },
|
||||||
|
{ label: "My subordinates are on the team", value: "ORA_MYSUBORDSSALESTEAMOPTIES" },
|
||||||
|
{ label: "My subordinates own", value: "ORA_MYSUBORDINATESOPTIES" },
|
||||||
|
{ label: "My territory", value: "ORA_MYASSGTERROPTIES" },
|
||||||
|
{ label: "My territory hierarchy", value: "ORA_MYASSGDESCTERROPTIES" },
|
||||||
|
{ label: "My territory membership", value: "ORA_MYASSGMEMBTERROPTIES" },
|
||||||
|
{ label: "My territory membership hierarchy", value: "ORA_MYASSGMEMBDESCTERROPTIES" }
|
||||||
|
];
|
||||||
|
const DEFAULT_OPPORTUNITY_TYPE_VALUES = [
|
||||||
|
"ORA_MYSALESTEAMOPTIES",
|
||||||
|
"ORA_MYASSGTERROPTIES",
|
||||||
|
"ORA_CREDITRECEIVER_ISME"
|
||||||
|
];
|
||||||
let tokenRelayRequest = null;
|
let tokenRelayRequest = null;
|
||||||
let accessToken = "";
|
let accessToken = "";
|
||||||
let periodRequestVersion = 0;
|
let periodRequestVersion = 0;
|
||||||
@@ -52,6 +72,7 @@
|
|||||||
let selectedOwnerKeys = new Set();
|
let selectedOwnerKeys = new Set();
|
||||||
let ownerSearch = "";
|
let ownerSearch = "";
|
||||||
let tableSearch = "";
|
let tableSearch = "";
|
||||||
|
let selectedOpportunityTypeValues = new Set(DEFAULT_OPPORTUNITY_TYPE_VALUES);
|
||||||
let hasSavedCustomerFilter = false;
|
let hasSavedCustomerFilter = false;
|
||||||
let hasSavedOwnerFilter = false;
|
let hasSavedOwnerFilter = false;
|
||||||
let customerFilterInitialized = false;
|
let customerFilterInitialized = false;
|
||||||
@@ -79,9 +100,9 @@
|
|||||||
{ key: "owner", label: "Owner", value: getOwnerName, display: getOwnerName },
|
{ key: "owner", label: "Owner", value: getOwnerName, display: getOwnerName },
|
||||||
{ key: "revenue", label: "Amount", value: (item) => getNestedValue(item, ["PrimaryRevenue", "RevnAmount"]), display: formatRevenue },
|
{ key: "revenue", label: "Amount", value: (item) => getNestedValue(item, ["PrimaryRevenue", "RevnAmount"]), display: formatRevenue },
|
||||||
{ key: "closeDate", label: "Close Date", value: (item) => item.EffectiveDate, display: (item) => formatOracleResponseDate(item.EffectiveDate) },
|
{ key: "closeDate", label: "Close Date", value: (item) => item.EffectiveDate, display: (item) => formatOracleResponseDate(item.EffectiveDate) },
|
||||||
{ key: "stage", label: "Stage", value: (item) => item.ForecastGroup_c, display: (item) => item.ForecastGroup_c },
|
|
||||||
{ key: "winProbability", label: "Win Probability", value: (item) => getNestedValue(item, ["PrimaryRevenue", "WinProb"]), display: formatWinProbability },
|
|
||||||
{ key: "status", label: "Status", value: (item) => item.StatusCode, display: (item) => item.StatusCode },
|
{ key: "status", label: "Status", value: (item) => item.StatusCode, display: (item) => item.StatusCode },
|
||||||
|
{ key: "winProbability", label: "Win Probability", value: (item) => getNestedValue(item, ["PrimaryRevenue", "WinProb"]), display: formatWinProbability },
|
||||||
|
{ key: "stage", label: "Stage", value: (item) => item.ForecastGroup_c, display: (item) => item.ForecastGroup_c },
|
||||||
{ key: "lastUpdateDate", label: "Last Update Date", value: (item) => item.LastUpdateDate, display: (item) => formatOracleResponseDate(item.LastUpdateDate, true) }
|
{ key: "lastUpdateDate", label: "Last Update Date", value: (item) => item.LastUpdateDate, display: (item) => formatOracleResponseDate(item.LastUpdateDate, true) }
|
||||||
];
|
];
|
||||||
const ALLOWED_PATHS = [
|
const ALLOWED_PATHS = [
|
||||||
@@ -176,6 +197,7 @@
|
|||||||
periodRequestVersion = 0;
|
periodRequestVersion = 0;
|
||||||
authStatus = AUTH_STATUS.idle;
|
authStatus = AUTH_STATUS.idle;
|
||||||
resetOpportunitiesTable();
|
resetOpportunitiesTable();
|
||||||
|
selectedOpportunityTypeValues = new Set(getSavedOpportunityTypeValues());
|
||||||
overlay.id = MODAL_ID;
|
overlay.id = MODAL_ID;
|
||||||
overlay.className = "opportunities-extension-modal";
|
overlay.className = "opportunities-extension-modal";
|
||||||
overlay.setAttribute("data-theme", getSavedTheme());
|
overlay.setAttribute("data-theme", getSavedTheme());
|
||||||
@@ -226,9 +248,17 @@
|
|||||||
});
|
});
|
||||||
applyTheme(overlay, themeButton, overlay.getAttribute("data-theme"));
|
applyTheme(overlay, themeButton, overlay.getAttribute("data-theme"));
|
||||||
|
|
||||||
|
const preferencesButton = document.createElement("button");
|
||||||
|
preferencesButton.type = "button";
|
||||||
|
preferencesButton.className = "opportunities-extension-icon-button";
|
||||||
|
preferencesButton.setAttribute("aria-label", "Abrir preferências");
|
||||||
|
preferencesButton.title = "Preferences";
|
||||||
|
preferencesButton.append(createSettingsIcon());
|
||||||
|
preferencesButton.addEventListener("click", openPreferencesModal);
|
||||||
|
|
||||||
const headerActions = document.createElement("div");
|
const headerActions = document.createElement("div");
|
||||||
headerActions.className = "opportunities-extension-header-actions";
|
headerActions.className = "opportunities-extension-header-actions";
|
||||||
headerActions.append(themeButton, closeButton);
|
headerActions.append(themeButton, preferencesButton, closeButton);
|
||||||
|
|
||||||
header.append(titleBlock, headerActions);
|
header.append(titleBlock, headerActions);
|
||||||
|
|
||||||
@@ -242,7 +272,7 @@
|
|||||||
field.className = "opportunities-extension-field";
|
field.className = "opportunities-extension-field";
|
||||||
|
|
||||||
const labelText = document.createElement("span");
|
const labelText = document.createElement("span");
|
||||||
labelText.textContent = "Periodo";
|
labelText.textContent = "Reference";
|
||||||
|
|
||||||
const selectWrap = document.createElement("span");
|
const selectWrap = document.createElement("span");
|
||||||
selectWrap.className = "opportunities-extension-select-wrap";
|
selectWrap.className = "opportunities-extension-select-wrap";
|
||||||
@@ -502,8 +532,98 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function requestTokenRelay() {
|
async function requestTokenRelay() {
|
||||||
const xsrfTokenSource = await getXsrfToken();
|
let refreshResult = null;
|
||||||
const requestEntry = logRequestStart({
|
let refreshAttempted = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const xsrfTokenSource = await getXsrfToken();
|
||||||
|
|
||||||
|
if (!xsrfTokenSource.token) {
|
||||||
|
if (refreshAttempted) {
|
||||||
|
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult);
|
||||||
|
logRequestFailure(requestEntry, "XSRF token cookie not found after automatic refresh.");
|
||||||
|
throw new Error("XSRF token cookie not found after automatic refresh.");
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshResult = await refreshXsrfCookie("missing-cookie");
|
||||||
|
refreshAttempted = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(TOKEN_RELAY_URL, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"x-xsrf-token": xsrfTokenSource.token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseBody = await readResponseBodyForDebug(response);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted && isInvalidXsrfStatus(response.status)) {
|
||||||
|
refreshResult = await refreshXsrfCookie(`tokenrelay-${response.status}`);
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Token relay failed with status ${response.status}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
responseData = await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted) {
|
||||||
|
refreshResult = await refreshXsrfCookie("tokenrelay-invalid-response");
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted) {
|
||||||
|
refreshResult = await refreshXsrfCookie("tokenrelay-missing-access-token");
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Token relay response does not contain access_token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
logRequestSuccess(requestEntry, response, createDebugBody(JSON.stringify({
|
||||||
|
...responseData,
|
||||||
|
access_token: maskToken(responseData.access_token)
|
||||||
|
}, null, 2)));
|
||||||
|
|
||||||
|
return responseData.access_token;
|
||||||
|
} catch (error) {
|
||||||
|
logRequestFailure(requestEntry, error.message || "Request failed.");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTokenRelayRequestEntry(xsrfTokenSource, refreshResult) {
|
||||||
|
return logRequestStart({
|
||||||
name: "requestTokenRelay",
|
name: "requestTokenRelay",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: TOKEN_RELAY_URL,
|
url: TOKEN_RELAY_URL,
|
||||||
@@ -519,62 +639,40 @@
|
|||||||
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
||||||
? xsrfTokenSource.lookupDetails.join(" | ")
|
? xsrfTokenSource.lookupDetails.join(" | ")
|
||||||
: "(none)",
|
: "(none)",
|
||||||
xsrfTokenError: xsrfTokenSource.error || ""
|
xsrfTokenError: xsrfTokenSource.error || "",
|
||||||
|
xsrfRefresh: formatXsrfRefreshResult(refreshResult)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!xsrfTokenSource.token) {
|
function isInvalidXsrfStatus(status) {
|
||||||
logRequestFailure(requestEntry, "XSRF token cookie not found.");
|
return status === 400 || status === 401 || status === 403;
|
||||||
throw new Error("XSRF token cookie not found.");
|
}
|
||||||
|
|
||||||
|
function formatXsrfRefreshResult(result) {
|
||||||
|
if (!result) {
|
||||||
|
return "not-requested";
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const outcome = result.refreshed ? "refreshed" : "not-refreshed";
|
||||||
const response = await fetch(TOKEN_RELAY_URL, {
|
const cookieName = result.cookieName || "(none)";
|
||||||
method: "GET",
|
const duration = Number.isFinite(result.durationMs) ? `${result.durationMs}ms` : "unknown";
|
||||||
credentials: "include",
|
return `${result.reason}; ${outcome}; cookie=${cookieName}; duration=${duration}; ${result.error || ""}`.trim();
|
||||||
headers: {
|
|
||||||
"x-xsrf-token": xsrfTokenSource.token
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseBody = await readResponseBodyForDebug(response);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
throw new Error(`Token relay failed with status ${response.status}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let responseData;
|
|
||||||
|
|
||||||
try {
|
|
||||||
responseData = await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
throw new Error("Token relay response does not contain access_token.");
|
|
||||||
}
|
|
||||||
|
|
||||||
logRequestSuccess(requestEntry, response, createDebugBody(JSON.stringify({
|
|
||||||
...responseData,
|
|
||||||
access_token: maskToken(responseData.access_token)
|
|
||||||
}, null, 2)));
|
|
||||||
|
|
||||||
return responseData.access_token;
|
|
||||||
} catch (error) {
|
|
||||||
logRequestFailure(requestEntry, error.message || "Request failed.");
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestOpportunitiesForPeriod(period) {
|
async function requestOpportunitiesForPeriod(period) {
|
||||||
const dateRange = getFiscalPeriodRange(period, new Date());
|
const dateRange = getFiscalPeriodRange(period, new Date());
|
||||||
const requestVersion = ++periodRequestVersion;
|
const requestVersion = ++periodRequestVersion;
|
||||||
|
|
||||||
|
if (selectedOpportunityTypeValues.size === 0) {
|
||||||
|
setOpportunitiesTableState({
|
||||||
|
items: [],
|
||||||
|
status: "empty",
|
||||||
|
message: "Select at least one Opportunity Type View in Preferences."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!dateRange) {
|
if (!dateRange) {
|
||||||
setOpportunitiesTableState({
|
setOpportunitiesTableState({
|
||||||
items: [],
|
items: [],
|
||||||
@@ -1302,6 +1400,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (owner && typeof owner === "object") {
|
if (owner && typeof owner === "object") {
|
||||||
|
const firstName = owner.PersonFirstName;
|
||||||
|
const lastName = owner.PersonLastName;
|
||||||
|
|
||||||
|
if (firstName || lastName) {
|
||||||
|
return [firstName, lastName].filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
return owner.PartyName
|
return owner.PartyName
|
||||||
|| owner.PartyUniqueName
|
|| owner.PartyUniqueName
|
||||||
|| owner.Name
|
|| owner.Name
|
||||||
@@ -1435,7 +1540,7 @@
|
|||||||
const normalizedSearch = tableSearch.trim().toLocaleLowerCase();
|
const normalizedSearch = tableSearch.trim().toLocaleLowerCase();
|
||||||
const items = opportunitiesTableState.items.filter((item) => {
|
const items = opportunitiesTableState.items.filter((item) => {
|
||||||
return selectedStages.has(item.ForecastGroup_c)
|
return selectedStages.has(item.ForecastGroup_c)
|
||||||
&& selectedStatuses.has(item.StatusCode)
|
&& matchesSelectedStatus(item.StatusCode)
|
||||||
&& selectedCustomerKeys.has(getCustomerKey(item))
|
&& selectedCustomerKeys.has(getCustomerKey(item))
|
||||||
&& selectedOwnerKeys.has(getOwnerKey(item))
|
&& selectedOwnerKeys.has(getOwnerKey(item))
|
||||||
&& matchesTableSearch(item, normalizedSearch);
|
&& matchesTableSearch(item, normalizedSearch);
|
||||||
@@ -1488,6 +1593,14 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function matchesSelectedStatus(statusCode) {
|
||||||
|
if (statusCode === "WON_PENDING") {
|
||||||
|
return selectedStatuses.has("WON");
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedStatuses.has(statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
function stringifySearchValue(value) {
|
function stringifySearchValue(value) {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return "";
|
return "";
|
||||||
@@ -1679,10 +1792,11 @@
|
|||||||
criteria: [
|
criteria: [
|
||||||
{
|
{
|
||||||
op: "$or",
|
op: "$or",
|
||||||
criteria: [
|
criteria: Array.from(selectedOpportunityTypeValues, (value) => ({
|
||||||
{ op: "$eq", attribute: "RecordSet", value: "ORA_MYSALESTEAMOPTIES" },
|
op: "$eq",
|
||||||
{ op: "$eq", attribute: "RecordSet", value: "ORA_MYASSGTERROPTIES" }
|
attribute: "RecordSet",
|
||||||
]
|
value
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
op: "$wi",
|
op: "$wi",
|
||||||
@@ -2091,6 +2205,52 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshXsrfCookie(reason) {
|
||||||
|
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||||
|
|
||||||
|
if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
reason,
|
||||||
|
error: "Runtime messaging API unavailable."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
runtimeApi.runtime.sendMessage({
|
||||||
|
type: "opportunitiesExtension.refreshXsrfCookie"
|
||||||
|
}, (response) => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
resolve({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
reason,
|
||||||
|
error: lastError.message
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
...(response || {
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
error: "Empty refresh response."
|
||||||
|
}),
|
||||||
|
reason
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateAuthBadge(status) {
|
function updateAuthBadge(status) {
|
||||||
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);
|
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);
|
||||||
|
|
||||||
@@ -2115,6 +2275,7 @@
|
|||||||
const modal = document.getElementById(MODAL_ID);
|
const modal = document.getElementById(MODAL_ID);
|
||||||
|
|
||||||
if (modal) {
|
if (modal) {
|
||||||
|
closePreferencesModal();
|
||||||
modal.hidden = true;
|
modal.hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2168,6 +2329,185 @@
|
|||||||
return svg;
|
return svg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSettingsIcon() {
|
||||||
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
const upperControl = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
||||||
|
const lowerControl = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
||||||
|
|
||||||
|
svg.setAttribute("viewBox", "0 0 24 24");
|
||||||
|
svg.setAttribute("class", "opportunities-extension-settings-icon");
|
||||||
|
svg.setAttribute("aria-hidden", "true");
|
||||||
|
svg.setAttribute("focusable", "false");
|
||||||
|
appendPath(svg, "", "M4 7h16M4 17h16");
|
||||||
|
|
||||||
|
[upperControl, lowerControl].forEach((control, index) => {
|
||||||
|
control.setAttribute("cx", index === 0 ? "9" : "15");
|
||||||
|
control.setAttribute("cy", index === 0 ? "7" : "17");
|
||||||
|
control.setAttribute("r", "2.5");
|
||||||
|
control.setAttribute("fill", "currentColor");
|
||||||
|
control.setAttribute("stroke", "currentColor");
|
||||||
|
control.setAttribute("stroke-width", "1.8");
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.querySelector("path").setAttribute("fill", "none");
|
||||||
|
svg.querySelector("path").setAttribute("stroke", "currentColor");
|
||||||
|
svg.querySelector("path").setAttribute("stroke-linecap", "round");
|
||||||
|
svg.querySelector("path").setAttribute("stroke-width", "1.8");
|
||||||
|
svg.append(upperControl, lowerControl);
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCloseIcon() {
|
||||||
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||||
|
|
||||||
|
svg.setAttribute("viewBox", "0 0 24 24");
|
||||||
|
svg.setAttribute("aria-hidden", "true");
|
||||||
|
svg.setAttribute("focusable", "false");
|
||||||
|
path.setAttribute("d", "m6 6 12 12M18 6 6 18");
|
||||||
|
path.setAttribute("fill", "none");
|
||||||
|
path.setAttribute("stroke", "currentColor");
|
||||||
|
path.setAttribute("stroke-linecap", "round");
|
||||||
|
path.setAttribute("stroke-width", "1.8");
|
||||||
|
svg.append(path);
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSavedOpportunityTypeValues() {
|
||||||
|
try {
|
||||||
|
const rawPreferences = localStorage.getItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY);
|
||||||
|
|
||||||
|
if (!rawPreferences) {
|
||||||
|
return DEFAULT_OPPORTUNITY_TYPE_VALUES;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferences = JSON.parse(rawPreferences);
|
||||||
|
const values = Array.isArray(preferences.opportunityTypeValues) ? preferences.opportunityTypeValues : null;
|
||||||
|
|
||||||
|
if (!values) {
|
||||||
|
return DEFAULT_OPPORTUNITY_TYPE_VALUES;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedValues = new Set(OPPORTUNITY_TYPE_OPTIONS.map((option) => option.value));
|
||||||
|
return values.filter((value) => allowedValues.has(value));
|
||||||
|
} catch (error) {
|
||||||
|
return DEFAULT_OPPORTUNITY_TYPE_VALUES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPreferencesModal() {
|
||||||
|
const overlay = document.getElementById(MODAL_ID);
|
||||||
|
|
||||||
|
if (!overlay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingModal = document.getElementById(PREFERENCES_MODAL_ID);
|
||||||
|
|
||||||
|
if (existingModal) {
|
||||||
|
existingModal.hidden = false;
|
||||||
|
existingModal.querySelector("input")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferencesModal = document.createElement("section");
|
||||||
|
preferencesModal.id = PREFERENCES_MODAL_ID;
|
||||||
|
preferencesModal.className = "opportunities-extension-preferences-modal";
|
||||||
|
preferencesModal.setAttribute("role", "dialog");
|
||||||
|
preferencesModal.setAttribute("aria-modal", "true");
|
||||||
|
preferencesModal.setAttribute("aria-labelledby", "opportunities-extension-preferences-title");
|
||||||
|
|
||||||
|
const dialog = document.createElement("div");
|
||||||
|
dialog.className = "opportunities-extension-preferences-dialog";
|
||||||
|
|
||||||
|
const header = document.createElement("header");
|
||||||
|
header.className = "opportunities-extension-preferences-header";
|
||||||
|
|
||||||
|
const title = document.createElement("h2");
|
||||||
|
title.id = "opportunities-extension-preferences-title";
|
||||||
|
title.textContent = "Preferences";
|
||||||
|
|
||||||
|
const closeButton = document.createElement("button");
|
||||||
|
closeButton.type = "button";
|
||||||
|
closeButton.className = "opportunities-extension-icon-button";
|
||||||
|
closeButton.setAttribute("aria-label", "Fechar preferências");
|
||||||
|
closeButton.title = "Close";
|
||||||
|
closeButton.append(createCloseIcon());
|
||||||
|
closeButton.addEventListener("click", closePreferencesModal);
|
||||||
|
header.append(title, closeButton);
|
||||||
|
|
||||||
|
const content = document.createElement("div");
|
||||||
|
content.className = "opportunities-extension-preferences-content";
|
||||||
|
|
||||||
|
const fieldset = document.createElement("fieldset");
|
||||||
|
fieldset.className = "opportunities-extension-preferences-fieldset";
|
||||||
|
|
||||||
|
const legend = document.createElement("legend");
|
||||||
|
legend.textContent = "Opportunity Type View";
|
||||||
|
fieldset.append(legend);
|
||||||
|
|
||||||
|
const selectedValues = new Set(getSavedOpportunityTypeValues());
|
||||||
|
const options = document.createElement("div");
|
||||||
|
options.className = "opportunities-extension-preferences-options";
|
||||||
|
|
||||||
|
OPPORTUNITY_TYPE_OPTIONS.forEach((option) => {
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.className = "opportunities-extension-preferences-option";
|
||||||
|
|
||||||
|
const checkbox = document.createElement("input");
|
||||||
|
checkbox.type = "checkbox";
|
||||||
|
checkbox.name = "opportunityTypeView";
|
||||||
|
checkbox.value = option.value;
|
||||||
|
checkbox.checked = selectedValues.has(option.value);
|
||||||
|
|
||||||
|
const text = document.createElement("span");
|
||||||
|
text.textContent = option.label;
|
||||||
|
label.append(checkbox, text);
|
||||||
|
options.append(label);
|
||||||
|
});
|
||||||
|
|
||||||
|
fieldset.append(options);
|
||||||
|
content.append(fieldset);
|
||||||
|
|
||||||
|
const footer = document.createElement("footer");
|
||||||
|
footer.className = "opportunities-extension-preferences-footer";
|
||||||
|
|
||||||
|
const saveButton = document.createElement("button");
|
||||||
|
saveButton.type = "button";
|
||||||
|
saveButton.className = "opportunities-extension-button opportunities-extension-button-primary";
|
||||||
|
saveButton.textContent = "Save";
|
||||||
|
saveButton.addEventListener("click", () => {
|
||||||
|
const selectedValues = Array.from(preferencesModal.querySelectorAll("input[name='opportunityTypeView']:checked"), (checkbox) => checkbox.value);
|
||||||
|
selectedOpportunityTypeValues = new Set(selectedValues);
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY, JSON.stringify({
|
||||||
|
opportunityTypeValues: selectedValues
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
// Continue with the saved selection for the active modal when storage is unavailable.
|
||||||
|
}
|
||||||
|
|
||||||
|
closePreferencesModal();
|
||||||
|
const periodSelect = overlay.querySelector("select[name='opportunityPeriod']");
|
||||||
|
requestOpportunitiesForPeriod(periodSelect?.value || PERIOD_OPTIONS[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
footer.append(saveButton);
|
||||||
|
dialog.append(header, content, footer);
|
||||||
|
preferencesModal.append(dialog);
|
||||||
|
overlay.append(preferencesModal);
|
||||||
|
preferencesModal.querySelector("input")?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePreferencesModal() {
|
||||||
|
const preferencesModal = document.getElementById(PREFERENCES_MODAL_ID);
|
||||||
|
|
||||||
|
if (preferencesModal) {
|
||||||
|
preferencesModal.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ensureEscapeKeyHandler() {
|
function ensureEscapeKeyHandler() {
|
||||||
if (window[ESCAPE_LISTENER_KEY]) {
|
if (window[ESCAPE_LISTENER_KEY]) {
|
||||||
return;
|
return;
|
||||||
@@ -2189,6 +2529,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
|
const preferencesModal = document.getElementById(PREFERENCES_MODAL_ID);
|
||||||
|
|
||||||
|
if (preferencesModal && !preferencesModal.hidden) {
|
||||||
|
closePreferencesModal();
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (closeCustomerFilterPanel()) {
|
if (closeCustomerFilterPanel()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
@@ -2365,6 +2713,11 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-icon-button .opportunities-extension-settings-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-icon-button:hover,
|
.opportunities-extension-icon-button:hover,
|
||||||
.opportunities-extension-icon-button:focus {
|
.opportunities-extension-icon-button:focus {
|
||||||
border-color: #b8b2ad;
|
border-color: #b8b2ad;
|
||||||
@@ -2447,7 +2800,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-total-dashboard-card {
|
.opportunities-extension-total-dashboard-card {
|
||||||
border-top-color: #00758f;
|
border-top-color: #312d2a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-stage-dashboard-card[data-stage="PIPELINE"] {
|
.opportunities-extension-stage-dashboard-card[data-stage="PIPELINE"] {
|
||||||
@@ -2867,12 +3220,13 @@
|
|||||||
border: 1px solid #dedbd7;
|
border: 1px solid #dedbd7;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
overflow: auto;
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table {
|
.opportunities-extension-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 1440px;
|
min-width: 0;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
color: #312d2a;
|
color: #312d2a;
|
||||||
@@ -2907,7 +3261,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(1) {
|
.opportunities-extension-table th:nth-child(1) {
|
||||||
width: 14%;
|
width: 17%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(2) {
|
.opportunities-extension-table th:nth-child(2) {
|
||||||
@@ -2915,7 +3269,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(3) {
|
.opportunities-extension-table th:nth-child(3) {
|
||||||
width: 17%;
|
width: 19%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(4) {
|
.opportunities-extension-table th:nth-child(4) {
|
||||||
@@ -2923,19 +3277,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(5) {
|
.opportunities-extension-table th:nth-child(5) {
|
||||||
width: 9%;
|
width: 8%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(6) {
|
.opportunities-extension-table th:nth-child(6) {
|
||||||
width: 9%;
|
width: 8%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(7) {
|
.opportunities-extension-table th:nth-child(7) {
|
||||||
width: 8%;
|
width: 7%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(8) {
|
.opportunities-extension-table th:nth-child(8) {
|
||||||
width: 8%;
|
width: 6%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(9) {
|
.opportunities-extension-table th:nth-child(9) {
|
||||||
@@ -2949,10 +3303,30 @@
|
|||||||
.opportunities-extension-table td:nth-child(2),
|
.opportunities-extension-table td:nth-child(2),
|
||||||
.opportunities-extension-table td:nth-child(5),
|
.opportunities-extension-table td:nth-child(5),
|
||||||
.opportunities-extension-table td:nth-child(6),
|
.opportunities-extension-table td:nth-child(6),
|
||||||
.opportunities-extension-table td:nth-child(7),
|
.opportunities-extension-table td:nth-child(8) {
|
||||||
.opportunities-extension-table td:nth-child(8),
|
white-space: nowrap;
|
||||||
.opportunities-extension-table td:nth-child(9),
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table td:nth-child(9) {
|
||||||
|
padding-right: 6px;
|
||||||
|
padding-left: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table td:nth-child(7) {
|
||||||
|
padding-right: 6px;
|
||||||
|
padding-left: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-table td:nth-child(10) {
|
.opportunities-extension-table td:nth-child(10) {
|
||||||
|
padding-right: 6px;
|
||||||
|
padding-left: 6px;
|
||||||
|
font-size: 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3112,14 +3486,16 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
padding: 10px 30px 10px 16px;
|
padding: 8px 42px 8px 16px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
|
line-height: 1.2;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
white-space: normal;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3170,6 +3546,135 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-modal {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 3;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
background: rgba(0, 0, 0, .42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-modal[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-dialog {
|
||||||
|
display: grid;
|
||||||
|
width: min(680px, 100%);
|
||||||
|
max-height: min(720px, calc(100vh - 48px));
|
||||||
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #8f8a85;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, .24);
|
||||||
|
color: #312d2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
border-bottom: 1px solid #dedbd7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-content {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-fieldset {
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-fieldset legend {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 0;
|
||||||
|
color: #312d2a;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 4px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 9px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #312d2a;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.35;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-option:hover {
|
||||||
|
background: #f0eeeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-option input {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex: 0 0 18px;
|
||||||
|
margin: 0;
|
||||||
|
accent-color: #00758f;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 14px 20px;
|
||||||
|
border-top: 1px solid #dedbd7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-button {
|
||||||
|
min-width: 88px;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 7px 16px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-button-primary {
|
||||||
|
border-color: #00758f;
|
||||||
|
background: #00758f;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-button-primary:hover,
|
||||||
|
.opportunities-extension-button-primary:focus-visible {
|
||||||
|
border-color: #005e73;
|
||||||
|
background: #005e73;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-debug-panel {
|
.opportunities-extension-debug-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 24px;
|
right: 24px;
|
||||||
@@ -3368,6 +3873,7 @@
|
|||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table-surface,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table-surface,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-panel,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-panel,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-dialog,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item[open] {
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item[open] {
|
||||||
border-color: #4e4a46;
|
border-color: #4e4a46;
|
||||||
@@ -3405,6 +3911,9 @@
|
|||||||
|
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-select-all,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-select-all,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-header h2,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-fieldset legend,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-heading,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-heading,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item pre,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item pre,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel h2 {
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel h2 {
|
||||||
@@ -3416,6 +3925,30 @@
|
|||||||
border-bottom-color: #4e4a46;
|
border-bottom-color: #4e4a46;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-header,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-footer {
|
||||||
|
border-color: #4e4a46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option:hover {
|
||||||
|
background: #3b3937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option input {
|
||||||
|
accent-color: #43c4d5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary {
|
||||||
|
border-color: #00a6be;
|
||||||
|
background: #008aa6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary:hover,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary:focus-visible {
|
||||||
|
border-color: #43c4d5;
|
||||||
|
background: #006d7a;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option:hover,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option:hover,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table tbody tr:hover,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table tbody tr:hover,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sort-button:hover,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sort-button:hover,
|
||||||
@@ -3471,7 +4004,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-total-dashboard-card {
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-total-dashboard-card {
|
||||||
border-top-color: #43c4d5;
|
border-top-color: #f6f4f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card[data-stage="PIPELINE"] {
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card[data-stage="PIPELINE"] {
|
||||||
@@ -3569,6 +4102,62 @@
|
|||||||
color: #f6f4f2;
|
color: #f6f4f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1201px) and (max-width: 1450px) {
|
||||||
|
.opportunities-extension-table th:nth-child(1) {
|
||||||
|
width: 16%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(3) {
|
||||||
|
width: 18%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(4) {
|
||||||
|
width: 12%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(6) {
|
||||||
|
width: 7%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(7) {
|
||||||
|
width: 8%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(10) {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.opportunities-extension-table {
|
||||||
|
min-width: 1180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(1) {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(3) {
|
||||||
|
width: 17%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(4) {
|
||||||
|
width: 13%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(6) {
|
||||||
|
width: 6%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(7) {
|
||||||
|
width: 9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(10) {
|
||||||
|
width: 11%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.opportunities-extension-header {
|
.opportunities-extension-header {
|
||||||
padding: 24px 20px 18px;
|
padding: 24px 20px 18px;
|
||||||
@@ -3627,6 +4216,18 @@
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-modal {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-dialog {
|
||||||
|
max-height: calc(100vh - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-options {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-stage-filter-controls {
|
.opportunities-extension-stage-filter-controls {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|||||||
2
dist/chromium/manifest.json
vendored
2
dist/chromium/manifest.json
vendored
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Opportunities Extension",
|
"name": "Opportunities Extension",
|
||||||
"description": "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
|
"description": "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"cookies"
|
"cookies"
|
||||||
],
|
],
|
||||||
|
|||||||
186
dist/firefox/background.js
vendored
186
dist/firefox/background.js
vendored
@@ -10,37 +10,181 @@
|
|||||||
];
|
];
|
||||||
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
|
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
|
||||||
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-";
|
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-";
|
||||||
|
const OPPORTUNITIES_LIST_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-list";
|
||||||
|
const COOKIE_REFRESH_TIMEOUT_MS = 15000;
|
||||||
|
const COOKIE_REFRESH_POLL_MS = 250;
|
||||||
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||||
|
|
||||||
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
if (!message || message.type !== "opportunitiesExtension.getXsrfToken") {
|
if (!message || !message.type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getXsrfTokenCookie()
|
if (message.type === "opportunitiesExtension.getXsrfToken") {
|
||||||
.then((result) => {
|
getXsrfTokenCookie()
|
||||||
sendResponse({
|
.then((result) => {
|
||||||
ok: true,
|
sendResponse({
|
||||||
cookieName: result.cookie ? result.cookie.name : "",
|
ok: true,
|
||||||
token: result.cookie ? result.cookie.value : "",
|
cookieName: result.cookie ? result.cookie.name : "",
|
||||||
matchedCookieNames: result.matchedCookieNames,
|
token: result.cookie ? result.cookie.value : "",
|
||||||
lookupDetails: result.lookupDetails
|
matchedCookieNames: result.matchedCookieNames,
|
||||||
|
lookupDetails: result.lookupDetails
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
cookieName: "",
|
||||||
|
token: "",
|
||||||
|
matchedCookieNames: [],
|
||||||
|
lookupDetails: [],
|
||||||
|
error: error.message || "Unable to read cookies."
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
sendResponse({
|
|
||||||
ok: false,
|
|
||||||
cookieName: "",
|
|
||||||
token: "",
|
|
||||||
matchedCookieNames: [],
|
|
||||||
lookupDetails: [],
|
|
||||||
error: error.message || "Unable to read cookies."
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "opportunitiesExtension.refreshXsrfCookie") {
|
||||||
|
refreshXsrfCookie(sender)
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
error: error.message || "Unable to refresh XSRF cookie."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function refreshXsrfCookie(sender) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const previous = (await getXsrfTokenCookie()).cookie;
|
||||||
|
const createProperties = {
|
||||||
|
url: OPPORTUNITIES_LIST_URL,
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sender && sender.tab && Number.isInteger(sender.tab.windowId)) {
|
||||||
|
createProperties.windowId = sender.tab.windowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsFirefoxCookieStoreTabs() && sender && sender.tab && sender.tab.cookieStoreId) {
|
||||||
|
createProperties.cookieStoreId = sender.tab.cookieStoreId;
|
||||||
|
}
|
||||||
|
|
||||||
|
let refreshTab;
|
||||||
|
|
||||||
|
try {
|
||||||
|
refreshTab = await tabsCreate(createProperties);
|
||||||
|
const current = await waitForXsrfCookieChange(previous);
|
||||||
|
const refreshed = hasCookieChanged(previous, current);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: Boolean(current),
|
||||||
|
refreshed,
|
||||||
|
cookieName: current ? current.name : "",
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
error: refreshed ? "" : "XSRF cookie was not created or changed before timeout."
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
if (refreshTab && Number.isInteger(refreshTab.id)) {
|
||||||
|
try {
|
||||||
|
await tabsRemove(refreshTab.id);
|
||||||
|
} catch (error) {
|
||||||
|
// The temporary tab may already have been closed.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForXsrfCookieChange(previousCookie) {
|
||||||
|
const deadline = Date.now() + COOKIE_REFRESH_TIMEOUT_MS;
|
||||||
|
let currentCookie = null;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
currentCookie = (await getXsrfTokenCookie()).cookie;
|
||||||
|
|
||||||
|
if (hasCookieChanged(previousCookie, currentCookie)) {
|
||||||
|
return currentCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
await wait(COOKIE_REFRESH_POLL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCookieChanged(previousCookie, currentCookie) {
|
||||||
|
if (!currentCookie || !currentCookie.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previousCookie) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return previousCookie.name !== currentCookie.name
|
||||||
|
|| previousCookie.value !== currentCookie.value
|
||||||
|
|| previousCookie.expirationDate !== currentCookie.expirationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportsFirefoxCookieStoreTabs() {
|
||||||
|
return Boolean(
|
||||||
|
runtimeApi.runtime
|
||||||
|
&& typeof runtimeApi.runtime.getBrowserInfo === "function"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabsCreate(details) {
|
||||||
|
if (runtimeApi.tabs.create.length <= 1) {
|
||||||
|
return runtimeApi.tabs.create(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
runtimeApi.tabs.create(details, (tab) => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
reject(new Error(lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(tab);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabsRemove(tabId) {
|
||||||
|
if (runtimeApi.tabs.remove.length <= 1) {
|
||||||
|
return runtimeApi.tabs.remove(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
runtimeApi.tabs.remove(tabId, () => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
reject(new Error(lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function wait(durationMs) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, durationMs));
|
||||||
|
}
|
||||||
|
|
||||||
async function getXsrfTokenCookie() {
|
async function getXsrfTokenCookie() {
|
||||||
const lookupDetails = [];
|
const lookupDetails = [];
|
||||||
const allCookies = [];
|
const allCookies = [];
|
||||||
|
|||||||
741
dist/firefox/content.js
vendored
741
dist/firefox/content.js
vendored
@@ -26,6 +26,8 @@
|
|||||||
const STAGE_DASHBOARD_ID = "opportunities-extension-stage-dashboard";
|
const STAGE_DASHBOARD_ID = "opportunities-extension-stage-dashboard";
|
||||||
const FILTER_PREFERENCES_STORAGE_KEY = "opportunities-extension-filter-preferences";
|
const FILTER_PREFERENCES_STORAGE_KEY = "opportunities-extension-filter-preferences";
|
||||||
const THEME_STORAGE_KEY = "opportunities-extension-theme";
|
const THEME_STORAGE_KEY = "opportunities-extension-theme";
|
||||||
|
const OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY = "opportunities-extension-opportunity-type-preferences";
|
||||||
|
const PREFERENCES_MODAL_ID = "opportunities-extension-preferences-modal";
|
||||||
const OPPORTUNITY_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?puid=";
|
const OPPORTUNITY_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?puid=";
|
||||||
const ACCOUNT_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/accounts/accounts-detail?id=";
|
const ACCOUNT_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/accounts/accounts-detail?id=";
|
||||||
const DEBUG_BODY_LIMIT = 12000;
|
const DEBUG_BODY_LIMIT = 12000;
|
||||||
@@ -40,6 +42,24 @@
|
|||||||
};
|
};
|
||||||
const STAGE_OPTIONS = ["SQL", "PIPELINE", "UPSIDE", "FORECAST", "WON"];
|
const STAGE_OPTIONS = ["SQL", "PIPELINE", "UPSIDE", "FORECAST", "WON"];
|
||||||
const STATUS_OPTIONS = ["OPEN", "CLOSED", "LOST", "WON"];
|
const STATUS_OPTIONS = ["OPEN", "CLOSED", "LOST", "WON"];
|
||||||
|
const OPPORTUNITY_TYPE_OPTIONS = [
|
||||||
|
{ label: "All records I can see", value: "ORA_ALLOPTIES" },
|
||||||
|
{ label: "I am credit receiver", value: "ORA_CREDITRECEIVER_ISME" },
|
||||||
|
{ label: "I am on the team", value: "ORA_MYSALESTEAMOPTIES" },
|
||||||
|
{ label: "I or my subordinate is a credit receiver", value: "ORA_CREDITRECEIVER_MYORG" },
|
||||||
|
{ label: "I own", value: "ORA_MYOPTIES" },
|
||||||
|
{ label: "My subordinates are on the team", value: "ORA_MYSUBORDSSALESTEAMOPTIES" },
|
||||||
|
{ label: "My subordinates own", value: "ORA_MYSUBORDINATESOPTIES" },
|
||||||
|
{ label: "My territory", value: "ORA_MYASSGTERROPTIES" },
|
||||||
|
{ label: "My territory hierarchy", value: "ORA_MYASSGDESCTERROPTIES" },
|
||||||
|
{ label: "My territory membership", value: "ORA_MYASSGMEMBTERROPTIES" },
|
||||||
|
{ label: "My territory membership hierarchy", value: "ORA_MYASSGMEMBDESCTERROPTIES" }
|
||||||
|
];
|
||||||
|
const DEFAULT_OPPORTUNITY_TYPE_VALUES = [
|
||||||
|
"ORA_MYSALESTEAMOPTIES",
|
||||||
|
"ORA_MYASSGTERROPTIES",
|
||||||
|
"ORA_CREDITRECEIVER_ISME"
|
||||||
|
];
|
||||||
let tokenRelayRequest = null;
|
let tokenRelayRequest = null;
|
||||||
let accessToken = "";
|
let accessToken = "";
|
||||||
let periodRequestVersion = 0;
|
let periodRequestVersion = 0;
|
||||||
@@ -52,6 +72,7 @@
|
|||||||
let selectedOwnerKeys = new Set();
|
let selectedOwnerKeys = new Set();
|
||||||
let ownerSearch = "";
|
let ownerSearch = "";
|
||||||
let tableSearch = "";
|
let tableSearch = "";
|
||||||
|
let selectedOpportunityTypeValues = new Set(DEFAULT_OPPORTUNITY_TYPE_VALUES);
|
||||||
let hasSavedCustomerFilter = false;
|
let hasSavedCustomerFilter = false;
|
||||||
let hasSavedOwnerFilter = false;
|
let hasSavedOwnerFilter = false;
|
||||||
let customerFilterInitialized = false;
|
let customerFilterInitialized = false;
|
||||||
@@ -79,9 +100,9 @@
|
|||||||
{ key: "owner", label: "Owner", value: getOwnerName, display: getOwnerName },
|
{ key: "owner", label: "Owner", value: getOwnerName, display: getOwnerName },
|
||||||
{ key: "revenue", label: "Amount", value: (item) => getNestedValue(item, ["PrimaryRevenue", "RevnAmount"]), display: formatRevenue },
|
{ key: "revenue", label: "Amount", value: (item) => getNestedValue(item, ["PrimaryRevenue", "RevnAmount"]), display: formatRevenue },
|
||||||
{ key: "closeDate", label: "Close Date", value: (item) => item.EffectiveDate, display: (item) => formatOracleResponseDate(item.EffectiveDate) },
|
{ key: "closeDate", label: "Close Date", value: (item) => item.EffectiveDate, display: (item) => formatOracleResponseDate(item.EffectiveDate) },
|
||||||
{ key: "stage", label: "Stage", value: (item) => item.ForecastGroup_c, display: (item) => item.ForecastGroup_c },
|
|
||||||
{ key: "winProbability", label: "Win Probability", value: (item) => getNestedValue(item, ["PrimaryRevenue", "WinProb"]), display: formatWinProbability },
|
|
||||||
{ key: "status", label: "Status", value: (item) => item.StatusCode, display: (item) => item.StatusCode },
|
{ key: "status", label: "Status", value: (item) => item.StatusCode, display: (item) => item.StatusCode },
|
||||||
|
{ key: "winProbability", label: "Win Probability", value: (item) => getNestedValue(item, ["PrimaryRevenue", "WinProb"]), display: formatWinProbability },
|
||||||
|
{ key: "stage", label: "Stage", value: (item) => item.ForecastGroup_c, display: (item) => item.ForecastGroup_c },
|
||||||
{ key: "lastUpdateDate", label: "Last Update Date", value: (item) => item.LastUpdateDate, display: (item) => formatOracleResponseDate(item.LastUpdateDate, true) }
|
{ key: "lastUpdateDate", label: "Last Update Date", value: (item) => item.LastUpdateDate, display: (item) => formatOracleResponseDate(item.LastUpdateDate, true) }
|
||||||
];
|
];
|
||||||
const ALLOWED_PATHS = [
|
const ALLOWED_PATHS = [
|
||||||
@@ -176,6 +197,7 @@
|
|||||||
periodRequestVersion = 0;
|
periodRequestVersion = 0;
|
||||||
authStatus = AUTH_STATUS.idle;
|
authStatus = AUTH_STATUS.idle;
|
||||||
resetOpportunitiesTable();
|
resetOpportunitiesTable();
|
||||||
|
selectedOpportunityTypeValues = new Set(getSavedOpportunityTypeValues());
|
||||||
overlay.id = MODAL_ID;
|
overlay.id = MODAL_ID;
|
||||||
overlay.className = "opportunities-extension-modal";
|
overlay.className = "opportunities-extension-modal";
|
||||||
overlay.setAttribute("data-theme", getSavedTheme());
|
overlay.setAttribute("data-theme", getSavedTheme());
|
||||||
@@ -226,9 +248,17 @@
|
|||||||
});
|
});
|
||||||
applyTheme(overlay, themeButton, overlay.getAttribute("data-theme"));
|
applyTheme(overlay, themeButton, overlay.getAttribute("data-theme"));
|
||||||
|
|
||||||
|
const preferencesButton = document.createElement("button");
|
||||||
|
preferencesButton.type = "button";
|
||||||
|
preferencesButton.className = "opportunities-extension-icon-button";
|
||||||
|
preferencesButton.setAttribute("aria-label", "Abrir preferências");
|
||||||
|
preferencesButton.title = "Preferences";
|
||||||
|
preferencesButton.append(createSettingsIcon());
|
||||||
|
preferencesButton.addEventListener("click", openPreferencesModal);
|
||||||
|
|
||||||
const headerActions = document.createElement("div");
|
const headerActions = document.createElement("div");
|
||||||
headerActions.className = "opportunities-extension-header-actions";
|
headerActions.className = "opportunities-extension-header-actions";
|
||||||
headerActions.append(themeButton, closeButton);
|
headerActions.append(themeButton, preferencesButton, closeButton);
|
||||||
|
|
||||||
header.append(titleBlock, headerActions);
|
header.append(titleBlock, headerActions);
|
||||||
|
|
||||||
@@ -242,7 +272,7 @@
|
|||||||
field.className = "opportunities-extension-field";
|
field.className = "opportunities-extension-field";
|
||||||
|
|
||||||
const labelText = document.createElement("span");
|
const labelText = document.createElement("span");
|
||||||
labelText.textContent = "Periodo";
|
labelText.textContent = "Reference";
|
||||||
|
|
||||||
const selectWrap = document.createElement("span");
|
const selectWrap = document.createElement("span");
|
||||||
selectWrap.className = "opportunities-extension-select-wrap";
|
selectWrap.className = "opportunities-extension-select-wrap";
|
||||||
@@ -502,8 +532,98 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function requestTokenRelay() {
|
async function requestTokenRelay() {
|
||||||
const xsrfTokenSource = await getXsrfToken();
|
let refreshResult = null;
|
||||||
const requestEntry = logRequestStart({
|
let refreshAttempted = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const xsrfTokenSource = await getXsrfToken();
|
||||||
|
|
||||||
|
if (!xsrfTokenSource.token) {
|
||||||
|
if (refreshAttempted) {
|
||||||
|
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult);
|
||||||
|
logRequestFailure(requestEntry, "XSRF token cookie not found after automatic refresh.");
|
||||||
|
throw new Error("XSRF token cookie not found after automatic refresh.");
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshResult = await refreshXsrfCookie("missing-cookie");
|
||||||
|
refreshAttempted = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(TOKEN_RELAY_URL, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"x-xsrf-token": xsrfTokenSource.token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseBody = await readResponseBodyForDebug(response);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted && isInvalidXsrfStatus(response.status)) {
|
||||||
|
refreshResult = await refreshXsrfCookie(`tokenrelay-${response.status}`);
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Token relay failed with status ${response.status}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
responseData = await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted) {
|
||||||
|
refreshResult = await refreshXsrfCookie("tokenrelay-invalid-response");
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted) {
|
||||||
|
refreshResult = await refreshXsrfCookie("tokenrelay-missing-access-token");
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Token relay response does not contain access_token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
logRequestSuccess(requestEntry, response, createDebugBody(JSON.stringify({
|
||||||
|
...responseData,
|
||||||
|
access_token: maskToken(responseData.access_token)
|
||||||
|
}, null, 2)));
|
||||||
|
|
||||||
|
return responseData.access_token;
|
||||||
|
} catch (error) {
|
||||||
|
logRequestFailure(requestEntry, error.message || "Request failed.");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTokenRelayRequestEntry(xsrfTokenSource, refreshResult) {
|
||||||
|
return logRequestStart({
|
||||||
name: "requestTokenRelay",
|
name: "requestTokenRelay",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: TOKEN_RELAY_URL,
|
url: TOKEN_RELAY_URL,
|
||||||
@@ -519,62 +639,40 @@
|
|||||||
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
||||||
? xsrfTokenSource.lookupDetails.join(" | ")
|
? xsrfTokenSource.lookupDetails.join(" | ")
|
||||||
: "(none)",
|
: "(none)",
|
||||||
xsrfTokenError: xsrfTokenSource.error || ""
|
xsrfTokenError: xsrfTokenSource.error || "",
|
||||||
|
xsrfRefresh: formatXsrfRefreshResult(refreshResult)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!xsrfTokenSource.token) {
|
function isInvalidXsrfStatus(status) {
|
||||||
logRequestFailure(requestEntry, "XSRF token cookie not found.");
|
return status === 400 || status === 401 || status === 403;
|
||||||
throw new Error("XSRF token cookie not found.");
|
}
|
||||||
|
|
||||||
|
function formatXsrfRefreshResult(result) {
|
||||||
|
if (!result) {
|
||||||
|
return "not-requested";
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const outcome = result.refreshed ? "refreshed" : "not-refreshed";
|
||||||
const response = await fetch(TOKEN_RELAY_URL, {
|
const cookieName = result.cookieName || "(none)";
|
||||||
method: "GET",
|
const duration = Number.isFinite(result.durationMs) ? `${result.durationMs}ms` : "unknown";
|
||||||
credentials: "include",
|
return `${result.reason}; ${outcome}; cookie=${cookieName}; duration=${duration}; ${result.error || ""}`.trim();
|
||||||
headers: {
|
|
||||||
"x-xsrf-token": xsrfTokenSource.token
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseBody = await readResponseBodyForDebug(response);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
throw new Error(`Token relay failed with status ${response.status}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let responseData;
|
|
||||||
|
|
||||||
try {
|
|
||||||
responseData = await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
throw new Error("Token relay response does not contain access_token.");
|
|
||||||
}
|
|
||||||
|
|
||||||
logRequestSuccess(requestEntry, response, createDebugBody(JSON.stringify({
|
|
||||||
...responseData,
|
|
||||||
access_token: maskToken(responseData.access_token)
|
|
||||||
}, null, 2)));
|
|
||||||
|
|
||||||
return responseData.access_token;
|
|
||||||
} catch (error) {
|
|
||||||
logRequestFailure(requestEntry, error.message || "Request failed.");
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestOpportunitiesForPeriod(period) {
|
async function requestOpportunitiesForPeriod(period) {
|
||||||
const dateRange = getFiscalPeriodRange(period, new Date());
|
const dateRange = getFiscalPeriodRange(period, new Date());
|
||||||
const requestVersion = ++periodRequestVersion;
|
const requestVersion = ++periodRequestVersion;
|
||||||
|
|
||||||
|
if (selectedOpportunityTypeValues.size === 0) {
|
||||||
|
setOpportunitiesTableState({
|
||||||
|
items: [],
|
||||||
|
status: "empty",
|
||||||
|
message: "Select at least one Opportunity Type View in Preferences."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!dateRange) {
|
if (!dateRange) {
|
||||||
setOpportunitiesTableState({
|
setOpportunitiesTableState({
|
||||||
items: [],
|
items: [],
|
||||||
@@ -1302,6 +1400,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (owner && typeof owner === "object") {
|
if (owner && typeof owner === "object") {
|
||||||
|
const firstName = owner.PersonFirstName;
|
||||||
|
const lastName = owner.PersonLastName;
|
||||||
|
|
||||||
|
if (firstName || lastName) {
|
||||||
|
return [firstName, lastName].filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
return owner.PartyName
|
return owner.PartyName
|
||||||
|| owner.PartyUniqueName
|
|| owner.PartyUniqueName
|
||||||
|| owner.Name
|
|| owner.Name
|
||||||
@@ -1435,7 +1540,7 @@
|
|||||||
const normalizedSearch = tableSearch.trim().toLocaleLowerCase();
|
const normalizedSearch = tableSearch.trim().toLocaleLowerCase();
|
||||||
const items = opportunitiesTableState.items.filter((item) => {
|
const items = opportunitiesTableState.items.filter((item) => {
|
||||||
return selectedStages.has(item.ForecastGroup_c)
|
return selectedStages.has(item.ForecastGroup_c)
|
||||||
&& selectedStatuses.has(item.StatusCode)
|
&& matchesSelectedStatus(item.StatusCode)
|
||||||
&& selectedCustomerKeys.has(getCustomerKey(item))
|
&& selectedCustomerKeys.has(getCustomerKey(item))
|
||||||
&& selectedOwnerKeys.has(getOwnerKey(item))
|
&& selectedOwnerKeys.has(getOwnerKey(item))
|
||||||
&& matchesTableSearch(item, normalizedSearch);
|
&& matchesTableSearch(item, normalizedSearch);
|
||||||
@@ -1488,6 +1593,14 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function matchesSelectedStatus(statusCode) {
|
||||||
|
if (statusCode === "WON_PENDING") {
|
||||||
|
return selectedStatuses.has("WON");
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedStatuses.has(statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
function stringifySearchValue(value) {
|
function stringifySearchValue(value) {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return "";
|
return "";
|
||||||
@@ -1679,10 +1792,11 @@
|
|||||||
criteria: [
|
criteria: [
|
||||||
{
|
{
|
||||||
op: "$or",
|
op: "$or",
|
||||||
criteria: [
|
criteria: Array.from(selectedOpportunityTypeValues, (value) => ({
|
||||||
{ op: "$eq", attribute: "RecordSet", value: "ORA_MYSALESTEAMOPTIES" },
|
op: "$eq",
|
||||||
{ op: "$eq", attribute: "RecordSet", value: "ORA_MYASSGTERROPTIES" }
|
attribute: "RecordSet",
|
||||||
]
|
value
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
op: "$wi",
|
op: "$wi",
|
||||||
@@ -2091,6 +2205,52 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshXsrfCookie(reason) {
|
||||||
|
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||||
|
|
||||||
|
if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
reason,
|
||||||
|
error: "Runtime messaging API unavailable."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
runtimeApi.runtime.sendMessage({
|
||||||
|
type: "opportunitiesExtension.refreshXsrfCookie"
|
||||||
|
}, (response) => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
resolve({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
reason,
|
||||||
|
error: lastError.message
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
...(response || {
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
error: "Empty refresh response."
|
||||||
|
}),
|
||||||
|
reason
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateAuthBadge(status) {
|
function updateAuthBadge(status) {
|
||||||
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);
|
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);
|
||||||
|
|
||||||
@@ -2115,6 +2275,7 @@
|
|||||||
const modal = document.getElementById(MODAL_ID);
|
const modal = document.getElementById(MODAL_ID);
|
||||||
|
|
||||||
if (modal) {
|
if (modal) {
|
||||||
|
closePreferencesModal();
|
||||||
modal.hidden = true;
|
modal.hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2168,6 +2329,185 @@
|
|||||||
return svg;
|
return svg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSettingsIcon() {
|
||||||
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
const upperControl = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
||||||
|
const lowerControl = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
||||||
|
|
||||||
|
svg.setAttribute("viewBox", "0 0 24 24");
|
||||||
|
svg.setAttribute("class", "opportunities-extension-settings-icon");
|
||||||
|
svg.setAttribute("aria-hidden", "true");
|
||||||
|
svg.setAttribute("focusable", "false");
|
||||||
|
appendPath(svg, "", "M4 7h16M4 17h16");
|
||||||
|
|
||||||
|
[upperControl, lowerControl].forEach((control, index) => {
|
||||||
|
control.setAttribute("cx", index === 0 ? "9" : "15");
|
||||||
|
control.setAttribute("cy", index === 0 ? "7" : "17");
|
||||||
|
control.setAttribute("r", "2.5");
|
||||||
|
control.setAttribute("fill", "currentColor");
|
||||||
|
control.setAttribute("stroke", "currentColor");
|
||||||
|
control.setAttribute("stroke-width", "1.8");
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.querySelector("path").setAttribute("fill", "none");
|
||||||
|
svg.querySelector("path").setAttribute("stroke", "currentColor");
|
||||||
|
svg.querySelector("path").setAttribute("stroke-linecap", "round");
|
||||||
|
svg.querySelector("path").setAttribute("stroke-width", "1.8");
|
||||||
|
svg.append(upperControl, lowerControl);
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCloseIcon() {
|
||||||
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||||
|
|
||||||
|
svg.setAttribute("viewBox", "0 0 24 24");
|
||||||
|
svg.setAttribute("aria-hidden", "true");
|
||||||
|
svg.setAttribute("focusable", "false");
|
||||||
|
path.setAttribute("d", "m6 6 12 12M18 6 6 18");
|
||||||
|
path.setAttribute("fill", "none");
|
||||||
|
path.setAttribute("stroke", "currentColor");
|
||||||
|
path.setAttribute("stroke-linecap", "round");
|
||||||
|
path.setAttribute("stroke-width", "1.8");
|
||||||
|
svg.append(path);
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSavedOpportunityTypeValues() {
|
||||||
|
try {
|
||||||
|
const rawPreferences = localStorage.getItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY);
|
||||||
|
|
||||||
|
if (!rawPreferences) {
|
||||||
|
return DEFAULT_OPPORTUNITY_TYPE_VALUES;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferences = JSON.parse(rawPreferences);
|
||||||
|
const values = Array.isArray(preferences.opportunityTypeValues) ? preferences.opportunityTypeValues : null;
|
||||||
|
|
||||||
|
if (!values) {
|
||||||
|
return DEFAULT_OPPORTUNITY_TYPE_VALUES;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedValues = new Set(OPPORTUNITY_TYPE_OPTIONS.map((option) => option.value));
|
||||||
|
return values.filter((value) => allowedValues.has(value));
|
||||||
|
} catch (error) {
|
||||||
|
return DEFAULT_OPPORTUNITY_TYPE_VALUES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPreferencesModal() {
|
||||||
|
const overlay = document.getElementById(MODAL_ID);
|
||||||
|
|
||||||
|
if (!overlay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingModal = document.getElementById(PREFERENCES_MODAL_ID);
|
||||||
|
|
||||||
|
if (existingModal) {
|
||||||
|
existingModal.hidden = false;
|
||||||
|
existingModal.querySelector("input")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferencesModal = document.createElement("section");
|
||||||
|
preferencesModal.id = PREFERENCES_MODAL_ID;
|
||||||
|
preferencesModal.className = "opportunities-extension-preferences-modal";
|
||||||
|
preferencesModal.setAttribute("role", "dialog");
|
||||||
|
preferencesModal.setAttribute("aria-modal", "true");
|
||||||
|
preferencesModal.setAttribute("aria-labelledby", "opportunities-extension-preferences-title");
|
||||||
|
|
||||||
|
const dialog = document.createElement("div");
|
||||||
|
dialog.className = "opportunities-extension-preferences-dialog";
|
||||||
|
|
||||||
|
const header = document.createElement("header");
|
||||||
|
header.className = "opportunities-extension-preferences-header";
|
||||||
|
|
||||||
|
const title = document.createElement("h2");
|
||||||
|
title.id = "opportunities-extension-preferences-title";
|
||||||
|
title.textContent = "Preferences";
|
||||||
|
|
||||||
|
const closeButton = document.createElement("button");
|
||||||
|
closeButton.type = "button";
|
||||||
|
closeButton.className = "opportunities-extension-icon-button";
|
||||||
|
closeButton.setAttribute("aria-label", "Fechar preferências");
|
||||||
|
closeButton.title = "Close";
|
||||||
|
closeButton.append(createCloseIcon());
|
||||||
|
closeButton.addEventListener("click", closePreferencesModal);
|
||||||
|
header.append(title, closeButton);
|
||||||
|
|
||||||
|
const content = document.createElement("div");
|
||||||
|
content.className = "opportunities-extension-preferences-content";
|
||||||
|
|
||||||
|
const fieldset = document.createElement("fieldset");
|
||||||
|
fieldset.className = "opportunities-extension-preferences-fieldset";
|
||||||
|
|
||||||
|
const legend = document.createElement("legend");
|
||||||
|
legend.textContent = "Opportunity Type View";
|
||||||
|
fieldset.append(legend);
|
||||||
|
|
||||||
|
const selectedValues = new Set(getSavedOpportunityTypeValues());
|
||||||
|
const options = document.createElement("div");
|
||||||
|
options.className = "opportunities-extension-preferences-options";
|
||||||
|
|
||||||
|
OPPORTUNITY_TYPE_OPTIONS.forEach((option) => {
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.className = "opportunities-extension-preferences-option";
|
||||||
|
|
||||||
|
const checkbox = document.createElement("input");
|
||||||
|
checkbox.type = "checkbox";
|
||||||
|
checkbox.name = "opportunityTypeView";
|
||||||
|
checkbox.value = option.value;
|
||||||
|
checkbox.checked = selectedValues.has(option.value);
|
||||||
|
|
||||||
|
const text = document.createElement("span");
|
||||||
|
text.textContent = option.label;
|
||||||
|
label.append(checkbox, text);
|
||||||
|
options.append(label);
|
||||||
|
});
|
||||||
|
|
||||||
|
fieldset.append(options);
|
||||||
|
content.append(fieldset);
|
||||||
|
|
||||||
|
const footer = document.createElement("footer");
|
||||||
|
footer.className = "opportunities-extension-preferences-footer";
|
||||||
|
|
||||||
|
const saveButton = document.createElement("button");
|
||||||
|
saveButton.type = "button";
|
||||||
|
saveButton.className = "opportunities-extension-button opportunities-extension-button-primary";
|
||||||
|
saveButton.textContent = "Save";
|
||||||
|
saveButton.addEventListener("click", () => {
|
||||||
|
const selectedValues = Array.from(preferencesModal.querySelectorAll("input[name='opportunityTypeView']:checked"), (checkbox) => checkbox.value);
|
||||||
|
selectedOpportunityTypeValues = new Set(selectedValues);
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY, JSON.stringify({
|
||||||
|
opportunityTypeValues: selectedValues
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
// Continue with the saved selection for the active modal when storage is unavailable.
|
||||||
|
}
|
||||||
|
|
||||||
|
closePreferencesModal();
|
||||||
|
const periodSelect = overlay.querySelector("select[name='opportunityPeriod']");
|
||||||
|
requestOpportunitiesForPeriod(periodSelect?.value || PERIOD_OPTIONS[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
footer.append(saveButton);
|
||||||
|
dialog.append(header, content, footer);
|
||||||
|
preferencesModal.append(dialog);
|
||||||
|
overlay.append(preferencesModal);
|
||||||
|
preferencesModal.querySelector("input")?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePreferencesModal() {
|
||||||
|
const preferencesModal = document.getElementById(PREFERENCES_MODAL_ID);
|
||||||
|
|
||||||
|
if (preferencesModal) {
|
||||||
|
preferencesModal.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ensureEscapeKeyHandler() {
|
function ensureEscapeKeyHandler() {
|
||||||
if (window[ESCAPE_LISTENER_KEY]) {
|
if (window[ESCAPE_LISTENER_KEY]) {
|
||||||
return;
|
return;
|
||||||
@@ -2189,6 +2529,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
|
const preferencesModal = document.getElementById(PREFERENCES_MODAL_ID);
|
||||||
|
|
||||||
|
if (preferencesModal && !preferencesModal.hidden) {
|
||||||
|
closePreferencesModal();
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (closeCustomerFilterPanel()) {
|
if (closeCustomerFilterPanel()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
@@ -2365,6 +2713,11 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-icon-button .opportunities-extension-settings-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-icon-button:hover,
|
.opportunities-extension-icon-button:hover,
|
||||||
.opportunities-extension-icon-button:focus {
|
.opportunities-extension-icon-button:focus {
|
||||||
border-color: #b8b2ad;
|
border-color: #b8b2ad;
|
||||||
@@ -2447,7 +2800,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-total-dashboard-card {
|
.opportunities-extension-total-dashboard-card {
|
||||||
border-top-color: #00758f;
|
border-top-color: #312d2a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-stage-dashboard-card[data-stage="PIPELINE"] {
|
.opportunities-extension-stage-dashboard-card[data-stage="PIPELINE"] {
|
||||||
@@ -2867,12 +3220,13 @@
|
|||||||
border: 1px solid #dedbd7;
|
border: 1px solid #dedbd7;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
overflow: auto;
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table {
|
.opportunities-extension-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 1440px;
|
min-width: 0;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
color: #312d2a;
|
color: #312d2a;
|
||||||
@@ -2907,7 +3261,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(1) {
|
.opportunities-extension-table th:nth-child(1) {
|
||||||
width: 14%;
|
width: 17%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(2) {
|
.opportunities-extension-table th:nth-child(2) {
|
||||||
@@ -2915,7 +3269,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(3) {
|
.opportunities-extension-table th:nth-child(3) {
|
||||||
width: 17%;
|
width: 19%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(4) {
|
.opportunities-extension-table th:nth-child(4) {
|
||||||
@@ -2923,19 +3277,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(5) {
|
.opportunities-extension-table th:nth-child(5) {
|
||||||
width: 9%;
|
width: 8%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(6) {
|
.opportunities-extension-table th:nth-child(6) {
|
||||||
width: 9%;
|
width: 8%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(7) {
|
.opportunities-extension-table th:nth-child(7) {
|
||||||
width: 8%;
|
width: 7%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(8) {
|
.opportunities-extension-table th:nth-child(8) {
|
||||||
width: 8%;
|
width: 6%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(9) {
|
.opportunities-extension-table th:nth-child(9) {
|
||||||
@@ -2949,10 +3303,30 @@
|
|||||||
.opportunities-extension-table td:nth-child(2),
|
.opportunities-extension-table td:nth-child(2),
|
||||||
.opportunities-extension-table td:nth-child(5),
|
.opportunities-extension-table td:nth-child(5),
|
||||||
.opportunities-extension-table td:nth-child(6),
|
.opportunities-extension-table td:nth-child(6),
|
||||||
.opportunities-extension-table td:nth-child(7),
|
.opportunities-extension-table td:nth-child(8) {
|
||||||
.opportunities-extension-table td:nth-child(8),
|
white-space: nowrap;
|
||||||
.opportunities-extension-table td:nth-child(9),
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table td:nth-child(9) {
|
||||||
|
padding-right: 6px;
|
||||||
|
padding-left: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table td:nth-child(7) {
|
||||||
|
padding-right: 6px;
|
||||||
|
padding-left: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-table td:nth-child(10) {
|
.opportunities-extension-table td:nth-child(10) {
|
||||||
|
padding-right: 6px;
|
||||||
|
padding-left: 6px;
|
||||||
|
font-size: 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3112,14 +3486,16 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
padding: 10px 30px 10px 16px;
|
padding: 8px 42px 8px 16px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
|
line-height: 1.2;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
white-space: normal;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3170,6 +3546,135 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-modal {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 3;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
background: rgba(0, 0, 0, .42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-modal[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-dialog {
|
||||||
|
display: grid;
|
||||||
|
width: min(680px, 100%);
|
||||||
|
max-height: min(720px, calc(100vh - 48px));
|
||||||
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #8f8a85;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, .24);
|
||||||
|
color: #312d2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
border-bottom: 1px solid #dedbd7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-content {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-fieldset {
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-fieldset legend {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 0;
|
||||||
|
color: #312d2a;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 4px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 9px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #312d2a;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.35;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-option:hover {
|
||||||
|
background: #f0eeeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-option input {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex: 0 0 18px;
|
||||||
|
margin: 0;
|
||||||
|
accent-color: #00758f;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 14px 20px;
|
||||||
|
border-top: 1px solid #dedbd7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-button {
|
||||||
|
min-width: 88px;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 7px 16px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-button-primary {
|
||||||
|
border-color: #00758f;
|
||||||
|
background: #00758f;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-button-primary:hover,
|
||||||
|
.opportunities-extension-button-primary:focus-visible {
|
||||||
|
border-color: #005e73;
|
||||||
|
background: #005e73;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-debug-panel {
|
.opportunities-extension-debug-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 24px;
|
right: 24px;
|
||||||
@@ -3368,6 +3873,7 @@
|
|||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table-surface,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table-surface,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-panel,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-panel,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-dialog,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item[open] {
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item[open] {
|
||||||
border-color: #4e4a46;
|
border-color: #4e4a46;
|
||||||
@@ -3405,6 +3911,9 @@
|
|||||||
|
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-select-all,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-select-all,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-header h2,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-fieldset legend,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-heading,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-heading,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item pre,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item pre,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel h2 {
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel h2 {
|
||||||
@@ -3416,6 +3925,30 @@
|
|||||||
border-bottom-color: #4e4a46;
|
border-bottom-color: #4e4a46;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-header,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-footer {
|
||||||
|
border-color: #4e4a46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option:hover {
|
||||||
|
background: #3b3937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option input {
|
||||||
|
accent-color: #43c4d5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary {
|
||||||
|
border-color: #00a6be;
|
||||||
|
background: #008aa6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary:hover,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary:focus-visible {
|
||||||
|
border-color: #43c4d5;
|
||||||
|
background: #006d7a;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option:hover,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option:hover,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table tbody tr:hover,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table tbody tr:hover,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sort-button:hover,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sort-button:hover,
|
||||||
@@ -3471,7 +4004,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-total-dashboard-card {
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-total-dashboard-card {
|
||||||
border-top-color: #43c4d5;
|
border-top-color: #f6f4f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card[data-stage="PIPELINE"] {
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card[data-stage="PIPELINE"] {
|
||||||
@@ -3569,6 +4102,62 @@
|
|||||||
color: #f6f4f2;
|
color: #f6f4f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1201px) and (max-width: 1450px) {
|
||||||
|
.opportunities-extension-table th:nth-child(1) {
|
||||||
|
width: 16%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(3) {
|
||||||
|
width: 18%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(4) {
|
||||||
|
width: 12%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(6) {
|
||||||
|
width: 7%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(7) {
|
||||||
|
width: 8%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(10) {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.opportunities-extension-table {
|
||||||
|
min-width: 1180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(1) {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(3) {
|
||||||
|
width: 17%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(4) {
|
||||||
|
width: 13%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(6) {
|
||||||
|
width: 6%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(7) {
|
||||||
|
width: 9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(10) {
|
||||||
|
width: 11%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.opportunities-extension-header {
|
.opportunities-extension-header {
|
||||||
padding: 24px 20px 18px;
|
padding: 24px 20px 18px;
|
||||||
@@ -3627,6 +4216,18 @@
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-modal {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-dialog {
|
||||||
|
max-height: calc(100vh - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-options {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-stage-filter-controls {
|
.opportunities-extension-stage-filter-controls {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|||||||
2
dist/firefox/manifest.json
vendored
2
dist/firefox/manifest.json
vendored
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Opportunities Extension",
|
"name": "Opportunities Extension",
|
||||||
"description": "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
|
"description": "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"cookies"
|
"cookies"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "opportunities-extension",
|
"name": "opportunities-extension",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const baseManifest = {
|
|||||||
manifest_version: 3,
|
manifest_version: 3,
|
||||||
name: "Opportunities Extension",
|
name: "Opportunities Extension",
|
||||||
description: "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
|
description: "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
|
||||||
version: "0.1.0",
|
version: "1.0.0",
|
||||||
permissions: [
|
permissions: [
|
||||||
"cookies"
|
"cookies"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -10,37 +10,181 @@
|
|||||||
];
|
];
|
||||||
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
|
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
|
||||||
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-";
|
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-";
|
||||||
|
const OPPORTUNITIES_LIST_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-list";
|
||||||
|
const COOKIE_REFRESH_TIMEOUT_MS = 15000;
|
||||||
|
const COOKIE_REFRESH_POLL_MS = 250;
|
||||||
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||||
|
|
||||||
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
if (!message || message.type !== "opportunitiesExtension.getXsrfToken") {
|
if (!message || !message.type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getXsrfTokenCookie()
|
if (message.type === "opportunitiesExtension.getXsrfToken") {
|
||||||
.then((result) => {
|
getXsrfTokenCookie()
|
||||||
sendResponse({
|
.then((result) => {
|
||||||
ok: true,
|
sendResponse({
|
||||||
cookieName: result.cookie ? result.cookie.name : "",
|
ok: true,
|
||||||
token: result.cookie ? result.cookie.value : "",
|
cookieName: result.cookie ? result.cookie.name : "",
|
||||||
matchedCookieNames: result.matchedCookieNames,
|
token: result.cookie ? result.cookie.value : "",
|
||||||
lookupDetails: result.lookupDetails
|
matchedCookieNames: result.matchedCookieNames,
|
||||||
|
lookupDetails: result.lookupDetails
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
cookieName: "",
|
||||||
|
token: "",
|
||||||
|
matchedCookieNames: [],
|
||||||
|
lookupDetails: [],
|
||||||
|
error: error.message || "Unable to read cookies."
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
sendResponse({
|
|
||||||
ok: false,
|
|
||||||
cookieName: "",
|
|
||||||
token: "",
|
|
||||||
matchedCookieNames: [],
|
|
||||||
lookupDetails: [],
|
|
||||||
error: error.message || "Unable to read cookies."
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "opportunitiesExtension.refreshXsrfCookie") {
|
||||||
|
refreshXsrfCookie(sender)
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
error: error.message || "Unable to refresh XSRF cookie."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function refreshXsrfCookie(sender) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const previous = (await getXsrfTokenCookie()).cookie;
|
||||||
|
const createProperties = {
|
||||||
|
url: OPPORTUNITIES_LIST_URL,
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sender && sender.tab && Number.isInteger(sender.tab.windowId)) {
|
||||||
|
createProperties.windowId = sender.tab.windowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsFirefoxCookieStoreTabs() && sender && sender.tab && sender.tab.cookieStoreId) {
|
||||||
|
createProperties.cookieStoreId = sender.tab.cookieStoreId;
|
||||||
|
}
|
||||||
|
|
||||||
|
let refreshTab;
|
||||||
|
|
||||||
|
try {
|
||||||
|
refreshTab = await tabsCreate(createProperties);
|
||||||
|
const current = await waitForXsrfCookieChange(previous);
|
||||||
|
const refreshed = hasCookieChanged(previous, current);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: Boolean(current),
|
||||||
|
refreshed,
|
||||||
|
cookieName: current ? current.name : "",
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
error: refreshed ? "" : "XSRF cookie was not created or changed before timeout."
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
if (refreshTab && Number.isInteger(refreshTab.id)) {
|
||||||
|
try {
|
||||||
|
await tabsRemove(refreshTab.id);
|
||||||
|
} catch (error) {
|
||||||
|
// The temporary tab may already have been closed.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForXsrfCookieChange(previousCookie) {
|
||||||
|
const deadline = Date.now() + COOKIE_REFRESH_TIMEOUT_MS;
|
||||||
|
let currentCookie = null;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
currentCookie = (await getXsrfTokenCookie()).cookie;
|
||||||
|
|
||||||
|
if (hasCookieChanged(previousCookie, currentCookie)) {
|
||||||
|
return currentCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
await wait(COOKIE_REFRESH_POLL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCookieChanged(previousCookie, currentCookie) {
|
||||||
|
if (!currentCookie || !currentCookie.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previousCookie) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return previousCookie.name !== currentCookie.name
|
||||||
|
|| previousCookie.value !== currentCookie.value
|
||||||
|
|| previousCookie.expirationDate !== currentCookie.expirationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportsFirefoxCookieStoreTabs() {
|
||||||
|
return Boolean(
|
||||||
|
runtimeApi.runtime
|
||||||
|
&& typeof runtimeApi.runtime.getBrowserInfo === "function"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabsCreate(details) {
|
||||||
|
if (runtimeApi.tabs.create.length <= 1) {
|
||||||
|
return runtimeApi.tabs.create(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
runtimeApi.tabs.create(details, (tab) => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
reject(new Error(lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(tab);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabsRemove(tabId) {
|
||||||
|
if (runtimeApi.tabs.remove.length <= 1) {
|
||||||
|
return runtimeApi.tabs.remove(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
runtimeApi.tabs.remove(tabId, () => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
reject(new Error(lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function wait(durationMs) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, durationMs));
|
||||||
|
}
|
||||||
|
|
||||||
async function getXsrfTokenCookie() {
|
async function getXsrfTokenCookie() {
|
||||||
const lookupDetails = [];
|
const lookupDetails = [];
|
||||||
const allCookies = [];
|
const allCookies = [];
|
||||||
|
|||||||
741
src/content.js
741
src/content.js
@@ -26,6 +26,8 @@
|
|||||||
const STAGE_DASHBOARD_ID = "opportunities-extension-stage-dashboard";
|
const STAGE_DASHBOARD_ID = "opportunities-extension-stage-dashboard";
|
||||||
const FILTER_PREFERENCES_STORAGE_KEY = "opportunities-extension-filter-preferences";
|
const FILTER_PREFERENCES_STORAGE_KEY = "opportunities-extension-filter-preferences";
|
||||||
const THEME_STORAGE_KEY = "opportunities-extension-theme";
|
const THEME_STORAGE_KEY = "opportunities-extension-theme";
|
||||||
|
const OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY = "opportunities-extension-opportunity-type-preferences";
|
||||||
|
const PREFERENCES_MODAL_ID = "opportunities-extension-preferences-modal";
|
||||||
const OPPORTUNITY_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?puid=";
|
const OPPORTUNITY_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?puid=";
|
||||||
const ACCOUNT_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/accounts/accounts-detail?id=";
|
const ACCOUNT_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/accounts/accounts-detail?id=";
|
||||||
const DEBUG_BODY_LIMIT = 12000;
|
const DEBUG_BODY_LIMIT = 12000;
|
||||||
@@ -40,6 +42,24 @@
|
|||||||
};
|
};
|
||||||
const STAGE_OPTIONS = ["SQL", "PIPELINE", "UPSIDE", "FORECAST", "WON"];
|
const STAGE_OPTIONS = ["SQL", "PIPELINE", "UPSIDE", "FORECAST", "WON"];
|
||||||
const STATUS_OPTIONS = ["OPEN", "CLOSED", "LOST", "WON"];
|
const STATUS_OPTIONS = ["OPEN", "CLOSED", "LOST", "WON"];
|
||||||
|
const OPPORTUNITY_TYPE_OPTIONS = [
|
||||||
|
{ label: "All records I can see", value: "ORA_ALLOPTIES" },
|
||||||
|
{ label: "I am credit receiver", value: "ORA_CREDITRECEIVER_ISME" },
|
||||||
|
{ label: "I am on the team", value: "ORA_MYSALESTEAMOPTIES" },
|
||||||
|
{ label: "I or my subordinate is a credit receiver", value: "ORA_CREDITRECEIVER_MYORG" },
|
||||||
|
{ label: "I own", value: "ORA_MYOPTIES" },
|
||||||
|
{ label: "My subordinates are on the team", value: "ORA_MYSUBORDSSALESTEAMOPTIES" },
|
||||||
|
{ label: "My subordinates own", value: "ORA_MYSUBORDINATESOPTIES" },
|
||||||
|
{ label: "My territory", value: "ORA_MYASSGTERROPTIES" },
|
||||||
|
{ label: "My territory hierarchy", value: "ORA_MYASSGDESCTERROPTIES" },
|
||||||
|
{ label: "My territory membership", value: "ORA_MYASSGMEMBTERROPTIES" },
|
||||||
|
{ label: "My territory membership hierarchy", value: "ORA_MYASSGMEMBDESCTERROPTIES" }
|
||||||
|
];
|
||||||
|
const DEFAULT_OPPORTUNITY_TYPE_VALUES = [
|
||||||
|
"ORA_MYSALESTEAMOPTIES",
|
||||||
|
"ORA_MYASSGTERROPTIES",
|
||||||
|
"ORA_CREDITRECEIVER_ISME"
|
||||||
|
];
|
||||||
let tokenRelayRequest = null;
|
let tokenRelayRequest = null;
|
||||||
let accessToken = "";
|
let accessToken = "";
|
||||||
let periodRequestVersion = 0;
|
let periodRequestVersion = 0;
|
||||||
@@ -52,6 +72,7 @@
|
|||||||
let selectedOwnerKeys = new Set();
|
let selectedOwnerKeys = new Set();
|
||||||
let ownerSearch = "";
|
let ownerSearch = "";
|
||||||
let tableSearch = "";
|
let tableSearch = "";
|
||||||
|
let selectedOpportunityTypeValues = new Set(DEFAULT_OPPORTUNITY_TYPE_VALUES);
|
||||||
let hasSavedCustomerFilter = false;
|
let hasSavedCustomerFilter = false;
|
||||||
let hasSavedOwnerFilter = false;
|
let hasSavedOwnerFilter = false;
|
||||||
let customerFilterInitialized = false;
|
let customerFilterInitialized = false;
|
||||||
@@ -79,9 +100,9 @@
|
|||||||
{ key: "owner", label: "Owner", value: getOwnerName, display: getOwnerName },
|
{ key: "owner", label: "Owner", value: getOwnerName, display: getOwnerName },
|
||||||
{ key: "revenue", label: "Amount", value: (item) => getNestedValue(item, ["PrimaryRevenue", "RevnAmount"]), display: formatRevenue },
|
{ key: "revenue", label: "Amount", value: (item) => getNestedValue(item, ["PrimaryRevenue", "RevnAmount"]), display: formatRevenue },
|
||||||
{ key: "closeDate", label: "Close Date", value: (item) => item.EffectiveDate, display: (item) => formatOracleResponseDate(item.EffectiveDate) },
|
{ key: "closeDate", label: "Close Date", value: (item) => item.EffectiveDate, display: (item) => formatOracleResponseDate(item.EffectiveDate) },
|
||||||
{ key: "stage", label: "Stage", value: (item) => item.ForecastGroup_c, display: (item) => item.ForecastGroup_c },
|
|
||||||
{ key: "winProbability", label: "Win Probability", value: (item) => getNestedValue(item, ["PrimaryRevenue", "WinProb"]), display: formatWinProbability },
|
|
||||||
{ key: "status", label: "Status", value: (item) => item.StatusCode, display: (item) => item.StatusCode },
|
{ key: "status", label: "Status", value: (item) => item.StatusCode, display: (item) => item.StatusCode },
|
||||||
|
{ key: "winProbability", label: "Win Probability", value: (item) => getNestedValue(item, ["PrimaryRevenue", "WinProb"]), display: formatWinProbability },
|
||||||
|
{ key: "stage", label: "Stage", value: (item) => item.ForecastGroup_c, display: (item) => item.ForecastGroup_c },
|
||||||
{ key: "lastUpdateDate", label: "Last Update Date", value: (item) => item.LastUpdateDate, display: (item) => formatOracleResponseDate(item.LastUpdateDate, true) }
|
{ key: "lastUpdateDate", label: "Last Update Date", value: (item) => item.LastUpdateDate, display: (item) => formatOracleResponseDate(item.LastUpdateDate, true) }
|
||||||
];
|
];
|
||||||
const ALLOWED_PATHS = [
|
const ALLOWED_PATHS = [
|
||||||
@@ -176,6 +197,7 @@
|
|||||||
periodRequestVersion = 0;
|
periodRequestVersion = 0;
|
||||||
authStatus = AUTH_STATUS.idle;
|
authStatus = AUTH_STATUS.idle;
|
||||||
resetOpportunitiesTable();
|
resetOpportunitiesTable();
|
||||||
|
selectedOpportunityTypeValues = new Set(getSavedOpportunityTypeValues());
|
||||||
overlay.id = MODAL_ID;
|
overlay.id = MODAL_ID;
|
||||||
overlay.className = "opportunities-extension-modal";
|
overlay.className = "opportunities-extension-modal";
|
||||||
overlay.setAttribute("data-theme", getSavedTheme());
|
overlay.setAttribute("data-theme", getSavedTheme());
|
||||||
@@ -226,9 +248,17 @@
|
|||||||
});
|
});
|
||||||
applyTheme(overlay, themeButton, overlay.getAttribute("data-theme"));
|
applyTheme(overlay, themeButton, overlay.getAttribute("data-theme"));
|
||||||
|
|
||||||
|
const preferencesButton = document.createElement("button");
|
||||||
|
preferencesButton.type = "button";
|
||||||
|
preferencesButton.className = "opportunities-extension-icon-button";
|
||||||
|
preferencesButton.setAttribute("aria-label", "Abrir preferências");
|
||||||
|
preferencesButton.title = "Preferences";
|
||||||
|
preferencesButton.append(createSettingsIcon());
|
||||||
|
preferencesButton.addEventListener("click", openPreferencesModal);
|
||||||
|
|
||||||
const headerActions = document.createElement("div");
|
const headerActions = document.createElement("div");
|
||||||
headerActions.className = "opportunities-extension-header-actions";
|
headerActions.className = "opportunities-extension-header-actions";
|
||||||
headerActions.append(themeButton, closeButton);
|
headerActions.append(themeButton, preferencesButton, closeButton);
|
||||||
|
|
||||||
header.append(titleBlock, headerActions);
|
header.append(titleBlock, headerActions);
|
||||||
|
|
||||||
@@ -242,7 +272,7 @@
|
|||||||
field.className = "opportunities-extension-field";
|
field.className = "opportunities-extension-field";
|
||||||
|
|
||||||
const labelText = document.createElement("span");
|
const labelText = document.createElement("span");
|
||||||
labelText.textContent = "Periodo";
|
labelText.textContent = "Reference";
|
||||||
|
|
||||||
const selectWrap = document.createElement("span");
|
const selectWrap = document.createElement("span");
|
||||||
selectWrap.className = "opportunities-extension-select-wrap";
|
selectWrap.className = "opportunities-extension-select-wrap";
|
||||||
@@ -502,8 +532,98 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function requestTokenRelay() {
|
async function requestTokenRelay() {
|
||||||
const xsrfTokenSource = await getXsrfToken();
|
let refreshResult = null;
|
||||||
const requestEntry = logRequestStart({
|
let refreshAttempted = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const xsrfTokenSource = await getXsrfToken();
|
||||||
|
|
||||||
|
if (!xsrfTokenSource.token) {
|
||||||
|
if (refreshAttempted) {
|
||||||
|
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult);
|
||||||
|
logRequestFailure(requestEntry, "XSRF token cookie not found after automatic refresh.");
|
||||||
|
throw new Error("XSRF token cookie not found after automatic refresh.");
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshResult = await refreshXsrfCookie("missing-cookie");
|
||||||
|
refreshAttempted = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(TOKEN_RELAY_URL, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"x-xsrf-token": xsrfTokenSource.token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseBody = await readResponseBodyForDebug(response);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted && isInvalidXsrfStatus(response.status)) {
|
||||||
|
refreshResult = await refreshXsrfCookie(`tokenrelay-${response.status}`);
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Token relay failed with status ${response.status}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
responseData = await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted) {
|
||||||
|
refreshResult = await refreshXsrfCookie("tokenrelay-invalid-response");
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
||||||
|
logRequestSuccess(requestEntry, response, responseBody);
|
||||||
|
|
||||||
|
if (!refreshAttempted) {
|
||||||
|
refreshResult = await refreshXsrfCookie("tokenrelay-missing-access-token");
|
||||||
|
refreshAttempted = true;
|
||||||
|
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||||
|
renderDebugPanel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Token relay response does not contain access_token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
logRequestSuccess(requestEntry, response, createDebugBody(JSON.stringify({
|
||||||
|
...responseData,
|
||||||
|
access_token: maskToken(responseData.access_token)
|
||||||
|
}, null, 2)));
|
||||||
|
|
||||||
|
return responseData.access_token;
|
||||||
|
} catch (error) {
|
||||||
|
logRequestFailure(requestEntry, error.message || "Request failed.");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTokenRelayRequestEntry(xsrfTokenSource, refreshResult) {
|
||||||
|
return logRequestStart({
|
||||||
name: "requestTokenRelay",
|
name: "requestTokenRelay",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: TOKEN_RELAY_URL,
|
url: TOKEN_RELAY_URL,
|
||||||
@@ -519,62 +639,40 @@
|
|||||||
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
||||||
? xsrfTokenSource.lookupDetails.join(" | ")
|
? xsrfTokenSource.lookupDetails.join(" | ")
|
||||||
: "(none)",
|
: "(none)",
|
||||||
xsrfTokenError: xsrfTokenSource.error || ""
|
xsrfTokenError: xsrfTokenSource.error || "",
|
||||||
|
xsrfRefresh: formatXsrfRefreshResult(refreshResult)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!xsrfTokenSource.token) {
|
function isInvalidXsrfStatus(status) {
|
||||||
logRequestFailure(requestEntry, "XSRF token cookie not found.");
|
return status === 400 || status === 401 || status === 403;
|
||||||
throw new Error("XSRF token cookie not found.");
|
}
|
||||||
|
|
||||||
|
function formatXsrfRefreshResult(result) {
|
||||||
|
if (!result) {
|
||||||
|
return "not-requested";
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const outcome = result.refreshed ? "refreshed" : "not-refreshed";
|
||||||
const response = await fetch(TOKEN_RELAY_URL, {
|
const cookieName = result.cookieName || "(none)";
|
||||||
method: "GET",
|
const duration = Number.isFinite(result.durationMs) ? `${result.durationMs}ms` : "unknown";
|
||||||
credentials: "include",
|
return `${result.reason}; ${outcome}; cookie=${cookieName}; duration=${duration}; ${result.error || ""}`.trim();
|
||||||
headers: {
|
|
||||||
"x-xsrf-token": xsrfTokenSource.token
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseBody = await readResponseBodyForDebug(response);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
throw new Error(`Token relay failed with status ${response.status}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let responseData;
|
|
||||||
|
|
||||||
try {
|
|
||||||
responseData = await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
|
||||||
logRequestSuccess(requestEntry, response, responseBody);
|
|
||||||
throw new Error("Token relay response does not contain access_token.");
|
|
||||||
}
|
|
||||||
|
|
||||||
logRequestSuccess(requestEntry, response, createDebugBody(JSON.stringify({
|
|
||||||
...responseData,
|
|
||||||
access_token: maskToken(responseData.access_token)
|
|
||||||
}, null, 2)));
|
|
||||||
|
|
||||||
return responseData.access_token;
|
|
||||||
} catch (error) {
|
|
||||||
logRequestFailure(requestEntry, error.message || "Request failed.");
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestOpportunitiesForPeriod(period) {
|
async function requestOpportunitiesForPeriod(period) {
|
||||||
const dateRange = getFiscalPeriodRange(period, new Date());
|
const dateRange = getFiscalPeriodRange(period, new Date());
|
||||||
const requestVersion = ++periodRequestVersion;
|
const requestVersion = ++periodRequestVersion;
|
||||||
|
|
||||||
|
if (selectedOpportunityTypeValues.size === 0) {
|
||||||
|
setOpportunitiesTableState({
|
||||||
|
items: [],
|
||||||
|
status: "empty",
|
||||||
|
message: "Select at least one Opportunity Type View in Preferences."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!dateRange) {
|
if (!dateRange) {
|
||||||
setOpportunitiesTableState({
|
setOpportunitiesTableState({
|
||||||
items: [],
|
items: [],
|
||||||
@@ -1302,6 +1400,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (owner && typeof owner === "object") {
|
if (owner && typeof owner === "object") {
|
||||||
|
const firstName = owner.PersonFirstName;
|
||||||
|
const lastName = owner.PersonLastName;
|
||||||
|
|
||||||
|
if (firstName || lastName) {
|
||||||
|
return [firstName, lastName].filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
return owner.PartyName
|
return owner.PartyName
|
||||||
|| owner.PartyUniqueName
|
|| owner.PartyUniqueName
|
||||||
|| owner.Name
|
|| owner.Name
|
||||||
@@ -1435,7 +1540,7 @@
|
|||||||
const normalizedSearch = tableSearch.trim().toLocaleLowerCase();
|
const normalizedSearch = tableSearch.trim().toLocaleLowerCase();
|
||||||
const items = opportunitiesTableState.items.filter((item) => {
|
const items = opportunitiesTableState.items.filter((item) => {
|
||||||
return selectedStages.has(item.ForecastGroup_c)
|
return selectedStages.has(item.ForecastGroup_c)
|
||||||
&& selectedStatuses.has(item.StatusCode)
|
&& matchesSelectedStatus(item.StatusCode)
|
||||||
&& selectedCustomerKeys.has(getCustomerKey(item))
|
&& selectedCustomerKeys.has(getCustomerKey(item))
|
||||||
&& selectedOwnerKeys.has(getOwnerKey(item))
|
&& selectedOwnerKeys.has(getOwnerKey(item))
|
||||||
&& matchesTableSearch(item, normalizedSearch);
|
&& matchesTableSearch(item, normalizedSearch);
|
||||||
@@ -1488,6 +1593,14 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function matchesSelectedStatus(statusCode) {
|
||||||
|
if (statusCode === "WON_PENDING") {
|
||||||
|
return selectedStatuses.has("WON");
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedStatuses.has(statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
function stringifySearchValue(value) {
|
function stringifySearchValue(value) {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return "";
|
return "";
|
||||||
@@ -1679,10 +1792,11 @@
|
|||||||
criteria: [
|
criteria: [
|
||||||
{
|
{
|
||||||
op: "$or",
|
op: "$or",
|
||||||
criteria: [
|
criteria: Array.from(selectedOpportunityTypeValues, (value) => ({
|
||||||
{ op: "$eq", attribute: "RecordSet", value: "ORA_MYSALESTEAMOPTIES" },
|
op: "$eq",
|
||||||
{ op: "$eq", attribute: "RecordSet", value: "ORA_MYASSGTERROPTIES" }
|
attribute: "RecordSet",
|
||||||
]
|
value
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
op: "$wi",
|
op: "$wi",
|
||||||
@@ -2091,6 +2205,52 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshXsrfCookie(reason) {
|
||||||
|
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||||
|
|
||||||
|
if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
reason,
|
||||||
|
error: "Runtime messaging API unavailable."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
runtimeApi.runtime.sendMessage({
|
||||||
|
type: "opportunitiesExtension.refreshXsrfCookie"
|
||||||
|
}, (response) => {
|
||||||
|
const lastError = runtimeApi.runtime.lastError;
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
resolve({
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
reason,
|
||||||
|
error: lastError.message
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
...(response || {
|
||||||
|
ok: false,
|
||||||
|
refreshed: false,
|
||||||
|
cookieName: "",
|
||||||
|
durationMs: 0,
|
||||||
|
error: "Empty refresh response."
|
||||||
|
}),
|
||||||
|
reason
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateAuthBadge(status) {
|
function updateAuthBadge(status) {
|
||||||
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);
|
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);
|
||||||
|
|
||||||
@@ -2115,6 +2275,7 @@
|
|||||||
const modal = document.getElementById(MODAL_ID);
|
const modal = document.getElementById(MODAL_ID);
|
||||||
|
|
||||||
if (modal) {
|
if (modal) {
|
||||||
|
closePreferencesModal();
|
||||||
modal.hidden = true;
|
modal.hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2168,6 +2329,185 @@
|
|||||||
return svg;
|
return svg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSettingsIcon() {
|
||||||
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
const upperControl = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
||||||
|
const lowerControl = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
||||||
|
|
||||||
|
svg.setAttribute("viewBox", "0 0 24 24");
|
||||||
|
svg.setAttribute("class", "opportunities-extension-settings-icon");
|
||||||
|
svg.setAttribute("aria-hidden", "true");
|
||||||
|
svg.setAttribute("focusable", "false");
|
||||||
|
appendPath(svg, "", "M4 7h16M4 17h16");
|
||||||
|
|
||||||
|
[upperControl, lowerControl].forEach((control, index) => {
|
||||||
|
control.setAttribute("cx", index === 0 ? "9" : "15");
|
||||||
|
control.setAttribute("cy", index === 0 ? "7" : "17");
|
||||||
|
control.setAttribute("r", "2.5");
|
||||||
|
control.setAttribute("fill", "currentColor");
|
||||||
|
control.setAttribute("stroke", "currentColor");
|
||||||
|
control.setAttribute("stroke-width", "1.8");
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.querySelector("path").setAttribute("fill", "none");
|
||||||
|
svg.querySelector("path").setAttribute("stroke", "currentColor");
|
||||||
|
svg.querySelector("path").setAttribute("stroke-linecap", "round");
|
||||||
|
svg.querySelector("path").setAttribute("stroke-width", "1.8");
|
||||||
|
svg.append(upperControl, lowerControl);
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCloseIcon() {
|
||||||
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||||
|
|
||||||
|
svg.setAttribute("viewBox", "0 0 24 24");
|
||||||
|
svg.setAttribute("aria-hidden", "true");
|
||||||
|
svg.setAttribute("focusable", "false");
|
||||||
|
path.setAttribute("d", "m6 6 12 12M18 6 6 18");
|
||||||
|
path.setAttribute("fill", "none");
|
||||||
|
path.setAttribute("stroke", "currentColor");
|
||||||
|
path.setAttribute("stroke-linecap", "round");
|
||||||
|
path.setAttribute("stroke-width", "1.8");
|
||||||
|
svg.append(path);
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSavedOpportunityTypeValues() {
|
||||||
|
try {
|
||||||
|
const rawPreferences = localStorage.getItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY);
|
||||||
|
|
||||||
|
if (!rawPreferences) {
|
||||||
|
return DEFAULT_OPPORTUNITY_TYPE_VALUES;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferences = JSON.parse(rawPreferences);
|
||||||
|
const values = Array.isArray(preferences.opportunityTypeValues) ? preferences.opportunityTypeValues : null;
|
||||||
|
|
||||||
|
if (!values) {
|
||||||
|
return DEFAULT_OPPORTUNITY_TYPE_VALUES;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedValues = new Set(OPPORTUNITY_TYPE_OPTIONS.map((option) => option.value));
|
||||||
|
return values.filter((value) => allowedValues.has(value));
|
||||||
|
} catch (error) {
|
||||||
|
return DEFAULT_OPPORTUNITY_TYPE_VALUES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPreferencesModal() {
|
||||||
|
const overlay = document.getElementById(MODAL_ID);
|
||||||
|
|
||||||
|
if (!overlay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingModal = document.getElementById(PREFERENCES_MODAL_ID);
|
||||||
|
|
||||||
|
if (existingModal) {
|
||||||
|
existingModal.hidden = false;
|
||||||
|
existingModal.querySelector("input")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferencesModal = document.createElement("section");
|
||||||
|
preferencesModal.id = PREFERENCES_MODAL_ID;
|
||||||
|
preferencesModal.className = "opportunities-extension-preferences-modal";
|
||||||
|
preferencesModal.setAttribute("role", "dialog");
|
||||||
|
preferencesModal.setAttribute("aria-modal", "true");
|
||||||
|
preferencesModal.setAttribute("aria-labelledby", "opportunities-extension-preferences-title");
|
||||||
|
|
||||||
|
const dialog = document.createElement("div");
|
||||||
|
dialog.className = "opportunities-extension-preferences-dialog";
|
||||||
|
|
||||||
|
const header = document.createElement("header");
|
||||||
|
header.className = "opportunities-extension-preferences-header";
|
||||||
|
|
||||||
|
const title = document.createElement("h2");
|
||||||
|
title.id = "opportunities-extension-preferences-title";
|
||||||
|
title.textContent = "Preferences";
|
||||||
|
|
||||||
|
const closeButton = document.createElement("button");
|
||||||
|
closeButton.type = "button";
|
||||||
|
closeButton.className = "opportunities-extension-icon-button";
|
||||||
|
closeButton.setAttribute("aria-label", "Fechar preferências");
|
||||||
|
closeButton.title = "Close";
|
||||||
|
closeButton.append(createCloseIcon());
|
||||||
|
closeButton.addEventListener("click", closePreferencesModal);
|
||||||
|
header.append(title, closeButton);
|
||||||
|
|
||||||
|
const content = document.createElement("div");
|
||||||
|
content.className = "opportunities-extension-preferences-content";
|
||||||
|
|
||||||
|
const fieldset = document.createElement("fieldset");
|
||||||
|
fieldset.className = "opportunities-extension-preferences-fieldset";
|
||||||
|
|
||||||
|
const legend = document.createElement("legend");
|
||||||
|
legend.textContent = "Opportunity Type View";
|
||||||
|
fieldset.append(legend);
|
||||||
|
|
||||||
|
const selectedValues = new Set(getSavedOpportunityTypeValues());
|
||||||
|
const options = document.createElement("div");
|
||||||
|
options.className = "opportunities-extension-preferences-options";
|
||||||
|
|
||||||
|
OPPORTUNITY_TYPE_OPTIONS.forEach((option) => {
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.className = "opportunities-extension-preferences-option";
|
||||||
|
|
||||||
|
const checkbox = document.createElement("input");
|
||||||
|
checkbox.type = "checkbox";
|
||||||
|
checkbox.name = "opportunityTypeView";
|
||||||
|
checkbox.value = option.value;
|
||||||
|
checkbox.checked = selectedValues.has(option.value);
|
||||||
|
|
||||||
|
const text = document.createElement("span");
|
||||||
|
text.textContent = option.label;
|
||||||
|
label.append(checkbox, text);
|
||||||
|
options.append(label);
|
||||||
|
});
|
||||||
|
|
||||||
|
fieldset.append(options);
|
||||||
|
content.append(fieldset);
|
||||||
|
|
||||||
|
const footer = document.createElement("footer");
|
||||||
|
footer.className = "opportunities-extension-preferences-footer";
|
||||||
|
|
||||||
|
const saveButton = document.createElement("button");
|
||||||
|
saveButton.type = "button";
|
||||||
|
saveButton.className = "opportunities-extension-button opportunities-extension-button-primary";
|
||||||
|
saveButton.textContent = "Save";
|
||||||
|
saveButton.addEventListener("click", () => {
|
||||||
|
const selectedValues = Array.from(preferencesModal.querySelectorAll("input[name='opportunityTypeView']:checked"), (checkbox) => checkbox.value);
|
||||||
|
selectedOpportunityTypeValues = new Set(selectedValues);
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY, JSON.stringify({
|
||||||
|
opportunityTypeValues: selectedValues
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
// Continue with the saved selection for the active modal when storage is unavailable.
|
||||||
|
}
|
||||||
|
|
||||||
|
closePreferencesModal();
|
||||||
|
const periodSelect = overlay.querySelector("select[name='opportunityPeriod']");
|
||||||
|
requestOpportunitiesForPeriod(periodSelect?.value || PERIOD_OPTIONS[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
footer.append(saveButton);
|
||||||
|
dialog.append(header, content, footer);
|
||||||
|
preferencesModal.append(dialog);
|
||||||
|
overlay.append(preferencesModal);
|
||||||
|
preferencesModal.querySelector("input")?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePreferencesModal() {
|
||||||
|
const preferencesModal = document.getElementById(PREFERENCES_MODAL_ID);
|
||||||
|
|
||||||
|
if (preferencesModal) {
|
||||||
|
preferencesModal.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ensureEscapeKeyHandler() {
|
function ensureEscapeKeyHandler() {
|
||||||
if (window[ESCAPE_LISTENER_KEY]) {
|
if (window[ESCAPE_LISTENER_KEY]) {
|
||||||
return;
|
return;
|
||||||
@@ -2189,6 +2529,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
|
const preferencesModal = document.getElementById(PREFERENCES_MODAL_ID);
|
||||||
|
|
||||||
|
if (preferencesModal && !preferencesModal.hidden) {
|
||||||
|
closePreferencesModal();
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (closeCustomerFilterPanel()) {
|
if (closeCustomerFilterPanel()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
@@ -2365,6 +2713,11 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-icon-button .opportunities-extension-settings-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-icon-button:hover,
|
.opportunities-extension-icon-button:hover,
|
||||||
.opportunities-extension-icon-button:focus {
|
.opportunities-extension-icon-button:focus {
|
||||||
border-color: #b8b2ad;
|
border-color: #b8b2ad;
|
||||||
@@ -2447,7 +2800,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-total-dashboard-card {
|
.opportunities-extension-total-dashboard-card {
|
||||||
border-top-color: #00758f;
|
border-top-color: #312d2a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-stage-dashboard-card[data-stage="PIPELINE"] {
|
.opportunities-extension-stage-dashboard-card[data-stage="PIPELINE"] {
|
||||||
@@ -2867,12 +3220,13 @@
|
|||||||
border: 1px solid #dedbd7;
|
border: 1px solid #dedbd7;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
overflow: auto;
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table {
|
.opportunities-extension-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 1440px;
|
min-width: 0;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
color: #312d2a;
|
color: #312d2a;
|
||||||
@@ -2907,7 +3261,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(1) {
|
.opportunities-extension-table th:nth-child(1) {
|
||||||
width: 14%;
|
width: 17%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(2) {
|
.opportunities-extension-table th:nth-child(2) {
|
||||||
@@ -2915,7 +3269,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(3) {
|
.opportunities-extension-table th:nth-child(3) {
|
||||||
width: 17%;
|
width: 19%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(4) {
|
.opportunities-extension-table th:nth-child(4) {
|
||||||
@@ -2923,19 +3277,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(5) {
|
.opportunities-extension-table th:nth-child(5) {
|
||||||
width: 9%;
|
width: 8%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(6) {
|
.opportunities-extension-table th:nth-child(6) {
|
||||||
width: 9%;
|
width: 8%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(7) {
|
.opportunities-extension-table th:nth-child(7) {
|
||||||
width: 8%;
|
width: 7%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(8) {
|
.opportunities-extension-table th:nth-child(8) {
|
||||||
width: 8%;
|
width: 6%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-table th:nth-child(9) {
|
.opportunities-extension-table th:nth-child(9) {
|
||||||
@@ -2949,10 +3303,30 @@
|
|||||||
.opportunities-extension-table td:nth-child(2),
|
.opportunities-extension-table td:nth-child(2),
|
||||||
.opportunities-extension-table td:nth-child(5),
|
.opportunities-extension-table td:nth-child(5),
|
||||||
.opportunities-extension-table td:nth-child(6),
|
.opportunities-extension-table td:nth-child(6),
|
||||||
.opportunities-extension-table td:nth-child(7),
|
.opportunities-extension-table td:nth-child(8) {
|
||||||
.opportunities-extension-table td:nth-child(8),
|
white-space: nowrap;
|
||||||
.opportunities-extension-table td:nth-child(9),
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table td:nth-child(9) {
|
||||||
|
padding-right: 6px;
|
||||||
|
padding-left: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table td:nth-child(7) {
|
||||||
|
padding-right: 6px;
|
||||||
|
padding-left: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-table td:nth-child(10) {
|
.opportunities-extension-table td:nth-child(10) {
|
||||||
|
padding-right: 6px;
|
||||||
|
padding-left: 6px;
|
||||||
|
font-size: 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3112,14 +3486,16 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
padding: 10px 30px 10px 16px;
|
padding: 8px 42px 8px 16px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
|
line-height: 1.2;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
white-space: normal;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3170,6 +3546,135 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-modal {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 3;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
background: rgba(0, 0, 0, .42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-modal[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-dialog {
|
||||||
|
display: grid;
|
||||||
|
width: min(680px, 100%);
|
||||||
|
max-height: min(720px, calc(100vh - 48px));
|
||||||
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #8f8a85;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, .24);
|
||||||
|
color: #312d2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
border-bottom: 1px solid #dedbd7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-content {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-fieldset {
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-fieldset legend {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 0;
|
||||||
|
color: #312d2a;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 4px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 9px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #312d2a;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.35;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-option:hover {
|
||||||
|
background: #f0eeeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-option input {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex: 0 0 18px;
|
||||||
|
margin: 0;
|
||||||
|
accent-color: #00758f;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 14px 20px;
|
||||||
|
border-top: 1px solid #dedbd7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-button {
|
||||||
|
min-width: 88px;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 7px 16px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-button-primary {
|
||||||
|
border-color: #00758f;
|
||||||
|
background: #00758f;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-button-primary:hover,
|
||||||
|
.opportunities-extension-button-primary:focus-visible {
|
||||||
|
border-color: #005e73;
|
||||||
|
background: #005e73;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-debug-panel {
|
.opportunities-extension-debug-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 24px;
|
right: 24px;
|
||||||
@@ -3368,6 +3873,7 @@
|
|||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table-surface,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table-surface,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-panel,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-panel,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-dialog,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item[open] {
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item[open] {
|
||||||
border-color: #4e4a46;
|
border-color: #4e4a46;
|
||||||
@@ -3405,6 +3911,9 @@
|
|||||||
|
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-select-all,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-select-all,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-header h2,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-fieldset legend,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-heading,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-heading,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item pre,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item pre,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel h2 {
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel h2 {
|
||||||
@@ -3416,6 +3925,30 @@
|
|||||||
border-bottom-color: #4e4a46;
|
border-bottom-color: #4e4a46;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-header,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-footer {
|
||||||
|
border-color: #4e4a46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option:hover {
|
||||||
|
background: #3b3937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option input {
|
||||||
|
accent-color: #43c4d5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary {
|
||||||
|
border-color: #00a6be;
|
||||||
|
background: #008aa6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary:hover,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary:focus-visible {
|
||||||
|
border-color: #43c4d5;
|
||||||
|
background: #006d7a;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option:hover,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option:hover,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table tbody tr:hover,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table tbody tr:hover,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sort-button:hover,
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sort-button:hover,
|
||||||
@@ -3471,7 +4004,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-total-dashboard-card {
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-total-dashboard-card {
|
||||||
border-top-color: #43c4d5;
|
border-top-color: #f6f4f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card[data-stage="PIPELINE"] {
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card[data-stage="PIPELINE"] {
|
||||||
@@ -3569,6 +4102,62 @@
|
|||||||
color: #f6f4f2;
|
color: #f6f4f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1201px) and (max-width: 1450px) {
|
||||||
|
.opportunities-extension-table th:nth-child(1) {
|
||||||
|
width: 16%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(3) {
|
||||||
|
width: 18%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(4) {
|
||||||
|
width: 12%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(6) {
|
||||||
|
width: 7%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(7) {
|
||||||
|
width: 8%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(10) {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.opportunities-extension-table {
|
||||||
|
min-width: 1180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(1) {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(3) {
|
||||||
|
width: 17%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(4) {
|
||||||
|
width: 13%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(6) {
|
||||||
|
width: 6%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(7) {
|
||||||
|
width: 9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table th:nth-child(10) {
|
||||||
|
width: 11%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.opportunities-extension-header {
|
.opportunities-extension-header {
|
||||||
padding: 24px 20px 18px;
|
padding: 24px 20px 18px;
|
||||||
@@ -3627,6 +4216,18 @@
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-modal {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-dialog {
|
||||||
|
max-height: calc(100vh - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-preferences-options {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-stage-filter-controls {
|
.opportunities-extension-stage-filter-controls {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user