@memberjunction/ng-explorer-core 6.2.0-edge.3 → 6.2.0-edge.4
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/README.md +10 -0
- package/dist/app-routing.module.d.ts.map +1 -1
- package/dist/app-routing.module.js +5 -0
- package/dist/app-routing.module.js.map +1 -1
- package/dist/generated/lazy-feature-config.d.ts +1 -1
- package/dist/generated/lazy-feature-config.d.ts.map +1 -1
- package/dist/generated/lazy-feature-config.js +3 -2
- package/dist/generated/lazy-feature-config.js.map +1 -1
- package/dist/lib/guards/moved-nav-item-redirect.d.ts +15 -0
- package/dist/lib/guards/moved-nav-item-redirect.d.ts.map +1 -0
- package/dist/lib/guards/moved-nav-item-redirect.js +37 -0
- package/dist/lib/guards/moved-nav-item-redirect.js.map +1 -0
- package/dist/lib/omnibar/omnibar-dashboard-matches.d.ts +34 -0
- package/dist/lib/omnibar/omnibar-dashboard-matches.d.ts.map +1 -0
- package/dist/lib/omnibar/omnibar-dashboard-matches.js +70 -0
- package/dist/lib/omnibar/omnibar-dashboard-matches.js.map +1 -0
- package/dist/lib/omnibar/omnibar-palette.component.d.ts +27 -2
- package/dist/lib/omnibar/omnibar-palette.component.d.ts.map +1 -1
- package/dist/lib/omnibar/omnibar-palette.component.js +126 -65
- package/dist/lib/omnibar/omnibar-palette.component.js.map +1 -1
- package/dist/lib/omnibar/omnibar-provider.d.ts +4 -0
- package/dist/lib/omnibar/omnibar-provider.d.ts.map +1 -1
- package/dist/lib/omnibar/omnibar-provider.js.map +1 -1
- package/dist/lib/omnibar/providers/omnibar-record.provider.d.ts +2 -2
- package/dist/lib/omnibar/providers/omnibar-record.provider.d.ts.map +1 -1
- package/dist/lib/omnibar/providers/omnibar-record.provider.js +3 -7
- package/dist/lib/omnibar/providers/omnibar-record.provider.js.map +1 -1
- package/dist/lib/omnibar/providers/omnibar-search.provider.d.ts +15 -2
- package/dist/lib/omnibar/providers/omnibar-search.provider.d.ts.map +1 -1
- package/dist/lib/omnibar/providers/omnibar-search.provider.js +59 -7
- package/dist/lib/omnibar/providers/omnibar-search.provider.js.map +1 -1
- package/dist/lib/profile/forking-setting.d.ts +8 -0
- package/dist/lib/profile/forking-setting.d.ts.map +1 -0
- package/dist/lib/profile/forking-setting.js +18 -0
- package/dist/lib/profile/forking-setting.js.map +1 -0
- package/dist/lib/profile/profile-dialog.component.d.ts +8 -0
- package/dist/lib/profile/profile-dialog.component.d.ts.map +1 -1
- package/dist/lib/profile/profile-dialog.component.js +182 -92
- package/dist/lib/profile/profile-dialog.component.js.map +1 -1
- package/dist/lib/resource-wrappers/artifact-resource.component.d.ts +16 -1
- package/dist/lib/resource-wrappers/artifact-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/artifact-resource.component.js +27 -4
- package/dist/lib/resource-wrappers/artifact-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts +7 -18
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.js +20 -41
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/chat-search-routing.d.ts +10 -3
- package/dist/lib/resource-wrappers/chat-search-routing.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/chat-search-routing.js +11 -4
- package/dist/lib/resource-wrappers/chat-search-routing.js.map +1 -1
- package/dist/lib/resource-wrappers/dashboard-add-to-menu.component.d.ts +29 -0
- package/dist/lib/resource-wrappers/dashboard-add-to-menu.component.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-add-to-menu.component.js +141 -0
- package/dist/lib/resource-wrappers/dashboard-add-to-menu.component.js.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-part-config.d.ts +28 -0
- package/dist/lib/resource-wrappers/dashboard-part-config.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-part-config.js +150 -0
- package/dist/lib/resource-wrappers/dashboard-part-config.js.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-resource.component.d.ts +361 -56
- package/dist/lib/resource-wrappers/dashboard-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/dashboard-resource.component.js +1312 -361
- package/dist/lib/resource-wrappers/dashboard-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/dashboard-source-search.d.ts +86 -0
- package/dist/lib/resource-wrappers/dashboard-source-search.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-source-search.js +136 -0
- package/dist/lib/resource-wrappers/dashboard-source-search.js.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-studio-pane.d.ts +30 -0
- package/dist/lib/resource-wrappers/dashboard-studio-pane.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-studio-pane.js +55 -0
- package/dist/lib/resource-wrappers/dashboard-studio-pane.js.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-studio-tools.d.ts +135 -0
- package/dist/lib/resource-wrappers/dashboard-studio-tools.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-studio-tools.js +856 -0
- package/dist/lib/resource-wrappers/dashboard-studio-tools.js.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-tab-agent.d.ts +87 -0
- package/dist/lib/resource-wrappers/dashboard-tab-agent.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-tab-agent.js +125 -0
- package/dist/lib/resource-wrappers/dashboard-tab-agent.js.map +1 -0
- package/dist/lib/resource-wrappers/livekit-room-resource.component.d.ts +28 -7
- package/dist/lib/resource-wrappers/livekit-room-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/livekit-room-resource.component.js +174 -112
- package/dist/lib/resource-wrappers/livekit-room-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/search-results-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/search-results-resource.component.js +5 -4
- package/dist/lib/resource-wrappers/search-results-resource.component.js.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts +7 -0
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.js +20 -2
- package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
- package/dist/lib/shell/dashboard-tab-for-url.d.ts +35 -0
- package/dist/lib/shell/dashboard-tab-for-url.d.ts.map +1 -0
- package/dist/lib/shell/dashboard-tab-for-url.js +39 -0
- package/dist/lib/shell/dashboard-tab-for-url.js.map +1 -0
- package/dist/lib/shell/shell.component.d.ts +31 -2
- package/dist/lib/shell/shell.component.d.ts.map +1 -1
- package/dist/lib/shell/shell.component.js +109 -35
- package/dist/lib/shell/shell.component.js.map +1 -1
- package/dist/lib/shell/shell.module.d.ts +2 -1
- package/dist/lib/shell/shell.module.d.ts.map +1 -1
- package/dist/lib/shell/shell.module.js +6 -2
- package/dist/lib/shell/shell.module.js.map +1 -1
- package/dist/lib/single-dashboard/single-dashboard.component.js +1 -1
- package/dist/lib/single-dashboard/single-dashboard.component.js.map +1 -1
- package/dist/lib/template-user/template-user-banner.component.d.ts +20 -0
- package/dist/lib/template-user/template-user-banner.component.d.ts.map +1 -0
- package/dist/lib/template-user/template-user-banner.component.js +67 -0
- package/dist/lib/template-user/template-user-banner.component.js.map +1 -0
- package/dist/module.d.ts +61 -59
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +7 -0
- package/dist/module.js.map +1 -1
- package/dist/public-api.d.ts +1 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +1 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +58 -53
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Component, EventEmitter, HostListener, Input, Output } from '@angular/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
function DashboardAddToMenuComponent_Conditional_5_Conditional_10_Template(rf, ctx) { if (rf & 1) {
|
|
4
|
+
i0.ɵɵelementStart(0, "span", 10);
|
|
5
|
+
i0.ɵɵelement(1, "i", 12);
|
|
6
|
+
i0.ɵɵtext(2, " Added");
|
|
7
|
+
i0.ɵɵelementEnd();
|
|
8
|
+
} }
|
|
9
|
+
function DashboardAddToMenuComponent_Conditional_5_Conditional_11_Template(rf, ctx) { if (rf & 1) {
|
|
10
|
+
i0.ɵɵelement(0, "i", 11);
|
|
11
|
+
} }
|
|
12
|
+
function DashboardAddToMenuComponent_Conditional_5_Conditional_12_Template(rf, ctx) { if (rf & 1) {
|
|
13
|
+
const _r3 = i0.ɵɵgetCurrentView();
|
|
14
|
+
i0.ɵɵelement(0, "div", 13);
|
|
15
|
+
i0.ɵɵelementStart(1, "button", 14);
|
|
16
|
+
i0.ɵɵlistener("click", function DashboardAddToMenuComponent_Conditional_5_Conditional_12_Template_button_click_1_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.ChooseItem(ctx_r1.Share)); });
|
|
17
|
+
i0.ɵɵelement(2, "i", 15);
|
|
18
|
+
i0.ɵɵelementStart(3, "span", 9)(4, "b");
|
|
19
|
+
i0.ɵɵtext(5, "Share with people or roles\u2026");
|
|
20
|
+
i0.ɵɵelementEnd()()();
|
|
21
|
+
} }
|
|
22
|
+
function DashboardAddToMenuComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) {
|
|
23
|
+
const _r1 = i0.ɵɵgetCurrentView();
|
|
24
|
+
i0.ɵɵelementStart(0, "div", 5);
|
|
25
|
+
i0.ɵɵlistener("click", function DashboardAddToMenuComponent_Conditional_5_Template_div_click_0_listener($event) { return $event.stopPropagation(); });
|
|
26
|
+
i0.ɵɵelementStart(1, "div", 6);
|
|
27
|
+
i0.ɵɵtext(2, "Home");
|
|
28
|
+
i0.ɵɵelementEnd();
|
|
29
|
+
i0.ɵɵelementStart(3, "button", 7);
|
|
30
|
+
i0.ɵɵlistener("click", function DashboardAddToMenuComponent_Conditional_5_Template_button_click_3_listener() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.ChooseItem(ctx_r1.PinToHome)); });
|
|
31
|
+
i0.ɵɵelement(4, "i", 8);
|
|
32
|
+
i0.ɵɵelementStart(5, "span", 9)(6, "b");
|
|
33
|
+
i0.ɵɵtext(7, "Pin card");
|
|
34
|
+
i0.ɵɵelementEnd();
|
|
35
|
+
i0.ɵɵelementStart(8, "small");
|
|
36
|
+
i0.ɵɵtext(9, "A thumbnail card in Pinned");
|
|
37
|
+
i0.ɵɵelementEnd()();
|
|
38
|
+
i0.ɵɵconditionalCreate(10, DashboardAddToMenuComponent_Conditional_5_Conditional_10_Template, 3, 0, "span", 10)(11, DashboardAddToMenuComponent_Conditional_5_Conditional_11_Template, 1, 0, "i", 11);
|
|
39
|
+
i0.ɵɵelementEnd();
|
|
40
|
+
i0.ɵɵconditionalCreate(12, DashboardAddToMenuComponent_Conditional_5_Conditional_12_Template, 6, 0);
|
|
41
|
+
i0.ɵɵelementEnd();
|
|
42
|
+
} if (rf & 2) {
|
|
43
|
+
const ctx_r1 = i0.ɵɵnextContext();
|
|
44
|
+
i0.ɵɵadvance(10);
|
|
45
|
+
i0.ɵɵconditional(ctx_r1.IsPinned ? 10 : 11);
|
|
46
|
+
i0.ɵɵadvance(2);
|
|
47
|
+
i0.ɵɵconditional(ctx_r1.CanShare ? 12 : -1);
|
|
48
|
+
} }
|
|
49
|
+
/**
|
|
50
|
+
* The Add to menu on a dashboard tab: Pin card and Share. Presentational: it shows
|
|
51
|
+
* where the dashboard is already placed and emits the item the user picks.
|
|
52
|
+
*/
|
|
53
|
+
export class DashboardAddToMenuComponent {
|
|
54
|
+
/** True when the dashboard is already pinned to Home. */
|
|
55
|
+
IsPinned = false;
|
|
56
|
+
/** True when the user may share the dashboard. False leaves out the Share item. */
|
|
57
|
+
CanShare = true;
|
|
58
|
+
/** The user picked Pin card. */
|
|
59
|
+
PinToHome = new EventEmitter();
|
|
60
|
+
/** The user picked Share. */
|
|
61
|
+
Share = new EventEmitter();
|
|
62
|
+
/** True while the menu is open. */
|
|
63
|
+
Open = false;
|
|
64
|
+
/** Opens or closes the menu. The click does not reach the document, so it does not close it again. */
|
|
65
|
+
ToggleOpen(event) {
|
|
66
|
+
event.stopPropagation();
|
|
67
|
+
this.Open = !this.Open;
|
|
68
|
+
}
|
|
69
|
+
/** Closes the menu, then emits the picked item. */
|
|
70
|
+
ChooseItem(item) {
|
|
71
|
+
this.Open = false;
|
|
72
|
+
item.emit();
|
|
73
|
+
}
|
|
74
|
+
/** Closes the menu when the user clicks anywhere outside it. */
|
|
75
|
+
OnDocumentClick() {
|
|
76
|
+
this.Open = false;
|
|
77
|
+
}
|
|
78
|
+
/** Closes the menu on Escape. */
|
|
79
|
+
OnEscape() {
|
|
80
|
+
this.Open = false;
|
|
81
|
+
}
|
|
82
|
+
static ɵfac = function DashboardAddToMenuComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || DashboardAddToMenuComponent)(); };
|
|
83
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: DashboardAddToMenuComponent, selectors: [["mj-dashboard-add-to-menu"]], hostBindings: function DashboardAddToMenuComponent_HostBindings(rf, ctx) { if (rf & 1) {
|
|
84
|
+
i0.ɵɵlistener("click", function DashboardAddToMenuComponent_click_HostBindingHandler() { return ctx.OnDocumentClick(); }, i0.ɵɵresolveDocument)("keydown.escape", function DashboardAddToMenuComponent_keydown_escape_HostBindingHandler() { return ctx.OnEscape(); }, i0.ɵɵresolveDocument);
|
|
85
|
+
} }, inputs: { IsPinned: "IsPinned", CanShare: "CanShare" }, outputs: { PinToHome: "PinToHome", Share: "Share" }, standalone: false, decls: 6, vars: 2, consts: [[1, "atm"], ["type", "button", "title", "Add to", "aria-haspopup", "menu", 1, "atm-trigger", 3, "click"], [1, "fa-solid", "fa-plus"], [1, "fa-solid", "fa-chevron-down", "atm-caret"], ["role", "menu", "aria-label", "Add to", 1, "atm-menu"], ["role", "menu", "aria-label", "Add to", 1, "atm-menu", 3, "click"], [1, "atm-label"], ["type", "button", "role", "menuitem", 1, "atm-item", "atm-pin", 3, "click"], [1, "fa-solid", "fa-thumbtack"], [1, "atm-text"], [1, "atm-added"], [1, "fa-solid", "fa-plus", "atm-plus"], [1, "fa-solid", "fa-check"], [1, "atm-sep"], ["type", "button", "role", "menuitem", 1, "atm-item", "atm-share", 3, "click"], [1, "fa-solid", "fa-share-nodes"]], template: function DashboardAddToMenuComponent_Template(rf, ctx) { if (rf & 1) {
|
|
86
|
+
i0.ɵɵelementStart(0, "span", 0)(1, "button", 1);
|
|
87
|
+
i0.ɵɵlistener("click", function DashboardAddToMenuComponent_Template_button_click_1_listener($event) { return ctx.ToggleOpen($event); });
|
|
88
|
+
i0.ɵɵelement(2, "i", 2);
|
|
89
|
+
i0.ɵɵtext(3, " Add to ");
|
|
90
|
+
i0.ɵɵelement(4, "i", 3);
|
|
91
|
+
i0.ɵɵelementEnd();
|
|
92
|
+
i0.ɵɵconditionalCreate(5, DashboardAddToMenuComponent_Conditional_5_Template, 13, 2, "div", 4);
|
|
93
|
+
i0.ɵɵelementEnd();
|
|
94
|
+
} if (rf & 2) {
|
|
95
|
+
i0.ɵɵadvance();
|
|
96
|
+
i0.ɵɵattribute("aria-expanded", ctx.Open ? "true" : "false");
|
|
97
|
+
i0.ɵɵadvance(4);
|
|
98
|
+
i0.ɵɵconditional(ctx.Open ? 5 : -1);
|
|
99
|
+
} }, styles: [".atm[_ngcontent-%COMP%] { position: relative; display: inline-flex; }\n .atm-trigger[_ngcontent-%COMP%] { display: inline-flex; align-items: center; gap: 6px; height: 36px; padding: 0 12px; border: 1px solid var(--mj-border-default); border-radius: 6px; background: var(--mj-bg-surface-card); color: var(--mj-text-secondary); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; transition: background 0.2s, color 0.2s; }\n .atm-trigger[_ngcontent-%COMP%]:hover, .atm-trigger[aria-expanded='true'][_ngcontent-%COMP%] { background: var(--mj-bg-surface-sunken); color: var(--mj-text-primary); }\n .atm-trigger[_ngcontent-%COMP%]:focus-visible, .atm-item[_ngcontent-%COMP%]:focus-visible { outline: 2px solid var(--mj-border-focus); outline-offset: 2px; }\n .atm-caret[_ngcontent-%COMP%] { font-size: 10px; }\n .atm-menu[_ngcontent-%COMP%] { position: absolute; right: 0; top: calc(100% + 6px); width: 320px; max-width: calc(100vw - 32px); background: var(--mj-bg-surface); border: 1px solid var(--mj-border-default); border-radius: var(--mj-radius-lg); box-shadow: var(--mj-shadow-lg); padding: 6px; z-index: var(--mj-z-dropdown); }\n .atm-label[_ngcontent-%COMP%] { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--mj-text-muted); padding: 8px 10px 4px; }\n .atm-item[_ngcontent-%COMP%] { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; padding: 8px 10px; border: none; background: transparent; border-radius: var(--mj-radius-md); color: var(--mj-text-primary); font: inherit; cursor: pointer; }\n .atm-item[_ngcontent-%COMP%]:hover { background: var(--mj-bg-surface-hover); }\n .atm-item[_ngcontent-%COMP%] > i[_ngcontent-%COMP%] { color: var(--mj-text-secondary); }\n .atm-text[_ngcontent-%COMP%] { flex: 1; display: flex; flex-direction: column; }\n .atm-item[_ngcontent-%COMP%] b[_ngcontent-%COMP%] { font-weight: 500; font-size: 13px; }\n .atm-item[_ngcontent-%COMP%] small[_ngcontent-%COMP%] { font-size: 11px; color: var(--mj-text-muted); }\n .atm-added[_ngcontent-%COMP%] { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: var(--mj-radius-full); background: var(--mj-status-success-bg); color: var(--mj-status-success-text); }\n .atm-plus[_ngcontent-%COMP%] { color: var(--mj-text-muted); }\n .atm-sep[_ngcontent-%COMP%] { height: 1px; background: var(--mj-border-subtle); margin: 6px 4px; }"] });
|
|
100
|
+
}
|
|
101
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(DashboardAddToMenuComponent, [{
|
|
102
|
+
type: Component,
|
|
103
|
+
args: [{ standalone: false, selector: 'mj-dashboard-add-to-menu', template: `
|
|
104
|
+
<span class="atm">
|
|
105
|
+
<button type="button" class="atm-trigger" title="Add to" aria-haspopup="menu" [attr.aria-expanded]="Open ? 'true' : 'false'" (click)="ToggleOpen($event)">
|
|
106
|
+
<i class="fa-solid fa-plus"></i> Add to <i class="fa-solid fa-chevron-down atm-caret"></i>
|
|
107
|
+
</button>
|
|
108
|
+
@if (Open) {
|
|
109
|
+
<div class="atm-menu" role="menu" aria-label="Add to" (click)="$event.stopPropagation()">
|
|
110
|
+
<div class="atm-label">Home</div>
|
|
111
|
+
<button type="button" role="menuitem" class="atm-item atm-pin" (click)="ChooseItem(PinToHome)">
|
|
112
|
+
<i class="fa-solid fa-thumbtack"></i><span class="atm-text"><b>Pin card</b><small>A thumbnail card in Pinned</small></span>
|
|
113
|
+
@if (IsPinned) { <span class="atm-added"><i class="fa-solid fa-check"></i> Added</span> } @else { <i class="fa-solid fa-plus atm-plus"></i> }
|
|
114
|
+
</button>
|
|
115
|
+
@if (CanShare) {
|
|
116
|
+
<div class="atm-sep"></div>
|
|
117
|
+
<button type="button" role="menuitem" class="atm-item atm-share" (click)="ChooseItem(Share)">
|
|
118
|
+
<i class="fa-solid fa-share-nodes"></i><span class="atm-text"><b>Share with people or roles…</b></span>
|
|
119
|
+
</button>
|
|
120
|
+
}
|
|
121
|
+
</div>
|
|
122
|
+
}
|
|
123
|
+
</span>
|
|
124
|
+
`, styles: ["\n .atm { position: relative; display: inline-flex; }\n .atm-trigger { display: inline-flex; align-items: center; gap: 6px; height: 36px; padding: 0 12px; border: 1px solid var(--mj-border-default); border-radius: 6px; background: var(--mj-bg-surface-card); color: var(--mj-text-secondary); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; transition: background 0.2s, color 0.2s; }\n .atm-trigger:hover, .atm-trigger[aria-expanded='true'] { background: var(--mj-bg-surface-sunken); color: var(--mj-text-primary); }\n .atm-trigger:focus-visible, .atm-item:focus-visible { outline: 2px solid var(--mj-border-focus); outline-offset: 2px; }\n .atm-caret { font-size: 10px; }\n .atm-menu { position: absolute; right: 0; top: calc(100% + 6px); width: 320px; max-width: calc(100vw - 32px); background: var(--mj-bg-surface); border: 1px solid var(--mj-border-default); border-radius: var(--mj-radius-lg); box-shadow: var(--mj-shadow-lg); padding: 6px; z-index: var(--mj-z-dropdown); }\n .atm-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--mj-text-muted); padding: 8px 10px 4px; }\n .atm-item { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; padding: 8px 10px; border: none; background: transparent; border-radius: var(--mj-radius-md); color: var(--mj-text-primary); font: inherit; cursor: pointer; }\n .atm-item:hover { background: var(--mj-bg-surface-hover); }\n .atm-item > i { color: var(--mj-text-secondary); }\n .atm-text { flex: 1; display: flex; flex-direction: column; }\n .atm-item b { font-weight: 500; font-size: 13px; }\n .atm-item small { font-size: 11px; color: var(--mj-text-muted); }\n .atm-added { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: var(--mj-radius-full); background: var(--mj-status-success-bg); color: var(--mj-status-success-text); }\n .atm-plus { color: var(--mj-text-muted); }\n .atm-sep { height: 1px; background: var(--mj-border-subtle); margin: 6px 4px; }\n "] }]
|
|
125
|
+
}], null, { IsPinned: [{
|
|
126
|
+
type: Input
|
|
127
|
+
}], CanShare: [{
|
|
128
|
+
type: Input
|
|
129
|
+
}], PinToHome: [{
|
|
130
|
+
type: Output
|
|
131
|
+
}], Share: [{
|
|
132
|
+
type: Output
|
|
133
|
+
}], OnDocumentClick: [{
|
|
134
|
+
type: HostListener,
|
|
135
|
+
args: ['document:click']
|
|
136
|
+
}], OnEscape: [{
|
|
137
|
+
type: HostListener,
|
|
138
|
+
args: ['document:keydown.escape']
|
|
139
|
+
}] }); })();
|
|
140
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(DashboardAddToMenuComponent, { className: "DashboardAddToMenuComponent", filePath: "src/lib/resource-wrappers/dashboard-add-to-menu.component.ts", lineNumber: 51 }); })();
|
|
141
|
+
//# sourceMappingURL=dashboard-add-to-menu.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-add-to-menu.component.js","sourceRoot":"","sources":["../../../src/lib/resource-wrappers/dashboard-add-to-menu.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;;;IAmBxD,gCAAwB;IAAA,wBAAiC;IAAC,sBAAK;IAAA,iBAAO;;;IAAW,wBAAyC;;;;IAG3I,0BAA2B;IAC3B,kCAA6F;IAA5B,+MAAS,+BAAiB,KAAC;IAC1F,wBAAuC;IAAuB,AAAvB,+BAAuB,QAAG;IAAA,gDAA2B;IAC9F,AADkG,AAAJ,iBAAI,EAAO,EAChG;;;;IAVb,8BAAyF;IAAnC,yHAAS,wBAAwB,IAAC;IACtF,8BAAuB;IAAA,oBAAI;IAAA,iBAAM;IACjC,iCAA+F;IAAhC,+LAAS,mCAAqB,KAAC;IAC5F,uBAAqC;IAAuB,AAAvB,+BAAuB,QAAG;IAAA,wBAAQ;IAAA,iBAAI;IAAA,6BAAO;IAAA,0CAA0B;IAAQ,AAAR,iBAAQ,EAAO;IACjC,AAA1F,+GAAgB,sFAAiF;IACnG,iBAAS;IACT,mGAAgB;IAMlB,iBAAM;;;IARF,gBAA6I;IAA7I,2CAA6I;IAE/I,eAKC;IALD,2CAKC;;AAxBX;;;GAGG;AA6CH,MAAM,OAAO,2BAA2B;IACtC,yDAAyD;IAChD,QAAQ,GAAG,KAAK,CAAC;IAC1B,mFAAmF;IAC1E,QAAQ,GAAG,IAAI,CAAC;IACzB,gCAAgC;IACtB,SAAS,GAAG,IAAI,YAAY,EAAQ,CAAC;IAC/C,6BAA6B;IACnB,KAAK,GAAG,IAAI,YAAY,EAAQ,CAAC;IAE3C,mCAAmC;IAC5B,IAAI,GAAG,KAAK,CAAC;IAEpB,sGAAsG;IAC/F,UAAU,CAAC,KAAiB;QACjC,KAAK,CAAC,eAAe,EAAE,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;IACzB,CAAC;IAED,mDAAmD;IAC5C,UAAU,CAAC,IAAwB;QACxC,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;QAClB,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAED,gEAAgE;IAEzD,eAAe;QACpB,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;IACpB,CAAC;IAED,iCAAiC;IAE1B,QAAQ;QACb,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;IACpB,CAAC;qHAnCU,2BAA2B;6DAA3B,2BAA2B;YAA3B,gGAAA,qBAAiB,0BAAU,qGAA3B,cAAU,0BAAiB;;YAvClC,AADF,+BAAkB,gBAC0I;YAA7B,8GAAS,sBAAkB,IAAC;YACvJ,uBAAgC;YAAC,wBAAO;YAAA,uBAAkD;YAC5F,iBAAS;YACT,8FAAY;YAed,iBAAO;;YAlByE,cAA8C;;YAG5H,eAcC;YAdD,mCAcC;;;iFAsBM,2BAA2B;cA5CvC,SAAS;6BACI,KAAK,YACP,0BAA0B,YAC1B;;;;;;;;;;;;;;;;;;;;;GAqBT;;kBAsBA,KAAK;;kBAEL,KAAK;;kBAEL,MAAM;;kBAEN,MAAM;;kBAkBN,YAAY;mBAAC,gBAAgB;;kBAM7B,YAAY;mBAAC,yBAAyB;;kFAhC5B,2BAA2B","sourcesContent":["import { Component, EventEmitter, HostListener, Input, Output } from '@angular/core';\n\n/**\n * The Add to menu on a dashboard tab: Pin card and Share. Presentational: it shows\n * where the dashboard is already placed and emits the item the user picks.\n */\n@Component({\n standalone: false,\n selector: 'mj-dashboard-add-to-menu',\n template: `\n <span class=\"atm\">\n <button type=\"button\" class=\"atm-trigger\" title=\"Add to\" aria-haspopup=\"menu\" [attr.aria-expanded]=\"Open ? 'true' : 'false'\" (click)=\"ToggleOpen($event)\">\n <i class=\"fa-solid fa-plus\"></i> Add to <i class=\"fa-solid fa-chevron-down atm-caret\"></i>\n </button>\n @if (Open) {\n <div class=\"atm-menu\" role=\"menu\" aria-label=\"Add to\" (click)=\"$event.stopPropagation()\">\n <div class=\"atm-label\">Home</div>\n <button type=\"button\" role=\"menuitem\" class=\"atm-item atm-pin\" (click)=\"ChooseItem(PinToHome)\">\n <i class=\"fa-solid fa-thumbtack\"></i><span class=\"atm-text\"><b>Pin card</b><small>A thumbnail card in Pinned</small></span>\n @if (IsPinned) { <span class=\"atm-added\"><i class=\"fa-solid fa-check\"></i> Added</span> } @else { <i class=\"fa-solid fa-plus atm-plus\"></i> }\n </button>\n @if (CanShare) {\n <div class=\"atm-sep\"></div>\n <button type=\"button\" role=\"menuitem\" class=\"atm-item atm-share\" (click)=\"ChooseItem(Share)\">\n <i class=\"fa-solid fa-share-nodes\"></i><span class=\"atm-text\"><b>Share with people or roles…</b></span>\n </button>\n }\n </div>\n }\n </span>\n `,\n styles: [`\n .atm { position: relative; display: inline-flex; }\n .atm-trigger { display: inline-flex; align-items: center; gap: 6px; height: 36px; padding: 0 12px; border: 1px solid var(--mj-border-default); border-radius: 6px; background: var(--mj-bg-surface-card); color: var(--mj-text-secondary); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; transition: background 0.2s, color 0.2s; }\n .atm-trigger:hover, .atm-trigger[aria-expanded='true'] { background: var(--mj-bg-surface-sunken); color: var(--mj-text-primary); }\n .atm-trigger:focus-visible, .atm-item:focus-visible { outline: 2px solid var(--mj-border-focus); outline-offset: 2px; }\n .atm-caret { font-size: 10px; }\n .atm-menu { position: absolute; right: 0; top: calc(100% + 6px); width: 320px; max-width: calc(100vw - 32px); background: var(--mj-bg-surface); border: 1px solid var(--mj-border-default); border-radius: var(--mj-radius-lg); box-shadow: var(--mj-shadow-lg); padding: 6px; z-index: var(--mj-z-dropdown); }\n .atm-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--mj-text-muted); padding: 8px 10px 4px; }\n .atm-item { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; padding: 8px 10px; border: none; background: transparent; border-radius: var(--mj-radius-md); color: var(--mj-text-primary); font: inherit; cursor: pointer; }\n .atm-item:hover { background: var(--mj-bg-surface-hover); }\n .atm-item > i { color: var(--mj-text-secondary); }\n .atm-text { flex: 1; display: flex; flex-direction: column; }\n .atm-item b { font-weight: 500; font-size: 13px; }\n .atm-item small { font-size: 11px; color: var(--mj-text-muted); }\n .atm-added { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: var(--mj-radius-full); background: var(--mj-status-success-bg); color: var(--mj-status-success-text); }\n .atm-plus { color: var(--mj-text-muted); }\n .atm-sep { height: 1px; background: var(--mj-border-subtle); margin: 6px 4px; }\n `],\n})\nexport class DashboardAddToMenuComponent {\n /** True when the dashboard is already pinned to Home. */\n @Input() IsPinned = false;\n /** True when the user may share the dashboard. False leaves out the Share item. */\n @Input() CanShare = true;\n /** The user picked Pin card. */\n @Output() PinToHome = new EventEmitter<void>();\n /** The user picked Share. */\n @Output() Share = new EventEmitter<void>();\n\n /** True while the menu is open. */\n public Open = false;\n\n /** Opens or closes the menu. The click does not reach the document, so it does not close it again. */\n public ToggleOpen(event: MouseEvent): void {\n event.stopPropagation();\n this.Open = !this.Open;\n }\n\n /** Closes the menu, then emits the picked item. */\n public ChooseItem(item: EventEmitter<void>): void {\n this.Open = false;\n item.emit();\n }\n\n /** Closes the menu when the user clicks anywhere outside it. */\n @HostListener('document:click')\n public OnDocumentClick(): void {\n this.Open = false;\n }\n\n /** Closes the menu on Escape. */\n @HostListener('document:keydown.escape')\n public OnEscape(): void {\n this.Open = false;\n }\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PanelConfig } from '@memberjunction/ng-dashboard-viewer';
|
|
2
|
+
/** The part types a Config dashboard ships with. */
|
|
3
|
+
export type DashboardPartTypeName = 'View' | 'Query' | 'Artifact' | 'WebURL';
|
|
4
|
+
/** Every {@link DashboardPartTypeName}, in the order tool descriptions list them. */
|
|
5
|
+
export declare const DASHBOARD_PART_TYPE_NAMES: readonly DashboardPartTypeName[];
|
|
6
|
+
/** The outcome of {@link ValidatePartConfig}: the checked config with `type` set, or a message for the agent. */
|
|
7
|
+
export type PartConfigValidation = {
|
|
8
|
+
ok: true;
|
|
9
|
+
config: PanelConfig;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
error: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Checks a config for a part type: an object, at least one of the required keys, no unknown keys, and values
|
|
16
|
+
* of the type and choices the config panel writes. Unset values (undefined or null) are not checked.
|
|
17
|
+
* A `type` key in the input is replaced by `partType`, and ids, names and URLs come back trimmed, as the
|
|
18
|
+
* config panels write them. The input is not changed.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ValidatePartConfig(partType: DashboardPartTypeName, raw: unknown): PartConfigValidation;
|
|
21
|
+
/** The current config with the patch's keys laid over it. `type` stays the current part type. Neither input changes. */
|
|
22
|
+
export declare function MergePartConfig(current: PanelConfig, patch: Record<string, unknown>): PanelConfig;
|
|
23
|
+
/**
|
|
24
|
+
* One sentence per part type naming its required and optional keys, with the accepted values or format
|
|
25
|
+
* where they are fixed, for tool descriptions. The text has no closing period, so a caller can embed it.
|
|
26
|
+
*/
|
|
27
|
+
export declare function DescribePartConfigs(): string;
|
|
28
|
+
//# sourceMappingURL=dashboard-part-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-part-config.d.ts","sourceRoot":"","sources":["../../../src/lib/resource-wrappers/dashboard-part-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAEvE,oDAAoD;AACpD,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE7E,qFAAqF;AACrF,eAAO,MAAM,yBAAyB,EAAE,SAAS,qBAAqB,EAA4C,CAAC;AAkFnH,iHAAiH;AACjH,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GACjC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,qBAAqB,EAAE,GAAG,EAAE,OAAO,GAAG,oBAAoB,CAUtG;AAED,wHAAwH;AACxH,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW,CAEjG;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/** Every {@link DashboardPartTypeName}, in the order tool descriptions list them. */
|
|
2
|
+
export const DASHBOARD_PART_TYPE_NAMES = ['View', 'Query', 'Artifact', 'WebURL'];
|
|
3
|
+
const TEXT = { check: (value) => (typeof value === 'string' ? null : 'must be a string') };
|
|
4
|
+
const ID_OR_NAME = {
|
|
5
|
+
check: (value) => (typeof value === 'string' && value.trim() !== '' ? null : 'must be a non-blank string'),
|
|
6
|
+
trim: true,
|
|
7
|
+
};
|
|
8
|
+
const FLAG = { check: (value) => (typeof value === 'boolean' ? null : 'must be true or false') };
|
|
9
|
+
const WHOLE_NUMBER = {
|
|
10
|
+
check: (value) => (typeof value === 'number' && Number.isInteger(value) && value > 0 ? null : 'must be a whole number above 0'),
|
|
11
|
+
};
|
|
12
|
+
const HTTP_URL = {
|
|
13
|
+
check: (value) => (typeof value === 'string' && isHttpUrl(value) ? null : 'must be a full http:// or https:// URL'),
|
|
14
|
+
hint: 'http:// or https://',
|
|
15
|
+
trim: true,
|
|
16
|
+
};
|
|
17
|
+
/** A value that must be one of `choices`. */
|
|
18
|
+
function oneOf(...choices) {
|
|
19
|
+
return {
|
|
20
|
+
check: (value) => ((typeof value === 'string' || typeof value === 'number') && choices.includes(value) ? null : `must be one of ${choices.join(', ')}`),
|
|
21
|
+
hint: choices.join('|'),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The keys each part type's config panel writes (`buildConfig` in the dashboard-viewer config panels).
|
|
26
|
+
* Fixed choices are the panel's field types, and the auto refresh intervals its select offers.
|
|
27
|
+
*/
|
|
28
|
+
const RULES = {
|
|
29
|
+
View: {
|
|
30
|
+
requireOneOf: ['viewId', 'entityName'],
|
|
31
|
+
keys: {
|
|
32
|
+
viewId: ID_OR_NAME,
|
|
33
|
+
entityName: ID_OR_NAME,
|
|
34
|
+
extraFilter: TEXT,
|
|
35
|
+
displayMode: oneOf('grid', 'cards', 'timeline', 'map'),
|
|
36
|
+
mapRenderMode: oneOf('point', 'choropleth', 'heatmap'),
|
|
37
|
+
allowModeSwitch: FLAG,
|
|
38
|
+
enableSelection: FLAG,
|
|
39
|
+
selectionMode: oneOf('none', 'single', 'multiple'),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
Query: {
|
|
43
|
+
requireOneOf: ['queryId', 'queryName'],
|
|
44
|
+
keys: {
|
|
45
|
+
queryId: ID_OR_NAME,
|
|
46
|
+
queryName: ID_OR_NAME,
|
|
47
|
+
showParameterControls: FLAG,
|
|
48
|
+
parameterLayout: oneOf('header', 'sidebar', 'dialog'),
|
|
49
|
+
autoRefreshSeconds: oneOf(0, 30, 60, 300, 600),
|
|
50
|
+
showExecutionMetadata: FLAG,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
Artifact: {
|
|
54
|
+
requireOneOf: ['artifactId'],
|
|
55
|
+
keys: { artifactId: ID_OR_NAME, versionNumber: WHOLE_NUMBER, showHeader: FLAG, showTabs: FLAG, showVersionSelector: FLAG, showMetadata: FLAG },
|
|
56
|
+
},
|
|
57
|
+
WebURL: {
|
|
58
|
+
requireOneOf: ['url'],
|
|
59
|
+
keys: { url: HTTP_URL, sandboxMode: oneOf('standard', 'strict', 'permissive'), allowFullscreen: FLAG, refreshOnResize: FLAG },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Checks a config for a part type: an object, at least one of the required keys, no unknown keys, and values
|
|
64
|
+
* of the type and choices the config panel writes. Unset values (undefined or null) are not checked.
|
|
65
|
+
* A `type` key in the input is replaced by `partType`, and ids, names and URLs come back trimmed, as the
|
|
66
|
+
* config panels write them. The input is not changed.
|
|
67
|
+
*/
|
|
68
|
+
export function ValidatePartConfig(partType, raw) {
|
|
69
|
+
if (!DASHBOARD_PART_TYPE_NAMES.includes(partType)) {
|
|
70
|
+
return { ok: false, error: `Unknown part type "${partType}". Use one of ${DASHBOARD_PART_TYPE_NAMES.join(', ')}.` };
|
|
71
|
+
}
|
|
72
|
+
if (!isRecord(raw))
|
|
73
|
+
return { ok: false, error: `${partType} config must be an object.` };
|
|
74
|
+
const rule = RULES[partType];
|
|
75
|
+
const config = { ...raw };
|
|
76
|
+
delete config['type'];
|
|
77
|
+
const error = findUnknownKeys(partType, rule, config) ?? findMissingKey(partType, rule, config) ?? findBadValue(partType, rule, config);
|
|
78
|
+
return error ? { ok: false, error } : { ok: true, config: { type: partType, ...trimText(rule, config) } };
|
|
79
|
+
}
|
|
80
|
+
/** The current config with the patch's keys laid over it. `type` stays the current part type. Neither input changes. */
|
|
81
|
+
export function MergePartConfig(current, patch) {
|
|
82
|
+
return { ...current, ...patch, type: current.type };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* One sentence per part type naming its required and optional keys, with the accepted values or format
|
|
86
|
+
* where they are fixed, for tool descriptions. The text has no closing period, so a caller can embed it.
|
|
87
|
+
*/
|
|
88
|
+
export function DescribePartConfigs() {
|
|
89
|
+
return DASHBOARD_PART_TYPE_NAMES.map((name) => describeRule(name, RULES[name])).join('. ');
|
|
90
|
+
}
|
|
91
|
+
/** True for an object that is not null and not an array. */
|
|
92
|
+
function isRecord(value) {
|
|
93
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
94
|
+
}
|
|
95
|
+
/** False for undefined, null and blank text, the values a config panel treats as an empty field. */
|
|
96
|
+
function isSet(value) {
|
|
97
|
+
return value !== undefined && value !== null && !(typeof value === 'string' && value.trim() === '');
|
|
98
|
+
}
|
|
99
|
+
/** True for text that parses as an http or https URL, as the WebURL config panel checks it. */
|
|
100
|
+
function isHttpUrl(text) {
|
|
101
|
+
try {
|
|
102
|
+
const protocol = new URL(text).protocol;
|
|
103
|
+
return protocol === 'http:' || protocol === 'https:';
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** An error that names the keys the rule does not allow and lists the allowed ones, or null. */
|
|
110
|
+
function findUnknownKeys(partType, rule, config) {
|
|
111
|
+
const allowed = Object.keys(rule.keys);
|
|
112
|
+
const unknownKeys = Object.keys(config).filter((key) => !allowed.includes(key));
|
|
113
|
+
return unknownKeys.length ? `${partType} config has unknown keys: ${unknownKeys.join(', ')}. Allowed keys: ${allowed.join(', ')}.` : null;
|
|
114
|
+
}
|
|
115
|
+
/** An error when none of the required keys is set, or null. */
|
|
116
|
+
function findMissingKey(partType, rule, config) {
|
|
117
|
+
return rule.requireOneOf.some((key) => isSet(config[key])) ? null : `${partType} config needs ${rule.requireOneOf.join(' or ')}.`;
|
|
118
|
+
}
|
|
119
|
+
/** An error for the first set value that its key's rule rejects, or null. */
|
|
120
|
+
function findBadValue(partType, rule, config) {
|
|
121
|
+
for (const [key, value] of Object.entries(config)) {
|
|
122
|
+
if (value === undefined || value === null)
|
|
123
|
+
continue;
|
|
124
|
+
const problem = rule.keys[key]?.check(value);
|
|
125
|
+
if (problem)
|
|
126
|
+
return `${partType} config: ${key} ${problem}.`;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
/** A copy of a checked config with the text values of trimmed keys (ids, names and URLs) trimmed. */
|
|
131
|
+
function trimText(rule, config) {
|
|
132
|
+
const trimmed = { ...config };
|
|
133
|
+
for (const [key, value] of Object.entries(config)) {
|
|
134
|
+
if (rule.keys[key]?.trim && typeof value === 'string')
|
|
135
|
+
trimmed[key] = value.trim();
|
|
136
|
+
}
|
|
137
|
+
return trimmed;
|
|
138
|
+
}
|
|
139
|
+
/** "<name>: needs <required keys>; optional <other keys>", with each key's hint in parentheses. */
|
|
140
|
+
function describeRule(name, rule) {
|
|
141
|
+
const required = rule.requireOneOf.map((key) => describeKey(rule, key)).join(' or ');
|
|
142
|
+
const optional = Object.keys(rule.keys).filter((key) => !rule.requireOneOf.includes(key));
|
|
143
|
+
return `${name}: needs ${required}; optional ${optional.map((key) => describeKey(rule, key)).join(', ')}`;
|
|
144
|
+
}
|
|
145
|
+
/** The key name, followed by its hint in parentheses when it has one. */
|
|
146
|
+
function describeKey(rule, key) {
|
|
147
|
+
const hint = rule.keys[key]?.hint;
|
|
148
|
+
return hint ? `${key} (${hint})` : key;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=dashboard-part-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-part-config.js","sourceRoot":"","sources":["../../../src/lib/resource-wrappers/dashboard-part-config.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PanelConfig } from '@memberjunction/ng-dashboard-viewer';\n\n/** The part types a Config dashboard ships with. */\nexport type DashboardPartTypeName = 'View' | 'Query' | 'Artifact' | 'WebURL';\n\n/** Every {@link DashboardPartTypeName}, in the order tool descriptions list them. */\nexport const DASHBOARD_PART_TYPE_NAMES: readonly DashboardPartTypeName[] = ['View', 'Query', 'Artifact', 'WebURL'];\n\n/** The rule for one config key's value. */\ninterface ValueRule {\n /** Checks a value that is set. Returns what is wrong with it, or null. */\n check: (value: unknown) => string | null;\n /** The accepted values or format, for tool descriptions. */\n hint?: string;\n /** True when a text value comes back trimmed, as the config panels trim ids, names and URLs. */\n trim?: boolean;\n}\n\n/** The keys one part type's config may hold. */\ninterface PartConfigRule {\n /** At least one of these keys must be set. */\n requireOneOf: readonly string[];\n /** Every allowed key with the rule for its value, in the order the config panel writes them. */\n keys: Readonly<Record<string, ValueRule>>;\n}\n\nconst TEXT: ValueRule = { check: (value) => (typeof value === 'string' ? null : 'must be a string') };\nconst ID_OR_NAME: ValueRule = {\n check: (value) => (typeof value === 'string' && value.trim() !== '' ? null : 'must be a non-blank string'),\n trim: true,\n};\nconst FLAG: ValueRule = { check: (value) => (typeof value === 'boolean' ? null : 'must be true or false') };\nconst WHOLE_NUMBER: ValueRule = {\n check: (value) => (typeof value === 'number' && Number.isInteger(value) && value > 0 ? null : 'must be a whole number above 0'),\n};\nconst HTTP_URL: ValueRule = {\n check: (value) => (typeof value === 'string' && isHttpUrl(value) ? null : 'must be a full http:// or https:// URL'),\n hint: 'http:// or https://',\n trim: true,\n};\n\n/** A value that must be one of `choices`. */\nfunction oneOf(...choices: readonly (string | number)[]): ValueRule {\n return {\n check: (value) => ((typeof value === 'string' || typeof value === 'number') && choices.includes(value) ? null : `must be one of ${choices.join(', ')}`),\n hint: choices.join('|'),\n };\n}\n\n/**\n * The keys each part type's config panel writes (`buildConfig` in the dashboard-viewer config panels).\n * Fixed choices are the panel's field types, and the auto refresh intervals its select offers.\n */\nconst RULES: Readonly<Record<DashboardPartTypeName, PartConfigRule>> = {\n View: {\n requireOneOf: ['viewId', 'entityName'],\n keys: {\n viewId: ID_OR_NAME,\n entityName: ID_OR_NAME,\n extraFilter: TEXT,\n displayMode: oneOf('grid', 'cards', 'timeline', 'map'),\n mapRenderMode: oneOf('point', 'choropleth', 'heatmap'),\n allowModeSwitch: FLAG,\n enableSelection: FLAG,\n selectionMode: oneOf('none', 'single', 'multiple'),\n },\n },\n Query: {\n requireOneOf: ['queryId', 'queryName'],\n keys: {\n queryId: ID_OR_NAME,\n queryName: ID_OR_NAME,\n showParameterControls: FLAG,\n parameterLayout: oneOf('header', 'sidebar', 'dialog'),\n autoRefreshSeconds: oneOf(0, 30, 60, 300, 600),\n showExecutionMetadata: FLAG,\n },\n },\n Artifact: {\n requireOneOf: ['artifactId'],\n keys: { artifactId: ID_OR_NAME, versionNumber: WHOLE_NUMBER, showHeader: FLAG, showTabs: FLAG, showVersionSelector: FLAG, showMetadata: FLAG },\n },\n WebURL: {\n requireOneOf: ['url'],\n keys: { url: HTTP_URL, sandboxMode: oneOf('standard', 'strict', 'permissive'), allowFullscreen: FLAG, refreshOnResize: FLAG },\n },\n};\n\n/** The outcome of {@link ValidatePartConfig}: the checked config with `type` set, or a message for the agent. */\nexport type PartConfigValidation =\n | { ok: true; config: PanelConfig } // case-violation-ok-legacy-back-compat: matches the { ok, … } discriminated-union shape the spec fixes for validators\n | { ok: false; error: string }; // case-violation-ok-legacy-back-compat: matches the { ok, … } discriminated-union shape the spec fixes for validators\n\n/**\n * Checks a config for a part type: an object, at least one of the required keys, no unknown keys, and values\n * of the type and choices the config panel writes. Unset values (undefined or null) are not checked.\n * A `type` key in the input is replaced by `partType`, and ids, names and URLs come back trimmed, as the\n * config panels write them. The input is not changed.\n */\nexport function ValidatePartConfig(partType: DashboardPartTypeName, raw: unknown): PartConfigValidation {\n if (!DASHBOARD_PART_TYPE_NAMES.includes(partType)) {\n return { ok: false, error: `Unknown part type \"${partType}\". Use one of ${DASHBOARD_PART_TYPE_NAMES.join(', ')}.` };\n }\n if (!isRecord(raw)) return { ok: false, error: `${partType} config must be an object.` };\n const rule = RULES[partType];\n const config: Record<string, unknown> = { ...raw };\n delete config['type'];\n const error = findUnknownKeys(partType, rule, config) ?? findMissingKey(partType, rule, config) ?? findBadValue(partType, rule, config);\n return error ? { ok: false, error } : { ok: true, config: { type: partType, ...trimText(rule, config) } };\n}\n\n/** The current config with the patch's keys laid over it. `type` stays the current part type. Neither input changes. */\nexport function MergePartConfig(current: PanelConfig, patch: Record<string, unknown>): PanelConfig {\n return { ...current, ...patch, type: current.type };\n}\n\n/**\n * One sentence per part type naming its required and optional keys, with the accepted values or format\n * where they are fixed, for tool descriptions. The text has no closing period, so a caller can embed it.\n */\nexport function DescribePartConfigs(): string {\n return DASHBOARD_PART_TYPE_NAMES.map((name) => describeRule(name, RULES[name])).join('. ');\n}\n\n/** True for an object that is not null and not an array. */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** False for undefined, null and blank text, the values a config panel treats as an empty field. */\nfunction isSet(value: unknown): boolean {\n return value !== undefined && value !== null && !(typeof value === 'string' && value.trim() === '');\n}\n\n/** True for text that parses as an http or https URL, as the WebURL config panel checks it. */\nfunction isHttpUrl(text: string): boolean {\n try {\n const protocol = new URL(text).protocol;\n return protocol === 'http:' || protocol === 'https:';\n } catch {\n return false;\n }\n}\n\n/** An error that names the keys the rule does not allow and lists the allowed ones, or null. */\nfunction findUnknownKeys(partType: DashboardPartTypeName, rule: PartConfigRule, config: Record<string, unknown>): string | null {\n const allowed = Object.keys(rule.keys);\n const unknownKeys = Object.keys(config).filter((key) => !allowed.includes(key));\n return unknownKeys.length ? `${partType} config has unknown keys: ${unknownKeys.join(', ')}. Allowed keys: ${allowed.join(', ')}.` : null;\n}\n\n/** An error when none of the required keys is set, or null. */\nfunction findMissingKey(partType: DashboardPartTypeName, rule: PartConfigRule, config: Record<string, unknown>): string | null {\n return rule.requireOneOf.some((key) => isSet(config[key])) ? null : `${partType} config needs ${rule.requireOneOf.join(' or ')}.`;\n}\n\n/** An error for the first set value that its key's rule rejects, or null. */\nfunction findBadValue(partType: DashboardPartTypeName, rule: PartConfigRule, config: Record<string, unknown>): string | null {\n for (const [key, value] of Object.entries(config)) {\n if (value === undefined || value === null) continue;\n const problem = rule.keys[key]?.check(value);\n if (problem) return `${partType} config: ${key} ${problem}.`;\n }\n return null;\n}\n\n/** A copy of a checked config with the text values of trimmed keys (ids, names and URLs) trimmed. */\nfunction trimText(rule: PartConfigRule, config: Record<string, unknown>): Record<string, unknown> {\n const trimmed: Record<string, unknown> = { ...config };\n for (const [key, value] of Object.entries(config)) {\n if (rule.keys[key]?.trim && typeof value === 'string') trimmed[key] = value.trim();\n }\n return trimmed;\n}\n\n/** \"<name>: needs <required keys>; optional <other keys>\", with each key's hint in parentheses. */\nfunction describeRule(name: DashboardPartTypeName, rule: PartConfigRule): string {\n const required = rule.requireOneOf.map((key) => describeKey(rule, key)).join(' or ');\n const optional = Object.keys(rule.keys).filter((key) => !rule.requireOneOf.includes(key));\n return `${name}: needs ${required}; optional ${optional.map((key) => describeKey(rule, key)).join(', ')}`;\n}\n\n/** The key name, followed by its hint in parentheses when it has one. */\nfunction describeKey(rule: PartConfigRule, key: string): string {\n const hint = rule.keys[key]?.hint;\n return hint ? `${key} (${hint})` : key;\n}\n"]}
|