Skip to content

Commit da99cc9

Browse files
author
John75SunCity
committed
fix: Convert portal_organization_diagram.js to vanilla JavaScript - org chart now works
1 parent ca650cd commit da99cc9

File tree

2 files changed

+515
-80
lines changed

2 files changed

+515
-80
lines changed

records_management/static/src/js/portal/portal_organization_diagram.js

Lines changed: 165 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Portal Organization Diagram - Frontend Widget (Production-Ready)
2+
* Portal Organization Diagram - Frontend Widget (Vanilla JavaScript - Odoo 18 Compatible)
33
*
44
* PURPOSE: Customer-facing portal organization chart visualization
55
* USE CASE: /my/organization route - shows company hierarchy to portal users
@@ -15,11 +15,11 @@
1515
* 3. vis.Network renders interactive diagram
1616
* 4. User interactions (search, export, layout) handled by widget
1717
*
18-
* PERFORMANCE OPTIMIZATIONS (Grok 2025):
19-
* - Batch DOM queries (cache elements, single pass updates)
20-
* - Optimized search with node highlighting
21-
* - Better animation easing for smooth UX
22-
* - Accessibility logging for screen readers
18+
* CONVERSION NOTES (Odoo 18):
19+
* - Removed: odoo.define(), publicWidget dependency
20+
* - Replaced: jQuery with native DOM APIs
21+
* - Added: IIFE wrapper for module isolation
22+
* - Preserved: All features, vis-network integration
2323
*
2424
* FEATURES:
2525
* ✓ Interactive diagram with drag/zoom
@@ -31,35 +31,81 @@
3131
*
3232
* BROWSER SUPPORT: Modern browsers (ES6+), graceful fallback for older browsers
3333
*/
34-
odoo.define('records_management.portal_organization_diagram', ['web.public.widget'], function(require) {
34+
35+
(function() {
3536
'use strict';
3637

37-
const publicWidget = require('web.public.widget');
38+
class OrgDiagramPortal {
39+
constructor(containerElement) {
40+
this.container = containerElement;
41+
this.diagramData = null;
42+
this.network = null;
43+
this.init();
44+
}
3845

39-
const OrgDiagramPortal = publicWidget.Widget.extend({
40-
selector: '.o_portal_organization_diagram',
41-
events: {
42-
'click #refresh-diagram': '_onRefresh',
43-
'click #export-diagram': '_onExport',
44-
'click #search-button': '_onSearch',
45-
'keyup #search-query': '_onSearchKey',
46-
'change #layout-select': '_onLayoutChanged',
47-
},
48-
start() {
49-
const self = this;
46+
init() {
5047
this._parseData();
5148
this._updateStats();
49+
this._setupEventHandlers();
5250

5351
// Load vis-network library from CDN, then render diagram
54-
this._loadVisNetwork().then(function() {
55-
self._renderDiagram();
56-
}).catch(function(err) {
57-
console.error('[OrgDiagramPortal] Failed to load vis-network library', err);
58-
self._showFallbackMessage();
59-
});
60-
61-
return this._super.apply(this, arguments);
62-
},
52+
this._loadVisNetwork()
53+
.then(() => {
54+
this._renderDiagram();
55+
})
56+
.catch((err) => {
57+
console.error('[OrgDiagramPortal] Failed to load vis-network library', err);
58+
this._showFallbackMessage();
59+
});
60+
}
61+
62+
_setupEventHandlers() {
63+
// Refresh button
64+
const refreshBtn = this.container.querySelector('#refresh-diagram');
65+
if (refreshBtn) {
66+
refreshBtn.addEventListener('click', (e) => {
67+
e.preventDefault();
68+
this._onRefresh();
69+
});
70+
}
71+
72+
// Export button
73+
const exportBtn = this.container.querySelector('#export-diagram');
74+
if (exportBtn) {
75+
exportBtn.addEventListener('click', (e) => {
76+
e.preventDefault();
77+
this._onExport();
78+
});
79+
}
80+
81+
// Search button
82+
const searchBtn = this.container.querySelector('#search-button');
83+
if (searchBtn) {
84+
searchBtn.addEventListener('click', (e) => {
85+
e.preventDefault();
86+
this._applySearch();
87+
});
88+
}
89+
90+
// Search input (enter key)
91+
const searchInput = this.container.querySelector('#search-query');
92+
if (searchInput) {
93+
searchInput.addEventListener('keyup', (e) => {
94+
if (e.key === 'Enter') {
95+
this._applySearch();
96+
}
97+
});
98+
}
99+
100+
// Layout selector
101+
const layoutSelect = this.container.querySelector('#layout-select');
102+
if (layoutSelect) {
103+
layoutSelect.addEventListener('change', () => {
104+
this._onLayoutChanged();
105+
});
106+
}
107+
}
108+
63109
_loadVisNetwork() {
64110
// Check if already loaded
65111
if (window.vis && window.vis.Network) {
@@ -85,11 +131,12 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
85131
script.onerror = reject;
86132
document.head.appendChild(script);
87133
});
88-
},
134+
}
135+
89136
_showFallbackMessage() {
90-
const container = this.el.querySelector('#organization-diagram-container');
91-
if (container) {
92-
container.innerHTML = `
137+
const diagramContainer = this.container.querySelector('#organization-diagram-container');
138+
if (diagramContainer) {
139+
diagramContainer.innerHTML = `
93140
<div class="p-5 text-center">
94141
<i class="fa fa-exclamation-triangle fa-3x text-warning mb-3"></i>
95142
<h5>Diagram Visualization Unavailable</h5>
@@ -98,10 +145,11 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
98145
</div>
99146
`;
100147
}
101-
},
148+
}
149+
102150
_parseData() {
103151
try {
104-
const jsonEl = this.el.querySelector('#diagram-data');
152+
const jsonEl = this.container.querySelector('#diagram-data');
105153
if (jsonEl) {
106154
this.diagramData = JSON.parse(jsonEl.textContent.trim());
107155
} else {
@@ -111,10 +159,11 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
111159
console.error('[OrgDiagramPortal] Failed to parse diagram JSON', e);
112160
this.diagramData = { nodes: [], edges: [], stats: {}, config: {} };
113161
}
114-
},
162+
}
163+
115164
_renderDiagram() {
116-
const container = this.el.querySelector('#organization-diagram-container');
117-
if (!container) {
165+
const diagramContainer = this.container.querySelector('#organization-diagram-container');
166+
if (!diagramContainer) {
118167
console.warn('[OrgDiagramPortal] Container #organization-diagram-container not found');
119168
return;
120169
}
@@ -128,7 +177,7 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
128177

129178
// Check if we have data
130179
if (!this.diagramData.nodes || !this.diagramData.nodes.length) {
131-
container.innerHTML = `
180+
diagramContainer.innerHTML = `
132181
<div class="p-5 text-center">
133182
<i class="fa fa-info-circle fa-3x text-info mb-3"></i>
134183
<h5>No Organization Data</h5>
@@ -140,7 +189,7 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
140189
}
141190

142191
// Remove loading overlay
143-
const loadingOverlay = container.querySelector('.loading-overlay');
192+
const loadingOverlay = diagramContainer.querySelector('.loading-overlay');
144193
if (loadingOverlay) {
145194
loadingOverlay.remove();
146195
}
@@ -150,21 +199,22 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
150199

151200
// Render the network
152201
try {
153-
this.network = new vis.Network(container, {
202+
this.network = new vis.Network(diagramContainer, {
154203
nodes: new vis.DataSet(this.diagramData.nodes),
155204
edges: new vis.DataSet(this.diagramData.edges),
156205
}, options);
157206

158207
console.log('[OrgDiagramPortal] Diagram rendered successfully with', this.diagramData.nodes.length, 'nodes');
159208

160209
// Add event listeners
161-
this.network.on('click', this._onNodeClick.bind(this));
210+
this.network.on('click', (params) => this._onNodeClick(params));
162211

163212
} catch (e) {
164213
console.error('[OrgDiagramPortal] vis.Network rendering failed', e);
165214
this._showFallbackMessage();
166215
}
167-
},
216+
}
217+
168218
_buildVisOptions() {
169219
const layoutType = (this.diagramData.config && this.diagramData.config.layout_type) || 'hierarchical';
170220
const hierarchical = layoutType === 'hierarchical';
@@ -222,7 +272,8 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
222272
};
223273

224274
return options;
225-
},
275+
}
276+
226277
_onNodeClick(params) {
227278
if (params.nodes && params.nodes.length > 0) {
228279
const nodeId = params.nodes[0];
@@ -231,11 +282,12 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
231282
this._showNodeDetails(node);
232283
}
233284
}
234-
},
285+
}
286+
235287
_showNodeDetails(node) {
236-
const modal = this.el.querySelector('#node-details-modal');
237-
const title = this.el.querySelector('#node-modal-title');
238-
const body = this.el.querySelector('#node-modal-body');
288+
const modal = this.container.querySelector('#node-details-modal');
289+
const title = this.container.querySelector('#node-modal-title');
290+
const body = this.container.querySelector('#node-modal-body');
239291

240292
if (!modal || !title || !body) return;
241293

@@ -267,9 +319,20 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
267319

268320
body.innerHTML = html;
269321

270-
// Show modal using Bootstrap
271-
$(modal).modal('show');
272-
},
322+
// Show modal using Bootstrap 5
323+
if (window.bootstrap && window.bootstrap.Modal) {
324+
const modalInstance = new bootstrap.Modal(modal);
325+
modalInstance.show();
326+
} else {
327+
// Fallback for older Bootstrap
328+
modal.classList.add('show');
329+
modal.style.display = 'block';
330+
const backdrop = document.createElement('div');
331+
backdrop.className = 'modal-backdrop fade show';
332+
document.body.appendChild(backdrop);
333+
}
334+
}
335+
273336
_updateStats() {
274337
const stats = this.diagramData.stats || {};
275338
const mapping = {
@@ -278,61 +341,60 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
278341
users: '#users-count',
279342
connections: '#connections-count',
280343
};
344+
281345
// Batch DOM queries for performance
282346
const elements = {};
283347
for (const key in mapping) {
284-
elements[key] = this.el.querySelector(mapping[key]);
348+
elements[key] = this.container.querySelector(mapping[key]);
285349
}
350+
286351
// Batch DOM updates
287352
for (const key in elements) {
288353
const el = elements[key];
289354
if (el) {
290355
el.textContent = stats[key] != null ? stats[key] : '-';
291356
}
292357
}
293-
},
294-
_onRefresh(ev) {
295-
ev.preventDefault();
358+
}
359+
360+
_onRefresh() {
296361
this._parseData();
297362
this._renderDiagram();
298363
this._updateStats();
299-
},
300-
_onExport(ev) {
301-
ev.preventDefault();
364+
}
365+
366+
_onExport() {
302367
const blob = new Blob([JSON.stringify(this.diagramData, null, 2)], { type: 'application/json' });
303368
const a = document.createElement('a');
304369
a.href = URL.createObjectURL(blob);
305370
a.download = 'organization_diagram.json';
306371
a.click();
307-
},
308-
_onSearch(ev) {
309-
ev.preventDefault();
310-
this._applySearch();
311-
},
312-
_onSearchKey(ev) {
313-
if (ev.key === 'Enter') {
314-
this._applySearch();
315-
}
316-
},
317-
_onLayoutChanged() {
318-
if (!this.network) { return; }
319-
this.diagramData.config = this.diagramData.config || {};
320-
this.diagramData.config.layout_type = this.el.querySelector('#layout-select')?.value;
321-
this._renderDiagram();
322-
},
372+
}
373+
323374
_applySearch() {
324375
if (!this.network) { return; }
325-
const query = (this.el.querySelector('#search-query')?.value || '').trim().toLowerCase();
376+
377+
const searchInput = this.container.querySelector('#search-query');
378+
const query = (searchInput?.value || '').trim().toLowerCase();
379+
326380
if (!query) {
327381
// Clear previous selection
328382
this.network.unselectAll();
329383
return;
330384
}
385+
331386
const matches = this.diagramData.nodes.filter(n => (n.label || '').toLowerCase().includes(query));
387+
332388
if (matches.length) {
333389
const ids = matches.map(m => m.id);
334390
this.network.selectNodes(ids, true);
335-
this.network.focus(ids[0], { scale: 1.2, animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
391+
this.network.focus(ids[0], {
392+
scale: 1.2,
393+
animation: {
394+
duration: 500,
395+
easingFunction: 'easeInOutQuad'
396+
}
397+
});
336398

337399
// Accessibility: Announce results
338400
const resultMsg = matches.length === 1
@@ -342,9 +404,32 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
342404
} else {
343405
console.log('[OrgDiagramPortal] Search: No matches found for "' + query + '"');
344406
}
345-
},
346-
});
407+
}
408+
409+
_onLayoutChanged() {
410+
if (!this.network) { return; }
411+
412+
const layoutSelect = this.container.querySelector('#layout-select');
413+
this.diagramData.config = this.diagramData.config || {};
414+
this.diagramData.config.layout_type = layoutSelect?.value;
415+
this._renderDiagram();
416+
}
417+
}
418+
419+
// Auto-initialize on page load
420+
if (document.readyState === 'loading') {
421+
document.addEventListener('DOMContentLoaded', initOrgDiagram);
422+
} else {
423+
initOrgDiagram();
424+
}
425+
426+
function initOrgDiagram() {
427+
const containers = document.querySelectorAll('.o_portal_organization_diagram');
428+
containers.forEach(container => {
429+
new OrgDiagramPortal(container);
430+
});
431+
}
347432

348-
publicWidget.registry.OrgDiagramPortal = OrgDiagramPortal;
349-
return OrgDiagramPortal;
350-
});
433+
// Expose globally for manual initialization if needed
434+
window.RecordsManagementOrgDiagram = OrgDiagramPortal;
435+
})();

0 commit comments

Comments
 (0)