@c8y/tutorial 1024.15.13 → 1024.16.10

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.
Files changed (27) hide show
  1. package/cumulocity.config.ts +7 -0
  2. package/package.json +7 -7
  3. package/src/__mocks/index.ts +11 -11
  4. package/src/__mocks/mock.model.ts +1 -2
  5. package/src/__mocks/scoped-mocks/cluster-map.ts +207 -0
  6. package/src/__mocks/utils/generators/clusterDevices.ts +203 -0
  7. package/src/__mocks/utils/generators/managedObjects.ts +21 -4
  8. package/src/__mocks/utils/realtime.ts +27 -4
  9. package/src/ai/ai-chat-example.component.html +32 -0
  10. package/src/ai/ai-chat-example.component.ts +7 -0
  11. package/src/grids/server-grid-example/type-data-grid-column/type.header-cell-renderer.component.ts +3 -2
  12. package/src/maps/cluster-map-root-node/cluster-map-root-node-example.component.html +8 -5
  13. package/src/maps/cluster-map-root-node/cluster-map-root-node-example.component.ts +29 -1
  14. package/src/maps/simple-map/simple-map-example.component.ts +5 -1
  15. package/src/maps/simple-map-custom-config/map-layer.service.ts +6 -1
  16. package/src/maps/simple-map-custom-config/simple-map-custom-config.component.html +28 -5
  17. package/src/maps/simple-map-custom-config/simple-map-custom-config.component.ts +25 -4
  18. package/src/modal/ngx-modal/ngx-modal-example.component.ts +5 -1
  19. package/src/modal/ngx-modal/simple-modal-example.component.ts +4 -0
  20. package/src/modal/ngx-modal-accessibility/simple-modal-accessibility-example.component.ts +2 -1
  21. package/src/modal/ngx-modal-selectors/simple-modal-example-with-content-selectors.component.ts +8 -2
  22. package/src/modal/ngx-modal-sizes/large-modal-example.component.ts +4 -0
  23. package/src/modal/ngx-modal-sizes/medium-modal-example.component.ts +4 -0
  24. package/src/popconfirm/pop-confirm-example.component.ts +3 -2
  25. package/src/selector/icon-selector-example/icon-selector-example.component.html +38 -0
  26. package/src/selector/icon-selector-example/icon-selector-example.component.ts +36 -0
  27. package/src/selector/icon-selector-example/icon-selector-example.module.ts +23 -0
@@ -58,6 +58,13 @@ export default {
58
58
  description: 'An introduction to asset selector',
59
59
  scope: 'self'
60
60
  },
61
+ {
62
+ name: 'Introduction to icon selector',
63
+ module: 'NgxIconSelectorExampleModule',
64
+ path: './src/selector/icon-selector-example/icon-selector-example.module.ts',
65
+ description: 'An introduction to the icon selector component.',
66
+ scope: 'self'
67
+ },
61
68
  {
62
69
  name: 'Introduction Example Module',
63
70
  module: 'IntroductionExampleModule',
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@c8y/tutorial",
3
- "version": "1024.15.13",
3
+ "version": "1024.16.10",
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
6
  "@angular/animations": "^21.2.0",
7
7
  "@angular/cdk": "^21.2.0",
8
- "@c8y/bootstrap": "1024.15.13",
9
- "@c8y/client": "1024.15.13",
10
- "@c8y/ngx-components": "1024.15.13",
11
- "@c8y/style": "1024.15.13",
8
+ "@c8y/bootstrap": "1024.16.10",
9
+ "@c8y/client": "1024.16.10",
10
+ "@c8y/ngx-components": "1024.16.10",
11
+ "@c8y/style": "1024.16.10",
12
12
  "gridstack": "^12.4.2",
13
13
  "leaflet": "1.9.4",
14
14
  "monaco-editor": "~0.53.0",
@@ -17,8 +17,8 @@
17
17
  "zone.js": "~0.15.1"
18
18
  },
