-
Notifications
You must be signed in to change notification settings - Fork 9
Analyze plugin #147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release/v3
Are you sure you want to change the base?
Analyze plugin #147
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
|
|
||
| Analyze plugin | ||
| ======================== | ||
|
|
||
| Purpose | ||
| ------- | ||
| Adds an "Analyze" tab to the AppBar with two actions: a "Run Recent →" anchor that opens a right-side recent-jobs panel, and "Create New App" which creates floating window with new app form. | ||
|
|
||
| Files | ||
| ----- | ||
| - `analyzeDropdown.mjs` - registers the tab and wires dropdown items. | ||
| - `newAppForm.mjs` - the floating form used by "Create New App". | ||
|
|
||
| How to use | ||
| ---------- | ||
| - Provide recent jobs by passing `params.recentJobs` or saving `recentJobs` via plugin options. | ||
| - Handle job clicks by implementing `onJobClick({ index, label })` on the plugin instance. | ||
| - Provide `params.onCreate` to receive form submission data from `NewAppForm`. | ||
|
|
||
| Implementation notes | ||
| -------------------- | ||
| - UI behaviors (menu, positioning, hover) are implemented in `SidePanel` (`setMenu`, `showNear`, `scheduleHide`, `cancelHide`) — reuse it for other flyouts. | ||
| - `SidePanel.hide()` currently removes the element; consider switching to `display:none` if you need faster show/hide cycles. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| import { Dropdown } from "../../ui/classes/elements/dropdown.mjs"; | ||
| import { NewAppForm } from "./newAppForm.mjs"; | ||
| import { SidePanel } from "../../ui/classes/components/sidePanel.mjs"; | ||
|
|
||
| addPlugin('analyze', class extends XOpatPlugin { | ||
| constructor(id, params) { | ||
| super(id); | ||
| this.params = params || {}; | ||
| // plugin-level stored recent jobs can be configured via params or saved options | ||
| this.recentJobs = this.getOption('recentJobs') || this.params.recentJobs || []; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Params are already read from using |
||
| } | ||
|
|
||
| pluginReady() { | ||
| const register = () => { | ||
|
|
||
| if (!window.USER_INTERFACE || !USER_INTERFACE.AppBar || !USER_INTERFACE.AppBar.menu) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bad alignment.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, user interface should be already available, if it is not, it is a bug. |
||
|
|
||
| // retry shortly if AppBar not ready yet | ||
| return setTimeout(register, 50); | ||
| } | ||
|
|
||
| // safe translation helper: return translated value or fallback when missing | ||
| const tOr = (key, fallback) => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need this generic 'safe' utility, we should put this function to the CORE api, not here... Why is |
||
| if (typeof $?.t === 'function') { | ||
| try { | ||
| const translated = $.t(key); | ||
| if (translated && translated !== key) return translated; | ||
| } catch (e) { /* ignore and fallback */ } | ||
| } | ||
| return fallback; | ||
| }; | ||
|
|
||
| const title = tOr('analyze.title', 'Analyze'); | ||
| const tab = USER_INTERFACE.AppBar.addTab( | ||
| this.id, // ownerPluginId | ||
| title, // title (localized if available) | ||
| 'fa-magnifying-glass', // icon | ||
| [], // body | ||
| Dropdown // itemClass so Menu constructs plugin component | ||
| ); | ||
|
|
||
|
|
||
| if (tab) { | ||
| const attachToggle = () => { | ||
| try { | ||
| const btnId = `${tab.parentId}-b-${tab.id}`; | ||
| const btnEl = document.getElementById(btnId); | ||
| if (!btnEl) return false; | ||
| let wrapper = btnEl.closest('.dropdown'); | ||
| if (!wrapper) { | ||
| try { | ||
| const newWrapper = tab.create(); | ||
| const parent = btnEl.parentElement; | ||
| if (parent) { | ||
| parent.insertBefore(newWrapper, btnEl); | ||
| btnEl.remove(); | ||
| wrapper = newWrapper; | ||
| } | ||
| } catch (e) { | ||
| } | ||
| } | ||
|
|
||
| if (wrapper) { | ||
| const trigger = wrapper.querySelector('[tabindex]') || wrapper; | ||
| trigger.addEventListener('click', (e) => { | ||
| try { | ||
| wrapper.classList.toggle('dropdown-open'); | ||
| if (!wrapper.classList.contains('dropdown-open')) { | ||
| try { tab.hideRecent?.(); } catch(_) {} | ||
| } | ||
| } catch(_) {} | ||
| e.stopPropagation(); | ||
| }); | ||
| return true; | ||
| } | ||
| } catch (e) { console.error('[analyze] attachToggle error', e); } | ||
| return false; | ||
| }; | ||
| // Try immediate attach; if DOM not present yet, retry shortly | ||
| if (!attachToggle()) setTimeout(attachToggle, 50); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, DOM should be already ready at |
||
| } | ||
|
|
||
| // Configure dropdown items using the Dropdown API | ||
| try { | ||
| if (tab && typeof tab.addItem === 'function') { | ||
| // create the 'recent' section but keep its title empty so no uppercase header is shown | ||
| try { tab.addSection({ id: 'recent', title: '' }); } catch (e) {} | ||
| // prefer a slightly wider dropdown to match previous styling | ||
| try { if (tab) { tab.widthClass = 'w-64'; if (tab._contentEl) tab._contentEl.classList.add('w-64'); } } catch(e) {} | ||
|
|
||
| // Only add a single anchor item for Run Recent; the detailed list appears in the SidePanel on hover | ||
| tab.addItem({ | ||
| id: 'run-recent', | ||
| section: 'recent', | ||
| label: tOr('analyze.runRecent', 'Run Recent') + ' \u2192', | ||
| onClick: () => false, | ||
| }); | ||
|
|
||
| // create a reusable SidePanel and attach delegated hover handlers to show it | ||
| try { | ||
| // let the panel size to its content by default (width: 'auto') | ||
| const side = new SidePanel({ id: `${this.id}-recent-panel`, width: 'auto', maxHeight: '70vh' }); | ||
| const attachHover = () => { | ||
| try { | ||
| const content = tab._contentEl; | ||
| if (!content) return false; | ||
|
|
||
| // delegate to the 'run-recent' item inside the dropdown content | ||
| content.addEventListener('mouseover', (e) => { | ||
| const hit = e.target.closest && e.target.closest('[data-item-id]'); | ||
| if (hit && hit.dataset && hit.dataset.itemId === 'run-recent') { | ||
| try { | ||
| // cancel any pending hide so we can reopen immediately | ||
| side.cancelHide?.(); | ||
| const jobs = (this.recentJobs && this.recentJobs.length) ? this.recentJobs : ['Recent Job 1','Recent Job 2','Recent Job 3']; | ||
| // use SidePanel helper to build a menu and position the panel next to the anchor | ||
| side.setMenu(jobs, (it, idx) => { | ||
| try { if (typeof this.onJobClick === 'function') this.onJobClick({ index: idx, label: (typeof it === 'string' ? it : (it && it.label)) }); } catch(_){} | ||
| }); | ||
| side.showNear(hit, { nudge: 1 }); | ||
| try { tab.hideRecent = () => side.hide(); } catch(_) {} | ||
| } catch (err) { console.error('[analyze] show side panel error', err); } | ||
| } | ||
| }); | ||
|
|
||
| content.addEventListener('mouseout', (e) => { | ||
| const related = e.relatedTarget; | ||
| if (!related || !related.closest || !related.closest(`#${side.id}`)) side.scheduleHide(); | ||
| }); | ||
| return true; | ||
| } catch (e) { console.error('[analyze] attachHover error', e); } | ||
| return false; | ||
| }; | ||
| if (!attachHover()) setTimeout(attachHover, 50); | ||
| } catch (e) { /* ignore */ } | ||
|
|
||
|
|
||
|
|
||
| tab.addItem({ | ||
| id: 'create-app', | ||
| label: tOr('analyze.createApp', 'Create New App'), | ||
| onClick: () => { | ||
| try { | ||
| const form = new NewAppForm({ onSubmit: (data) => { | ||
| try { if (this.params.onCreate?.(data) !== false) { alert('Created new app: ' + JSON.stringify(data)); } } | ||
| catch (err) { console.error(err); } | ||
| }}); | ||
| const win = form.showFloating({ title: tOr('analyze.createApp', 'Create New App'), width: 420, height: 360 }); | ||
| if (!win) { | ||
| const overlayId = `${this.id}-newapp-overlay`; | ||
| USER_INTERFACE.Dialogs.showCustom(overlayId, 'New App', `<div id="${overlayId}-content"></div>`, '', { allowClose: true }); | ||
| const container = document.getElementById(overlayId)?.querySelector('.card-body'); | ||
| if (container) form.attachTo(container); | ||
| } | ||
| } catch (e) { console.error('[analyze] create-app error', e); } | ||
| return false; | ||
| } | ||
| }); | ||
| } | ||
| } catch (e) { console.warn('[analyze] failed to configure dropdown items', e); } | ||
| // Close dropdowns when clicking away: attach a document-level click handler once per tab | ||
| const attachDocumentCloser = (t) => { | ||
| try { | ||
| if (!t || t.__analyzeDocCloserAttached) return; | ||
| const btnId = `${t.parentId}-b-${t.id}`; | ||
| const docHandler = (ev) => { | ||
| try { | ||
| const openWrappers = Array.from(document.querySelectorAll('.dropdown.dropdown-open')); | ||
| openWrappers.forEach((wrapper) => { | ||
| const btnEl = document.getElementById(btnId); | ||
| if (btnEl && (btnEl === ev.target || btnEl.contains(ev.target))) return; | ||
| try { wrapper.classList.remove('dropdown-open'); } catch(_) {} | ||
| }); | ||
| try { t.hideRecent?.(); } catch(_) {} | ||
| } catch (_) { /* swallow */ } | ||
| }; | ||
| document.addEventListener('click', docHandler, true); | ||
| const keyHandler = (ev) => { if (ev.key === 'Escape') { try { Array.from(document.querySelectorAll('.dropdown.dropdown-open')).forEach(w=>w.classList.remove('dropdown-open')); try { t.hideRecent?.(); } catch(_){} } catch(_){} } }; | ||
| document.addEventListener('keydown', keyHandler, true); | ||
| t.__analyzeDocCloserAttached = true; | ||
| } catch (e) { /* ignore */ } | ||
| }; | ||
| try { attachDocumentCloser(tab); } catch(e) { /* ignore */ } | ||
| // diagnostic logs removed | ||
| }; | ||
|
|
||
| register(); | ||
| } | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "id": "analyze", | ||
| "name": "Analyze dropdown in Main Menu", | ||
| "author": "Filip Vrubel", | ||
| "version": "1.0.0", | ||
| "description": "Plugin for creating and running jobs", | ||
| "icon": null, | ||
| "includes" : ["newAppForm.mjs", "analyzeDropdown.mjs"], | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can include only main file, import/export is handled by browser automatically. This will preload the files no matter whether they are needed or not, browser will do it dynamically. Just FYI no need to change. |
||
| "permaLoad": true | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Documentation? Awesome!