@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.
@@ -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