@yuuvis/client-components 3.2.2 → 3.3.0

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.
@@ -1,60 +1,87 @@
1
- import * as i1 from '@angular/common';
2
- import { CommonModule } from '@angular/common';
3
1
  import * as i0 from '@angular/core';
4
- import { Injectable, Input, Component, inject, input, effect, output, viewChild, TemplateRef, signal, computed, untracked, NgModule } from '@angular/core';
5
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
- import { MAT_DIALOG_DATA, MatDialogRef, MatDialog } from '@angular/material/dialog';
7
- import * as i3$2 from '@angular/material/icon';
8
- import { MatIconModule } from '@angular/material/icon';
9
- import { TranslateService, TranslatePipe } from '@yuuvis/client-core';
10
- import { Gridster, DisplayGrid, GridType, GridsterItem } from 'angular-gridster2';
11
- import * as i3 from 'ng-dynamic-component';
12
- import { DynamicIoModule } from 'ng-dynamic-component';
13
- import { ReplaySubject, Subject } from 'rxjs';
14
- import * as i3$1 from '@angular/forms';
15
- import { FormControl, ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
2
+ import { Input, Component, inject, Injectable, input, output, signal, effect, DestroyRef, viewChild, TemplateRef, computed, untracked, NgModule } from '@angular/core';
3
+ import * as i3$2 from '@angular/forms';
4
+ import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
16
5
  import * as i5 from '@angular/material/button';
17
6
  import { MatButtonModule } from '@angular/material/button';
18
- import * as i2 from '@angular/material/list';
19
- import { MatListModule } from '@angular/material/list';
20
- import { DialogComponent, ConfirmService } from '@yuuvis/client-components/common';
21
- import { YmtButtonDirective, YmtIconButtonDirective } from '@yuuvis/material';
22
- import { SignalComponentIoModule } from 'ng-dynamic-component/signal-component-io';
23
- import * as i4 from '@angular/material/tooltip';
24
- import { MatTooltipModule, MatTooltip } from '@angular/material/tooltip';
7
+ import { MAT_DIALOG_DATA, MatDialogRef, MatDialog } from '@angular/material/dialog';
8
+ import * as i5$1 from '@angular/material/divider';
25
9
  import { MatDividerModule } from '@angular/material/divider';
26
10
  import * as i1$1 from '@angular/material/form-field';
27
11
  import { MatFormFieldModule } from '@angular/material/form-field';
28
- import * as i2$1 from '@angular/material/input';
12
+ import * as i3 from '@angular/material/icon';
13
+ import { MatIconModule } from '@angular/material/icon';
14
+ import * as i2 from '@angular/material/input';
29
15
  import { MatInputModule } from '@angular/material/input';
30
16
  import * as i4$1 from '@angular/material/menu';
31
17
  import { MatMenuModule } from '@angular/material/menu';
18
+ import * as i4 from '@angular/material/tooltip';
19
+ import { MatTooltipModule, MatTooltip } from '@angular/material/tooltip';
20
+ import { DialogComponent, ConfirmService } from '@yuuvis/client-components/common';
21
+ import { TranslateService, TranslatePipe, Logger } from '@yuuvis/client-core';
22
+ import { YmtButtonDirective, YmtIconButtonDirective } from '@yuuvis/material';
23
+ import * as i1 from '@angular/common';
24
+ import { CommonModule } from '@angular/common';
25
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
26
+ import { Gridster, DisplayGrid, GridType, GridsterItem } from 'angular-gridster2';
27
+ import * as i3$1 from 'ng-dynamic-component';
28
+ import { DynamicIoModule } from 'ng-dynamic-component';
29
+ import { SignalComponentIoModule } from 'ng-dynamic-component/signal-component-io';
30
+ import { ReplaySubject, Subject } from 'rxjs';
32
31
  import { TranslatePipe as TranslatePipe$1 } from '@ngx-translate/core';
33
32
 
34
- class WidgetGridEventService {
35
- widgetGridEventSource = new ReplaySubject();
36
- widgetEvents$ = this.widgetGridEventSource.asObservable();
37
- trigger(evt) {
38
- this.widgetGridEventSource.next(evt);
33
+ class WidgetGridUtils {
34
+ static PREF_FUNCTION = 'Function';
35
+ static PREF_RANGEVALUE = 'RangeValue';
36
+ /**
37
+ * Takes a `WidgetGridItemConfig` and stringifies it. The challenge
38
+ * here is that callback functions should also be stringified.
39
+ * @param gridItemConfig the config object
40
+ */
41
+ static gridConfigStringify(o) {
42
+ return JSON.stringify(o, (key, value) => {
43
+ if (typeof value === 'function') {
44
+ return `/${WidgetGridUtils.PREF_FUNCTION}(${value.toString()})/`;
45
+ }
46
+ return value;
47
+ });
48
+ }
49
+ /**
50
+ * Takes a string stringified with `gridConfigStringify()` and parses it
51
+ * to output a proper WidgetGridItemConfig.
52
+ * @param stringifiedConfig stringified widget grid config
53
+ */
54
+ static gridConfigParse(stringifiedConfig) {
55
+ return JSON.parse(stringifiedConfig, function (key, value) {
56
+ if (typeof value === 'string' &&
57
+ value.startsWith(`/${WidgetGridUtils.PREF_FUNCTION}(`) &&
58
+ // value.startsWith('/Function(') &&
59
+ value.endsWith(')/')) {
60
+ value = value.substring(WidgetGridUtils.PREF_FUNCTION.length + 2, value.length - 2);
61
+ return (0, eval)('(' + value + ')');
62
+ }
63
+ return value;
64
+ });
65
+ }
66
+ static uuid() {
67
+ return WidgetGridUtils._p8() + WidgetGridUtils._p8(true) + WidgetGridUtils._p8(true) + WidgetGridUtils._p8();
68
+ }
69
+ static _p8(s) {
70
+ const p = (Math.random().toString(16) + '000000000').substr(2, 8);
71
+ return s ? `-${p.substr(0, 4)}-${p.substr(4, 4)}` : p;
39
72
  }
40
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WidgetGridEventService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
41
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WidgetGridEventService, providedIn: 'root' });
42
73
  }
43
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WidgetGridEventService, decorators: [{
44
- type: Injectable,
45
- args: [{
46
- providedIn: 'root',
47
- }]
48
- }] });
49
74
 
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
76
  class NoopComponent {
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
78
  widgetConfig;
52
79
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NoopComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
53
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.12", type: NoopComponent, isStandalone: true, selector: "yuv-noop", inputs: { widgetConfig: "widgetConfig" }, ngImport: i0, template: "<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 0 24 24\" width=\"24px\" fill=\"#000000\"><path d=\"M0 0h24v24H0V0z\" fill=\"none\"/><circle cx=\"15.5\" cy=\"9.5\" r=\"1.5\"/><circle cx=\"8.5\" cy=\"9.5\" r=\"1.5\"/><path d=\"M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-6c-2.33 0-4.32 1.45-5.12 3.5h1.67c.69-1.19 1.97-2 3.45-2s2.75.81 3.45 2h1.67c-.8-2.05-2.79-3.5-5.12-3.5z\"/></svg>", styles: [":host{height:100%;display:flex;flex-flow:column;align-items:center;justify-content:center;padding:var(--ymt-spacing-m);background-color:var(--panel-background);opacity:.7}:host svg{width:48px;height:48px;fill:var(--text-color-caption)}:host span{margin-top:var(--ymt-spacing-m)}\n"] });
80
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.12", type: NoopComponent, isStandalone: true, selector: "yuv-noop", inputs: { widgetConfig: "widgetConfig" }, ngImport: i0, template: "<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 0 24 24\" width=\"24px\" fill=\"#000000\"><path d=\"M0 0h24v24H0V0z\" fill=\"none\"/><circle cx=\"15.5\" cy=\"9.5\" r=\"1.5\"/><circle cx=\"8.5\" cy=\"9.5\" r=\"1.5\"/><path d=\"M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-6c-2.33 0-4.32 1.45-5.12 3.5h1.67c.69-1.19 1.97-2 3.45-2s2.75.81 3.45 2h1.67c-.8-2.05-2.79-3.5-5.12-3.5z\"/></svg>", styles: [":host{height:100%;display:flex;flex-flow:column;align-items:center;justify-content:center;padding:var(--ymt-spacing-m);background-color:var(--panel-background);opacity:.7}:host svg{width:48px;height:48px;fill:var(--ymt-text-color-subtle)}:host span{margin-top:var(--ymt-spacing-m)}\n"] });
54
81
  }
55
82
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NoopComponent, decorators: [{
56
83
  type: Component,
57
- args: [{ selector: 'yuv-noop', template: "<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 0 24 24\" width=\"24px\" fill=\"#000000\"><path d=\"M0 0h24v24H0V0z\" fill=\"none\"/><circle cx=\"15.5\" cy=\"9.5\" r=\"1.5\"/><circle cx=\"8.5\" cy=\"9.5\" r=\"1.5\"/><path d=\"M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-6c-2.33 0-4.32 1.45-5.12 3.5h1.67c.69-1.19 1.97-2 3.45-2s2.75.81 3.45 2h1.67c-.8-2.05-2.79-3.5-5.12-3.5z\"/></svg>", styles: [":host{height:100%;display:flex;flex-flow:column;align-items:center;justify-content:center;padding:var(--ymt-spacing-m);background-color:var(--panel-background);opacity:.7}:host svg{width:48px;height:48px;fill:var(--text-color-caption)}:host span{margin-top:var(--ymt-spacing-m)}\n"] }]
84
+ args: [{ selector: 'yuv-noop', template: "<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 0 24 24\" width=\"24px\" fill=\"#000000\"><path d=\"M0 0h24v24H0V0z\" fill=\"none\"/><circle cx=\"15.5\" cy=\"9.5\" r=\"1.5\"/><circle cx=\"8.5\" cy=\"9.5\" r=\"1.5\"/><path d=\"M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-6c-2.33 0-4.32 1.45-5.12 3.5h1.67c.69-1.19 1.97-2 3.45-2s2.75.81 3.45 2h1.67c-.8-2.05-2.79-3.5-5.12-3.5z\"/></svg>", styles: [":host{height:100%;display:flex;flex-flow:column;align-items:center;justify-content:center;padding:var(--ymt-spacing-m);background-color:var(--panel-background);opacity:.7}:host svg{width:48px;height:48px;fill:var(--ymt-text-color-subtle)}:host span{margin-top:var(--ymt-spacing-m)}\n"] }]
58
85
  }], propDecorators: { widgetConfig: [{
59
86
  type: Input
60
87
  }] } });
