web-mojo 2.2.89 → 2.2.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -0
- package/dist/admin.cjs.js +1 -1
- package/dist/admin.cjs.js.map +1 -1
- package/dist/admin.css +48 -2
- package/dist/admin.es.js +1 -1
- package/dist/admin.es.js.map +1 -1
- package/dist/auth.cjs.js +1 -1
- package/dist/auth.es.js +1 -1
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +1 -1
- package/dist/chunks/{ChatView-C9VR3pfa.js → ChatView-BeJiXws6.js} +2 -2
- package/dist/chunks/{ChatView-C9VR3pfa.js.map → ChatView-BeJiXws6.js.map} +1 -1
- package/dist/chunks/{ChatView-wuMHOvns.js → ChatView-DhV0Ycul.js} +2 -2
- package/dist/chunks/{ChatView-wuMHOvns.js.map → ChatView-DhV0Ycul.js.map} +1 -1
- package/dist/chunks/{Dialog-EZ9XD2AO.js → Dialog-DZuk-4Ck.js} +2 -2
- package/dist/chunks/{Dialog-EZ9XD2AO.js.map → Dialog-DZuk-4Ck.js.map} +1 -1
- package/dist/chunks/{FormView-BqqtbPVb.js → FormView-C0oytbfM.js} +2 -2
- package/dist/chunks/{FormView-BqqtbPVb.js.map → FormView-C0oytbfM.js.map} +1 -1
- package/dist/chunks/GroupView-DAzE4A-4.js +2 -0
- package/dist/chunks/GroupView-DAzE4A-4.js.map +1 -0
- package/dist/chunks/GroupView-FfwdNW9H.js +2 -0
- package/dist/chunks/GroupView-FfwdNW9H.js.map +1 -0
- package/dist/chunks/{ListView-DekXjgMV.js → ListView-BZTAh7jk.js} +2 -2
- package/dist/chunks/ListView-BZTAh7jk.js.map +1 -0
- package/dist/chunks/{ListView-DqZ6BQKk.js → ListView-DnjpwUSD.js} +2 -2
- package/dist/chunks/ListView-DnjpwUSD.js.map +1 -0
- package/dist/chunks/{MetricsMiniChartWidget-B4CcNlv-.js → MetricsMiniChartWidget-BVbBD-su.js} +2 -2
- package/dist/chunks/{MetricsMiniChartWidget-B4CcNlv-.js.map → MetricsMiniChartWidget-BVbBD-su.js.map} +1 -1
- package/dist/chunks/{MetricsMiniChartWidget-y-KklF3c.js → MetricsMiniChartWidget-DnIxJqxS.js} +2 -2
- package/dist/chunks/{MetricsMiniChartWidget-y-KklF3c.js.map → MetricsMiniChartWidget-DnIxJqxS.js.map} +1 -1
- package/dist/chunks/{Modal-DoIYUAKi.js → Modal-RvUBle9X.js} +2 -2
- package/dist/chunks/{Modal-DoIYUAKi.js.map → Modal-RvUBle9X.js.map} +1 -1
- package/dist/chunks/{PDFViewer-DYAJqRAV.js → PDFViewer-XPhqOvSX.js} +2 -2
- package/dist/chunks/{PDFViewer-DYAJqRAV.js.map → PDFViewer-XPhqOvSX.js.map} +1 -1
- package/dist/chunks/{Passkeys-DNpner4L.js → Passkeys-C7-z9-v5.js} +2 -2
- package/dist/chunks/{Passkeys-DNpner4L.js.map → Passkeys-C7-z9-v5.js.map} +1 -1
- package/dist/chunks/{Passkeys-DapTiqNW.js → Passkeys-ZNOvvdIC.js} +2 -2
- package/dist/chunks/{Passkeys-DapTiqNW.js.map → Passkeys-ZNOvvdIC.js.map} +1 -1
- package/dist/chunks/{TokenManager-BLd64x-1.js → TokenManager-D2YDvzww.js} +2 -2
- package/dist/chunks/{TokenManager-BLd64x-1.js.map → TokenManager-D2YDvzww.js.map} +1 -1
- package/dist/chunks/{UserProfileView-q29bxm0T.js → UserProfileView-CxH58ZvK.js} +2 -2
- package/dist/chunks/{UserProfileView-q29bxm0T.js.map → UserProfileView-CxH58ZvK.js.map} +1 -1
- package/dist/chunks/{UserProfileView-BDD3WOjd.js → UserProfileView-gTSICPvR.js} +2 -2
- package/dist/chunks/{UserProfileView-BDD3WOjd.js.map → UserProfileView-gTSICPvR.js.map} +1 -1
- package/dist/chunks/{WebApp-DgmB8vpR.js → WebApp-hSbcg7WF.js} +2 -2
- package/dist/chunks/{WebApp-DgmB8vpR.js.map → WebApp-hSbcg7WF.js.map} +1 -1
- package/dist/chunks/{WebSocketClient-C9VS1m8v.js → WebSocketClient-BRxSW8O4.js} +2 -2
- package/dist/chunks/WebSocketClient-BRxSW8O4.js.map +1 -0
- package/dist/chunks/{WebSocketClient-CMV76kSt.js → WebSocketClient-Ctjkjn45.js} +2 -2
- package/dist/chunks/WebSocketClient-Ctjkjn45.js.map +1 -0
- package/dist/chunks/{index-iPINfbSy.js → index-BXBnnQiG.js} +2 -2
- package/dist/chunks/{index-iPINfbSy.js.map → index-BXBnnQiG.js.map} +1 -1
- package/dist/chunks/{index-DXlIy6bc.js → index-C1Qa4dbR.js} +2 -2
- package/dist/chunks/{index-DXlIy6bc.js.map → index-C1Qa4dbR.js.map} +1 -1
- package/dist/chunks/{version-BvwIbhO-.js → version-CmEMzl3n.js} +2 -2
- package/dist/chunks/{version-BvwIbhO-.js.map → version-CmEMzl3n.js.map} +1 -1
- package/dist/chunks/{version-CToejY47.js → version-fsZmab6w.js} +2 -2
- package/dist/chunks/{version-CToejY47.js.map → version-fsZmab6w.js.map} +1 -1
- package/dist/css/web-mojo.css +1 -1
- package/dist/docit.cjs.js +1 -1
- package/dist/docit.es.js +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1 -1
- package/dist/index.es.js.map +1 -1
- package/dist/lightbox.cjs.js +1 -1
- package/dist/lightbox.es.js +1 -1
- package/dist/map.es.js +1 -1
- package/dist/timeline.cjs.js +1 -1
- package/dist/timeline.es.js +1 -1
- package/dist/user-profile.cjs.js +1 -1
- package/dist/user-profile.es.js +1 -1
- package/dist/web-mojo.lite.iife.js +5 -1
- package/dist/web-mojo.lite.iife.js.map +1 -1
- package/dist/web-mojo.lite.iife.min.js +1 -1
- package/dist/web-mojo.lite.iife.min.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunks/ListView-DekXjgMV.js.map +0 -1
- package/dist/chunks/ListView-DqZ6BQKk.js.map +0 -1
- package/dist/chunks/WebSocketClient-C9VS1m8v.js.map +0 -1
- package/dist/chunks/WebSocketClient-CMV76kSt.js.map +0 -1
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";const e=require("./Collection-Bgp386gn.js"),t=require("./Passkeys-C7-z9-v5.js"),i=require("./ContextMenu-DBeueYpI.js"),n=require("./Dialog-2gXM2UcO.js"),a=require("./ChatView-BeJiXws6.js");class SideNavView extends e.View{constructor(e={}){const{sections:t=[],activeSection:i,navWidth:n,contentPadding:a,enableResponsive:s,minWidth:o,...d}=e;super({tagName:"div",className:"side-nav-view",...d}),this.navWidth=n||200,this.contentPadding=a||"1.5rem 2.5rem",this.enableResponsive=!1!==s,this.minWidth=o||500,this.sectionConfigs=[],this.sectionViews={},this.sectionKeys=[],this.activeSection=null,this.currentMode="sidebar",this.resizeObserver=null,this.lastContainerWidth=0;for(const l of t)this._addSectionConfig(l);this.activeSection=i||this.sectionKeys[0]||null,this.handleResize=this.handleResize.bind(this)}_addSectionConfig(e){"divider"!==e.type?e.permissions&&!this._hasPermission(e.permissions)||(this.sectionConfigs.push(e),this.sectionKeys.push(e.key),e.view&&(this.sectionViews[e.key]=e.view,e.view.parent=this)):this.sectionConfigs.push({type:"divider",label:e.label})}_hasPermission(e){try{return this.getApp().activeUser.hasPerm(e)}catch{return!0}}async renderTemplate(){const e="dropdown"===this.currentMode?this._buildDropdownNav():this._buildSidebarNav();return`\n <style>\n .snv-layout { display: flex; height: 100%; min-height: 0; }\n .snv-nav {\n width: ${this.navWidth}px;\n background: #f8f9fc;\n border-right: 1px solid #e9ecef;\n padding: 0.75rem 0;\n flex-shrink: 0;\n overflow-y: auto;\n }\n .snv-nav a {\n color: #495057;\n padding: 0.45rem 1.25rem;\n font-size: 0.85rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n text-decoration: none;\n cursor: pointer;\n }\n .snv-nav a:hover { background: #e9ecef; }\n .snv-nav a.active {\n background: #e7f1ff;\n color: #0d6efd;\n font-weight: 600;\n border-right: 2px solid #0d6efd;\n }\n .snv-nav a i { width: 18px; text-align: center; font-size: 0.9rem; }\n .snv-nav-label {\n font-size: 0.65rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: #adb5bd;\n padding: 0.75rem 1.25rem 0.25rem;\n }\n .snv-content {\n flex: 1;\n overflow-y: auto;\n padding: ${this.contentPadding};\n min-width: 0;\n }\n .snv-content > .snv-section { display: none; }\n .snv-content > .snv-section.snv-active { display: block; }\n .snv-dropdown { margin-bottom: 0.75rem; }\n .snv-select-btn {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n width: 100%;\n padding: 0.5rem 1rem;\n background: #f8f9fc;\n border: 1px solid #dee2e6;\n border-radius: 0.375rem;\n font-size: 0.85rem;\n color: #495057;\n cursor: pointer;\n }\n .snv-select-btn:hover { background: #e9ecef; }\n .snv-select-btn::after {\n content: '';\n display: inline-block;\n margin-left: auto;\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-left: 0.3em solid transparent;\n }\n @media (max-width: 576px) {\n .snv-nav { display: none; }\n .snv-content { padding: 1.25rem; }\n }\n </style>\n ${"dropdown"===this.currentMode?`\n <div class="snv-dropdown">${e}</div>\n <div class="snv-content" data-container="snv-content"></div>\n `:`\n <div class="snv-layout">\n <nav class="snv-nav">${e}</nav>\n <div class="snv-content" data-container="snv-content"></div>\n </div>\n `}\n `}_buildSidebarNav(){return this.sectionConfigs.map(e=>{if("divider"===e.type)return`<div class="snv-nav-label">${this.escapeHtml(e.label)}</div>`;const t=e.key===this.activeSection,i=e.icon?`<i class="bi ${e.icon}"></i>`:"";return`<a role="button" class="${t?"active":""}" data-action="navigate" data-section="${e.key}">${i} ${this.escapeHtml(e.label)}</a>`}).join("")}_buildDropdownNav(){const e=this.sectionConfigs.find(e=>e.key===this.activeSection),t=e?e.label:this.sectionKeys[0],i=this.sectionConfigs.filter(e=>"divider"!==e.type).map(e=>{const t=e.key===this.activeSection;return`\n <li>\n <button class="dropdown-item ${t?"active":""}"\n data-action="navigate"\n data-section="${e.key}"\n type="button">\n ${e.icon?`<i class="bi ${e.icon} me-2"></i>`:""}\n ${this.escapeHtml(e.label)}\n ${t?'<i class="bi bi-check-lg ms-2"></i>':""}\n </button>\n </li>\n `}).join("");return`\n <div class="dropdown">\n <button class="snv-select-btn" type="button"\n data-bs-toggle="dropdown" aria-expanded="false">\n ${e?.icon?`<i class="bi ${e.icon}"></i>`:""}\n <span>${this.escapeHtml(t)}</span>\n </button>\n <ul class="dropdown-menu w-100">${i}</ul>\n </div>\n `}async onAfterRender(){await super.onAfterRender(),this.activeSection&&await this._mountSection(this.activeSection),this.enableResponsive&&this._setupResponsive()}async onBeforeDestroy(){await super.onBeforeDestroy(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),"undefined"!=typeof window&&window.removeEventListener("resize",this.handleResize);for(const e of Object.values(this.sectionViews))e&&"function"==typeof e.destroy&&await e.destroy()}async showSection(e){if(!this.sectionViews[e])return console.warn(`SideNavView: Section "${e}" does not exist`),!1;if(e===this.activeSection){const t=this.sectionViews[e];if(t&&t.isMounted()&&this.element?.contains(t.element))return!0}const t=this.activeSection;this.activeSection=e,t&&t!==e&&await this._unmountSection(t),await this._mountSection(e);const i=this.sectionViews[e];return i?.onSectionActivated&&await i.onSectionActivated(),this._updateNavState(e),this.emit("section:changed",{activeSection:e,previousSection:t}),!0}async _mountSection(e){const t=this.sectionViews[e];if(!t)return;const i=this.element?.querySelector('[data-container="snv-content"]');if(i&&!t.isMounted()){this._showContentLoading(i);try{await t.render(!0,i)}finally{this._hideContentLoading(i)}}}_showContentLoading(e){if(!e)return;let t=e.querySelector(".snv-loading");t||(t=document.createElement("div"),t.className="snv-loading",t.innerHTML='<div class="spinner-border spinner-border-sm text-secondary" role="status"><span class="visually-hidden">Loading...</span></div>',t.style.cssText="display:flex;align-items:center;justify-content:center;padding:3rem;",e.prepend(t))}_hideContentLoading(e){if(!e)return;const t=e.querySelector(".snv-loading");t&&t.remove()}async _unmountSection(e){const t=this.sectionViews[e];t&&t.isMounted()&&await t.unmount()}_updateNavState(e){if(!this.element)return;this.element.querySelectorAll(".snv-nav a, .dropdown-item").forEach(t=>{const i=t.dataset.section;i&&t.classList.toggle("active",i===e)});const t=this.element.querySelector(".snv-select-btn span");if(t){const i=this.sectionConfigs.find(t=>t.key===e);i&&(t.textContent=i.label)}}async onActionNavigate(e,t){e.preventDefault();const i=t.dataset.section;return i&&await this.showSection(i),!0}_setupResponsive(){if(this.element&&this.enableResponsive)if(this._updateMode(),"undefined"!=typeof ResizeObserver){this.resizeObserver=new ResizeObserver(()=>{this.handleResize()});const e=this.element.parentElement||this.element;this.resizeObserver.observe(e)}else window.addEventListener("resize",this.handleResize)}async handleResize(){const e=this._getContainerWidth();Math.abs(e-this.lastContainerWidth)>50&&(this.lastContainerWidth=e,await this._updateMode())}_getContainerWidth(){return this.element&&(this.element.parentElement||this.element).offsetWidth||this.minWidth}async _updateMode(){const e=this._getContainerWidth(),t=e<this.minWidth?"dropdown":"sidebar";t!==this.currentMode&&(this.currentMode=t,this.isMounted()&&await this.render(),this.emit("navigation:modeChanged",{mode:this.currentMode,containerWidth:e}))}getActiveSection(){return this.activeSection}getSectionKeys(){return[...this.sectionKeys]}getSection(e){return this.sectionViews[e]||null}async addSection(e,t=!1){return e.key&&this.sectionViews[e.key]?(console.warn(`SideNavView: Section "${e.key}" already exists`),!1):(this._addSectionConfig(e),this.isMounted()&&(await this.render(),t&&e.key&&await this.showSection(e.key)),this.emit("section:added",{config:e}),!0)}async removeSection(e){const t=this.sectionViews[e];return t?("function"==typeof t.destroy&&await t.destroy(),delete this.sectionViews[e],this.sectionKeys=this.sectionKeys.filter(t=>t!==e),this.sectionConfigs=this.sectionConfigs.filter(t=>t.key!==e),this.activeSection===e&&(this.activeSection=this.sectionKeys[0]||null),this.isMounted()&&await this.render(),this.emit("section:removed",{key:e}),!0):(console.warn(`SideNavView: Section "${e}" does not exist`),!1)}_onModelChange(){}static create(e={}){return new SideNavView(e)}}class AdminMetadataSection extends e.View{constructor(e={}){super({className:"admin-metadata-section",template:'\n <style>\n .amd-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }\n .amd-header h6 { margin: 0; font-weight: 600; }\n .amd-list { border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; }\n .amd-item { display: flex; align-items: center; padding: 0.6rem 1rem; border-bottom: 1px solid #f0f0f0; gap: 0.75rem; }\n .amd-item:last-child { border-bottom: none; }\n .amd-key { font-family: ui-monospace, monospace; font-size: 0.82rem; font-weight: 600; color: #495057; min-width: 120px; flex-shrink: 0; }\n .amd-value { flex: 1; font-size: 0.85rem; color: #212529; word-break: break-word; min-width: 0; }\n .amd-actions { flex-shrink: 0; display: flex; gap: 0.25rem; }\n .amd-actions .btn { font-size: 0.7rem; padding: 0.15rem 0.35rem; }\n .amd-empty { padding: 2rem; text-align: center; color: #6c757d; font-size: 0.85rem; }\n .amd-empty i { font-size: 1.5rem; display: block; margin-bottom: 0.5rem; }\n </style>\n\n <div class="amd-header">\n <h6>Metadata</h6>\n <button type="button" class="btn btn-primary btn-sm" data-action="add-entry">\n <i class="bi bi-plus-lg me-1"></i>Add\n </button>\n </div>\n\n <div id="amd-entries"></div>\n ',...e})}onAfterRender(){this._renderEntries()}_getMetadata(){return this.model?.get("metadata")||{}}_renderEntries(){const e=this.element?.querySelector("#amd-entries");if(!e)return;const t=this._getMetadata(),i=Object.keys(t).sort();if(!i.length)return void(e.innerHTML='\n <div class="amd-list">\n <div class="amd-empty">\n <i class="bi bi-braces"></i>\n No metadata entries\n </div>\n </div>');const n=i.map(e=>{const i=t[e],n="object"==typeof i?JSON.stringify(i):String(i);return`\n <div class="amd-item">\n <div class="amd-key">${this._escapeHtml(e)}</div>\n <div class="amd-value">${this._escapeHtml(n)}</div>\n <div class="amd-actions">\n <button type="button" class="btn btn-outline-secondary" data-action="edit-entry" data-key="${this._escapeHtml(e)}" title="Edit"><i class="bi bi-pencil"></i></button>\n <button type="button" class="btn btn-outline-danger" data-action="remove-entry" data-key="${this._escapeHtml(e)}" title="Remove"><i class="bi bi-trash"></i></button>\n </div>\n </div>`}).join("");e.innerHTML=`<div class="amd-list">${n}</div>`}_escapeHtml(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}async onActionAddEntry(){const e=await n.Dialog.showForm({title:"Add Metadata Entry",icon:"bi-braces",size:"sm",fields:[{name:"key",type:"text",label:"Key",required:!0,placeholder:"e.g., timezone"},{name:"value",type:"text",label:"Value",required:!0,placeholder:"e.g., America/New_York"}]});if(!e)return!0;const t={...this._getMetadata()};try{t[e.key]=JSON.parse(e.value)}catch{t[e.key]=e.value}return 200===(await this.model.save({metadata:t})).status?(this.getApp()?.toast?.success("Metadata entry added"),this._renderEntries()):this.getApp()?.toast?.error("Failed to save metadata"),!0}async onActionEditEntry(e,t){const i=t.dataset.key;if(!i)return!0;const a=this._getMetadata(),s=a[i],o="object"==typeof s?JSON.stringify(s):String(s),d=await n.Dialog.showForm({title:`Edit "${i}"`,icon:"bi-braces",size:"sm",fields:[{name:"value",type:"text",label:"Value",required:!0,value:o}]});if(!d)return!0;const l={...a};try{l[i]=JSON.parse(d.value)}catch{l[i]=d.value}return 200===(await this.model.save({metadata:l})).status?(this.getApp()?.toast?.success("Metadata updated"),this._renderEntries()):this.getApp()?.toast?.error("Failed to save metadata"),!0}async onActionRemoveEntry(e,t){const i=t.dataset.key;if(!i)return!0;if(!(await n.Dialog.confirm(`Remove metadata key "<strong>${this._escapeHtml(i)}</strong>"?`,"Remove Entry")))return!0;const a={...this._getMetadata()};return delete a[i],200===(await this.model.save({metadata:a})).status?(this.getApp()?.toast?.success("Metadata entry removed"),this._renderEntries()):this.getApp()?.toast?.error("Failed to remove metadata entry"),!0}}class ApiKey extends e.Model{constructor(e={},t={}){super(e,{endpoint:"/api/group/apikey",...t})}}class ApiKeyList extends e.Collection{constructor(e={}){super({ModelClass:ApiKey,endpoint:"/api/group/apikey",size:25,...e})}}const s={create:{title:"Create API Key",fields:[{name:"name",type:"text",label:"Name",placeholder:"Mobile App v2",required:!0,columns:12,help:"A descriptive name to identify this key."},{name:"group",type:"number",label:"Group ID",required:!0,columns:12,help:"The group this key is scoped to."},{name:"permissions",type:"textarea",label:"Permissions (JSON)",placeholder:'{"view_orders": true, "create_orders": true}',columns:12,help:"JSON dict of permissions to grant. Leave empty for no permissions."}]},edit:{title:"Edit API Key",fields:[{name:"name",type:"text",label:"Name",required:!0,columns:12},{name:"is_active",type:"switch",label:"Active",columns:12,help:"Deactivate to revoke access without deleting the key."},{name:"permissions",type:"textarea",label:"Permissions (JSON)",columns:12,help:"JSON dict of granted permissions."}]}};class GroupView extends e.View{constructor(e={}){super({className:"group-view",...e}),this.model=e.model||new n.Group(e.data||{}),this.template='\n <div class="group-view-container">\n \x3c!-- Header --\x3e\n <div data-container="group-header"></div>\n \x3c!-- Side Nav --\x3e\n <div data-container="group-sidenav" style="min-height: 400px;"></div>\n </div>\n '}async onInit(){this.header=new e.View({containerId:"group-header",template:'\n <div class="d-flex justify-content-between align-items-start mb-4">\n \x3c!-- Left Side: Identity --\x3e\n <div class="d-flex align-items-center gap-3">\n {{#model.avatar}}\n {{{model.avatar|avatar(\'md\',\'rounded\')}}}\n {{/model.avatar}}\n {{^model.avatar}}\n <div class="d-flex align-items-center justify-content-center rounded bg-light" style="width: 56px; height: 56px;">\n <i class="bi bi-people text-secondary" style="font-size: 1.5rem;"></i>\n </div>\n {{/model.avatar}}\n <div>\n <h3 class="mb-0">{{model.name|default(\'Unnamed Group\')}}</h3>\n <div class="d-flex align-items-center gap-2 mt-1">\n <span class="badge bg-primary bg-opacity-10 text-primary" style="font-size: 0.72rem;">{{model.kind|capitalize}}</span>\n {{#model.parent}}\n <span class="text-muted small">\n <i class="bi bi-diagram-3 me-1"></i>\n <a href="#" data-action="view-parent" data-id="{{model.parent.id}}" class="text-decoration-none">{{model.parent.name}}</a>\n </span>\n {{/model.parent}}\n </div>\n {{#model.metadata.timezone}}\n <div class="text-muted small mt-1"><i class="bi bi-clock me-1"></i>{{model.metadata.timezone}}</div>\n {{/model.metadata.timezone}}\n </div>\n </div>\n\n \x3c!-- Right Side: Status & Actions --\x3e\n <div class="d-flex align-items-start gap-4">\n <div class="text-end">\n <div class="d-flex align-items-center justify-content-end gap-3">\n <span class="d-inline-flex align-items-center gap-1" title="{{model.is_active|boolean(\'Group Active\',\'Group Inactive\')}}">\n <i class="bi {{model.is_active|boolean(\'bi-toggle-on text-success\',\'bi-toggle-off text-secondary\')}}" style="font-size: 1.1rem;"></i>\n <span class="small">{{model.is_active|boolean(\'Active\',\'Inactive\')}}</span>\n </span>\n </div>\n {{#model.last_activity}}\n <div class="text-muted small mt-1">Last active {{model.last_activity|relative}}</div>\n {{/model.last_activity}}\n {{#model.created}}\n <div class="text-muted small mt-1">Created {{model.created|date}}</div>\n {{/model.created}}\n </div>\n <div data-container="group-context-menu"></div>\n </div>\n </div>'}),this.header.setModel(this.model),this.addChild(this.header);const o=new e.View({model:this.model,template:'\n <style>\n .gv-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.5rem; }\n .gv-section-label:first-child { margin-top: 0; }\n .gv-field-row { display: flex; align-items: baseline; padding: 0.5rem 0; border-bottom: 1px solid #f0f0f0; }\n .gv-field-row:last-child { border-bottom: none; }\n .gv-field-label { width: 140px; font-size: 0.78rem; color: #6c757d; flex-shrink: 0; }\n .gv-field-value { flex: 1; font-size: 0.88rem; color: #212529; }\n .gv-field-action { color: #6c757d; cursor: pointer; font-size: 0.8rem; margin-left: auto; padding: 0.15rem 0.4rem; border-radius: 4px; background: none; border: none; }\n .gv-field-action:hover { background: #f0f0f0; color: #0d6efd; }\n </style>\n\n <div class="gv-section-label">Group</div>\n <div class="gv-field-row">\n <div class="gv-field-label">Name</div>\n <div class="gv-field-value">{{model.name}}</div>\n <button type="button" class="gv-field-action" data-action="edit-group" title="Edit"><i class="bi bi-pencil"></i></button>\n </div>\n <div class="gv-field-row">\n <div class="gv-field-label">Kind</div>\n <div class="gv-field-value"><span class="badge bg-primary bg-opacity-10 text-primary">{{model.kind|capitalize}}</span></div>\n </div>\n <div class="gv-field-row">\n <div class="gv-field-label">Status</div>\n <div class="gv-field-value">\n {{#model.is_active|bool}}<span style="font-size:0.65rem; padding:0.15em 0.45em; background:#d1e7dd; color:#0f5132; border-radius:3px;">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span style="font-size:0.65rem; padding:0.15em 0.45em; background:#fff3cd; color:#856404; border-radius:3px;">Inactive</span>{{/model.is_active|bool}}\n </div>\n </div>\n <div class="gv-field-row">\n <div class="gv-field-label">ID</div>\n <div class="gv-field-value" style="font-family: ui-monospace, monospace; font-size: 0.82rem;">{{model.id}}</div>\n </div>\n\n <div class="gv-section-label">Hierarchy</div>\n <div class="gv-field-row">\n <div class="gv-field-label">Parent</div>\n <div class="gv-field-value">\n {{#model.parent}}\n <a href="#" data-action="view-parent" data-id="{{model.parent.id}}" class="text-decoration-none">{{model.parent.name}}</a>\n <span class="text-muted small ms-1">({{model.parent.kind|capitalize}})</span>\n {{/model.parent}}\n {{^model.parent}}<span style="color:#adb5bd; font-style:italic; font-size:0.85rem;">None — top-level group</span>{{/model.parent}}\n </div>\n </div>\n\n <div class="gv-section-label">Settings</div>\n {{#model.metadata.timezone}}\n <div class="gv-field-row">\n <div class="gv-field-label">Timezone</div>\n <div class="gv-field-value">{{model.metadata.timezone}}</div>\n </div>\n {{/model.metadata.timezone}}\n {{#model.metadata.domain}}\n <div class="gv-field-row">\n <div class="gv-field-label">Domain</div>\n <div class="gv-field-value">{{model.metadata.domain}}</div>\n </div>\n {{/model.metadata.domain}}\n {{#model.metadata.portal}}\n <div class="gv-field-row">\n <div class="gv-field-label">Portal URL</div>\n <div class="gv-field-value"><a href="{{model.metadata.portal}}" target="_blank" class="text-decoration-none">{{model.metadata.portal}}</a></div>\n </div>\n {{/model.metadata.portal}}\n {{#model.metadata.eod_hour}}\n <div class="gv-field-row">\n <div class="gv-field-label">End of Day</div>\n <div class="gv-field-value">{{model.metadata.eod_hour}}:00</div>\n </div>\n {{/model.metadata.eod_hour}}\n\n <div class="gv-section-label">Dates</div>\n <div class="gv-field-row">\n <div class="gv-field-label">Created</div>\n <div class="gv-field-value">{{model.created|datetime|default(\'—\')}}</div>\n </div>\n <div class="gv-field-row">\n <div class="gv-field-label">Modified</div>\n <div class="gv-field-value">{{model.modified|datetime|default(\'—\')}}</div>\n </div>\n '}),d=new t.TableView({collection:new t.MemberList({params:{group:this.model.get("id"),size:10}}),hideActivePillNames:["group"],clickAction:"view",showAdd:!0,addButtonLabel:"Invite",onAdd:e=>this.onInviteClick(e),columns:[{key:"user.display_name",label:"User",sortable:!0},{key:"user.email",label:"Email",sortable:!0},{key:"permissions|keys|badge",label:"Permissions"},{key:"created",label:"Joined",formatter:"date",sortable:!0}]}),l=new t.TableView({collection:new n.GroupList({params:{parent:this.model.get("id"),size:10}}),hideActivePillNames:["parent"],clickAction:"view",showAdd:!0,addButtonLabel:"Add Group",onAdd:()=>this.onActionAddChildGroup(),columns:[{key:"name",label:"Name",sortable:!0},{key:"kind",label:"Kind",formatter:"badge"},{key:"is_active",label:"Status",width:"80px",template:'\n {{#model.is_active|bool}}<span class="badge bg-success bg-opacity-10 text-success">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span class="badge bg-secondary bg-opacity-10 text-secondary">Inactive</span>{{/model.is_active|bool}}'},{key:"created",label:"Created",formatter:"date",sortable:!0}]}),r=new t.TableView({collection:new a.IncidentEventList({params:{size:10,model_name:"account.Group",model_id:this.model.get("id")}}),hideActivePillNames:["model_name","model_id"],columns:[{key:"created",label:"Date",formatter:"datetime",sortable:!0,width:"150px"},{key:"category|badge",label:"Category"},{key:"title",label:"Title"}]}),c=new t.TableView({collection:new ApiKeyList({params:{group:this.model.get("id"),size:10}}),hideActivePillNames:["group"],clickAction:"view",showAdd:!0,addButtonLabel:"Create Key",addFormConfig:{...s.create,defaults:{group:this.model.get("id")}},columns:[{key:"name",label:"Name",sortable:!0},{key:"is_active",label:"Status",width:"80px",template:'\n {{#model.is_active|bool}}<span class="badge bg-success bg-opacity-10 text-success">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span class="badge bg-secondary bg-opacity-10 text-secondary">Inactive</span>{{/model.is_active|bool}}'},{key:"permissions|keys|badge",label:"Permissions"},{key:"created",label:"Created",formatter:"datetime",sortable:!0}]}),m=new AdminMetadataSection({model:this.model}),v=new t.TableView({collection:new t.LogList({params:{size:10,model_name:"account.Group",model_id:this.model.get("id")}}),permissions:"view_logs",hideActivePillNames:["model_name","model_id"],columns:[{key:"created",label:"Timestamp",sortable:!0,formatter:"epoch|datetime",filter:{name:"created",type:"daterange",startName:"dr_start",endName:"dr_end",fieldName:"dr_field",label:"Date Range",format:"YYYY-MM-DD",displayFormat:"MMM DD, YYYY",separator:" to "}},{key:"level",label:"Level",sortable:!0,filter:{type:"select",options:[{value:"info",label:"Info"},{value:"warning",label:"Warning"},{value:"error",label:"Error"}]}},{key:"kind",label:"Kind",filter:{type:"text"}},{name:"log",label:"Log"}]});this.sideNavView=new SideNavView({containerId:"group-sidenav",activeSection:"details",navWidth:180,contentPadding:"1.25rem 2rem",enableResponsive:!0,minWidth:500,sections:[{key:"details",label:"Details",icon:"bi-info-circle",view:o},{key:"members",label:"Members",icon:"bi-people",view:d},{key:"children",label:"Sub-Groups",icon:"bi-diagram-3",view:l},{key:"api_keys",label:"API Keys",icon:"bi-key",view:c},{type:"divider",label:"Activity"},{key:"events",label:"Events",icon:"bi-calendar-event",view:r},{key:"logs",label:"Logs",icon:"bi-journal-text",view:v,permissions:"view_logs"},{type:"divider",label:"Settings"},{key:"metadata",label:"Metadata",icon:"bi-braces",view:m}]}),this.addChild(this.sideNavView);const p=new i.ContextMenu({containerId:"group-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit Group",action:"edit-group",icon:"bi-pencil"},{type:"divider"},{label:"Invite Member",action:"invite-member",icon:"bi-person-plus"},{label:"Add Sub-Group",action:"add-child-group",icon:"bi-diagram-3"},{type:"divider"},this.model.get("is_active")?{label:"Deactivate Group",action:"deactivate-group",icon:"bi-toggle-off"}:{label:"Activate Group",action:"activate-group",icon:"bi-toggle-on"}]}});this.addChild(p)}async onActionEditGroup(){await n.Dialog.showModelForm({title:`Edit Group — ${this.model.get("name")}`,model:this.model,size:"lg",formConfig:n.GroupForms.detailed})&&await this.render()}async onActionInviteMember(){return this.onInviteClick(new Event("click"))}async onInviteClick(e){e?.preventDefault&&(e.preventDefault(),e.stopPropagation());const t=await n.Dialog.showForm({title:`Invite User to ${this.model.get("name")}`,size:"sm",fields:[{type:"email",name:"email",label:"Email",required:!0,cols:12}]});if(!t?.email)return!0;const i=this.getApp(),a=await i.rest.POST("/api/group/member/invite",{group:this.model.id,email:t.email});return a.success?(i.toast.success("User invited successfully"),"members"===this.sideNavView?.getActiveSection()&&await this.sideNavView.showSection("members")):i.toast.error(a.message||"Failed to invite user"),!0}async onActionAddChildGroup(){const e=await n.Dialog.showForm({title:`Add Sub-Group to ${this.model.get("name")}`,size:"sm",fields:n.GroupForms.create.fields.filter(e=>"parent"!==e.name)});if(!e)return!0;e.parent=this.model.id;const t=new n.Group(e),i=await t.save();return 200===i.status||201===i.status?(this.getApp()?.toast?.success("Sub-group created"),"children"===this.sideNavView?.getActiveSection()&&await this.sideNavView.showSection("children")):this.getApp()?.toast?.error(i.message||"Failed to create sub-group"),!0}async onActionDeactivateGroup(){return!(await n.Dialog.confirm(`Are you sure you want to deactivate <strong>${this.model.get("name")}</strong>?`,"Deactivate Group"))||(200===(await this.model.save({is_active:!1})).status?(this.getApp()?.toast?.success("Group deactivated"),await this.render()):this.getApp()?.toast?.error("Failed to deactivate group"),!0)}async onActionActivateGroup(){return!(await n.Dialog.confirm(`Are you sure you want to activate <strong>${this.model.get("name")}</strong>?`,"Activate Group"))||(200===(await this.model.save({is_active:!0})).status?(this.getApp()?.toast?.success("Group activated"),await this.render()):this.getApp()?.toast?.error("Failed to activate group"),!0)}async onActionViewParent(e,t){const i=t?.dataset?.id;if(!i)return!0;const a=new n.Group({id:i});return await a.fetch(),a.id&&n.Dialog.showDialog({title:!1,size:"lg",body:new GroupView({model:a}),buttons:[{text:"Close",class:"btn-secondary",dismiss:!0}]}),!0}async showSection(e){this.sideNavView&&await this.sideNavView.showSection(e)}getActiveSection(){return this.sideNavView?this.sideNavView.getActiveSection():null}_onModelChange(){}static create(e={}){return new GroupView(e)}}n.Group.VIEW_CLASS=GroupView;const o=/* @__PURE__ */Object.freeze(/* @__PURE__ */Object.defineProperty({__proto__:null,default:GroupView},Symbol.toStringTag,{value:"Module"}));exports.AdminMetadataSection=AdminMetadataSection,exports.ApiKey=ApiKey,exports.ApiKeyForms=s,exports.ApiKeyList=ApiKeyList,exports.GroupView=GroupView,exports.GroupView$1=o,exports.SideNavView=SideNavView;
|
|
2
|
+
//# sourceMappingURL=GroupView-DAzE4A-4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GroupView-DAzE4A-4.js","sources":["../../src/core/views/navigation/SideNavView.js","../../src/extensions/admin/shared/AdminMetadataSection.js","../../src/core/models/ApiKey.js","../../src/extensions/admin/account/groups/GroupView.js"],"sourcesContent":["/**\n * SideNavView - Left sidebar navigation with content panel\n *\n * A reusable navigation component that displays a vertical sidebar with\n * nav links, optional group labels, and icons. The content panel mounts\n * one child view at a time, switching on nav click.\n *\n * Features:\n * - Left sidebar with nav links, icons, and group dividers\n * - Active state with accent border\n * - Mount/unmount child views on section switch\n * - Responsive: collapses to dropdown on narrow containers\n * - Permission-aware: skips sections the user lacks permission for\n * - Configurable nav width and content padding\n * - Smooth fade transitions between sections\n *\n * Example Usage:\n * ```javascript\n * const sideNav = new SideNavView({\n * sections: [\n * { key: 'profile', label: 'Profile', icon: 'bi-person', view: profileView },\n * { key: 'security', label: 'Security', icon: 'bi-shield-lock', view: securityView },\n * { type: 'divider', label: 'Activity' },\n * { key: 'sessions', label: 'Sessions', icon: 'bi-clock-history', view: sessionsView },\n * ],\n * activeSection: 'profile',\n * navWidth: 200,\n * contentPadding: '1.5rem 2.5rem',\n * enableResponsive: true\n * });\n * ```\n */\n\nimport View from '@core/View.js';\n\nclass SideNavView extends View {\n constructor(options = {}) {\n const {\n sections = [],\n activeSection,\n navWidth,\n contentPadding,\n enableResponsive,\n minWidth,\n ...viewOptions\n } = options;\n\n super({\n tagName: 'div',\n className: 'side-nav-view',\n ...viewOptions\n });\n\n // Configuration\n this.navWidth = navWidth || 200;\n this.contentPadding = contentPadding || '1.5rem 2.5rem';\n this.enableResponsive = enableResponsive !== false;\n this.minWidth = minWidth || 500;\n\n // State\n this.sectionConfigs = []; // Full config array (including dividers)\n this.sectionViews = {}; // key → view instance\n this.sectionKeys = []; // Ordered navigable section keys\n this.activeSection = null;\n this.currentMode = 'sidebar'; // 'sidebar' or 'dropdown'\n this.resizeObserver = null;\n this.lastContainerWidth = 0;\n\n // Process sections config\n for (const config of sections) {\n this._addSectionConfig(config);\n }\n\n // Set initial active section\n this.activeSection = activeSection || this.sectionKeys[0] || null;\n\n // Bind resize handler\n this.handleResize = this.handleResize.bind(this);\n }\n\n /**\n * Process and store a section config entry\n * @param {object} config - Section config (navigable or divider)\n * @private\n */\n _addSectionConfig(config) {\n if (config.type === 'divider') {\n this.sectionConfigs.push({ type: 'divider', label: config.label });\n return;\n }\n\n // Skip if user lacks required permission\n if (config.permissions && !this._hasPermission(config.permissions)) {\n return;\n }\n\n this.sectionConfigs.push(config);\n this.sectionKeys.push(config.key);\n\n if (config.view) {\n this.sectionViews[config.key] = config.view;\n config.view.parent = this;\n }\n }\n\n /**\n * Check if the current user has a permission\n * @param {string} perm - Permission string\n * @returns {boolean}\n * @private\n */\n _hasPermission(perm) {\n try {\n return this.getApp().activeUser.hasPerm(perm);\n } catch {\n return true; // If app isn't available yet, allow — will be checked at render\n }\n }\n\n // ───────────────────────────────────────────────\n // Template\n // ───────────────────────────────────────────────\n\n async renderTemplate() {\n const nav = this.currentMode === 'dropdown'\n ? this._buildDropdownNav()\n : this._buildSidebarNav();\n\n return `\n <style>\n .snv-layout { display: flex; height: 100%; min-height: 0; }\n .snv-nav {\n width: ${this.navWidth}px;\n background: #f8f9fc;\n border-right: 1px solid #e9ecef;\n padding: 0.75rem 0;\n flex-shrink: 0;\n overflow-y: auto;\n }\n .snv-nav a {\n color: #495057;\n padding: 0.45rem 1.25rem;\n font-size: 0.85rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n text-decoration: none;\n cursor: pointer;\n }\n .snv-nav a:hover { background: #e9ecef; }\n .snv-nav a.active {\n background: #e7f1ff;\n color: #0d6efd;\n font-weight: 600;\n border-right: 2px solid #0d6efd;\n }\n .snv-nav a i { width: 18px; text-align: center; font-size: 0.9rem; }\n .snv-nav-label {\n font-size: 0.65rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: #adb5bd;\n padding: 0.75rem 1.25rem 0.25rem;\n }\n .snv-content {\n flex: 1;\n overflow-y: auto;\n padding: ${this.contentPadding};\n min-width: 0;\n }\n .snv-content > .snv-section { display: none; }\n .snv-content > .snv-section.snv-active { display: block; }\n .snv-dropdown { margin-bottom: 0.75rem; }\n .snv-select-btn {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n width: 100%;\n padding: 0.5rem 1rem;\n background: #f8f9fc;\n border: 1px solid #dee2e6;\n border-radius: 0.375rem;\n font-size: 0.85rem;\n color: #495057;\n cursor: pointer;\n }\n .snv-select-btn:hover { background: #e9ecef; }\n .snv-select-btn::after {\n content: '';\n display: inline-block;\n margin-left: auto;\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-left: 0.3em solid transparent;\n }\n @media (max-width: 576px) {\n .snv-nav { display: none; }\n .snv-content { padding: 1.25rem; }\n }\n </style>\n ${this.currentMode === 'dropdown' ? `\n <div class=\"snv-dropdown\">${nav}</div>\n <div class=\"snv-content\" data-container=\"snv-content\"></div>\n ` : `\n <div class=\"snv-layout\">\n <nav class=\"snv-nav\">${nav}</nav>\n <div class=\"snv-content\" data-container=\"snv-content\"></div>\n </div>\n `}\n `;\n }\n\n /**\n * Build sidebar navigation HTML\n * @returns {string}\n * @private\n */\n _buildSidebarNav() {\n return this.sectionConfigs.map(config => {\n if (config.type === 'divider') {\n return `<div class=\"snv-nav-label\">${this.escapeHtml(config.label)}</div>`;\n }\n const isActive = config.key === this.activeSection;\n const icon = config.icon ? `<i class=\"bi ${config.icon}\"></i>` : '';\n return `<a role=\"button\" class=\"${isActive ? 'active' : ''}\" data-action=\"navigate\" data-section=\"${config.key}\">${icon} ${this.escapeHtml(config.label)}</a>`;\n }).join('');\n }\n\n /**\n * Build dropdown navigation HTML (responsive mode)\n * @returns {string}\n * @private\n */\n _buildDropdownNav() {\n const activeConfig = this.sectionConfigs.find(c => c.key === this.activeSection);\n const activeLabel = activeConfig ? activeConfig.label : this.sectionKeys[0];\n\n const items = this.sectionConfigs\n .filter(c => c.type !== 'divider')\n .map(config => {\n const isActive = config.key === this.activeSection;\n return `\n <li>\n <button class=\"dropdown-item ${isActive ? 'active' : ''}\"\n data-action=\"navigate\"\n data-section=\"${config.key}\"\n type=\"button\">\n ${config.icon ? `<i class=\"bi ${config.icon} me-2\"></i>` : ''}\n ${this.escapeHtml(config.label)}\n ${isActive ? '<i class=\"bi bi-check-lg ms-2\"></i>' : ''}\n </button>\n </li>\n `;\n }).join('');\n\n return `\n <div class=\"dropdown\">\n <button class=\"snv-select-btn\" type=\"button\"\n data-bs-toggle=\"dropdown\" aria-expanded=\"false\">\n ${activeConfig?.icon ? `<i class=\"bi ${activeConfig.icon}\"></i>` : ''}\n <span>${this.escapeHtml(activeLabel)}</span>\n </button>\n <ul class=\"dropdown-menu w-100\">${items}</ul>\n </div>\n `;\n }\n\n // ───────────────────────────────────────────────\n // Lifecycle\n // ───────────────────────────────────────────────\n\n async onAfterRender() {\n await super.onAfterRender();\n\n // Mount the active section\n if (this.activeSection) {\n await this._mountSection(this.activeSection);\n }\n\n // Set up responsive behavior\n if (this.enableResponsive) {\n this._setupResponsive();\n }\n }\n\n async onBeforeDestroy() {\n await super.onBeforeDestroy();\n\n // Clean up resize observer\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = null;\n }\n\n if (typeof window !== 'undefined') {\n window.removeEventListener('resize', this.handleResize);\n }\n\n // Destroy all section views\n for (const view of Object.values(this.sectionViews)) {\n if (view && typeof view.destroy === 'function') {\n await view.destroy();\n }\n }\n }\n\n // ───────────────────────────────────────────────\n // Section switching\n // ───────────────────────────────────────────────\n\n /**\n * Navigate to a section\n * @param {string} key - Section key\n * @returns {Promise<boolean>}\n */\n async showSection(key) {\n if (!this.sectionViews[key]) {\n console.warn(`SideNavView: Section \"${key}\" does not exist`);\n return false;\n }\n\n if (key === this.activeSection) {\n // Already active — but ensure it's mounted\n const view = this.sectionViews[key];\n if (view && view.isMounted() && this.element?.contains(view.element)) {\n return true;\n }\n }\n\n const previousSection = this.activeSection;\n this.activeSection = key;\n\n // Unmount previous section\n if (previousSection && previousSection !== key) {\n await this._unmountSection(previousSection);\n }\n\n // Mount new section\n await this._mountSection(key);\n\n // Call onSectionActivated hook after the view is mounted and visible\n const activeView = this.sectionViews[key];\n if (activeView?.onSectionActivated) {\n await activeView.onSectionActivated();\n }\n\n // Update nav visual state\n this._updateNavState(key);\n\n this.emit('section:changed', {\n activeSection: key,\n previousSection\n });\n\n return true;\n }\n\n /**\n * Mount a section view into the content area\n * @param {string} key - Section key\n * @private\n */\n async _mountSection(key) {\n const view = this.sectionViews[key];\n if (!view) return;\n\n const container = this.element?.querySelector('[data-container=\"snv-content\"]');\n if (!container) return;\n\n if (!view.isMounted()) {\n this._showContentLoading(container);\n try {\n await view.render(true, container);\n } finally {\n this._hideContentLoading(container);\n }\n }\n }\n\n /**\n * Show a lightweight spinner in the content panel\n * @param {HTMLElement} container\n * @private\n */\n _showContentLoading(container) {\n if (!container) return;\n let spinner = container.querySelector('.snv-loading');\n if (!spinner) {\n spinner = document.createElement('div');\n spinner.className = 'snv-loading';\n spinner.innerHTML = '<div class=\"spinner-border spinner-border-sm text-secondary\" role=\"status\"><span class=\"visually-hidden\">Loading...</span></div>';\n spinner.style.cssText = 'display:flex;align-items:center;justify-content:center;padding:3rem;';\n container.prepend(spinner);\n }\n }\n\n /**\n * Remove the content panel spinner\n * @param {HTMLElement} container\n * @private\n */\n _hideContentLoading(container) {\n if (!container) return;\n const spinner = container.querySelector('.snv-loading');\n if (spinner) spinner.remove();\n }\n\n /**\n * Unmount a section view\n * @param {string} key - Section key\n * @private\n */\n async _unmountSection(key) {\n const view = this.sectionViews[key];\n if (!view || !view.isMounted()) return;\n\n await view.unmount();\n }\n\n /**\n * Update nav link active state\n * @param {string} activeKey - Active section key\n * @private\n */\n _updateNavState(activeKey) {\n if (!this.element) return;\n\n // Update sidebar links\n this.element.querySelectorAll('.snv-nav a, .dropdown-item').forEach(link => {\n const section = link.dataset.section;\n if (section) {\n link.classList.toggle('active', section === activeKey);\n }\n });\n\n // Update dropdown button label\n const selectBtn = this.element.querySelector('.snv-select-btn span');\n if (selectBtn) {\n const config = this.sectionConfigs.find(c => c.key === activeKey);\n if (config) {\n selectBtn.textContent = config.label;\n }\n }\n }\n\n // ───────────────────────────────────────────────\n // Action handlers\n // ───────────────────────────────────────────────\n\n async onActionNavigate(event, el) {\n event.preventDefault();\n const section = el.dataset.section;\n if (section) {\n await this.showSection(section);\n }\n return true;\n }\n\n // ───────────────────────────────────────────────\n // Responsive\n // ───────────────────────────────────────────────\n\n /**\n * Set up responsive width detection\n * @private\n */\n _setupResponsive() {\n if (!this.element || !this.enableResponsive) return;\n\n this._updateMode();\n\n if (typeof ResizeObserver !== 'undefined') {\n this.resizeObserver = new ResizeObserver(() => {\n this.handleResize();\n });\n const container = this.element.parentElement || this.element;\n this.resizeObserver.observe(container);\n } else {\n window.addEventListener('resize', this.handleResize);\n }\n }\n\n /**\n * Handle resize events\n */\n async handleResize() {\n const containerWidth = this._getContainerWidth();\n if (Math.abs(containerWidth - this.lastContainerWidth) > 50) {\n this.lastContainerWidth = containerWidth;\n await this._updateMode();\n }\n }\n\n /**\n * Get the container width\n * @returns {number}\n * @private\n */\n _getContainerWidth() {\n if (!this.element) return this.minWidth;\n const container = this.element.parentElement || this.element;\n return container.offsetWidth || this.minWidth;\n }\n\n /**\n * Check and switch between sidebar and dropdown modes\n * @private\n */\n async _updateMode() {\n const containerWidth = this._getContainerWidth();\n const newMode = containerWidth < this.minWidth ? 'dropdown' : 'sidebar';\n\n if (newMode !== this.currentMode) {\n this.currentMode = newMode;\n if (this.isMounted()) {\n await this.render();\n }\n this.emit('navigation:modeChanged', {\n mode: this.currentMode,\n containerWidth\n });\n }\n }\n\n // ───────────────────────────────────────────────\n // Public API\n // ───────────────────────────────────────────────\n\n /**\n * Get the active section key\n * @returns {string|null}\n */\n getActiveSection() {\n return this.activeSection;\n }\n\n /**\n * Get all navigable section keys\n * @returns {string[]}\n */\n getSectionKeys() {\n return [...this.sectionKeys];\n }\n\n /**\n * Get a section's view by key\n * @param {string} key - Section key\n * @returns {View|null}\n */\n getSection(key) {\n return this.sectionViews[key] || null;\n }\n\n /**\n * Add a section dynamically\n * @param {object} config - Section config\n * @param {boolean} makeActive - Whether to activate the section\n * @returns {Promise<boolean>}\n */\n async addSection(config, makeActive = false) {\n if (config.key && this.sectionViews[config.key]) {\n console.warn(`SideNavView: Section \"${config.key}\" already exists`);\n return false;\n }\n\n this._addSectionConfig(config);\n\n if (this.isMounted()) {\n await this.render();\n if (makeActive && config.key) {\n await this.showSection(config.key);\n }\n }\n\n this.emit('section:added', { config });\n return true;\n }\n\n /**\n * Remove a section dynamically\n * @param {string} key - Section key to remove\n * @returns {Promise<boolean>}\n */\n async removeSection(key) {\n const view = this.sectionViews[key];\n if (!view) {\n console.warn(`SideNavView: Section \"${key}\" does not exist`);\n return false;\n }\n\n // Destroy the view\n if (typeof view.destroy === 'function') {\n await view.destroy();\n }\n\n // Remove from data structures\n delete this.sectionViews[key];\n this.sectionKeys = this.sectionKeys.filter(k => k !== key);\n this.sectionConfigs = this.sectionConfigs.filter(c => c.key !== key);\n\n // Handle active section removal\n if (this.activeSection === key) {\n this.activeSection = this.sectionKeys[0] || null;\n }\n\n if (this.isMounted()) {\n await this.render();\n }\n\n this.emit('section:removed', { key });\n return true;\n }\n\n /**\n * Prevent model changes from triggering a full re-render.\n * Section views manage their own model reactivity.\n */\n _onModelChange() {\n // no-op — same pattern as UserView\n }\n\n static create(options = {}) {\n return new SideNavView(options);\n }\n}\n\nexport default SideNavView;\n","/**\n * AdminMetadataSection - View/edit metadata on any model with a metadata field\n *\n * Displays metadata as a key-value list with add/edit/remove capability.\n * Saves changes via standard model CRUD (model.save()).\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\n\nexport default class AdminMetadataSection extends View {\n constructor(options = {}) {\n super({\n className: 'admin-metadata-section',\n template: `\n <style>\n .amd-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }\n .amd-header h6 { margin: 0; font-weight: 600; }\n .amd-list { border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; }\n .amd-item { display: flex; align-items: center; padding: 0.6rem 1rem; border-bottom: 1px solid #f0f0f0; gap: 0.75rem; }\n .amd-item:last-child { border-bottom: none; }\n .amd-key { font-family: ui-monospace, monospace; font-size: 0.82rem; font-weight: 600; color: #495057; min-width: 120px; flex-shrink: 0; }\n .amd-value { flex: 1; font-size: 0.85rem; color: #212529; word-break: break-word; min-width: 0; }\n .amd-actions { flex-shrink: 0; display: flex; gap: 0.25rem; }\n .amd-actions .btn { font-size: 0.7rem; padding: 0.15rem 0.35rem; }\n .amd-empty { padding: 2rem; text-align: center; color: #6c757d; font-size: 0.85rem; }\n .amd-empty i { font-size: 1.5rem; display: block; margin-bottom: 0.5rem; }\n </style>\n\n <div class=\"amd-header\">\n <h6>Metadata</h6>\n <button type=\"button\" class=\"btn btn-primary btn-sm\" data-action=\"add-entry\">\n <i class=\"bi bi-plus-lg me-1\"></i>Add\n </button>\n </div>\n\n <div id=\"amd-entries\"></div>\n `,\n ...options\n });\n }\n\n onAfterRender() {\n this._renderEntries();\n }\n\n _getMetadata() {\n return this.model?.get('metadata') || {};\n }\n\n _renderEntries() {\n const container = this.element?.querySelector('#amd-entries');\n if (!container) return;\n\n const metadata = this._getMetadata();\n const keys = Object.keys(metadata).sort();\n\n if (!keys.length) {\n container.innerHTML = `\n <div class=\"amd-list\">\n <div class=\"amd-empty\">\n <i class=\"bi bi-braces\"></i>\n No metadata entries\n </div>\n </div>`;\n return;\n }\n\n const rows = keys.map(key => {\n const val = metadata[key];\n const display = typeof val === 'object' ? JSON.stringify(val) : String(val);\n return `\n <div class=\"amd-item\">\n <div class=\"amd-key\">${this._escapeHtml(key)}</div>\n <div class=\"amd-value\">${this._escapeHtml(display)}</div>\n <div class=\"amd-actions\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"edit-entry\" data-key=\"${this._escapeHtml(key)}\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n <button type=\"button\" class=\"btn btn-outline-danger\" data-action=\"remove-entry\" data-key=\"${this._escapeHtml(key)}\" title=\"Remove\"><i class=\"bi bi-trash\"></i></button>\n </div>\n </div>`;\n }).join('');\n\n container.innerHTML = `<div class=\"amd-list\">${rows}</div>`;\n }\n\n _escapeHtml(str) {\n const div = document.createElement('div');\n div.textContent = str;\n return div.innerHTML;\n }\n\n async onActionAddEntry() {\n const data = await Dialog.showForm({\n title: 'Add Metadata Entry',\n icon: 'bi-braces',\n size: 'sm',\n fields: [\n { name: 'key', type: 'text', label: 'Key', required: true, placeholder: 'e.g., timezone' },\n { name: 'value', type: 'text', label: 'Value', required: true, placeholder: 'e.g., America/New_York' }\n ]\n });\n if (!data) return true;\n\n const metadata = { ...this._getMetadata() };\n // Try to parse JSON values\n try {\n metadata[data.key] = JSON.parse(data.value);\n } catch {\n metadata[data.key] = data.value;\n }\n\n const resp = await this.model.save({ metadata });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Metadata entry added');\n this._renderEntries();\n } else {\n this.getApp()?.toast?.error('Failed to save metadata');\n }\n return true;\n }\n\n async onActionEditEntry(event, el) {\n const key = el.dataset.key;\n if (!key) return true;\n\n const metadata = this._getMetadata();\n const currentValue = metadata[key];\n const displayValue = typeof currentValue === 'object' ? JSON.stringify(currentValue) : String(currentValue);\n\n const data = await Dialog.showForm({\n title: `Edit \"${key}\"`,\n icon: 'bi-braces',\n size: 'sm',\n fields: [\n { name: 'value', type: 'text', label: 'Value', required: true, value: displayValue }\n ]\n });\n if (!data) return true;\n\n const updated = { ...metadata };\n try {\n updated[key] = JSON.parse(data.value);\n } catch {\n updated[key] = data.value;\n }\n\n const resp = await this.model.save({ metadata: updated });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Metadata updated');\n this._renderEntries();\n } else {\n this.getApp()?.toast?.error('Failed to save metadata');\n }\n return true;\n }\n\n async onActionRemoveEntry(event, el) {\n const key = el.dataset.key;\n if (!key) return true;\n\n const confirmed = await Dialog.confirm(\n `Remove metadata key \"<strong>${this._escapeHtml(key)}</strong>\"?`,\n 'Remove Entry'\n );\n if (!confirmed) return true;\n\n const updated = { ...this._getMetadata() };\n delete updated[key];\n\n const resp = await this.model.save({ metadata: updated });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Metadata entry removed');\n this._renderEntries();\n } else {\n this.getApp()?.toast?.error('Failed to remove metadata entry');\n }\n return true;\n }\n}\n","import Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\n\n/**\n * ApiKey - Group-scoped API key for external integrations and services.\n * Maps to REST endpoints under /api/group/apikey\n *\n * Key properties:\n * - Scoped to a single group\n * - Carries only explicitly granted permissions (least-privilege)\n * - sys.* permissions always denied\n * - No IP restriction (unlike User Auth Tokens)\n * - Header format: Authorization: apikey <token>\n *\n * The raw token is only returned at creation time — it is never shown again.\n *\n * Endpoints:\n * GET /api/group/apikey - List keys (filter by ?group=<id>)\n * POST /api/group/apikey - Create a key\n * GET /api/group/apikey/<id> - Get key details\n * POST /api/group/apikey/<id> - Update name, permissions, limits, is_active\n * DELETE /api/group/apikey/<id> - Delete key\n */\nclass ApiKey extends Model {\n constructor(data = {}, options = {}) {\n super(data, {\n endpoint: '/api/group/apikey',\n ...options\n });\n }\n}\n\n/**\n * ApiKeyList - Collection of ApiKey records.\n * Filter by group: new ApiKeyList({ params: { group: groupId } })\n */\nclass ApiKeyList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: ApiKey,\n endpoint: '/api/group/apikey',\n size: 25,\n ...options\n });\n }\n}\n\n/**\n * Forms configuration for ApiKey\n */\nconst ApiKeyForms = {\n create: {\n title: 'Create API Key',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Name',\n placeholder: 'Mobile App v2',\n required: true,\n columns: 12,\n help: 'A descriptive name to identify this key.'\n },\n {\n name: 'group',\n type: 'number',\n label: 'Group ID',\n required: true,\n columns: 12,\n help: 'The group this key is scoped to.'\n },\n {\n name: 'permissions',\n type: 'textarea',\n label: 'Permissions (JSON)',\n placeholder: '{\"view_orders\": true, \"create_orders\": true}',\n columns: 12,\n help: 'JSON dict of permissions to grant. Leave empty for no permissions.'\n }\n ]\n },\n\n edit: {\n title: 'Edit API Key',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Name',\n required: true,\n columns: 12\n },\n {\n name: 'is_active',\n type: 'switch',\n label: 'Active',\n columns: 12,\n help: 'Deactivate to revoke access without deleting the key.'\n },\n {\n name: 'permissions',\n type: 'textarea',\n label: 'Permissions (JSON)',\n columns: 12,\n help: 'JSON dict of granted permissions.'\n }\n ]\n }\n};\n\nexport { ApiKey, ApiKeyList, ApiKeyForms };\n","/**\n * GroupView - Modern group management interface\n *\n * Features:\n * - Clean header with avatar, name, kind badge, parent link, active/online status\n * - SideNavView with: Details, Members, Children, Events, Logs\n * - Expanded context menu with quick actions\n * - Clean Bootstrap 5 styling matching UserView patterns\n */\n\nimport View from '@core/View.js';\nimport SideNavView from '@core/views/navigation/SideNavView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { Group, GroupList, GroupForms } from '@core/models/Group.js';\nimport { MemberList } from '@core/models/Member.js';\nimport { IncidentEventList } from '@core/models/Incident.js';\nimport { LogList } from '@core/models/Log.js';\nimport { ApiKeyList, ApiKeyForms } from '@core/models/ApiKey.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport AdminMetadataSection from '../../shared/AdminMetadataSection.js';\n\nclass GroupView extends View {\n constructor(options = {}) {\n super({\n className: 'group-view',\n ...options\n });\n\n this.model = options.model || new Group(options.data || {});\n\n this.template = `\n <div class=\"group-view-container\">\n <!-- Header -->\n <div data-container=\"group-header\"></div>\n <!-- Side Nav -->\n <div data-container=\"group-sidenav\" style=\"min-height: 400px;\"></div>\n </div>\n `;\n }\n\n async onInit() {\n // ── Header ──────────────────────────────────\n this.header = new View({\n containerId: 'group-header',\n template: `\n <div class=\"d-flex justify-content-between align-items-start mb-4\">\n <!-- Left Side: Identity -->\n <div class=\"d-flex align-items-center gap-3\">\n {{#model.avatar}}\n {{{model.avatar|avatar('md','rounded')}}}\n {{/model.avatar}}\n {{^model.avatar}}\n <div class=\"d-flex align-items-center justify-content-center rounded bg-light\" style=\"width: 56px; height: 56px;\">\n <i class=\"bi bi-people text-secondary\" style=\"font-size: 1.5rem;\"></i>\n </div>\n {{/model.avatar}}\n <div>\n <h3 class=\"mb-0\">{{model.name|default('Unnamed Group')}}</h3>\n <div class=\"d-flex align-items-center gap-2 mt-1\">\n <span class=\"badge bg-primary bg-opacity-10 text-primary\" style=\"font-size: 0.72rem;\">{{model.kind|capitalize}}</span>\n {{#model.parent}}\n <span class=\"text-muted small\">\n <i class=\"bi bi-diagram-3 me-1\"></i>\n <a href=\"#\" data-action=\"view-parent\" data-id=\"{{model.parent.id}}\" class=\"text-decoration-none\">{{model.parent.name}}</a>\n </span>\n {{/model.parent}}\n </div>\n {{#model.metadata.timezone}}\n <div class=\"text-muted small mt-1\"><i class=\"bi bi-clock me-1\"></i>{{model.metadata.timezone}}</div>\n {{/model.metadata.timezone}}\n </div>\n </div>\n\n <!-- Right Side: Status & Actions -->\n <div class=\"d-flex align-items-start gap-4\">\n <div class=\"text-end\">\n <div class=\"d-flex align-items-center justify-content-end gap-3\">\n <span class=\"d-inline-flex align-items-center gap-1\" title=\"{{model.is_active|boolean('Group Active','Group Inactive')}}\">\n <i class=\"bi {{model.is_active|boolean('bi-toggle-on text-success','bi-toggle-off text-secondary')}}\" style=\"font-size: 1.1rem;\"></i>\n <span class=\"small\">{{model.is_active|boolean('Active','Inactive')}}</span>\n </span>\n </div>\n {{#model.last_activity}}\n <div class=\"text-muted small mt-1\">Last active {{model.last_activity|relative}}</div>\n {{/model.last_activity}}\n {{#model.created}}\n <div class=\"text-muted small mt-1\">Created {{model.created|date}}</div>\n {{/model.created}}\n </div>\n <div data-container=\"group-context-menu\"></div>\n </div>\n </div>`\n });\n this.header.setModel(this.model);\n this.addChild(this.header);\n\n // ── Details section ─────────────────────────\n const detailsView = new View({\n model: this.model,\n template: `\n <style>\n .gv-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.5rem; }\n .gv-section-label:first-child { margin-top: 0; }\n .gv-field-row { display: flex; align-items: baseline; padding: 0.5rem 0; border-bottom: 1px solid #f0f0f0; }\n .gv-field-row:last-child { border-bottom: none; }\n .gv-field-label { width: 140px; font-size: 0.78rem; color: #6c757d; flex-shrink: 0; }\n .gv-field-value { flex: 1; font-size: 0.88rem; color: #212529; }\n .gv-field-action { color: #6c757d; cursor: pointer; font-size: 0.8rem; margin-left: auto; padding: 0.15rem 0.4rem; border-radius: 4px; background: none; border: none; }\n .gv-field-action:hover { background: #f0f0f0; color: #0d6efd; }\n </style>\n\n <div class=\"gv-section-label\">Group</div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Name</div>\n <div class=\"gv-field-value\">{{model.name}}</div>\n <button type=\"button\" class=\"gv-field-action\" data-action=\"edit-group\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Kind</div>\n <div class=\"gv-field-value\"><span class=\"badge bg-primary bg-opacity-10 text-primary\">{{model.kind|capitalize}}</span></div>\n </div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Status</div>\n <div class=\"gv-field-value\">\n {{#model.is_active|bool}}<span style=\"font-size:0.65rem; padding:0.15em 0.45em; background:#d1e7dd; color:#0f5132; border-radius:3px;\">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span style=\"font-size:0.65rem; padding:0.15em 0.45em; background:#fff3cd; color:#856404; border-radius:3px;\">Inactive</span>{{/model.is_active|bool}}\n </div>\n </div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">ID</div>\n <div class=\"gv-field-value\" style=\"font-family: ui-monospace, monospace; font-size: 0.82rem;\">{{model.id}}</div>\n </div>\n\n <div class=\"gv-section-label\">Hierarchy</div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Parent</div>\n <div class=\"gv-field-value\">\n {{#model.parent}}\n <a href=\"#\" data-action=\"view-parent\" data-id=\"{{model.parent.id}}\" class=\"text-decoration-none\">{{model.parent.name}}</a>\n <span class=\"text-muted small ms-1\">({{model.parent.kind|capitalize}})</span>\n {{/model.parent}}\n {{^model.parent}}<span style=\"color:#adb5bd; font-style:italic; font-size:0.85rem;\">None — top-level group</span>{{/model.parent}}\n </div>\n </div>\n\n <div class=\"gv-section-label\">Settings</div>\n {{#model.metadata.timezone}}\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Timezone</div>\n <div class=\"gv-field-value\">{{model.metadata.timezone}}</div>\n </div>\n {{/model.metadata.timezone}}\n {{#model.metadata.domain}}\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Domain</div>\n <div class=\"gv-field-value\">{{model.metadata.domain}}</div>\n </div>\n {{/model.metadata.domain}}\n {{#model.metadata.portal}}\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Portal URL</div>\n <div class=\"gv-field-value\"><a href=\"{{model.metadata.portal}}\" target=\"_blank\" class=\"text-decoration-none\">{{model.metadata.portal}}</a></div>\n </div>\n {{/model.metadata.portal}}\n {{#model.metadata.eod_hour}}\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">End of Day</div>\n <div class=\"gv-field-value\">{{model.metadata.eod_hour}}:00</div>\n </div>\n {{/model.metadata.eod_hour}}\n\n <div class=\"gv-section-label\">Dates</div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Created</div>\n <div class=\"gv-field-value\">{{model.created|datetime|default('—')}}</div>\n </div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Modified</div>\n <div class=\"gv-field-value\">{{model.modified|datetime|default('—')}}</div>\n </div>\n `\n });\n\n // ── Members ─────────────────────────────────\n const membersView = new TableView({\n collection: new MemberList({ params: { group: this.model.get('id'), size: 10 } }),\n hideActivePillNames: ['group'],\n clickAction: 'view',\n showAdd: true,\n addButtonLabel: 'Invite',\n onAdd: (event) => this.onInviteClick(event),\n columns: [\n { key: 'user.display_name', label: 'User', sortable: true },\n { key: 'user.email', label: 'Email', sortable: true },\n { key: 'permissions|keys|badge', label: 'Permissions' },\n { key: 'created', label: 'Joined', formatter: 'date', sortable: true }\n ]\n });\n\n // ── Children (sub-groups) ───────────────────\n const childrenView = new TableView({\n collection: new GroupList({ params: { parent: this.model.get('id'), size: 10 } }),\n hideActivePillNames: ['parent'],\n clickAction: 'view',\n showAdd: true,\n addButtonLabel: 'Add Group',\n onAdd: () => this.onActionAddChildGroup(),\n columns: [\n { key: 'name', label: 'Name', sortable: true },\n { key: 'kind', label: 'Kind', formatter: 'badge' },\n {\n key: 'is_active', label: 'Status', width: '80px',\n template: `\n {{#model.is_active|bool}}<span class=\"badge bg-success bg-opacity-10 text-success\">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span class=\"badge bg-secondary bg-opacity-10 text-secondary\">Inactive</span>{{/model.is_active|bool}}`\n },\n { key: 'created', label: 'Created', formatter: 'date', sortable: true }\n ]\n });\n\n // ── Events ──────────────────────────────────\n const eventsView = new TableView({\n collection: new IncidentEventList({\n params: { size: 10, model_name: 'account.Group', model_id: this.model.get('id') }\n }),\n hideActivePillNames: ['model_name', 'model_id'],\n columns: [\n { key: 'created', label: 'Date', formatter: 'datetime', sortable: true, width: '150px' },\n { key: 'category|badge', label: 'Category' },\n { key: 'title', label: 'Title' }\n ]\n });\n\n // ── API Keys ──────────────────────────────────\n const apiKeysView = new TableView({\n collection: new ApiKeyList({ params: { group: this.model.get('id'), size: 10 } }),\n hideActivePillNames: ['group'],\n clickAction: 'view',\n showAdd: true,\n addButtonLabel: 'Create Key',\n addFormConfig: {\n ...ApiKeyForms.create,\n defaults: { group: this.model.get('id') }\n },\n columns: [\n { key: 'name', label: 'Name', sortable: true },\n {\n key: 'is_active', label: 'Status', width: '80px',\n template: `\n {{#model.is_active|bool}}<span class=\"badge bg-success bg-opacity-10 text-success\">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span class=\"badge bg-secondary bg-opacity-10 text-secondary\">Inactive</span>{{/model.is_active|bool}}`\n },\n { key: 'permissions|keys|badge', label: 'Permissions' },\n { key: 'created', label: 'Created', formatter: 'datetime', sortable: true }\n ]\n });\n\n // ── Metadata ─────────────────────────────────\n const metadataView = new AdminMetadataSection({ model: this.model });\n\n // ── Logs ────────────────────────────────────\n const logsView = new TableView({\n collection: new LogList({\n params: { size: 10, model_name: 'account.Group', model_id: this.model.get('id') }\n }),\n permissions: 'view_logs',\n hideActivePillNames: ['model_name', 'model_id'],\n columns: [\n {\n key: 'created', label: 'Timestamp', sortable: true, formatter: 'epoch|datetime',\n filter: { name: 'created', type: 'daterange', startName: 'dr_start', endName: 'dr_end', fieldName: 'dr_field', label: 'Date Range', format: 'YYYY-MM-DD', displayFormat: 'MMM DD, YYYY', separator: ' to ' }\n },\n {\n key: 'level', label: 'Level', sortable: true,\n filter: { type: 'select', options: [{ value: 'info', label: 'Info' }, { value: 'warning', label: 'Warning' }, { value: 'error', label: 'Error' }] }\n },\n { key: 'kind', label: 'Kind', filter: { type: 'text' } },\n { name: 'log', label: 'Log' }\n ]\n });\n\n // ── SideNavView ─────────────────────────────\n this.sideNavView = new SideNavView({\n containerId: 'group-sidenav',\n activeSection: 'details',\n navWidth: 180,\n contentPadding: '1.25rem 2rem',\n enableResponsive: true,\n minWidth: 500,\n sections: [\n { key: 'details', label: 'Details', icon: 'bi-info-circle', view: detailsView },\n { key: 'members', label: 'Members', icon: 'bi-people', view: membersView },\n { key: 'children', label: 'Sub-Groups', icon: 'bi-diagram-3', view: childrenView },\n { key: 'api_keys', label: 'API Keys', icon: 'bi-key', view: apiKeysView },\n { type: 'divider', label: 'Activity' },\n { key: 'events', label: 'Events', icon: 'bi-calendar-event', view: eventsView },\n { key: 'logs', label: 'Logs', icon: 'bi-journal-text', view: logsView, permissions: 'view_logs' },\n { type: 'divider', label: 'Settings' },\n { key: 'metadata', label: 'Metadata', icon: 'bi-braces', view: metadataView }\n ]\n });\n this.addChild(this.sideNavView);\n\n // ── Context Menu ────────────────────────────\n const groupMenu = new ContextMenu({\n containerId: 'group-context-menu',\n className: 'context-menu-view header-menu-absolute',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'Edit Group', action: 'edit-group', icon: 'bi-pencil' },\n { type: 'divider' },\n { label: 'Invite Member', action: 'invite-member', icon: 'bi-person-plus' },\n { label: 'Add Sub-Group', action: 'add-child-group', icon: 'bi-diagram-3' },\n { type: 'divider' },\n this.model.get('is_active')\n ? { label: 'Deactivate Group', action: 'deactivate-group', icon: 'bi-toggle-off' }\n : { label: 'Activate Group', action: 'activate-group', icon: 'bi-toggle-on' },\n ]\n }\n });\n this.addChild(groupMenu);\n }\n\n // ── Actions ─────────────────────────────────\n\n async onActionEditGroup() {\n const resp = await Dialog.showModelForm({\n title: `Edit Group — ${this.model.get('name')}`,\n model: this.model,\n size: 'lg',\n formConfig: GroupForms.detailed,\n });\n if (resp) {\n await this.render();\n }\n }\n\n async onActionInviteMember() {\n return this.onInviteClick(new Event('click'));\n }\n\n async onInviteClick(event) {\n if (event?.preventDefault) {\n event.preventDefault();\n event.stopPropagation();\n }\n const data = await Dialog.showForm({\n title: `Invite User to ${this.model.get('name')}`,\n size: 'sm',\n fields: [\n { type: 'email', name: 'email', label: 'Email', required: true, cols: 12 }\n ]\n });\n if (!data?.email) return true;\n\n const app = this.getApp();\n const resp = await app.rest.POST('/api/group/member/invite', {\n group: this.model.id,\n email: data.email\n });\n if (resp.success) {\n app.toast.success('User invited successfully');\n // Refresh members if on that section\n if (this.sideNavView?.getActiveSection() === 'members') {\n await this.sideNavView.showSection('members');\n }\n } else {\n app.toast.error(resp.message || 'Failed to invite user');\n }\n return true;\n }\n\n async onActionAddChildGroup() {\n const data = await Dialog.showForm({\n title: `Add Sub-Group to ${this.model.get('name')}`,\n size: 'sm',\n fields: GroupForms.create.fields.filter(f => f.name !== 'parent')\n });\n if (!data) return true;\n\n data.parent = this.model.id;\n const newGroup = new Group(data);\n const resp = await newGroup.save();\n if (resp.status === 200 || resp.status === 201) {\n this.getApp()?.toast?.success('Sub-group created');\n if (this.sideNavView?.getActiveSection() === 'children') {\n await this.sideNavView.showSection('children');\n }\n } else {\n this.getApp()?.toast?.error(resp.message || 'Failed to create sub-group');\n }\n return true;\n }\n\n async onActionDeactivateGroup() {\n const confirmed = await Dialog.confirm(\n `Are you sure you want to deactivate <strong>${this.model.get('name')}</strong>?`,\n 'Deactivate Group'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.save({ is_active: false });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Group deactivated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to deactivate group');\n }\n return true;\n }\n\n async onActionActivateGroup() {\n const confirmed = await Dialog.confirm(\n `Are you sure you want to activate <strong>${this.model.get('name')}</strong>?`,\n 'Activate Group'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.save({ is_active: true });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Group activated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to activate group');\n }\n return true;\n }\n\n async onActionViewParent(event, element) {\n const parentId = element?.dataset?.id;\n if (!parentId) return true;\n\n const parent = new Group({ id: parentId });\n await parent.fetch();\n if (parent.id) {\n Dialog.showDialog({\n title: false,\n size: 'lg',\n body: new GroupView({ model: parent }),\n buttons: [{ text: 'Close', class: 'btn-secondary', dismiss: true }]\n });\n }\n return true;\n }\n\n // ── Navigation helpers ──────────────────────\n\n async showSection(sectionName) {\n if (this.sideNavView) {\n await this.sideNavView.showSection(sectionName);\n }\n }\n\n getActiveSection() {\n return this.sideNavView ? this.sideNavView.getActiveSection() : null;\n }\n\n _onModelChange() {\n // Prevent full re-render on model changes\n }\n\n static create(options = {}) {\n return new GroupView(options);\n }\n}\n\nGroup.VIEW_CLASS = GroupView;\n\nexport default GroupView;\n"],"names":["SideNavView","View","constructor","options","sections","activeSection","navWidth","contentPadding","enableResponsive","minWidth","viewOptions","super","tagName","className","this","sectionConfigs","sectionViews","sectionKeys","currentMode","resizeObserver","lastContainerWidth","config","_addSectionConfig","handleResize","bind","type","permissions","_hasPermission","push","key","view","parent","label","perm","getApp","activeUser","hasPerm","renderTemplate","nav","_buildDropdownNav","_buildSidebarNav","map","escapeHtml","isActive","icon","join","activeConfig","find","c","activeLabel","items","filter","onAfterRender","_mountSection","_setupResponsive","onBeforeDestroy","disconnect","window","removeEventListener","Object","values","destroy","showSection","console","warn","isMounted","element","contains","previousSection","_unmountSection","activeView","onSectionActivated","_updateNavState","emit","container","querySelector","_showContentLoading","render","_hideContentLoading","spinner","document","createElement","innerHTML","style","cssText","prepend","remove","unmount","activeKey","querySelectorAll","forEach","link","section","dataset","classList","toggle","selectBtn","textContent","onActionNavigate","event","el","preventDefault","_updateMode","ResizeObserver","parentElement","observe","addEventListener","containerWidth","_getContainerWidth","Math","abs","offsetWidth","newMode","mode","getActiveSection","getSectionKeys","getSection","addSection","makeActive","removeSection","k","_onModelChange","create","AdminMetadataSection","template","_renderEntries","_getMetadata","model","get","metadata","keys","sort","length","rows","val","display","JSON","stringify","String","_escapeHtml","str","div","onActionAddEntry","data","Dialog","showForm","title","size","fields","name","required","placeholder","parse","value","save","status","toast","success","error","onActionEditEntry","currentValue","displayValue","updated","onActionRemoveEntry","confirm","ApiKey","Model","endpoint","ApiKeyList","Collection","ModelClass","ApiKeyForms","columns","help","edit","GroupView","Group","onInit","header","containerId","setModel","addChild","detailsView","membersView","TableView","collection","MemberList","params","group","hideActivePillNames","clickAction","showAdd","addButtonLabel","onAdd","onInviteClick","sortable","formatter","childrenView","GroupList","onActionAddChildGroup","width","eventsView","IncidentEventList","model_name","model_id","apiKeysView","addFormConfig","defaults","metadataView","logsView","LogList","startName","endName","fieldName","format","displayFormat","separator","sideNavView","groupMenu","ContextMenu","context","action","onActionEditGroup","showModelForm","formConfig","GroupForms","detailed","onActionInviteMember","Event","stopPropagation","cols","email","app","resp","rest","POST","id","message","f","newGroup","onActionDeactivateGroup","is_active","onActionActivateGroup","onActionViewParent","parentId","fetch","showDialog","body","buttons","text","class","dismiss","sectionName","VIEW_CLASS"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{V as e,C as t,M as i}from"./Collection-dB21lBnQ.js";import{T as n,M as a,L as s}from"./Passkeys-ZNOvvdIC.js";import{C as o}from"./ContextMenu-DA7wKiGO.js";import{D as d,a as l,G as r,b as c}from"./Dialog-DZuk-4Ck.js";import{I as m}from"./ChatView-DhV0Ycul.js";class SideNavView extends e{constructor(e={}){const{sections:t=[],activeSection:i,navWidth:n,contentPadding:a,enableResponsive:s,minWidth:o,...d}=e;super({tagName:"div",className:"side-nav-view",...d}),this.navWidth=n||200,this.contentPadding=a||"1.5rem 2.5rem",this.enableResponsive=!1!==s,this.minWidth=o||500,this.sectionConfigs=[],this.sectionViews={},this.sectionKeys=[],this.activeSection=null,this.currentMode="sidebar",this.resizeObserver=null,this.lastContainerWidth=0;for(const l of t)this._addSectionConfig(l);this.activeSection=i||this.sectionKeys[0]||null,this.handleResize=this.handleResize.bind(this)}_addSectionConfig(e){"divider"!==e.type?e.permissions&&!this._hasPermission(e.permissions)||(this.sectionConfigs.push(e),this.sectionKeys.push(e.key),e.view&&(this.sectionViews[e.key]=e.view,e.view.parent=this)):this.sectionConfigs.push({type:"divider",label:e.label})}_hasPermission(e){try{return this.getApp().activeUser.hasPerm(e)}catch{return!0}}async renderTemplate(){const e="dropdown"===this.currentMode?this._buildDropdownNav():this._buildSidebarNav();return`\n <style>\n .snv-layout { display: flex; height: 100%; min-height: 0; }\n .snv-nav {\n width: ${this.navWidth}px;\n background: #f8f9fc;\n border-right: 1px solid #e9ecef;\n padding: 0.75rem 0;\n flex-shrink: 0;\n overflow-y: auto;\n }\n .snv-nav a {\n color: #495057;\n padding: 0.45rem 1.25rem;\n font-size: 0.85rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n text-decoration: none;\n cursor: pointer;\n }\n .snv-nav a:hover { background: #e9ecef; }\n .snv-nav a.active {\n background: #e7f1ff;\n color: #0d6efd;\n font-weight: 600;\n border-right: 2px solid #0d6efd;\n }\n .snv-nav a i { width: 18px; text-align: center; font-size: 0.9rem; }\n .snv-nav-label {\n font-size: 0.65rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: #adb5bd;\n padding: 0.75rem 1.25rem 0.25rem;\n }\n .snv-content {\n flex: 1;\n overflow-y: auto;\n padding: ${this.contentPadding};\n min-width: 0;\n }\n .snv-content > .snv-section { display: none; }\n .snv-content > .snv-section.snv-active { display: block; }\n .snv-dropdown { margin-bottom: 0.75rem; }\n .snv-select-btn {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n width: 100%;\n padding: 0.5rem 1rem;\n background: #f8f9fc;\n border: 1px solid #dee2e6;\n border-radius: 0.375rem;\n font-size: 0.85rem;\n color: #495057;\n cursor: pointer;\n }\n .snv-select-btn:hover { background: #e9ecef; }\n .snv-select-btn::after {\n content: '';\n display: inline-block;\n margin-left: auto;\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-left: 0.3em solid transparent;\n }\n @media (max-width: 576px) {\n .snv-nav { display: none; }\n .snv-content { padding: 1.25rem; }\n }\n </style>\n ${"dropdown"===this.currentMode?`\n <div class="snv-dropdown">${e}</div>\n <div class="snv-content" data-container="snv-content"></div>\n `:`\n <div class="snv-layout">\n <nav class="snv-nav">${e}</nav>\n <div class="snv-content" data-container="snv-content"></div>\n </div>\n `}\n `}_buildSidebarNav(){return this.sectionConfigs.map(e=>{if("divider"===e.type)return`<div class="snv-nav-label">${this.escapeHtml(e.label)}</div>`;const t=e.key===this.activeSection,i=e.icon?`<i class="bi ${e.icon}"></i>`:"";return`<a role="button" class="${t?"active":""}" data-action="navigate" data-section="${e.key}">${i} ${this.escapeHtml(e.label)}</a>`}).join("")}_buildDropdownNav(){const e=this.sectionConfigs.find(e=>e.key===this.activeSection),t=e?e.label:this.sectionKeys[0],i=this.sectionConfigs.filter(e=>"divider"!==e.type).map(e=>{const t=e.key===this.activeSection;return`\n <li>\n <button class="dropdown-item ${t?"active":""}"\n data-action="navigate"\n data-section="${e.key}"\n type="button">\n ${e.icon?`<i class="bi ${e.icon} me-2"></i>`:""}\n ${this.escapeHtml(e.label)}\n ${t?'<i class="bi bi-check-lg ms-2"></i>':""}\n </button>\n </li>\n `}).join("");return`\n <div class="dropdown">\n <button class="snv-select-btn" type="button"\n data-bs-toggle="dropdown" aria-expanded="false">\n ${e?.icon?`<i class="bi ${e.icon}"></i>`:""}\n <span>${this.escapeHtml(t)}</span>\n </button>\n <ul class="dropdown-menu w-100">${i}</ul>\n </div>\n `}async onAfterRender(){await super.onAfterRender(),this.activeSection&&await this._mountSection(this.activeSection),this.enableResponsive&&this._setupResponsive()}async onBeforeDestroy(){await super.onBeforeDestroy(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),"undefined"!=typeof window&&window.removeEventListener("resize",this.handleResize);for(const e of Object.values(this.sectionViews))e&&"function"==typeof e.destroy&&await e.destroy()}async showSection(e){if(!this.sectionViews[e])return console.warn(`SideNavView: Section "${e}" does not exist`),!1;if(e===this.activeSection){const t=this.sectionViews[e];if(t&&t.isMounted()&&this.element?.contains(t.element))return!0}const t=this.activeSection;this.activeSection=e,t&&t!==e&&await this._unmountSection(t),await this._mountSection(e);const i=this.sectionViews[e];return i?.onSectionActivated&&await i.onSectionActivated(),this._updateNavState(e),this.emit("section:changed",{activeSection:e,previousSection:t}),!0}async _mountSection(e){const t=this.sectionViews[e];if(!t)return;const i=this.element?.querySelector('[data-container="snv-content"]');if(i&&!t.isMounted()){this._showContentLoading(i);try{await t.render(!0,i)}finally{this._hideContentLoading(i)}}}_showContentLoading(e){if(!e)return;let t=e.querySelector(".snv-loading");t||(t=document.createElement("div"),t.className="snv-loading",t.innerHTML='<div class="spinner-border spinner-border-sm text-secondary" role="status"><span class="visually-hidden">Loading...</span></div>',t.style.cssText="display:flex;align-items:center;justify-content:center;padding:3rem;",e.prepend(t))}_hideContentLoading(e){if(!e)return;const t=e.querySelector(".snv-loading");t&&t.remove()}async _unmountSection(e){const t=this.sectionViews[e];t&&t.isMounted()&&await t.unmount()}_updateNavState(e){if(!this.element)return;this.element.querySelectorAll(".snv-nav a, .dropdown-item").forEach(t=>{const i=t.dataset.section;i&&t.classList.toggle("active",i===e)});const t=this.element.querySelector(".snv-select-btn span");if(t){const i=this.sectionConfigs.find(t=>t.key===e);i&&(t.textContent=i.label)}}async onActionNavigate(e,t){e.preventDefault();const i=t.dataset.section;return i&&await this.showSection(i),!0}_setupResponsive(){if(this.element&&this.enableResponsive)if(this._updateMode(),"undefined"!=typeof ResizeObserver){this.resizeObserver=new ResizeObserver(()=>{this.handleResize()});const e=this.element.parentElement||this.element;this.resizeObserver.observe(e)}else window.addEventListener("resize",this.handleResize)}async handleResize(){const e=this._getContainerWidth();Math.abs(e-this.lastContainerWidth)>50&&(this.lastContainerWidth=e,await this._updateMode())}_getContainerWidth(){return this.element&&(this.element.parentElement||this.element).offsetWidth||this.minWidth}async _updateMode(){const e=this._getContainerWidth(),t=e<this.minWidth?"dropdown":"sidebar";t!==this.currentMode&&(this.currentMode=t,this.isMounted()&&await this.render(),this.emit("navigation:modeChanged",{mode:this.currentMode,containerWidth:e}))}getActiveSection(){return this.activeSection}getSectionKeys(){return[...this.sectionKeys]}getSection(e){return this.sectionViews[e]||null}async addSection(e,t=!1){return e.key&&this.sectionViews[e.key]?(console.warn(`SideNavView: Section "${e.key}" already exists`),!1):(this._addSectionConfig(e),this.isMounted()&&(await this.render(),t&&e.key&&await this.showSection(e.key)),this.emit("section:added",{config:e}),!0)}async removeSection(e){const t=this.sectionViews[e];return t?("function"==typeof t.destroy&&await t.destroy(),delete this.sectionViews[e],this.sectionKeys=this.sectionKeys.filter(t=>t!==e),this.sectionConfigs=this.sectionConfigs.filter(t=>t.key!==e),this.activeSection===e&&(this.activeSection=this.sectionKeys[0]||null),this.isMounted()&&await this.render(),this.emit("section:removed",{key:e}),!0):(console.warn(`SideNavView: Section "${e}" does not exist`),!1)}_onModelChange(){}static create(e={}){return new SideNavView(e)}}class AdminMetadataSection extends e{constructor(e={}){super({className:"admin-metadata-section",template:'\n <style>\n .amd-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }\n .amd-header h6 { margin: 0; font-weight: 600; }\n .amd-list { border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; }\n .amd-item { display: flex; align-items: center; padding: 0.6rem 1rem; border-bottom: 1px solid #f0f0f0; gap: 0.75rem; }\n .amd-item:last-child { border-bottom: none; }\n .amd-key { font-family: ui-monospace, monospace; font-size: 0.82rem; font-weight: 600; color: #495057; min-width: 120px; flex-shrink: 0; }\n .amd-value { flex: 1; font-size: 0.85rem; color: #212529; word-break: break-word; min-width: 0; }\n .amd-actions { flex-shrink: 0; display: flex; gap: 0.25rem; }\n .amd-actions .btn { font-size: 0.7rem; padding: 0.15rem 0.35rem; }\n .amd-empty { padding: 2rem; text-align: center; color: #6c757d; font-size: 0.85rem; }\n .amd-empty i { font-size: 1.5rem; display: block; margin-bottom: 0.5rem; }\n </style>\n\n <div class="amd-header">\n <h6>Metadata</h6>\n <button type="button" class="btn btn-primary btn-sm" data-action="add-entry">\n <i class="bi bi-plus-lg me-1"></i>Add\n </button>\n </div>\n\n <div id="amd-entries"></div>\n ',...e})}onAfterRender(){this._renderEntries()}_getMetadata(){return this.model?.get("metadata")||{}}_renderEntries(){const e=this.element?.querySelector("#amd-entries");if(!e)return;const t=this._getMetadata(),i=Object.keys(t).sort();if(!i.length)return void(e.innerHTML='\n <div class="amd-list">\n <div class="amd-empty">\n <i class="bi bi-braces"></i>\n No metadata entries\n </div>\n </div>');const n=i.map(e=>{const i=t[e],n="object"==typeof i?JSON.stringify(i):String(i);return`\n <div class="amd-item">\n <div class="amd-key">${this._escapeHtml(e)}</div>\n <div class="amd-value">${this._escapeHtml(n)}</div>\n <div class="amd-actions">\n <button type="button" class="btn btn-outline-secondary" data-action="edit-entry" data-key="${this._escapeHtml(e)}" title="Edit"><i class="bi bi-pencil"></i></button>\n <button type="button" class="btn btn-outline-danger" data-action="remove-entry" data-key="${this._escapeHtml(e)}" title="Remove"><i class="bi bi-trash"></i></button>\n </div>\n </div>`}).join("");e.innerHTML=`<div class="amd-list">${n}</div>`}_escapeHtml(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}async onActionAddEntry(){const e=await d.showForm({title:"Add Metadata Entry",icon:"bi-braces",size:"sm",fields:[{name:"key",type:"text",label:"Key",required:!0,placeholder:"e.g., timezone"},{name:"value",type:"text",label:"Value",required:!0,placeholder:"e.g., America/New_York"}]});if(!e)return!0;const t={...this._getMetadata()};try{t[e.key]=JSON.parse(e.value)}catch{t[e.key]=e.value}return 200===(await this.model.save({metadata:t})).status?(this.getApp()?.toast?.success("Metadata entry added"),this._renderEntries()):this.getApp()?.toast?.error("Failed to save metadata"),!0}async onActionEditEntry(e,t){const i=t.dataset.key;if(!i)return!0;const n=this._getMetadata(),a=n[i],s="object"==typeof a?JSON.stringify(a):String(a),o=await d.showForm({title:`Edit "${i}"`,icon:"bi-braces",size:"sm",fields:[{name:"value",type:"text",label:"Value",required:!0,value:s}]});if(!o)return!0;const l={...n};try{l[i]=JSON.parse(o.value)}catch{l[i]=o.value}return 200===(await this.model.save({metadata:l})).status?(this.getApp()?.toast?.success("Metadata updated"),this._renderEntries()):this.getApp()?.toast?.error("Failed to save metadata"),!0}async onActionRemoveEntry(e,t){const i=t.dataset.key;if(!i)return!0;if(!(await d.confirm(`Remove metadata key "<strong>${this._escapeHtml(i)}</strong>"?`,"Remove Entry")))return!0;const n={...this._getMetadata()};return delete n[i],200===(await this.model.save({metadata:n})).status?(this.getApp()?.toast?.success("Metadata entry removed"),this._renderEntries()):this.getApp()?.toast?.error("Failed to remove metadata entry"),!0}}class ApiKey extends i{constructor(e={},t={}){super(e,{endpoint:"/api/group/apikey",...t})}}class ApiKeyList extends t{constructor(e={}){super({ModelClass:ApiKey,endpoint:"/api/group/apikey",size:25,...e})}}const v={create:{title:"Create API Key",fields:[{name:"name",type:"text",label:"Name",placeholder:"Mobile App v2",required:!0,columns:12,help:"A descriptive name to identify this key."},{name:"group",type:"number",label:"Group ID",required:!0,columns:12,help:"The group this key is scoped to."},{name:"permissions",type:"textarea",label:"Permissions (JSON)",placeholder:'{"view_orders": true, "create_orders": true}',columns:12,help:"JSON dict of permissions to grant. Leave empty for no permissions."}]},edit:{title:"Edit API Key",fields:[{name:"name",type:"text",label:"Name",required:!0,columns:12},{name:"is_active",type:"switch",label:"Active",columns:12,help:"Deactivate to revoke access without deleting the key."},{name:"permissions",type:"textarea",label:"Permissions (JSON)",columns:12,help:"JSON dict of granted permissions."}]}};class GroupView extends e{constructor(e={}){super({className:"group-view",...e}),this.model=e.model||new l(e.data||{}),this.template='\n <div class="group-view-container">\n \x3c!-- Header --\x3e\n <div data-container="group-header"></div>\n \x3c!-- Side Nav --\x3e\n <div data-container="group-sidenav" style="min-height: 400px;"></div>\n </div>\n '}async onInit(){this.header=new e({containerId:"group-header",template:'\n <div class="d-flex justify-content-between align-items-start mb-4">\n \x3c!-- Left Side: Identity --\x3e\n <div class="d-flex align-items-center gap-3">\n {{#model.avatar}}\n {{{model.avatar|avatar(\'md\',\'rounded\')}}}\n {{/model.avatar}}\n {{^model.avatar}}\n <div class="d-flex align-items-center justify-content-center rounded bg-light" style="width: 56px; height: 56px;">\n <i class="bi bi-people text-secondary" style="font-size: 1.5rem;"></i>\n </div>\n {{/model.avatar}}\n <div>\n <h3 class="mb-0">{{model.name|default(\'Unnamed Group\')}}</h3>\n <div class="d-flex align-items-center gap-2 mt-1">\n <span class="badge bg-primary bg-opacity-10 text-primary" style="font-size: 0.72rem;">{{model.kind|capitalize}}</span>\n {{#model.parent}}\n <span class="text-muted small">\n <i class="bi bi-diagram-3 me-1"></i>\n <a href="#" data-action="view-parent" data-id="{{model.parent.id}}" class="text-decoration-none">{{model.parent.name}}</a>\n </span>\n {{/model.parent}}\n </div>\n {{#model.metadata.timezone}}\n <div class="text-muted small mt-1"><i class="bi bi-clock me-1"></i>{{model.metadata.timezone}}</div>\n {{/model.metadata.timezone}}\n </div>\n </div>\n\n \x3c!-- Right Side: Status & Actions --\x3e\n <div class="d-flex align-items-start gap-4">\n <div class="text-end">\n <div class="d-flex align-items-center justify-content-end gap-3">\n <span class="d-inline-flex align-items-center gap-1" title="{{model.is_active|boolean(\'Group Active\',\'Group Inactive\')}}">\n <i class="bi {{model.is_active|boolean(\'bi-toggle-on text-success\',\'bi-toggle-off text-secondary\')}}" style="font-size: 1.1rem;"></i>\n <span class="small">{{model.is_active|boolean(\'Active\',\'Inactive\')}}</span>\n </span>\n </div>\n {{#model.last_activity}}\n <div class="text-muted small mt-1">Last active {{model.last_activity|relative}}</div>\n {{/model.last_activity}}\n {{#model.created}}\n <div class="text-muted small mt-1">Created {{model.created|date}}</div>\n {{/model.created}}\n </div>\n <div data-container="group-context-menu"></div>\n </div>\n </div>'}),this.header.setModel(this.model),this.addChild(this.header);const t=new e({model:this.model,template:'\n <style>\n .gv-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.5rem; }\n .gv-section-label:first-child { margin-top: 0; }\n .gv-field-row { display: flex; align-items: baseline; padding: 0.5rem 0; border-bottom: 1px solid #f0f0f0; }\n .gv-field-row:last-child { border-bottom: none; }\n .gv-field-label { width: 140px; font-size: 0.78rem; color: #6c757d; flex-shrink: 0; }\n .gv-field-value { flex: 1; font-size: 0.88rem; color: #212529; }\n .gv-field-action { color: #6c757d; cursor: pointer; font-size: 0.8rem; margin-left: auto; padding: 0.15rem 0.4rem; border-radius: 4px; background: none; border: none; }\n .gv-field-action:hover { background: #f0f0f0; color: #0d6efd; }\n </style>\n\n <div class="gv-section-label">Group</div>\n <div class="gv-field-row">\n <div class="gv-field-label">Name</div>\n <div class="gv-field-value">{{model.name}}</div>\n <button type="button" class="gv-field-action" data-action="edit-group" title="Edit"><i class="bi bi-pencil"></i></button>\n </div>\n <div class="gv-field-row">\n <div class="gv-field-label">Kind</div>\n <div class="gv-field-value"><span class="badge bg-primary bg-opacity-10 text-primary">{{model.kind|capitalize}}</span></div>\n </div>\n <div class="gv-field-row">\n <div class="gv-field-label">Status</div>\n <div class="gv-field-value">\n {{#model.is_active|bool}}<span style="font-size:0.65rem; padding:0.15em 0.45em; background:#d1e7dd; color:#0f5132; border-radius:3px;">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span style="font-size:0.65rem; padding:0.15em 0.45em; background:#fff3cd; color:#856404; border-radius:3px;">Inactive</span>{{/model.is_active|bool}}\n </div>\n </div>\n <div class="gv-field-row">\n <div class="gv-field-label">ID</div>\n <div class="gv-field-value" style="font-family: ui-monospace, monospace; font-size: 0.82rem;">{{model.id}}</div>\n </div>\n\n <div class="gv-section-label">Hierarchy</div>\n <div class="gv-field-row">\n <div class="gv-field-label">Parent</div>\n <div class="gv-field-value">\n {{#model.parent}}\n <a href="#" data-action="view-parent" data-id="{{model.parent.id}}" class="text-decoration-none">{{model.parent.name}}</a>\n <span class="text-muted small ms-1">({{model.parent.kind|capitalize}})</span>\n {{/model.parent}}\n {{^model.parent}}<span style="color:#adb5bd; font-style:italic; font-size:0.85rem;">None — top-level group</span>{{/model.parent}}\n </div>\n </div>\n\n <div class="gv-section-label">Settings</div>\n {{#model.metadata.timezone}}\n <div class="gv-field-row">\n <div class="gv-field-label">Timezone</div>\n <div class="gv-field-value">{{model.metadata.timezone}}</div>\n </div>\n {{/model.metadata.timezone}}\n {{#model.metadata.domain}}\n <div class="gv-field-row">\n <div class="gv-field-label">Domain</div>\n <div class="gv-field-value">{{model.metadata.domain}}</div>\n </div>\n {{/model.metadata.domain}}\n {{#model.metadata.portal}}\n <div class="gv-field-row">\n <div class="gv-field-label">Portal URL</div>\n <div class="gv-field-value"><a href="{{model.metadata.portal}}" target="_blank" class="text-decoration-none">{{model.metadata.portal}}</a></div>\n </div>\n {{/model.metadata.portal}}\n {{#model.metadata.eod_hour}}\n <div class="gv-field-row">\n <div class="gv-field-label">End of Day</div>\n <div class="gv-field-value">{{model.metadata.eod_hour}}:00</div>\n </div>\n {{/model.metadata.eod_hour}}\n\n <div class="gv-section-label">Dates</div>\n <div class="gv-field-row">\n <div class="gv-field-label">Created</div>\n <div class="gv-field-value">{{model.created|datetime|default(\'—\')}}</div>\n </div>\n <div class="gv-field-row">\n <div class="gv-field-label">Modified</div>\n <div class="gv-field-value">{{model.modified|datetime|default(\'—\')}}</div>\n </div>\n '}),i=new n({collection:new a({params:{group:this.model.get("id"),size:10}}),hideActivePillNames:["group"],clickAction:"view",showAdd:!0,addButtonLabel:"Invite",onAdd:e=>this.onInviteClick(e),columns:[{key:"user.display_name",label:"User",sortable:!0},{key:"user.email",label:"Email",sortable:!0},{key:"permissions|keys|badge",label:"Permissions"},{key:"created",label:"Joined",formatter:"date",sortable:!0}]}),d=new n({collection:new r({params:{parent:this.model.get("id"),size:10}}),hideActivePillNames:["parent"],clickAction:"view",showAdd:!0,addButtonLabel:"Add Group",onAdd:()=>this.onActionAddChildGroup(),columns:[{key:"name",label:"Name",sortable:!0},{key:"kind",label:"Kind",formatter:"badge"},{key:"is_active",label:"Status",width:"80px",template:'\n {{#model.is_active|bool}}<span class="badge bg-success bg-opacity-10 text-success">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span class="badge bg-secondary bg-opacity-10 text-secondary">Inactive</span>{{/model.is_active|bool}}'},{key:"created",label:"Created",formatter:"date",sortable:!0}]}),l=new n({collection:new m({params:{size:10,model_name:"account.Group",model_id:this.model.get("id")}}),hideActivePillNames:["model_name","model_id"],columns:[{key:"created",label:"Date",formatter:"datetime",sortable:!0,width:"150px"},{key:"category|badge",label:"Category"},{key:"title",label:"Title"}]}),c=new n({collection:new ApiKeyList({params:{group:this.model.get("id"),size:10}}),hideActivePillNames:["group"],clickAction:"view",showAdd:!0,addButtonLabel:"Create Key",addFormConfig:{...v.create,defaults:{group:this.model.get("id")}},columns:[{key:"name",label:"Name",sortable:!0},{key:"is_active",label:"Status",width:"80px",template:'\n {{#model.is_active|bool}}<span class="badge bg-success bg-opacity-10 text-success">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span class="badge bg-secondary bg-opacity-10 text-secondary">Inactive</span>{{/model.is_active|bool}}'},{key:"permissions|keys|badge",label:"Permissions"},{key:"created",label:"Created",formatter:"datetime",sortable:!0}]}),p=new AdminMetadataSection({model:this.model}),u=new n({collection:new s({params:{size:10,model_name:"account.Group",model_id:this.model.get("id")}}),permissions:"view_logs",hideActivePillNames:["model_name","model_id"],columns:[{key:"created",label:"Timestamp",sortable:!0,formatter:"epoch|datetime",filter:{name:"created",type:"daterange",startName:"dr_start",endName:"dr_end",fieldName:"dr_field",label:"Date Range",format:"YYYY-MM-DD",displayFormat:"MMM DD, YYYY",separator:" to "}},{key:"level",label:"Level",sortable:!0,filter:{type:"select",options:[{value:"info",label:"Info"},{value:"warning",label:"Warning"},{value:"error",label:"Error"}]}},{key:"kind",label:"Kind",filter:{type:"text"}},{name:"log",label:"Log"}]});this.sideNavView=new SideNavView({containerId:"group-sidenav",activeSection:"details",navWidth:180,contentPadding:"1.25rem 2rem",enableResponsive:!0,minWidth:500,sections:[{key:"details",label:"Details",icon:"bi-info-circle",view:t},{key:"members",label:"Members",icon:"bi-people",view:i},{key:"children",label:"Sub-Groups",icon:"bi-diagram-3",view:d},{key:"api_keys",label:"API Keys",icon:"bi-key",view:c},{type:"divider",label:"Activity"},{key:"events",label:"Events",icon:"bi-calendar-event",view:l},{key:"logs",label:"Logs",icon:"bi-journal-text",view:u,permissions:"view_logs"},{type:"divider",label:"Settings"},{key:"metadata",label:"Metadata",icon:"bi-braces",view:p}]}),this.addChild(this.sideNavView);const h=new o({containerId:"group-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit Group",action:"edit-group",icon:"bi-pencil"},{type:"divider"},{label:"Invite Member",action:"invite-member",icon:"bi-person-plus"},{label:"Add Sub-Group",action:"add-child-group",icon:"bi-diagram-3"},{type:"divider"},this.model.get("is_active")?{label:"Deactivate Group",action:"deactivate-group",icon:"bi-toggle-off"}:{label:"Activate Group",action:"activate-group",icon:"bi-toggle-on"}]}});this.addChild(h)}async onActionEditGroup(){await d.showModelForm({title:`Edit Group — ${this.model.get("name")}`,model:this.model,size:"lg",formConfig:c.detailed})&&await this.render()}async onActionInviteMember(){return this.onInviteClick(new Event("click"))}async onInviteClick(e){e?.preventDefault&&(e.preventDefault(),e.stopPropagation());const t=await d.showForm({title:`Invite User to ${this.model.get("name")}`,size:"sm",fields:[{type:"email",name:"email",label:"Email",required:!0,cols:12}]});if(!t?.email)return!0;const i=this.getApp(),n=await i.rest.POST("/api/group/member/invite",{group:this.model.id,email:t.email});return n.success?(i.toast.success("User invited successfully"),"members"===this.sideNavView?.getActiveSection()&&await this.sideNavView.showSection("members")):i.toast.error(n.message||"Failed to invite user"),!0}async onActionAddChildGroup(){const e=await d.showForm({title:`Add Sub-Group to ${this.model.get("name")}`,size:"sm",fields:c.create.fields.filter(e=>"parent"!==e.name)});if(!e)return!0;e.parent=this.model.id;const t=new l(e),i=await t.save();return 200===i.status||201===i.status?(this.getApp()?.toast?.success("Sub-group created"),"children"===this.sideNavView?.getActiveSection()&&await this.sideNavView.showSection("children")):this.getApp()?.toast?.error(i.message||"Failed to create sub-group"),!0}async onActionDeactivateGroup(){return!(await d.confirm(`Are you sure you want to deactivate <strong>${this.model.get("name")}</strong>?`,"Deactivate Group"))||(200===(await this.model.save({is_active:!1})).status?(this.getApp()?.toast?.success("Group deactivated"),await this.render()):this.getApp()?.toast?.error("Failed to deactivate group"),!0)}async onActionActivateGroup(){return!(await d.confirm(`Are you sure you want to activate <strong>${this.model.get("name")}</strong>?`,"Activate Group"))||(200===(await this.model.save({is_active:!0})).status?(this.getApp()?.toast?.success("Group activated"),await this.render()):this.getApp()?.toast?.error("Failed to activate group"),!0)}async onActionViewParent(e,t){const i=t?.dataset?.id;if(!i)return!0;const n=new l({id:i});return await n.fetch(),n.id&&d.showDialog({title:!1,size:"lg",body:new GroupView({model:n}),buttons:[{text:"Close",class:"btn-secondary",dismiss:!0}]}),!0}async showSection(e){this.sideNavView&&await this.sideNavView.showSection(e)}getActiveSection(){return this.sideNavView?this.sideNavView.getActiveSection():null}_onModelChange(){}static create(e={}){return new GroupView(e)}}l.VIEW_CLASS=GroupView;const p=/* @__PURE__ */Object.freeze(/* @__PURE__ */Object.defineProperty({__proto__:null,default:GroupView},Symbol.toStringTag,{value:"Module"}));export{AdminMetadataSection as A,GroupView as G,SideNavView as S,ApiKey as a,v as b,ApiKeyList as c,p as d};
|
|
2
|
+
//# sourceMappingURL=GroupView-FfwdNW9H.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GroupView-FfwdNW9H.js","sources":["../../src/core/views/navigation/SideNavView.js","../../src/extensions/admin/shared/AdminMetadataSection.js","../../src/core/models/ApiKey.js","../../src/extensions/admin/account/groups/GroupView.js"],"sourcesContent":["/**\n * SideNavView - Left sidebar navigation with content panel\n *\n * A reusable navigation component that displays a vertical sidebar with\n * nav links, optional group labels, and icons. The content panel mounts\n * one child view at a time, switching on nav click.\n *\n * Features:\n * - Left sidebar with nav links, icons, and group dividers\n * - Active state with accent border\n * - Mount/unmount child views on section switch\n * - Responsive: collapses to dropdown on narrow containers\n * - Permission-aware: skips sections the user lacks permission for\n * - Configurable nav width and content padding\n * - Smooth fade transitions between sections\n *\n * Example Usage:\n * ```javascript\n * const sideNav = new SideNavView({\n * sections: [\n * { key: 'profile', label: 'Profile', icon: 'bi-person', view: profileView },\n * { key: 'security', label: 'Security', icon: 'bi-shield-lock', view: securityView },\n * { type: 'divider', label: 'Activity' },\n * { key: 'sessions', label: 'Sessions', icon: 'bi-clock-history', view: sessionsView },\n * ],\n * activeSection: 'profile',\n * navWidth: 200,\n * contentPadding: '1.5rem 2.5rem',\n * enableResponsive: true\n * });\n * ```\n */\n\nimport View from '@core/View.js';\n\nclass SideNavView extends View {\n constructor(options = {}) {\n const {\n sections = [],\n activeSection,\n navWidth,\n contentPadding,\n enableResponsive,\n minWidth,\n ...viewOptions\n } = options;\n\n super({\n tagName: 'div',\n className: 'side-nav-view',\n ...viewOptions\n });\n\n // Configuration\n this.navWidth = navWidth || 200;\n this.contentPadding = contentPadding || '1.5rem 2.5rem';\n this.enableResponsive = enableResponsive !== false;\n this.minWidth = minWidth || 500;\n\n // State\n this.sectionConfigs = []; // Full config array (including dividers)\n this.sectionViews = {}; // key → view instance\n this.sectionKeys = []; // Ordered navigable section keys\n this.activeSection = null;\n this.currentMode = 'sidebar'; // 'sidebar' or 'dropdown'\n this.resizeObserver = null;\n this.lastContainerWidth = 0;\n\n // Process sections config\n for (const config of sections) {\n this._addSectionConfig(config);\n }\n\n // Set initial active section\n this.activeSection = activeSection || this.sectionKeys[0] || null;\n\n // Bind resize handler\n this.handleResize = this.handleResize.bind(this);\n }\n\n /**\n * Process and store a section config entry\n * @param {object} config - Section config (navigable or divider)\n * @private\n */\n _addSectionConfig(config) {\n if (config.type === 'divider') {\n this.sectionConfigs.push({ type: 'divider', label: config.label });\n return;\n }\n\n // Skip if user lacks required permission\n if (config.permissions && !this._hasPermission(config.permissions)) {\n return;\n }\n\n this.sectionConfigs.push(config);\n this.sectionKeys.push(config.key);\n\n if (config.view) {\n this.sectionViews[config.key] = config.view;\n config.view.parent = this;\n }\n }\n\n /**\n * Check if the current user has a permission\n * @param {string} perm - Permission string\n * @returns {boolean}\n * @private\n */\n _hasPermission(perm) {\n try {\n return this.getApp().activeUser.hasPerm(perm);\n } catch {\n return true; // If app isn't available yet, allow — will be checked at render\n }\n }\n\n // ───────────────────────────────────────────────\n // Template\n // ───────────────────────────────────────────────\n\n async renderTemplate() {\n const nav = this.currentMode === 'dropdown'\n ? this._buildDropdownNav()\n : this._buildSidebarNav();\n\n return `\n <style>\n .snv-layout { display: flex; height: 100%; min-height: 0; }\n .snv-nav {\n width: ${this.navWidth}px;\n background: #f8f9fc;\n border-right: 1px solid #e9ecef;\n padding: 0.75rem 0;\n flex-shrink: 0;\n overflow-y: auto;\n }\n .snv-nav a {\n color: #495057;\n padding: 0.45rem 1.25rem;\n font-size: 0.85rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n text-decoration: none;\n cursor: pointer;\n }\n .snv-nav a:hover { background: #e9ecef; }\n .snv-nav a.active {\n background: #e7f1ff;\n color: #0d6efd;\n font-weight: 600;\n border-right: 2px solid #0d6efd;\n }\n .snv-nav a i { width: 18px; text-align: center; font-size: 0.9rem; }\n .snv-nav-label {\n font-size: 0.65rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: #adb5bd;\n padding: 0.75rem 1.25rem 0.25rem;\n }\n .snv-content {\n flex: 1;\n overflow-y: auto;\n padding: ${this.contentPadding};\n min-width: 0;\n }\n .snv-content > .snv-section { display: none; }\n .snv-content > .snv-section.snv-active { display: block; }\n .snv-dropdown { margin-bottom: 0.75rem; }\n .snv-select-btn {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n width: 100%;\n padding: 0.5rem 1rem;\n background: #f8f9fc;\n border: 1px solid #dee2e6;\n border-radius: 0.375rem;\n font-size: 0.85rem;\n color: #495057;\n cursor: pointer;\n }\n .snv-select-btn:hover { background: #e9ecef; }\n .snv-select-btn::after {\n content: '';\n display: inline-block;\n margin-left: auto;\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-left: 0.3em solid transparent;\n }\n @media (max-width: 576px) {\n .snv-nav { display: none; }\n .snv-content { padding: 1.25rem; }\n }\n </style>\n ${this.currentMode === 'dropdown' ? `\n <div class=\"snv-dropdown\">${nav}</div>\n <div class=\"snv-content\" data-container=\"snv-content\"></div>\n ` : `\n <div class=\"snv-layout\">\n <nav class=\"snv-nav\">${nav}</nav>\n <div class=\"snv-content\" data-container=\"snv-content\"></div>\n </div>\n `}\n `;\n }\n\n /**\n * Build sidebar navigation HTML\n * @returns {string}\n * @private\n */\n _buildSidebarNav() {\n return this.sectionConfigs.map(config => {\n if (config.type === 'divider') {\n return `<div class=\"snv-nav-label\">${this.escapeHtml(config.label)}</div>`;\n }\n const isActive = config.key === this.activeSection;\n const icon = config.icon ? `<i class=\"bi ${config.icon}\"></i>` : '';\n return `<a role=\"button\" class=\"${isActive ? 'active' : ''}\" data-action=\"navigate\" data-section=\"${config.key}\">${icon} ${this.escapeHtml(config.label)}</a>`;\n }).join('');\n }\n\n /**\n * Build dropdown navigation HTML (responsive mode)\n * @returns {string}\n * @private\n */\n _buildDropdownNav() {\n const activeConfig = this.sectionConfigs.find(c => c.key === this.activeSection);\n const activeLabel = activeConfig ? activeConfig.label : this.sectionKeys[0];\n\n const items = this.sectionConfigs\n .filter(c => c.type !== 'divider')\n .map(config => {\n const isActive = config.key === this.activeSection;\n return `\n <li>\n <button class=\"dropdown-item ${isActive ? 'active' : ''}\"\n data-action=\"navigate\"\n data-section=\"${config.key}\"\n type=\"button\">\n ${config.icon ? `<i class=\"bi ${config.icon} me-2\"></i>` : ''}\n ${this.escapeHtml(config.label)}\n ${isActive ? '<i class=\"bi bi-check-lg ms-2\"></i>' : ''}\n </button>\n </li>\n `;\n }).join('');\n\n return `\n <div class=\"dropdown\">\n <button class=\"snv-select-btn\" type=\"button\"\n data-bs-toggle=\"dropdown\" aria-expanded=\"false\">\n ${activeConfig?.icon ? `<i class=\"bi ${activeConfig.icon}\"></i>` : ''}\n <span>${this.escapeHtml(activeLabel)}</span>\n </button>\n <ul class=\"dropdown-menu w-100\">${items}</ul>\n </div>\n `;\n }\n\n // ───────────────────────────────────────────────\n // Lifecycle\n // ───────────────────────────────────────────────\n\n async onAfterRender() {\n await super.onAfterRender();\n\n // Mount the active section\n if (this.activeSection) {\n await this._mountSection(this.activeSection);\n }\n\n // Set up responsive behavior\n if (this.enableResponsive) {\n this._setupResponsive();\n }\n }\n\n async onBeforeDestroy() {\n await super.onBeforeDestroy();\n\n // Clean up resize observer\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = null;\n }\n\n if (typeof window !== 'undefined') {\n window.removeEventListener('resize', this.handleResize);\n }\n\n // Destroy all section views\n for (const view of Object.values(this.sectionViews)) {\n if (view && typeof view.destroy === 'function') {\n await view.destroy();\n }\n }\n }\n\n // ───────────────────────────────────────────────\n // Section switching\n // ───────────────────────────────────────────────\n\n /**\n * Navigate to a section\n * @param {string} key - Section key\n * @returns {Promise<boolean>}\n */\n async showSection(key) {\n if (!this.sectionViews[key]) {\n console.warn(`SideNavView: Section \"${key}\" does not exist`);\n return false;\n }\n\n if (key === this.activeSection) {\n // Already active — but ensure it's mounted\n const view = this.sectionViews[key];\n if (view && view.isMounted() && this.element?.contains(view.element)) {\n return true;\n }\n }\n\n const previousSection = this.activeSection;\n this.activeSection = key;\n\n // Unmount previous section\n if (previousSection && previousSection !== key) {\n await this._unmountSection(previousSection);\n }\n\n // Mount new section\n await this._mountSection(key);\n\n // Call onSectionActivated hook after the view is mounted and visible\n const activeView = this.sectionViews[key];\n if (activeView?.onSectionActivated) {\n await activeView.onSectionActivated();\n }\n\n // Update nav visual state\n this._updateNavState(key);\n\n this.emit('section:changed', {\n activeSection: key,\n previousSection\n });\n\n return true;\n }\n\n /**\n * Mount a section view into the content area\n * @param {string} key - Section key\n * @private\n */\n async _mountSection(key) {\n const view = this.sectionViews[key];\n if (!view) return;\n\n const container = this.element?.querySelector('[data-container=\"snv-content\"]');\n if (!container) return;\n\n if (!view.isMounted()) {\n this._showContentLoading(container);\n try {\n await view.render(true, container);\n } finally {\n this._hideContentLoading(container);\n }\n }\n }\n\n /**\n * Show a lightweight spinner in the content panel\n * @param {HTMLElement} container\n * @private\n */\n _showContentLoading(container) {\n if (!container) return;\n let spinner = container.querySelector('.snv-loading');\n if (!spinner) {\n spinner = document.createElement('div');\n spinner.className = 'snv-loading';\n spinner.innerHTML = '<div class=\"spinner-border spinner-border-sm text-secondary\" role=\"status\"><span class=\"visually-hidden\">Loading...</span></div>';\n spinner.style.cssText = 'display:flex;align-items:center;justify-content:center;padding:3rem;';\n container.prepend(spinner);\n }\n }\n\n /**\n * Remove the content panel spinner\n * @param {HTMLElement} container\n * @private\n */\n _hideContentLoading(container) {\n if (!container) return;\n const spinner = container.querySelector('.snv-loading');\n if (spinner) spinner.remove();\n }\n\n /**\n * Unmount a section view\n * @param {string} key - Section key\n * @private\n */\n async _unmountSection(key) {\n const view = this.sectionViews[key];\n if (!view || !view.isMounted()) return;\n\n await view.unmount();\n }\n\n /**\n * Update nav link active state\n * @param {string} activeKey - Active section key\n * @private\n */\n _updateNavState(activeKey) {\n if (!this.element) return;\n\n // Update sidebar links\n this.element.querySelectorAll('.snv-nav a, .dropdown-item').forEach(link => {\n const section = link.dataset.section;\n if (section) {\n link.classList.toggle('active', section === activeKey);\n }\n });\n\n // Update dropdown button label\n const selectBtn = this.element.querySelector('.snv-select-btn span');\n if (selectBtn) {\n const config = this.sectionConfigs.find(c => c.key === activeKey);\n if (config) {\n selectBtn.textContent = config.label;\n }\n }\n }\n\n // ───────────────────────────────────────────────\n // Action handlers\n // ───────────────────────────────────────────────\n\n async onActionNavigate(event, el) {\n event.preventDefault();\n const section = el.dataset.section;\n if (section) {\n await this.showSection(section);\n }\n return true;\n }\n\n // ───────────────────────────────────────────────\n // Responsive\n // ───────────────────────────────────────────────\n\n /**\n * Set up responsive width detection\n * @private\n */\n _setupResponsive() {\n if (!this.element || !this.enableResponsive) return;\n\n this._updateMode();\n\n if (typeof ResizeObserver !== 'undefined') {\n this.resizeObserver = new ResizeObserver(() => {\n this.handleResize();\n });\n const container = this.element.parentElement || this.element;\n this.resizeObserver.observe(container);\n } else {\n window.addEventListener('resize', this.handleResize);\n }\n }\n\n /**\n * Handle resize events\n */\n async handleResize() {\n const containerWidth = this._getContainerWidth();\n if (Math.abs(containerWidth - this.lastContainerWidth) > 50) {\n this.lastContainerWidth = containerWidth;\n await this._updateMode();\n }\n }\n\n /**\n * Get the container width\n * @returns {number}\n * @private\n */\n _getContainerWidth() {\n if (!this.element) return this.minWidth;\n const container = this.element.parentElement || this.element;\n return container.offsetWidth || this.minWidth;\n }\n\n /**\n * Check and switch between sidebar and dropdown modes\n * @private\n */\n async _updateMode() {\n const containerWidth = this._getContainerWidth();\n const newMode = containerWidth < this.minWidth ? 'dropdown' : 'sidebar';\n\n if (newMode !== this.currentMode) {\n this.currentMode = newMode;\n if (this.isMounted()) {\n await this.render();\n }\n this.emit('navigation:modeChanged', {\n mode: this.currentMode,\n containerWidth\n });\n }\n }\n\n // ───────────────────────────────────────────────\n // Public API\n // ───────────────────────────────────────────────\n\n /**\n * Get the active section key\n * @returns {string|null}\n */\n getActiveSection() {\n return this.activeSection;\n }\n\n /**\n * Get all navigable section keys\n * @returns {string[]}\n */\n getSectionKeys() {\n return [...this.sectionKeys];\n }\n\n /**\n * Get a section's view by key\n * @param {string} key - Section key\n * @returns {View|null}\n */\n getSection(key) {\n return this.sectionViews[key] || null;\n }\n\n /**\n * Add a section dynamically\n * @param {object} config - Section config\n * @param {boolean} makeActive - Whether to activate the section\n * @returns {Promise<boolean>}\n */\n async addSection(config, makeActive = false) {\n if (config.key && this.sectionViews[config.key]) {\n console.warn(`SideNavView: Section \"${config.key}\" already exists`);\n return false;\n }\n\n this._addSectionConfig(config);\n\n if (this.isMounted()) {\n await this.render();\n if (makeActive && config.key) {\n await this.showSection(config.key);\n }\n }\n\n this.emit('section:added', { config });\n return true;\n }\n\n /**\n * Remove a section dynamically\n * @param {string} key - Section key to remove\n * @returns {Promise<boolean>}\n */\n async removeSection(key) {\n const view = this.sectionViews[key];\n if (!view) {\n console.warn(`SideNavView: Section \"${key}\" does not exist`);\n return false;\n }\n\n // Destroy the view\n if (typeof view.destroy === 'function') {\n await view.destroy();\n }\n\n // Remove from data structures\n delete this.sectionViews[key];\n this.sectionKeys = this.sectionKeys.filter(k => k !== key);\n this.sectionConfigs = this.sectionConfigs.filter(c => c.key !== key);\n\n // Handle active section removal\n if (this.activeSection === key) {\n this.activeSection = this.sectionKeys[0] || null;\n }\n\n if (this.isMounted()) {\n await this.render();\n }\n\n this.emit('section:removed', { key });\n return true;\n }\n\n /**\n * Prevent model changes from triggering a full re-render.\n * Section views manage their own model reactivity.\n */\n _onModelChange() {\n // no-op — same pattern as UserView\n }\n\n static create(options = {}) {\n return new SideNavView(options);\n }\n}\n\nexport default SideNavView;\n","/**\n * AdminMetadataSection - View/edit metadata on any model with a metadata field\n *\n * Displays metadata as a key-value list with add/edit/remove capability.\n * Saves changes via standard model CRUD (model.save()).\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\n\nexport default class AdminMetadataSection extends View {\n constructor(options = {}) {\n super({\n className: 'admin-metadata-section',\n template: `\n <style>\n .amd-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }\n .amd-header h6 { margin: 0; font-weight: 600; }\n .amd-list { border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; }\n .amd-item { display: flex; align-items: center; padding: 0.6rem 1rem; border-bottom: 1px solid #f0f0f0; gap: 0.75rem; }\n .amd-item:last-child { border-bottom: none; }\n .amd-key { font-family: ui-monospace, monospace; font-size: 0.82rem; font-weight: 600; color: #495057; min-width: 120px; flex-shrink: 0; }\n .amd-value { flex: 1; font-size: 0.85rem; color: #212529; word-break: break-word; min-width: 0; }\n .amd-actions { flex-shrink: 0; display: flex; gap: 0.25rem; }\n .amd-actions .btn { font-size: 0.7rem; padding: 0.15rem 0.35rem; }\n .amd-empty { padding: 2rem; text-align: center; color: #6c757d; font-size: 0.85rem; }\n .amd-empty i { font-size: 1.5rem; display: block; margin-bottom: 0.5rem; }\n </style>\n\n <div class=\"amd-header\">\n <h6>Metadata</h6>\n <button type=\"button\" class=\"btn btn-primary btn-sm\" data-action=\"add-entry\">\n <i class=\"bi bi-plus-lg me-1\"></i>Add\n </button>\n </div>\n\n <div id=\"amd-entries\"></div>\n `,\n ...options\n });\n }\n\n onAfterRender() {\n this._renderEntries();\n }\n\n _getMetadata() {\n return this.model?.get('metadata') || {};\n }\n\n _renderEntries() {\n const container = this.element?.querySelector('#amd-entries');\n if (!container) return;\n\n const metadata = this._getMetadata();\n const keys = Object.keys(metadata).sort();\n\n if (!keys.length) {\n container.innerHTML = `\n <div class=\"amd-list\">\n <div class=\"amd-empty\">\n <i class=\"bi bi-braces\"></i>\n No metadata entries\n </div>\n </div>`;\n return;\n }\n\n const rows = keys.map(key => {\n const val = metadata[key];\n const display = typeof val === 'object' ? JSON.stringify(val) : String(val);\n return `\n <div class=\"amd-item\">\n <div class=\"amd-key\">${this._escapeHtml(key)}</div>\n <div class=\"amd-value\">${this._escapeHtml(display)}</div>\n <div class=\"amd-actions\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"edit-entry\" data-key=\"${this._escapeHtml(key)}\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n <button type=\"button\" class=\"btn btn-outline-danger\" data-action=\"remove-entry\" data-key=\"${this._escapeHtml(key)}\" title=\"Remove\"><i class=\"bi bi-trash\"></i></button>\n </div>\n </div>`;\n }).join('');\n\n container.innerHTML = `<div class=\"amd-list\">${rows}</div>`;\n }\n\n _escapeHtml(str) {\n const div = document.createElement('div');\n div.textContent = str;\n return div.innerHTML;\n }\n\n async onActionAddEntry() {\n const data = await Dialog.showForm({\n title: 'Add Metadata Entry',\n icon: 'bi-braces',\n size: 'sm',\n fields: [\n { name: 'key', type: 'text', label: 'Key', required: true, placeholder: 'e.g., timezone' },\n { name: 'value', type: 'text', label: 'Value', required: true, placeholder: 'e.g., America/New_York' }\n ]\n });\n if (!data) return true;\n\n const metadata = { ...this._getMetadata() };\n // Try to parse JSON values\n try {\n metadata[data.key] = JSON.parse(data.value);\n } catch {\n metadata[data.key] = data.value;\n }\n\n const resp = await this.model.save({ metadata });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Metadata entry added');\n this._renderEntries();\n } else {\n this.getApp()?.toast?.error('Failed to save metadata');\n }\n return true;\n }\n\n async onActionEditEntry(event, el) {\n const key = el.dataset.key;\n if (!key) return true;\n\n const metadata = this._getMetadata();\n const currentValue = metadata[key];\n const displayValue = typeof currentValue === 'object' ? JSON.stringify(currentValue) : String(currentValue);\n\n const data = await Dialog.showForm({\n title: `Edit \"${key}\"`,\n icon: 'bi-braces',\n size: 'sm',\n fields: [\n { name: 'value', type: 'text', label: 'Value', required: true, value: displayValue }\n ]\n });\n if (!data) return true;\n\n const updated = { ...metadata };\n try {\n updated[key] = JSON.parse(data.value);\n } catch {\n updated[key] = data.value;\n }\n\n const resp = await this.model.save({ metadata: updated });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Metadata updated');\n this._renderEntries();\n } else {\n this.getApp()?.toast?.error('Failed to save metadata');\n }\n return true;\n }\n\n async onActionRemoveEntry(event, el) {\n const key = el.dataset.key;\n if (!key) return true;\n\n const confirmed = await Dialog.confirm(\n `Remove metadata key \"<strong>${this._escapeHtml(key)}</strong>\"?`,\n 'Remove Entry'\n );\n if (!confirmed) return true;\n\n const updated = { ...this._getMetadata() };\n delete updated[key];\n\n const resp = await this.model.save({ metadata: updated });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Metadata entry removed');\n this._renderEntries();\n } else {\n this.getApp()?.toast?.error('Failed to remove metadata entry');\n }\n return true;\n }\n}\n","import Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\n\n/**\n * ApiKey - Group-scoped API key for external integrations and services.\n * Maps to REST endpoints under /api/group/apikey\n *\n * Key properties:\n * - Scoped to a single group\n * - Carries only explicitly granted permissions (least-privilege)\n * - sys.* permissions always denied\n * - No IP restriction (unlike User Auth Tokens)\n * - Header format: Authorization: apikey <token>\n *\n * The raw token is only returned at creation time — it is never shown again.\n *\n * Endpoints:\n * GET /api/group/apikey - List keys (filter by ?group=<id>)\n * POST /api/group/apikey - Create a key\n * GET /api/group/apikey/<id> - Get key details\n * POST /api/group/apikey/<id> - Update name, permissions, limits, is_active\n * DELETE /api/group/apikey/<id> - Delete key\n */\nclass ApiKey extends Model {\n constructor(data = {}, options = {}) {\n super(data, {\n endpoint: '/api/group/apikey',\n ...options\n });\n }\n}\n\n/**\n * ApiKeyList - Collection of ApiKey records.\n * Filter by group: new ApiKeyList({ params: { group: groupId } })\n */\nclass ApiKeyList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: ApiKey,\n endpoint: '/api/group/apikey',\n size: 25,\n ...options\n });\n }\n}\n\n/**\n * Forms configuration for ApiKey\n */\nconst ApiKeyForms = {\n create: {\n title: 'Create API Key',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Name',\n placeholder: 'Mobile App v2',\n required: true,\n columns: 12,\n help: 'A descriptive name to identify this key.'\n },\n {\n name: 'group',\n type: 'number',\n label: 'Group ID',\n required: true,\n columns: 12,\n help: 'The group this key is scoped to.'\n },\n {\n name: 'permissions',\n type: 'textarea',\n label: 'Permissions (JSON)',\n placeholder: '{\"view_orders\": true, \"create_orders\": true}',\n columns: 12,\n help: 'JSON dict of permissions to grant. Leave empty for no permissions.'\n }\n ]\n },\n\n edit: {\n title: 'Edit API Key',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Name',\n required: true,\n columns: 12\n },\n {\n name: 'is_active',\n type: 'switch',\n label: 'Active',\n columns: 12,\n help: 'Deactivate to revoke access without deleting the key.'\n },\n {\n name: 'permissions',\n type: 'textarea',\n label: 'Permissions (JSON)',\n columns: 12,\n help: 'JSON dict of granted permissions.'\n }\n ]\n }\n};\n\nexport { ApiKey, ApiKeyList, ApiKeyForms };\n","/**\n * GroupView - Modern group management interface\n *\n * Features:\n * - Clean header with avatar, name, kind badge, parent link, active/online status\n * - SideNavView with: Details, Members, Children, Events, Logs\n * - Expanded context menu with quick actions\n * - Clean Bootstrap 5 styling matching UserView patterns\n */\n\nimport View from '@core/View.js';\nimport SideNavView from '@core/views/navigation/SideNavView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { Group, GroupList, GroupForms } from '@core/models/Group.js';\nimport { MemberList } from '@core/models/Member.js';\nimport { IncidentEventList } from '@core/models/Incident.js';\nimport { LogList } from '@core/models/Log.js';\nimport { ApiKeyList, ApiKeyForms } from '@core/models/ApiKey.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport AdminMetadataSection from '../../shared/AdminMetadataSection.js';\n\nclass GroupView extends View {\n constructor(options = {}) {\n super({\n className: 'group-view',\n ...options\n });\n\n this.model = options.model || new Group(options.data || {});\n\n this.template = `\n <div class=\"group-view-container\">\n <!-- Header -->\n <div data-container=\"group-header\"></div>\n <!-- Side Nav -->\n <div data-container=\"group-sidenav\" style=\"min-height: 400px;\"></div>\n </div>\n `;\n }\n\n async onInit() {\n // ── Header ──────────────────────────────────\n this.header = new View({\n containerId: 'group-header',\n template: `\n <div class=\"d-flex justify-content-between align-items-start mb-4\">\n <!-- Left Side: Identity -->\n <div class=\"d-flex align-items-center gap-3\">\n {{#model.avatar}}\n {{{model.avatar|avatar('md','rounded')}}}\n {{/model.avatar}}\n {{^model.avatar}}\n <div class=\"d-flex align-items-center justify-content-center rounded bg-light\" style=\"width: 56px; height: 56px;\">\n <i class=\"bi bi-people text-secondary\" style=\"font-size: 1.5rem;\"></i>\n </div>\n {{/model.avatar}}\n <div>\n <h3 class=\"mb-0\">{{model.name|default('Unnamed Group')}}</h3>\n <div class=\"d-flex align-items-center gap-2 mt-1\">\n <span class=\"badge bg-primary bg-opacity-10 text-primary\" style=\"font-size: 0.72rem;\">{{model.kind|capitalize}}</span>\n {{#model.parent}}\n <span class=\"text-muted small\">\n <i class=\"bi bi-diagram-3 me-1\"></i>\n <a href=\"#\" data-action=\"view-parent\" data-id=\"{{model.parent.id}}\" class=\"text-decoration-none\">{{model.parent.name}}</a>\n </span>\n {{/model.parent}}\n </div>\n {{#model.metadata.timezone}}\n <div class=\"text-muted small mt-1\"><i class=\"bi bi-clock me-1\"></i>{{model.metadata.timezone}}</div>\n {{/model.metadata.timezone}}\n </div>\n </div>\n\n <!-- Right Side: Status & Actions -->\n <div class=\"d-flex align-items-start gap-4\">\n <div class=\"text-end\">\n <div class=\"d-flex align-items-center justify-content-end gap-3\">\n <span class=\"d-inline-flex align-items-center gap-1\" title=\"{{model.is_active|boolean('Group Active','Group Inactive')}}\">\n <i class=\"bi {{model.is_active|boolean('bi-toggle-on text-success','bi-toggle-off text-secondary')}}\" style=\"font-size: 1.1rem;\"></i>\n <span class=\"small\">{{model.is_active|boolean('Active','Inactive')}}</span>\n </span>\n </div>\n {{#model.last_activity}}\n <div class=\"text-muted small mt-1\">Last active {{model.last_activity|relative}}</div>\n {{/model.last_activity}}\n {{#model.created}}\n <div class=\"text-muted small mt-1\">Created {{model.created|date}}</div>\n {{/model.created}}\n </div>\n <div data-container=\"group-context-menu\"></div>\n </div>\n </div>`\n });\n this.header.setModel(this.model);\n this.addChild(this.header);\n\n // ── Details section ─────────────────────────\n const detailsView = new View({\n model: this.model,\n template: `\n <style>\n .gv-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.5rem; }\n .gv-section-label:first-child { margin-top: 0; }\n .gv-field-row { display: flex; align-items: baseline; padding: 0.5rem 0; border-bottom: 1px solid #f0f0f0; }\n .gv-field-row:last-child { border-bottom: none; }\n .gv-field-label { width: 140px; font-size: 0.78rem; color: #6c757d; flex-shrink: 0; }\n .gv-field-value { flex: 1; font-size: 0.88rem; color: #212529; }\n .gv-field-action { color: #6c757d; cursor: pointer; font-size: 0.8rem; margin-left: auto; padding: 0.15rem 0.4rem; border-radius: 4px; background: none; border: none; }\n .gv-field-action:hover { background: #f0f0f0; color: #0d6efd; }\n </style>\n\n <div class=\"gv-section-label\">Group</div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Name</div>\n <div class=\"gv-field-value\">{{model.name}}</div>\n <button type=\"button\" class=\"gv-field-action\" data-action=\"edit-group\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Kind</div>\n <div class=\"gv-field-value\"><span class=\"badge bg-primary bg-opacity-10 text-primary\">{{model.kind|capitalize}}</span></div>\n </div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Status</div>\n <div class=\"gv-field-value\">\n {{#model.is_active|bool}}<span style=\"font-size:0.65rem; padding:0.15em 0.45em; background:#d1e7dd; color:#0f5132; border-radius:3px;\">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span style=\"font-size:0.65rem; padding:0.15em 0.45em; background:#fff3cd; color:#856404; border-radius:3px;\">Inactive</span>{{/model.is_active|bool}}\n </div>\n </div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">ID</div>\n <div class=\"gv-field-value\" style=\"font-family: ui-monospace, monospace; font-size: 0.82rem;\">{{model.id}}</div>\n </div>\n\n <div class=\"gv-section-label\">Hierarchy</div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Parent</div>\n <div class=\"gv-field-value\">\n {{#model.parent}}\n <a href=\"#\" data-action=\"view-parent\" data-id=\"{{model.parent.id}}\" class=\"text-decoration-none\">{{model.parent.name}}</a>\n <span class=\"text-muted small ms-1\">({{model.parent.kind|capitalize}})</span>\n {{/model.parent}}\n {{^model.parent}}<span style=\"color:#adb5bd; font-style:italic; font-size:0.85rem;\">None — top-level group</span>{{/model.parent}}\n </div>\n </div>\n\n <div class=\"gv-section-label\">Settings</div>\n {{#model.metadata.timezone}}\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Timezone</div>\n <div class=\"gv-field-value\">{{model.metadata.timezone}}</div>\n </div>\n {{/model.metadata.timezone}}\n {{#model.metadata.domain}}\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Domain</div>\n <div class=\"gv-field-value\">{{model.metadata.domain}}</div>\n </div>\n {{/model.metadata.domain}}\n {{#model.metadata.portal}}\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Portal URL</div>\n <div class=\"gv-field-value\"><a href=\"{{model.metadata.portal}}\" target=\"_blank\" class=\"text-decoration-none\">{{model.metadata.portal}}</a></div>\n </div>\n {{/model.metadata.portal}}\n {{#model.metadata.eod_hour}}\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">End of Day</div>\n <div class=\"gv-field-value\">{{model.metadata.eod_hour}}:00</div>\n </div>\n {{/model.metadata.eod_hour}}\n\n <div class=\"gv-section-label\">Dates</div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Created</div>\n <div class=\"gv-field-value\">{{model.created|datetime|default('—')}}</div>\n </div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Modified</div>\n <div class=\"gv-field-value\">{{model.modified|datetime|default('—')}}</div>\n </div>\n `\n });\n\n // ── Members ─────────────────────────────────\n const membersView = new TableView({\n collection: new MemberList({ params: { group: this.model.get('id'), size: 10 } }),\n hideActivePillNames: ['group'],\n clickAction: 'view',\n showAdd: true,\n addButtonLabel: 'Invite',\n onAdd: (event) => this.onInviteClick(event),\n columns: [\n { key: 'user.display_name', label: 'User', sortable: true },\n { key: 'user.email', label: 'Email', sortable: true },\n { key: 'permissions|keys|badge', label: 'Permissions' },\n { key: 'created', label: 'Joined', formatter: 'date', sortable: true }\n ]\n });\n\n // ── Children (sub-groups) ───────────────────\n const childrenView = new TableView({\n collection: new GroupList({ params: { parent: this.model.get('id'), size: 10 } }),\n hideActivePillNames: ['parent'],\n clickAction: 'view',\n showAdd: true,\n addButtonLabel: 'Add Group',\n onAdd: () => this.onActionAddChildGroup(),\n columns: [\n { key: 'name', label: 'Name', sortable: true },\n { key: 'kind', label: 'Kind', formatter: 'badge' },\n {\n key: 'is_active', label: 'Status', width: '80px',\n template: `\n {{#model.is_active|bool}}<span class=\"badge bg-success bg-opacity-10 text-success\">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span class=\"badge bg-secondary bg-opacity-10 text-secondary\">Inactive</span>{{/model.is_active|bool}}`\n },\n { key: 'created', label: 'Created', formatter: 'date', sortable: true }\n ]\n });\n\n // ── Events ──────────────────────────────────\n const eventsView = new TableView({\n collection: new IncidentEventList({\n params: { size: 10, model_name: 'account.Group', model_id: this.model.get('id') }\n }),\n hideActivePillNames: ['model_name', 'model_id'],\n columns: [\n { key: 'created', label: 'Date', formatter: 'datetime', sortable: true, width: '150px' },\n { key: 'category|badge', label: 'Category' },\n { key: 'title', label: 'Title' }\n ]\n });\n\n // ── API Keys ──────────────────────────────────\n const apiKeysView = new TableView({\n collection: new ApiKeyList({ params: { group: this.model.get('id'), size: 10 } }),\n hideActivePillNames: ['group'],\n clickAction: 'view',\n showAdd: true,\n addButtonLabel: 'Create Key',\n addFormConfig: {\n ...ApiKeyForms.create,\n defaults: { group: this.model.get('id') }\n },\n columns: [\n { key: 'name', label: 'Name', sortable: true },\n {\n key: 'is_active', label: 'Status', width: '80px',\n template: `\n {{#model.is_active|bool}}<span class=\"badge bg-success bg-opacity-10 text-success\">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span class=\"badge bg-secondary bg-opacity-10 text-secondary\">Inactive</span>{{/model.is_active|bool}}`\n },\n { key: 'permissions|keys|badge', label: 'Permissions' },\n { key: 'created', label: 'Created', formatter: 'datetime', sortable: true }\n ]\n });\n\n // ── Metadata ─────────────────────────────────\n const metadataView = new AdminMetadataSection({ model: this.model });\n\n // ── Logs ────────────────────────────────────\n const logsView = new TableView({\n collection: new LogList({\n params: { size: 10, model_name: 'account.Group', model_id: this.model.get('id') }\n }),\n permissions: 'view_logs',\n hideActivePillNames: ['model_name', 'model_id'],\n columns: [\n {\n key: 'created', label: 'Timestamp', sortable: true, formatter: 'epoch|datetime',\n filter: { name: 'created', type: 'daterange', startName: 'dr_start', endName: 'dr_end', fieldName: 'dr_field', label: 'Date Range', format: 'YYYY-MM-DD', displayFormat: 'MMM DD, YYYY', separator: ' to ' }\n },\n {\n key: 'level', label: 'Level', sortable: true,\n filter: { type: 'select', options: [{ value: 'info', label: 'Info' }, { value: 'warning', label: 'Warning' }, { value: 'error', label: 'Error' }] }\n },\n { key: 'kind', label: 'Kind', filter: { type: 'text' } },\n { name: 'log', label: 'Log' }\n ]\n });\n\n // ── SideNavView ─────────────────────────────\n this.sideNavView = new SideNavView({\n containerId: 'group-sidenav',\n activeSection: 'details',\n navWidth: 180,\n contentPadding: '1.25rem 2rem',\n enableResponsive: true,\n minWidth: 500,\n sections: [\n { key: 'details', label: 'Details', icon: 'bi-info-circle', view: detailsView },\n { key: 'members', label: 'Members', icon: 'bi-people', view: membersView },\n { key: 'children', label: 'Sub-Groups', icon: 'bi-diagram-3', view: childrenView },\n { key: 'api_keys', label: 'API Keys', icon: 'bi-key', view: apiKeysView },\n { type: 'divider', label: 'Activity' },\n { key: 'events', label: 'Events', icon: 'bi-calendar-event', view: eventsView },\n { key: 'logs', label: 'Logs', icon: 'bi-journal-text', view: logsView, permissions: 'view_logs' },\n { type: 'divider', label: 'Settings' },\n { key: 'metadata', label: 'Metadata', icon: 'bi-braces', view: metadataView }\n ]\n });\n this.addChild(this.sideNavView);\n\n // ── Context Menu ────────────────────────────\n const groupMenu = new ContextMenu({\n containerId: 'group-context-menu',\n className: 'context-menu-view header-menu-absolute',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'Edit Group', action: 'edit-group', icon: 'bi-pencil' },\n { type: 'divider' },\n { label: 'Invite Member', action: 'invite-member', icon: 'bi-person-plus' },\n { label: 'Add Sub-Group', action: 'add-child-group', icon: 'bi-diagram-3' },\n { type: 'divider' },\n this.model.get('is_active')\n ? { label: 'Deactivate Group', action: 'deactivate-group', icon: 'bi-toggle-off' }\n : { label: 'Activate Group', action: 'activate-group', icon: 'bi-toggle-on' },\n ]\n }\n });\n this.addChild(groupMenu);\n }\n\n // ── Actions ─────────────────────────────────\n\n async onActionEditGroup() {\n const resp = await Dialog.showModelForm({\n title: `Edit Group — ${this.model.get('name')}`,\n model: this.model,\n size: 'lg',\n formConfig: GroupForms.detailed,\n });\n if (resp) {\n await this.render();\n }\n }\n\n async onActionInviteMember() {\n return this.onInviteClick(new Event('click'));\n }\n\n async onInviteClick(event) {\n if (event?.preventDefault) {\n event.preventDefault();\n event.stopPropagation();\n }\n const data = await Dialog.showForm({\n title: `Invite User to ${this.model.get('name')}`,\n size: 'sm',\n fields: [\n { type: 'email', name: 'email', label: 'Email', required: true, cols: 12 }\n ]\n });\n if (!data?.email) return true;\n\n const app = this.getApp();\n const resp = await app.rest.POST('/api/group/member/invite', {\n group: this.model.id,\n email: data.email\n });\n if (resp.success) {\n app.toast.success('User invited successfully');\n // Refresh members if on that section\n if (this.sideNavView?.getActiveSection() === 'members') {\n await this.sideNavView.showSection('members');\n }\n } else {\n app.toast.error(resp.message || 'Failed to invite user');\n }\n return true;\n }\n\n async onActionAddChildGroup() {\n const data = await Dialog.showForm({\n title: `Add Sub-Group to ${this.model.get('name')}`,\n size: 'sm',\n fields: GroupForms.create.fields.filter(f => f.name !== 'parent')\n });\n if (!data) return true;\n\n data.parent = this.model.id;\n const newGroup = new Group(data);\n const resp = await newGroup.save();\n if (resp.status === 200 || resp.status === 201) {\n this.getApp()?.toast?.success('Sub-group created');\n if (this.sideNavView?.getActiveSection() === 'children') {\n await this.sideNavView.showSection('children');\n }\n } else {\n this.getApp()?.toast?.error(resp.message || 'Failed to create sub-group');\n }\n return true;\n }\n\n async onActionDeactivateGroup() {\n const confirmed = await Dialog.confirm(\n `Are you sure you want to deactivate <strong>${this.model.get('name')}</strong>?`,\n 'Deactivate Group'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.save({ is_active: false });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Group deactivated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to deactivate group');\n }\n return true;\n }\n\n async onActionActivateGroup() {\n const confirmed = await Dialog.confirm(\n `Are you sure you want to activate <strong>${this.model.get('name')}</strong>?`,\n 'Activate Group'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.save({ is_active: true });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Group activated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to activate group');\n }\n return true;\n }\n\n async onActionViewParent(event, element) {\n const parentId = element?.dataset?.id;\n if (!parentId) return true;\n\n const parent = new Group({ id: parentId });\n await parent.fetch();\n if (parent.id) {\n Dialog.showDialog({\n title: false,\n size: 'lg',\n body: new GroupView({ model: parent }),\n buttons: [{ text: 'Close', class: 'btn-secondary', dismiss: true }]\n });\n }\n return true;\n }\n\n // ── Navigation helpers ──────────────────────\n\n async showSection(sectionName) {\n if (this.sideNavView) {\n await this.sideNavView.showSection(sectionName);\n }\n }\n\n getActiveSection() {\n return this.sideNavView ? this.sideNavView.getActiveSection() : null;\n }\n\n _onModelChange() {\n // Prevent full re-render on model changes\n }\n\n static create(options = {}) {\n return new GroupView(options);\n }\n}\n\nGroup.VIEW_CLASS = GroupView;\n\nexport default GroupView;\n"],"names":["SideNavView","View","constructor","options","sections","activeSection","navWidth","contentPadding","enableResponsive","minWidth","viewOptions","super","tagName","className","this","sectionConfigs","sectionViews","sectionKeys","currentMode","resizeObserver","lastContainerWidth","config","_addSectionConfig","handleResize","bind","type","permissions","_hasPermission","push","key","view","parent","label","perm","getApp","activeUser","hasPerm","renderTemplate","nav","_buildDropdownNav","_buildSidebarNav","map","escapeHtml","isActive","icon","join","activeConfig","find","c","activeLabel","items","filter","onAfterRender","_mountSection","_setupResponsive","onBeforeDestroy","disconnect","window","removeEventListener","Object","values","destroy","showSection","console","warn","isMounted","element","contains","previousSection","_unmountSection","activeView","onSectionActivated","_updateNavState","emit","container","querySelector","_showContentLoading","render","_hideContentLoading","spinner","document","createElement","innerHTML","style","cssText","prepend","remove","unmount","activeKey","querySelectorAll","forEach","link","section","dataset","classList","toggle","selectBtn","textContent","onActionNavigate","event","el","preventDefault","_updateMode","ResizeObserver","parentElement","observe","addEventListener","containerWidth","_getContainerWidth","Math","abs","offsetWidth","newMode","mode","getActiveSection","getSectionKeys","getSection","addSection","makeActive","removeSection","k","_onModelChange","create","AdminMetadataSection","template","_renderEntries","_getMetadata","model","get","metadata","keys","sort","length","rows","val","display","JSON","stringify","String","_escapeHtml","str","div","onActionAddEntry","data","Dialog","showForm","title","size","fields","name","required","placeholder","parse","value","save","status","toast","success","error","onActionEditEntry","currentValue","displayValue","updated","onActionRemoveEntry","confirm","ApiKey","Model","endpoint","ApiKeyList","Collection","ModelClass","ApiKeyForms","columns","help","edit","GroupView","Group","onInit","header","containerId","setModel","addChild","detailsView","membersView","TableView","collection","MemberList","params","group","hideActivePillNames","clickAction","showAdd","addButtonLabel","onAdd","onInviteClick","sortable","formatter","childrenView","GroupList","onActionAddChildGroup","width","eventsView","IncidentEventList","model_name","model_id","apiKeysView","addFormConfig","defaults","metadataView","logsView","LogList","startName","endName","fieldName","format","displayFormat","separator","sideNavView","groupMenu","ContextMenu","context","action","onActionEditGroup","showModelForm","formConfig","GroupForms","detailed","onActionInviteMember","Event","stopPropagation","cols","email","app","resp","rest","POST","id","message","f","newGroup","onActionDeactivateGroup","is_active","onActionActivateGroup","onActionViewParent","parentId","fetch","showDialog","body","buttons","text","class","dismiss","sectionName","VIEW_CLASS"],"mappings":"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"}
|