19
19
  "devDependencies": {
20
- "@c8y/devkit": "1024.15.13",
21
- "@c8y/options": "1024.15.13"
20
+ "@c8y/devkit": "1024.16.10",
21
+ "@c8y/options": "1024.16.10"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "@angular/common": ">=21 <22"
@@ -12,6 +12,7 @@ import { InventoryInterceptor } from './global-mocks/inventory.interceptor';
12
12
  import { MeasurementsInterceptor } from './global-mocks/measurements.interceptor';
13
13
  import { API_MOCK_CONFIG, ApiMockConfig } from './mock.model';
14
14
  import { RealtimeSubjectServiceWithMocking } from './mock.realtime';
15
+ import { ClusterMapInterceptor } from './scoped-mocks/cluster-map';
15
16
  import { ContextDashboardInterceptor } from './scoped-mocks/context-dashboard';
16
17
  import { DeviceDataGridInterceptor } from './scoped-mocks/device-data-grid';
17
18
  import { ListsInterceptor } from './scoped-mocks/lists';
@@ -29,17 +30,16 @@ import { ProviderConfigurationsApiInterceptor } from './global-mocks/provider-co
29
30
 
30
31
  export function provideAPIMock() {
31
32
  return [
32
- // TODO: ask Jan about mocking clusters maps/cluster
33
- // {
34
- // provide: API_MOCK_CONFIG,
35
- // useValue: {
36
- // id: 'a-cluster-map-interceptor',
37
- // path: 'maps/cluster',
38
- // mockService: BoilerplateInterceptor,
39
- // debug: true
40
- // } as ApiMockConfig,
41
- // multi: true
42
- // },
33
+ {
34
+ provide: API_MOCK_CONFIG,
35
+ useValue: {
36
+ // Scoped to both cluster map examples, `maps/cluster` and `maps/cluster-root-node`.
37
+ id: 'a-cluster-map-interceptor',
38
+ path: 'maps/cluster',
39
+ mockService: ClusterMapInterceptor
40
+ } as ApiMockConfig,
41
+ multi: true
42
+ },
43
43
  {
44
44
  provide: API_MOCK_CONFIG,
45
45
  useValue: {
@@ -39,8 +39,7 @@ export enum Channels {
39
39
  BulkOperations = '/bulkOperations/*',
40
40
  Measurements = '/measurements/*',
41
41
  Alarms = '/alarms/*',
42
- Events = '/events/*',
43
- MangedObjectsMap = '/managedobjects/1'
42
+ Events = '/events/*'
44
43
  }
45
44
  /**
46
45
  * path and mockService
@@ -0,0 +1,207 @@
1
+ import { IFetchResponse, IManagedObject } from '@c8y/client';
2
+ import { ApiCall, HttpHandler, HttpInterceptor } from '@c8y/ngx-components/api';
3
+ import { from, Observable } from 'rxjs';
4
+ import { generateResponse } from '../utils/common';
5
+ import {
6
+ getClusterMapSampleSites,
7
+ getClusterMapSiteGroups,
8
+ SampleSiteDevices
9
+ } from '../utils/generators/clusterDevices';
10
+
11
+ /**
12
+ * What a cluster map query asks for. The cluster map loads its devices per bounding box, counts
13
+ * them before loading them and asks for the outermost device to fit the map to all devices.
14
+ */
15
+ interface PositionQuery {
16
+ latMin?: number;
17
+ latMax?: number;
18
+ lngMin?: number;
19
+ lngMax?: number;
20
+ /**
21
+ * True if the bounding box crosses the antimeridian. The longitudes are combined with `or`
22
+ * instead of `and` in that case.
23
+ */
24
+ lngWraps: boolean;
25
+ orderBy?: {
26
+ coordinate: 'lat' | 'lng';
27
+ ascending: boolean;
28
+ };
29
+ /**
30
+ * Id of the managed object the query is restricted to, if the map uses a root node.
31
+ */
32
+ scopedToId?: string;
33
+ }
34
+
35
+ /**
36
+ * Leaflet is lazy loaded. Answering too fast leads to markers that are not rendered.
37
+ */
38
+ const LEAFLET_LAZY_LOAD_DELAY_MS = 300;
39
+
40
+ /**
41
+ * A number in a query, optionally suffixed by the `f` or `d` type marker the platform uses.
42
+ */
43
+ const QUERY_NUMBER = '(-?\\d+(?:\\.\\d+)?)[fd]?';
44
+
45
+ /**
46
+ * Answers the inventory queries of the cluster map examples with a fixed set of sample devices,
47
+ * spread over places around the world. Queries the cluster map does not send, for example the ones
48
+ * of the asset selector, are passed on to the global inventory mock.
49
+ */
50
+ export class ClusterMapInterceptor implements HttpInterceptor {
51
+ private readonly sites: SampleSiteDevices[] = getClusterMapSampleSites();
52
+ private readonly devices: IManagedObject[] = this.sites.flatMap(site => site.devices);
53
+
54
+ intercept(req: ApiCall, next: HttpHandler): Observable<IFetchResponse> {
55
+ const params = (req?.options?.params ?? {}) as Record<string, unknown>;
56
+ const query = [params.query, params.q].find(value => typeof value === 'string') as string;
57
+ const { method = 'GET' } = req?.options || {};
58
+
59
+ if (method !== 'GET' || !req?.url?.includes('inventory/managedObjects')) {
60
+ return next.handle(req);
61
+ }
62
+
63
+ if (query?.includes('has(c8y_Position)')) {
64
+ return from(this.mockPositionQuery(query, params));
65
+ }
66
+
67
+ // The asset selector of the root node example lists the places as selectable groups.
68
+ if (isRootGroupQuery(query, params)) {
69
+ return from(this.mockGroupQuery());
70
+ }
71
+
72
+ return next.handle(req);
73
+ }
74
+
75
+ private async mockGroupQuery() {
76
+ await new Promise(resolve => setTimeout(resolve, LEAFLET_LAZY_LOAD_DELAY_MS));
77
+ const groups = getClusterMapSiteGroups();
78
+
79
+ return generateResponse(() => ({ managedObjects: groups }), {
80
+ totalPages: 1,
81
+ pageSize: groups.length,
82
+ currentPage: 1
83
+ });
84
+ }
85
+
86
+ private async mockPositionQuery(query: string, params: Record<string, unknown>) {
87
+ await new Promise(resolve => setTimeout(resolve, LEAFLET_LAZY_LOAD_DELAY_MS));
88
+
89
+ const matching = this.getMatchingDevices(parsePositionQuery(query));
90
+ const pageSize = Number(params?.pageSize) || matching.length || 1;
91
+
92
+ return generateResponse(() => ({ managedObjects: matching.slice(0, pageSize) }), {
93
+ // The cluster map counts the devices of a bounding box by requesting a single one and
94
+ // reading the total pages, so this has to reflect the number of matching devices.
95
+ totalPages: Math.ceil(matching.length / pageSize),
96
+ pageSize,
97
+ currentPage: 1
98
+ });
99
+ }
100
+
101
+ private getMatchingDevices(query: PositionQuery): IManagedObject[] {
102
+ const searched = query.scopedToId ? this.getDevicesOfRootNode(query.scopedToId) : this.devices;
103
+ const matching = searched.filter(device => isInBounds(device, query));
104
+
105
+ if (!query.orderBy) {
106
+ return matching;
107
+ }
108
+
109
+ const { coordinate, ascending } = query.orderBy;
110
+ return [...matching].sort(
111
+ (a, b) => (getCoordinate(a, coordinate) - getCoordinate(b, coordinate)) * (ascending ? 1 : -1)
112
+ );
113
+ }
114
+
115
+ /**
116
+ * Returns the devices a map with a root node shows. A root node that is one of the place groups
117
+ * resolves to the devices of that place, one that is a sample device to that single device, so
118
+ * that selecting a place in the asset selector shows exactly its devices.
119
+ * @param id Id of the root node.
120
+ * @returns The devices belonging to the root node.
121
+ */
122
+ private getDevicesOfRootNode(id: string): IManagedObject[] {
123
+ const site = this.sites.find(({ id: siteId }) => siteId === id);
124
+ if (site) {
125
+ return site.devices;
126
+ }
127
+
128
+ const device = this.devices.find(({ id: deviceId }) => deviceId === id);
129
+ if (device) {
130
+ return [device];
131
+ }
132
+
133
+ const siteIndex = Math.abs(Number(id) || 0) % this.sites.length;
134
+ return this.sites[siteIndex].devices;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Whether the request is the asset selector asking for its root groups. A user with inventory
140
+ * roles gets a `has(c8y_IsDeviceGroup)` query; the `noLogin` user of the examples lacks those
141
+ * roles, so the asset selector falls back to a `fragmentType=c8y_IsDeviceGroup` parameter instead.
142
+ */
143
+ function isRootGroupQuery(query: string, params: Record<string, unknown>): boolean {
144
+ return query?.includes('c8y_IsDeviceGroup') || params.fragmentType === 'c8y_IsDeviceGroup';
145
+ }
146
+
147
+ function parsePositionQuery(query: string): PositionQuery {
148
+ const lngConditions = new RegExp(
149
+ `\\(c8y_Position\\.lng gt ${QUERY_NUMBER}\\)\\s*(and|or)\\s*\\(c8y_Position\\.lng lt`
150
+ ).exec(query);
151
+
152
+ return {
153
+ latMin: parseCondition(query, 'lat', 'gt'),
154
+ latMax: parseCondition(query, 'lat', 'lt'),
155
+ lngMin: parseCondition(query, 'lng', 'gt'),
156
+ lngMax: parseCondition(query, 'lng', 'lt'),
157
+ lngWraps: lngConditions?.[2] === 'or',
158
+ orderBy: parseOrderBy(query),
159
+ scopedToId: parseScopedToId(query)
160
+ };
161
+ }
162
+
163
+ function parseCondition(
164
+ query: string,
165
+ coordinate: 'lat' | 'lng',
166
+ operator: 'gt' | 'lt'
167
+ ): number | undefined {
168
+ const match = new RegExp(`c8y_Position\\.${coordinate} ${operator} ${QUERY_NUMBER}`).exec(query);
169
+ return match ? Number(match[1]) : undefined;
170
+ }
171
+
172
+ function parseOrderBy(query: string): PositionQuery['orderBy'] {
173
+ const match = /\$orderby=c8y_Position\.(lat|lng) (asc|desc)/.exec(query);
174
+ return match
175
+ ? { coordinate: match[1] as 'lat' | 'lng', ascending: match[2] === 'asc' }
176
+ : undefined;
177
+ }
178
+
179
+ function parseScopedToId(query: string): string | undefined {
180
+ // The platform writes these as a function call, `bygroupid(1)`, and the id of a managed object
181
+ // as a quoted value, `id eq '1'`.
182
+ const ofGroup = /(?:bygroupid|isinhierarchyof)(?:\(|\s+eq\s+)'?(\d+)'?/.exec(query);
183
+ const ofManagedObject = /\bid eq '?(\d+)'?/.exec(query);
184
+
185
+ return (ofGroup ?? ofManagedObject)?.[1];
186
+ }
187
+
188
+ function isInBounds(device: IManagedObject, query: PositionQuery): boolean {
189
+ const lat = getCoordinate(device, 'lat');
190
+ const lng = getCoordinate(device, 'lng');
191
+
192
+ if (query.latMin !== undefined && lat <= query.latMin) {
193
+ return false;
194
+ }
195
+ if (query.latMax !== undefined && lat >= query.latMax) {
196
+ return false;
197
+ }
198
+
199
+ const afterMin = query.lngMin === undefined || lng > query.lngMin;
200
+ const beforeMax = query.lngMax === undefined || lng < query.lngMax;
201
+
202
+ return query.lngWraps ? afterMin || beforeMax : afterMin && beforeMax;
203
+ }
204
+
205
+ function getCoordinate(device: IManagedObject, coordinate: 'lat' | 'lng'): number {
206
+ return Number(device.c8y_Position?.[coordinate]);
207
+ }
@@ -0,0 +1,203 @@
1
+ import { IManagedObject } from '@c8y/client';
2
+ import { getMOCommonProps } from './managedObjects';
3
+
4
+ /**
5
+ * A place around which sample devices are positioned.
6
+ */
7
+ interface SampleSite {
8
+ /**
9
+ * Name of the place, used to name its devices.
10
+ */
11
+ name: string;
12
+ lat: number;
13
+ lng: number;
14
+ /**
15
+ * How many devices are positioned around the place.
16
+ */
17
+ deviceCount: number;
18
+ /**
19
+ * Radius in degrees over which the devices are spread.
20
+ */
21
+ spread: number;
22
+ }
23
+
24
+ /**
25
+ * The devices of one place, plus the group that holds them.
26
+ */
27
+ export interface SampleSiteDevices {
28
+ /**
29
+ * Id of the group managed object that represents the place in the asset selector.
30
+ */
31
+ id: string;
32
+ name: string;
33
+ /**
34
+ * Center of the place, used to center the map on the group when it is selected.
35
+ */
36
+ lat: number;
37
+ lng: number;
38
+ devices: IManagedObject[];
39
+ }
40
+
41
+ /**
42
+ * The places the sample devices are positioned around. The amounts are chosen so that the cluster
43
+ * map has something to cluster: zoomed out, the places are merged into a few markers with a count,
44
+ * while zooming into a place reveals its individual devices.
45
+ */
46
+ const SAMPLE_SITES: SampleSite[] = [
47
+ { name: 'Düsseldorf', lat: 51.2254, lng: 6.7763, deviceCount: 120, spread: 0.08 },
48
+ { name: 'Berlin', lat: 52.52, lng: 13.405, deviceCount: 90, spread: 0.08 },
49
+ { name: 'Munich', lat: 48.1372, lng: 11.5755, deviceCount: 40, spread: 0.06 },
50
+ { name: 'London', lat: 51.5074, lng: -0.1278, deviceCount: 70, spread: 0.08 },
51
+ { name: 'Paris', lat: 48.8566, lng: 2.3522, deviceCount: 55, spread: 0.06 },
52
+ { name: 'Madrid', lat: 40.4168, lng: -3.7038, deviceCount: 25, spread: 0.05 },
53
+ { name: 'Rome', lat: 41.9028, lng: 12.4964, deviceCount: 20, spread: 0.05 },
54
+ { name: 'Stockholm', lat: 59.3293, lng: 18.0686, deviceCount: 12, spread: 0.04 },
55
+ { name: 'New York', lat: 40.7128, lng: -74.006, deviceCount: 65, spread: 0.08 },
56
+ { name: 'San Francisco', lat: 37.7749, lng: -122.4194, deviceCount: 35, spread: 0.06 },
57
+ { name: 'Chicago', lat: 41.8781, lng: -87.6298, deviceCount: 18, spread: 0.05 },
58
+ { name: 'São Paulo', lat: -23.5505, lng: -46.6333, deviceCount: 14, spread: 0.05 },
59
+ { name: 'Tokyo', lat: 35.6762, lng: 139.6503, deviceCount: 22, spread: 0.06 },
60
+ { name: 'Singapore', lat: 1.3521, lng: 103.8198, deviceCount: 10, spread: 0.04 },
61
+ { name: 'Sydney', lat: -33.8688, lng: 151.2093, deviceCount: 8, spread: 0.04 }
62
+ ];
63
+
64
+ /**
65
+ * Angle of Vogel's spiral, used to spread the devices of a place evenly. Compared to random
66
+ * positions this keeps the sample data the same on every request, so panning and zooming the map
67
+ * does not make devices jump around.
68
+ */
69
+ const GOLDEN_ANGLE = 2.399963229728653;
70
+
71
+ /**
72
+ * The id the generated devices start at. High enough to not collide with the randomly generated
73
+ * ids of the other mocks.
74
+ */
75
+ const FIRST_DEVICE_ID = 700000;
76
+
77
+ /**
78
+ * The id the site groups start at. Kept above the device ids so the two never collide.
79
+ */
80
+ const FIRST_GROUP_ID = 800000;
81
+
82
+ /**
83
+ * Severities the devices with an active alarm cycle through, so that the map shows the status
84
+ * colors of single devices and of a cluster of devices.
85
+ */
86
+ const ALARM_SEVERITIES = ['critical', 'major', 'minor', 'warning'] as const;
87
+
88
+ /**
89
+ * Every n-th device gets an active alarm.
90
+ */
91
+ const DEVICE_WITH_ALARM_INTERVAL = 7;
92
+
93
+ /**
94
+ * How far a device moves per realtime update.
95
+ */
96
+ const MOVE_STEP_DEGREES = 0.0015;
97
+
98
+ /**
99
+ * The sample devices, created once so that every mock works with the same devices and a device
100
+ * that moved stays where it moved to.
101
+ */
102
+ let sampleSites: SampleSiteDevices[];
103
+
104
+ /**
105
+ * How often each device moved already, used to move it along a circle.
106
+ */
107
+ const moveCount = new Map<string, number>();
108
+
109
+ /**
110
+ * Returns the sample devices of every place, positioned around it.
111
+ * @returns The devices per place.
112
+ */
113
+ export function getClusterMapSampleSites(): SampleSiteDevices[] {
114
+ if (!sampleSites) {
115
+ let id = FIRST_DEVICE_ID;
116
+ sampleSites = SAMPLE_SITES.map((site, siteIndex) => ({
117
+ id: `${FIRST_GROUP_ID + siteIndex}`,
118
+ name: site.name,
119
+ lat: site.lat,
120
+ lng: site.lng,
121
+ devices: [...Array(site.deviceCount)].map((_, index) => createDevice(site, index, id++))
122
+ }));
123
+ }
124
+
125
+ return sampleSites;
126
+ }
127
+
128
+ /**
129
+ * Returns one group managed object per place, so that the asset selector of the cluster map with
130
+ * a root node lists the places as selectable groups.
131
+ * @returns The group of every place.
132
+ */
133
+ export function getClusterMapSiteGroups(): IManagedObject[] {
134
+ return getClusterMapSampleSites().map(
135
+ site =>
136
+ ({
137
+ id: site.id,
138
+ name: site.name,
139
+ ...getMOCommonProps(),
140
+ type: 'c8y_DeviceGroup',
141
+ c8y_IsDeviceGroup: {},
142
+ // Lets the example center the map on the place when its group is selected.
143
+ c8y_Position: { lat: site.lat, lng: site.lng, alt: 0, accuracy: 0 }
144
+ }) as IManagedObject
145
+ );
146
+ }
147
+
148
+ /**
149
+ * Moves the sample device with the given id a bit further along a circle around its place, so that
150
+ * a map following the device shows it moving instead of jumping to an unrelated position.
151
+ * @param id Id of the device to move.
152
+ * @returns The moved device, or `undefined` if the id is not one of the sample devices.
153
+ */
154
+ export function moveClusterMapSampleDevice(id: string): IManagedObject | undefined {
155
+ const device = getClusterMapSampleSites()
156
+ .flatMap(site => site.devices)
157
+ .find(sampleDevice => sampleDevice.id === id);
158
+
159
+ if (!device) {
160
+ return undefined;
161
+ }
162
+
163
+ const step = (moveCount.get(id) ?? 0) + 1;
164
+ moveCount.set(id, step);
165
+ const angle = step * GOLDEN_ANGLE;
166
+ device.c8y_Position = {
167
+ ...device.c8y_Position,
168
+ lat: round(device.c8y_Position.lat + MOVE_STEP_DEGREES * Math.cos(angle)),
169
+ lng: round(device.c8y_Position.lng + MOVE_STEP_DEGREES * Math.sin(angle))
170
+ };
171
+
172
+ return device;
173
+ }
174
+
175
+ function createDevice(site: SampleSite, index: number, id: number): IManagedObject {
176
+ const radius = site.spread * Math.sqrt((index + 1) / site.deviceCount);
177
+ const angle = index * GOLDEN_ANGLE;
178
+ // Compensating the latitude keeps the devices spread in a circle instead of an ellipse.
179
+ const lngScale = 1 / Math.cos((site.lat * Math.PI) / 180);
180
+ const severity =
181
+ id % DEVICE_WITH_ALARM_INTERVAL === 0
182
+ ? ALARM_SEVERITIES[id % ALARM_SEVERITIES.length]
183
+ : undefined;
184
+
185
+ return {
186
+ id: `${id}`,
187
+ name: `${site.name} sensor ${index + 1}`,
188
+ ...getMOCommonProps(),
189
+ type: 'c8y_SensorPhone',
190
+ c8y_IsDevice: {},
191
+ c8y_Position: {
192
+ lat: round(site.lat + radius * Math.cos(angle)),
193
+ lng: round(site.lng + radius * Math.sin(angle) * lngScale),
194
+ alt: 0,
195
+ accuracy: 10
196
+ },
197
+ ...(severity ? { c8y_ActiveAlarmsStatus: { [severity]: 1 } } : {})
198
+ } as IManagedObject;
199
+ }
200
+
201
+ function round(coordinate: number): number {
202
+ return Math.round(coordinate * 1e6) / 1e6;
203
+ }
@@ -1,5 +1,6 @@
1
1
  import { IManagedObject, IManagedObjectReferences } from '@c8y/client';
2
2
  import { generateId } from '../common';
3
+ import { moveClusterMapSampleDevice } from './clusterDevices';
3
4
 
4
5
  const cords = [
5
6
  {
@@ -44,11 +45,27 @@ const cords = [
44
45
  }
45
46
  ];
46
47
 
47
- export function generateRealtimeDeviceMO(): IManagedObject {
48
- return generateDevice<{ id: string; customFragment: string }>({
49
- id: '1',
48
+ /**
49
+ * Returns the managed object a realtime update is sent for.
50
+ * @param id Id of the device the update is for. Realtime updates of the sample devices of the
51
+ * cluster map examples move that device, any other id gets a generated device.
52
+ * @returns The managed object of the update.
53
+ */
54
+ export function generateRealtimeDeviceMO(id?: string): IManagedObject {
55
+ const movedSampleDevice = id ? moveClusterMapSampleDevice(id) : undefined;
56
+ if (movedSampleDevice) {
57
+ return movedSampleDevice;
58
+ }
59
+
60
+ const device = generateDevice<{ id: string; customFragment: string }>({
61
+ id: id ?? '1',
50
62
  customFragment: 'customFragment'
51
63
  });
64
+ // Give the device an active alarm so the map does not render it as muted. A muted pin ignores
65
+ // the color set through the map config, which the simple map "Change color" example relies on;
66
+ // keeping it here means the device stays colorable across realtime updates too.
67
+ device.c8y_ActiveAlarmsStatus = { critical: 0, major: 0, minor: 0, warning: 1 };
68
+ return device;
52
69
  }
53
70
 
54
71
  export function generateDevice<T>(customAttributes?: T): IManagedObject {
@@ -135,7 +152,7 @@ export function generateRandomMo() {
135
152
  return generators[randomPosition]();
136
153
  }
137
154
 
138
- function getMOCommonProps() {
155
+ export function getMOCommonProps() {
139
156
  return {
140
157
  creationTime: new Date().toISOString(),
141
158
  lastUpdated: new Date().toISOString(),
@@ -9,9 +9,8 @@ import { getFakeOperation, getFakeOperationBulk } from './generators/operations'
9
9
  /**
10
10
  * Map of generators for various channels. Each generator function generates a mock data of specific type.
11
11
  */
12
- const generators = {
12
+ const generators: Record<string, (id?: string) => IAnyData> = {
13
13
  [Channels.ManagedObjects]: generateRealtimeDeviceMO,
14
- [Channels.MangedObjectsMap]: generateRealtimeDeviceMO,
15
14
  [Channels.Operations]: getFakeOperation,
16
15
  [Channels.BulkOperations]: getFakeOperationBulk,
17
16
  [Channels.Measurements]: getFakeMeasurement,
@@ -27,7 +26,7 @@ const generators = {
27
26
  * @throws - Throws an error if there's no generator available for the given channel.
28
27
  */
29
28
  export function getFakeData<T>(channel: string): RealtimeMessage<T> {
30
- const generator = generators[channel];
29
+ const { generator, subscribedId } = resolveGenerator(channel);
31
30
  if (!generator) {
32
31
  throw new Error(
33
32
  `No fake data generator for channel ${channel}, either stay with the supported mocked channels (${Object.keys(
@@ -35,7 +34,11 @@ export function getFakeData<T>(channel: string): RealtimeMessage<T> {
35
34
  )}), provide a generator for your channel or remove the "noLogin" query parameter and login.`
36
35
  );
37
36
  }
38
- const data: IAnyData = generator();
37
+ const data: IAnyData = generator(subscribedId);
38
+ if (subscribedId) {
39
+ // A channel of a single entity only delivers data of that entity.
40
+ data.id = subscribedId;
41
+ }
39
42
  const message: RealtimeMessage<T> = {
40
43
  id: (data?.id).toString(),
41
44
  channel: data?.id ? channel.replace('*', (data?.id).toString()) : channel,
@@ -45,6 +48,26 @@ export function getFakeData<T>(channel: string): RealtimeMessage<T> {
45
48
  return message;
46
49
  }
47
50
 
51
+ /**
52
+ * Resolves the generator of a channel. A channel of a single entity, for example
53
+ * `/managedobjects/1`, is served by the generator of its wildcard channel, `/managedobjects/*`.
54
+ *
55
+ * @param channel - The channel to resolve the generator for.
56
+ * @returns The generator and, for a channel of a single entity, the id of that entity.
57
+ */
58
+ function resolveGenerator(channel: string): {
59
+ generator?: (id?: string) => IAnyData;
60
+ subscribedId?: string;
61
+ } {
62
+ if (generators[channel]) {
63
+ return { generator: generators[channel] };
64
+ }
65
+
66
+ const [, wildcardChannel, subscribedId] = /^(.*\/)([^/*]+)$/.exec(channel) ?? [];
67
+
68
+ return { generator: generators[`${wildcardChannel}*`], subscribedId };
69
+ }
70
+
48
71
  /**
49
72
  * Function to generate a random RealtimeAction.
50
73
  * It returns 'UPDATE' in 80% of the cases, otherwise, it returns either 'CREATE' or 'DELETE' randomly.
@@ -320,6 +320,38 @@
320
320
  <div>This is content that is projected by default and always shown.</div>
321
321
  }
322
322
 
323
+ <!-- Action bar below the input box. Project any control and place it left, right or in the
324
+ overflow (more) dropdown. -->
325
+ <c8y-ai-chat-action placement="left">
326
+ <button
327
+ class="btn btn-link btn-sm"
328
+ type="button"
329
+ (click)="clearMessages()"
330
+ >
331
+ <i c8yIcon="delete-bin"></i> {{ 'Clear chat' | translate }}
332
+ </button>
333
+ </c8y-ai-chat-action>
334
+
335
+ <c8y-ai-chat-action placement="right">
336
+ <button
337
+ class="btn btn-link btn-sm"
338
+ type="button"
339
+ (click)="reloadComponent()"
340
+ >
341
+ {{ 'Reset example' | translate }}
342
+ </button>
343
+ </c8y-ai-chat-action>
344
+
345
+ <c8y-ai-chat-action placement="more">
346
+ <button
347
+ class="dropdown-item"
348
+ type="button"
349
+ (click)="clearMessages()"
350
+ >
351
+ <i c8yIcon="delete-bin"></i> {{ 'Clear chat' | translate }}
352
+ </button>
353
+ </c8y-ai-chat-action>
354
+
323
355
  <c8y-ai-chat-suggestion
324
356
  [label]="'Create device '"
325
357
  [prompt]="'AHOI, create a new device'"
@@ -4,6 +4,7 @@ import { FormsModule } from '@angular/forms';
4
4
  import { CoreModule, LoadingComponent } from '@c8y/ngx-components';
5
5
  import { AIMessage, AssistantMessageDisplayConfig, ChatConfig } from '@c8y/ngx-components/ai';
6
6
  import {
7
+ AiChatActionComponent,
7
8
  AiChatAssistantMessageComponent,
8
9
  AiChatComponent,
9
10
  AiChatMessageActionComponent,
@@ -21,6 +22,7 @@ import { gettext } from '@c8y/ngx-components/gettext';
21
22
  AiChatMessageComponent,
22
23
  AiChatMessageActionComponent,
23
24
  AiChatSuggestionComponent,
25
+ AiChatActionComponent,
24
26
  LoadingComponent,
25
27
  NgComponentOutlet,
26
28
  CoreModule,
@@ -127,6 +129,11 @@ export class ChatExampleComponent {
127
129
  this.updateDisplayMessages();
128
130
  }
129
131
 
132
+ clearMessages() {
133
+ this.messages = [];
134
+ this.updateDisplayMessages();
135
+ }
136
+
130
137
  reloadComponent() {
131
138
  // Reload the component by toggling the flag
132
139
  this.showComponent = false;
@@ -1,5 +1,5 @@
1
1
  import { Component } from '@angular/core';
2
- import { CellRendererContext } from '@c8y/ngx-components';
2
+ import { CellRendererContext, IconDirective } from '@c8y/ngx-components';
3
3
 
4
4
  /**
5
5
  * The example component for custom header renderer.
@@ -14,7 +14,8 @@ import { CellRendererContext } from '@c8y/ngx-components';
14
14
  </span>
15
15
  `,
16
16
  selector: 'example-type-header-cell-renderer',
17
- standalone: true
17
+ standalone: true,
18
+ imports: [IconDirective]
18
19
  })
19
20
  export class TypeHeaderCellRendererComponent {
20
21
  constructor(public context: CellRendererContext) {}
@@ -1,16 +1,19 @@
1
1
  <c8y-title>Cluster map with root node</c8y-title>
2
2
 
3
3
  <div class="d-flex">
4
- <div class="d-flex d-col" style="width: 280px;">
4
+ <div
5
+ class="d-flex d-col"
6
+ style="width: 280px"
7
+ >
5
8
  <c8y-asset-selector
9
+ class="bg-level-1 min-height-0"
10
+ style="height: 420px"
6
11
  [(ngModel)]="rootNode"
7
12
  [config]="{
8
13
  groupsSelectable: true,
9
14
  groupsOnly: false
10
15
  }"
11
- (change)="resetConfig()"
12
- class="bg-level-1 min-height-0"
13
- style="height: 420px;"
16
+ (onSelected)="onGroupSelected($event)"
14
17
  ></c8y-asset-selector>
15
18
  </div>
16
19
  <!-- important -->
@@ -40,7 +43,7 @@
40
43
  <!-- /important -->
41
44
  <div
42
45
  class="a-s-stretch p-16 d-col bg-info-light"
43
- style="width: 320px; height: 420px;"
46
+ style="width: 320px; height: 420px"
44
47
  >
45
48
  <label>RootNode</label>
46
49
  <pre style="height: 120px">{{ rootNode | json }} </pre>
@@ -2,7 +2,16 @@ import { Component } from '@angular/core';
2
2
  import { ClusterMapConfig, defaultMapConfig, MapModule } from '@c8y/ngx-components/map';
3
3
  import { CommonModule } from '@angular/common';
4
4
  import { CoreModule } from '@c8y/ngx-components';
5
- import { AssetSelectorModule } from '@c8y/ngx-components/assets-navigator';
5
+ import {
6
+ AssetSelectionChangeEvent,
7
+ AssetSelectorModule
8
+ } from '@c8y/ngx-components/assets-navigator';
9
+
10
+ /**
11
+ * Zoom level the map uses when it centers on a selected group, chosen so that the devices of one
12
+ * place fill the map.
13
+ */
14
+ const GROUP_ZOOM_LEVEL = 11;
6
15
 
7
16
  @Component({
8
17
  selector: 'tut-cluster-map-root-node-example',
@@ -17,6 +26,25 @@ export class ClusterMapRootNodeExampleComponent {
17
26
  rootNode: unknown;
18
27
  oldRootNode: unknown;
19
28
 
29
+ /**
30
+ * Centers the map on the selected group. The asset selector passes the full managed object as
31
+ * `change.item` (its model value is simplified to id and name), so the group position is read
32
+ * from there.
33
+ * @param event The selection change emitted by the asset selector.
34
+ */
35
+ onGroupSelected(event: AssetSelectionChangeEvent) {
36
+ this.resetConfig();
37
+
38
+ const position = event?.change?.isSelected ? event.change.item?.c8y_Position : undefined;
39
+ if (position) {
40
+ this.followConfig = {
41
+ ...this.followConfig,
42
+ center: [position.lat, position.lng],
43
+ zoomLevel: GROUP_ZOOM_LEVEL
44
+ };
45
+ }
46
+ }
47
+
20
48
  toggleAutorefresh() {
21
49
  this.config = {
22
50
  ...this.config,
@@ -44,7 +44,11 @@ export class SimpleMapExampleComponent implements OnInit {
44
44
  }
45
45
 
46
46
  setCenter() {
47
- this.config = { ...this.config, center: defaultMapConfig.center };
47
+ this.config = {
48
+ ...this.config,
49
+ center: [defaultMapConfig.center[0], defaultMapConfig.center[1]],
50
+ zoomLevel: 12
51
+ };
48
52
  }
49
53
 
50
54
  changeIcon() {
@@ -3,6 +3,11 @@ import { defaultLayer } from '@c8y/ngx-components/map';
3
3
  import { MapTileLayer } from '@c8y/options';
4
4
  import { BehaviorSubject, Observable } from 'rxjs';
5
5
 
6
+ /**
7
+ * The tile layers this example can switch to, next to the default layer.
8
+ */
9
+ export type CustomMapTileLayerName = 'Carto' | 'OpenTopo';
10
+
6
11
  @Injectable({
7
12
  providedIn: 'root'
8
13
  })
@@ -17,7 +22,7 @@ export class MapLayerService implements CumulocityServiceRegistry.MapTileLayerPr
17
22
  return true;
18
23
  }
19
24
 
20
- changeLayer(name: 'Carto' | 'OpenTopo') {
25
+ changeLayer(name: CustomMapTileLayerName) {
21
26
  if (name === 'Carto') {
22
27
  this.maps$.next([
23
28
  {
@@ -1,27 +1,50 @@
1
1
  <c8y-title>Simple map with custom config</c8y-title>
2
2
 
3
3
  <div class="d-flex">
4
- <div class="d-flex d-col j-c-center p-r-16" style="width: 280px; height: 420px">
4
+ <div
5
+ class="d-flex d-col j-c-center p-r-16"
6
+ style="width: 280px; height: 420px"
7
+ >
5
8
  <button
6
9
  class="btn btn-default btn-block"
7
- (click)="mapLayerService?.changeLayer('Carto')"
10
+ [class.active]="selectedLayer() === 'Carto'"
11
+ [attr.aria-pressed]="selectedLayer() === 'Carto'"
12
+ (click)="showLayer('Carto')"
8
13
  >
9
14
  Change to Carto layer
10
15
  </button>
11
16
  <button
12
17
  class="btn btn-default btn-block"
13
- (click)="mapLayerService?.changeLayer('OpenTopo')"
18
+ [class.active]="selectedLayer() === 'OpenTopo'"
19
+ [attr.aria-pressed]="selectedLayer() === 'OpenTopo'"
20
+ (click)="showLayer('OpenTopo')"
14
21
  >
15
22
  Change to OpenTopo layer
16
23
  </button>
17
24
  <button
18
25
  class="btn btn-default btn-block"
19
- (click)="mapLayerService?.revert()"
26
+ [class.active]="selectedLayer() === 'default'"
27
+ [attr.aria-pressed]="selectedLayer() === 'default'"
28
+ (click)="showLayer('default')"
20
29
  >
21
30
  Change to default
22
31
  </button>
23
32
  </div>
24
33
  <div class="flex-grow p-relative a-s-stretch">
25
- <c8y-map></c8y-map>
34
+ <!--
35
+ The map picks up the layers of `MapLayerService` while it initializes. Rendering one map per
36
+ layer re-creates it whenever another layer is selected, so the new layer is applied.
37
+ -->
38
+ @switch (selectedLayer()) {
39
+ @case ('Carto') {
40
+ <c8y-map></c8y-map>
41
+ }
42
+ @case ('OpenTopo') {
43
+ <c8y-map></c8y-map>
44
+ }
45
+ @default {
46
+ <c8y-map></c8y-map>
47
+ }
48
+ }
26
49
  </div>
27
50
  </div>
@@ -1,16 +1,37 @@
1
1
  import { CommonModule } from '@angular/common';
2
- import { Component, inject } from '@angular/core';
2
+ import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
3
3
  import { CoreModule } from '@c8y/ngx-components';
4
4
  import { AssetSelectorModule } from '@c8y/ngx-components/assets-navigator';
5
5
  import { MapModule } from '@c8y/ngx-components/map';
6
- import { MapLayerService } from './map-layer.service';
6
+ import { CustomMapTileLayerName, MapLayerService } from './map-layer.service';
7
7
 
8
8
  @Component({
9
9
  selector: 'c8y-simple-map-custom-config',
10
10
  templateUrl: './simple-map-custom-config.component.html',
11
11
  standalone: true,
12
- imports: [CommonModule, MapModule, CoreModule, AssetSelectorModule]
12
+ imports: [CommonModule, MapModule, CoreModule, AssetSelectorModule],
13
+ changeDetection: ChangeDetectionStrategy.OnPush
13
14
  })
14
15
  export class SimpleMapCustomConfigComponent {
15
- mapLayerService = inject(MapLayerService);
16
+ /**
17
+ * The layer the map is currently showing. `c8y-map` reads the layers provided via
18
+ * `mapTileLayerHook` while it initializes, so the template uses this value to re-create the
19
+ * map whenever another layer is selected.
20
+ */
21
+ protected readonly selectedLayer = signal<CustomMapTileLayerName | 'default'>('default');
22
+
23
+ private readonly mapLayerService = inject(MapLayerService);
24
+
25
+ /**
26
+ * Provides the given layer through `MapLayerService` and shows the map with it.
27
+ * @param layer The layer to show, or `default` for the layer the map ships with.
28
+ */
29
+ protected showLayer(layer: CustomMapTileLayerName | 'default') {
30
+ if (layer === 'default') {
31
+ this.mapLayerService.revert();
32
+ } else {
33
+ this.mapLayerService.changeLayer(layer);
34
+ }
35
+ this.selectedLayer.set(layer);
36
+ }
16
37
  }
@@ -30,8 +30,12 @@ import { SimpleModalExampleComponent } from './simple-modal-example.component';
30
30
  (onClose)="onTemplateRefClose($event)"
31
31
  (onDismiss)="onTemplateRefDismiss($event)"
32
32
  [labels]="labels"
33
+ [headerClasses]="'dialog-header'"
33
34
  [disabled]="true"
34
35
  >
36
+ <ng-container c8y-modal-title>
37
+ <span [c8yIcon]="'export'"></span>
38
+ </ng-container>
35
39
  <c8y-list-group>
36
40
  <c8y-li *ngFor="let a of listArray; let index = index">
37
41
  <c8y-li-checkbox></c8y-li-checkbox>
@@ -81,6 +85,6 @@ export class NgxModalExampleComponent {
81
85
  ariaDescribedby: 'modal-body',
82
86
  ariaLabelledBy: 'modal-title',
83
87
  ignoreBackdropClick: true
84
- }).content as SimpleModalExampleComponent;
88
+ });
85
89
  }
86
90
  }
@@ -16,8 +16,12 @@ import {
16
16
  (onClose)="onClose($event)"
17
17
  (onDismiss)="onDismiss($event)"
18
18
  [labels]="labels"
19
+ [headerClasses]="'dialog-header'"
19
20
  [disabled]="true"
20
21
  >
22
+ <ng-container c8y-modal-title>
23
+ <span [c8yIcon]="'export'"></span>
24
+ </ng-container>
21
25
  <c8y-list-group>
22
26
  <c8y-li *ngFor="let a of listArray; let index = index">
23
27
  <c8y-li-checkbox></c8y-li-checkbox>
@@ -3,6 +3,7 @@ import {
3
3
  CommonModule,
4
4
  FormsModule,
5
5
  HeaderModule,
6
+ IconDirective,
6
7
  ListGroupModule,
7
8
  ModalLabels,
8
9
  ModalModule
@@ -32,7 +33,7 @@ import {
32
33
  </c8y-modal>
33
34
  `,
34
35
  standalone: true,
35
- imports: [ModalModule, HeaderModule, CommonModule, FormsModule, ListGroupModule]
36
+ imports: [ModalModule, HeaderModule, CommonModule, FormsModule, ListGroupModule, IconDirective]
36
37
  })
37
38
  export class SimpleModalAccessibilityExampleComponent {
38
39
  labels: ModalLabels = { ok: 'Export', cancel: 'Cancel' };
@@ -1,5 +1,11 @@
1
1
  import { Component, ViewChild } from '@angular/core';
2
- import { CommonModule, FormsModule, HeaderModule, ModalModule } from '@c8y/ngx-components';
2
+ import {
3
+ CommonModule,
4
+ FormsModule,
5
+ HeaderModule,
6
+ IconDirective,
7
+ ModalModule
8
+ } from '@c8y/ngx-components';
3
9
 
4
10
  @Component({
5
11
  selector: 'tut-simple-modal-with-selectors',
@@ -46,7 +52,7 @@ import { CommonModule, FormsModule, HeaderModule, ModalModule } from '@c8y/ngx-c
46
52
  </c8y-modal>
47
53
  `,
48
54
  standalone: true,
49
- imports: [ModalModule, HeaderModule, CommonModule, FormsModule]
55
+ imports: [ModalModule, HeaderModule, CommonModule, FormsModule, IconDirective]
50
56
  })
51
57
  export class SimpleModalExampleWithContentSelectorsComponent {
52
58
  smartGroup = {
@@ -16,8 +16,12 @@ import {
16
16
  (onClose)="onClose($event)"
17
17
  (onDismiss)="onDismiss($event)"
18
18
  [labels]="labels"
19
+ [headerClasses]="'dialog-header'"
19
20
  [disabled]="true"
20
21
  >
22
+ <ng-container c8y-modal-title>
23
+ <span [c8yIcon]="'export'"></span>
24
+ </ng-container>
21
25
  <c8y-list-group>
22
26
  <c8y-li *ngFor="let a of listArray; let index = index">
23
27
  <c8y-li-checkbox></c8y-li-checkbox>
@@ -16,8 +16,12 @@ import {
16
16
  (onClose)="onClose($event)"
17
17
  (onDismiss)="onDismiss($event)"
18
18
  [labels]="labels"
19
+ [headerClasses]="'dialog-header'"
19
20
  [disabled]="true"
20
21
  >
22
+ <ng-container c8y-modal-title>
23
+ <span [c8yIcon]="'export'"></span>
24
+ </ng-container>
21
25
  <c8y-list-group>
22
26
  <c8y-li *ngFor="let a of listArray; let index = index">
23
27
  <c8y-li-checkbox></c8y-li-checkbox>
@@ -3,6 +3,7 @@ import { gettext } from '@c8y/ngx-components/gettext';
3
3
  import {
4
4
  CommonModule,
5
5
  HeaderModule,
6
+ IconDirective,
6
7
  ModalModule,
7
8
  PopoverConfirmButtons,
8
9
  PopoverConfirmComponent
@@ -29,7 +30,7 @@ import {
29
30
  ></c8y-popover-confirm>
30
31
  </div>`,
31
32
  standalone: true,
32
- imports: [ModalModule, CommonModule, HeaderModule]
33
+ imports: [ModalModule, CommonModule, HeaderModule, IconDirective]
33
34
  })
34
35
  export class PopConfirmExampleComponent {
35
36
  // you can provide custom buttons
@@ -59,7 +60,7 @@ export class PopConfirmExampleComponent {
59
60
  }
60
61
  // eslint-disable-next-line no-console
61
62
  console.log('You successfully deleted the item!');
62
- } catch (e) {
63
+ } catch {
63
64
  // do nothing or display "deletion failed" message
64
65
  }
65
66
  }
@@ -0,0 +1,38 @@
1
+ <c8y-title>Icon selector examples</c8y-title>
2
+ <div class="">
3
+ <p class="m-b-8 text-bold">{{ 'Browsing and selecting' | translate }}</p>
4
+ <!-- important -->
5
+ <div
6
+ class="bg-component border-all"
7
+ style="height: 480px; overflow-y: auto"
8
+ >
9
+ <c8y-icon-selector (onSelect)="onSelect($event)"></c8y-icon-selector>
10
+ </div>
11
+ <!-- /important -->
12
+ @if (selectedIcon(); as icon) {
13
+ <p class="m-t-8 d-flex a-i-center gap-8">
14
+ <span>{{ 'Selected' | translate }}:</span>
15
+ <i
16
+ [c8yIcon]="icon"
17
+ aria-hidden="true"
18
+ ></i>
19
+ <code>{{ icon }}</code>
20
+ </p>
21
+ }
22
+
23
+ <p class="m-t-24 m-b-8 text-bold">{{ 'Form integration' | translate }}</p>
24
+ <form [formGroup]="form">
25
+ <div class="d-inline-block">
26
+ <!-- important -->
27
+ <c8y-icon-selector-wrapper
28
+ [canRemoveIcon]="true"
29
+ [iconSize]="32"
30
+ formControlName="icon"
31
+ ></c8y-icon-selector-wrapper>
32
+ <!-- /important -->
33
+ </div>
34
+ </form>
35
+ <p class="m-t-8">
36
+ <code>{{ form.value | json }}</code>
37
+ </p>
38
+ </div>
@@ -0,0 +1,36 @@
1
+ import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
2
+ import { JsonPipe } from '@angular/common';
3
+ import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
4
+ import { C8yTranslatePipe, IconDirective, TitleComponent } from '@c8y/ngx-components';
5
+ import {
6
+ IconSelectorComponent,
7
+ IconSelectorWrapperComponent
8
+ } from '@c8y/ngx-components/icon-selector';
9
+
10
+ @Component({
11
+ selector: 'c8y-icon-selector-example',
12
+ templateUrl: './icon-selector-example.component.html',
13
+ changeDetection: ChangeDetectionStrategy.OnPush,
14
+ imports: [
15
+ JsonPipe,
16
+ ReactiveFormsModule,
17
+ C8yTranslatePipe,
18
+ IconDirective,
19
+ TitleComponent,
20
+ IconSelectorComponent,
21
+ IconSelectorWrapperComponent
22
+ ]
23
+ })
24
+ export class IconSelectorExampleComponent {
25
+ /** Last icon chosen from the inline selector grid. */
26
+ protected readonly selectedIcon = signal<string | undefined>(undefined);
27
+
28
+ /** Form-bound icon selection driven by the icon-selector wrapper. */
29
+ protected readonly form = new FormGroup({
30
+ icon: new FormControl('c8y-device')
31
+ });
32
+
33
+ protected onSelect(icon: string | undefined): void {
34
+ this.selectedIcon.set(icon);
35
+ }
36
+ }
@@ -0,0 +1,23 @@
1
+ import { NgModule } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { NavigatorNode, hookNavigator, hookRoute } from '@c8y/ngx-components';
4
+
5
+ @NgModule({
6
+ imports: [CommonModule],
7
+ providers: [
8
+ hookRoute({
9
+ path: 'icon-selector',
10
+ loadComponent: () =>
11
+ import('./icon-selector-example.component').then(m => m.IconSelectorExampleComponent)
12
+ }),
13
+ hookNavigator(
14
+ new NavigatorNode({
15
+ path: '/icon-selector',
16
+ label: 'Icon selector',
17
+ icon: 'c8y-circle-star',
18
+ priority: 85
19
+ })
20
+ )
21
+ ]
22
+ })
23
+ export class NgxIconSelectorExampleModule {}