Implemented the Organization Chart feature to create customer organizational
This commit is contained in:
148
dist/chromium/background.js
vendored
148
dist/chromium/background.js
vendored
@@ -1,5 +1,140 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
// Source: src/features/org-chart/model.js
|
||||
function createOrgChartModel() {
|
||||
function normalize(nodes) {
|
||||
if (!Array.isArray(nodes) || nodes.length > 500) throw new Error("An organization chart supports up to 500 people.");
|
||||
const ids = new Set();
|
||||
const result = nodes.map(node => {
|
||||
if (!node || typeof node.id !== "string" || !/^[\w-]{1,80}$/.test(node.id) || ids.has(node.id)) throw new Error("Invalid or duplicate person ID.");
|
||||
ids.add(node.id);
|
||||
const item = { id: node.id, parentId: node.parentId == null ? null : node.parentId, collapsed: node.collapsed === true };
|
||||
const jobTitle = node.jobTitle ?? node.role ?? "";
|
||||
if (typeof jobTitle !== "string" || jobTitle.length > 160) throw new Error("Invalid Job Title.");
|
||||
item.jobTitle = jobTitle.trim();
|
||||
if (node.PartyNumber != null && node.PartyNumber !== "") {
|
||||
if (!/^[A-Za-z0-9]{1,100}$/.test(String(node.PartyNumber))) throw new Error("Invalid contact PartyNumber.");
|
||||
item.PartyNumber = String(node.PartyNumber);
|
||||
}
|
||||
if (node.contactPartyId != null) {
|
||||
if (typeof node.contactPartyId !== "string" || !/^[\w-]{1,100}$/.test(node.contactPartyId)) throw new Error("Invalid contact ID.");
|
||||
item.contactPartyId = node.contactPartyId;
|
||||
}
|
||||
for (const [key, max] of [["name", 160], ["notes", 4000]]) {
|
||||
if (typeof node[key] !== "string" || node[key].length > max || (key === "name" && !node[key].trim())) throw new Error(`Enter a valid ${key} (up to ${max} characters).`);
|
||||
item[key] = key === "notes" ? node[key] : node[key].trim();
|
||||
}
|
||||
return item;
|
||||
});
|
||||
const byId = new Map(result.map(node => [node.id, node]));
|
||||
for (const node of result) {
|
||||
const seen = new Set([node.id]);
|
||||
let parent = node.parentId;
|
||||
while (parent !== null) {
|
||||
if (!byId.has(parent) || seen.has(parent)) throw new Error("The reporting structure contains a missing manager or a cycle.");
|
||||
seen.add(parent);
|
||||
if (seen.size > 60) throw new Error("The reporting structure supports up to 60 levels.");
|
||||
parent = byId.get(parent).parentId;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function descendants(nodes, id) {
|
||||
const found = new Set([id]);
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const node of nodes) if (found.has(node.parentId) && !found.has(node.id)) { found.add(node.id); changed = true; }
|
||||
}
|
||||
return found;
|
||||
}
|
||||
function upsert(nodes, person) {
|
||||
return normalize(nodes.some(node => node.id === person.id) ? nodes.map(node => node.id === person.id ? person : node) : [...nodes, person]);
|
||||
}
|
||||
function remove(nodes, id) {
|
||||
const person = nodes.find(node => node.id === id);
|
||||
if (!person) return nodes;
|
||||
return normalize(nodes.filter(node => node.id !== id).map(node => node.parentId === id ? { ...node, parentId: person.parentId } : node));
|
||||
}
|
||||
function hierarchy(nodes) {
|
||||
const root = { id: "", children: [] }, byId = new Map([[null, root]]);
|
||||
nodes.forEach(node => byId.set(node.id, { ...node, children: [] }));
|
||||
nodes.forEach(node => { if (!byId.get(node.parentId).collapsed) byId.get(node.parentId).children.push(byId.get(node.id)); });
|
||||
return root;
|
||||
}
|
||||
return { normalize, descendants, upsert, remove, hierarchy };
|
||||
}
|
||||
|
||||
// Source: src/features/org-chart/storage.js
|
||||
function createOrgChartStorage({ environment, model }) {
|
||||
let opening = null;
|
||||
function database() {
|
||||
if (opening) return opening;
|
||||
opening = new Promise((resolve, reject) => {
|
||||
if (!environment.indexedDB) { reject(new Error("IndexedDB is unavailable. The organization chart cannot be saved locally.")); return; }
|
||||
const request = environment.indexedDB.open("opportunities-extension-organizations", 3);
|
||||
let blocked = false;
|
||||
request.onupgradeneeded = () => {
|
||||
if (!request.result.objectStoreNames.contains("charts")) request.result.createObjectStore("charts", { keyPath: "key" });
|
||||
else {
|
||||
const cursor = request.transaction.objectStore("charts").openCursor();
|
||||
cursor.onsuccess = () => {
|
||||
const current = cursor.result;
|
||||
if (!current) return;
|
||||
const record = current.value;
|
||||
if (Array.isArray(record.nodes)) {
|
||||
record.nodes = record.nodes.map(node => {
|
||||
const clean = { ...node, jobTitle: node.jobTitle ?? node.role ?? "" }; delete clean.role; delete clean.emailAddress; return clean;
|
||||
});
|
||||
current.update(record);
|
||||
}
|
||||
current.continue();
|
||||
};
|
||||
}
|
||||
};
|
||||
request.onerror = () => reject(new Error("Unable to open local organization charts."));
|
||||
request.onblocked = () => { blocked = true; reject(new Error("Local storage is blocked by another window. Close it and retry.")); };
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
if (blocked) { db.close(); return; }
|
||||
db.onversionchange = () => { db.close(); opening = null; };
|
||||
resolve(db);
|
||||
};
|
||||
}).catch(error => { opening = null; throw error; });
|
||||
return opening;
|
||||
}
|
||||
function identity({ principal, customerId }) {
|
||||
if (typeof principal !== "string" || !principal.trim() || principal.length > 254 || typeof customerId !== "string" || !/^[\w-]{1,100}$/.test(customerId)) throw new Error("A current user and customer ID are required.");
|
||||
return JSON.stringify([principal.trim().toLowerCase(), customerId]);
|
||||
}
|
||||
async function transact(message, write) {
|
||||
const key = identity(message);
|
||||
const nodes = write ? model.normalize(message.nodes) : null;
|
||||
if (write && (!Number.isSafeInteger(message.revision) || message.revision < 0)) throw new Error("Invalid organization chart revision.");
|
||||
const db = await database();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction("charts", write ? "readwrite" : "readonly");
|
||||
const store = transaction.objectStore("charts");
|
||||
const request = store.get(key);
|
||||
let result, failure;
|
||||
request.onsuccess = () => {
|
||||
try {
|
||||
const existing = request.result;
|
||||
if (existing && existing.schemaVersion !== 1) throw new Error("Unsupported organization chart version.");
|
||||
if (write) {
|
||||
if ((existing?.revision || 0) !== message.revision) throw new Error("This chart changed in another window. Reopen it before saving your changes.");
|
||||
result = { key, schemaVersion: 1, revision: message.revision + 1, nodes, updatedAt: new Date().toISOString() };
|
||||
store.put(result);
|
||||
} else result = existing ? { ...existing, nodes: model.normalize(existing.nodes) } : { schemaVersion: 1, revision: 0, nodes: [], updatedAt: null };
|
||||
} catch (error) { failure = error; transaction.abort(); }
|
||||
};
|
||||
transaction.oncomplete = () => resolve({ ok: true, chart: result });
|
||||
transaction.onabort = transaction.onerror = () => reject(failure || new Error("Unable to save or read the organization chart. Check available browser storage and retry."));
|
||||
});
|
||||
}
|
||||
return { load: message => transact(message, false), save: message => transact(message, true) };
|
||||
}
|
||||
|
||||
// Source: src/shared/time-dates.js
|
||||
function createTimeDates() {
|
||||
const months = "JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC".split(" ");
|
||||
@@ -157,6 +292,8 @@ function createTimeEntries({ fetchJson, dates, getResourceUser, saveHeaders }) {
|
||||
/** Stable wire names. All payloads remain plain, serializable objects. */
|
||||
function createMessageTypes() {
|
||||
return Object.freeze({
|
||||
loadOrganizationChart: "opportunitiesExtension.loadOrganizationChart",
|
||||
saveOrganizationChart: "opportunitiesExtension.saveOrganizationChart",
|
||||
requestWeekDetails: "opportunitiesExtension.requestWeekDetails",
|
||||
requestTimeEntries: "opportunitiesExtension.requestTimeEntries",
|
||||
requestTimeEntriesSave: "opportunitiesExtension.requestTimeEntriesSave",
|
||||
@@ -2939,6 +3076,8 @@ function startBackgroundApplication(dependencies) {
|
||||
// Source: src/background/messages.js
|
||||
/** Register synchronously; replies retain the WebExtension sendResponse contract. */
|
||||
function registerBackgroundMessages({
|
||||
loadOrganizationChart,
|
||||
saveOrganizationChart,
|
||||
messageTypes,
|
||||
requestWeekDetails,
|
||||
requestTimeEntries,
|
||||
@@ -2985,6 +3124,11 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (!message || !message.type) {
|
||||
return false;
|
||||
}
|
||||
if ([messageTypes.loadOrganizationChart, messageTypes.saveOrganizationChart].includes(message.type)) {
|
||||
const operation = message.type === messageTypes.loadOrganizationChart ? loadOrganizationChart : saveOrganizationChart;
|
||||
operation(message).then(sendResponse).catch(error => sendResponse({ ok: false, error: error.message }));
|
||||
return true;
|
||||
}
|
||||
if (message.type === messageTypes.requestLookupTaskType) {
|
||||
requestLookupTaskType().then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||
error: error.message, httpStatus: error.httpStatus, debugRequests: error.debugRequests || [] }));
|
||||
@@ -3209,6 +3353,7 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
// Source: src/background/application.js
|
||||
function createBackgroundApplication(overrides = {}) {
|
||||
const environment = {
|
||||
indexedDB: globalThis.indexedDB,
|
||||
fetch: globalThis.fetch,
|
||||
browser: globalThis.browser,
|
||||
chrome: globalThis.chrome,
|
||||
@@ -3217,6 +3362,7 @@ function createBackgroundApplication(overrides = {}) {
|
||||
...overrides
|
||||
};
|
||||
const messageTypes = createMessageTypes();
|
||||
const orgChartStorage = createOrgChartStorage({ environment, model: createOrgChartModel() });
|
||||
const timeEntries = createTimeEntries({ dates: createTimeDates(), fetchJson: (...args) => httpBackgroundJson.fetchJson(...args), getResourceUser: message => fusionSrCreate.getResourceUser(message), saveHeaders: url => srRequestHeaders.timeEntriesHeaders(url) });
|
||||
const contracts = createBackgroundContracts();
|
||||
const spaPageClient = createSpaPageClient({
|
||||
@@ -3384,6 +3530,8 @@ function createBackgroundApplication(overrides = {}) {
|
||||
function start() {
|
||||
srRequestHeaders.start();
|
||||
startBackgroundApplication({
|
||||
loadOrganizationChart: message => orgChartStorage.load(message),
|
||||
saveOrganizationChart: message => orgChartStorage.save(message),
|
||||
requestWorkloads: message => spaWorkloads.requestWorkloads(message),
|
||||
requestWorkloadForecastUpdate: message => spaWorkloads.requestWorkloadForecastUpdate(message),
|
||||
invalidateWorkloadsCache: () => workloadsCache.invalidate(),
|
||||
|
||||
Reference in New Issue
Block a user