@c8y/tutorial 1023.47.3 → 1023.48.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@c8y/tutorial",
3
- "version": "1023.47.3",
3
+ "version": "1023.48.2",
4
4
  "description": "This package is used to scaffold a tutorial for Cumulocity IoT Web SDK which explains a lot of concepts.",
5
5
  "dependencies": {
6
- "@c8y/style": "1023.47.3",
7
- "@c8y/ngx-components": "1023.47.3",
8
- "@c8y/client": "1023.47.3",
9
- "@c8y/bootstrap": "1023.47.3",
6
+ "@c8y/style": "1023.48.2",
7
+ "@c8y/ngx-components": "1023.48.2",
8
+ "@c8y/client": "1023.48.2",
9
+ "@c8y/bootstrap": "1023.48.2",
10
10
  "@angular/cdk": "^20.2.14",
11
11
  "monaco-editor": "~0.53.0",
12
12
  "ngx-bootstrap": "20.0.2",
@@ -14,8 +14,8 @@
14
14
  "rxjs": "7.8.2"
15
15
  },
16
16
  "devDependencies": {
17
- "@c8y/options": "1023.47.3",
18
- "@c8y/devkit": "1023.47.3"
17
+ "@c8y/options": "1023.48.2",
18
+ "@c8y/devkit": "1023.48.2"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "@angular/common": ">=20 <21"
@@ -5,7 +5,9 @@ import { generateResponse, handleRequest } from '../utils/common';
5
5
  import { generateDashboard } from '../../__mocks/utils/generators/managedObjects';
6
6
 
7
7
  export class ContextDashboardInterceptor implements HttpInterceptor {
8
- dashboard = generateDashboard();
8
+ dashboard = generateDashboard({
9
+ device: { id: '12345', name: 'Demo Sensor Device' }
10
+ });
9
11
 
10
12
  intercept(req: ApiCall, next: HttpHandler): Observable<IFetchResponse> {
11
13
  return handleRequest(req, next, 'inventory/managedObjects', {
@@ -172,10 +172,10 @@ export function generateDashboard({
172
172
  _x: 3,
173
173
  _y: 0,
174
174
  id: id,
175
- title: 'Hello',
175
+ title: 'Demo Widget Example',
176
176
  _width: 6,
177
177
  config: {
178
- text: 'Welcome to a context dashboard'
178
+ text: 'This text is configured via the widget settings. Click the edit button to change it!'
179
179
  },
180
180
  _height: 6
181
181
  }
@@ -17,6 +17,14 @@ import { ContextDashboardModule } from '@c8y/ngx-components/context-dashboard';
17
17
  imports: [ContextDashboardModule, CoreModule, CommonModule]
18
18
  })
19
19
  export class WidgetGuideContextDashboardComponent {
20
+ /**
21
+ * Default widgets shown when no dashboard exists in the backend.
22
+ *
23
+ * Note: In the codex environment, the mock interceptor at
24
+ * `packages/tutorial/src/__mocks/utils/generators/managedObjects.ts`
25
+ * returns a fake dashboard, so these defaults are not used.
26
+ * If you change these values, also update `generateDashboard()` in the mock.
27
+ */
20
28
  defaultWidgets: Widget[] = [
21
29
  {
22
30
  _x: 3,
@@ -25,10 +33,10 @@ export class WidgetGuideContextDashboardComponent {
25
33
  _height: 6,
26
34
  componentId: 'angular.widget.demo',
27
35
  config: {
28
- text: 'Welcome to a context dashboard'
36
+ text: 'This text is configured via the widget settings. Click the edit button to change it!'
29
37
  },
30
- title: 'Hello',
31
- id: 'some_unique_id'
38
+ title: 'Demo Widget Example1',
39
+ id: 'demo_widget_example'
32
40
  }
33
41
  ];
34
42
  }
@@ -1,13 +1,27 @@
1
- import { Component, Input } from '@angular/core';
2
- import { ControlContainer, NgForm, FormsModule } from '@angular/forms';
1
+ import { AsyncPipe } from '@angular/common';
3
2
  import {
4
- DynamicComponent,
5
- OnBeforeSave,
6
- AlertService,
7
- FormGroupComponent
8
- } from '@c8y/ngx-components';
9
- import { omit } from 'lodash';
10
- import { JsonPipe } from '@angular/common';
3
+ Component,
4
+ DestroyRef,
5
+ inject,
6
+ Input,
7
+ OnInit,
8
+ TemplateRef,
9
+ ViewChild
10
+ } from '@angular/core';
11
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
12
+ import {
13
+ ControlContainer,
14
+ FormBuilder,
15
+ FormControl,
16
+ FormGroup,
17
+ NgForm,
18
+ ReactiveFormsModule,
19
+ Validators
20
+ } from '@angular/forms';
21
+ import { AlertService, DynamicComponent, FormGroupComponent } from '@c8y/ngx-components';
22
+ import { WidgetConfigService } from '@c8y/ngx-components/context-dashboard';
23
+ import { BehaviorSubject } from 'rxjs';
24
+ import { WidgetDemo } from './demo-widget.component';
11
25
  import { WidgetConfig } from './widget-config.model';
12
26
 
13
27
  @Component({
@@ -16,75 +30,64 @@ import { WidgetConfig } from './widget-config.model';
16
30
  <div class="form-group">
17
31
  <c8y-form-group>
18
32
  <label>Text</label>
19
- <textarea
20
- style="width: 100%"
21
- name="text"
22
- [(ngModel)]="config.text"
23
- [required]="true"
24
- ></textarea>
33
+ <textarea style="width: 100%" [formControl]="formGroup.controls.text"></textarea>
25
34
  </c8y-form-group>
26
-
27
- <label
28
- >Configuration
29
- <button class="btn btn-primary btn-xs" type="button" (click)="more = !more">
30
- {{ more ? 'Hide' : 'Show' }} settings
31
- </button></label
32
- >
33
- <pre><code>{{ clean(config, more) | json }}</code></pre>
34
35
  </div>
36
+
37
+ <ng-template #widgetPreview>
38
+ <c8y-widget-demo [config]="config$ | async"></c8y-widget-demo>
39
+ </ng-template>
35
40
  `,
36
- // We connect our parent Form to this form (for disabling the save button)
37
- // you can also enable the button by using ContextServiceDashboard.formDisabled
38
- // property instead (by default it is enabled).
39
41
  viewProviders: [{ provide: ControlContainer, useExisting: NgForm }],
40
42
  standalone: true,
41
- imports: [FormGroupComponent, FormsModule, JsonPipe]
43
+ imports: [FormGroupComponent, ReactiveFormsModule, WidgetDemo, AsyncPipe]
42
44
  })
43
- export class WidgetConfigDemo implements DynamicComponent, OnBeforeSave {
44
- /**
45
- * The configuration which is shared between configuration component and display component.
46
- * Should be searilzabled to allow to save it to the API. The config is saved automatically
47
- * to the API on "save"-button hit. The onBeforeSave handler can be used to change this behavior,
48
- * or to manipulate the object.
49
- *
50
- * Note: The dashboard itself adds certain properties. As such, some properties are not allowed (e.g. device or settings)
51
- */
45
+ export class WidgetConfigDemo implements DynamicComponent, OnInit {
46
+ /** Configuration passed by the dashboard framework. */
52
47
  @Input() config: WidgetConfig = {};
53
48
 
54
- /**
55
- * An internal component property used to hide/show settings.
56
- */
57
- more = false;
49
+ /** Reactive form group for the widget configuration. */
50
+ formGroup: FormGroup<{ text: FormControl<string | null> }>;
58
51
 
59
- /**
60
- * Default Angular DI can be used, to use additional services.
61
- */
62
- constructor(private alert: AlertService) {}
52
+ /** Emits config changes for the preview template. */
53
+ config$ = new BehaviorSubject<WidgetConfig>({});
63
54
 
64
- /**
65
- * This example onBeforeSave handler cancels the saving, if the text is only a white-space.
66
- */
67
- onBeforeSave(config: any): boolean {
68
- if (config.text.trim() === '') {
69
- this.alert.warning('Please enter a valid text.');
70
- return false;
71
- }
72
- this.config.widgetInstanceGlobalTimeContext = true;
73
- this.config.canDecoupleGlobalTimeContext = true;
74
- return true;
75
- }
55
+ private readonly alert = inject(AlertService);
56
+ private readonly widgetConfigService = inject(WidgetConfigService);
57
+ private readonly formBuilder = inject(FormBuilder);
58
+ private readonly form = inject(NgForm);
59
+ private readonly destroyRef = inject(DestroyRef);
76
60
 
77
- /**
78
- * Used to hide/show config settings on toggle.
79
- */
80
- clean(config, more) {
81
- if (more) {
82
- return config;
83
- }
84
- return omit(config, 'settings');
61
+ @ViewChild('widgetPreview')
62
+ set preview(template: TemplateRef<any>) {
63
+ this.widgetConfigService.setPreview(template ?? null);
85
64
  }
86
65
 
87
- selectionChanged(e) {
88
- console.log(e);
66
+ ngOnInit(): void {
67
+ // Create form with initial values from config
68
+ this.formGroup = this.formBuilder.group({
69
+ text: [this.config?.text || '', Validators.required]
70
+ });
71
+
72
+ // Register form with parent NgForm for validation
73
+ this.form.form.addControl('widgetConfig', this.formGroup);
74
+
75
+ // Initialize preview
76
+ this.config$.next(this.config);
77
+
78
+ // Update preview when form values change
79
+ this.formGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
80
+ this.config$.next({ ...this.config, ...value });
81
+ });
82
+
83
+ // Register save callback - validates and merges form values into config
84
+ this.widgetConfigService.addOnBeforeSave(config => {
85
+ if (this.formGroup.invalid) {
86
+ this.alert.warning('Please enter a valid text.');
87
+ return false;
88
+ }
89
+ Object.assign(config, this.formGroup.value);
90
+ return true;
91
+ });
89
92
  }
90
93
  }
@@ -1,65 +1,55 @@
1
- import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
2
- import { gettext } from '@c8y/ngx-components/gettext';
1
+ import { Component, computed, input, OnInit } from '@angular/core';
3
2
  import {
4
3
  DismissAlertStrategy,
5
4
  DynamicComponentAlert,
6
- DynamicComponentAlertAggregator,
7
- DatePipe
5
+ DynamicComponentAlertAggregator
8
6
  } from '@c8y/ngx-components';
9
- import { NgIf } from '@angular/common';
10
7
  import { WidgetConfig } from './widget-config.model';
11
8
 
12
9
  @Component({
13
10
  selector: 'c8y-widget-demo',
14
11
  template: `
15
12
  <div class="p-16">
16
- <h1>Hi I'm a widget from angular</h1>
17
- <p class="text">Text from config object: {{ config?.text || 'No text' }}</p>
18
- <small>My context is: {{ config?.device?.name || 'No context' }}</small
19
- ><br />
20
- <ng-container *ngIf="config?.date?.length === 2">
21
- My time context is:
22
- <ul>
23
- <li>from: {{ (config?.date[0] | c8yDate) || 'No time context' }}</li>
24
- <li>To: {{ (config?.date[1] | c8yDate) || 'No time context' }}</li>
25
- </ul>
26
- </ng-container>
27
- <button class="btn btn-default" (click)="addAlert()">Add warning alert</button>
13
+ <h1>Demo Widget</h1>
14
+ <p class="text">{{ displayText() }}</p>
15
+ @if (deviceName()) {
16
+ <small>Device: {{ deviceName() }}</small>
17
+ }
18
+ <div class="m-t-16">
19
+ <button class="btn btn-default btn-sm" (click)="showAlert()">Show alert</button>
20
+ </div>
28
21
  </div>
29
22
  `,
30
23
  styles: [
31
24
  `
32
25
  .text {
33
- font-size: 2em;
26
+ font-size: 1.5em;
27
+ color: var(--c8y-brand-primary);
34
28
  }
35
29
  `
36
30
  ],
37
- standalone: true,
38
- imports: [NgIf, DatePipe]
31
+ standalone: true
39
32
  })
40
- export class WidgetDemo implements OnChanges {
41
- @Input() config: WidgetConfig;
42
- alerts: DynamicComponentAlertAggregator;
33
+ export class WidgetDemo implements OnInit {
34
+ readonly config = input<WidgetConfig>();
43
35
 
44
- ngOnInit() {
45
- this.alerts.setAlertGroupDismissStrategy(
46
- 'warning',
47
- DismissAlertStrategy.TEMPORARY_OR_PERMANENT
48
- );
49
- }
36
+ /** Computed signal for display text with fallback */
37
+ readonly displayText = computed(() => this.config()?.text || 'No text configured');
38
+
39
+ /** Computed signal for device name */
40
+ readonly deviceName = computed(() => this.config()?.device?.name);
41
+
42
+ /** Set by the dashboard framework - used to display alerts on the widget */
43
+ alerts: DynamicComponentAlertAggregator;
50
44
 
51
- ngOnChanges(changes: SimpleChanges): void {
52
- if (!changes['config']?.firstChange && changes['config']?.currentValue.date) {
53
- console.log('Global time context changed:', this.config.date);
54
- }
45
+ ngOnInit(): void {
46
+ // Enable dismissible alerts for warning type
47
+ this.alerts?.setAlertGroupDismissStrategy('warning', DismissAlertStrategy.TEMPORARY);
55
48
  }
56
49
 
57
- addAlert() {
58
- this.alerts.addAlerts(
59
- new DynamicComponentAlert({
60
- type: 'warning',
61
- text: gettext('Operation not supported by this device.')
62
- })
50
+ showAlert(): void {
51
+ this.alerts?.addAlerts(
52
+ new DynamicComponentAlert({ type: 'warning', text: 'This is a dismissible demo alert!' })
63
53
  );
64
54
  }
65
55
  }
@@ -4,36 +4,17 @@ import { WidgetConfigDemo } from './demo-widget-config.component';
4
4
 
5
5
  export function provideDemoWidget() {
6
6
  return [
7
- /**
8
- * This demo widget provides an example on how
9
- * to use the hookWidget. The component itself
10
- * is implemented in the dashboard on the
11
- * ../hello/hello.component.html by using the
12
- * dynamic-component tag.
13
- */
14
7
  hookWidget({
15
8
  id: 'angular.widget.demo',
16
- label: 'My angular widget',
17
- description: 'This is a description from angular',
9
+ label: 'Demo Widget',
10
+ description: 'A simple demo widget showing text and device context',
18
11
  component: WidgetDemo,
19
12
  configComponent: WidgetConfigDemo,
20
13
  errorStrategy: DynamicComponentErrorStrategy.OVERLAY_ERROR,
21
- /** new Angular-Dashboard definition */
22
14
  data: {
23
15
  schema: () => import('c8y-schema-loader?interfaceName=WidgetConfig!./widget-config.model'),
24
- // The settings object can be used to configure the configComponent
25
16
  settings: {
26
- noNewWidgets: false, // Set this to true, to don't allow adding new widgets.
27
- ng1: {
28
- options: {
29
- noDeviceTarget: false, // Set this to true to hide the AngularJS device selector.
30
- groupsSelectable: false // Set this, if not only devices should be selectable.
31
- }
32
- }
33
- },
34
- // Settings that are attached to the display component (in this case: WidgetDemo)
35
- displaySettings: {
36
- globalTimeContext: true // Set this to true, to add a global time context binding
17
+ noNewWidgets: false
37
18
  }
38
19
  }
39
20
  })
@@ -1,3 +1,6 @@
1
- import { GlobalTimeContextWidgetConfig } from '@c8y/ngx-components';
1
+ import { IIdentified, IManagedObject } from '@c8y/client';
2
2
 
3
- export type WidgetConfig = GlobalTimeContextWidgetConfig & { [key: string]: any };
3
+ export interface WidgetConfig {
4
+ text?: string;
5
+ device?: IIdentified & Partial<IManagedObject>;
6
+ }