@spike-rabbit/dashboards-ng 49.0.0-next.1
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/LICENSE.md +20 -0
- package/README.md +230 -0
- package/assets/i18n/de.json +46 -0
- package/assets/i18n/en.json +46 -0
- package/fesm2022/spike-rabbit-dashboards-ng-module-federation.mjs +37 -0
- package/fesm2022/spike-rabbit-dashboards-ng-module-federation.mjs.map +1 -0
- package/fesm2022/spike-rabbit-dashboards-ng-translate.mjs +21 -0
- package/fesm2022/spike-rabbit-dashboards-ng-translate.mjs.map +1 -0
- package/fesm2022/spike-rabbit-dashboards-ng.mjs +2184 -0
- package/fesm2022/spike-rabbit-dashboards-ng.mjs.map +1 -0
- package/index.d.ts +1071 -0
- package/module-federation/index.d.ts +3 -0
- package/module-federation/package.json +3 -0
- package/package.json +51 -0
- package/template-i18n.json +35 -0
- package/translate/index.d.ts +47 -0
- package/translate/package.json +3 -0
|
@@ -0,0 +1,2184 @@
|
|
|
1
|
+
import { NgClass, AsyncPipe, NgTemplateOutlet } from '@angular/common';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { InjectionToken, inject, input, model, output, computed, ViewChild, Component, Injectable, Renderer2, DOCUMENT, ElementRef, Input, EventEmitter, createNgModule, Injector, EnvironmentInjector, viewChild, ViewContainerRef, isSignal, NgZone, signal, NgModule } from '@angular/core';
|
|
4
|
+
import { SiDashboardCardComponent, SiDashboardComponent } from '@spike-rabbit/element-ng/dashboard';
|
|
5
|
+
import * as i1 from '@spike-rabbit/element-translate-ng/translate';
|
|
6
|
+
import { t, SiTranslateModule, SiTranslatePipe } from '@spike-rabbit/element-translate-ng/translate';
|
|
7
|
+
import { BehaviorSubject, of, Subject, throwError, ReplaySubject, combineLatest } from 'rxjs';
|
|
8
|
+
import { takeUntil, take, map } from 'rxjs/operators';
|
|
9
|
+
import { BOOTSTRAP_BREAKPOINTS, SiResponsiveContainerDirective } from '@spike-rabbit/element-ng/resize-observer';
|
|
10
|
+
import { ActivatedRoute, RouterLink } from '@angular/router';
|
|
11
|
+
import { SiContentActionBarComponent } from '@spike-rabbit/element-ng/content-action-bar';
|
|
12
|
+
import { SiLinkDirective } from '@spike-rabbit/element-ng/link';
|
|
13
|
+
import { SiLoadingSpinnerComponent, SiLoadingService, SiLoadingSpinnerDirective } from '@spike-rabbit/element-ng/loading-spinner';
|
|
14
|
+
import { SiModalService } from '@spike-rabbit/element-ng/modal';
|
|
15
|
+
import { GridStack } from 'gridstack';
|
|
16
|
+
import { SiActionDialogService } from '@spike-rabbit/element-ng/action-modal';
|
|
17
|
+
import { SiCircleStatusComponent } from '@spike-rabbit/element-ng/circle-status';
|
|
18
|
+
import { SiEmptyStateComponent } from '@spike-rabbit/element-ng/empty-state';
|
|
19
|
+
import { SiSearchBarComponent } from '@spike-rabbit/element-ng/search-bar';
|
|
20
|
+
export * from '@spike-rabbit/dashboards-ng/translate';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
24
|
+
* SPDX-License-Identifier: MIT
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* The default gridstack configuration options.
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_GRIDSTACK_OPTIONS = {
|
|
30
|
+
handle: '.draggable-overlay',
|
|
31
|
+
cellHeight: 100,
|
|
32
|
+
columnOpts: { breakpoints: [{ w: BOOTSTRAP_BREAKPOINTS.smMinimum, c: 1 }] },
|
|
33
|
+
column: 12,
|
|
34
|
+
margin: '0',
|
|
35
|
+
placeholderClass: 'si-grid-stack-placeholder'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
40
|
+
* SPDX-License-Identifier: MIT
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Injection token to configure dashboards. Use `{ provide: SI_DASHBOARD_CONFIGURATION, useValue: config }`
|
|
44
|
+
* in your app configuration.
|
|
45
|
+
*/
|
|
46
|
+
const SI_DASHBOARD_CONFIGURATION = new InjectionToken('Injection token to configure dashboards.', {
|
|
47
|
+
providedIn: 'root',
|
|
48
|
+
factory: () => ({ grid: { gridStackOptions: DEFAULT_GRIDSTACK_OPTIONS } })
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* @deprecated Use SI_DASHBOARD_CONFIGURATION instead.
|
|
52
|
+
*/
|
|
53
|
+
const CONFIG_TOKEN = SI_DASHBOARD_CONFIGURATION;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
57
|
+
* SPDX-License-Identifier: MIT
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* Injection token to configure your widget store implementation. Use
|
|
61
|
+
* `{ provide: SI_WIDGET_STORE, useClass: AppWidgetStorage }` in your app configuration.
|
|
62
|
+
*/
|
|
63
|
+
const SI_WIDGET_STORE = new InjectionToken('Injection token to configure your widget store implementation.', { providedIn: 'root', factory: () => new SiDefaultWidgetStorage() });
|
|
64
|
+
/**
|
|
65
|
+
* Widget storage api to provide the persistence layer of the widgets of a dashboard
|
|
66
|
+
* (typically from a web service). The dashboard grid uses this API to load and save
|
|
67
|
+
* the widget configurations. Applications using siemens-dashboard needs to implement
|
|
68
|
+
* this abstract class and provide it in the module configuration.
|
|
69
|
+
*/
|
|
70
|
+
class SiWidgetStorage {
|
|
71
|
+
/**
|
|
72
|
+
* Optional method to provide primary and secondary toolbar menu items.
|
|
73
|
+
* @param dashboardId - The id of the dashboard if present to allow dashboard specific menu items.
|
|
74
|
+
*/
|
|
75
|
+
getToolbarMenuItems;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The {@link SiDefaultWidgetStorage} uses this key to persist the dashboard
|
|
79
|
+
* configurations in the session of local storage.
|
|
80
|
+
*/
|
|
81
|
+
const STORAGE_KEY = 'dashboard-store';
|
|
82
|
+
/**
|
|
83
|
+
* Injection token to optionally inject the {@link Storage} implementation
|
|
84
|
+
* that will be used by the {@link SiDefaultWidgetStorage}. The default implementation
|
|
85
|
+
* is {@link sessionStorage}.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* The following shows how to provide the localStorage.
|
|
89
|
+
* ```
|
|
90
|
+
* providers: [..., { provide: DEFAULT_WIDGET_STORAGE_TOKEN, useValue: localStorage }]
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
*/
|
|
94
|
+
const DEFAULT_WIDGET_STORAGE_TOKEN = new InjectionToken('default storage for dashboard store');
|
|
95
|
+
/**
|
|
96
|
+
* Default implementation of {@link SiWidgetStorage} that uses the
|
|
97
|
+
* {@link Storage} implementation provided by the {@link DEFAULT_WIDGET_STORAGE_TOKEN}.
|
|
98
|
+
* In general, it persists the dashboard's configurations in the Browser's session or local
|
|
99
|
+
* storage. *
|
|
100
|
+
*/
|
|
101
|
+
class SiDefaultWidgetStorage extends SiWidgetStorage {
|
|
102
|
+
storage;
|
|
103
|
+
map = new Map();
|
|
104
|
+
widgets;
|
|
105
|
+
constructor() {
|
|
106
|
+
super();
|
|
107
|
+
this.storage = inject(DEFAULT_WIDGET_STORAGE_TOKEN, { optional: true }) ?? sessionStorage;
|
|
108
|
+
}
|
|
109
|
+
load(dashboardId) {
|
|
110
|
+
if (!dashboardId) {
|
|
111
|
+
this.widgets ??= new BehaviorSubject(this.loadFromStorage());
|
|
112
|
+
return this.widgets;
|
|
113
|
+
}
|
|
114
|
+
else if (!this.map.has(dashboardId)) {
|
|
115
|
+
this.map.set(dashboardId, new BehaviorSubject(this.loadFromStorage(dashboardId)));
|
|
116
|
+
}
|
|
117
|
+
return this.map.get(dashboardId);
|
|
118
|
+
}
|
|
119
|
+
save(widgets, removedWidgets, dashboardId) {
|
|
120
|
+
const newWidgets = widgets.map(widget => {
|
|
121
|
+
if (widget.id === undefined) {
|
|
122
|
+
return { ...widget, id: Math.random().toString(36).substring(2, 9) };
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
return widget;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
this.update(newWidgets, dashboardId);
|
|
129
|
+
return of(newWidgets);
|
|
130
|
+
}
|
|
131
|
+
update(newConfigs, dashboardId) {
|
|
132
|
+
const widgets$ = this.load(dashboardId);
|
|
133
|
+
widgets$.next(newConfigs);
|
|
134
|
+
if (!dashboardId) {
|
|
135
|
+
this.storage.setItem(STORAGE_KEY, JSON.stringify(newConfigs));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
this.storage.setItem(`${STORAGE_KEY}-${dashboardId}`, JSON.stringify(newConfigs));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
loadFromStorage(dashboardId) {
|
|
142
|
+
let configStr;
|
|
143
|
+
if (!dashboardId) {
|
|
144
|
+
configStr = this.storage.getItem(STORAGE_KEY);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
configStr = this.storage.getItem(`${STORAGE_KEY}-${dashboardId}`);
|
|
148
|
+
}
|
|
149
|
+
let widgetConfigs = [];
|
|
150
|
+
if (configStr) {
|
|
151
|
+
widgetConfigs = JSON.parse(configStr);
|
|
152
|
+
}
|
|
153
|
+
return widgetConfigs;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
159
|
+
* SPDX-License-Identifier: MIT
|
|
160
|
+
*/
|
|
161
|
+
/**
|
|
162
|
+
* The toolbar of the flexible dashboard is either `editable` or not. When not
|
|
163
|
+
* `editable`, it supports content projection for applications to inject toolbar
|
|
164
|
+
* controls for e.g. filtering. Use the attribute `filters-slot` to inject the filters.
|
|
165
|
+
* In addition it shows the button to switch to the `editable`. When `editable`, it support
|
|
166
|
+
* the cancel and save buttons, as well as displaying primary and secondary actions.
|
|
167
|
+
*/
|
|
168
|
+
class SiDashboardToolbarComponent {
|
|
169
|
+
/**
|
|
170
|
+
* Set primary actions that are in `editable` mode first visible or in
|
|
171
|
+
* the expanded content action bar of the toolbar.
|
|
172
|
+
*
|
|
173
|
+
* @defaultValue []
|
|
174
|
+
*/
|
|
175
|
+
primaryEditActions = input([]);
|
|
176
|
+
/**
|
|
177
|
+
* Set secondary actions that are in `editable` mode second visible or in
|
|
178
|
+
* the dropdown part of the content action bar of the toolbar.
|
|
179
|
+
*
|
|
180
|
+
* @defaultValue [] */
|
|
181
|
+
secondaryEditActions = input([]);
|
|
182
|
+
/**
|
|
183
|
+
* Input to disable the save button. Note, the input `disabled` disables all
|
|
184
|
+
* actions and the save button of the toolbar.
|
|
185
|
+
*
|
|
186
|
+
* @defaultValue true
|
|
187
|
+
*/
|
|
188
|
+
disableSaveButton = model(true);
|
|
189
|
+
/**
|
|
190
|
+
* Input to disable all actions and buttons of the toolbar.
|
|
191
|
+
*
|
|
192
|
+
* @defaultValue false */
|
|
193
|
+
disabled = input(false);
|
|
194
|
+
/**
|
|
195
|
+
* Input option to set the `editable` mode. When editable
|
|
196
|
+
* the toolbar shows a cancel and save button. Otherwise,
|
|
197
|
+
* it displays an edit button.
|
|
198
|
+
*
|
|
199
|
+
* @defaultValue false */
|
|
200
|
+
editable = model(false);
|
|
201
|
+
/**
|
|
202
|
+
* Option to hide the dashboard edit button.
|
|
203
|
+
*
|
|
204
|
+
* @defaultValue false
|
|
205
|
+
*/
|
|
206
|
+
hideEditButton = input(false);
|
|
207
|
+
/**
|
|
208
|
+
* Option to display the edit button as a text button instead, only if the window is larger than xs {@link SiResponsiveContainerDirective}.
|
|
209
|
+
*
|
|
210
|
+
* @defaultValue false
|
|
211
|
+
*/
|
|
212
|
+
showEditButtonLabel = input(false);
|
|
213
|
+
/**
|
|
214
|
+
* Emits on save button click.
|
|
215
|
+
*/
|
|
216
|
+
save = output();
|
|
217
|
+
/**
|
|
218
|
+
* Emits on cancel button click.
|
|
219
|
+
*/
|
|
220
|
+
// eslint-disable-next-line @angular-eslint/no-output-native
|
|
221
|
+
cancel = output();
|
|
222
|
+
labelEdit = t(() => $localize `:@@DASHBOARD.EDIT:Edit`);
|
|
223
|
+
labelCancel = t(() => $localize `:@@DASHBOARD.CANCEL:Cancel`);
|
|
224
|
+
labelSave = t(() => $localize `:@@DASHBOARD.SAVE:Save`);
|
|
225
|
+
activatedRoute = inject(ActivatedRoute, { optional: true });
|
|
226
|
+
dashboardToolbarContainer;
|
|
227
|
+
showContentActionBar = computed(() => this.primaryEditActions()?.length + this.secondaryEditActions()?.length > 3);
|
|
228
|
+
editActions = computed(() => [
|
|
229
|
+
...this.primaryEditActions(),
|
|
230
|
+
...this.secondaryEditActions()
|
|
231
|
+
]);
|
|
232
|
+
onEdit() {
|
|
233
|
+
this.editable.set(true);
|
|
234
|
+
}
|
|
235
|
+
onSave() {
|
|
236
|
+
if (this.editable()) {
|
|
237
|
+
this.save.emit();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
onCancel() {
|
|
241
|
+
if (this.editable()) {
|
|
242
|
+
this.cancel.emit();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
isToolbarItem(item) {
|
|
246
|
+
return 'label' in item;
|
|
247
|
+
}
|
|
248
|
+
showEditButtonLabelDesktop() {
|
|
249
|
+
return this.showEditButtonLabel() && !this.dashboardToolbarContainer?.xs();
|
|
250
|
+
}
|
|
251
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiDashboardToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
252
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: SiDashboardToolbarComponent, isStandalone: true, selector: "si-dashboard-toolbar", inputs: { primaryEditActions: { classPropertyName: "primaryEditActions", publicName: "primaryEditActions", isSignal: true, isRequired: false, transformFunction: null }, secondaryEditActions: { classPropertyName: "secondaryEditActions", publicName: "secondaryEditActions", isSignal: true, isRequired: false, transformFunction: null }, disableSaveButton: { classPropertyName: "disableSaveButton", publicName: "disableSaveButton", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, hideEditButton: { classPropertyName: "hideEditButton", publicName: "hideEditButton", isSignal: true, isRequired: false, transformFunction: null }, showEditButtonLabel: { classPropertyName: "showEditButtonLabel", publicName: "showEditButtonLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disableSaveButton: "disableSaveButtonChange", editable: "editableChange", save: "save", cancel: "cancel" }, viewQueries: [{ propertyName: "dashboardToolbarContainer", first: true, predicate: SiResponsiveContainerDirective, descendants: true }], ngImport: i0, template: "<div class=\"row mb-3\" siResponsiveContainer>\n <div class=\"col-12 d-flex justify-content-between\">\n @if (!editable()) {\n <ng-content select=\"[filters-slot]\" />\n @if (!hideEditButton()) {\n <button\n type=\"button\"\n class=\"btn ms-auto\"\n [ngClass]=\"\n showEditButtonLabelDesktop()\n ? 'btn-secondary'\n : 'btn-circle btn-sm btn-tertiary element-edit'\n \"\n [attr.aria-label]=\"labelEdit | translate\"\n [attr.title]=\"!showEditButtonLabelDesktop() ? (labelEdit | translate) : undefined\"\n [disabled]=\"disabled()\"\n (click)=\"onEdit()\"\n >\n @if (showEditButtonLabelDesktop()) {\n {{ labelEdit | translate }}\n }\n </button>\n }\n } @else {\n @if (showContentActionBar()) {\n <si-content-action-bar\n toggleItemLabel=\"toggle\"\n viewType=\"expanded\"\n [disabled]=\"disabled()\"\n [primaryActions]=\"primaryEditActions()\"\n [secondaryActions]=\"secondaryEditActions()\"\n />\n } @else {\n <div>\n @for (action of editActions(); track $index) {\n @if (isToolbarItem(action)) {\n @if (disabled()) {\n <!-- Links do not support disabled, so we need to render a button instead in this case. -->\n <button type=\"button\" class=\"btn btn-secondary me-4\" disabled>\n {{ action.label! | translate }}\n </button>\n } @else {\n @switch (action.type) {\n @case ('action') {\n <!-- eslint-disable @angular-eslint/template/no-any -->\n <!-- This item has a patched action which will inject the grid. But our types do not reflect this. -->\n <!-- TODO: make our types reflect that this item has a patched action. -->\n <button\n type=\"button\"\n class=\"btn btn-secondary me-4\"\n (click)=\"$any(action.action)()\"\n >\n {{ action.label! | translate }}\n </button>\n <!-- eslint-disable-enable @angular-eslint/template/no-any -->\n }\n @case ('router-link') {\n <a\n class=\"btn btn-secondary me-4\"\n [routerLink]=\"action.routerLink\"\n [queryParams]=\"action.extras?.queryParams\"\n [queryParamsHandling]=\"action.extras?.queryParamsHandling\"\n [fragment]=\"action.extras?.fragment\"\n [state]=\"action.extras?.state\"\n [relativeTo]=\"action.extras?.relativeTo ?? activatedRoute\"\n [preserveFragment]=\"action.extras?.preserveFragment\"\n [skipLocationChange]=\"action.extras?.skipLocationChange\"\n [replaceUrl]=\"action.extras?.replaceUrl\"\n >\n {{ action.label! | translate }}\n </a>\n }\n @case ('link') {\n <a class=\"btn btn-secondary me-4\" [href]=\"action.href\" [target]=\"action.target\">\n {{ action.label! | translate }}\n </a>\n }\n }\n }\n } @else {\n <button\n type=\"button\"\n class=\"btn btn-secondary me-4\"\n [disabled]=\"disabled()\"\n [siLink]=\"action\"\n >\n {{ action.title! | translate }}\n </button>\n }\n }\n </div>\n }\n <div>\n <button\n type=\"button\"\n class=\"btn btn-secondary me-4\"\n [disabled]=\"disabled()\"\n (click)=\"onCancel()\"\n >\n {{ labelCancel | translate }}\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n [disabled]=\"disableSaveButton() || disabled()\"\n (click)=\"onSave()\"\n >\n @if (disabled()) {\n <si-loading-spinner />\n } @else {\n <span>{{ labelSave | translate }}</span>\n }\n </button>\n </div>\n }\n </div>\n</div>\n", styles: [":host{display:flex;flex:1 1 0;flex-direction:column}si-loading-spinner{--loading-spinner-size: 1rem;--loading-spinner-color: var(--element-text-inverse)}\n"], dependencies: [{ kind: "component", type: SiContentActionBarComponent, selector: "si-content-action-bar", inputs: ["primaryActions", "secondaryActions", "actionParam", "viewType", "toggleItemLabel", "preventIconsInDropdownMenus", "disabled"] }, { kind: "directive", type: SiLinkDirective, selector: "[siLink]", inputs: ["siLink", "siLinkDefaultTarget", "actionParam", "activeClass", "exactMatch", "ariaCurrent"], outputs: ["activeChange"], exportAs: ["siLink"] }, { kind: "component", type: SiLoadingSpinnerComponent, selector: "si-loading-spinner", inputs: ["isBlockingSpinner", "isSpinnerOverlay", "ariaLabel"] }, { kind: "ngmodule", type: SiTranslateModule }, { kind: "pipe", type: i1.SiTranslatePipe, name: "translate" }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: SiResponsiveContainerDirective, selector: "[siResponsiveContainer]", inputs: ["resizeThrottle", "breakpoints"], exportAs: ["siResponsiveContainer"] }] });
|
|
253
|
+
}
|
|
254
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiDashboardToolbarComponent, decorators: [{
|
|
255
|
+
type: Component,
|
|
256
|
+
args: [{ selector: 'si-dashboard-toolbar', imports: [
|
|
257
|
+
SiContentActionBarComponent,
|
|
258
|
+
SiLinkDirective,
|
|
259
|
+
SiLoadingSpinnerComponent,
|
|
260
|
+
SiTranslateModule,
|
|
261
|
+
RouterLink,
|
|
262
|
+
NgClass,
|
|
263
|
+
SiResponsiveContainerDirective
|
|
264
|
+
], template: "<div class=\"row mb-3\" siResponsiveContainer>\n <div class=\"col-12 d-flex justify-content-between\">\n @if (!editable()) {\n <ng-content select=\"[filters-slot]\" />\n @if (!hideEditButton()) {\n <button\n type=\"button\"\n class=\"btn ms-auto\"\n [ngClass]=\"\n showEditButtonLabelDesktop()\n ? 'btn-secondary'\n : 'btn-circle btn-sm btn-tertiary element-edit'\n \"\n [attr.aria-label]=\"labelEdit | translate\"\n [attr.title]=\"!showEditButtonLabelDesktop() ? (labelEdit | translate) : undefined\"\n [disabled]=\"disabled()\"\n (click)=\"onEdit()\"\n >\n @if (showEditButtonLabelDesktop()) {\n {{ labelEdit | translate }}\n }\n </button>\n }\n } @else {\n @if (showContentActionBar()) {\n <si-content-action-bar\n toggleItemLabel=\"toggle\"\n viewType=\"expanded\"\n [disabled]=\"disabled()\"\n [primaryActions]=\"primaryEditActions()\"\n [secondaryActions]=\"secondaryEditActions()\"\n />\n } @else {\n <div>\n @for (action of editActions(); track $index) {\n @if (isToolbarItem(action)) {\n @if (disabled()) {\n <!-- Links do not support disabled, so we need to render a button instead in this case. -->\n <button type=\"button\" class=\"btn btn-secondary me-4\" disabled>\n {{ action.label! | translate }}\n </button>\n } @else {\n @switch (action.type) {\n @case ('action') {\n <!-- eslint-disable @angular-eslint/template/no-any -->\n <!-- This item has a patched action which will inject the grid. But our types do not reflect this. -->\n <!-- TODO: make our types reflect that this item has a patched action. -->\n <button\n type=\"button\"\n class=\"btn btn-secondary me-4\"\n (click)=\"$any(action.action)()\"\n >\n {{ action.label! | translate }}\n </button>\n <!-- eslint-disable-enable @angular-eslint/template/no-any -->\n }\n @case ('router-link') {\n <a\n class=\"btn btn-secondary me-4\"\n [routerLink]=\"action.routerLink\"\n [queryParams]=\"action.extras?.queryParams\"\n [queryParamsHandling]=\"action.extras?.queryParamsHandling\"\n [fragment]=\"action.extras?.fragment\"\n [state]=\"action.extras?.state\"\n [relativeTo]=\"action.extras?.relativeTo ?? activatedRoute\"\n [preserveFragment]=\"action.extras?.preserveFragment\"\n [skipLocationChange]=\"action.extras?.skipLocationChange\"\n [replaceUrl]=\"action.extras?.replaceUrl\"\n >\n {{ action.label! | translate }}\n </a>\n }\n @case ('link') {\n <a class=\"btn btn-secondary me-4\" [href]=\"action.href\" [target]=\"action.target\">\n {{ action.label! | translate }}\n </a>\n }\n }\n }\n } @else {\n <button\n type=\"button\"\n class=\"btn btn-secondary me-4\"\n [disabled]=\"disabled()\"\n [siLink]=\"action\"\n >\n {{ action.title! | translate }}\n </button>\n }\n }\n </div>\n }\n <div>\n <button\n type=\"button\"\n class=\"btn btn-secondary me-4\"\n [disabled]=\"disabled()\"\n (click)=\"onCancel()\"\n >\n {{ labelCancel | translate }}\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n [disabled]=\"disableSaveButton() || disabled()\"\n (click)=\"onSave()\"\n >\n @if (disabled()) {\n <si-loading-spinner />\n } @else {\n <span>{{ labelSave | translate }}</span>\n }\n </button>\n </div>\n }\n </div>\n</div>\n", styles: [":host{display:flex;flex:1 1 0;flex-direction:column}si-loading-spinner{--loading-spinner-size: 1rem;--loading-spinner-color: var(--element-text-inverse)}\n"] }]
|
|
265
|
+
}], propDecorators: { dashboardToolbarContainer: [{
|
|
266
|
+
type: ViewChild,
|
|
267
|
+
args: [SiResponsiveContainerDirective]
|
|
268
|
+
}] } });
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
272
|
+
* SPDX-License-Identifier: MIT
|
|
273
|
+
*/
|
|
274
|
+
class SiGridService {
|
|
275
|
+
/** @defaultValue [] */
|
|
276
|
+
widgetCatalog = [];
|
|
277
|
+
/**
|
|
278
|
+
* Observable that emits true if si-grid is set to editable.
|
|
279
|
+
* The owner of the editable state is the si-grid component.
|
|
280
|
+
* This subject is used to propagate these state changes via
|
|
281
|
+
* the si-widget-host components to the widgets.
|
|
282
|
+
*/
|
|
283
|
+
editable$ = new BehaviorSubject(false);
|
|
284
|
+
getWidget(id) {
|
|
285
|
+
return this.widgetCatalog.find(widget => widget.id === id);
|
|
286
|
+
}
|
|
287
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiGridService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
288
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiGridService });
|
|
289
|
+
}
|
|
290
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiGridService, decorators: [{
|
|
291
|
+
type: Injectable
|
|
292
|
+
}] });
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
296
|
+
* SPDX-License-Identifier: MIT
|
|
297
|
+
*/
|
|
298
|
+
class SiWebComponentWrapperBaseComponent {
|
|
299
|
+
_config;
|
|
300
|
+
get config() {
|
|
301
|
+
return this._config;
|
|
302
|
+
}
|
|
303
|
+
set config(config) {
|
|
304
|
+
this._config = config;
|
|
305
|
+
if (this.webComponentHost.nativeElement.children.length > 0) {
|
|
306
|
+
this.webComponentHost.nativeElement.children[0].config = config;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
elementTagName;
|
|
310
|
+
url;
|
|
311
|
+
webComponentHost;
|
|
312
|
+
webComponent;
|
|
313
|
+
renderer2 = inject(Renderer2);
|
|
314
|
+
document = inject(DOCUMENT);
|
|
315
|
+
ngAfterViewInit() {
|
|
316
|
+
if (!this.isScriptLoaded(this.url)) {
|
|
317
|
+
const script = this.renderer2.createElement('script');
|
|
318
|
+
script.src = this.url;
|
|
319
|
+
this.renderer2.appendChild(this.document.body, script);
|
|
320
|
+
}
|
|
321
|
+
this.webComponent = this.renderer2.createElement(this.elementTagName);
|
|
322
|
+
this.webComponent.config = this.config;
|
|
323
|
+
}
|
|
324
|
+
isScriptLoaded(url) {
|
|
325
|
+
const script = document.querySelector(`script[src='${url}']`);
|
|
326
|
+
return script ? true : false;
|
|
327
|
+
}
|
|
328
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiWebComponentWrapperBaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
329
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.6", type: SiWebComponentWrapperBaseComponent, isStandalone: true, selector: "ng-component", inputs: { config: "config", elementTagName: "elementTagName", url: "url" }, viewQueries: [{ propertyName: "webComponentHost", first: true, predicate: ["webComponentHost"], descendants: true, read: ElementRef, static: true }], ngImport: i0, template: '', isInline: true });
|
|
330
|
+
}
|
|
331
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiWebComponentWrapperBaseComponent, decorators: [{
|
|
332
|
+
type: Component,
|
|
333
|
+
args: [{
|
|
334
|
+
template: ''
|
|
335
|
+
}]
|
|
336
|
+
}], propDecorators: { config: [{
|
|
337
|
+
type: Input
|
|
338
|
+
}], elementTagName: [{
|
|
339
|
+
type: Input
|
|
340
|
+
}], url: [{
|
|
341
|
+
type: Input
|
|
342
|
+
}], webComponentHost: [{
|
|
343
|
+
type: ViewChild,
|
|
344
|
+
args: ['webComponentHost', { static: true, read: ElementRef }]
|
|
345
|
+
}] } });
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
349
|
+
* SPDX-License-Identifier: MIT
|
|
350
|
+
*/
|
|
351
|
+
class SiWebComponentEditorWrapperComponent extends SiWebComponentWrapperBaseComponent {
|
|
352
|
+
state;
|
|
353
|
+
stateChange = new Subject();
|
|
354
|
+
configChange = new Subject();
|
|
355
|
+
/**
|
|
356
|
+
* This will be set when setupWidgetInstanceEditor is executed
|
|
357
|
+
*/
|
|
358
|
+
statusChangesHandler;
|
|
359
|
+
webComponentEventListener = (event) => this.configChange.next(event.detail);
|
|
360
|
+
webComponentStateChangeListener = (event) => {
|
|
361
|
+
this.state = event.detail;
|
|
362
|
+
this.stateChange.next(event.detail);
|
|
363
|
+
};
|
|
364
|
+
webComponentStatusChangesListener = (event) => {
|
|
365
|
+
this.statusChangesHandler?.(event.detail);
|
|
366
|
+
};
|
|
367
|
+
ngAfterViewInit() {
|
|
368
|
+
super.ngAfterViewInit();
|
|
369
|
+
this.webComponent?.addEventListener('stateChange', this.webComponentStateChangeListener);
|
|
370
|
+
this.webComponent?.addEventListener('statusChanges', this.webComponentStatusChangesListener);
|
|
371
|
+
this.webComponent?.addEventListener('configChange', this.webComponentEventListener);
|
|
372
|
+
this.webComponentHost.nativeElement.appendChild(this.webComponent);
|
|
373
|
+
}
|
|
374
|
+
ngOnDestroy() {
|
|
375
|
+
this.webComponent?.removeEventListener('stateChange', this.webComponentStateChangeListener);
|
|
376
|
+
this.webComponent?.removeEventListener('statusChanges', this.webComponentStatusChangesListener);
|
|
377
|
+
this.webComponent?.removeEventListener('configChange', this.webComponentEventListener);
|
|
378
|
+
}
|
|
379
|
+
next() {
|
|
380
|
+
this.webComponent?.dispatchEvent(new CustomEvent('next'));
|
|
381
|
+
}
|
|
382
|
+
previous() {
|
|
383
|
+
this.webComponent?.dispatchEvent(new CustomEvent('previous'));
|
|
384
|
+
}
|
|
385
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiWebComponentEditorWrapperComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
386
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.6", type: SiWebComponentEditorWrapperComponent, isStandalone: true, selector: "si-web-component-editor-wrapper", usesInheritance: true, ngImport: i0, template: "<div #webComponentHost class=\"h-100\"></div>\n" });
|
|
387
|
+
}
|
|
388
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiWebComponentEditorWrapperComponent, decorators: [{
|
|
389
|
+
type: Component,
|
|
390
|
+
args: [{ selector: 'si-web-component-editor-wrapper', template: "<div #webComponentHost class=\"h-100\"></div>\n" }]
|
|
391
|
+
}] });
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
395
|
+
* SPDX-License-Identifier: MIT
|
|
396
|
+
*/
|
|
397
|
+
class SiWebComponentWrapperComponent extends SiWebComponentWrapperBaseComponent {
|
|
398
|
+
_editable;
|
|
399
|
+
get editable() {
|
|
400
|
+
return this._editable ?? false;
|
|
401
|
+
}
|
|
402
|
+
set editable(editable) {
|
|
403
|
+
this._editable = editable;
|
|
404
|
+
if (this.webComponentHost.nativeElement.children.length > 0) {
|
|
405
|
+
this.webComponentHost.nativeElement.children[0].editable = editable;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
primaryActions;
|
|
409
|
+
secondaryActions;
|
|
410
|
+
primaryEditActions;
|
|
411
|
+
secondaryEditActions;
|
|
412
|
+
configChange = new EventEmitter();
|
|
413
|
+
webComponentEventListener = (event) => this.configChanged(event.detail);
|
|
414
|
+
ngAfterViewInit() {
|
|
415
|
+
super.ngAfterViewInit();
|
|
416
|
+
this.webComponent?.addEventListener('configChange', this.webComponentEventListener);
|
|
417
|
+
this.webComponentHost.nativeElement.appendChild(this.webComponent);
|
|
418
|
+
}
|
|
419
|
+
ngOnDestroy() {
|
|
420
|
+
this.webComponent?.removeEventListener('configChange', this.webComponentEventListener);
|
|
421
|
+
}
|
|
422
|
+
configChanged(event) {
|
|
423
|
+
this.primaryActions = event.primaryActions ?? this.primaryActions;
|
|
424
|
+
this.secondaryActions = event.secondaryActions ?? this.secondaryActions;
|
|
425
|
+
this.primaryEditActions = event.primaryEditActions ?? this.primaryEditActions;
|
|
426
|
+
this.secondaryEditActions = event.secondaryEditActions ?? this.secondaryEditActions;
|
|
427
|
+
this.configChange.emit(event);
|
|
428
|
+
}
|
|
429
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiWebComponentWrapperComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
430
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.6", type: SiWebComponentWrapperComponent, isStandalone: true, selector: "si-web-component-wrapper", inputs: { editable: "editable" }, usesInheritance: true, ngImport: i0, template: "<div #webComponentHost class=\"h-100\"></div>\n" });
|
|
431
|
+
}
|
|
432
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiWebComponentWrapperComponent, decorators: [{
|
|
433
|
+
type: Component,
|
|
434
|
+
args: [{ selector: 'si-web-component-wrapper', template: "<div #webComponentHost class=\"h-100\"></div>\n" }]
|
|
435
|
+
}], propDecorators: { editable: [{
|
|
436
|
+
type: Input
|
|
437
|
+
}] } });
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
441
|
+
* SPDX-License-Identifier: MIT
|
|
442
|
+
*/
|
|
443
|
+
const widgetFactoryRegistry = {
|
|
444
|
+
_factories: {},
|
|
445
|
+
register(name, factoryFn) {
|
|
446
|
+
this._factories[name] = factoryFn;
|
|
447
|
+
},
|
|
448
|
+
getFactoryFn(name) {
|
|
449
|
+
return this._factories[name];
|
|
450
|
+
},
|
|
451
|
+
hasFactoryFn(name) {
|
|
452
|
+
return this._factories[name] !== undefined;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
const setupWidgetInstance = (widgetComponentFactory, host, injector, envInjector) => setupComponent(widgetComponentFactory, host, injector, envInjector, 'componentName');
|
|
456
|
+
const setupWidgetEditor = (widgetComponentFactory, host, injector, envInjector) => setupComponent(widgetComponentFactory, host, injector, envInjector, 'editorComponentName');
|
|
457
|
+
const setupComponent = (widgetComponentFactory, host, injector, envInjector, componentName) => {
|
|
458
|
+
if (!widgetComponentFactory.factoryType || widgetComponentFactory.factoryType === 'default') {
|
|
459
|
+
return loadAndAttachComponent(widgetComponentFactory, componentName, host, injector, envInjector);
|
|
460
|
+
}
|
|
461
|
+
else if (widgetComponentFactory.factoryType === 'web-component') {
|
|
462
|
+
return loadAndAttachWebComponentWrapper(widgetComponentFactory, componentName, host);
|
|
463
|
+
}
|
|
464
|
+
else if (widgetFactoryRegistry.hasFactoryFn(widgetComponentFactory.factoryType)) {
|
|
465
|
+
const setupFn = widgetFactoryRegistry.getFactoryFn(widgetComponentFactory.factoryType);
|
|
466
|
+
return setupFn(widgetComponentFactory, componentName, host, injector, envInjector);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
return throwError(() => new Error(`Unknown widget factory type ${widgetComponentFactory.factoryType}.`));
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
const loadAndAttachComponent = (factory, componentName, host, injector, envInjector) => {
|
|
473
|
+
const result = new Subject();
|
|
474
|
+
if (factory[componentName]) {
|
|
475
|
+
factory.moduleLoader(factory[componentName]).then(module => {
|
|
476
|
+
const ngModuleRef = createNgModule(module[factory.moduleName], envInjector);
|
|
477
|
+
const componentKey = factory[componentName];
|
|
478
|
+
if (!componentKey) {
|
|
479
|
+
throw new Error(`Component configuration for ${componentName} is undefined.`);
|
|
480
|
+
}
|
|
481
|
+
const componentType = module[componentKey];
|
|
482
|
+
const widgetInstanceRef = host.createComponent(componentType, { injector, ngModuleRef });
|
|
483
|
+
result.next(widgetInstanceRef);
|
|
484
|
+
result.complete();
|
|
485
|
+
}, rejection => {
|
|
486
|
+
const msg = rejection
|
|
487
|
+
? `Loading widget module ${factory.moduleName} failed with ${JSON.stringify(rejection.toString())}`
|
|
488
|
+
: `Loading widget module ${factory.moduleName} failed`;
|
|
489
|
+
result.error(msg);
|
|
490
|
+
result.complete();
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
result.error(`Provided component factory has no ${componentName} component configuration`);
|
|
495
|
+
}
|
|
496
|
+
return result;
|
|
497
|
+
};
|
|
498
|
+
const loadAndAttachWebComponentWrapper = (factory, componentName, host) => {
|
|
499
|
+
const result = new ReplaySubject();
|
|
500
|
+
if (factory[componentName]) {
|
|
501
|
+
let widgetInstanceRef;
|
|
502
|
+
if (componentName === 'componentName') {
|
|
503
|
+
widgetInstanceRef = host.createComponent(SiWebComponentWrapperComponent);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
widgetInstanceRef = host.createComponent(SiWebComponentEditorWrapperComponent);
|
|
507
|
+
}
|
|
508
|
+
widgetInstanceRef.instance.elementTagName = factory[componentName];
|
|
509
|
+
widgetInstanceRef.instance.url = factory.url;
|
|
510
|
+
result.next(widgetInstanceRef);
|
|
511
|
+
result.complete();
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
result.error(`Provided component factory has no ${componentName} component configuration`);
|
|
515
|
+
}
|
|
516
|
+
return result;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
521
|
+
* SPDX-License-Identifier: MIT
|
|
522
|
+
*/
|
|
523
|
+
class SiWidgetHostComponent {
|
|
524
|
+
siModal = inject(SiActionDialogService);
|
|
525
|
+
gridService = inject(SiGridService);
|
|
526
|
+
injector = inject(Injector);
|
|
527
|
+
envInjector = inject(EnvironmentInjector);
|
|
528
|
+
unsubscribe = new Subject();
|
|
529
|
+
widgetConfig = input.required();
|
|
530
|
+
remove = output();
|
|
531
|
+
edit = output();
|
|
532
|
+
initCompleted = output();
|
|
533
|
+
card = viewChild.required('card');
|
|
534
|
+
widgetHost = viewChild.required('widgetHost', { read: ViewContainerRef });
|
|
535
|
+
labelEdit = t(() => $localize `:@@DASHBOARD.WIDGET.EDIT:Edit`);
|
|
536
|
+
labelRemove = t(() => $localize `:@@DASHBOARD.WIDGET.REMOVE:Remove`);
|
|
537
|
+
labelExpand = t(() => $localize `:@@DASHBOARD.WIDGET.EXPAND:Expand`);
|
|
538
|
+
labelRestore = t(() => $localize `:@@DASHBOARD.WIDGET.RESTORE:Restore`);
|
|
539
|
+
labelDialogMessage = t(() => $localize `:@@DASHBOARD.REMOVE_WIDGET_CONFIRMATION_DIALOG.MESSAGE:Do you really want to remove the widget?`);
|
|
540
|
+
labelDialogHeading = t(() => $localize `:@@DASHBOARD.REMOVE_WIDGET_CONFIRMATION_DIALOG.HEADING:Remove widget`);
|
|
541
|
+
labelDialogRemove = t(() => $localize `:@@DASHBOARD.REMOVE_WIDGET_CONFIRMATION_DIALOG.REMOVE:Remove`);
|
|
542
|
+
labelDialogCancel = t(() => $localize `:@@DASHBOARD.REMOVE_WIDGET_CONFIRMATION_DIALOG.CANCEL:Cancel`);
|
|
543
|
+
widgetInstance;
|
|
544
|
+
widgetRef;
|
|
545
|
+
/** @defaultValue [] */
|
|
546
|
+
primaryActions = [];
|
|
547
|
+
/** @defaultValue [] */
|
|
548
|
+
secondaryActions = [];
|
|
549
|
+
/** @defaultValue 'expanded' */
|
|
550
|
+
actionBarViewType = 'expanded';
|
|
551
|
+
editable$ = this.gridService.editable$;
|
|
552
|
+
/** @defaultValue [] */
|
|
553
|
+
editablePrimaryActions = [];
|
|
554
|
+
/** @defaultValue [] */
|
|
555
|
+
editableSecondaryActions = [];
|
|
556
|
+
/**
|
|
557
|
+
* @defaultValue
|
|
558
|
+
* ```
|
|
559
|
+
* {
|
|
560
|
+
* type: 'action',
|
|
561
|
+
* label: this.labelEdit,
|
|
562
|
+
* icon: 'element-edit',
|
|
563
|
+
* iconOnly: true,
|
|
564
|
+
* action: () => this.onEdit()
|
|
565
|
+
* }
|
|
566
|
+
* ```
|
|
567
|
+
*/
|
|
568
|
+
editAction = {
|
|
569
|
+
type: 'action',
|
|
570
|
+
label: this.labelEdit,
|
|
571
|
+
icon: 'element-edit',
|
|
572
|
+
iconOnly: true,
|
|
573
|
+
action: () => this.onEdit()
|
|
574
|
+
};
|
|
575
|
+
/**
|
|
576
|
+
* @defaultValue
|
|
577
|
+
* ```
|
|
578
|
+
* {
|
|
579
|
+
* type: 'action',
|
|
580
|
+
* label: this.labelRemove,
|
|
581
|
+
* icon: 'element-delete',
|
|
582
|
+
* iconOnly: true,
|
|
583
|
+
* action: () => this.onRemove()
|
|
584
|
+
* }
|
|
585
|
+
* ```
|
|
586
|
+
*/
|
|
587
|
+
removeAction = {
|
|
588
|
+
type: 'action',
|
|
589
|
+
label: this.labelRemove,
|
|
590
|
+
icon: 'element-delete',
|
|
591
|
+
iconOnly: true,
|
|
592
|
+
action: () => this.onRemove()
|
|
593
|
+
};
|
|
594
|
+
widgetInstanceFooter;
|
|
595
|
+
get accentLine() {
|
|
596
|
+
const widgetConfig = this.widgetConfig();
|
|
597
|
+
return widgetConfig.accentLine ? 'accent-' + widgetConfig.accentLine : '';
|
|
598
|
+
}
|
|
599
|
+
ngOnChanges(changes) {
|
|
600
|
+
if (changes.widgetConfig) {
|
|
601
|
+
if (this.widgetRef) {
|
|
602
|
+
if (isSignal(this.widgetRef.instance.config)) {
|
|
603
|
+
this.widgetRef.setInput('config', this.widgetConfig());
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
this.widgetRef.instance.config = this.widgetConfig();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
ngOnInit() {
|
|
612
|
+
this.attachWidgetInstance();
|
|
613
|
+
this.editable$
|
|
614
|
+
.pipe(takeUntil(this.unsubscribe))
|
|
615
|
+
.subscribe(editable => this.setupEditable(editable));
|
|
616
|
+
}
|
|
617
|
+
ngOnDestroy() {
|
|
618
|
+
this.unsubscribe.next();
|
|
619
|
+
this.unsubscribe.complete();
|
|
620
|
+
}
|
|
621
|
+
attachWidgetInstance() {
|
|
622
|
+
const widget = this.gridService.getWidget(this.widgetConfig().widgetId);
|
|
623
|
+
if (widget) {
|
|
624
|
+
setupWidgetInstance(widget.componentFactory, this.widgetHost(), this.injector, this.envInjector).subscribe({
|
|
625
|
+
next: (widgetRef) => {
|
|
626
|
+
this.widgetInstance = widgetRef.instance;
|
|
627
|
+
this.widgetRef = widgetRef;
|
|
628
|
+
if (this.widgetInstance.configChange) {
|
|
629
|
+
// Note: setTimeout is needed to prevent ExpressionChangedAfterItHasBeenCheckedError
|
|
630
|
+
// on web component, who pushes their configuration through an event after being attached
|
|
631
|
+
// to the DOM.
|
|
632
|
+
this.widgetInstance.configChange
|
|
633
|
+
.pipe(takeUntil(this.unsubscribe))
|
|
634
|
+
.subscribe(event => setTimeout(() => this.setupEditable(this.editable$.value, event)));
|
|
635
|
+
}
|
|
636
|
+
if (isSignal(this.widgetInstance.config)) {
|
|
637
|
+
this.widgetRef.setInput('config', this.widgetConfig());
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
this.widgetInstance.config = this.widgetConfig();
|
|
641
|
+
}
|
|
642
|
+
this.widgetInstanceFooter = this.widgetInstance.footer;
|
|
643
|
+
this.setupEditable(this.gridService.editable$.value);
|
|
644
|
+
this.initCompleted.emit();
|
|
645
|
+
},
|
|
646
|
+
error: error => console.error('Error: ', error)
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
console.error(`Cannot find widget with id ${this.widgetConfig().widgetId}`);
|
|
651
|
+
this.initCompleted.emit();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
setupEditable(editable, widgetConfig) {
|
|
655
|
+
widgetConfig ??= {
|
|
656
|
+
primaryActions: this.widgetInstance?.primaryActions,
|
|
657
|
+
secondaryActions: this.widgetInstance?.secondaryActions,
|
|
658
|
+
primaryEditActions: this.widgetInstance?.primaryEditActions,
|
|
659
|
+
secondaryEditActions: this.widgetInstance?.secondaryEditActions
|
|
660
|
+
};
|
|
661
|
+
if (editable) {
|
|
662
|
+
this.editablePrimaryActions = [];
|
|
663
|
+
if (this.isEditable()) {
|
|
664
|
+
this.editablePrimaryActions.push(this.editAction);
|
|
665
|
+
}
|
|
666
|
+
if (!this.widgetConfig().isNotRemovable) {
|
|
667
|
+
this.editablePrimaryActions.push(this.removeAction);
|
|
668
|
+
}
|
|
669
|
+
if (widgetConfig.primaryEditActions) {
|
|
670
|
+
this.primaryActions = [...widgetConfig.primaryEditActions, ...this.editablePrimaryActions];
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
this.primaryActions = this.editablePrimaryActions;
|
|
674
|
+
}
|
|
675
|
+
if (widgetConfig.secondaryEditActions) {
|
|
676
|
+
this.secondaryActions = [
|
|
677
|
+
...widgetConfig.secondaryEditActions,
|
|
678
|
+
...this.editableSecondaryActions
|
|
679
|
+
];
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
this.secondaryActions = this.editableSecondaryActions;
|
|
683
|
+
}
|
|
684
|
+
this.actionBarViewType = 'expanded';
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
this.actionBarViewType = this.widgetConfig().actionBarViewType ?? 'expanded';
|
|
688
|
+
this.primaryActions = widgetConfig.primaryActions ?? [];
|
|
689
|
+
this.secondaryActions = widgetConfig.secondaryActions ?? [];
|
|
690
|
+
}
|
|
691
|
+
if (this.widgetInstance?.editable !== undefined) {
|
|
692
|
+
if (isSignal(this.widgetInstance.editable)) {
|
|
693
|
+
this.widgetRef?.setInput('editable', editable);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
this.widgetInstance.editable = editable;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
doRemove() {
|
|
701
|
+
const card = this.card();
|
|
702
|
+
if (card.isExpanded()) {
|
|
703
|
+
card.restore();
|
|
704
|
+
}
|
|
705
|
+
const widgetConfig = this.widgetConfig();
|
|
706
|
+
if (widgetConfig.id) {
|
|
707
|
+
this.remove.emit(widgetConfig.id);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
isEditable() {
|
|
711
|
+
const widgetConfig = this.widgetConfig();
|
|
712
|
+
return (!widgetConfig.immutable &&
|
|
713
|
+
!!this.gridService.getWidget(widgetConfig.widgetId)?.componentFactory?.editorComponentName);
|
|
714
|
+
}
|
|
715
|
+
onEdit() {
|
|
716
|
+
this.edit.emit(this.widgetConfig());
|
|
717
|
+
}
|
|
718
|
+
onRemove() {
|
|
719
|
+
this.siModal
|
|
720
|
+
.showActionDialog({
|
|
721
|
+
type: 'delete-confirm',
|
|
722
|
+
message: this.labelDialogMessage,
|
|
723
|
+
heading: this.labelDialogHeading,
|
|
724
|
+
deleteBtnName: this.labelDialogRemove,
|
|
725
|
+
cancelBtnName: this.labelDialogCancel
|
|
726
|
+
})
|
|
727
|
+
.subscribe(result => {
|
|
728
|
+
if (result === 'delete') {
|
|
729
|
+
this.doRemove();
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiWidgetHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
734
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: SiWidgetHostComponent, isStandalone: true, selector: "si-widget-host", inputs: { widgetConfig: { classPropertyName: "widgetConfig", publicName: "widgetConfig", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { remove: "remove", edit: "edit", initCompleted: "initCompleted" }, host: { classAttribute: "grid-stack-item" }, viewQueries: [{ propertyName: "card", first: true, predicate: ["card"], descendants: true, isSignal: true }, { propertyName: "widgetHost", first: true, predicate: ["widgetHost"], descendants: true, read: ViewContainerRef, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"grid-stack-item-content p-4\">\n @if (editable$ | async) {\n <div class=\"resize-handle\">\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <g>\n <path d=\"M16 16H18V18H16V16Z\" fill=\"currentColor\" />\n <path d=\"M11 16H13V18H11V16Z\" fill=\"currentColor\" />\n <path d=\"M11 11H13V13H11V11Z\" fill=\"currentColor\" />\n <path d=\"M6 16H8V18H6V16Z\" fill=\"currentColor\" />\n <path d=\"M16 11H18V13H16V11Z\" fill=\"currentColor\" />\n <path d=\"M16 6H18V8H16V6Z\" fill=\"currentColor\" />\n </g>\n </svg>\n </div>\n <div class=\"draggable-overlay\"></div>\n }\n <si-dashboard-card\n #card\n class=\"h-100 dragging-card\"\n [ngClass]=\"accentLine\"\n [expandText]=\"labelExpand\"\n [restoreText]=\"labelRestore\"\n [heading]=\"widgetConfig().heading\"\n [primaryActions]=\"primaryActions\"\n [secondaryActions]=\"secondaryActions\"\n [actionBarViewType]=\"actionBarViewType\"\n [enableExpandInteraction]=\"widgetConfig().expandable\"\n [imgAlt]=\"widgetConfig().image?.alt\"\n [imgDir]=\"widgetConfig().image?.dir\"\n [imgObjectFit]=\"widgetConfig().image?.objectFit\"\n [imgObjectPosition]=\"widgetConfig().image?.objectPosition\"\n [imgSrc]=\"widgetConfig().image?.src\"\n >\n <div class=\"card-body overflow-auto\" body>\n <ng-container #widgetHost />\n </div>\n\n @if (widgetInstanceFooter) {\n <div class=\"card-footer\" footer>\n <ng-container *ngTemplateOutlet=\"widgetInstanceFooter\" />\n </div>\n }\n </si-dashboard-card>\n</div>\n", styles: [":host{display:flex}::ng-deep si-content-action-bar{z-index:500}::ng-deep .ui-droppable.ui-droppable-over>*:not(.ui-droppable){cursor:move!important;pointer-events:inherit!important}::ng-deep .ui-draggable-dragging .dragging-card,::ng-deep .ui-resizable-resizing .dragging-card{box-shadow:0 0 8px var(--element-box-shadow-color-1),0 8px 8px var(--element-box-shadow-color-2)!important}::ng-deep .ui-draggable-dragging>.grid-stack-item-content,::ng-deep .ui-resizable-resizing>.grid-stack-item-content{box-shadow:none;opacity:1}::ng-deep .ui-resizable-handle{display:block!important}::ng-deep .ui-resizable-se{background-image:none!important;transform:none!important}.resize-handle{inset-block-end:6px;color:var(--element-ui-2);pointer-events:none;position:absolute;inset-inline-end:6px;text-align:center;z-index:10}.draggable-overlay{cursor:move;inset-block-end:24px;inset-block-start:40px;inset-inline:24px;position:absolute;z-index:100}\n"], dependencies: [{ kind: "component", type: SiDashboardCardComponent, selector: "si-dashboard-card", inputs: ["restoreText", "expandText", "enableExpandInteraction", "showMenubar"], outputs: ["expandChange"] }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
735
|
+
}
|
|
736
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiWidgetHostComponent, decorators: [{
|
|
737
|
+
type: Component,
|
|
738
|
+
args: [{ selector: 'si-widget-host', imports: [SiDashboardCardComponent, AsyncPipe, NgClass, NgTemplateOutlet], host: {
|
|
739
|
+
class: 'grid-stack-item'
|
|
740
|
+
}, template: "<div class=\"grid-stack-item-content p-4\">\n @if (editable$ | async) {\n <div class=\"resize-handle\">\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <g>\n <path d=\"M16 16H18V18H16V16Z\" fill=\"currentColor\" />\n <path d=\"M11 16H13V18H11V16Z\" fill=\"currentColor\" />\n <path d=\"M11 11H13V13H11V11Z\" fill=\"currentColor\" />\n <path d=\"M6 16H8V18H6V16Z\" fill=\"currentColor\" />\n <path d=\"M16 11H18V13H16V11Z\" fill=\"currentColor\" />\n <path d=\"M16 6H18V8H16V6Z\" fill=\"currentColor\" />\n </g>\n </svg>\n </div>\n <div class=\"draggable-overlay\"></div>\n }\n <si-dashboard-card\n #card\n class=\"h-100 dragging-card\"\n [ngClass]=\"accentLine\"\n [expandText]=\"labelExpand\"\n [restoreText]=\"labelRestore\"\n [heading]=\"widgetConfig().heading\"\n [primaryActions]=\"primaryActions\"\n [secondaryActions]=\"secondaryActions\"\n [actionBarViewType]=\"actionBarViewType\"\n [enableExpandInteraction]=\"widgetConfig().expandable\"\n [imgAlt]=\"widgetConfig().image?.alt\"\n [imgDir]=\"widgetConfig().image?.dir\"\n [imgObjectFit]=\"widgetConfig().image?.objectFit\"\n [imgObjectPosition]=\"widgetConfig().image?.objectPosition\"\n [imgSrc]=\"widgetConfig().image?.src\"\n >\n <div class=\"card-body overflow-auto\" body>\n <ng-container #widgetHost />\n </div>\n\n @if (widgetInstanceFooter) {\n <div class=\"card-footer\" footer>\n <ng-container *ngTemplateOutlet=\"widgetInstanceFooter\" />\n </div>\n }\n </si-dashboard-card>\n</div>\n", styles: [":host{display:flex}::ng-deep si-content-action-bar{z-index:500}::ng-deep .ui-droppable.ui-droppable-over>*:not(.ui-droppable){cursor:move!important;pointer-events:inherit!important}::ng-deep .ui-draggable-dragging .dragging-card,::ng-deep .ui-resizable-resizing .dragging-card{box-shadow:0 0 8px var(--element-box-shadow-color-1),0 8px 8px var(--element-box-shadow-color-2)!important}::ng-deep .ui-draggable-dragging>.grid-stack-item-content,::ng-deep .ui-resizable-resizing>.grid-stack-item-content{box-shadow:none;opacity:1}::ng-deep .ui-resizable-handle{display:block!important}::ng-deep .ui-resizable-se{background-image:none!important;transform:none!important}.resize-handle{inset-block-end:6px;color:var(--element-ui-2);pointer-events:none;position:absolute;inset-inline-end:6px;text-align:center;z-index:10}.draggable-overlay{cursor:move;inset-block-end:24px;inset-block-start:40px;inset-inline:24px;position:absolute;z-index:100}\n"] }]
|
|
741
|
+
}] });
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
745
|
+
* SPDX-License-Identifier: MIT
|
|
746
|
+
*/
|
|
747
|
+
class SiGridstackWrapperComponent {
|
|
748
|
+
/**
|
|
749
|
+
* Grid items to render inside the gridstack
|
|
750
|
+
*
|
|
751
|
+
* @defaultValue []
|
|
752
|
+
*/
|
|
753
|
+
widgetConfigs = input([]);
|
|
754
|
+
/**
|
|
755
|
+
* Whenever gridstack allows to drag, resize or delete the grid item
|
|
756
|
+
*
|
|
757
|
+
* @defaultValue false
|
|
758
|
+
*/
|
|
759
|
+
editable = input(false);
|
|
760
|
+
/**
|
|
761
|
+
* Module configuration
|
|
762
|
+
*/
|
|
763
|
+
gridConfig = input();
|
|
764
|
+
/**
|
|
765
|
+
* Emits dashboard grid events.
|
|
766
|
+
*/
|
|
767
|
+
gridEvent = output();
|
|
768
|
+
/**
|
|
769
|
+
* Emits the id of a widget instance that shall be removed.
|
|
770
|
+
*/
|
|
771
|
+
widgetInstanceRemove = output();
|
|
772
|
+
/**
|
|
773
|
+
* Emits the id of a widget instance that shall be edited.
|
|
774
|
+
*/
|
|
775
|
+
widgetInstanceEdit = output();
|
|
776
|
+
gridstackContainer = viewChild('gridstackContainer', { read: ViewContainerRef });
|
|
777
|
+
grid;
|
|
778
|
+
markedForRender = [];
|
|
779
|
+
gridItems = [];
|
|
780
|
+
itemIdAttr = 'item-id';
|
|
781
|
+
widgetIdSubscriptionMap = new Map();
|
|
782
|
+
ngZone = inject(NgZone);
|
|
783
|
+
elementRef = inject(ElementRef);
|
|
784
|
+
ngOnChanges(changes) {
|
|
785
|
+
if (changes.widgetConfigs) {
|
|
786
|
+
const { currentValue, previousValue, firstChange } = changes.widgetConfigs;
|
|
787
|
+
this.grid?.batchUpdate(true);
|
|
788
|
+
if (firstChange) {
|
|
789
|
+
this.markedForRender = currentValue;
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
// Get newly added items
|
|
793
|
+
const toBeAdded = currentValue.filter((item) => !previousValue.find((i) => i.id === item.id));
|
|
794
|
+
// Get deleted items
|
|
795
|
+
const toBeRemoved = previousValue.filter((item) => !currentValue.find((i) => i.id === item.id));
|
|
796
|
+
if (toBeAdded) {
|
|
797
|
+
this.mount(toBeAdded);
|
|
798
|
+
}
|
|
799
|
+
if (toBeRemoved) {
|
|
800
|
+
this.unmount(toBeRemoved);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
// Detect changes
|
|
804
|
+
this.updateViewComponents(currentValue);
|
|
805
|
+
this.updateLayout(currentValue);
|
|
806
|
+
this.grid?.batchUpdate(false);
|
|
807
|
+
}
|
|
808
|
+
if (changes.editable) {
|
|
809
|
+
const { currentValue, firstChange } = changes.editable;
|
|
810
|
+
if (!firstChange) {
|
|
811
|
+
this.grid.enableMove(currentValue);
|
|
812
|
+
this.grid.enableResize(currentValue);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
ngOnInit() {
|
|
817
|
+
const initialViewMode = {
|
|
818
|
+
disableDrag: !this.editable(),
|
|
819
|
+
disableResize: !this.editable()
|
|
820
|
+
};
|
|
821
|
+
const options = {
|
|
822
|
+
...DEFAULT_GRIDSTACK_OPTIONS,
|
|
823
|
+
...this.gridConfig()?.gridStackOptions,
|
|
824
|
+
...initialViewMode
|
|
825
|
+
};
|
|
826
|
+
this.grid = GridStack.init(options, this.elementRef.nativeElement.firstChild);
|
|
827
|
+
this.hookEvents(this.grid);
|
|
828
|
+
this.mount(this.markedForRender);
|
|
829
|
+
}
|
|
830
|
+
ngOnDestroy() {
|
|
831
|
+
this.widgetIdSubscriptionMap.forEach(subscriptions => {
|
|
832
|
+
subscriptions.forEach(subscription => subscription.unsubscribe());
|
|
833
|
+
});
|
|
834
|
+
this.widgetIdSubscriptionMap.clear();
|
|
835
|
+
}
|
|
836
|
+
mount(items) {
|
|
837
|
+
if (items.length > 0) {
|
|
838
|
+
items.forEach(item => {
|
|
839
|
+
this.addToView(item);
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
unmount(items) {
|
|
844
|
+
if (items.length > 0) {
|
|
845
|
+
items.forEach(item => {
|
|
846
|
+
this.removeFromView(item.id);
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
getLayout() {
|
|
851
|
+
const gridItems = this.grid?.getGridItems();
|
|
852
|
+
if (gridItems.length > 0) {
|
|
853
|
+
const positions = gridItems.map(gridItemHTMLElement => {
|
|
854
|
+
const id = gridItemHTMLElement.getAttribute('item-id');
|
|
855
|
+
const x = Number(gridItemHTMLElement.getAttribute('gs-x')) || 0;
|
|
856
|
+
const y = Number(gridItemHTMLElement.getAttribute('gs-y')) || 0;
|
|
857
|
+
const width = Number(gridItemHTMLElement.getAttribute('gs-w')) || 0;
|
|
858
|
+
const height = Number(gridItemHTMLElement.getAttribute('gs-h')) || 0;
|
|
859
|
+
return { id, x, y, width, height };
|
|
860
|
+
});
|
|
861
|
+
return positions;
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
return [];
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
updateLayout(widgets) {
|
|
868
|
+
const tmp = widgets.map(w => ({ ...w, w: w.width, h: w.height }));
|
|
869
|
+
if (this.grid) {
|
|
870
|
+
const gridItems = this.grid.getGridItems();
|
|
871
|
+
gridItems.forEach(gridItem => {
|
|
872
|
+
const config = tmp.find(widget => widget.id === gridItem.getAttribute('item-id'));
|
|
873
|
+
this.grid.update(gridItem, { ...config });
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
addToView(item) {
|
|
878
|
+
const componentRef = this.gridstackContainer().createComponent(SiWidgetHostComponent);
|
|
879
|
+
componentRef.setInput('widgetConfig', item);
|
|
880
|
+
const subscriptions = [];
|
|
881
|
+
const subscriptionRemove = componentRef.instance.remove.subscribe(widgetId => {
|
|
882
|
+
const widgetSubscriptions = this.widgetIdSubscriptionMap.get(widgetId);
|
|
883
|
+
if (widgetSubscriptions) {
|
|
884
|
+
widgetSubscriptions.forEach(sub => sub.unsubscribe());
|
|
885
|
+
}
|
|
886
|
+
this.widgetInstanceRemove.emit(widgetId);
|
|
887
|
+
});
|
|
888
|
+
subscriptions.push(subscriptionRemove);
|
|
889
|
+
const subscriptionEdit = componentRef.instance.edit.subscribe(widgetConfig => {
|
|
890
|
+
this.widgetInstanceEdit.emit(widgetConfig);
|
|
891
|
+
});
|
|
892
|
+
subscriptions.push(subscriptionEdit);
|
|
893
|
+
this.widgetIdSubscriptionMap.set(item.id, subscriptions);
|
|
894
|
+
const element = componentRef.location.nativeElement;
|
|
895
|
+
element.setAttribute(this.itemIdAttr, item.id);
|
|
896
|
+
this.gridItems.push({ id: item.id, component: componentRef });
|
|
897
|
+
this.grid.makeWidget(element, {
|
|
898
|
+
w: item.width,
|
|
899
|
+
h: item.height,
|
|
900
|
+
x: item.x,
|
|
901
|
+
y: item.y,
|
|
902
|
+
minW: item.minWidth,
|
|
903
|
+
minH: item.minHeight
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
removeFromView(widgetId) {
|
|
907
|
+
const gridItemElements = this.grid.getGridItems();
|
|
908
|
+
const toRemove = gridItemElements.find((el) => el.getAttribute(this.itemIdAttr) === widgetId);
|
|
909
|
+
if (toRemove) {
|
|
910
|
+
this.grid.removeWidget(toRemove);
|
|
911
|
+
const index = this.gridItems.findIndex(i => i.id === widgetId);
|
|
912
|
+
this.gridItems[index].component.destroy();
|
|
913
|
+
this.gridItems.splice(index, 1);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* GridItemComponents are created dynamically, change detection won't trigger as there is no \@Input binding.
|
|
918
|
+
* We have to update instance and run ChangeDetectionRef manually.
|
|
919
|
+
*/
|
|
920
|
+
updateViewComponents(newConfigs) {
|
|
921
|
+
// TODO: lookup by id would fasten the update
|
|
922
|
+
for (const config of newConfigs) {
|
|
923
|
+
const gridItem = this.gridItems.find(item => item.id === config.id);
|
|
924
|
+
if (gridItem) {
|
|
925
|
+
gridItem.component.setInput('widgetConfig', config);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
hookEvents(grid) {
|
|
930
|
+
grid.on('added removed dragstop resizestop disable enable dropped resize resizestart drag dragstart change', (event) => {
|
|
931
|
+
this.ngZone.run(() => {
|
|
932
|
+
this.gridEvent.emit({
|
|
933
|
+
event,
|
|
934
|
+
grid
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiGridstackWrapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
940
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.0.6", type: SiGridstackWrapperComponent, isStandalone: true, selector: "si-gridstack-wrapper", inputs: { widgetConfigs: { classPropertyName: "widgetConfigs", publicName: "widgetConfigs", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, gridConfig: { classPropertyName: "gridConfig", publicName: "gridConfig", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { gridEvent: "gridEvent", widgetInstanceRemove: "widgetInstanceRemove", widgetInstanceEdit: "widgetInstanceEdit" }, viewQueries: [{ propertyName: "gridstackContainer", first: true, predicate: ["gridstackContainer"], descendants: true, read: ViewContainerRef, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"grid-stack\">\n <ng-container #gridstackContainer />\n</div>\n", styles: ["::ng-deep .si-grid-stack-placeholder{background-color:var(--element-base-information);opacity:.3}:host{display:flex;flex:1 1 0;flex-direction:column;margin-inline-start:-8px;margin-inline-end:-8px;margin-block-start:-8px}.grid-stack{display:block}\n"] });
|
|
941
|
+
}
|
|
942
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiGridstackWrapperComponent, decorators: [{
|
|
943
|
+
type: Component,
|
|
944
|
+
args: [{ selector: 'si-gridstack-wrapper', template: "<div class=\"grid-stack\">\n <ng-container #gridstackContainer />\n</div>\n", styles: ["::ng-deep .si-grid-stack-placeholder{background-color:var(--element-base-information);opacity:.3}:host{display:flex;flex:1 1 0;flex-direction:column;margin-inline-start:-8px;margin-inline-end:-8px;margin-block-start:-8px}.grid-stack{display:block}\n"] }]
|
|
945
|
+
}] });
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
949
|
+
* SPDX-License-Identifier: MIT
|
|
950
|
+
*/
|
|
951
|
+
/**
|
|
952
|
+
* The Dialog component is utilized when editing a widget instance within a dashboard.
|
|
953
|
+
* It dynamically loads and creates the associated widget editor and incorporates it
|
|
954
|
+
* into its content. The dialog component is accountable for interacting with the dashboard
|
|
955
|
+
* and offers options for saving changes or terminating the editing process.
|
|
956
|
+
*/
|
|
957
|
+
class SiWidgetInstanceEditorDialogComponent {
|
|
958
|
+
/**
|
|
959
|
+
* Input for the widget instance configuration. It is used to populate the
|
|
960
|
+
* widget editor.
|
|
961
|
+
*/
|
|
962
|
+
widgetConfig = model.required();
|
|
963
|
+
/**
|
|
964
|
+
* Input for the widget definition. It is required to retrieve the component
|
|
965
|
+
* factory to instantiate the widget editor.
|
|
966
|
+
*/
|
|
967
|
+
widget = input.required();
|
|
968
|
+
/**
|
|
969
|
+
* Emits the edited widget instance configuration if the user confirms by
|
|
970
|
+
* saving, or `undefined` if the user cancels the dialog.
|
|
971
|
+
*/
|
|
972
|
+
closed = output();
|
|
973
|
+
/**
|
|
974
|
+
* Emits when the editor instantiation is completed.
|
|
975
|
+
*/
|
|
976
|
+
editorSetupCompleted = output();
|
|
977
|
+
editorHost = viewChild.required('editorHost', { read: ViewContainerRef });
|
|
978
|
+
/** Indicates if the current config is valid or not. If invalid, the save button will be disabled. */
|
|
979
|
+
invalidConfig = signal(false);
|
|
980
|
+
showNextButton = computed(() => this.editorWizardState() !== undefined ? true : false);
|
|
981
|
+
disableNextButton = computed(() => {
|
|
982
|
+
const wizardState = this.editorWizardState();
|
|
983
|
+
if (!wizardState) {
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
else if (!wizardState.hasNext) {
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
else if (wizardState.disableNext !== undefined) {
|
|
990
|
+
return wizardState.disableNext;
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
showPreviousButton = computed(() => !!this.editorWizardState());
|
|
997
|
+
disablePreviousButton = computed(() => {
|
|
998
|
+
const wizardState = this.editorWizardState();
|
|
999
|
+
return wizardState ? !wizardState.hasPrevious : true;
|
|
1000
|
+
});
|
|
1001
|
+
labelSave = t(() => $localize `:@@DASHBOARD.WIDGET_EDITOR_DIALOG.SAVE:Save`);
|
|
1002
|
+
labelCancel = t(() => $localize `:@@DASHBOARD.WIDGET_EDITOR_DIALOG.CANCEL:Cancel`);
|
|
1003
|
+
labelPrevious = t(() => $localize `:@@DASHBOARD.WIDGET_EDITOR_DIALOG.PREVIOUS:Previous`);
|
|
1004
|
+
labelNext = t(() => $localize `:@@DASHBOARD.WIDGET_EDITOR_DIALOG.NEXT:Next`);
|
|
1005
|
+
labelDialogMessage = t(() => $localize `:@@DASHBOARD.WIDGET_EDITOR_DIALOG.DISCARD_CONFIG_CHANGE_DIALOG.MESSAGE:The widget configuration changed. Do you want to discard the changes?`);
|
|
1006
|
+
labelDialogHeading = t(() => $localize `:@@DASHBOARD.WIDGET_EDITOR_DIALOG.DISCARD_CONFIG_CHANGE_DIALOG.HEADING:Widget configuration changed`);
|
|
1007
|
+
labelDialogSave = t(() => $localize `:@@DASHBOARD.WIDGET_EDITOR_DIALOG.DISCARD_CONFIG_CHANGE_DIALOG.SAVE:Save`);
|
|
1008
|
+
labelDialogDiscard = t(() => $localize `:@@DASHBOARD.WIDGET_EDITOR_DIALOG.DISCARD_CONFIG_CHANGE_DIALOG.DISCARD:Discard`);
|
|
1009
|
+
labelDialogCancel = t(() => $localize `:@@DASHBOARD.WIDGET_EDITOR_DIALOG.DISCARD_CONFIG_CHANGE_DIALOG.CANCEL:Cancel`);
|
|
1010
|
+
/**
|
|
1011
|
+
* Marks the widget configuration as modified. Is set when widget editor instance
|
|
1012
|
+
* emits configChange events. Triggers edit discard confirmation dialog when widget config
|
|
1013
|
+
* is modified but not dialog is canceled.
|
|
1014
|
+
* */
|
|
1015
|
+
widgetConfigModified = signal(false);
|
|
1016
|
+
widgetInstanceEditor;
|
|
1017
|
+
editorWizardState = signal(undefined);
|
|
1018
|
+
subscriptions = [];
|
|
1019
|
+
injector = inject(Injector);
|
|
1020
|
+
envInjector = inject(EnvironmentInjector);
|
|
1021
|
+
dialogService = inject(SiActionDialogService);
|
|
1022
|
+
ngOnInit() {
|
|
1023
|
+
setupWidgetEditor(this.widget().componentFactory, this.editorHost(), this.injector, this.envInjector).subscribe(componentRef => {
|
|
1024
|
+
this.widgetInstanceEditor = componentRef.instance;
|
|
1025
|
+
if (isSignal(this.widgetInstanceEditor.config)) {
|
|
1026
|
+
componentRef.setInput('config', this.widgetConfig());
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
this.widgetInstanceEditor.config = this.widgetConfig();
|
|
1030
|
+
}
|
|
1031
|
+
// To be used by webcomponent wrapper
|
|
1032
|
+
if ('statusChangesHandler' in this.widgetInstanceEditor) {
|
|
1033
|
+
this.widgetInstanceEditor.statusChangesHandler = this.handleStatusChanges.bind(this);
|
|
1034
|
+
}
|
|
1035
|
+
let hasStatusChangesEmitter = false;
|
|
1036
|
+
if (this.widgetInstanceEditor.statusChanges) {
|
|
1037
|
+
hasStatusChangesEmitter = true;
|
|
1038
|
+
this.subscriptions.push(this.widgetInstanceEditor.statusChanges.subscribe(statusChanges => {
|
|
1039
|
+
this.handleStatusChanges(statusChanges);
|
|
1040
|
+
}));
|
|
1041
|
+
}
|
|
1042
|
+
if (this.widgetInstanceEditor.configChange) {
|
|
1043
|
+
this.subscriptions.push(this.widgetInstanceEditor.configChange.subscribe(config => {
|
|
1044
|
+
if (!hasStatusChangesEmitter) {
|
|
1045
|
+
this.invalidConfig.set(!!config.invalid);
|
|
1046
|
+
this.widgetConfigModified.set(true);
|
|
1047
|
+
}
|
|
1048
|
+
}));
|
|
1049
|
+
}
|
|
1050
|
+
if (this.isEditorWizdard(this.widgetInstanceEditor)) {
|
|
1051
|
+
this.editorWizardState.set(this.widgetInstanceEditor.state);
|
|
1052
|
+
if (this.widgetInstanceEditor.stateChange) {
|
|
1053
|
+
this.subscriptions.push(this.widgetInstanceEditor.stateChange.subscribe(state => {
|
|
1054
|
+
this.editorWizardState.set(state);
|
|
1055
|
+
}));
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
this.editorSetupCompleted.emit();
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
ngOnDestroy() {
|
|
1062
|
+
this.tearDownEditor();
|
|
1063
|
+
}
|
|
1064
|
+
onCancel() {
|
|
1065
|
+
if (!this.widgetConfigModified()) {
|
|
1066
|
+
this.closed.emit(undefined);
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
this.dialogService
|
|
1070
|
+
.showActionDialog({
|
|
1071
|
+
type: 'edit-discard',
|
|
1072
|
+
disableSave: this.invalidConfig(),
|
|
1073
|
+
message: this.labelDialogMessage,
|
|
1074
|
+
heading: this.labelDialogHeading,
|
|
1075
|
+
saveBtnName: this.labelDialogSave,
|
|
1076
|
+
discardBtnName: this.labelDialogDiscard,
|
|
1077
|
+
cancelBtnName: this.labelDialogCancel,
|
|
1078
|
+
disableSaveMessage: this.labelDialogMessage,
|
|
1079
|
+
disableSaveDiscardBtnName: this.labelDialogDiscard
|
|
1080
|
+
})
|
|
1081
|
+
.subscribe(result => {
|
|
1082
|
+
if (result === 'discard') {
|
|
1083
|
+
this.closed.emit(undefined);
|
|
1084
|
+
}
|
|
1085
|
+
else if (result === 'save') {
|
|
1086
|
+
this.closed.emit(this.widgetConfig());
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
onNext() {
|
|
1092
|
+
if (this.isEditorWizdard(this.widgetInstanceEditor)) {
|
|
1093
|
+
this.widgetInstanceEditor.next();
|
|
1094
|
+
this.editorWizardState.set(this.widgetInstanceEditor.state);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
onPrevious() {
|
|
1098
|
+
if (this.isEditorWizdard(this.widgetInstanceEditor) && this.editorWizardState()?.hasPrevious) {
|
|
1099
|
+
this.widgetInstanceEditor.previous();
|
|
1100
|
+
this.editorWizardState.set(this.widgetInstanceEditor.state);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
onSave() {
|
|
1104
|
+
// In case the widget instance editor did not only change values of the config
|
|
1105
|
+
// object, but set a new config object reference, we need to grab it and replace
|
|
1106
|
+
// our local config object.
|
|
1107
|
+
if (this.widgetInstanceEditor?.config) {
|
|
1108
|
+
if (isSignal(this.widgetInstanceEditor.config)) {
|
|
1109
|
+
this.widgetConfig.set(this.widgetInstanceEditor?.config());
|
|
1110
|
+
}
|
|
1111
|
+
else {
|
|
1112
|
+
this.widgetConfig.set(this.widgetInstanceEditor?.config);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
this.closed.emit(this.widgetConfig());
|
|
1116
|
+
}
|
|
1117
|
+
tearDownEditor() {
|
|
1118
|
+
this.editorWizardState.set(undefined);
|
|
1119
|
+
this.widgetInstanceEditor = undefined;
|
|
1120
|
+
this.subscriptions.forEach(s => s.unsubscribe());
|
|
1121
|
+
this.subscriptions = [];
|
|
1122
|
+
}
|
|
1123
|
+
isEditorWizdard(editor) {
|
|
1124
|
+
return !!editor && 'state' in editor;
|
|
1125
|
+
}
|
|
1126
|
+
handleStatusChanges(statusChanges) {
|
|
1127
|
+
if (statusChanges.invalid !== undefined) {
|
|
1128
|
+
this.invalidConfig.set(statusChanges.invalid);
|
|
1129
|
+
}
|
|
1130
|
+
if (statusChanges.modified !== undefined) {
|
|
1131
|
+
this.widgetConfigModified.set(statusChanges.modified);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiWidgetInstanceEditorDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1135
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: SiWidgetInstanceEditorDialogComponent, isStandalone: true, selector: "si-widget-instance-editor-dialog", inputs: { widgetConfig: { classPropertyName: "widgetConfig", publicName: "widgetConfig", isSignal: true, isRequired: true, transformFunction: null }, widget: { classPropertyName: "widget", publicName: "widget", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { widgetConfig: "widgetConfigChange", closed: "closed", editorSetupCompleted: "editorSetupCompleted" }, viewQueries: [{ propertyName: "editorHost", first: true, predicate: ["editorHost"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "<div class=\"si-layout-fixed-height mb-8 mx-0 overflow-auto px-2 pt-2\">\n <ng-container #editorHost />\n</div>\n\n<div class=\"editor-footer px-2\">\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"onCancel()\">{{\n labelCancel | translate\n }}</button>\n\n @if (showPreviousButton()) {\n <button\n type=\"button\"\n class=\"btn btn-secondary\"\n [disabled]=\"disablePreviousButton()\"\n (click)=\"onPrevious()\"\n >{{ labelPrevious | translate }}</button\n >\n }\n @if (showNextButton()) {\n <button\n type=\"button\"\n class=\"btn btn-secondary\"\n [disabled]=\"disableNextButton()\"\n (click)=\"onNext()\"\n >{{ labelNext | translate }}</button\n >\n }\n\n <button type=\"button\" class=\"btn btn-primary\" [disabled]=\"invalidConfig()\" (click)=\"onSave()\">{{\n labelSave | translate\n }}</button>\n</div>\n", styles: [":host{display:flex;flex:1 1 0;flex-direction:column;margin-inline:-4px;margin-block-start:-4px}.editor-footer{align-items:center;display:flex;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end}.editor-footer>*{margin-inline-start:16px}\n"], dependencies: [{ kind: "ngmodule", type: SiTranslateModule }, { kind: "pipe", type: i1.SiTranslatePipe, name: "translate" }] });
|
|
1136
|
+
}
|
|
1137
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiWidgetInstanceEditorDialogComponent, decorators: [{
|
|
1138
|
+
type: Component,
|
|
1139
|
+
args: [{ selector: 'si-widget-instance-editor-dialog', imports: [SiTranslateModule], template: "<div class=\"si-layout-fixed-height mb-8 mx-0 overflow-auto px-2 pt-2\">\n <ng-container #editorHost />\n</div>\n\n<div class=\"editor-footer px-2\">\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"onCancel()\">{{\n labelCancel | translate\n }}</button>\n\n @if (showPreviousButton()) {\n <button\n type=\"button\"\n class=\"btn btn-secondary\"\n [disabled]=\"disablePreviousButton()\"\n (click)=\"onPrevious()\"\n >{{ labelPrevious | translate }}</button\n >\n }\n @if (showNextButton()) {\n <button\n type=\"button\"\n class=\"btn btn-secondary\"\n [disabled]=\"disableNextButton()\"\n (click)=\"onNext()\"\n >{{ labelNext | translate }}</button\n >\n }\n\n <button type=\"button\" class=\"btn btn-primary\" [disabled]=\"invalidConfig()\" (click)=\"onSave()\">{{\n labelSave | translate\n }}</button>\n</div>\n", styles: [":host{display:flex;flex:1 1 0;flex-direction:column;margin-inline:-4px;margin-block-start:-4px}.editor-footer{align-items:center;display:flex;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end}.editor-footer>*{margin-inline-start:16px}\n"] }]
|
|
1140
|
+
}] });
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
1144
|
+
* SPDX-License-Identifier: MIT
|
|
1145
|
+
*/
|
|
1146
|
+
const NEW_WIDGET_PREFIX = 'new-widget-';
|
|
1147
|
+
let idCounter = 1;
|
|
1148
|
+
/**
|
|
1149
|
+
* The grid component is the actual component on which the widget instances are placed and visualized. You can think of
|
|
1150
|
+
* a headless dashboard, without a title, toolbar or edit buttons.
|
|
1151
|
+
*/
|
|
1152
|
+
class SiGridComponent {
|
|
1153
|
+
storeSubscription;
|
|
1154
|
+
gridService = inject(SiGridService);
|
|
1155
|
+
modalService = inject(SiModalService);
|
|
1156
|
+
widgetStorage = inject(SI_WIDGET_STORE);
|
|
1157
|
+
/**
|
|
1158
|
+
* Configuration options for a grid instance. Default is the optional
|
|
1159
|
+
* value from the {@link SI_DASHBOARD_CONFIGURATION}.
|
|
1160
|
+
*
|
|
1161
|
+
* @defaultValue inject(SI_DASHBOARD_CONFIGURATION)?.grid
|
|
1162
|
+
*/
|
|
1163
|
+
gridConfig = input(inject(SI_DASHBOARD_CONFIGURATION)?.grid);
|
|
1164
|
+
/**
|
|
1165
|
+
* Sets the grid into editable mode, in which the widget instances can be moved,
|
|
1166
|
+
* resized, removed or new ones added.
|
|
1167
|
+
*
|
|
1168
|
+
* @defaultValue false
|
|
1169
|
+
*/
|
|
1170
|
+
editable = model(false);
|
|
1171
|
+
/**
|
|
1172
|
+
* This is the internal owner of the current editable state and is used to track if
|
|
1173
|
+
* editable or not. Not editable can be changed by either calling the `edit()` api
|
|
1174
|
+
* method or by setting the `editable` input. When setting the input, the `ngOnChanges(...)`
|
|
1175
|
+
* hook is used to call the `edit()` method. Similar, to get from editable to not editable,
|
|
1176
|
+
* `cancel()` or `save()` is used and can be triggered from `ngOnChanges(...)`.
|
|
1177
|
+
*/
|
|
1178
|
+
editableInternal = false;
|
|
1179
|
+
/**
|
|
1180
|
+
* An optional, but recommended dashboard id that is used in persistence and passed
|
|
1181
|
+
* to the widget store for saving and loading data.
|
|
1182
|
+
*/
|
|
1183
|
+
dashboardId = input();
|
|
1184
|
+
/**
|
|
1185
|
+
* Provides the available widgets to the grid. When loading the widget configurations from
|
|
1186
|
+
* the storage, we need to have the widget definition available to be able to create the widget
|
|
1187
|
+
* instances on the grid.
|
|
1188
|
+
*
|
|
1189
|
+
* @defaultValue [] */
|
|
1190
|
+
widgetCatalog = input([]);
|
|
1191
|
+
/**
|
|
1192
|
+
* When the user clicks edit on a widget instance, an editor need to appear and the
|
|
1193
|
+
* widget editor component need to be loaded. When the grid is used standalone, it
|
|
1194
|
+
* takes care and opens a modal dialog and loads the configured widget editor component.
|
|
1195
|
+
* When the grid is used in a container like the flexible dashboard, the container manages
|
|
1196
|
+
* where the widget instance editor is displayed. In this case this options prevents the grid
|
|
1197
|
+
* from showing the editor in the dialog, and emits on `widgetInstanceEdit` on clicking the
|
|
1198
|
+
* widget `edit` action.
|
|
1199
|
+
*
|
|
1200
|
+
* @defaultValue false */
|
|
1201
|
+
emitWidgetInstanceEditEvents = input(false);
|
|
1202
|
+
/**
|
|
1203
|
+
* Option to turn off the loading spinner on save and load operations.
|
|
1204
|
+
*
|
|
1205
|
+
* @defaultValue false
|
|
1206
|
+
*/
|
|
1207
|
+
hideProgressIndicator = input(false);
|
|
1208
|
+
/**
|
|
1209
|
+
* Option to configure a custom widget instance editor dialog component. The component provides the
|
|
1210
|
+
* editor hosting and the buttons to save and cancel.
|
|
1211
|
+
*/
|
|
1212
|
+
widgetInstanceEditorDialogComponent = input();
|
|
1213
|
+
/**
|
|
1214
|
+
* Emits the modification state of the grid. It is `unmodified` when the visible state
|
|
1215
|
+
* is equal to the loaded state from the widget storage. When the user modifies the dashboard by
|
|
1216
|
+
* e.g. while moving the widgets, the dashboard is marked as `modified` and emits `true` and when the user
|
|
1217
|
+
* persists the change by saving, or reverts the state by canceling, the state is `unmodified`
|
|
1218
|
+
* again and emits `false`.
|
|
1219
|
+
*/
|
|
1220
|
+
isModified = output();
|
|
1221
|
+
/**
|
|
1222
|
+
* Modified state that is emitted in the `isModified` output. Should only be
|
|
1223
|
+
changed using the {@link setModified} method.
|
|
1224
|
+
*/
|
|
1225
|
+
modified = false;
|
|
1226
|
+
/**
|
|
1227
|
+
* Emits to notify about edit events of widget instances. Only emits
|
|
1228
|
+
* if `emitWidgetInstanceEditEvents` is set to `true`.
|
|
1229
|
+
*/
|
|
1230
|
+
widgetInstanceEdit = output();
|
|
1231
|
+
/**
|
|
1232
|
+
* All widget configuration objects of the visible widget instances.
|
|
1233
|
+
*
|
|
1234
|
+
* @internal
|
|
1235
|
+
*/
|
|
1236
|
+
visibleWidgetInstances$ = new BehaviorSubject([]);
|
|
1237
|
+
/**
|
|
1238
|
+
* All widget instance configs that are on the grid by loading from the widget
|
|
1239
|
+
* storage. Thus, these widget are persisted.
|
|
1240
|
+
*
|
|
1241
|
+
* @defaultValue []
|
|
1242
|
+
* @internal
|
|
1243
|
+
*/
|
|
1244
|
+
persistedWidgetInstances = [];
|
|
1245
|
+
/**
|
|
1246
|
+
* Widget instance configs that are added to the grid in edit mode, but not
|
|
1247
|
+
* persisted yet, as the user did not confirm the modification by save.
|
|
1248
|
+
*
|
|
1249
|
+
* @defaultValue []
|
|
1250
|
+
* @internal
|
|
1251
|
+
*/
|
|
1252
|
+
transientWidgetInstances = [];
|
|
1253
|
+
/**
|
|
1254
|
+
* All widget instance configurations that are removed from the grid in edit mode.
|
|
1255
|
+
* These instances are persistently removed on save or re-added again on cancel.
|
|
1256
|
+
*
|
|
1257
|
+
* @defaultValue []
|
|
1258
|
+
* @internal
|
|
1259
|
+
*/
|
|
1260
|
+
markedForRemoval = [];
|
|
1261
|
+
/** @defaultValue viewChild.required(SiGridstackWrapperComponent) */
|
|
1262
|
+
gridStackWrapper = viewChild.required(SiGridstackWrapperComponent);
|
|
1263
|
+
/**
|
|
1264
|
+
* Service used to indicate load and save indication.
|
|
1265
|
+
* @deprecated Use `isLoading` instead.
|
|
1266
|
+
*
|
|
1267
|
+
* @defaultValue inject(SiLoadingService)
|
|
1268
|
+
*/
|
|
1269
|
+
loadingService = inject(SiLoadingService);
|
|
1270
|
+
/**
|
|
1271
|
+
* Indication for load and save operations.
|
|
1272
|
+
*/
|
|
1273
|
+
isLoading = new BehaviorSubject(false);
|
|
1274
|
+
initialLoad = true;
|
|
1275
|
+
get showEmptyState() {
|
|
1276
|
+
return !this.initialLoad && this.visibleWidgetInstances$.value.length === 0;
|
|
1277
|
+
}
|
|
1278
|
+
ngOnChanges(changes) {
|
|
1279
|
+
// Reload widgets if the dashboardId changes. Do not load on inital
|
|
1280
|
+
// dashboardId property binding as first load will be done in ngOnInit()
|
|
1281
|
+
if (changes.dashboardId && !changes.dashboardId.firstChange) {
|
|
1282
|
+
queueMicrotask(() => this.loadAndSubscribeWidgets());
|
|
1283
|
+
}
|
|
1284
|
+
if (changes.editable) {
|
|
1285
|
+
if (changes.editable.currentValue) {
|
|
1286
|
+
this.edit();
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
this.cancel();
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
if (changes.widgetCatalog) {
|
|
1293
|
+
this.gridService.widgetCatalog = this.widgetCatalog();
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
ngOnInit() {
|
|
1297
|
+
queueMicrotask(() => this.loadAndSubscribeWidgets());
|
|
1298
|
+
this.gridService.widgetCatalog = this.widgetCatalog();
|
|
1299
|
+
}
|
|
1300
|
+
ngOnDestroy() {
|
|
1301
|
+
this.storeSubscription?.unsubscribe();
|
|
1302
|
+
this.isLoading.complete();
|
|
1303
|
+
this.loadingService.counter.complete();
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Set dashboard grid in editable mode to modify widget instances.
|
|
1307
|
+
*/
|
|
1308
|
+
edit() {
|
|
1309
|
+
if (!this.editableInternal) {
|
|
1310
|
+
this.transientWidgetInstances = [];
|
|
1311
|
+
this.markedForRemoval = [];
|
|
1312
|
+
this.setModified(false);
|
|
1313
|
+
this.editableInternal = true;
|
|
1314
|
+
this.editable.set(true);
|
|
1315
|
+
this.gridService.editable$.next(this.editableInternal);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Persists the current widget instances to the widget storage and
|
|
1320
|
+
* changes the editable and isModified modes.
|
|
1321
|
+
*/
|
|
1322
|
+
save() {
|
|
1323
|
+
if (!this.editableInternal) {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
this.isLoading.next(true);
|
|
1327
|
+
this.loadingService.counter.next(1);
|
|
1328
|
+
// Update position information and remove temporary ids
|
|
1329
|
+
const widgets = this.updateWidgetPositions(this.visibleWidgetInstances$.value).map(widget => widget.id.startsWith(NEW_WIDGET_PREFIX) ? { ...widget, id: undefined } : widget);
|
|
1330
|
+
const toRemove = this.markedForRemoval.filter(widget => !widget.id.startsWith(NEW_WIDGET_PREFIX));
|
|
1331
|
+
this.widgetStorage
|
|
1332
|
+
.save(widgets, toRemove, this.dashboardId())
|
|
1333
|
+
.pipe(take(1))
|
|
1334
|
+
.subscribe({
|
|
1335
|
+
next: (value) => {
|
|
1336
|
+
this.setModified(false);
|
|
1337
|
+
this.editableInternal = false;
|
|
1338
|
+
this.editable.set(false);
|
|
1339
|
+
this.gridService.editable$.next(this.editableInternal);
|
|
1340
|
+
this.isLoading.next(false);
|
|
1341
|
+
this.loadingService.counter.next(0);
|
|
1342
|
+
},
|
|
1343
|
+
error: (err) => {
|
|
1344
|
+
console.error('Saving dashboard configuration failed.', err);
|
|
1345
|
+
this.isLoading.next(false);
|
|
1346
|
+
this.loadingService.counter.next(0);
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Cancel current changes and restore last saved state.
|
|
1352
|
+
*/
|
|
1353
|
+
cancel() {
|
|
1354
|
+
if (!this.editableInternal) {
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
if (this.modified) {
|
|
1358
|
+
this.visibleWidgetInstances$.next([...this.persistedWidgetInstances]);
|
|
1359
|
+
this.setModified(false);
|
|
1360
|
+
}
|
|
1361
|
+
this.editableInternal = false;
|
|
1362
|
+
if (this.editable()) {
|
|
1363
|
+
this.editable.set(false);
|
|
1364
|
+
}
|
|
1365
|
+
this.gridService.editable$.next(this.editableInternal);
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Adds a new widget instance configuration to the dashboard grid. It is not
|
|
1369
|
+
* persisted yet and is added to the transient widget instances.
|
|
1370
|
+
*
|
|
1371
|
+
* @param widgetInstanceConfig - The new widget configuration.
|
|
1372
|
+
*/
|
|
1373
|
+
addWidgetInstance(widgetInstanceConfig) {
|
|
1374
|
+
const id = `${NEW_WIDGET_PREFIX}${idCounter++}`;
|
|
1375
|
+
const newWidget = { ...widgetInstanceConfig, id };
|
|
1376
|
+
const nextWidgets = this.updateWidgetPositions([
|
|
1377
|
+
...this.visibleWidgetInstances$.value,
|
|
1378
|
+
newWidget
|
|
1379
|
+
]);
|
|
1380
|
+
this.transientWidgetInstances.push(newWidget);
|
|
1381
|
+
this.visibleWidgetInstances$.next(nextWidgets);
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Removes a widget instance from the visible widgets and puts it in the
|
|
1385
|
+
* {@link markedForRemoval} array.
|
|
1386
|
+
*
|
|
1387
|
+
* @param widgetInstanceId - The id of the widget instance to be removed.
|
|
1388
|
+
*/
|
|
1389
|
+
removeWidgetInstance(widgetInstanceId) {
|
|
1390
|
+
const widgetToRemove = this.visibleWidgetInstances$.value.find(widget => widget.id === widgetInstanceId);
|
|
1391
|
+
if (widgetToRemove) {
|
|
1392
|
+
this.markedForRemoval.push(widgetToRemove);
|
|
1393
|
+
let nextWidgets = this.visibleWidgetInstances$.value.filter(widget => widget.id !== widgetInstanceId);
|
|
1394
|
+
nextWidgets = this.updateWidgetPositions(nextWidgets);
|
|
1395
|
+
this.visibleWidgetInstances$.next(nextWidgets);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Opens the provided widget configuration in the related editor or
|
|
1400
|
+
* emits on {@link widgetInstanceEdit}, if {@link emitWidgetInstanceEditEvents}
|
|
1401
|
+
* is true.
|
|
1402
|
+
*
|
|
1403
|
+
* @param widgetInstanceConfig - The config of the widget instance to edit.
|
|
1404
|
+
*/
|
|
1405
|
+
editWidgetInstance(widgetInstanceConfig) {
|
|
1406
|
+
// Need to edit a clone to avoid runtime editing
|
|
1407
|
+
const widgetConfigClone = JSON.parse(JSON.stringify(widgetInstanceConfig));
|
|
1408
|
+
if (this.emitWidgetInstanceEditEvents()) {
|
|
1409
|
+
this.widgetInstanceEdit.emit(widgetConfigClone);
|
|
1410
|
+
}
|
|
1411
|
+
else {
|
|
1412
|
+
const widget = this.gridService.getWidget(widgetConfigClone.widgetId);
|
|
1413
|
+
const config = {
|
|
1414
|
+
animated: true,
|
|
1415
|
+
keyboard: true,
|
|
1416
|
+
inputValues: {
|
|
1417
|
+
widgetConfig: widgetConfigClone,
|
|
1418
|
+
widget
|
|
1419
|
+
},
|
|
1420
|
+
class: widget?.componentFactory.editorModalClass ?? 'modal-xl'
|
|
1421
|
+
};
|
|
1422
|
+
const widgetInstanceEditorDialogComponent = this.widgetInstanceEditorDialogComponent();
|
|
1423
|
+
const componentType = widgetInstanceEditorDialogComponent ?? SiWidgetInstanceEditorDialogComponent;
|
|
1424
|
+
const modalRef = this.modalService.show(componentType, config);
|
|
1425
|
+
const subscription = modalRef.content.closed.subscribe(editedWidgetConfig => {
|
|
1426
|
+
subscription.unsubscribe();
|
|
1427
|
+
modalRef.hide();
|
|
1428
|
+
if (editedWidgetConfig) {
|
|
1429
|
+
this.updateWidgetInstance(editedWidgetConfig);
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Updates the visible widgets with an updated configuration. Will update the
|
|
1436
|
+
* user interface and emit on {@link isModified}.
|
|
1437
|
+
*
|
|
1438
|
+
* @param editedWidgetConfig - The config of the widget instance that was updated.
|
|
1439
|
+
*/
|
|
1440
|
+
updateWidgetInstance(editedWidgetConfig) {
|
|
1441
|
+
const index = this.visibleWidgetInstances$.value.findIndex(wc => wc.id === editedWidgetConfig.id);
|
|
1442
|
+
if (index >= 0) {
|
|
1443
|
+
let nextWidgets = this.updateWidgetPositions([...this.visibleWidgetInstances$.value]);
|
|
1444
|
+
nextWidgets[index] = editedWidgetConfig;
|
|
1445
|
+
nextWidgets = this.updateWidgetPositions(nextWidgets);
|
|
1446
|
+
this.visibleWidgetInstances$.next(nextWidgets);
|
|
1447
|
+
setTimeout(() => this.setModified(true), 0);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
handleGridEvent(event) {
|
|
1451
|
+
const relevantEventTypes = ['added', 'removed', 'dragstop', 'resizestop'];
|
|
1452
|
+
if (this.editable() && relevantEventTypes.includes(event.event.type)) {
|
|
1453
|
+
// Make sure the widget config always holds the latest position information
|
|
1454
|
+
const widgets = this.updateWidgetPositions(this.visibleWidgetInstances$.value);
|
|
1455
|
+
this.visibleWidgetInstances$.next(widgets);
|
|
1456
|
+
setTimeout(() => this.setModified(true), 0);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
loadAndSubscribeWidgets() {
|
|
1460
|
+
this.storeSubscription?.unsubscribe();
|
|
1461
|
+
// remove existing widgets
|
|
1462
|
+
this.visibleWidgetInstances$.next([]);
|
|
1463
|
+
// Only when we change the dashboard id, this method is invoked
|
|
1464
|
+
// directly. In this case we start the progress here and we stop
|
|
1465
|
+
// it in the `next` subscription. The subscription stays hot and
|
|
1466
|
+
// also get called when save() is invoked. In this case, we do not
|
|
1467
|
+
// want to decrease the progress counter as we do it in the save
|
|
1468
|
+
// subscription. To handle this, we use the boolean marker `initialLoad`.
|
|
1469
|
+
this.initialLoad = true;
|
|
1470
|
+
this.isLoading.next(true);
|
|
1471
|
+
this.loadingService.counter.next(1);
|
|
1472
|
+
this.storeSubscription = this.widgetStorage.load(this.dashboardId()).subscribe({
|
|
1473
|
+
next: widgets => {
|
|
1474
|
+
this.visibleWidgetInstances$.next(widgets);
|
|
1475
|
+
this.persistedWidgetInstances = widgets;
|
|
1476
|
+
if (this.initialLoad) {
|
|
1477
|
+
this.initialLoad = false;
|
|
1478
|
+
this.isLoading.next(false);
|
|
1479
|
+
this.loadingService.counter.next(0);
|
|
1480
|
+
}
|
|
1481
|
+
},
|
|
1482
|
+
error: err => {
|
|
1483
|
+
console.error('Loading dashboard configuration failed', err);
|
|
1484
|
+
this.isLoading.next(false);
|
|
1485
|
+
this.loadingService.counter.next(0);
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
updateWidgetPositions(widgetConfigs) {
|
|
1490
|
+
const layout = this.gridStackWrapper().getLayout();
|
|
1491
|
+
const widgets = widgetConfigs.map(widget => {
|
|
1492
|
+
const position = layout.find(p => p.id === widget.id);
|
|
1493
|
+
if (position) {
|
|
1494
|
+
return {
|
|
1495
|
+
...widget,
|
|
1496
|
+
x: position.x,
|
|
1497
|
+
y: position.y,
|
|
1498
|
+
width: position.width,
|
|
1499
|
+
height: position.height
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
else {
|
|
1503
|
+
return widget;
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
return widgets || [];
|
|
1507
|
+
}
|
|
1508
|
+
setModified(modified) {
|
|
1509
|
+
if (this.modified !== modified) {
|
|
1510
|
+
this.modified = modified;
|
|
1511
|
+
this.isModified.emit(this.modified);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1515
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: SiGridComponent, isStandalone: true, selector: "si-grid", inputs: { gridConfig: { classPropertyName: "gridConfig", publicName: "gridConfig", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, dashboardId: { classPropertyName: "dashboardId", publicName: "dashboardId", isSignal: true, isRequired: false, transformFunction: null }, widgetCatalog: { classPropertyName: "widgetCatalog", publicName: "widgetCatalog", isSignal: true, isRequired: false, transformFunction: null }, emitWidgetInstanceEditEvents: { classPropertyName: "emitWidgetInstanceEditEvents", publicName: "emitWidgetInstanceEditEvents", isSignal: true, isRequired: false, transformFunction: null }, hideProgressIndicator: { classPropertyName: "hideProgressIndicator", publicName: "hideProgressIndicator", isSignal: true, isRequired: false, transformFunction: null }, widgetInstanceEditorDialogComponent: { classPropertyName: "widgetInstanceEditorDialogComponent", publicName: "widgetInstanceEditorDialogComponent", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { editable: "editableChange", isModified: "isModified", widgetInstanceEdit: "widgetInstanceEdit" }, providers: [SiGridService, SiLoadingService], viewQueries: [{ propertyName: "gridStackWrapper", first: true, predicate: SiGridstackWrapperComponent, descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "@if (showEmptyState) {\n <ng-content select=\"[empty-state]\" />\n}\n<si-gridstack-wrapper\n [widgetConfigs]=\"(visibleWidgetInstances$ | async) ?? []\"\n [gridConfig]=\"gridConfig()\"\n [editable]=\"editable()\"\n [siLoading]=\"!hideProgressIndicator() && initialLoad && (isLoading | async)!\"\n [blocking]=\"false\"\n (gridEvent)=\"handleGridEvent($event)\"\n (widgetInstanceRemove)=\"removeWidgetInstance($event)\"\n (widgetInstanceEdit)=\"editWidgetInstance($event)\"\n/>\n", styles: [":host{display:flex;flex:1 1 0;flex-direction:column}::ng-deep .grid-stack-animate,::ng-deep .grid-stack-animate .grid-stack-item{transition-property:inset-inline-start,inset-block-start}\n"], dependencies: [{ kind: "component", type: SiGridstackWrapperComponent, selector: "si-gridstack-wrapper", inputs: ["widgetConfigs", "editable", "gridConfig"], outputs: ["gridEvent", "widgetInstanceRemove", "widgetInstanceEdit"] }, { kind: "directive", type: SiLoadingSpinnerDirective, selector: "[siLoading]", inputs: ["siLoading", "blocking", "initialDelay"] }, { kind: "pipe", type: AsyncPipe, name: "async" }] });
|
|
1516
|
+
}
|
|
1517
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiGridComponent, decorators: [{
|
|
1518
|
+
type: Component,
|
|
1519
|
+
args: [{ selector: 'si-grid', imports: [SiGridstackWrapperComponent, SiLoadingSpinnerDirective, AsyncPipe], providers: [SiGridService, SiLoadingService], template: "@if (showEmptyState) {\n <ng-content select=\"[empty-state]\" />\n}\n<si-gridstack-wrapper\n [widgetConfigs]=\"(visibleWidgetInstances$ | async) ?? []\"\n [gridConfig]=\"gridConfig()\"\n [editable]=\"editable()\"\n [siLoading]=\"!hideProgressIndicator() && initialLoad && (isLoading | async)!\"\n [blocking]=\"false\"\n (gridEvent)=\"handleGridEvent($event)\"\n (widgetInstanceRemove)=\"removeWidgetInstance($event)\"\n (widgetInstanceEdit)=\"editWidgetInstance($event)\"\n/>\n", styles: [":host{display:flex;flex:1 1 0;flex-direction:column}::ng-deep .grid-stack-animate,::ng-deep .grid-stack-animate .grid-stack-item{transition-property:inset-inline-start,inset-block-start}\n"] }]
|
|
1520
|
+
}] });
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Function creates a new {@link WidgetConfig} without id from a {@link Widget} and
|
|
1524
|
+
* copies all default values into the {@link WidgetConfig}.
|
|
1525
|
+
* @param widget - The source to create the new {@link WidgetConfig} from.
|
|
1526
|
+
* @returns The created {@link WidgetConfig} without id.
|
|
1527
|
+
*/
|
|
1528
|
+
const createWidgetConfig = (widget) => {
|
|
1529
|
+
const widgetConfig = {
|
|
1530
|
+
heading: widget.name,
|
|
1531
|
+
widgetId: widget.id,
|
|
1532
|
+
version: widget.version,
|
|
1533
|
+
minWidth: 3,
|
|
1534
|
+
...widget.defaults,
|
|
1535
|
+
payload: { ...widget.payload }
|
|
1536
|
+
};
|
|
1537
|
+
return widgetConfig;
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
1542
|
+
* SPDX-License-Identifier: MIT
|
|
1543
|
+
*/
|
|
1544
|
+
/**
|
|
1545
|
+
* Default widget catalog implementation to show all available widgets that can be added
|
|
1546
|
+
* to a dashboard. It consists of a list view, that lists all available widgets and after
|
|
1547
|
+
* selection, a host in which the widget specific editor is loaded. Applications can either
|
|
1548
|
+
* stay with the default catalog or implement their own by extending this class.
|
|
1549
|
+
*/
|
|
1550
|
+
class SiWidgetCatalogComponent {
|
|
1551
|
+
/**
|
|
1552
|
+
* Placeholder text for the search input field in the widget catalog.
|
|
1553
|
+
*
|
|
1554
|
+
* @defaultValue
|
|
1555
|
+
* ```
|
|
1556
|
+
* t(() => $localize`:@@DASHBOARD.WIDGET_LIBRARY.SEARCH_PLACEHOLDER:Search widget`)
|
|
1557
|
+
* ```
|
|
1558
|
+
*/
|
|
1559
|
+
searchPlaceholder = input(t(() => $localize `:@@DASHBOARD.WIDGET_LIBRARY.SEARCH_PLACEHOLDER:Search widget`));
|
|
1560
|
+
/**
|
|
1561
|
+
* Emits when the catalog is `closed`, either by canceling or by adding or saving
|
|
1562
|
+
* a widget configuration. On cancel `undefined` is emitted, otherwise the related
|
|
1563
|
+
* widget configuration is emitted.
|
|
1564
|
+
*/
|
|
1565
|
+
closed = output();
|
|
1566
|
+
/**
|
|
1567
|
+
* View defines if the catalog widget list or the widget editor is visible.
|
|
1568
|
+
*
|
|
1569
|
+
* @internal
|
|
1570
|
+
* @defaultValue 'list'
|
|
1571
|
+
*/
|
|
1572
|
+
view = signal('list');
|
|
1573
|
+
editorHost = viewChild.required('editorHost', { read: ViewContainerRef });
|
|
1574
|
+
/**
|
|
1575
|
+
* Property to provide the available widgets to the catalog. The flexible
|
|
1576
|
+
* dashboard creates the catalog by Angular's `createComponent()` method
|
|
1577
|
+
* and sets the available widgets to this attribute.
|
|
1578
|
+
*
|
|
1579
|
+
* @defaultValue [] */
|
|
1580
|
+
widgetCatalog = [];
|
|
1581
|
+
/**
|
|
1582
|
+
* Holds the search term from the catalog to be visible when going back
|
|
1583
|
+
* by pressing the previous button from the widget edit view.
|
|
1584
|
+
*/
|
|
1585
|
+
searchTerm = '';
|
|
1586
|
+
/**
|
|
1587
|
+
* Array used to hold the search result on the widget catalog.
|
|
1588
|
+
* @defaultValue [] */
|
|
1589
|
+
filteredWidgetCatalog = [];
|
|
1590
|
+
selected = signal(undefined);
|
|
1591
|
+
widgetConfig;
|
|
1592
|
+
hasEditor = signal(false);
|
|
1593
|
+
labelCancel = t(() => $localize `:@@DASHBOARD.WIDGET_LIBRARY.CANCEL:Cancel`);
|
|
1594
|
+
labelPrevious = t(() => $localize `:@@DASHBOARD.WIDGET_LIBRARY.PREVIOUS:Previous`);
|
|
1595
|
+
labelNext = t(() => $localize `:@@DASHBOARD.WIDGET_LIBRARY.NEXT:Next`);
|
|
1596
|
+
labelAdd = t(() => $localize `:@@DASHBOARD.WIDGET_LIBRARY.ADD:Add`);
|
|
1597
|
+
labelEmpty = t(() => $localize `:@@DASHBOARD.WIDGET_LIBRARY.EMPTY:Empty`);
|
|
1598
|
+
labelDialogHeading = t(() => $localize `:@@DASHBOARD.WIDGET_LIBRARY.DISCARD_CONFIG_CHANGE_DIALOG.HEADING:Widget configuration changed`);
|
|
1599
|
+
labelDialogCancel = t(() => $localize `:@@DASHBOARD.WIDGET_LIBRARY.DISCARD_CONFIG_CHANGE_DIALOG.CANCEL:Cancel`);
|
|
1600
|
+
labelDialogMessage = t(() => $localize `:@@DASHBOARD.WIDGET_LIBRARY.DISCARD_CONFIG_CHANGE_DIALOG.MESSAGE:The widget configuration changed. Do you want to discard the changes?`);
|
|
1601
|
+
labelDialogDiscard = t(() => $localize `:@@DASHBOARD.WIDGET_LIBRARY.DISCARD_CONFIG_CHANGE_DIALOG.DISCARD:Discard`);
|
|
1602
|
+
showAddButton = computed(() => this.view() === 'list' ? !this.hasEditor() : true);
|
|
1603
|
+
showNextButton = computed(() => this.view() === 'list' ? this.hasEditor() : this.editorWizardState() !== undefined);
|
|
1604
|
+
showPreviousButton = computed(() => this.view() === 'editor');
|
|
1605
|
+
disableAddButton = computed(() => !this.selected() || this.invalidConfig());
|
|
1606
|
+
disableNextButton = computed(() => {
|
|
1607
|
+
const wizardState = this.editorWizardState();
|
|
1608
|
+
if (this.view() === 'list') {
|
|
1609
|
+
return !this.selected();
|
|
1610
|
+
}
|
|
1611
|
+
else if (!wizardState) {
|
|
1612
|
+
return true;
|
|
1613
|
+
}
|
|
1614
|
+
else if (!wizardState.hasNext) {
|
|
1615
|
+
return true;
|
|
1616
|
+
}
|
|
1617
|
+
else if (wizardState.disableNext !== undefined) {
|
|
1618
|
+
return wizardState.disableNext;
|
|
1619
|
+
}
|
|
1620
|
+
else {
|
|
1621
|
+
return false;
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
/** Indicates if the current config is valid or not. If invalid, the add button is disabled. */
|
|
1625
|
+
invalidConfig = signal(false);
|
|
1626
|
+
/**
|
|
1627
|
+
* Marks the widget configuration as modified. Is set when widget editor instance
|
|
1628
|
+
* emits configChange events. Triggers edit discard confirmation dialog when widget config
|
|
1629
|
+
* is modified but not added to the dashboard.
|
|
1630
|
+
* */
|
|
1631
|
+
widgetConfigModified = false;
|
|
1632
|
+
widgetInstanceEditor;
|
|
1633
|
+
editorWizardState = signal(undefined);
|
|
1634
|
+
widgetInstanceEditorRef;
|
|
1635
|
+
subscriptions = [];
|
|
1636
|
+
dialogService = inject(SiActionDialogService);
|
|
1637
|
+
injector = inject(Injector);
|
|
1638
|
+
envInjector = inject(EnvironmentInjector);
|
|
1639
|
+
ngOnInit() {
|
|
1640
|
+
this.filteredWidgetCatalog = this.widgetCatalog;
|
|
1641
|
+
if (this.widgetCatalog.length > 0) {
|
|
1642
|
+
this.selectWidget(this.widgetCatalog[0]);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
ngOnDestroy() {
|
|
1646
|
+
this.tearDownEditor();
|
|
1647
|
+
}
|
|
1648
|
+
onSearch(searchTerm) {
|
|
1649
|
+
if (!searchTerm || searchTerm.trim().length === 0) {
|
|
1650
|
+
this.searchTerm = '';
|
|
1651
|
+
this.filteredWidgetCatalog = this.widgetCatalog;
|
|
1652
|
+
}
|
|
1653
|
+
else {
|
|
1654
|
+
this.searchTerm = searchTerm;
|
|
1655
|
+
this.filteredWidgetCatalog = this.widgetCatalog.filter(wd => wd.name.toLowerCase().includes(searchTerm.trim().toLowerCase()));
|
|
1656
|
+
}
|
|
1657
|
+
if (this.filteredWidgetCatalog.length > 0) {
|
|
1658
|
+
this.selectWidget(this.filteredWidgetCatalog[0]);
|
|
1659
|
+
}
|
|
1660
|
+
else {
|
|
1661
|
+
this.selectWidget(undefined);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
onCancel() {
|
|
1665
|
+
if (!this.widgetConfigModified) {
|
|
1666
|
+
this.closed.emit(undefined);
|
|
1667
|
+
}
|
|
1668
|
+
else {
|
|
1669
|
+
this.dialogService
|
|
1670
|
+
.showActionDialog({
|
|
1671
|
+
type: 'edit-discard',
|
|
1672
|
+
disableSave: true,
|
|
1673
|
+
heading: this.labelDialogHeading,
|
|
1674
|
+
cancelBtnName: this.labelDialogCancel,
|
|
1675
|
+
disableSaveMessage: this.labelDialogMessage,
|
|
1676
|
+
disableSaveDiscardBtnName: this.labelDialogDiscard
|
|
1677
|
+
})
|
|
1678
|
+
.subscribe(result => {
|
|
1679
|
+
if (result === 'discard') {
|
|
1680
|
+
this.closed.emit(undefined);
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
onNext() {
|
|
1686
|
+
if (this.view() === 'list') {
|
|
1687
|
+
this.setupWidgetInstanceEditor();
|
|
1688
|
+
}
|
|
1689
|
+
else {
|
|
1690
|
+
if (this.isEditorWizdard(this.widgetInstanceEditor)) {
|
|
1691
|
+
this.widgetInstanceEditor.next();
|
|
1692
|
+
this.editorWizardState.set(this.widgetInstanceEditor.state);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
onPrevious() {
|
|
1697
|
+
if (this.isEditorWizdard(this.widgetInstanceEditor) && this.editorWizardState()?.hasPrevious) {
|
|
1698
|
+
this.widgetInstanceEditor.previous();
|
|
1699
|
+
this.editorWizardState.set(this.widgetInstanceEditor.state);
|
|
1700
|
+
}
|
|
1701
|
+
else if (!this.widgetConfigModified) {
|
|
1702
|
+
this.setupCatalog();
|
|
1703
|
+
}
|
|
1704
|
+
else {
|
|
1705
|
+
this.dialogService
|
|
1706
|
+
.showActionDialog({
|
|
1707
|
+
type: 'edit-discard',
|
|
1708
|
+
disableSave: true,
|
|
1709
|
+
heading: this.labelDialogHeading,
|
|
1710
|
+
cancelBtnName: this.labelDialogCancel,
|
|
1711
|
+
message: this.labelDialogMessage,
|
|
1712
|
+
discardBtnName: this.labelDialogDiscard
|
|
1713
|
+
})
|
|
1714
|
+
.subscribe(result => {
|
|
1715
|
+
if (result === 'discard') {
|
|
1716
|
+
this.setupCatalog();
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
setupWidgetInstanceEditor() {
|
|
1722
|
+
const selected = this.selected();
|
|
1723
|
+
if (!selected) {
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
this.tearDownEditor();
|
|
1727
|
+
this.view.set('editor');
|
|
1728
|
+
this.widgetConfig = createWidgetConfig(selected);
|
|
1729
|
+
setupWidgetEditor(selected.componentFactory, this.editorHost(), this.injector, this.envInjector).subscribe({
|
|
1730
|
+
next: componentRef => {
|
|
1731
|
+
this.widgetInstanceEditorRef = componentRef;
|
|
1732
|
+
this.widgetInstanceEditor = componentRef.instance;
|
|
1733
|
+
if (isSignal(this.widgetInstanceEditor.config)) {
|
|
1734
|
+
this.widgetInstanceEditorRef.setInput('config', this.widgetConfig);
|
|
1735
|
+
}
|
|
1736
|
+
else {
|
|
1737
|
+
this.widgetInstanceEditor.config = this.widgetConfig;
|
|
1738
|
+
}
|
|
1739
|
+
// To be used by webcomponent wrapper
|
|
1740
|
+
if ('statusChangesHandler' in this.widgetInstanceEditor) {
|
|
1741
|
+
this.widgetInstanceEditor.statusChangesHandler = this.handleStatusChanges.bind(this);
|
|
1742
|
+
}
|
|
1743
|
+
let hasStatusChangesEmitter = false;
|
|
1744
|
+
if (this.widgetInstanceEditor.statusChanges) {
|
|
1745
|
+
hasStatusChangesEmitter = true;
|
|
1746
|
+
this.subscriptions.push(this.widgetInstanceEditor.statusChanges.subscribe(statusChanges => {
|
|
1747
|
+
this.handleStatusChanges(statusChanges);
|
|
1748
|
+
}));
|
|
1749
|
+
}
|
|
1750
|
+
if (this.widgetInstanceEditor.configChange) {
|
|
1751
|
+
this.subscriptions.push(this.widgetInstanceEditor.configChange.subscribe(config => {
|
|
1752
|
+
if (!hasStatusChangesEmitter) {
|
|
1753
|
+
this.invalidConfig.set(!!config.invalid);
|
|
1754
|
+
this.widgetConfigModified = true;
|
|
1755
|
+
}
|
|
1756
|
+
}));
|
|
1757
|
+
}
|
|
1758
|
+
if (this.isEditorWizdard(this.widgetInstanceEditor)) {
|
|
1759
|
+
this.editorWizardState.set(this.widgetInstanceEditor.state);
|
|
1760
|
+
if (this.widgetInstanceEditor.stateChange) {
|
|
1761
|
+
this.subscriptions.push(this.widgetInstanceEditor.stateChange.subscribe(state => {
|
|
1762
|
+
this.editorWizardState.set(state);
|
|
1763
|
+
}));
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
},
|
|
1767
|
+
error: error => {
|
|
1768
|
+
console.error(error);
|
|
1769
|
+
}
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
tearDownEditor() {
|
|
1773
|
+
this.invalidConfig.set(false);
|
|
1774
|
+
this.widgetConfigModified = false;
|
|
1775
|
+
this.editorWizardState.set(undefined);
|
|
1776
|
+
this.widgetInstanceEditor = undefined;
|
|
1777
|
+
this.subscriptions.forEach(s => s.unsubscribe());
|
|
1778
|
+
this.subscriptions = [];
|
|
1779
|
+
}
|
|
1780
|
+
setupCatalog() {
|
|
1781
|
+
if (this.editorHost().length > 0) {
|
|
1782
|
+
this.editorHost().remove(0);
|
|
1783
|
+
}
|
|
1784
|
+
this.tearDownEditor();
|
|
1785
|
+
this.widgetConfig = undefined;
|
|
1786
|
+
this.view.set('list');
|
|
1787
|
+
}
|
|
1788
|
+
onAddWidget() {
|
|
1789
|
+
const selected = this.selected();
|
|
1790
|
+
if (!selected) {
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
if (!this.widgetConfig) {
|
|
1794
|
+
this.widgetConfig = createWidgetConfig(selected);
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
// Make sure we use the same config object as the editor
|
|
1798
|
+
if (isSignal(this.widgetInstanceEditor?.config)) {
|
|
1799
|
+
this.widgetConfig = this.widgetInstanceEditor?.config() ?? this.widgetConfig;
|
|
1800
|
+
}
|
|
1801
|
+
else {
|
|
1802
|
+
this.widgetConfig = this.widgetInstanceEditor?.config ?? this.widgetConfig;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
this.closed.emit(this.widgetConfig);
|
|
1806
|
+
}
|
|
1807
|
+
selectWidget(widget) {
|
|
1808
|
+
this.selected.set(widget);
|
|
1809
|
+
if (widget?.componentFactory?.editorComponentName) {
|
|
1810
|
+
this.hasEditor.set(true);
|
|
1811
|
+
}
|
|
1812
|
+
else {
|
|
1813
|
+
this.hasEditor.set(false);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
isEditorWizdard(editor) {
|
|
1817
|
+
return !!editor && 'state' in editor;
|
|
1818
|
+
}
|
|
1819
|
+
handleStatusChanges(statusChanges) {
|
|
1820
|
+
if (statusChanges.invalid !== undefined) {
|
|
1821
|
+
this.invalidConfig.set(statusChanges.invalid);
|
|
1822
|
+
}
|
|
1823
|
+
if (statusChanges.modified !== undefined) {
|
|
1824
|
+
this.widgetConfigModified = statusChanges.modified;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiWidgetCatalogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1828
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: SiWidgetCatalogComponent, isStandalone: true, selector: "si-widget-catalog", inputs: { searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed" }, viewQueries: [{ propertyName: "editorHost", first: true, predicate: ["editorHost"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "<div class=\"si-layout-fixed-height mb-8 mx-0 overflow-auto px-2 pt-2\">\n @if (view() === 'list') {\n <div class=\"si-layout-fixed-height\">\n <div class=\"mx-0\">\n <si-search-bar\n class=\"w-100\"\n colorVariant=\"base-1\"\n showIcon=\"true\"\n [placeholder]=\"searchPlaceholder() | translate\"\n [value]=\"searchTerm\"\n (searchChange)=\"onSearch($event)\"\n />\n </div>\n <div\n class=\"si-layout-fixed-height text-pre-wrap text-break overflow-y-auto widget-list mt-6 bg-base-1\"\n >\n @if (filteredWidgetCatalog.length > 0) {\n <div class=\"si-layout-fixed-height list-group\">\n @for (widget of filteredWidgetCatalog; track $index) {\n <button\n type=\"button\"\n class=\"list-group-item list-group-item-action d-flex\"\n [class.active]=\"selected() === widget\"\n (click)=\"selectWidget(widget)\"\n >\n <si-circle-status class=\"my-n4 me-5\" [icon]=\"widget.iconClass\" />\n <div class=\"d-flex flex-column align-items-start\">\n <span class=\"si-title-2\">{{ widget.name }}</span>\n <span class=\"si-body-2\">{{ widget.description?.trim() }}</span>\n </div>\n </button>\n }\n </div>\n } @else {\n <div>\n <si-empty-state icon=\"element-report\" [heading]=\"labelEmpty\" />\n </div>\n }\n </div>\n </div>\n }\n <ng-container #editorHost />\n</div>\n\n<div class=\"catalog-footer py-0 px-2\">\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"onCancel()\">{{\n labelCancel | translate\n }}</button>\n @if (showPreviousButton()) {\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"onPrevious()\">{{\n labelPrevious | translate\n }}</button>\n }\n @if (showNextButton()) {\n <button\n type=\"button\"\n class=\"btn\"\n [class.btn-primary]=\"view() === 'list'\"\n [class.btn-secondary]=\"view() !== 'list'\"\n [disabled]=\"disableNextButton()\"\n (click)=\"onNext()\"\n >{{ labelNext | translate }}</button\n >\n }\n @if (showAddButton()) {\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n [disabled]=\"disableAddButton()\"\n (click)=\"onAddWidget()\"\n >{{ labelAdd | translate }}</button\n >\n }\n</div>\n", styles: [":host{display:flex;flex:1 1 0;flex-direction:column;margin-inline:-4px;margin-block-start:-4px}.widget-list{background-color:var(--element-base-1);border:1px solid var(--element-ui-4);border-radius:var(--element-radius-2);overflow-x:auto}.list-group-item{border-inline-color:transparent;border-radius:0}.catalog-footer{align-items:center;display:flex;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end}.catalog-footer>*{margin-inline-start:16px}\n"], dependencies: [{ kind: "component", type: SiSearchBarComponent, selector: "si-search-bar", inputs: ["debounceTime", "prohibitedCharacters", "placeholder", "showIcon", "tabbable", "value", "readonly", "colorVariant", "disabled"], outputs: ["searchChange"] }, { kind: "component", type: SiCircleStatusComponent, selector: "si-circle-status", inputs: ["status", "color", "icon", "size", "eventOut", "eventIcon", "blink", "blinkPulse", "ariaLabel"] }, { kind: "component", type: SiEmptyStateComponent, selector: "si-empty-state", inputs: ["icon", "heading", "content"] }, { kind: "pipe", type: SiTranslatePipe, name: "translate" }] });
|
|
1829
|
+
}
|
|
1830
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiWidgetCatalogComponent, decorators: [{
|
|
1831
|
+
type: Component,
|
|
1832
|
+
args: [{ selector: 'si-widget-catalog', imports: [SiSearchBarComponent, SiCircleStatusComponent, SiEmptyStateComponent, SiTranslatePipe], template: "<div class=\"si-layout-fixed-height mb-8 mx-0 overflow-auto px-2 pt-2\">\n @if (view() === 'list') {\n <div class=\"si-layout-fixed-height\">\n <div class=\"mx-0\">\n <si-search-bar\n class=\"w-100\"\n colorVariant=\"base-1\"\n showIcon=\"true\"\n [placeholder]=\"searchPlaceholder() | translate\"\n [value]=\"searchTerm\"\n (searchChange)=\"onSearch($event)\"\n />\n </div>\n <div\n class=\"si-layout-fixed-height text-pre-wrap text-break overflow-y-auto widget-list mt-6 bg-base-1\"\n >\n @if (filteredWidgetCatalog.length > 0) {\n <div class=\"si-layout-fixed-height list-group\">\n @for (widget of filteredWidgetCatalog; track $index) {\n <button\n type=\"button\"\n class=\"list-group-item list-group-item-action d-flex\"\n [class.active]=\"selected() === widget\"\n (click)=\"selectWidget(widget)\"\n >\n <si-circle-status class=\"my-n4 me-5\" [icon]=\"widget.iconClass\" />\n <div class=\"d-flex flex-column align-items-start\">\n <span class=\"si-title-2\">{{ widget.name }}</span>\n <span class=\"si-body-2\">{{ widget.description?.trim() }}</span>\n </div>\n </button>\n }\n </div>\n } @else {\n <div>\n <si-empty-state icon=\"element-report\" [heading]=\"labelEmpty\" />\n </div>\n }\n </div>\n </div>\n }\n <ng-container #editorHost />\n</div>\n\n<div class=\"catalog-footer py-0 px-2\">\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"onCancel()\">{{\n labelCancel | translate\n }}</button>\n @if (showPreviousButton()) {\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"onPrevious()\">{{\n labelPrevious | translate\n }}</button>\n }\n @if (showNextButton()) {\n <button\n type=\"button\"\n class=\"btn\"\n [class.btn-primary]=\"view() === 'list'\"\n [class.btn-secondary]=\"view() !== 'list'\"\n [disabled]=\"disableNextButton()\"\n (click)=\"onNext()\"\n >{{ labelNext | translate }}</button\n >\n }\n @if (showAddButton()) {\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n [disabled]=\"disableAddButton()\"\n (click)=\"onAddWidget()\"\n >{{ labelAdd | translate }}</button\n >\n }\n</div>\n", styles: [":host{display:flex;flex:1 1 0;flex-direction:column;margin-inline:-4px;margin-block-start:-4px}.widget-list{background-color:var(--element-base-1);border:1px solid var(--element-ui-4);border-radius:var(--element-radius-2);overflow-x:auto}.list-group-item{border-inline-color:transparent;border-radius:0}.catalog-footer{align-items:center;display:flex;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end}.catalog-footer>*{margin-inline-start:16px}\n"] }]
|
|
1833
|
+
}] });
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
1837
|
+
* SPDX-License-Identifier: MIT
|
|
1838
|
+
*/
|
|
1839
|
+
/**
|
|
1840
|
+
* The component implements a dashboard with adding, removing and resizing widgets.
|
|
1841
|
+
* It consists and connects a toolbar, a grid with widgets, and a widget catalog.
|
|
1842
|
+
*/
|
|
1843
|
+
class SiFlexibleDashboardComponent {
|
|
1844
|
+
/**
|
|
1845
|
+
* Input to change the dashboard editable state.
|
|
1846
|
+
*
|
|
1847
|
+
* @defaultValue false
|
|
1848
|
+
*/
|
|
1849
|
+
editable = model(false);
|
|
1850
|
+
/**
|
|
1851
|
+
* Heading for the dashboard.
|
|
1852
|
+
*/
|
|
1853
|
+
heading = input();
|
|
1854
|
+
/**
|
|
1855
|
+
* Optionally, provide a custom subclass of the SiWidgetCatalogComponent
|
|
1856
|
+
* to use your own catalog component.
|
|
1857
|
+
*/
|
|
1858
|
+
widgetCatalogComponent = input();
|
|
1859
|
+
/**
|
|
1860
|
+
* Optionally, provide a custom subclass of the {@link SiWidgetInstanceEditorDialogComponent}
|
|
1861
|
+
* to use your own implementation.
|
|
1862
|
+
*/
|
|
1863
|
+
widgetInstanceEditorDialogComponent = input();
|
|
1864
|
+
/**
|
|
1865
|
+
* Optionally, but it is recommended to include an id for a dashboard.
|
|
1866
|
+
* The id is utilized in the persistence calls on the {@link SiWidgetStorage}.
|
|
1867
|
+
*/
|
|
1868
|
+
dashboardId = input();
|
|
1869
|
+
/**
|
|
1870
|
+
* Sets the available widgets for the widget catalog to the dashboard.
|
|
1871
|
+
*
|
|
1872
|
+
* @defaultValue [] */
|
|
1873
|
+
widgetCatalog = input([]);
|
|
1874
|
+
/**
|
|
1875
|
+
* Option to remove the add widget instance button from the primary toolbar.
|
|
1876
|
+
*
|
|
1877
|
+
* @defaultValue false
|
|
1878
|
+
*/
|
|
1879
|
+
hideAddWidgetInstanceButton = input(false);
|
|
1880
|
+
/**
|
|
1881
|
+
* Option to hide the dashboard edit button.
|
|
1882
|
+
*
|
|
1883
|
+
* @defaultValue false
|
|
1884
|
+
*/
|
|
1885
|
+
hideEditButton = input(false);
|
|
1886
|
+
/**
|
|
1887
|
+
* Option to display the edit button as a text button instead, only if the window is larger than xs {@link SiResponsiveContainerDirective}.
|
|
1888
|
+
*
|
|
1889
|
+
* @defaultValue false
|
|
1890
|
+
*/
|
|
1891
|
+
showEditButtonLabel = input(false);
|
|
1892
|
+
/**
|
|
1893
|
+
* Option to turn off the loading spinner on save and load operations.
|
|
1894
|
+
*
|
|
1895
|
+
* @defaultValue false
|
|
1896
|
+
*/
|
|
1897
|
+
hideProgressIndicator = input(false);
|
|
1898
|
+
/**
|
|
1899
|
+
* Option to configure a dashboard instance. Default is the optional value from
|
|
1900
|
+
* the {@link SI_DASHBOARD_CONFIGURATION}.
|
|
1901
|
+
*
|
|
1902
|
+
* @defaultValue inject(SI_DASHBOARD_CONFIGURATION)
|
|
1903
|
+
*/
|
|
1904
|
+
config = input(inject(SI_DASHBOARD_CONFIGURATION));
|
|
1905
|
+
/**
|
|
1906
|
+
* Placeholder text for the search input field in the widget catalog.
|
|
1907
|
+
*
|
|
1908
|
+
* @defaultValue
|
|
1909
|
+
* ```
|
|
1910
|
+
* t(() => $localize`:@@DASHBOARD.WIDGET_LIBRARY.SEARCH_PLACEHOLDER:Search widget`)
|
|
1911
|
+
* ```
|
|
1912
|
+
*/
|
|
1913
|
+
searchPlaceholder = input(t(() => $localize `:@@DASHBOARD.WIDGET_LIBRARY.SEARCH_PLACEHOLDER:Search widget`));
|
|
1914
|
+
/**
|
|
1915
|
+
* The grid component is the actual container for the widgets.
|
|
1916
|
+
*/
|
|
1917
|
+
grid = viewChild.required('grid');
|
|
1918
|
+
/** Property to access the dashboard component instance.
|
|
1919
|
+
*/
|
|
1920
|
+
dashboard = viewChild.required('dashboard');
|
|
1921
|
+
/** The view child holds the container that hosts the widget catalog.
|
|
1922
|
+
*/
|
|
1923
|
+
catalogHost = viewChild.required('catalogHost', { read: ViewContainerRef });
|
|
1924
|
+
/**
|
|
1925
|
+
* Emits the modification state of the grid. It is `unmodified` when the visible state
|
|
1926
|
+
* is equal to the loaded state from the widget storage. When the user modifies the dashboard by
|
|
1927
|
+
* e.g. while moving the widgets, the dashboard is marked as `modified` and emits `true` and when the user
|
|
1928
|
+
* persists the change by saving, or reverts the state by canceling, the state is `unmodified`
|
|
1929
|
+
* again and emits `false`.
|
|
1930
|
+
*/
|
|
1931
|
+
isModified = output();
|
|
1932
|
+
labelAddWidget = t(() => $localize `:@@DASHBOARD.ADD_WIDGET:Add widget`);
|
|
1933
|
+
labelEditor = t(() => $localize `:@@DASHBOARD.WIDGET_EDITOR_DIALOG.TITLE:Edit`);
|
|
1934
|
+
labelCatalog = t(() => $localize `:@@DASHBOARD.WIDGET_LIBRARY.TITLE:Add widget`);
|
|
1935
|
+
addWidgetInstanceAction = {
|
|
1936
|
+
type: 'action',
|
|
1937
|
+
label: this.labelAddWidget,
|
|
1938
|
+
action: () => this.showWidgetCatalog()
|
|
1939
|
+
};
|
|
1940
|
+
/**
|
|
1941
|
+
* The primary action menu items shown in the edit mode of the dashboard.
|
|
1942
|
+
*/
|
|
1943
|
+
primaryEditActions$ = new BehaviorSubject([
|
|
1944
|
+
this.addWidgetInstanceAction
|
|
1945
|
+
]);
|
|
1946
|
+
/**
|
|
1947
|
+
* The secondary action menu items shown in the edit mode of the dashboard. When all menu items are more than
|
|
1948
|
+
* three, they will be places in the secondary menu of the content action bar.
|
|
1949
|
+
*/
|
|
1950
|
+
secondaryEditActions$ = new BehaviorSubject([]);
|
|
1951
|
+
/**
|
|
1952
|
+
* @returns True, if the dashboard shows its widgets and not a catalog or and editor.
|
|
1953
|
+
*/
|
|
1954
|
+
isDashboardVisible = computed(() => this.viewState() === 'dashboard');
|
|
1955
|
+
/**
|
|
1956
|
+
* The page title of the dashboard, which is either {@link SiFlexibleDashboardComponent.heading} for the
|
|
1957
|
+
* default widget view or `DASHBOARD.WIDGET_EDITOR_DIALOG.TITLE` or 'DASHBOARD.WIDGET_LIBRARY.TITLE' when
|
|
1958
|
+
* adding new or editing widgets.
|
|
1959
|
+
*/
|
|
1960
|
+
pageTitle = computed(() => {
|
|
1961
|
+
const viewState = this.viewState();
|
|
1962
|
+
switch (viewState) {
|
|
1963
|
+
case 'editor':
|
|
1964
|
+
return this.labelEditor;
|
|
1965
|
+
case 'catalog':
|
|
1966
|
+
return this.labelCatalog;
|
|
1967
|
+
default:
|
|
1968
|
+
return this.heading();
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
viewState = signal('dashboard');
|
|
1972
|
+
subscriptions = [];
|
|
1973
|
+
widgetStorage = inject(SI_WIDGET_STORE);
|
|
1974
|
+
hideAddWidgetInstanceButton$ = new BehaviorSubject(this.hideAddWidgetInstanceButton());
|
|
1975
|
+
dashboardId$ = new Subject();
|
|
1976
|
+
outputRefSubscription = [];
|
|
1977
|
+
toolbar = viewChild.required('toolbar');
|
|
1978
|
+
ngOnChanges(changes) {
|
|
1979
|
+
if (changes.dashboardId) {
|
|
1980
|
+
const dashboard = this.dashboard();
|
|
1981
|
+
if (dashboard.isExpanded) {
|
|
1982
|
+
dashboard.restore();
|
|
1983
|
+
}
|
|
1984
|
+
if (this.editable()) {
|
|
1985
|
+
this.grid().cancel();
|
|
1986
|
+
this.viewState.set('dashboard');
|
|
1987
|
+
this.catalogHost().clear();
|
|
1988
|
+
}
|
|
1989
|
+
this.dashboardId$.next(changes.dashboardId.currentValue);
|
|
1990
|
+
this.setupMenuItems();
|
|
1991
|
+
}
|
|
1992
|
+
if (changes.editable) {
|
|
1993
|
+
if (changes.editable.currentValue) {
|
|
1994
|
+
this.grid().edit();
|
|
1995
|
+
}
|
|
1996
|
+
else {
|
|
1997
|
+
this.grid().cancel();
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
if (changes.hideAddWidgetInstanceButton) {
|
|
2001
|
+
this.hideAddWidgetInstanceButton$.next(changes.hideAddWidgetInstanceButton.currentValue);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
ngOnInit() {
|
|
2005
|
+
this.setupMenuItems();
|
|
2006
|
+
const grid = this.grid();
|
|
2007
|
+
this.outputRefSubscription.push(grid.widgetInstanceEdit.subscribe(widgetConfig => {
|
|
2008
|
+
this.editWidgetInstance(widgetConfig);
|
|
2009
|
+
}));
|
|
2010
|
+
this.outputRefSubscription.push(grid.editable.subscribe(editable => {
|
|
2011
|
+
this.editable.set(editable);
|
|
2012
|
+
}));
|
|
2013
|
+
}
|
|
2014
|
+
ngOnDestroy() {
|
|
2015
|
+
this.dashboardId$.next(undefined);
|
|
2016
|
+
this.subscriptions?.forEach(sub => sub.unsubscribe());
|
|
2017
|
+
this.outputRefSubscription?.forEach(sub => sub.unsubscribe());
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Shows the widget catalog of the dashboard. If a widget instance is expanded
|
|
2021
|
+
* it restores the widget instances before.
|
|
2022
|
+
*/
|
|
2023
|
+
showWidgetCatalog() {
|
|
2024
|
+
const dashboard = this.dashboard();
|
|
2025
|
+
if (dashboard.isExpanded) {
|
|
2026
|
+
dashboard.restore();
|
|
2027
|
+
}
|
|
2028
|
+
if (!this.editable()) {
|
|
2029
|
+
this.grid().edit();
|
|
2030
|
+
}
|
|
2031
|
+
this.viewState.set('catalog');
|
|
2032
|
+
const componentType = this.widgetCatalogComponent() ?? SiWidgetCatalogComponent;
|
|
2033
|
+
const catalogRef = this.catalogHost().createComponent(componentType);
|
|
2034
|
+
catalogRef.setInput('searchPlaceholder', this.searchPlaceholder());
|
|
2035
|
+
catalogRef.instance.widgetCatalog = this.widgetCatalog();
|
|
2036
|
+
const subscription = catalogRef.instance.closed.subscribe(widgetConfig => {
|
|
2037
|
+
subscription.unsubscribe();
|
|
2038
|
+
this.viewState.set('dashboard');
|
|
2039
|
+
this.catalogHost().clear();
|
|
2040
|
+
if (widgetConfig) {
|
|
2041
|
+
this.grid().addWidgetInstance(widgetConfig);
|
|
2042
|
+
}
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
onModified(event) {
|
|
2046
|
+
this.isModified.emit(event);
|
|
2047
|
+
this.toolbar().disableSaveButton.set(!event);
|
|
2048
|
+
}
|
|
2049
|
+
setupMenuItems() {
|
|
2050
|
+
const primaryMenuItems = this.widgetStorage.getToolbarMenuItems
|
|
2051
|
+
? this.widgetStorage.getToolbarMenuItems(this.dashboardId()).primary
|
|
2052
|
+
: of([]);
|
|
2053
|
+
combineLatest([primaryMenuItems, this.hideAddWidgetInstanceButton$])
|
|
2054
|
+
.pipe(takeUntil(this.dashboardId$))
|
|
2055
|
+
.subscribe(([items, hideAddButton]) => this.setupPrimaryMenuItems(items, hideAddButton));
|
|
2056
|
+
const secondaryMenuItems = this.widgetStorage.getToolbarMenuItems
|
|
2057
|
+
? this.widgetStorage.getToolbarMenuItems(this.dashboardId()).secondary
|
|
2058
|
+
: undefined;
|
|
2059
|
+
if (secondaryMenuItems) {
|
|
2060
|
+
secondaryMenuItems
|
|
2061
|
+
.pipe(takeUntil(this.dashboardId$), map(items => items.map(item => this.proxyMenuItemAction(item))))
|
|
2062
|
+
.subscribe(items => this.secondaryEditActions$.next(items));
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
setupPrimaryMenuItems(items, hideAddWidgetInstanceButton) {
|
|
2066
|
+
const next = hideAddWidgetInstanceButton
|
|
2067
|
+
? []
|
|
2068
|
+
: [this.addWidgetInstanceAction];
|
|
2069
|
+
next.push(...items.map(item => this.proxyMenuItemAction(item)));
|
|
2070
|
+
this.primaryEditActions$.next(next);
|
|
2071
|
+
}
|
|
2072
|
+
editWidgetInstance(widgetConfig) {
|
|
2073
|
+
const dashboard = this.dashboard();
|
|
2074
|
+
if (dashboard.isExpanded) {
|
|
2075
|
+
dashboard.restore();
|
|
2076
|
+
}
|
|
2077
|
+
const widget = this.getWidget(widgetConfig.widgetId);
|
|
2078
|
+
const widgetInstanceEditorDialogComponent = this.widgetInstanceEditorDialogComponent();
|
|
2079
|
+
const componentType = widgetInstanceEditorDialogComponent ?? SiWidgetInstanceEditorDialogComponent;
|
|
2080
|
+
this.viewState.set('editor');
|
|
2081
|
+
const catalogRef = this.catalogHost().createComponent(componentType);
|
|
2082
|
+
catalogRef.setInput('widgetConfig', widgetConfig);
|
|
2083
|
+
catalogRef.setInput('widget', widget);
|
|
2084
|
+
const subscription = catalogRef.instance.closed.subscribe(editedWidgetConfig => {
|
|
2085
|
+
subscription.unsubscribe();
|
|
2086
|
+
this.viewState.set('dashboard');
|
|
2087
|
+
this.catalogHost().clear();
|
|
2088
|
+
if (editedWidgetConfig) {
|
|
2089
|
+
this.grid().updateWidgetInstance(editedWidgetConfig);
|
|
2090
|
+
}
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
proxyMenuItemAction(item) {
|
|
2094
|
+
if ('action' in item) {
|
|
2095
|
+
if (!item.action || item.action instanceof String) {
|
|
2096
|
+
return item;
|
|
2097
|
+
}
|
|
2098
|
+
else {
|
|
2099
|
+
const realAction = item.action;
|
|
2100
|
+
item.action = () => {
|
|
2101
|
+
realAction(this.grid());
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
return item;
|
|
2106
|
+
}
|
|
2107
|
+
getWidget(id) {
|
|
2108
|
+
return this.widgetCatalog().find(widget => widget.id === id);
|
|
2109
|
+
}
|
|
2110
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiFlexibleDashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2111
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.0.6", type: SiFlexibleDashboardComponent, isStandalone: true, selector: "si-flexible-dashboard", inputs: { editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, widgetCatalogComponent: { classPropertyName: "widgetCatalogComponent", publicName: "widgetCatalogComponent", isSignal: true, isRequired: false, transformFunction: null }, widgetInstanceEditorDialogComponent: { classPropertyName: "widgetInstanceEditorDialogComponent", publicName: "widgetInstanceEditorDialogComponent", isSignal: true, isRequired: false, transformFunction: null }, dashboardId: { classPropertyName: "dashboardId", publicName: "dashboardId", isSignal: true, isRequired: false, transformFunction: null }, widgetCatalog: { classPropertyName: "widgetCatalog", publicName: "widgetCatalog", isSignal: true, isRequired: false, transformFunction: null }, hideAddWidgetInstanceButton: { classPropertyName: "hideAddWidgetInstanceButton", publicName: "hideAddWidgetInstanceButton", isSignal: true, isRequired: false, transformFunction: null }, hideEditButton: { classPropertyName: "hideEditButton", publicName: "hideEditButton", isSignal: true, isRequired: false, transformFunction: null }, showEditButtonLabel: { classPropertyName: "showEditButtonLabel", publicName: "showEditButtonLabel", isSignal: true, isRequired: false, transformFunction: null }, hideProgressIndicator: { classPropertyName: "hideProgressIndicator", publicName: "hideProgressIndicator", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { editable: "editableChange", isModified: "isModified" }, viewQueries: [{ propertyName: "grid", first: true, predicate: ["grid"], descendants: true, isSignal: true }, { propertyName: "dashboard", first: true, predicate: ["dashboard"], descendants: true, isSignal: true }, { propertyName: "catalogHost", first: true, predicate: ["catalogHost"], descendants: true, read: ViewContainerRef, isSignal: true }, { propertyName: "toolbar", first: true, predicate: ["toolbar"], descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<si-dashboard #dashboard [heading]=\"pageTitle()\" [sticky]=\"true\">\n <si-dashboard-toolbar\n #toolbar\n menubar\n [class.d-none]=\"!isDashboardVisible()\"\n [primaryEditActions]=\"(primaryEditActions$ | async) ?? []\"\n [secondaryEditActions]=\"(secondaryEditActions$ | async) ?? []\"\n [editable]=\"editable()\"\n [disabled]=\"(grid.isLoading | async) ?? false\"\n [hideEditButton]=\"hideEditButton()\"\n [showEditButtonLabel]=\"showEditButtonLabel()\"\n (editableChange)=\"grid.edit()\"\n (save)=\"grid.save()\"\n (cancel)=\"grid.cancel()\"\n >\n <ng-content select=\"[filters-slot]\" filters-slot />\n </si-dashboard-toolbar>\n\n <div dashboard class=\"d-contents\">\n <si-grid\n #grid\n [class.d-none]=\"!isDashboardVisible()\"\n [gridConfig]=\"config()?.grid\"\n [dashboardId]=\"dashboardId()\"\n [widgetCatalog]=\"widgetCatalog()\"\n [hideProgressIndicator]=\"hideProgressIndicator()\"\n [widgetInstanceEditorDialogComponent]=\"widgetInstanceEditorDialogComponent()\"\n [emitWidgetInstanceEditEvents]=\"true\"\n (isModified)=\"onModified($event)\"\n >\n <ng-content select=\"[empty-state]\" empty-state />\n </si-grid>\n <ng-container #catalogHost />\n </div>\n</si-dashboard>\n", styles: [":host{display:flex;flex:1 1 0;flex-direction:column}\n"], dependencies: [{ kind: "component", type: SiDashboardComponent, selector: "si-dashboard", inputs: ["heading", "enableExpandInteractions", "sticky", "hideMenubar"] }, { kind: "component", type: SiDashboardToolbarComponent, selector: "si-dashboard-toolbar", inputs: ["primaryEditActions", "secondaryEditActions", "disableSaveButton", "disabled", "editable", "hideEditButton", "showEditButtonLabel"], outputs: ["disableSaveButtonChange", "editableChange", "save", "cancel"] }, { kind: "component", type: SiGridComponent, selector: "si-grid", inputs: ["gridConfig", "editable", "dashboardId", "widgetCatalog", "emitWidgetInstanceEditEvents", "hideProgressIndicator", "widgetInstanceEditorDialogComponent"], outputs: ["editableChange", "isModified", "widgetInstanceEdit"] }, { kind: "pipe", type: AsyncPipe, name: "async" }] });
|
|
2112
|
+
}
|
|
2113
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiFlexibleDashboardComponent, decorators: [{
|
|
2114
|
+
type: Component,
|
|
2115
|
+
args: [{ selector: 'si-flexible-dashboard', imports: [SiDashboardComponent, SiDashboardToolbarComponent, SiGridComponent, AsyncPipe], template: "<si-dashboard #dashboard [heading]=\"pageTitle()\" [sticky]=\"true\">\n <si-dashboard-toolbar\n #toolbar\n menubar\n [class.d-none]=\"!isDashboardVisible()\"\n [primaryEditActions]=\"(primaryEditActions$ | async) ?? []\"\n [secondaryEditActions]=\"(secondaryEditActions$ | async) ?? []\"\n [editable]=\"editable()\"\n [disabled]=\"(grid.isLoading | async) ?? false\"\n [hideEditButton]=\"hideEditButton()\"\n [showEditButtonLabel]=\"showEditButtonLabel()\"\n (editableChange)=\"grid.edit()\"\n (save)=\"grid.save()\"\n (cancel)=\"grid.cancel()\"\n >\n <ng-content select=\"[filters-slot]\" filters-slot />\n </si-dashboard-toolbar>\n\n <div dashboard class=\"d-contents\">\n <si-grid\n #grid\n [class.d-none]=\"!isDashboardVisible()\"\n [gridConfig]=\"config()?.grid\"\n [dashboardId]=\"dashboardId()\"\n [widgetCatalog]=\"widgetCatalog()\"\n [hideProgressIndicator]=\"hideProgressIndicator()\"\n [widgetInstanceEditorDialogComponent]=\"widgetInstanceEditorDialogComponent()\"\n [emitWidgetInstanceEditEvents]=\"true\"\n (isModified)=\"onModified($event)\"\n >\n <ng-content select=\"[empty-state]\" empty-state />\n </si-grid>\n <ng-container #catalogHost />\n </div>\n</si-dashboard>\n", styles: [":host{display:flex;flex:1 1 0;flex-direction:column}\n"] }]
|
|
2116
|
+
}] });
|
|
2117
|
+
|
|
2118
|
+
/**
|
|
2119
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
2120
|
+
* SPDX-License-Identifier: MIT
|
|
2121
|
+
*/
|
|
2122
|
+
class SiDashboardsNgModule {
|
|
2123
|
+
/**
|
|
2124
|
+
* @deprecated The module configuration `forRoot` is not needed any more. You can use the injection tokens
|
|
2125
|
+
* `SI_DASHBOARD_CONFIGURATION` and `SI_WIDGET_STORE` direct in your app configuration. We migrated to standalone
|
|
2126
|
+
* components already and will delete this module definition later.
|
|
2127
|
+
*/
|
|
2128
|
+
static forRoot({ config, dashboardApi }) {
|
|
2129
|
+
return {
|
|
2130
|
+
ngModule: SiDashboardsNgModule,
|
|
2131
|
+
providers: [
|
|
2132
|
+
{
|
|
2133
|
+
provide: SI_DASHBOARD_CONFIGURATION,
|
|
2134
|
+
useValue: config
|
|
2135
|
+
},
|
|
2136
|
+
dashboardApi ?? {
|
|
2137
|
+
provide: SiWidgetStorage,
|
|
2138
|
+
useClass: SiDefaultWidgetStorage
|
|
2139
|
+
},
|
|
2140
|
+
dashboardApi ? { provide: SI_WIDGET_STORE, useExisting: SiWidgetStorage } : []
|
|
2141
|
+
]
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiDashboardsNgModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
2145
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.0.6", ngImport: i0, type: SiDashboardsNgModule, imports: [SiFlexibleDashboardComponent,
|
|
2146
|
+
SiGridComponent,
|
|
2147
|
+
SiWidgetCatalogComponent,
|
|
2148
|
+
SiWidgetInstanceEditorDialogComponent], exports: [SiFlexibleDashboardComponent,
|
|
2149
|
+
SiGridComponent,
|
|
2150
|
+
SiWidgetCatalogComponent,
|
|
2151
|
+
SiWidgetInstanceEditorDialogComponent] });
|
|
2152
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiDashboardsNgModule, imports: [SiFlexibleDashboardComponent,
|
|
2153
|
+
SiWidgetCatalogComponent,
|
|
2154
|
+
SiWidgetInstanceEditorDialogComponent] });
|
|
2155
|
+
}
|
|
2156
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiDashboardsNgModule, decorators: [{
|
|
2157
|
+
type: NgModule,
|
|
2158
|
+
args: [{
|
|
2159
|
+
imports: [
|
|
2160
|
+
SiFlexibleDashboardComponent,
|
|
2161
|
+
SiGridComponent,
|
|
2162
|
+
SiWidgetCatalogComponent,
|
|
2163
|
+
SiWidgetInstanceEditorDialogComponent
|
|
2164
|
+
],
|
|
2165
|
+
exports: [
|
|
2166
|
+
SiFlexibleDashboardComponent,
|
|
2167
|
+
SiGridComponent,
|
|
2168
|
+
SiWidgetCatalogComponent,
|
|
2169
|
+
SiWidgetInstanceEditorDialogComponent
|
|
2170
|
+
]
|
|
2171
|
+
}]
|
|
2172
|
+
}] });
|
|
2173
|
+
|
|
2174
|
+
/**
|
|
2175
|
+
* Copyright (c) Siemens 2016 - 2025
|
|
2176
|
+
* SPDX-License-Identifier: MIT
|
|
2177
|
+
*/
|
|
2178
|
+
|
|
2179
|
+
/**
|
|
2180
|
+
* Generated bundle index. Do not edit.
|
|
2181
|
+
*/
|
|
2182
|
+
|
|
2183
|
+
export { CONFIG_TOKEN, DEFAULT_GRIDSTACK_OPTIONS, DEFAULT_WIDGET_STORAGE_TOKEN, NEW_WIDGET_PREFIX, SI_DASHBOARD_CONFIGURATION, SI_WIDGET_STORE, STORAGE_KEY, SiDashboardsNgModule, SiDefaultWidgetStorage, SiFlexibleDashboardComponent, SiGridComponent, SiWidgetCatalogComponent, SiWidgetInstanceEditorDialogComponent, SiWidgetStorage, SiDashboardsNgModule as SimplDashboardsNgModule, createWidgetConfig, setupWidgetEditor, setupWidgetInstance, widgetFactoryRegistry };
|
|
2184
|
+
//# sourceMappingURL=spike-rabbit-dashboards-ng.mjs.map
|