@@ -73,7 +100,7 @@ class WidgetGridRegistry {
73
100
  registeredWidgets = [];
74
101
  /**
75
102
  * Buckets are collection of widget references.
76
- * You can put any widget registered gloabally into a bucket. Later on you can grab
103
+ * You can put any widget registered globally into a bucket. Later on you can grab
77
104
  * widgets from a certain bucket. This enables apps to structure their widgets when e.g.
78
105
  * using multiple widget grids with their own set of available widgets.
79
106
  */
@@ -102,7 +129,7 @@ class WidgetGridRegistry {
102
129
  * is not widget registered with the given name
103
130
  */
104
131
  getWidgetSetupComponent(widgetName) {
105
- const widget = this.registeredWidgets.find((w) => w.id === widgetName);
132
+ const widget = this.registeredWidgets.find((widget) => widget.id === widgetName);
106
133
  if (!widget)
107
134
  throw Error('Widget setup component not found');
108
135
  return widget.setupComponent;
@@ -115,11 +142,24 @@ class WidgetGridRegistry {
115
142
  * no component registered with the given name
116
143
  */
117
144
  getWidgetComponent(widgetName) {
118
- const widget = this.registeredWidgets.find((w) => w.id === widgetName);
145
+ const widget = this.registeredWidgets.find((widget) => widget.id === widgetName);
119
146
  if (!widget)
120
147
  console.error('Widget component not found');
121
148
  return widget?.widgetComponent || NoopComponent;
122
149
  }
150
+ /**
151
+ * Evaluates whether the widget with the given name is applicable for
152
+ * the current runtime context. Widgets without an `isApplicable`
153
+ * predicate are considered applicable. Unknown widgets are reported
154
+ * as applicable so the noop fallback can still render.
155
+ * @param widgetName The widgets name
156
+ */
157
+ isWidgetApplicable(widgetName) {
158
+ const widget = this.registeredWidgets.find((widget) => widget.id === widgetName);
159
+ if (!widget)
160
+ return true;
161
+ return widget.isApplicable?.() ?? true;
162
+ }
123
163
  /**
124
164
  * Adds a new widget to the list of registered widgets. That way
125
165
  * you can create custom widgets that are then available to be
@@ -129,12 +169,12 @@ class WidgetGridRegistry {
129
169
  * not exist it'll be created.
130
170
  */
131
171
  registerGridWidget(widget, buckets) {
132
- const existingWidget = this.registeredWidgets.find((w) => w.id === widget.id);
172
+ const existingWidget = this.registeredWidgets.find((item) => item.id === widget.id);
133
173
  if (!existingWidget)
134
174
  this.registeredWidgets.push(widget);
135
175
  if (buckets?.length) {
136
- buckets.forEach((b) => {
137
- this._addToBucket(b, widget.id);
176
+ buckets.forEach((bucket) => {
177
+ this._addToBucket(bucket, widget.id);
138
178
  });
139
179
  }
140
180
  }
@@ -145,24 +185,18 @@ class WidgetGridRegistry {
145
185
  * not exist it'll be created.
146
186
  */
147
187
  registerGridWidgets(widgets, buckets) {
148
- const alreadyRegisteredWidgetNames = this.registeredWidgets.map((w) => w.id);
149
- const widgetsToRegister = widgets.filter((w) => !alreadyRegisteredWidgetNames.includes(w.id));
150
- this.registeredWidgets = [...this.registeredWidgets, ...widgetsToRegister.map((w) => this.#translateLabel(w))];
188
+ const alreadyRegisteredWidgetNames = this.registeredWidgets.map((widget) => widget.id);
189
+ const widgetsToRegister = widgets.filter((widget) => !alreadyRegisteredWidgetNames.includes(widget.id));
190
+ this.registeredWidgets = [
191
+ ...this.registeredWidgets,
192
+ ...widgetsToRegister.map((widget) => this.#translateLabel(widget))
193
+ ];
151
194
  if (buckets?.length) {
152
- buckets.forEach((b) => widgets.forEach((w) => this._addToBucket(b, w.id)));
195
+ buckets.forEach((bucket) => widgets.forEach((widget) => this._addToBucket(bucket, widget.id)));
153
196
  }
154
197
  }
155
- #translateLabel(widget) {
156
- const translatedLabel = this.translate.instant(widget.label);
157
- return { ...widget, label: translatedLabel && !translatedLabel.startsWith('!missing') ? translatedLabel : widget.label };
158
- }
159
- _addToBucket(bucket, widgetName) {
160
- if (!this.widgetBuckets[bucket])
161
- this.widgetBuckets[bucket] = [];
162
- this.widgetBuckets[bucket].push(widgetName);
163
- }
164
198
  removeRegisteredWidget(id) {
165
- this.registeredWidgets = this.registeredWidgets.filter((w) => w.id !== id);
199
+ this.registeredWidgets = this.registeredWidgets.filter((widget) => widget.id !== id);
166
200
  }
167
201
  clearRegisteredWidget() {
168
202
  this.registeredWidgets = [];
@@ -175,35 +209,55 @@ class WidgetGridRegistry {
175
209
  * @returns Array of grid widgets
176
210
  */
177
211
  getRegisteredWidgets(buckets) {
212
+ const applicable = (widget) => widget.isApplicable?.() ?? true;
178
213
  if (buckets?.length) {
179
214
  // buckets may contain wildcards ...
180
215
  // grab all buckets matching the wildcards
181
216
  const matchedBuckets = {};
182
- buckets.forEach((b) => {
183
- if (b.match(/[*?]/)) {
217
+ buckets.forEach((bucket) => {
218
+ if (bucket.match(/[*?]/)) {
184
219
  // bucket has a pattern
185
- Object.keys(this.widgetBuckets).forEach((wb) => {
186
- if (this._wildcardMatch(wb, b))
187
- matchedBuckets[wb] = 0;
220
+ Object.keys(this.widgetBuckets).forEach((widgetBucket) => {
221
+ if (this._wildcardMatch(widgetBucket, bucket))
222
+ matchedBuckets[widgetBucket] = 0;
188
223
  });
189
224
  }
190
225
  else
191
- matchedBuckets[b] = 0;
226
+ matchedBuckets[bucket] = 0;
192
227
  });
193
228
  const bucketWidgetNames = {};
194
- Object.keys(matchedBuckets).forEach((b) => this.widgetBuckets[b].forEach((widgetName) => {
229
+ Object.keys(matchedBuckets).forEach((bucket) => this.widgetBuckets[bucket].forEach((widgetName) => {
195
230
  bucketWidgetNames[widgetName] = 0;
196
231
  }));
197
232
  const widgetNames = Object.keys(bucketWidgetNames);
198
- return this.registeredWidgets.filter((w) => widgetNames.includes(w.id)).map((w) => this.#translateLabel(w));
233
+ return this.registeredWidgets
234
+ .filter((gridWidget) => widgetNames.includes(gridWidget.id) && applicable(gridWidget))
235
+ .map((widget) => this.#translateLabel(widget));
199
236
  }
200
237
  else
201
- return this.registeredWidgets.map((w) => this.#translateLabel(w));
238
+ return this.registeredWidgets.filter(applicable).map((widget) => this.#translateLabel(widget));
202
239
  }
203
240
  _wildcardMatch(text, pattern) {
204
241
  const regexPattern = new RegExp('^' + pattern.replace(/\?/g, '.').replace(/\*/g, '.*') + '$');
205
242
  return regexPattern.test(text);
206
243
  }
244
+ _addToBucket(bucket, widgetName) {
245
+ if (!this.widgetBuckets[bucket])
246
+ this.widgetBuckets[bucket] = [];
247
+ this.widgetBuckets[bucket].push(widgetName);
248
+ }
249
+ #translateLabel(widget) {
250
+ return {
251
+ ...widget,
252
+ label: this.#tryTranslate(widget.label),
253
+ description: widget.description ? this.#tryTranslate(widget.description) : widget.description,
254
+ group: widget.group ? this.#tryTranslate(widget.group) : widget.group
255
+ };
256
+ }
257
+ #tryTranslate(value) {
258
+ const translated = this.translate.instant(value);
259
+ return translated && !translated.startsWith('!missing') ? translated : value;
260
+ }
207
261
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WidgetGridRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
208
262
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WidgetGridRegistry, providedIn: 'root' });
209
263
  }
@@ -214,48 +268,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
214
268
  }]
215
269
  }] });
216
270
 
217
- class WidgetGridUtils {
218
- static PREF_FUNCTION = 'Function';
219
- static PREF_RANGEVALUE = 'RangeValue';
220
- /**
221
- * Takes a `WidgetGridItemConfig` and stringifies it. The challenge
222
- * here is that callback functions should also be stringified.
223
- * @param gridItemConfig the config object
224
- */
225
- static gridConfigStringify(o) {
226
- return JSON.stringify(o, (key, value) => {
227
- if (typeof value === 'function') {
228
- return `/${WidgetGridUtils.PREF_FUNCTION}(${value.toString()})/`;
229
- }
230
- return value;
231
- });
232
- }
233
- /**
234
- * Takes a string stringified with `gridConfigStringify()` and parses it
235
- * to output a proper WidgetGridItemConfig.
236
- * @param stringifiedConfig stringified widget grid config
237
- */
238
- static gridConfigParse(stringifiedConfig) {
239
- return JSON.parse(stringifiedConfig, function (key, value) {
240
- if (typeof value === 'string' &&
241
- value.startsWith(`/${WidgetGridUtils.PREF_FUNCTION}(`) &&
242
- // value.startsWith('/Function(') &&
243
- value.endsWith(')/')) {
244
- value = value.substring(WidgetGridUtils.PREF_FUNCTION.length + 2, value.length - 2);
245
- return (0, eval)('(' + value + ')');
246
- }
247
- return value;
248
- });
249
- }
250
- static uuid() {
251
- return WidgetGridUtils._p8() + WidgetGridUtils._p8(true) + WidgetGridUtils._p8(true) + WidgetGridUtils._p8();
252
- }
253
- static _p8(s) {
254
- const p = (Math.random().toString(16) + '000000000').substr(2, 8);
255
- return s ? `-${p.substr(0, 4)}-${p.substr(4, 4)}` : p;
256
- }
257
- }
258
-
259
271
  /**
260
272
  * Service managing a widget grid. Also includes a set of labels that will be
261
273
  * used by the grid and its widgets. Default labels are provided out of the
@@ -265,45 +277,48 @@ class WidgetGridUtils {
265
277
  * consider using those labels as well.
266
278
  */
267
279
  class WidgetGridService {
268
- widgetGridRegistry;
269
- widgetGrid = [];
270
- widgetGridSource = new ReplaySubject();
271
- widgetGrid$ = this.widgetGridSource.asObservable();
272
- widgetGridUpdateSource = new Subject();
280
+ //#region Dependencies
281
+ #widgetGridRegistry = inject(WidgetGridRegistry);
282
+ //#endregion
283
+ //#region Class properties
284
+ #widgetGrid = [];
285
+ #widgetGridSource = new ReplaySubject();
286
+ widgetGrid$ = this.#widgetGridSource.asObservable();
287
+ #widgetGridUpdateSource = new Subject();
273
288
  /**
274
289
  * Emitted when the widget grid has been updated
275
290
  */
276
- widgetGridUpdate$ = this.widgetGridUpdateSource.asObservable();
291
+ widgetGridUpdate$ = this.#widgetGridUpdateSource.asObservable();
277
292
  addItemSize = {
278
293
  rows: 2,
279
- cols: 3,
294
+ cols: 3
280
295
  };
281
- constructor(widgetGridRegistry) {
282
- this.widgetGridRegistry = widgetGridRegistry;
283
- }
296
+ //#endregion
297
+ //#region Public methods
284
298
  setWidgetGrid(gridItemConfig) {
285
299
  if (gridItemConfig) {
286
300
  // create actual widget grid from widget grid config
287
- this.widgetGrid = [...gridItemConfig].map((gic) => {
288
- const cmp = this.widgetGridRegistry.getWidgetComponent(gic.widgetName);
289
- const gc = {
301
+ this.#widgetGrid = [...gridItemConfig].map((gic) => {
302
+ const cmp = this.#widgetGridRegistry.getWidgetComponent(gic.widgetName);
303
+ const gridItem = {
290
304
  ...gic,
291
305
  widget: cmp,
292
306
  widgetConfigMap: {
293
307
  widgetConfig: cmp === NoopComponent
294
308
  ? {
295
- widgetName: gic.widgetName,
309
+ widgetName: gic.widgetName
296
310
  }
297
- : gic.widgetConfig,
311
+ : gic.widgetConfig
298
312
  },
313
+ isApplicable: this.#widgetGridRegistry.isWidgetApplicable(gic.widgetName)
299
314
  };
300
- return gc;
315
+ return gridItem;
301
316
  });
302
317
  }
303
318
  else {
304
- this.widgetGrid = [];
319
+ this.#widgetGrid = [];
305
320
  }
306
- this.widgetGridSource.next(this.widgetGrid);
321
+ this.#widgetGridSource.next(this.#widgetGrid);
307
322
  }
308
323
  /**
309
324
  * Update config of an existing widget
@@ -311,29 +326,30 @@ class WidgetGridService {
311
326
  * @param setupWidgetConfig The updated configuration for that widget
312
327
  */
313
328
  updateWidget(widgetId, setupWidgetConfig) {
314
- const gridWidget = this.widgetGrid.find((w) => w.id === widgetId);
329
+ const gridWidget = this.#widgetGrid.find((widget) => widget.id === widgetId);
315
330
  if (gridWidget && setupWidgetConfig) {
316
331
  gridWidget.widgetConfig = setupWidgetConfig;
317
332
  gridWidget.widgetConfigMap = { widgetConfig: setupWidgetConfig };
318
333
  }
319
- this.widgetGridSource.next(this.widgetGrid);
320
- this.widgetGridUpdateSource.next(widgetId);
334
+ this.#widgetGridSource.next(this.#widgetGrid);
335
+ this.#widgetGridUpdateSource.next(widgetId);
321
336
  }
322
337
  replaceWidget(widgetId, widgetName, setupWidgetConfig) {
323
- const gridWidgetIndex = this.widgetGrid.findIndex((w) => w.id === widgetId);
338
+ const gridWidgetIndex = this.#widgetGrid.findIndex((widget) => widget.id === widgetId);
324
339
  if (gridWidgetIndex !== -1) {
325
- const w = {
326
- ...this.widgetGrid[gridWidgetIndex],
340
+ const widget = {
341
+ ...this.#widgetGrid[gridWidgetIndex],
327
342
  ...{
328
343
  widgetName,
329
344
  widgetConfig: setupWidgetConfig || {},
330
- widget: this.widgetGridRegistry.getWidgetComponent(widgetName),
345
+ widget: this.#widgetGridRegistry.getWidgetComponent(widgetName),
331
346
  widgetConfigMap: { widgetConfig: setupWidgetConfig || {} },
332
- },
347
+ isApplicable: this.#widgetGridRegistry.isWidgetApplicable(widgetName)
348
+ }
333
349
  };
334
- this.widgetGrid[gridWidgetIndex] = w;
335
- this.widgetGridSource.next(this.widgetGrid);
336
- this.widgetGridUpdateSource.next(widgetId);
350
+ this.#widgetGrid[gridWidgetIndex] = widget;
351
+ this.#widgetGridSource.next(this.#widgetGrid);
352
+ this.#widgetGridUpdateSource.next(widgetId);
337
353
  }
338
354
  }
339
355
  /**
@@ -346,101 +362,90 @@ class WidgetGridService {
346
362
  id: WidgetGridUtils.uuid(),
347
363
  widgetName,
348
364
  widgetConfig: setupWidgetConfig,
349
- widget: this.widgetGridRegistry.getWidgetComponent(widgetName),
365
+ widget: this.#widgetGridRegistry.getWidgetComponent(widgetName),
350
366
  widgetConfigMap: { widgetConfig: setupWidgetConfig },
367
+ isApplicable: this.#widgetGridRegistry.isWidgetApplicable(widgetName),
351
368
  x: 0,
352
369
  y: 0,
353
370
  rows: this.addItemSize.rows,
354
- cols: this.addItemSize.cols,
371
+ cols: this.addItemSize.cols
355
372
  };
356
- this.widgetGrid.push(gridWidget);
357
- this.widgetGridSource.next(this.widgetGrid);
358
- this.widgetGridUpdateSource.next(gridWidget.id);
373
+ this.#widgetGrid.push(gridWidget);
374
+ this.#widgetGridSource.next(this.#widgetGrid);
375
+ this.#widgetGridUpdateSource.next(gridWidget.id);
359
376
  }
360
377
  removeWidget(gridItemId) {
361
- const idx = this.widgetGrid.findIndex((w) => w.id === gridItemId);
362
- this.widgetGrid.splice(idx, 1);
363
- this.widgetGridSource.next(this.widgetGrid);
364
- this.widgetGridUpdateSource.next(gridItemId);
378
+ const idx = this.#widgetGrid.findIndex((widget) => widget.id === gridItemId);
379
+ this.#widgetGrid.splice(idx, 1);
380
+ this.#widgetGridSource.next(this.#widgetGrid);
381
+ this.#widgetGridUpdateSource.next(gridItemId);
365
382
  }
366
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WidgetGridService, deps: [{ token: WidgetGridRegistry }], target: i0.ɵɵFactoryTarget.Injectable });
383
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WidgetGridService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
367
384
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WidgetGridService });
368
385
  }
369
386
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WidgetGridService, decorators: [{
370
387
  type: Injectable
371
- }], ctorParameters: () => [{ type: WidgetGridRegistry }] });
388
+ }] });
372
389
 
373
390
  class WidgetPickerComponent {
391
+ //#region Dependencies
374
392
  #widgetGridRegistry = inject(WidgetGridRegistry);
375
393
  #widgetGridService = inject(WidgetGridService);
394
+ translate = inject(TranslateService);
376
395
  #dialogData = inject(MAT_DIALOG_DATA);
377
396
  #dialogRef = inject((MatDialogRef));
378
- pickerFormControl = new FormControl();
397
+ //#endregion
398
+ //#region Angular stuff
399
+ pickerData = input(this.#dialogData.pickerData, ...(ngDevMode ? [{ debugName: "pickerData" }] : /* istanbul ignore next */ []));
400
+ /**
401
+ * Collection of buckets to load available widgets from. Wildcards are also posssible:
402
+ * `[buckets]="['app.default', '*.public.*', 'app.no?.widgets']"`
403
+ *
404
+ * `*` represents any character 0-n times
405
+ * `?` represents exactly one character
406
+ */
407
+ buckets = input(this.#dialogData.buckets, ...(ngDevMode ? [{ debugName: "buckets" }] : /* istanbul ignore next */ []));
408
+ picked = output();
409
+ canceled = output();
410
+ //#endregion
411
+ //#region Properties
412
+ #DEFAULT_GROUP_KEY = 'yuv.widget-grid.widget-picker.group.default';
379
413
  widgetId;
380
414
  selectedWidget;
415
+ // ADR: InputsType (not AttributesMap) - bound via ndcDynamicInputs in the template,
416
+ // so values are component inputs (unknown), not HTML attribute strings.
381
417
  widgetConfigMap = {};
382
418
  setupWidgetConfig;
383
419
  widgetConfigDirty = false;
384
420
  widgetConfigState;
385
421
  registeredWidgets = [];
422
+ groupedWidgets = [];
386
423
  setupWidgetDynamicOutputs = {
387
424
  widgetConfigChange: (widgetConfig) => {
388
- this.onSetupConfigChange(widgetConfig);
425
+ this.#handleSetupConfigChange(widgetConfig);
389
426
  },
390
- widgetConfigStateChange: (s) => {
391
- this.widgetConfigState = s || undefined;
427
+ widgetConfigStateChange: (widgetConfigState) => {
428
+ this.widgetConfigState = widgetConfigState;
392
429
  }
393
430
  };
394
- pickerData = input(this.#dialogData.pickerData, ...(ngDevMode ? [{ debugName: "pickerData" }] : /* istanbul ignore next */ []));
395
- #pickerDataEffect = effect(() => {
396
- const pd = this.pickerData();
397
- if (pd) {
398
- const gridWidget = this.#widgetGridRegistry
399
- .getRegisteredWidgets()
400
- .find((w) => w.id === pd.widgetName);
401
- this.selectedWidget = gridWidget?.setupComponent ? gridWidget : undefined;
402
- this.widgetConfigMap = pd.widgetConfigMap || {};
403
- this.widgetId = pd.widgetId;
404
- }
405
- }, ...(ngDevMode ? [{ debugName: "#pickerDataEffect" }] : /* istanbul ignore next */ []));
406
- /**
407
- * Collection of buckets to load available widgets from. Wildcards are also posssible:
408
- * `[buckets]="['app.default', '*.public.*', 'app.no?.widgets']"`
409
- *
410
- * `*` represents any character 0-n times
411
- * `?` represents exactly one character
412
- */
413
- buckets = input(this.#dialogData.buckets, ...(ngDevMode ? [{ debugName: "buckets" }] : /* istanbul ignore next */ []));
414
- picked = output();
415
- canceled = output();
431
+ #expandedGroups = signal(new Set(), ...(ngDevMode ? [{ debugName: "#expandedGroups" }] : /* istanbul ignore next */ []));
432
+ expandedGroups = this.#expandedGroups.asReadonly();
433
+ //#endregion
416
434
  constructor() {
417
- this.pickerFormControl.valueChanges.pipe(takeUntilDestroyed()).subscribe((widgets) => {
418
- if (widgets.length)
419
- this.pick(widgets[0]);
420
- });
435
+ effect(this.#pickerDataEffect);
421
436
  }
422
- pick(widget) {
423
- if (widget?.setupComponent) {
424
- // if the selected widget has a setup component, we'll continue ...
425
- this.selectedWidget = widget?.setupComponent ? widget : undefined;
426
- }
427
- else {
428
- if (this.widgetId) {
429
- // update existing widget
430
- this.#widgetGridService.replaceWidget(this.widgetId, widget.id);
431
- }
432
- else {
433
- this.#widgetGridService.addWidget(widget.id);
434
- }
435
- this.picked.emit();
436
- this.#dialogRef.close();
437
- }
437
+ //#region Lifecycle hooks
438
+ ngOnInit() {
439
+ this.#initRegisteredWidgets();
440
+ this.#initGroupedWidgets();
438
441
  }
439
- onSetupConfigChange(setupConfig) {
440
- this.widgetConfigDirty = !!setupConfig;
441
- this.setupWidgetConfig = setupConfig;
442
+ //#endregion
443
+ //#region UI Methods
444
+ onSetupComponentCancel() {
445
+ this.selectedWidget = undefined;
446
+ this.setupWidgetConfig = undefined;
442
447
  }
443
- setupComponentSave() {
448
+ onSetupComponentSave() {
444
449
  // TODO: save/emit config
445
450
  if (this.widgetId && this.selectedWidget) {
446
451
  if (this.pickerData()?.widgetName !== this.selectedWidget.id) {
@@ -451,27 +456,86 @@ class WidgetPickerComponent {
451
456
  }
452
457
  }
453
458
  else {
459
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
454
460
  this.#widgetGridService.addWidget(this.selectedWidget.id, this.setupWidgetConfig);
455
461
  }
456
462
  this.picked.emit();
457
463
  this.#dialogRef.close();
458
464
  }
459
- setupComponentCancel() {
460
- this.reset();
461
- }
462
- cancel() {
465
+ onCancel() {
463
466
  this.canceled.emit();
464
467
  this.#dialogRef.close();
465
468
  }
466
- reset() {
467
- this.selectedWidget = undefined;
468
- this.setupWidgetConfig = undefined;
469
+ onToggleGroup(group) {
470
+ const next = new Set(this.#expandedGroups());
471
+ if (next.has(group))
472
+ next.delete(group);
473
+ else
474
+ next.add(group);
475
+ this.#expandedGroups.set(next);
469
476
  }
470
- ngOnInit() {
477
+ onPick(widget) {
478
+ if (widget?.setupComponent) {
479
+ // if the selected widget has a setup component, we'll continue ...
480
+ this.selectedWidget = widget?.setupComponent ? widget : undefined;
481
+ }
482
+ else {
483
+ if (this.widgetId) {
484
+ // update existing widget
485
+ this.#widgetGridService.replaceWidget(this.widgetId, widget.id);
486
+ }
487
+ else {
488
+ this.#widgetGridService.addWidget(widget.id);
489
+ }
490
+ this.picked.emit();
491
+ this.#dialogRef.close();
492
+ }
493
+ }
494
+ //#endregion
495
+ //#region Init
496
+ #initRegisteredWidgets() {
471
497
  this.registeredWidgets = this.#widgetGridRegistry.getRegisteredWidgets(this.buckets());
472
498
  }
499
+ #initGroupedWidgets() {
500
+ this.groupedWidgets = this.#groupWidgets(this.registeredWidgets);
501
+ }
502
+ //#endregion
503
+ //#region Utilities
504
+ #handleSetupConfigChange(setupConfig) {
505
+ this.widgetConfigDirty = !!setupConfig;
506
+ this.setupWidgetConfig = setupConfig;
507
+ }
508
+ #groupWidgets(widgets) {
509
+ const defaultGroupLabel = this.translate.instant(this.#DEFAULT_GROUP_KEY);
510
+ const order = [];
511
+ const buckets = new Map();
512
+ widgets.forEach((widget) => {
513
+ const key = widget.group || defaultGroupLabel;
514
+ let bucket = buckets.get(key);
515
+ if (!bucket) {
516
+ bucket = [];
517
+ buckets.set(key, bucket);
518
+ order.push(key);
519
+ }
520
+ bucket.push(widget);
521
+ });
522
+ return order.map((group) => ({ group, widgets: buckets.get(group) ?? [] }));
523
+ }
524
+ //#endregion
525
+ //#region Effects
526
+ #pickerDataEffect = () => {
527
+ const pickerData = this.pickerData();
528
+ if (pickerData) {
529
+ const gridWidget = this.#widgetGridRegistry
530
+ .getRegisteredWidgets()
531
+ .find((widget) => widget.id === pickerData.widgetName);
532
+ this.selectedWidget = gridWidget?.setupComponent ? gridWidget : undefined;
533
+ this.widgetConfigMap = pickerData.widgetConfigMap || {};
534
+ this.widgetId = pickerData.widgetId;
535
+ }
536
+ };
473
537
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WidgetPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
474
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: WidgetPickerComponent, isStandalone: true, selector: "yuv-widget-picker", inputs: { pickerData: { classPropertyName: "pickerData", publicName: "pickerData", isSignal: true, isRequired: false, transformFunction: null }, buckets: { classPropertyName: "buckets", publicName: "buckets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { picked: "picked", canceled: "canceled" }, ngImport: i0, template: "<yuv-dialog [headertitel]=\"selectedWidget ? selectedWidget.label : ('yuv.widget-grid.widget-picker.title' | translate)\">\n <!-- <header class=\"dark\" [ngClass]=\"{ listing: !selectedWidget }\">\n @if (selectedWidget) {\n <button mat-icon-button (click)=\"reset()\"><mat-icon>back</mat-icon></button>\n }\n <div class=\"title\">{{ selectedWidget ? selectedWidget.label : ('yuv.widget-grid.widget-picker.title' | translate) }}</div>\n <button mat-icon-button (click)=\"cancel()\"><mat-icon>clear</mat-icon></button>\n </header> -->\n\n <main class=\"{{ !selectedWidget ? 'widget-listing' : 'widget-setup' }}\">\n @if (!selectedWidget) {\n <mat-selection-list role=\"list\" [formControl]=\"pickerFormControl\" [multiple]=\"false\" [hideSingleSelectionIndicator]=\"true\">\n <!-- list of avalable widgets -->\n @for (w of registeredWidgets; track $index) {\n <mat-list-option [value]=\"w\">\n {{ w.label }}\n </mat-list-option>\n } @empty {\n <div class=\"empty\">\n {{ 'yuv.widget-grid.widget-picker.empty' | translate }}\n </div>\n }\n </mat-selection-list>\n } @else {\n <div class=\"component\">\n <ng-container\n *ngComponentOutlet=\"selectedWidget!.setupComponent!; ndcDynamicInputs: widgetConfigMap; ndcDynamicOutputs: setupWidgetDynamicOutputs\"\n ></ng-container>\n </div>\n }\n </main>\n <footer>\n @if (selectedWidget) {\n <button ymtButton=\"secondary\" (click)=\"setupComponentCancel()\">{{ 'yuv.widget-grid.button.cancel' | translate }}</button>\n <button ymtButton=\"primary\" [disabled]=\"!widgetConfigDirty || widgetConfigState === 'INVALID'\" (click)=\"setupComponentSave()\">\n {{ 'yuv.widget-grid.button.save' | translate }}\n </button>\n } @else {\n <button ymtButton=\"secondary\" (click)=\"cancel()\">{{ 'yuv.widget-grid.button.cancel' | translate }}</button>\n }\n </footer>\n</yuv-dialog>\n", styles: [":host .empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%}:host main.widget-setup{height:100%;display:grid;grid-template-rows:1fr auto;grid-template-columns:1fr;grid-template-areas:\"component\" \"buttons\"}:host main.widget-listing .widget{padding:var(--app-pane-padding);border-bottom:1px solid var(--panel-divider-color);cursor:pointer}:host main.widget-listing .widget:hover{background-color:var(--item-focus-background-color)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: DialogComponent, selector: "yuv-dialog", inputs: ["headertitle", "headertitel"] }, { kind: "directive", type: YmtButtonDirective, selector: "button[ymtButton], a[ymtButton]", inputs: ["ymtButton", "disabled", "aria-disabled", "disableRipple", "disabledInteractive", "button-size"] }, { kind: "ngmodule", type: MatListModule }, { kind: "component", type: i2.MatSelectionList, selector: "mat-selection-list", inputs: ["color", "compareWith", "multiple", "hideSingleSelectionIndicator", "disabled"], outputs: ["selectionChange"], exportAs: ["matSelectionList"] }, { kind: "component", type: i2.MatListOption, selector: "mat-list-option", inputs: ["togglePosition", "checkboxPosition", "color", "value", "selected"], outputs: ["selectedChange"], exportAs: ["matListOption"] }, { kind: "ngmodule", type: DynamicIoModule }, { kind: "directive", type: i3.ComponentOutletInjectorDirective, selector: "[ngComponentOutlet]", exportAs: ["ndcComponentOutletInjector"] }, { kind: "directive", type: i3.ComponentOutletIoDirective, selector: "[ngComponentOutletNdcDynamicInputs],[ngComponentOutletNdcDynamicOutputs]", inputs: ["ngComponentOutletNdcDynamicInputs", "ngComponentOutletNdcDynamicOutputs"], exportAs: ["ndcDynamicIo"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: SignalComponentIoModule }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
538
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: WidgetPickerComponent, isStandalone: true, selector: "yuv-widget-picker", inputs: { pickerData: { classPropertyName: "pickerData", publicName: "pickerData", isSignal: true, isRequired: false, transformFunction: null }, buckets: { classPropertyName: "buckets", publicName: "buckets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { picked: "picked", canceled: "canceled" }, ngImport: i0, template: "<yuv-dialog [headertitle]=\"selectedWidget ? selectedWidget.label : ('yuv.widget-grid.widget-picker.title' | translate)\">\n <!-- <header class=\"dark\" [ngClass]=\"{ listing: !selectedWidget }\">\n @if (selectedWidget) {\n <button mat-icon-button (click)=\"reset()\"><mat-icon>back</mat-icon></button>\n }\n <div class=\"title\">{{ selectedWidget ? selectedWidget.label : ('yuv.widget-grid.widget-picker.title' | translate) }}</div>\n <button mat-icon-button (click)=\"cancel()\"><mat-icon>clear</mat-icon></button>\n </header> -->\n\n <main [class]=\"!selectedWidget ? 'widget-listing' : 'widget-setup'\">\n @if (!selectedWidget) {\n <ul class=\"groups\" role=\"list\">\n @for (g of groupedWidgets; track g.group) {\n @let expanded = expandedGroups().has(g.group);\n <li class=\"group\" [class.group--expanded]=\"expanded\">\n <button type=\"button\" class=\"group-header\" [attr.aria-expanded]=\"expanded\" (click)=\"onToggleGroup(g.group)\">\n <mat-icon class=\"group-chevron ymt-icon--size-s\">\n {{ expanded ? 'expand_more' : 'chevron_right' }}\n </mat-icon>\n <span class=\"group-label\">{{ g.group }}</span>\n <span class=\"group-count\">{{ g.widgets.length }}</span>\n </button>\n @if (expanded) {\n <ul class=\"widgets\" role=\"list\">\n @for (w of g.widgets; track w.id) {\n <li>\n <button type=\"button\" class=\"widget-item\" (click)=\"onPick(w)\">\n <span class=\"widget-title\">{{ w.label }}</span>\n @if (w.description) {\n <span class=\"widget-description\">{{ w.description }}</span>\n }\n </button>\n </li>\n }\n </ul>\n }\n </li>\n } @empty {\n <div class=\"empty\">\n {{ 'yuv.widget-grid.widget-picker.empty' | translate }}\n </div>\n }\n </ul>\n } @else {\n <div class=\"component\">\n <ng-container\n *ngComponentOutlet=\"\n selectedWidget!.setupComponent!;\n ndcDynamicInputs: widgetConfigMap;\n ndcDynamicOutputs: setupWidgetDynamicOutputs\n \"\n />\n </div>\n }\n </main>\n <footer>\n @if (selectedWidget) {\n <button ymtButton=\"secondary\" (click)=\"onSetupComponentCancel()\">\n {{ 'yuv.widget-grid.button.cancel' | translate }}\n </button>\n <button\n ymtButton=\"primary\"\n [disabled]=\"!widgetConfigDirty || widgetConfigState === 'INVALID'\"\n (click)=\"onSetupComponentSave()\"\n >\n {{ 'yuv.widget-grid.button.save' | translate }}\n </button>\n } @else {\n <button ymtButton=\"secondary\" (click)=\"onCancel()\">{{ 'yuv.widget-grid.button.cancel' | translate }}</button>\n }\n </footer>\n</yuv-dialog>\n", styles: [":host .empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%}:host main.widget-setup{height:100%;display:grid;grid-template-rows:1fr auto;grid-template-columns:1fr;grid-template-areas:\"component\" \"buttons\"}:host main.widget-listing .groups{list-style:none;margin:0;padding:0}:host main.widget-listing .group{border-bottom:1px solid var(--panel-divider-color)}:host main.widget-listing .group-header{display:flex;align-items:center;gap:var(--ymt-spacing-s, 8px);width:100%;padding:var(--app-pane-padding, 12px);background:transparent;border:0;cursor:pointer;color:inherit;font:inherit;text-align:start}:host main.widget-listing .group-header:hover{background-color:var(--item-focus-background-color)}:host main.widget-listing .group-header .group-label{flex:1;font-weight:500}:host main.widget-listing .group-header .group-count{opacity:.6;font-size:.85em}:host main.widget-listing .widgets{list-style:none;margin:0;padding:0 0 var(--ymt-spacing-xs, 4px)}:host main.widget-listing .widget-item{display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:100%;padding:var(--ymt-spacing-s, 8px) calc(var(--app-pane-padding, 12px) + 24px);background:transparent;border:0;cursor:pointer;color:inherit;font:inherit;text-align:start}:host main.widget-listing .widget-item:hover{background-color:var(--item-focus-background-color)}:host main.widget-listing .widget-title{font-weight:400}:host main.widget-listing .widget-description{color:var(--ymt-text-color-subtle);font-size:.85em;white-space:normal}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: DialogComponent, selector: "yuv-dialog", inputs: ["headertitle", "headertitel"] }, { kind: "directive", type: YmtButtonDirective, selector: "button[ymtButton], a[ymtButton]", inputs: ["ymtButton", "disabled", "aria-disabled", "disableRipple", "disabledInteractive", "button-size"] }, { kind: "ngmodule", type: DynamicIoModule }, { kind: "directive", type: i3$1.ComponentOutletInjectorDirective, selector: "[ngComponentOutlet]", exportAs: ["ndcComponentOutletInjector"] }, { kind: "directive", type: i3$1.ComponentOutletIoDirective, selector: "[ngComponentOutletNdcDynamicInputs],[ngComponentOutletNdcDynamicOutputs]", inputs: ["ngComponentOutletNdcDynamicInputs", "ngComponentOutletNdcDynamicOutputs"], exportAs: ["ndcDynamicIo"] }, { kind: "ngmodule", type: SignalComponentIoModule }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
475
539
  }
476
540
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WidgetPickerComponent, decorators: [{
477
541
  type: Component,
@@ -482,62 +546,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
482
546
  MatIconModule,
483
547
  DialogComponent,
484
548
  YmtButtonDirective,
485
- MatListModule,
486
549
  DynamicIoModule,
487
- ReactiveFormsModule,
488
550
  SignalComponentIoModule
489
- ], template: "<yuv-dialog [headertitel]=\"selectedWidget ? selectedWidget.label : ('yuv.widget-grid.widget-picker.title' | translate)\">\n <!-- <header class=\"dark\" [ngClass]=\"{ listing: !selectedWidget }\">\n @if (selectedWidget) {\n <button mat-icon-button (click)=\"reset()\"><mat-icon>back</mat-icon></button>\n }\n <div class=\"title\">{{ selectedWidget ? selectedWidget.label : ('yuv.widget-grid.widget-picker.title' | translate) }}</div>\n <button mat-icon-button (click)=\"cancel()\"><mat-icon>clear</mat-icon></button>\n </header> -->\n\n <main class=\"{{ !selectedWidget ? 'widget-listing' : 'widget-setup' }}\">\n @if (!selectedWidget) {\n <mat-selection-list role=\"list\" [formControl]=\"pickerFormControl\" [multiple]=\"false\" [hideSingleSelectionIndicator]=\"true\">\n <!-- list of avalable widgets -->\n @for (w of registeredWidgets; track $index) {\n <mat-list-option [value]=\"w\">\n {{ w.label }}\n </mat-list-option>\n } @empty {\n <div class=\"empty\">\n {{ 'yuv.widget-grid.widget-picker.empty' | translate }}\n </div>\n }\n </mat-selection-list>\n } @else {\n <div class=\"component\">\n <ng-container\n *ngComponentOutlet=\"selectedWidget!.setupComponent!; ndcDynamicInputs: widgetConfigMap; ndcDynamicOutputs: setupWidgetDynamicOutputs\"\n ></ng-container>\n </div>\n }\n </main>\n <footer>\n @if (selectedWidget) {\n <button ymtButton=\"secondary\" (click)=\"setupComponentCancel()\">{{ 'yuv.widget-grid.button.cancel' | translate }}</button>\n <button ymtButton=\"primary\" [disabled]=\"!widgetConfigDirty || widgetConfigState === 'INVALID'\" (click)=\"setupComponentSave()\">\n {{ 'yuv.widget-grid.button.save' | translate }}\n </button>\n } @else {\n <button ymtButton=\"secondary\" (click)=\"cancel()\">{{ 'yuv.widget-grid.button.cancel' | translate }}</button>\n }\n </footer>\n</yuv-dialog>\n", styles: [":host .empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%}:host main.widget-setup{height:100%;display:grid;grid-template-rows:1fr auto;grid-template-columns:1fr;grid-template-areas:\"component\" \"buttons\"}:host main.widget-listing .widget{padding:var(--app-pane-padding);border-bottom:1px solid var(--panel-divider-color);cursor:pointer}:host main.widget-listing .widget:hover{background-color:var(--item-focus-background-color)}\n"] }]
551
+ ], template: "<yuv-dialog [headertitle]=\"selectedWidget ? selectedWidget.label : ('yuv.widget-grid.widget-picker.title' | translate)\">\n <!-- <header class=\"dark\" [ngClass]=\"{ listing: !selectedWidget }\">\n @if (selectedWidget) {\n <button mat-icon-button (click)=\"reset()\"><mat-icon>back</mat-icon></button>\n }\n <div class=\"title\">{{ selectedWidget ? selectedWidget.label : ('yuv.widget-grid.widget-picker.title' | translate) }}</div>\n <button mat-icon-button (click)=\"cancel()\"><mat-icon>clear</mat-icon></button>\n </header> -->\n\n <main [class]=\"!selectedWidget ? 'widget-listing' : 'widget-setup'\">\n @if (!selectedWidget) {\n <ul class=\"groups\" role=\"list\">\n @for (g of groupedWidgets; track g.group) {\n @let expanded = expandedGroups().has(g.group);\n <li class=\"group\" [class.group--expanded]=\"expanded\">\n <button type=\"button\" class=\"group-header\" [attr.aria-expanded]=\"expanded\" (click)=\"onToggleGroup(g.group)\">\n <mat-icon class=\"group-chevron ymt-icon--size-s\">\n {{ expanded ? 'expand_more' : 'chevron_right' }}\n </mat-icon>\n <span class=\"group-label\">{{ g.group }}</span>\n <span class=\"group-count\">{{ g.widgets.length }}</span>\n </button>\n @if (expanded) {\n <ul class=\"widgets\" role=\"list\">\n @for (w of g.widgets; track w.id) {\n <li>\n <button type=\"button\" class=\"widget-item\" (click)=\"onPick(w)\">\n <span class=\"widget-title\">{{ w.label }}</span>\n @if (w.description) {\n <span class=\"widget-description\">{{ w.description }}</span>\n }\n </button>\n </li>\n }\n </ul>\n }\n </li>\n } @empty {\n <div class=\"empty\">\n {{ 'yuv.widget-grid.widget-picker.empty' | translate }}\n </div>\n }\n </ul>\n } @else {\n <div class=\"component\">\n <ng-container\n *ngComponentOutlet=\"\n selectedWidget!.setupComponent!;\n ndcDynamicInputs: widgetConfigMap;\n ndcDynamicOutputs: setupWidgetDynamicOutputs\n \"\n />\n </div>\n }\n </main>\n <footer>\n @if (selectedWidget) {\n <button ymtButton=\"secondary\" (click)=\"onSetupComponentCancel()\">\n {{ 'yuv.widget-grid.button.cancel' | translate }}\n </button>\n <button\n ymtButton=\"primary\"\n [disabled]=\"!widgetConfigDirty || widgetConfigState === 'INVALID'\"\n (click)=\"onSetupComponentSave()\"\n >\n {{ 'yuv.widget-grid.button.save' | translate }}\n </button>\n } @else {\n <button ymtButton=\"secondary\" (click)=\"onCancel()\">{{ 'yuv.widget-grid.button.cancel' | translate }}</button>\n }\n </footer>\n</yuv-dialog>\n", styles: [":host .empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%}:host main.widget-setup{height:100%;display:grid;grid-template-rows:1fr auto;grid-template-columns:1fr;grid-template-areas:\"component\" \"buttons\"}:host main.widget-listing .groups{list-style:none;margin:0;padding:0}:host main.widget-listing .group{border-bottom:1px solid var(--panel-divider-color)}:host main.widget-listing .group-header{display:flex;align-items:center;gap:var(--ymt-spacing-s, 8px);width:100%;padding:var(--app-pane-padding, 12px);background:transparent;border:0;cursor:pointer;color:inherit;font:inherit;text-align:start}:host main.widget-listing .group-header:hover{background-color:var(--item-focus-background-color)}:host main.widget-listing .group-header .group-label{flex:1;font-weight:500}:host main.widget-listing .group-header .group-count{opacity:.6;font-size:.85em}:host main.widget-listing .widgets{list-style:none;margin:0;padding:0 0 var(--ymt-spacing-xs, 4px)}:host main.widget-listing .widget-item{display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:100%;padding:var(--ymt-spacing-s, 8px) calc(var(--app-pane-padding, 12px) + 24px);background:transparent;border:0;cursor:pointer;color:inherit;font:inherit;text-align:start}:host main.widget-listing .widget-item:hover{background-color:var(--item-focus-background-color)}:host main.widget-listing .widget-title{font-weight:400}:host main.widget-listing .widget-description{color:var(--ymt-text-color-subtle);font-size:.85em;white-space:normal}\n"] }]
490
552
  }], ctorParameters: () => [], propDecorators: { pickerData: [{ type: i0.Input, args: [{ isSignal: true, alias: "pickerData", required: false }] }], buckets: [{ type: i0.Input, args: [{ isSignal: true, alias: "buckets", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }], canceled: [{ type: i0.Output, args: ["canceled"] }] } });
491
553
 
554
+ class WidgetGridEventService {
555
+ //#region Properties
556
+ #widgetGridEventSource = new ReplaySubject();
557
+ widgetEvents$ = this.#widgetGridEventSource.asObservable();
558
+ //#endregion
559
+ //#region Public methods
560
+ trigger(evt) {
561
+ this.#widgetGridEventSource.next(evt);
562
+ }
563
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WidgetGridEventService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
564
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WidgetGridEventService, providedIn: 'root' });
565
+ }
566
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WidgetGridEventService, decorators: [{
567
+ type: Injectable,
568
+ args: [{
569
+ providedIn: 'root'
570
+ }]
571
+ }] });
572
+
492
573
  class YuvWidgetGridComponent {
574
+ //#region Dependencies
493
575
  #dialog = inject(MatDialog);
494
576
  #widgetGridService = inject(WidgetGridService);
495
577
  #widgetGridEventService = inject(WidgetGridEventService);
578
+ #destroyRef = inject(DestroyRef);
579
+ //#endregion
580
+ //#region Angular stuff
496
581
  gridster = viewChild(Gridster, ...(ngDevMode ? [{ debugName: "gridster" }] : /* istanbul ignore next */ []));
497
582
  widgetPicker = viewChild.required('widgetPicker', { read: TemplateRef });
498
- options = {
499
- gridType: GridType.Fit,
500
- displayGrid: DisplayGrid.None,
501
- pushItems: false,
502
- outerMargin: false,
503
- swap: false,
504
- draggable: {
505
- enabled: false,
506
- ignoreContent: true,
507
- dragHandleClass: 'dragHandle'
508
- },
509
- resizable: {
510
- enabled: false
511
- },
512
- itemChangeCallback: (item, itemComponent) => {
513
- this.emitChange();
514
- }
515
- };
516
583
  gridConfig = input(...(ngDevMode ? [undefined, { debugName: "gridConfig" }] : /* istanbul ignore next */ []));
517
- #gridConfigEffect = effect(() => {
518
- const cfg = this.gridConfig();
519
- if (cfg?.rows) {
520
- this.options.minRows = cfg.rows;
521
- this.options.maxRows = cfg.rows;
522
- }
523
- if (cfg?.columns) {
524
- this.options.minCols = cfg.columns;
525
- this.options.maxCols = cfg.columns;
526
- }
527
- if (cfg?.gap) {
528
- this.options.margin = cfg.gap;
529
- }
530
- if (cfg?.newItemWidth || cfg?.newItemHeight) {
531
- this.#widgetGridService.addItemSize = {
532
- cols: cfg?.newItemWidth || -1, // default value -1 means the grid will autoposition the new item
533
- rows: cfg?.newItemHeight || -1 // default value -1 means the grid will autoposition the new item
534
- };
535
- }
536
- if (cfg?.gridType) {
537
- this.options.gridType = cfg.gridType;
538
- }
539
- this.gridster()?.api.calculateLayout();
540
- }, ...(ngDevMode ? [{ debugName: "#gridConfigEffect" }] : /* istanbul ignore next */ []));
541
584
  _editMode = signal(false, ...(ngDevMode ? [{ debugName: "_editMode" }] : /* istanbul ignore next */ []));
542
585
  /**
543
586
  * Whether or not to enable edit mode. In edit mode controls
@@ -545,23 +588,9 @@ class YuvWidgetGridComponent {
545
588
  * This mode also enables positioning and resizing of the tiles.
546
589
  */
547
590
  editMode = input(false, ...(ngDevMode ? [{ debugName: "editMode" }] : /* istanbul ignore next */ []));
548
- #editModeEffect = effect(() => {
549
- const e = this.editMode();
550
- this._editMode.set(e);
551
- this.options.draggable.enabled = !!e;
552
- this.options.resizable.enabled = !!e;
553
- this.options.displayGrid = e ? DisplayGrid.Always : DisplayGrid.None;
554
- // set timeout to ensure that the grid has been redrawn before notifying the widgets about the change
555
- setTimeout(() => {
556
- this.gridster()?.api.calculateLayout();
557
- }, 0);
558
- }, ...(ngDevMode ? [{ debugName: "#editModeEffect" }] : /* istanbul ignore next */ []));
559
591
  gridItemConfig = input(undefined, ...(ngDevMode ? [{ debugName: "gridItemConfig" }] : /* istanbul ignore next */ []));
560
- #gridItemConfigEffect = effect(() => {
561
- this.#widgetGridService.setWidgetGrid(this.gridItemConfig());
562
- }, ...(ngDevMode ? [{ debugName: "#gridItemConfigEffect" }] : /* istanbul ignore next */ []));
563
592
  /**
564
- * Collection of buckets to load available widgets from. Wildcards are also posssible:
593
+ * Collection of buckets to load available widgets from. Wildcards are also possible:
565
594
  * `[buckets]="['app.default', '*.public.*', 'app.no?.widgets']"`
566
595
  *
567
596
  * `*` represents any character 0-n times
@@ -578,20 +607,48 @@ class YuvWidgetGridComponent {
578
607
  * Emitted when the widget picker is opened or closed in edit mode
579
608
  */
580
609
  widgetPickerOpen = output();
610
+ //#endregion
611
+ //#region Class properties
612
+ /**
613
+ * Held as a signal so gridster's `options` InputSignal sees a new reference
614
+ * whenever edit mode or grid config changes. Mutating the inner object would
615
+ * keep the same reference and gridster's `$options` computed would never
616
+ * recompute — leaving draggable/resizable stuck at their initial `false`.
617
+ */
618
+ options = signal({
619
+ gridType: GridType.Fit,
620
+ displayGrid: DisplayGrid.None,
621
+ pushItems: false,
622
+ outerMargin: false,
623
+ swap: false,
624
+ draggable: {
625
+ enabled: false,
626
+ ignoreContent: true,
627
+ dragHandleClass: 'dragHandle'
628
+ },
629
+ resizable: {
630
+ enabled: false
631
+ },
632
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
633
+ itemChangeCallback: (item, itemComponent) => {
634
+ this.#emitChange();
635
+ }
636
+ }, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
581
637
  widgetGrid = [];
582
638
  widgetPickerData;
639
+ //#endregion
583
640
  constructor() {
584
- this.#widgetGridService.widgetGrid$.pipe(takeUntilDestroyed()).subscribe((widgetGrid) => {
585
- this.widgetGrid = widgetGrid;
586
- });
587
- this.#widgetGridEventService.widgetEvents$.pipe(takeUntilDestroyed()).subscribe((evt) => {
588
- this.gridItemEvent.emit(evt);
589
- });
590
- this.#widgetGridService.widgetGridUpdate$.pipe(takeUntilDestroyed()).subscribe((_) => {
591
- this.emitChange();
592
- });
641
+ effect(this.#gridItemConfigEffect);
642
+ effect(this.#editModeEffect);
643
+ effect(this.#gridConfigEffect);
593
644
  }
594
- openWidgetPicker(item) {
645
+ //#region Lifecycle Hooks
646
+ ngOnInit() {
647
+ this.#respondToWidgetServiceEvents();
648
+ }
649
+ //#endregion
650
+ // #region UI Methods
651
+ onOpenWidgetPicker(item) {
595
652
  if (item) {
596
653
  // open setup component for selected grid item
597
654
  this.widgetPickerData = {
@@ -618,27 +675,98 @@ class YuvWidgetGridComponent {
618
675
  * Removes a widget from the grid
619
676
  * @param item The widget to be removed
620
677
  */
621
- removeItem(item) {
678
+ onRemoveItem(item) {
622
679
  this.#widgetGridService.removeWidget(item.id);
623
680
  }
624
681
  /**
625
682
  * Add a new widget to the grid by opening the widget picker
626
683
  */
627
- addItem() {
628
- this.openWidgetPicker();
684
+ onAddItem() {
685
+ this.onOpenWidgetPicker();
629
686
  }
630
- emitChange() {
687
+ //#endregion
688
+ //#region Utilities
689
+ #emitChange() {
631
690
  const mapped = [];
632
- this.widgetGrid.forEach((i) => {
633
- const m = { ...i };
634
- delete m['widget'];
635
- delete m['widgetConfigMap'];
636
- mapped.push(m);
691
+ this.widgetGrid.forEach((item) => {
692
+ const mappedWidget = { ...item };
693
+ delete mappedWidget['widget'];
694
+ delete mappedWidget['widgetConfigMap'];
695
+ delete mappedWidget['isApplicable'];
696
+ mapped.push(mappedWidget);
637
697
  });
638
698
  this.gridChange.emit(mapped);
639
699
  }
700
+ #respondToWidgetServiceEvents() {
701
+ this.#widgetGridService.widgetGrid$
702
+ .pipe(takeUntilDestroyed(this.#destroyRef))
703
+ .subscribe((widgetGrid) => {
704
+ this.widgetGrid = widgetGrid;
705
+ });
706
+ this.#widgetGridEventService.widgetEvents$
707
+ .pipe(takeUntilDestroyed(this.#destroyRef))
708
+ .subscribe((evt) => {
709
+ this.gridItemEvent.emit(evt);
710
+ });
711
+ this.#widgetGridService.widgetGridUpdate$.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe(() => {
712
+ this.#emitChange();
713
+ });
714
+ }
715
+ //#endregion
716
+ //#region Effects
717
+ #gridItemConfigEffect = () => {
718
+ this.#widgetGridService.setWidgetGrid(this.gridItemConfig());
719
+ };
720
+ #editModeEffect = () => {
721
+ const editMode = this.editMode();
722
+ this._editMode.set(editMode);
723
+ this.options.update((option) => ({
724
+ ...option,
725
+ draggable: { ...option.draggable, enabled: !!editMode },
726
+ resizable: { ...option.resizable, enabled: !!editMode },
727
+ displayGrid: editMode ? DisplayGrid.Always : DisplayGrid.None
728
+ }));
729
+ // set timeout to ensure that the grid has been redrawn before notifying the widgets about the change
730
+ setTimeout(() => {
731
+ this.gridster()?.api.calculateLayout();
732
+ }, 0);
733
+ };
734
+ #gridConfigEffect = () => {
735
+ const cfg = this.gridConfig();
736
+ this.options.update((option) => {
737
+ const next = { ...option };
738
+ if (cfg?.rows) {
739
+ next.minRows = cfg.rows;
740
+ next.maxRows = cfg.rows;
741
+ }
742
+ if (cfg?.columns) {
743
+ next.minCols = cfg.columns;
744
+ next.maxCols = cfg.columns;
745
+ }
746
+ if (cfg?.gap) {
747
+ next.margin = cfg.gap;
748
+ }
749
+ if (cfg?.gridType) {
750
+ next.gridType = cfg.gridType;
751
+ }
752
+ return next;
753
+ });
754
+ if (cfg?.newItemWidth || cfg?.newItemHeight) {
755
+ this.#widgetGridService.addItemSize = {
756
+ cols: cfg?.newItemWidth || -1, // default value -1 means the grid will auto-position the new item
757
+ rows: cfg?.newItemHeight || -1 // default value -1 means the grid will auto-position the new item
758
+ };
759
+ }
760
+ // setTimeout breaks the synchronous chain effect → options.update → calculateLayout →
761
+ // itemChangeCallback → gridChange.emit → parent → gridConfig, which would re-trigger
762
+ // this effect and cause NG0103. Also keeps the gridster() viewChild read out of the
763
+ // reactive scope so it doesn't become an effect dependency.
764
+ setTimeout(() => {
765
+ this.gridster()?.api.calculateLayout();
766
+ }, 0);
767
+ };
640
768
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: YuvWidgetGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
641
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: YuvWidgetGridComponent, isStandalone: true, selector: "yuv-widget-grid", inputs: { gridConfig: { classPropertyName: "gridConfig", publicName: "gridConfig", isSignal: true, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null }, gridItemConfig: { classPropertyName: "gridItemConfig", publicName: "gridItemConfig", isSignal: true, isRequired: false, transformFunction: null }, buckets: { classPropertyName: "buckets", publicName: "buckets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { gridChange: "gridChange", gridItemEvent: "gridItemEvent", widgetPickerOpen: "widgetPickerOpen" }, host: { properties: { "class.widget-grid-edit": "editMode()" } }, providers: [WidgetGridService], viewQueries: [{ propertyName: "gridster", first: true, predicate: Gridster, descendants: true, isSignal: true }, { propertyName: "widgetPicker", first: true, predicate: ["widgetPicker"], descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "@if (_editMode()) {\n <button class=\"fab\" mat-fab [attr.aria-label]=\"'yuv.widget-grid.widget.add.label' | translate\" (click)=\"addItem()\" [matTooltip]=\"'yuv.widget-grid.widget.add.tooltip' | translate\">\n <mat-icon>add</mat-icon>\n </button>\n}\n\n@if (widgetGrid.length === 0 && !_editMode()) {\n <div class=\"empty\">\n <!-- <p>{{ 'yuv.widget-grid.empty' | translate }}</p>\n <button ymtButton=\"primary\" (click)=\"_editMode.set(true)\">\n {{'yuv.widget-grid.empty.create' | translate}}</button> -->\n <ng-content select=\".empty\"><p>{{ 'yuv.widget-grid.empty' | translate }}</p></ng-content>\n </div>\n} @else {\n <gridster [options]=\"options\">\n @for (item of widgetGrid; track item.id) {\n <gridster-item [item]=\"item\">\n @if (_editMode()) {\n <div class=\"toolbar\">\n <!-- drag handle -->\n <mat-icon class=\"dragHandle ymt-icon--size-s\">drag_indicator</mat-icon>\n <!-- edit -->\n <button ymt-icon-button icon-button-size=\"small\"\n [matTooltip]=\"'yuv.widget-grid.widget.edit.tooltip' | translate\"\n (click)=\"openWidgetPicker(item)\"><mat-icon class=\"ymt-icon--size-s\">edit</mat-icon></button>\n <!-- remove -->\n <button ymt-icon-button icon-button-size=\"small\"\n [matTooltip]=\"'yuv.widget-grid.widget.remove.tooltip' | translate\"\n (click)=\"removeItem(item)\"><mat-icon class=\"ymt-icon--size-s\">clear</mat-icon></button>\n </div>\n }\n <div class=\"cmp\">\n <ng-container *ngComponentOutlet=\"item.widget; ndcDynamicInputs: item.widgetConfigMap\"></ng-container>\n </div>\n </gridster-item>\n }\n </gridster>\n}\n<ng-template #widgetPicker>\n <yuv-widget-picker [pickerData]=\"widgetPickerData\" [buckets]=\"buckets()\"></yuv-widget-picker>\n</ng-template>\n", styles: [":host{--_widget-grid-toolbar-background: var(--widget-grid-toolbar-background, var(--ymt-surface));--_widget-grid-on-toolbar: var(--widget-grid-on-toolbar, var(--ymt-on-surface));position:relative}:host .fab{position:absolute;inset-inline-end:var(--ymt-spacing-m);inset-block-end:var(--ymt-spacing-m);z-index:100}:host .cmp{height:100%}:host.widget-grid-edit gridster-item{outline:1px solid var(--ymt-outline);outline-offset:-1px}:host .empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--ymt-on-surface);font:var(--ymt-font-body-medium)}:host .empty p{padding:var(--ymt-spacing-m);max-width:45ch;text-align:center}:host gridster{background-color:transparent}:host gridster ::ng-deep .gridster-column,:host gridster ::ng-deep .gridster-row{border-color:var(--ymt-outline-variant);transition:none}:host gridster-item{transition:none;background-color:transparent}:host gridster-item .toolbar{position:absolute;z-index:1;background-color:var(--_widget-grid-toolbar-background);color:var(--_widget-grid-on-toolbar);right:0;top:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs);gap:var(--ymt-spacing-s);display:flex;flex-flow:row nowrap;align-items:center;outline:1px solid var(--ymt-outline);outline-offset:-1px}:host gridster-item .toolbar button{color:currentColor}:host gridster-item .dragHandle{display:flex;flex-flow:column;align-items:center;justify-content:center;padding:0 var(--ymt-spacing-s);cursor:move}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: DynamicIoModule }, { kind: "directive", type: i3.ComponentOutletInjectorDirective, selector: "[ngComponentOutlet]", exportAs: ["ndcComponentOutletInjector"] }, { kind: "directive", type: i3.ComponentOutletIoDirective, selector: "[ngComponentOutletNdcDynamicInputs],[ngComponentOutletNdcDynamicOutputs]", inputs: ["ngComponentOutletNdcDynamicInputs", "ngComponentOutletNdcDynamicOutputs"], exportAs: ["ndcDynamicIo"] }, { kind: "ngmodule", type: SignalComponentIoModule }, { kind: "component", type: Gridster, selector: "gridster", inputs: ["options"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i5.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: WidgetPickerComponent, selector: "yuv-widget-picker", inputs: ["pickerData", "buckets"], outputs: ["picked", "canceled"] }, { kind: "component", type: GridsterItem, selector: "gridster-item", inputs: ["item"], outputs: ["itemInit", "itemChange", "itemResize"] }, { kind: "directive", type: YmtIconButtonDirective, selector: "button[ymtIconButton],button[ymt-icon-button],a[ymtIconButton],a[ymt-icon-button]", inputs: ["disabled", "disableRipple", "aria-disabled", "disabledInteractive", "icon-button-size"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
769
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: YuvWidgetGridComponent, isStandalone: true, selector: "yuv-widget-grid", inputs: { gridConfig: { classPropertyName: "gridConfig", publicName: "gridConfig", isSignal: true, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null }, gridItemConfig: { classPropertyName: "gridItemConfig", publicName: "gridItemConfig", isSignal: true, isRequired: false, transformFunction: null }, buckets: { classPropertyName: "buckets", publicName: "buckets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { gridChange: "gridChange", gridItemEvent: "gridItemEvent", widgetPickerOpen: "widgetPickerOpen" }, host: { properties: { "class.widget-grid-edit": "editMode()" } }, providers: [WidgetGridService], viewQueries: [{ propertyName: "gridster", first: true, predicate: Gridster, descendants: true, isSignal: true }, { propertyName: "widgetPicker", first: true, predicate: ["widgetPicker"], descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "@if (_editMode()) {\n <button\n class=\"fab\"\n mat-fab\n [attr.aria-label]=\"'yuv.widget-grid.widget.add.label' | translate\"\n (click)=\"onAddItem()\"\n [matTooltip]=\"'yuv.widget-grid.widget.add.tooltip' | translate\"\n >\n <mat-icon>add</mat-icon>\n </button>\n}\n\n@if (widgetGrid.length === 0 && !_editMode()) {\n <div class=\"empty\">\n <!-- <p>{{ 'yuv.widget-grid.empty' | translate }}</p>\n <button ymtButton=\"primary\" (click)=\"_editMode.set(true)\">\n {{'yuv.widget-grid.empty.create' | translate}}</button> -->\n <ng-content select=\".empty\"\n ><p>{{ 'yuv.widget-grid.empty' | translate }}</p></ng-content\n >\n </div>\n} @else {\n <gridster [options]=\"options()\">\n @for (item of widgetGrid; track item.id) {\n @if (item.isApplicable) {\n <gridster-item [item]=\"item\">\n @if (_editMode()) {\n <div class=\"toolbar\">\n <!-- drag handle -->\n <mat-icon class=\"dragHandle ymt-icon--size-s\">drag_indicator</mat-icon>\n <!-- edit -->\n <button\n ymt-icon-button\n icon-button-size=\"small\"\n [matTooltip]=\"'yuv.widget-grid.widget.edit.tooltip' | translate\"\n (click)=\"onOpenWidgetPicker(item)\"\n >\n <mat-icon class=\"ymt-icon--size-s\">edit</mat-icon>\n </button>\n <!-- remove -->\n <button\n ymt-icon-button\n icon-button-size=\"small\"\n [matTooltip]=\"'yuv.widget-grid.widget.remove.tooltip' | translate\"\n (click)=\"onRemoveItem(item)\"\n >\n <mat-icon class=\"ymt-icon--size-s\">clear</mat-icon>\n </button>\n </div>\n }\n <div class=\"cmp\">\n <ng-container *ngComponentOutlet=\"item.widget; ndcDynamicInputs: item.widgetConfigMap\" />\n </div>\n </gridster-item>\n }\n }\n </gridster>\n}\n<ng-template #widgetPicker>\n <yuv-widget-picker [pickerData]=\"widgetPickerData\" [buckets]=\"buckets()\" />\n</ng-template>\n", styles: [":host{--_widget-grid-toolbar-background: var(--widget-grid-toolbar-background, var(--ymt-surface));--_widget-grid-on-toolbar: var(--widget-grid-on-toolbar, var(--ymt-on-surface));position:relative}:host .fab{position:absolute;inset-inline-end:var(--ymt-spacing-m);inset-block-end:var(--ymt-spacing-m);z-index:100}:host .cmp{height:100%}:host.widget-grid-edit gridster-item{outline:1px solid var(--ymt-outline);outline-offset:-1px}:host .empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--ymt-on-surface);font:var(--ymt-font-body-medium)}:host .empty p{padding:var(--ymt-spacing-m);max-width:45ch;text-align:center}:host gridster{background-color:transparent}:host gridster ::ng-deep .gridster-column,:host gridster ::ng-deep .gridster-row{border-color:var(--ymt-outline-variant);transition:none}:host gridster-item{transition:none;background-color:transparent}:host gridster-item .toolbar{position:absolute;z-index:1;background-color:var(--_widget-grid-toolbar-background);color:var(--_widget-grid-on-toolbar);right:0;top:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs);gap:var(--ymt-spacing-s);display:flex;flex-flow:row nowrap;align-items:center;outline:1px solid var(--ymt-outline);outline-offset:-1px}:host gridster-item .toolbar button{color:currentColor}:host gridster-item .dragHandle{display:flex;flex-flow:column;align-items:center;justify-content:center;padding:0 var(--ymt-spacing-s);cursor:move}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: DynamicIoModule }, { kind: "directive", type: i3$1.ComponentOutletInjectorDirective, selector: "[ngComponentOutlet]", exportAs: ["ndcComponentOutletInjector"] }, { kind: "directive", type: i3$1.ComponentOutletIoDirective, selector: "[ngComponentOutletNdcDynamicInputs],[ngComponentOutletNdcDynamicOutputs]", inputs: ["ngComponentOutletNdcDynamicInputs", "ngComponentOutletNdcDynamicOutputs"], exportAs: ["ndcDynamicIo"] }, { kind: "ngmodule", type: SignalComponentIoModule }, { kind: "component", type: Gridster, selector: "gridster", inputs: ["options"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i5.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: WidgetPickerComponent, selector: "yuv-widget-picker", inputs: ["pickerData", "buckets"], outputs: ["picked", "canceled"] }, { kind: "component", type: GridsterItem, selector: "gridster-item", inputs: ["item"], outputs: ["itemInit", "itemChange", "itemResize"] }, { kind: "directive", type: YmtIconButtonDirective, selector: "button[ymtIconButton],button[ymt-icon-button],a[ymtIconButton],a[ymt-icon-button]", inputs: ["disabled", "disableRipple", "aria-disabled", "disabledInteractive", "icon-button-size"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
642
770
  }
643
771
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: YuvWidgetGridComponent, decorators: [{
644
772
  type: Component,
@@ -655,15 +783,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
655
783
  GridsterItem,
656
784
  YmtIconButtonDirective,
657
785
  MatTooltip
658
- ], providers: [WidgetGridService], host: { '[class.widget-grid-edit]': 'editMode()' }, template: "@if (_editMode()) {\n <button class=\"fab\" mat-fab [attr.aria-label]=\"'yuv.widget-grid.widget.add.label' | translate\" (click)=\"addItem()\" [matTooltip]=\"'yuv.widget-grid.widget.add.tooltip' | translate\">\n <mat-icon>add</mat-icon>\n </button>\n}\n\n@if (widgetGrid.length === 0 && !_editMode()) {\n <div class=\"empty\">\n <!-- <p>{{ 'yuv.widget-grid.empty' | translate }}</p>\n <button ymtButton=\"primary\" (click)=\"_editMode.set(true)\">\n {{'yuv.widget-grid.empty.create' | translate}}</button> -->\n <ng-content select=\".empty\"><p>{{ 'yuv.widget-grid.empty' | translate }}</p></ng-content>\n </div>\n} @else {\n <gridster [options]=\"options\">\n @for (item of widgetGrid; track item.id) {\n <gridster-item [item]=\"item\">\n @if (_editMode()) {\n <div class=\"toolbar\">\n <!-- drag handle -->\n <mat-icon class=\"dragHandle ymt-icon--size-s\">drag_indicator</mat-icon>\n <!-- edit -->\n <button ymt-icon-button icon-button-size=\"small\"\n [matTooltip]=\"'yuv.widget-grid.widget.edit.tooltip' | translate\"\n (click)=\"openWidgetPicker(item)\"><mat-icon class=\"ymt-icon--size-s\">edit</mat-icon></button>\n <!-- remove -->\n <button ymt-icon-button icon-button-size=\"small\"\n [matTooltip]=\"'yuv.widget-grid.widget.remove.tooltip' | translate\"\n (click)=\"removeItem(item)\"><mat-icon class=\"ymt-icon--size-s\">clear</mat-icon></button>\n </div>\n }\n <div class=\"cmp\">\n <ng-container *ngComponentOutlet=\"item.widget; ndcDynamicInputs: item.widgetConfigMap\"></ng-container>\n </div>\n </gridster-item>\n }\n </gridster>\n}\n<ng-template #widgetPicker>\n <yuv-widget-picker [pickerData]=\"widgetPickerData\" [buckets]=\"buckets()\"></yuv-widget-picker>\n</ng-template>\n", styles: [":host{--_widget-grid-toolbar-background: var(--widget-grid-toolbar-background, var(--ymt-surface));--_widget-grid-on-toolbar: var(--widget-grid-on-toolbar, var(--ymt-on-surface));position:relative}:host .fab{position:absolute;inset-inline-end:var(--ymt-spacing-m);inset-block-end:var(--ymt-spacing-m);z-index:100}:host .cmp{height:100%}:host.widget-grid-edit gridster-item{outline:1px solid var(--ymt-outline);outline-offset:-1px}:host .empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--ymt-on-surface);font:var(--ymt-font-body-medium)}:host .empty p{padding:var(--ymt-spacing-m);max-width:45ch;text-align:center}:host gridster{background-color:transparent}:host gridster ::ng-deep .gridster-column,:host gridster ::ng-deep .gridster-row{border-color:var(--ymt-outline-variant);transition:none}:host gridster-item{transition:none;background-color:transparent}:host gridster-item .toolbar{position:absolute;z-index:1;background-color:var(--_widget-grid-toolbar-background);color:var(--_widget-grid-on-toolbar);right:0;top:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs);gap:var(--ymt-spacing-s);display:flex;flex-flow:row nowrap;align-items:center;outline:1px solid var(--ymt-outline);outline-offset:-1px}:host gridster-item .toolbar button{color:currentColor}:host gridster-item .dragHandle{display:flex;flex-flow:column;align-items:center;justify-content:center;padding:0 var(--ymt-spacing-s);cursor:move}\n"] }]
786
+ ], providers: [WidgetGridService], host: { '[class.widget-grid-edit]': 'editMode()' }, template: "@if (_editMode()) {\n <button\n class=\"fab\"\n mat-fab\n [attr.aria-label]=\"'yuv.widget-grid.widget.add.label' | translate\"\n (click)=\"onAddItem()\"\n [matTooltip]=\"'yuv.widget-grid.widget.add.tooltip' | translate\"\n >\n <mat-icon>add</mat-icon>\n </button>\n}\n\n@if (widgetGrid.length === 0 && !_editMode()) {\n <div class=\"empty\">\n <!-- <p>{{ 'yuv.widget-grid.empty' | translate }}</p>\n <button ymtButton=\"primary\" (click)=\"_editMode.set(true)\">\n {{'yuv.widget-grid.empty.create' | translate}}</button> -->\n <ng-content select=\".empty\"\n ><p>{{ 'yuv.widget-grid.empty' | translate }}</p></ng-content\n >\n </div>\n} @else {\n <gridster [options]=\"options()\">\n @for (item of widgetGrid; track item.id) {\n @if (item.isApplicable) {\n <gridster-item [item]=\"item\">\n @if (_editMode()) {\n <div class=\"toolbar\">\n <!-- drag handle -->\n <mat-icon class=\"dragHandle ymt-icon--size-s\">drag_indicator</mat-icon>\n <!-- edit -->\n <button\n ymt-icon-button\n icon-button-size=\"small\"\n [matTooltip]=\"'yuv.widget-grid.widget.edit.tooltip' | translate\"\n (click)=\"onOpenWidgetPicker(item)\"\n >\n <mat-icon class=\"ymt-icon--size-s\">edit</mat-icon>\n </button>\n <!-- remove -->\n <button\n ymt-icon-button\n icon-button-size=\"small\"\n [matTooltip]=\"'yuv.widget-grid.widget.remove.tooltip' | translate\"\n (click)=\"onRemoveItem(item)\"\n >\n <mat-icon class=\"ymt-icon--size-s\">clear</mat-icon>\n </button>\n </div>\n }\n <div class=\"cmp\">\n <ng-container *ngComponentOutlet=\"item.widget; ndcDynamicInputs: item.widgetConfigMap\" />\n </div>\n </gridster-item>\n }\n }\n </gridster>\n}\n<ng-template #widgetPicker>\n <yuv-widget-picker [pickerData]=\"widgetPickerData\" [buckets]=\"buckets()\" />\n</ng-template>\n", styles: [":host{--_widget-grid-toolbar-background: var(--widget-grid-toolbar-background, var(--ymt-surface));--_widget-grid-on-toolbar: var(--widget-grid-on-toolbar, var(--ymt-on-surface));position:relative}:host .fab{position:absolute;inset-inline-end:var(--ymt-spacing-m);inset-block-end:var(--ymt-spacing-m);z-index:100}:host .cmp{height:100%}:host.widget-grid-edit gridster-item{outline:1px solid var(--ymt-outline);outline-offset:-1px}:host .empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--ymt-on-surface);font:var(--ymt-font-body-medium)}:host .empty p{padding:var(--ymt-spacing-m);max-width:45ch;text-align:center}:host gridster{background-color:transparent}:host gridster ::ng-deep .gridster-column,:host gridster ::ng-deep .gridster-row{border-color:var(--ymt-outline-variant);transition:none}:host gridster-item{transition:none;background-color:transparent}:host gridster-item .toolbar{position:absolute;z-index:1;background-color:var(--_widget-grid-toolbar-background);color:var(--_widget-grid-on-toolbar);right:0;top:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs);gap:var(--ymt-spacing-s);display:flex;flex-flow:row nowrap;align-items:center;outline:1px solid var(--ymt-outline);outline-offset:-1px}:host gridster-item .toolbar button{color:currentColor}:host gridster-item .dragHandle{display:flex;flex-flow:column;align-items:center;justify-content:center;padding:0 var(--ymt-spacing-s);cursor:move}\n"] }]
659
787
  }], ctorParameters: () => [], propDecorators: { gridster: [{ type: i0.ViewChild, args: [i0.forwardRef(() => Gridster), { isSignal: true }] }], widgetPicker: [{ type: i0.ViewChild, args: ['widgetPicker', { ...{ read: TemplateRef }, isSignal: true }] }], gridConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "gridConfig", required: false }] }], editMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "editMode", required: false }] }], gridItemConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "gridItemConfig", required: false }] }], buckets: [{ type: i0.Input, args: [{ isSignal: true, alias: "buckets", required: false }] }], gridChange: [{ type: i0.Output, args: ["gridChange"] }], gridItemEvent: [{ type: i0.Output, args: ["gridItemEvent"] }], widgetPickerOpen: [{ type: i0.Output, args: ["widgetPickerOpen"] }] } });
660
788
 
661
789
  class WorkspaceEditComponent {
662
- dialogRef = inject((MatDialogRef));
790
+ //#region Dependencies
791
+ #dialogRef = inject((MatDialogRef));
792
+ //#endregion
793
+ //#region Angular stuff
663
794
  workspaceForm = input.required(...(ngDevMode ? [{ debugName: "workspaceForm" }] : /* istanbul ignore next */ []));
664
795
  workspaceSubmit = output();
796
+ //#endregion
797
+ //#region UI Methods
798
+ onCancel() {
799
+ this.#dialogRef.close();
800
+ }
665
801
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WorkspaceEditComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
666
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: WorkspaceEditComponent, isStandalone: true, selector: "yuv-workspace-edit", inputs: { workspaceForm: { classPropertyName: "workspaceForm", publicName: "workspaceForm", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { workspaceSubmit: "workspaceSubmit" }, ngImport: i0, template: "<yuv-dialog [headertitel]=\"'yuv.widget-grid.workspaces.edit.title' | translate\">\n <main>\n <form id=\"workspaceEditForm\" [formGroup]=\"workspaceForm()\" (ngSubmit)=\"workspaceSubmit.emit()\">\n <mat-form-field class=\"yuv-form-field\">\n <mat-label>{{ 'yuv.widget-grid.workspaces.workspace.label' | translate }}</mat-label>\n <input matInput formControlName=\"label\" />\n </mat-form-field>\n </form>\n </main>\n <footer>\n <button ymtButton=\"secondary\" type=\"button\" (click)=\"dialogRef.close()\">\n {{ 'yuv.widget-grid.workspaces.edit.cancel' | translate }}\n </button>\n <button ymtButton=\"primary\" form=\"workspaceEditForm\" type=\"submit\" [disabled]=\"workspaceForm().invalid\">\n {{ 'yuv.widget-grid.workspaces.edit.save' | translate }}\n </button>\n </footer>\n</yuv-dialog>\n", styles: [":host main{padding:var(--ymt-spacing-m)}:host mat-form-field{width:100%}\n"], dependencies: [{ kind: "component", type: DialogComponent, selector: "yuv-dialog", inputs: ["headertitle", "headertitel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i1$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i1$1.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i2$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: YmtButtonDirective, selector: "button[ymtButton], a[ymtButton]", inputs: ["ymtButton", "disabled", "aria-disabled", "disableRipple", "disabledInteractive", "button-size"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i3$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "pipe", type: TranslatePipe$1, name: "translate" }] });
802
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: WorkspaceEditComponent, isStandalone: true, selector: "yuv-workspace-edit", inputs: { workspaceForm: { classPropertyName: "workspaceForm", publicName: "workspaceForm", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { workspaceSubmit: "workspaceSubmit" }, ngImport: i0, template: "<yuv-dialog [headertitle]=\"'yuv.widget-grid.workspaces.edit.title' | translate\">\n <main>\n <form id=\"workspaceEditForm\" [formGroup]=\"workspaceForm()\" (ngSubmit)=\"workspaceSubmit.emit()\">\n <mat-form-field class=\"yuv-form-field\">\n <mat-label>{{ 'yuv.widget-grid.workspaces.workspace.label' | translate }}</mat-label>\n <input matInput formControlName=\"label\" />\n </mat-form-field>\n </form>\n </main>\n <footer>\n <button ymtButton=\"secondary\" type=\"button\" (click)=\"onCancel()\">\n {{ 'yuv.widget-grid.workspaces.edit.cancel' | translate }}\n </button>\n <button ymtButton=\"primary\" form=\"workspaceEditForm\" type=\"submit\" [disabled]=\"workspaceForm().invalid\">\n {{ 'yuv.widget-grid.workspaces.edit.save' | translate }}\n </button>\n </footer>\n</yuv-dialog>\n", styles: [":host main{padding:var(--ymt-spacing-m)}:host mat-form-field{width:100%}\n"], dependencies: [{ kind: "component", type: DialogComponent, selector: "yuv-dialog", inputs: ["headertitle", "headertitel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i1$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i1$1.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: YmtButtonDirective, selector: "button[ymtButton], a[ymtButton]", inputs: ["ymtButton", "disabled", "aria-disabled", "disableRipple", "disabledInteractive", "button-size"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i3$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "pipe", type: TranslatePipe$1, name: "translate" }] });
667
803
  }
668
804
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: WorkspaceEditComponent, decorators: [{
669
805
  type: Component,
@@ -674,7 +810,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
674
810
  YmtButtonDirective,
675
811
  TranslatePipe$1,
676
812
  ReactiveFormsModule
677
- ], template: "<yuv-dialog [headertitel]=\"'yuv.widget-grid.workspaces.edit.title' | translate\">\n <main>\n <form id=\"workspaceEditForm\" [formGroup]=\"workspaceForm()\" (ngSubmit)=\"workspaceSubmit.emit()\">\n <mat-form-field class=\"yuv-form-field\">\n <mat-label>{{ 'yuv.widget-grid.workspaces.workspace.label' | translate }}</mat-label>\n <input matInput formControlName=\"label\" />\n </mat-form-field>\n </form>\n </main>\n <footer>\n <button ymtButton=\"secondary\" type=\"button\" (click)=\"dialogRef.close()\">\n {{ 'yuv.widget-grid.workspaces.edit.cancel' | translate }}\n </button>\n <button ymtButton=\"primary\" form=\"workspaceEditForm\" type=\"submit\" [disabled]=\"workspaceForm().invalid\">\n {{ 'yuv.widget-grid.workspaces.edit.save' | translate }}\n </button>\n </footer>\n</yuv-dialog>\n", styles: [":host main{padding:var(--ymt-spacing-m)}:host mat-form-field{width:100%}\n"] }]
813
+ ], template: "<yuv-dialog [headertitle]=\"'yuv.widget-grid.workspaces.edit.title' | translate\">\n <main>\n <form id=\"workspaceEditForm\" [formGroup]=\"workspaceForm()\" (ngSubmit)=\"workspaceSubmit.emit()\">\n <mat-form-field class=\"yuv-form-field\">\n <mat-label>{{ 'yuv.widget-grid.workspaces.workspace.label' | translate }}</mat-label>\n <input matInput formControlName=\"label\" />\n </mat-form-field>\n </form>\n </main>\n <footer>\n <button ymtButton=\"secondary\" type=\"button\" (click)=\"onCancel()\">\n {{ 'yuv.widget-grid.workspaces.edit.cancel' | translate }}\n </button>\n <button ymtButton=\"primary\" form=\"workspaceEditForm\" type=\"submit\" [disabled]=\"workspaceForm().invalid\">\n {{ 'yuv.widget-grid.workspaces.edit.save' | translate }}\n </button>\n </footer>\n</yuv-dialog>\n", styles: [":host main{padding:var(--ymt-spacing-m)}:host mat-form-field{width:100%}\n"] }]
678
814
  }], propDecorators: { workspaceForm: [{ type: i0.Input, args: [{ isSignal: true, alias: "workspaceForm", required: true }] }], workspaceSubmit: [{ type: i0.Output, args: ["workspaceSubmit"] }] } });
679
815
 
680
816
  /**
@@ -683,10 +819,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
683
819
  * switch between workspaces to use or edit the widgets in the current workspace.
684
820
  */
685
821
  class YuvWidgetGridWorkspacesComponent {
822
+ //#region Dependencies
686
823
  #dialog = inject(MatDialog);
687
824
  #fb = inject(FormBuilder);
688
825
  #confirm = inject(ConfirmService);
826
+ #logger = inject(Logger);
689
827
  translate = inject(TranslateService);
828
+ //#endregion
829
+ //#region Angular stuff
830
+ /**
831
+ * Collection of buckets to load available widgets from. Wildcards are also posssible:
832
+ * `[buckets]="['app.default', '*.public.*', 'app.no?.widgets']"`
833
+ *
834
+ * `*` represents any character 0-n times
835
+ * `?` represents exactly one character
836
+ */
837
+ buckets = input(undefined, ...(ngDevMode ? [{ debugName: "buckets" }] : /* istanbul ignore next */ []));
838
+ configChange = output();
839
+ gridItemEvent = output();
840
+ editModeChange = output();
841
+ //#endregion
842
+ //#region Properties
690
843
  workspaceDialogRef;
691
844
  #DEFAULT_WORKSPACE_OPTIONS = {
692
845
  gridConfig: {
@@ -710,48 +863,24 @@ class YuvWidgetGridWorkspacesComponent {
710
863
  }), ...(ngDevMode ? [{ debugName: "_workspaceOptions" }] : /* istanbul ignore next */ []));
711
864
  workspaceConfig = input(undefined, ...(ngDevMode ? [{ debugName: "workspaceConfig" }] : /* istanbul ignore next */ []));
712
865
  _workspaceConfig = signal(undefined, ...(ngDevMode ? [{ debugName: "_workspaceConfig" }] : /* istanbul ignore next */ []));
713
- #workspaceConfigEffect = effect(() => {
714
- const wsc = this.workspaceConfig();
715
- untracked(() => {
716
- this._workspaceConfig.set(wsc);
717
- this.#updateOriginalWorkspaceConfig();
718
- if (wsc?.currentWorkspace) {
719
- this.setWorkspace(wsc.currentWorkspace, true);
720
- }
721
- });
722
- }, ...(ngDevMode ? [{ debugName: "#workspaceConfigEffect" }] : /* istanbul ignore next */ []));
723
- /**
724
- * Collection of buckets to load available widgets from. Wildcards are also posssible:
725
- * `[buckets]="['app.default', '*.public.*', 'app.no?.widgets']"`
726
- *
727
- * `*` represents any character 0-n times
728
- * `?` represents exactly one character
729
- */
730
- buckets = input(undefined, ...(ngDevMode ? [{ debugName: "buckets" }] : /* istanbul ignore next */ []));
731
- configChange = output();
732
- gridItemEvent = output();
733
- editModeChange = output();
734
866
  editMode = signal(false, ...(ngDevMode ? [{ debugName: "editMode" }] : /* istanbul ignore next */ []));
735
- // persist the last 'accepted' workspace config to be able to revert changes
736
- #originalWidgetGridWorkspaceConfig;
737
867
  // currently selected workspace
738
868
  workspace = signal(undefined, ...(ngDevMode ? [{ debugName: "workspace" }] : /* istanbul ignore next */ []));
739
- #workspaceEffect = effect(() => {
740
- const ws = this.workspace();
741
- this.workspaceLabel.set(ws ? this.getLabel(ws) : '');
742
- untracked(() => {
743
- if (ws)
744
- this.workspaceForm.patchValue({ label: this.workspaceLabel(), id: ws.id });
745
- this.gridItemConfig.set(ws ? JSON.parse(JSON.stringify(ws.grid)) : undefined);
746
- });
747
- }, ...(ngDevMode ? [{ debugName: "#workspaceEffect" }] : /* istanbul ignore next */ []));
748
869
  workspaceLabel = signal('', ...(ngDevMode ? [{ debugName: "workspaceLabel" }] : /* istanbul ignore next */ []));
749
870
  gridItemConfig = signal(undefined, ...(ngDevMode ? [{ debugName: "gridItemConfig" }] : /* istanbul ignore next */ []));
871
+ // persist the last 'accepted' workspace config to be able to revert changes
872
+ #originalWidgetGridWorkspaceConfig;
873
+ //#endregion
874
+ constructor() {
875
+ effect(this.#workspaceEffect);
876
+ effect(this.#workspaceConfigEffect);
877
+ }
750
878
  getLabel(workspace) {
751
879
  return workspace.translateLabel ? this.translate.instant(workspace.label) : workspace.label;
752
880
  }
753
- setWorkspace(id, silent = false) {
754
- const workspace = this._workspaceConfig()?.workspaces.find((w) => w.id === id);
881
+ //#region UI Methods
882
+ onSetWorkspace(id, silent = false) {
883
+ const workspace = this._workspaceConfig()?.workspaces.find((item) => item.id === id);
755
884
  if (workspace) {
756
885
  this.workspace.set(workspace);
757
886
  this._workspaceConfig.update((cfg) => ({
@@ -764,15 +893,90 @@ class YuvWidgetGridWorkspacesComponent {
764
893
  this.workspace.set(undefined);
765
894
  }
766
895
  if (!silent)
767
- this.emitConfigChange();
896
+ this.#emitConfigChange();
768
897
  }
769
- openWorkspaceDialog(create, tplRef) {
898
+ onOpenWorkspaceDialog(create, tplRef) {
770
899
  if (create) {
771
900
  this.workspaceForm.reset();
772
901
  }
773
902
  this.workspaceDialogRef = this.#dialog.open(tplRef);
774
903
  }
775
- saveWorkspace() {
904
+ onDeleteCurrentWorkspace() {
905
+ const workspaceConfig = this._workspaceConfig();
906
+ const workspace = this.workspace();
907
+ if (workspace && workspaceConfig) {
908
+ const idx = workspaceConfig.workspaces.findIndex((item) => item.id === workspace.id);
909
+ if (idx > -1) {
910
+ this.#confirm
911
+ .confirm({
912
+ message: this.translate.instant('yuv.widget-grid.workspaces.workspace.delete.confirm.message', {
913
+ label: workspace.label
914
+ })
915
+ })
916
+ .subscribe((confirmed) => {
917
+ if (!confirmed)
918
+ return;
919
+ const workspaces = workspaceConfig.workspaces;
920
+ workspaces.splice(idx, 1);
921
+ // pick another workspace if the current one is deleted
922
+ const newCurrentWorkspace = workspaces.length > 0 ? workspaces[0] : undefined;
923
+ this._workspaceConfig.set({
924
+ ...workspaceConfig,
925
+ workspaces,
926
+ currentWorkspace: newCurrentWorkspace?.id
927
+ });
928
+ this.workspace.set(newCurrentWorkspace);
929
+ this.#emitConfigChange();
930
+ });
931
+ }
932
+ }
933
+ }
934
+ onToggleEditMode() {
935
+ this.editMode.set(!this.editMode());
936
+ }
937
+ onRevertWorkspaceConfig() {
938
+ if (this.#originalWidgetGridWorkspaceConfig) {
939
+ this._workspaceConfig.set(WidgetGridUtils.gridConfigParse(this.#originalWidgetGridWorkspaceConfig));
940
+ }
941
+ const cws = this.workspaceConfig()?.currentWorkspace;
942
+ if (cws)
943
+ this.onSetWorkspace(cws, true);
944
+ this.editMode.set(false);
945
+ }
946
+ /**
947
+ * Emit current changes and reset original workspace config internally
948
+ *
949
+ * @returns void
950
+ */
951
+ onPersistWorkspaceConfig() {
952
+ this.onSaveWorkspace();
953
+ // this.#updateOriginalWorkspaceConfig();
954
+ // this.emitConfigChange();
955
+ this.editMode.set(false);
956
+ }
957
+ onGridChange(grid) {
958
+ const workspace = this.workspace();
959
+ if (workspace) {
960
+ workspace.grid = grid;
961
+ const wsc = this._workspaceConfig();
962
+ const idx = wsc ? wsc.workspaces.findIndex((item) => item.id === workspace.id) : -1;
963
+ if (idx > -1) {
964
+ const wgw = this._workspaceConfig()?.workspaces || [];
965
+ wgw[idx] = workspace;
966
+ this._workspaceConfig.update((curr) => ({
967
+ ...curr,
968
+ workspaces: wgw
969
+ }));
970
+ }
971
+ }
972
+ }
973
+ onGridEvent(gridItemEvent) {
974
+ this.gridItemEvent.emit(gridItemEvent);
975
+ }
976
+ onEnableEditMode() {
977
+ this.editMode.set(true);
978
+ }
979
+ onSaveWorkspace() {
776
980
  let cfg = this._workspaceConfig();
777
981
  if (!cfg) {
778
982
  cfg = {
@@ -795,98 +999,62 @@ class YuvWidgetGridWorkspacesComponent {
795
999
  this.workspaceForm.patchValue({ id: newWorkspace.id, label: newWorkspace.label });
796
1000
  }
797
1001
  else {
798
- const existingIndex = cfg.workspaces.findIndex((w) => w.id === this.workspaceForm.value.id);
799
- if (existingIndex === -1) {
800
- const updatedWorkspace = cfg.workspaces[existingIndex];
801
- updatedWorkspace.label = this.workspaceForm.value.label;
802
- const workspaces = cfg.workspaces;
803
- workspaces[existingIndex] = updatedWorkspace;
804
- this._workspaceConfig.set({
805
- ...cfg,
806
- workspaces
807
- });
1002
+ const existingIndex = cfg.workspaces.findIndex((item) => item.id === this.workspaceForm.value.id);
1003
+ /**
1004
+ * ADR: only update the label when the workspace is found in the current config.
1005
+ *
1006
+ * The previous implementation had the index check inverted (`existingIndex === -1`),
1007
+ * which would have dereferenced `cfg.workspaces[-1]` and crashed on `.label` if a
1008
+ * form submission ever arrived for a non-existent workspace id. In the current UI
1009
+ * flow this branch is only entered with an id that exists in the list, so the bug
1010
+ * stayed silent — but the inverted check was load-bearing on that invariant.
1011
+ *
1012
+ * `existingIndex === -1` is treated as a "should not happen" state — it would only
1013
+ * occur if the workspace was removed externally (other tab/race) between opening
1014
+ * the edit dialog and submitting it. We no-op and log a warning so the anomaly is
1015
+ * visible in dev tools without crashing the UI.
1016
+ */
1017
+ if (existingIndex !== -1) {
1018
+ cfg.workspaces[existingIndex].label = this.workspaceForm.value.label;
808
1019
  }
809
1020
  else {
810
- // workspace label may have changed
811
- cfg.workspaces[existingIndex].label = this.workspaceForm.value.label;
1021
+ this.#logger.error('onSaveWorkspace: workspace not in config; ignored', this.workspaceForm.value.id);
812
1022
  }
813
1023
  }
814
1024
  this.workspaceDialogRef?.close();
815
- this.emitConfigChange();
1025
+ this.#emitConfigChange();
1026
+ }
1027
+ //#endregion
1028
+ //#region Utilities
1029
+ #emitConfigChange() {
1030
+ this.configChange.emit(this._workspaceConfig());
816
1031
  }
817
1032
  #updateOriginalWorkspaceConfig() {
818
1033
  this.#originalWidgetGridWorkspaceConfig = WidgetGridUtils.gridConfigStringify(this.workspaceConfig());
819
1034
  }
820
- onGridEvent(e) {
821
- this.gridItemEvent.emit(e);
822
- }
823
- onGridChange(grid) {
824
- const ws = this.workspace();
825
- if (ws) {
826
- ws.grid = grid;
827
- const wsc = this._workspaceConfig();
828
- const idx = wsc ? wsc.workspaces.findIndex((w) => w.id === ws.id) : -1;
829
- if (idx > -1) {
830
- const wgw = this._workspaceConfig()?.workspaces || [];
831
- wgw[idx] = ws;
832
- this._workspaceConfig.update((curr) => ({
833
- ...curr,
834
- workspaces: wgw
835
- }));
836
- }
837
- }
838
- }
839
- // emit current changes and reset original workspace config internally
840
- persistWorkspaceConfig() {
841
- this.saveWorkspace();
842
- // this.#updateOriginalWorkspaceConfig();
843
- // this.emitConfigChange();
844
- this.editMode.set(false);
845
- }
846
- revertWorkspaceConfig() {
847
- if (this.#originalWidgetGridWorkspaceConfig) {
848
- this._workspaceConfig.set(WidgetGridUtils.gridConfigParse(this.#originalWidgetGridWorkspaceConfig));
849
- }
850
- const cws = this.workspaceConfig()?.currentWorkspace;
851
- if (cws)
852
- this.setWorkspace(cws, true);
853
- this.editMode.set(false);
854
- }
855
- deleteCurrentWorkspace() {
856
- const wsc = this._workspaceConfig();
857
- const ws = this.workspace();
858
- if (ws && wsc) {
859
- const idx = wsc.workspaces.findIndex((w) => w.id === ws.id);
860
- if (idx > -1) {
861
- this.#confirm
862
- .confirm({
863
- message: this.translate.instant('yuv.widget-grid.workspaces.workspace.delete.confirm.message', {
864
- label: ws.label
865
- })
866
- })
867
- .subscribe((confirmed) => {
868
- if (!confirmed)
869
- return;
870
- const workspaces = wsc.workspaces;
871
- workspaces.splice(idx, 1);
872
- // pick another workspace if the current one is deleted
873
- const newCurrentWorkspace = workspaces.length > 0 ? workspaces[0] : undefined;
874
- this._workspaceConfig.set({
875
- ...wsc,
876
- workspaces,
877
- currentWorkspace: newCurrentWorkspace?.id
878
- });
879
- this.workspace.set(newCurrentWorkspace);
880
- this.emitConfigChange();
881
- });
1035
+ //#endregion
1036
+ //#region Effect
1037
+ #workspaceEffect = () => {
1038
+ const workspace = this.workspace();
1039
+ this.workspaceLabel.set(workspace ? this.getLabel(workspace) : '');
1040
+ untracked(() => {
1041
+ if (workspace)
1042
+ this.workspaceForm.patchValue({ label: this.workspaceLabel(), id: workspace.id });
1043
+ this.gridItemConfig.set(workspace ? JSON.parse(JSON.stringify(workspace.grid)) : undefined);
1044
+ });
1045
+ };
1046
+ #workspaceConfigEffect = () => {
1047
+ const wsc = this.workspaceConfig();
1048
+ untracked(() => {
1049
+ this._workspaceConfig.set(wsc);
1050
+ this.#updateOriginalWorkspaceConfig();
1051
+ if (wsc?.currentWorkspace) {
1052
+ this.onSetWorkspace(wsc.currentWorkspace, true);
882
1053
  }
883
- }
884
- }
885
- emitConfigChange() {
886
- this.configChange.emit(this._workspaceConfig());
887
- }
1054
+ });
1055
+ };
888
1056
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: YuvWidgetGridWorkspacesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
889
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: YuvWidgetGridWorkspacesComponent, isStandalone: true, selector: "yuv-widget-grid-workspaces", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, workspaceConfig: { classPropertyName: "workspaceConfig", publicName: "workspaceConfig", isSignal: true, isRequired: false, transformFunction: null }, buckets: { classPropertyName: "buckets", publicName: "buckets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { configChange: "configChange", gridItemEvent: "gridItemEvent", editModeChange: "editModeChange" }, ngImport: i0, template: "@let ws = workspace();\n\n<div class=\"toolbar\">\n @if (ws) {\n @let workspaces = _workspaceConfig()?.workspaces || [];\n\n <button\n mat-icon-button\n [matTooltip]=\"'yuv.widget-grid.workspaces.workspace.menu.tooltip' | translate\"\n [disabled]=\"editMode()\"\n [matMenuTriggerFor]=\"workspacePickerMenu\"\n >\n <mat-icon>menu</mat-icon>\n </button>\n\n <mat-menu #workspacePickerMenu=\"matMenu\">\n @for (ws of workspaces; track ws.id) {\n <button mat-menu-item (click)=\"setWorkspace(ws.id)\">{{ getLabel(ws) }}</button>\n }\n <mat-divider></mat-divider>\n <button mat-menu-item (click)=\"openWorkspaceDialog(true, tplWorkspaceEdit)\">{{ 'yuv.widget-grid.workspaces.workspace.menu.create' | translate }}</button>\n </mat-menu>\n\n @if (!editMode()) {\n <h2 class=\"label\">{{ workspaceLabel() }}</h2>\n @if (!ws.preventEdit) {\n <button mat-icon-button [matMenuTriggerFor]=\"workspaceMenu\"><mat-icon>more_vert</mat-icon></button>\n <mat-menu #workspaceMenu=\"matMenu\">\n <button mat-menu-item (click)=\"editMode.set(!editMode())\">{{ 'yuv.widget-grid.workspaces.workspace.menu.edit' | translate }}</button>\n <button mat-menu-item (click)=\"deleteCurrentWorkspace()\">{{ 'yuv.widget-grid.workspaces.workspace.menu.delete' | translate }}</button>\n </mat-menu>\n }\n } @else {\n @if (ws.preventRename || ws.translateLabel) {\n <h2 class=\"label\">{{ workspaceLabel() }}</h2>\n } @else {\n <input type=\"text\" class=\"label\" [formControl]=\"workspaceLabelControl\" />\n }\n <button ymtButton=\"secondary\" (click)=\"revertWorkspaceConfig()\">\n {{ 'yuv.widget-grid.workspaces.editMode.cancel' | translate }}\n </button>\n <button ymtButton=\"primary\" (click)=\"persistWorkspaceConfig()\">\n {{ 'yuv.widget-grid.workspaces.editMode.save' | translate }}\n </button>\n }\n }\n</div>\n\n@if (ws) {\n <yuv-widget-grid\n [gridConfig]=\"_workspaceOptions().gridConfig\"\n [gridItemConfig]=\"gridItemConfig()\"\n [buckets]=\"buckets()\"\n [editMode]=\"editMode()\"\n (gridChange)=\"onGridChange($event)\"\n (gridItemEvent)=\"onGridEvent($event)\"\n >\n <div class=\"empty\">\n <p>{{ 'yuv.widget-grid.empty' | translate }}</p>\n <button ymtButton=\"primary\" (click)=\"editMode.set(true)\">\n {{ 'yuv.widget-grid.empty.create' | translate }}\n </button>\n </div>\n </yuv-widget-grid>\n} @else {\n <div class=\"empty\">\n <p>{{ 'yuv.widget-grid.workspaces.empty' | translate }}</p>\n <button ymtButton=\"primary\" (click)=\"openWorkspaceDialog(true, tplWorkspaceEdit)\">\n {{ 'yuv.widget-grid.workspaces.empty.button.create' | translate }}\n </button>\n </div>\n}\n\n<ng-template #tplWorkspaceEdit>\n <yuv-workspace-edit [workspaceForm]=\"workspaceForm\" (workspaceSubmit)=\"saveWorkspace()\"></yuv-workspace-edit>\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow-y:auto}:host .toolbar{display:flex;flex-direction:row;align-items:center;gap:var(--ymt-spacing-m)}:host .toolbar .label{flex:1;font:var(--ymt-font-headline-medium);letter-spacing:var(--ymt-font-headline-medium-tracking);margin-inline:0;margin-block:.75em;padding-inline:.25em 0;padding-block:.25em}:host .toolbar input.label{border:0;background-color:transparent}:host .toolbar input.label:not([disabled]){outline:1px solid var(--ymt-outline)}:host yuv-widget-grid{flex:1}:host .empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center}\n"], dependencies: [{ kind: "component", type: YuvWidgetGridComponent, selector: "yuv-widget-grid", inputs: ["gridConfig", "editMode", "gridItemConfig", "buckets"], outputs: ["gridChange", "gridItemEvent", "widgetPickerOpen"] }, { kind: "directive", type: YmtButtonDirective, selector: "button[ymtButton], a[ymtButton]", inputs: ["ymtButton", "disabled", "aria-disabled", "disableRipple", "disabledInteractive", "button-size"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i5.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i4$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i4$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i4$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i2.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: WorkspaceEditComponent, selector: "yuv-workspace-edit", inputs: ["workspaceForm"], outputs: ["workspaceSubmit"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1057
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: YuvWidgetGridWorkspacesComponent, isStandalone: true, selector: "yuv-widget-grid-workspaces", inputs: { buckets: { classPropertyName: "buckets", publicName: "buckets", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, workspaceConfig: { classPropertyName: "workspaceConfig", publicName: "workspaceConfig", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { configChange: "configChange", gridItemEvent: "gridItemEvent", editModeChange: "editModeChange" }, ngImport: i0, template: "@let ws = workspace();\n\n<div class=\"toolbar\">\n @if (ws) {\n @let workspaces = _workspaceConfig()?.workspaces || [];\n\n <button\n mat-icon-button\n [matTooltip]=\"'yuv.widget-grid.workspaces.workspace.menu.tooltip' | translate\"\n [disabled]=\"editMode()\"\n [matMenuTriggerFor]=\"workspacePickerMenu\"\n >\n <mat-icon>menu</mat-icon>\n </button>\n\n <mat-menu #workspacePickerMenu=\"matMenu\">\n @for (ws of workspaces; track ws.id) {\n <button mat-menu-item (click)=\"onSetWorkspace(ws.id)\">{{ getLabel(ws) }}</button>\n }\n <mat-divider />\n <button mat-menu-item (click)=\"onOpenWorkspaceDialog(true, tplWorkspaceEdit)\">\n {{ 'yuv.widget-grid.workspaces.workspace.menu.create' | translate }}\n </button>\n </mat-menu>\n\n @if (!editMode()) {\n <h2 class=\"label\">{{ workspaceLabel() }}</h2>\n @if (!ws.preventEdit) {\n <button mat-icon-button [matMenuTriggerFor]=\"workspaceMenu\"><mat-icon>more_vert</mat-icon></button>\n <mat-menu #workspaceMenu=\"matMenu\">\n <button mat-menu-item (click)=\"onToggleEditMode()\">\n {{ 'yuv.widget-grid.workspaces.workspace.menu.edit' | translate }}\n </button>\n <button mat-menu-item (click)=\"onDeleteCurrentWorkspace()\">\n {{ 'yuv.widget-grid.workspaces.workspace.menu.delete' | translate }}\n </button>\n </mat-menu>\n }\n } @else {\n @if (ws.preventRename || ws.translateLabel) {\n <h2 class=\"label\">{{ workspaceLabel() }}</h2>\n } @else {\n <input type=\"text\" class=\"label\" [formControl]=\"workspaceLabelControl\" />\n }\n <button ymtButton=\"secondary\" (click)=\"onRevertWorkspaceConfig()\">\n {{ 'yuv.widget-grid.workspaces.editMode.cancel' | translate }}\n </button>\n <button ymtButton=\"primary\" (click)=\"onPersistWorkspaceConfig()\">\n {{ 'yuv.widget-grid.workspaces.editMode.save' | translate }}\n </button>\n }\n }\n</div>\n\n@if (ws) {\n <yuv-widget-grid\n [gridConfig]=\"_workspaceOptions().gridConfig\"\n [gridItemConfig]=\"gridItemConfig()\"\n [buckets]=\"buckets()\"\n [editMode]=\"editMode()\"\n (gridChange)=\"onGridChange($event)\"\n (gridItemEvent)=\"onGridEvent($event)\"\n >\n <div class=\"empty\">\n <p>{{ 'yuv.widget-grid.empty' | translate }}</p>\n <button ymtButton=\"primary\" (click)=\"onEnableEditMode()\">\n {{ 'yuv.widget-grid.empty.create' | translate }}\n </button>\n </div>\n </yuv-widget-grid>\n} @else {\n <div class=\"empty\">\n <p>{{ 'yuv.widget-grid.workspaces.empty' | translate }}</p>\n <button ymtButton=\"primary\" (click)=\"onOpenWorkspaceDialog(true, tplWorkspaceEdit)\">\n {{ 'yuv.widget-grid.workspaces.empty.button.create' | translate }}\n </button>\n </div>\n}\n\n<ng-template #tplWorkspaceEdit>\n <yuv-workspace-edit [workspaceForm]=\"workspaceForm\" (workspaceSubmit)=\"onSaveWorkspace()\" />\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow-y:auto}:host .toolbar{display:flex;flex-direction:row;align-items:center;gap:var(--ymt-spacing-m)}:host .toolbar .label{flex:1;font:var(--ymt-font-headline-medium);letter-spacing:var(--ymt-font-headline-medium-tracking);margin-inline:0;margin-block:.75em;padding-inline:.25em 0;padding-block:.25em}:host .toolbar input.label{border:0;background-color:transparent}:host .toolbar input.label:not([disabled]){outline:1px solid var(--ymt-outline)}:host yuv-widget-grid{flex:1}:host .empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center}\n"], dependencies: [{ kind: "component", type: YuvWidgetGridComponent, selector: "yuv-widget-grid", inputs: ["gridConfig", "editMode", "gridItemConfig", "buckets"], outputs: ["gridChange", "gridItemEvent", "widgetPickerOpen"] }, { kind: "directive", type: YmtButtonDirective, selector: "button[ymtButton], a[ymtButton]", inputs: ["ymtButton", "disabled", "aria-disabled", "disableRipple", "disabledInteractive", "button-size"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i5.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i4$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i4$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i4$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i5$1.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: WorkspaceEditComponent, selector: "yuv-workspace-edit", inputs: ["workspaceForm"], outputs: ["workspaceSubmit"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
890
1058
  }
891
1059
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: YuvWidgetGridWorkspacesComponent, decorators: [{
892
1060
  type: Component,
@@ -903,18 +1071,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
903
1071
  MatTooltipModule,
904
1072
  TranslatePipe,
905
1073
  WorkspaceEditComponent
906
- ], template: "@let ws = workspace();\n\n<div class=\"toolbar\">\n @if (ws) {\n @let workspaces = _workspaceConfig()?.workspaces || [];\n\n <button\n mat-icon-button\n [matTooltip]=\"'yuv.widget-grid.workspaces.workspace.menu.tooltip' | translate\"\n [disabled]=\"editMode()\"\n [matMenuTriggerFor]=\"workspacePickerMenu\"\n >\n <mat-icon>menu</mat-icon>\n </button>\n\n <mat-menu #workspacePickerMenu=\"matMenu\">\n @for (ws of workspaces; track ws.id) {\n <button mat-menu-item (click)=\"setWorkspace(ws.id)\">{{ getLabel(ws) }}</button>\n }\n <mat-divider></mat-divider>\n <button mat-menu-item (click)=\"openWorkspaceDialog(true, tplWorkspaceEdit)\">{{ 'yuv.widget-grid.workspaces.workspace.menu.create' | translate }}</button>\n </mat-menu>\n\n @if (!editMode()) {\n <h2 class=\"label\">{{ workspaceLabel() }}</h2>\n @if (!ws.preventEdit) {\n <button mat-icon-button [matMenuTriggerFor]=\"workspaceMenu\"><mat-icon>more_vert</mat-icon></button>\n <mat-menu #workspaceMenu=\"matMenu\">\n <button mat-menu-item (click)=\"editMode.set(!editMode())\">{{ 'yuv.widget-grid.workspaces.workspace.menu.edit' | translate }}</button>\n <button mat-menu-item (click)=\"deleteCurrentWorkspace()\">{{ 'yuv.widget-grid.workspaces.workspace.menu.delete' | translate }}</button>\n </mat-menu>\n }\n } @else {\n @if (ws.preventRename || ws.translateLabel) {\n <h2 class=\"label\">{{ workspaceLabel() }}</h2>\n } @else {\n <input type=\"text\" class=\"label\" [formControl]=\"workspaceLabelControl\" />\n }\n <button ymtButton=\"secondary\" (click)=\"revertWorkspaceConfig()\">\n {{ 'yuv.widget-grid.workspaces.editMode.cancel' | translate }}\n </button>\n <button ymtButton=\"primary\" (click)=\"persistWorkspaceConfig()\">\n {{ 'yuv.widget-grid.workspaces.editMode.save' | translate }}\n </button>\n }\n }\n</div>\n\n@if (ws) {\n <yuv-widget-grid\n [gridConfig]=\"_workspaceOptions().gridConfig\"\n [gridItemConfig]=\"gridItemConfig()\"\n [buckets]=\"buckets()\"\n [editMode]=\"editMode()\"\n (gridChange)=\"onGridChange($event)\"\n (gridItemEvent)=\"onGridEvent($event)\"\n >\n <div class=\"empty\">\n <p>{{ 'yuv.widget-grid.empty' | translate }}</p>\n <button ymtButton=\"primary\" (click)=\"editMode.set(true)\">\n {{ 'yuv.widget-grid.empty.create' | translate }}\n </button>\n </div>\n </yuv-widget-grid>\n} @else {\n <div class=\"empty\">\n <p>{{ 'yuv.widget-grid.workspaces.empty' | translate }}</p>\n <button ymtButton=\"primary\" (click)=\"openWorkspaceDialog(true, tplWorkspaceEdit)\">\n {{ 'yuv.widget-grid.workspaces.empty.button.create' | translate }}\n </button>\n </div>\n}\n\n<ng-template #tplWorkspaceEdit>\n <yuv-workspace-edit [workspaceForm]=\"workspaceForm\" (workspaceSubmit)=\"saveWorkspace()\"></yuv-workspace-edit>\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow-y:auto}:host .toolbar{display:flex;flex-direction:row;align-items:center;gap:var(--ymt-spacing-m)}:host .toolbar .label{flex:1;font:var(--ymt-font-headline-medium);letter-spacing:var(--ymt-font-headline-medium-tracking);margin-inline:0;margin-block:.75em;padding-inline:.25em 0;padding-block:.25em}:host .toolbar input.label{border:0;background-color:transparent}:host .toolbar input.label:not([disabled]){outline:1px solid var(--ymt-outline)}:host yuv-widget-grid{flex:1}:host .empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center}\n"] }]
907
- }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], workspaceConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "workspaceConfig", required: false }] }], buckets: [{ type: i0.Input, args: [{ isSignal: true, alias: "buckets", required: false }] }], configChange: [{ type: i0.Output, args: ["configChange"] }], gridItemEvent: [{ type: i0.Output, args: ["gridItemEvent"] }], editModeChange: [{ type: i0.Output, args: ["editModeChange"] }] } });
1074
+ ], template: "@let ws = workspace();\n\n<div class=\"toolbar\">\n @if (ws) {\n @let workspaces = _workspaceConfig()?.workspaces || [];\n\n <button\n mat-icon-button\n [matTooltip]=\"'yuv.widget-grid.workspaces.workspace.menu.tooltip' | translate\"\n [disabled]=\"editMode()\"\n [matMenuTriggerFor]=\"workspacePickerMenu\"\n >\n <mat-icon>menu</mat-icon>\n </button>\n\n <mat-menu #workspacePickerMenu=\"matMenu\">\n @for (ws of workspaces; track ws.id) {\n <button mat-menu-item (click)=\"onSetWorkspace(ws.id)\">{{ getLabel(ws) }}</button>\n }\n <mat-divider />\n <button mat-menu-item (click)=\"onOpenWorkspaceDialog(true, tplWorkspaceEdit)\">\n {{ 'yuv.widget-grid.workspaces.workspace.menu.create' | translate }}\n </button>\n </mat-menu>\n\n @if (!editMode()) {\n <h2 class=\"label\">{{ workspaceLabel() }}</h2>\n @if (!ws.preventEdit) {\n <button mat-icon-button [matMenuTriggerFor]=\"workspaceMenu\"><mat-icon>more_vert</mat-icon></button>\n <mat-menu #workspaceMenu=\"matMenu\">\n <button mat-menu-item (click)=\"onToggleEditMode()\">\n {{ 'yuv.widget-grid.workspaces.workspace.menu.edit' | translate }}\n </button>\n <button mat-menu-item (click)=\"onDeleteCurrentWorkspace()\">\n {{ 'yuv.widget-grid.workspaces.workspace.menu.delete' | translate }}\n </button>\n </mat-menu>\n }\n } @else {\n @if (ws.preventRename || ws.translateLabel) {\n <h2 class=\"label\">{{ workspaceLabel() }}</h2>\n } @else {\n <input type=\"text\" class=\"label\" [formControl]=\"workspaceLabelControl\" />\n }\n <button ymtButton=\"secondary\" (click)=\"onRevertWorkspaceConfig()\">\n {{ 'yuv.widget-grid.workspaces.editMode.cancel' | translate }}\n </button>\n <button ymtButton=\"primary\" (click)=\"onPersistWorkspaceConfig()\">\n {{ 'yuv.widget-grid.workspaces.editMode.save' | translate }}\n </button>\n }\n }\n</div>\n\n@if (ws) {\n <yuv-widget-grid\n [gridConfig]=\"_workspaceOptions().gridConfig\"\n [gridItemConfig]=\"gridItemConfig()\"\n [buckets]=\"buckets()\"\n [editMode]=\"editMode()\"\n (gridChange)=\"onGridChange($event)\"\n (gridItemEvent)=\"onGridEvent($event)\"\n >\n <div class=\"empty\">\n <p>{{ 'yuv.widget-grid.empty' | translate }}</p>\n <button ymtButton=\"primary\" (click)=\"onEnableEditMode()\">\n {{ 'yuv.widget-grid.empty.create' | translate }}\n </button>\n </div>\n </yuv-widget-grid>\n} @else {\n <div class=\"empty\">\n <p>{{ 'yuv.widget-grid.workspaces.empty' | translate }}</p>\n <button ymtButton=\"primary\" (click)=\"onOpenWorkspaceDialog(true, tplWorkspaceEdit)\">\n {{ 'yuv.widget-grid.workspaces.empty.button.create' | translate }}\n </button>\n </div>\n}\n\n<ng-template #tplWorkspaceEdit>\n <yuv-workspace-edit [workspaceForm]=\"workspaceForm\" (workspaceSubmit)=\"onSaveWorkspace()\" />\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow-y:auto}:host .toolbar{display:flex;flex-direction:row;align-items:center;gap:var(--ymt-spacing-m)}:host .toolbar .label{flex:1;font:var(--ymt-font-headline-medium);letter-spacing:var(--ymt-font-headline-medium-tracking);margin-inline:0;margin-block:.75em;padding-inline:.25em 0;padding-block:.25em}:host .toolbar input.label{border:0;background-color:transparent}:host .toolbar input.label:not([disabled]){outline:1px solid var(--ymt-outline)}:host yuv-widget-grid{flex:1}:host .empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center}\n"] }]
1075
+ }], ctorParameters: () => [], propDecorators: { buckets: [{ type: i0.Input, args: [{ isSignal: true, alias: "buckets", required: false }] }], configChange: [{ type: i0.Output, args: ["configChange"] }], gridItemEvent: [{ type: i0.Output, args: ["gridItemEvent"] }], editModeChange: [{ type: i0.Output, args: ["editModeChange"] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], workspaceConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "workspaceConfig", required: false }] }] } });
1076
+
1077
+ // widget-grid-workspaces/widget-grid-workspaces.component
1078
+
1079
+ // export * from './widget-grid-event.service';
1080
+ // export * from './widget-grid.utils';
908
1081
 
909
- const cmp = [
910
- YuvWidgetGridComponent,
911
- YuvWidgetGridWorkspacesComponent
912
- ];
1082
+ const cmp = [YuvWidgetGridComponent, YuvWidgetGridWorkspacesComponent];
913
1083
  class YuvWidgetGridModule {
914
1084
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: YuvWidgetGridModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
915
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.12", ngImport: i0, type: YuvWidgetGridModule, imports: [YuvWidgetGridComponent,
916
- YuvWidgetGridWorkspacesComponent], exports: [YuvWidgetGridComponent,
917
- YuvWidgetGridWorkspacesComponent] });
1085
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.12", ngImport: i0, type: YuvWidgetGridModule, imports: [YuvWidgetGridComponent, YuvWidgetGridWorkspacesComponent], exports: [YuvWidgetGridComponent, YuvWidgetGridWorkspacesComponent] });
918
1086
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: YuvWidgetGridModule, imports: [cmp] });
919
1087
  }
920
1088
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: YuvWidgetGridModule, decorators: [{