@c8y/tutorial 1021.3.1 → 1021.6.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.
- package/cumulocity.config.ts +8 -1
- package/package.json +7 -7
- package/src/grids/server-grid-example/last-updated-data-grid-column/last-updated.cell-renderer.component.ts +10 -0
- package/src/grids/server-grid-example/last-updated-data-grid-column/last-updated.data-grid-column.ts +100 -0
- package/src/grids/server-grid-example/server-grid-example.module.ts +1 -2
- package/src/grids/server-grid-example/server-grid-example.service.ts +18 -41
- package/src/grids/server-grid-example/type-data-grid-column/type.data-grid-column.ts +72 -31
- package/src/grids/server-grid-example/type-data-grid-column/type.filtering-form-renderer.component.ts +5 -11
- package/src/hooks/service/client/basic-view.component.html +44 -0
- package/src/hooks/service/client/basic-view.component.ts +13 -0
- package/src/hooks/service/client/counter.component.html +15 -0
- package/src/hooks/service/client/counter.component.ts +21 -0
- package/src/hooks/service/client/index.ts +3 -0
- package/src/hooks/service/client/service-hook.module.ts +29 -0
- package/src/hooks/service/counter/counter.model.ts +43 -0
- package/src/hooks/service/counter/counter.module.ts +13 -0
- package/src/hooks/service/counter/counter.service.ts +10 -0
- package/src/hooks/service/service-hook-codex-sample.module.ts +13 -0
- package/src/properties-list/properties-list-example.component.html +10 -0
- package/src/properties-list/properties-list-example.component.ts +21 -0
package/cumulocity.config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConfigurationOptions } from '@c8y/devkit';
|
|
2
2
|
import { DefinePlugin } from 'webpack';
|
|
3
|
-
import { author, description,
|
|
3
|
+
import { author, description, name, version } from './package.json';
|
|
4
4
|
|
|
5
5
|
export default {
|
|
6
6
|
runTime: {
|
|
@@ -261,6 +261,13 @@ export default {
|
|
|
261
261
|
description: 'A sample for wizard hook.',
|
|
262
262
|
scope: 'self'
|
|
263
263
|
},
|
|
264
|
+
{
|
|
265
|
+
name: 'Service hook Codex sample',
|
|
266
|
+
module: 'ServiceHookCodexSampleModule',
|
|
267
|
+
path: './src/hooks/service/service-hook-codex-sample.module.ts',
|
|
268
|
+
description: 'A sample for hookService.',
|
|
269
|
+
scope: 'self'
|
|
270
|
+
},
|
|
264
271
|
{
|
|
265
272
|
name: 'Stepper',
|
|
266
273
|
module: 'StepperHookModule',
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c8y/tutorial",
|
|
3
|
-
"version": "1021.
|
|
3
|
+
"version": "1021.6.0",
|
|
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": "1021.
|
|
7
|
-
"@c8y/ngx-components": "1021.
|
|
8
|
-
"@c8y/client": "1021.
|
|
9
|
-
"@c8y/bootstrap": "1021.
|
|
6
|
+
"@c8y/style": "1021.6.0",
|
|
7
|
+
"@c8y/ngx-components": "1021.6.0",
|
|
8
|
+
"@c8y/client": "1021.6.0",
|
|
9
|
+
"@c8y/bootstrap": "1021.6.0",
|
|
10
10
|
"@angular/cdk": "^18.2.10",
|
|
11
11
|
"ngx-bootstrap": "18.0.0",
|
|
12
12
|
"leaflet": "1.9.4",
|
|
13
13
|
"rxjs": "^7.8.1"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@c8y/options": "1021.
|
|
17
|
-
"@c8y/devkit": "1021.
|
|
16
|
+
"@c8y/options": "1021.6.0",
|
|
17
|
+
"@c8y/devkit": "1021.6.0"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"@angular/common": ">=18 <19"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
import { CellRendererContext } from '@c8y/ngx-components';
|
|
3
|
+
|
|
4
|
+
@Component({
|
|
5
|
+
template: ` {{ context.value | c8yDate }} `,
|
|
6
|
+
selector: 'c8y-last-updated-cell-renderer'
|
|
7
|
+
})
|
|
8
|
+
export class LastUpdatedCellRendererComponent {
|
|
9
|
+
constructor(public context: CellRendererContext) {}
|
|
10
|
+
}
|
package/src/grids/server-grid-example/last-updated-data-grid-column/last-updated.data-grid-column.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { FormGroup } from '@angular/forms';
|
|
2
|
+
import { BaseColumn, ColumnConfig, gettext } from '@c8y/ngx-components';
|
|
3
|
+
import { LastUpdatedCellRendererComponent } from './last-updated.cell-renderer.component';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Defines a class for custom Last updated date column.
|
|
7
|
+
* Implements `Column` interface and sets basic properties, as well as custom components.
|
|
8
|
+
*/
|
|
9
|
+
export class LastUpdatedDataGridColumn extends BaseColumn {
|
|
10
|
+
constructor(initialColumnConfig?: ColumnConfig) {
|
|
11
|
+
super(initialColumnConfig);
|
|
12
|
+
this.name = 'lastUpdated';
|
|
13
|
+
this.path = 'lastUpdated';
|
|
14
|
+
this.header = 'Last updated';
|
|
15
|
+
|
|
16
|
+
this.cellRendererComponent = LastUpdatedCellRendererComponent;
|
|
17
|
+
|
|
18
|
+
this.filterable = true;
|
|
19
|
+
|
|
20
|
+
this.filteringConfig = {
|
|
21
|
+
/**
|
|
22
|
+
* When using Formly schema, filter chips can be automatically generated by the
|
|
23
|
+
* data-grid library based on the filtering schema definition. You do not need
|
|
24
|
+
* to manually implement the chip generation. The Formly field definitions provided
|
|
25
|
+
* in the `filteringConfig` will automatically be transformed into chips by the
|
|
26
|
+
* data-grid system.
|
|
27
|
+
*/
|
|
28
|
+
fields: [
|
|
29
|
+
{
|
|
30
|
+
type: 'object',
|
|
31
|
+
key: 'lastUpdated',
|
|
32
|
+
templateOptions: {
|
|
33
|
+
label: gettext('Show items last updated')
|
|
34
|
+
},
|
|
35
|
+
fieldGroup: [
|
|
36
|
+
{
|
|
37
|
+
type: 'date-time',
|
|
38
|
+
key: 'after',
|
|
39
|
+
templateOptions: {
|
|
40
|
+
label: gettext('from')
|
|
41
|
+
},
|
|
42
|
+
expressionProperties: {
|
|
43
|
+
'templateOptions.maxDate': (model: any) => model?.before
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: 'date-time',
|
|
48
|
+
key: 'before',
|
|
49
|
+
templateOptions: {
|
|
50
|
+
label: gettext('to')
|
|
51
|
+
},
|
|
52
|
+
expressionProperties: {
|
|
53
|
+
'templateOptions.minDate': (model: any) => model?.after
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
validators: {
|
|
58
|
+
atLeastOneFilled: {
|
|
59
|
+
expression: (formGroup: any) => {
|
|
60
|
+
const after = formGroup.get('after').value;
|
|
61
|
+
const before = formGroup.get('before').value;
|
|
62
|
+
return after || before;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
formGroup: new FormGroup({}),
|
|
69
|
+
getFilter: model => {
|
|
70
|
+
const filter: any = {};
|
|
71
|
+
const dates = model?.lastUpdated;
|
|
72
|
+
if (dates?.after || dates?.before) {
|
|
73
|
+
filter.__and = [];
|
|
74
|
+
if (dates?.after) {
|
|
75
|
+
const after = this.formatDate(dates.after);
|
|
76
|
+
filter.__and.push({
|
|
77
|
+
'lastUpdated.date': { __gt: after }
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (dates?.before) {
|
|
81
|
+
const before = this.formatDate(dates.before);
|
|
82
|
+
filter.__and.push({
|
|
83
|
+
'lastUpdated.date': { __lt: before }
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return filter;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this.sortable = true;
|
|
92
|
+
this.sortingConfig = {
|
|
93
|
+
pathSortingConfigs: [{ path: 'lastUpdated.date' }]
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
protected formatDate(dateToFormat: string): string {
|
|
98
|
+
return new Date(dateToFormat).toISOString();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { CommonModule } from '@angular/common';
|
|
2
1
|
import { NgModule } from '@angular/core';
|
|
3
|
-
import { hookNavigator, hookRoute, NavigatorNode } from '@c8y/ngx-components';
|
|
2
|
+
import { CommonModule, hookNavigator, hookRoute, NavigatorNode } from '@c8y/ngx-components';
|
|
4
3
|
import { ServerGridActionControlsModule } from './server-grid-action-controls.module';
|
|
5
4
|
|
|
6
5
|
@NgModule({
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
Pagination
|
|
11
11
|
} from '@c8y/ngx-components';
|
|
12
12
|
|
|
13
|
+
import { assign, get, identity } from 'lodash-es';
|
|
14
|
+
import { LastUpdatedDataGridColumn } from './last-updated-data-grid-column/last-updated.data-grid-column';
|
|
13
15
|
import { TypeDataGridColumn } from './type-data-grid-column/type.data-grid-column';
|
|
14
16
|
|
|
15
17
|
/** Model for custom type filtering form. */
|
|
@@ -59,7 +61,8 @@ export class ServerGridExampleService {
|
|
|
59
61
|
filterable: true,
|
|
60
62
|
sortable: true
|
|
61
63
|
},
|
|
62
|
-
new TypeDataGridColumn()
|
|
64
|
+
new TypeDataGridColumn(),
|
|
65
|
+
new LastUpdatedDataGridColumn()
|
|
63
66
|
];
|
|
64
67
|
|
|
65
68
|
return columns;
|
|
@@ -177,43 +180,6 @@ export class ServerGridExampleService {
|
|
|
177
180
|
return { icon, label };
|
|
178
181
|
}
|
|
179
182
|
|
|
180
|
-
/** Returns a query object for given settings of filtering by type. */
|
|
181
|
-
getTypeQuery(model: TypeFilteringModel): any {
|
|
182
|
-
let query: any = {};
|
|
183
|
-
|
|
184
|
-
if (model.group) {
|
|
185
|
-
query = this.queriesUtil.addOrFilter(query, { type: 'c8y_DeviceGroup' });
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (model.device) {
|
|
189
|
-
query = this.queriesUtil.addOrFilter(query, { __has: 'c8y_IsDevice' });
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (model.smartRule) {
|
|
193
|
-
query = this.queriesUtil.addOrFilter(query, {
|
|
194
|
-
type: { __in: ['c8y_SmartRule', 'c8y_PrivateSmartRule'] }
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (model.dashboard) {
|
|
199
|
-
query = this.queriesUtil.addOrFilter(query, {
|
|
200
|
-
type: { __has: 'c8y_Dashboard' }
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (model.file) {
|
|
205
|
-
query = this.queriesUtil.addOrFilter(query, {
|
|
206
|
-
type: { __has: 'c8y_IsBinary' }
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (model.application) {
|
|
211
|
-
query = this.queriesUtil.addOrFilter(query, { type: 'c8y_Application_*' });
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return query;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
183
|
/** Returns filters for given columns and pagination setup. */
|
|
218
184
|
private getFilters(columns: Column[], pagination: Pagination) {
|
|
219
185
|
return {
|
|
@@ -232,10 +198,11 @@ export class ServerGridExampleService {
|
|
|
232
198
|
}
|
|
233
199
|
|
|
234
200
|
/** Returns a query object based on columns setup. */
|
|
235
|
-
private getQueryObj(columns: Column[]): any {
|
|
201
|
+
private getQueryObj(columns: Column[], defaultFilter = {}): any {
|
|
236
202
|
return transform(columns, (query, column) => this.addColumnQuery(query, column), {
|
|
237
203
|
__filter: {},
|
|
238
|
-
__orderby: []
|
|
204
|
+
__orderby: [],
|
|
205
|
+
...defaultFilter
|
|
239
206
|
});
|
|
240
207
|
}
|
|
241
208
|
|
|
@@ -251,7 +218,17 @@ export class ServerGridExampleService {
|
|
|
251
218
|
|
|
252
219
|
// in the case of custom filtering form, we're storing the query in `externalFilterQuery.query`
|
|
253
220
|
if (column.externalFilterQuery) {
|
|
254
|
-
|
|
221
|
+
const getFilter = column.filteringConfig.getFilter || identity;
|
|
222
|
+
const queryObj = getFilter(column.externalFilterQuery);
|
|
223
|
+
|
|
224
|
+
if (queryObj.__or) {
|
|
225
|
+
query.__filter.__and = query.__filter.__and || [];
|
|
226
|
+
query.__filter.__and.push(queryObj);
|
|
227
|
+
} else if (queryObj.__and && get(query, '__filter.__and')) {
|
|
228
|
+
queryObj.__and.map(obj => query.__filter.__and.push(obj));
|
|
229
|
+
} else {
|
|
230
|
+
assign(query.__filter, queryObj);
|
|
231
|
+
}
|
|
255
232
|
}
|
|
256
233
|
}
|
|
257
234
|
|
|
@@ -1,47 +1,88 @@
|
|
|
1
1
|
import { Type } from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
import { TypeHeaderCellRendererComponent } from './type.header-cell-renderer.component';
|
|
2
|
+
import { BaseColumn, ColumnConfig, PartialFilterChipGenerationType } from '@c8y/ngx-components';
|
|
4
3
|
import { TypeCellRendererComponent } from './type.cell-renderer.component';
|
|
5
4
|
import { TypeFilteringFormRendererComponent } from './type.filtering-form-renderer.component';
|
|
5
|
+
import { TypeHeaderCellRendererComponent } from './type.header-cell-renderer.component';
|
|
6
|
+
|
|
7
|
+
const FILTER_TYPES = [
|
|
8
|
+
{ key: 'group', label: 'Group' },
|
|
9
|
+
{ key: 'device', label: 'Device' },
|
|
10
|
+
{ key: 'smartRule', label: 'Smart Rule' },
|
|
11
|
+
{ key: 'dashboard', label: 'Dashboard' },
|
|
12
|
+
{ key: 'file', label: 'File' },
|
|
13
|
+
{ key: 'application', label: 'Application' }
|
|
14
|
+
];
|
|
6
15
|
|
|
7
16
|
/**
|
|
8
|
-
* Defines a
|
|
9
|
-
* Implements `Column` interface and sets basic properties, as well as custom components.
|
|
17
|
+
* Defines a custom Type column with custom filtering form and chips generation.
|
|
10
18
|
*/
|
|
11
|
-
export class TypeDataGridColumn
|
|
12
|
-
name
|
|
13
|
-
|
|
14
|
-
header?: string;
|
|
15
|
-
dataType?: ColumnDataType;
|
|
16
|
-
|
|
17
|
-
visible?: boolean;
|
|
18
|
-
positionFixed?: boolean;
|
|
19
|
-
gridTrackSize?: string;
|
|
19
|
+
export class TypeDataGridColumn extends BaseColumn {
|
|
20
|
+
readonly name = 'type';
|
|
21
|
+
readonly header = 'Type';
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
headerCellRendererComponent: Type<any> = TypeHeaderCellRendererComponent;
|
|
24
|
+
cellRendererComponent: Type<any> = TypeCellRendererComponent;
|
|
25
|
+
sortable = false;
|
|
26
|
+
filterable = true;
|
|
27
|
+
filteringFormRendererComponent: Type<any> = TypeFilteringFormRendererComponent;
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
constructor(initialColumnConfig?: ColumnConfig) {
|
|
30
|
+
super(initialColumnConfig);
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
// Set the custom filtering configuration
|
|
33
|
+
this.filteringConfig = {
|
|
34
|
+
/**
|
|
35
|
+
* Generates filter chips based on the selected filters in the model.
|
|
36
|
+
* Each chip represents a filter and provides a way for users to visualize
|
|
37
|
+
* and remove the applied filters.
|
|
38
|
+
*
|
|
39
|
+
* @param model An object with defined structure (e.g. by schema).
|
|
40
|
+
* @returns return an array of partial filter chips with required properties 'displayValue' and 'value'.
|
|
41
|
+
*/
|
|
42
|
+
generateChips: (model): PartialFilterChipGenerationType[] => {
|
|
43
|
+
const chips = [];
|
|
29
44
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
FILTER_TYPES.forEach(type => {
|
|
46
|
+
if (model[type.key]) {
|
|
47
|
+
chips.push({
|
|
48
|
+
displayValue: type.label,
|
|
49
|
+
value: type.key,
|
|
50
|
+
remove: () => {
|
|
51
|
+
delete model[type.key];
|
|
52
|
+
return {
|
|
53
|
+
externalFilterQuery: { ...model },
|
|
54
|
+
columnName: this.name
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
34
60
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
61
|
+
return chips;
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* Transforms a filtering config model (e.g. coming from schema form component) to a query object.
|
|
65
|
+
* However, using schema form component is not necessary.
|
|
66
|
+
* Model can be defined arbitrarily but must converted to a valid query object.
|
|
67
|
+
* @param model An object with defined structure (e.g. by schema).
|
|
68
|
+
* @returns A query object to be used to generate a query string (QueryUtils).
|
|
69
|
+
*/
|
|
70
|
+
getFilter: model => {
|
|
71
|
+
const filter: any = {};
|
|
72
|
+
const ors = [];
|
|
38
73
|
|
|
39
|
-
|
|
40
|
-
|
|
74
|
+
if (model.group) ors.push({ type: 'c8y_DeviceGroup' });
|
|
75
|
+
if (model.device) ors.push({ __has: 'c8y_IsDevice' });
|
|
76
|
+
if (model.smartRule)
|
|
77
|
+
ors.push({ type: { __in: ['c8y_SmartRule', 'c8y_PrivateSmartRule'] } });
|
|
78
|
+
if (model.dashboard) ors.push({ type: { __has: 'c8y_Dashboard' } });
|
|
79
|
+
if (model.file) ors.push({ type: { __has: 'c8y_IsBinary' } });
|
|
80
|
+
if (model.application) ors.push({ type: 'c8y_Application_*' });
|
|
41
81
|
|
|
42
|
-
|
|
43
|
-
this.filteringFormRendererComponent = TypeFilteringFormRendererComponent;
|
|
82
|
+
if (ors.length) filter.__or = ors;
|
|
44
83
|
|
|
45
|
-
|
|
84
|
+
return filter;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
46
87
|
}
|
|
47
88
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Component
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
2
|
import { FilteringFormRendererContext, FormsModule } from '@c8y/ngx-components';
|
|
3
|
-
import {
|
|
3
|
+
import { TypeFilteringModel } from '../server-grid-example.service';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* This is the example component for custom filtering form.
|
|
@@ -79,12 +79,9 @@ import { ServerGridExampleService, TypeFilteringModel } from '../server-grid-exa
|
|
|
79
79
|
export class TypeFilteringFormRendererComponent {
|
|
80
80
|
model: TypeFilteringModel;
|
|
81
81
|
|
|
82
|
-
constructor(
|
|
83
|
-
public context: FilteringFormRendererContext,
|
|
84
|
-
@Inject(ServerGridExampleService) public service: ServerGridExampleService
|
|
85
|
-
) {
|
|
82
|
+
constructor(public context: FilteringFormRendererContext) {
|
|
86
83
|
// restores the settings from current column setup
|
|
87
|
-
this.model =
|
|
84
|
+
this.model = this.context.property.externalFilterQuery || {};
|
|
88
85
|
}
|
|
89
86
|
|
|
90
87
|
/**
|
|
@@ -94,10 +91,7 @@ export class TypeFilteringFormRendererComponent {
|
|
|
94
91
|
*/
|
|
95
92
|
applyFilter() {
|
|
96
93
|
this.context.applyFilter({
|
|
97
|
-
externalFilterQuery: {
|
|
98
|
-
model: this.model,
|
|
99
|
-
query: this.service.getTypeQuery(this.model)
|
|
100
|
-
}
|
|
94
|
+
externalFilterQuery: { ...this.model }
|
|
101
95
|
});
|
|
102
96
|
}
|
|
103
97
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<c8y-title>Sharing a service between plugins</c8y-title>
|
|
2
|
+
<div class="card-block">
|
|
3
|
+
<p>
|
|
4
|
+
A service instance can be used by components that need to communicate to each other or share a
|
|
5
|
+
common state. If these two components originate from different code bases, e.g. are deployed via
|
|
6
|
+
plugin-ins, then
|
|
7
|
+
<code>hookService</code>
|
|
8
|
+
comes as a way for such a shared service to be injected. The service interface might be declared
|
|
9
|
+
in a shared library known at compile time.
|
|
10
|
+
</p>
|
|
11
|
+
<p>
|
|
12
|
+
This example showcases two component instances that share a counter service. The counter service
|
|
13
|
+
interface is declared in the
|
|
14
|
+
<code>counder.model</code>
|
|
15
|
+
module which may be declared in a common library shared between plug-ins in a real world case.
|
|
16
|
+
The
|
|
17
|
+
<code>CounterHookModule</code>
|
|
18
|
+
leverages
|
|
19
|
+
<code>hookService</code>
|
|
20
|
+
to inject a service instance. This can happen in a plugin or in a shell application.
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="card-group">
|
|
24
|
+
<div class="col-md-6">
|
|
25
|
+
<div class="card">
|
|
26
|
+
<div class="card-header separator">
|
|
27
|
+
<p class="card-title">Component A</p>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="card-block">
|
|
30
|
+
<c8y-dynamic-component componentId="counter.component"></c8y-dynamic-component>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="col-md-6">
|
|
35
|
+
<div class="card">
|
|
36
|
+
<div class="card-header separator">
|
|
37
|
+
<p class="card-title">Component B</p>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="card-block">
|
|
40
|
+
<c8y-dynamic-component componentId="counter.component"></c8y-dynamic-component>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
import { CoreModule } from '@c8y/ngx-components';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This is the component that hosts the Service demo view.
|
|
6
|
+
*/
|
|
7
|
+
@Component({
|
|
8
|
+
selector: 'tut-basic-component-hook-view',
|
|
9
|
+
templateUrl: './basic-view.component.html',
|
|
10
|
+
standalone: true,
|
|
11
|
+
imports: [CoreModule]
|
|
12
|
+
})
|
|
13
|
+
export class BasicViewComponent {}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<p>
|
|
2
|
+
Hello there! I am a simple component added from a plugin by
|
|
3
|
+
<code>hookComponent</code>
|
|
4
|
+
. I use a shared counter service that has been provided in another plugin and injected by
|
|
5
|
+
<code>hookService</code>
|
|
6
|
+
.
|
|
7
|
+
</p>
|
|
8
|
+
<button
|
|
9
|
+
class="btn btn-default m-t-16"
|
|
10
|
+
type="button"
|
|
11
|
+
(click)="counter.count()"
|
|
12
|
+
>
|
|
13
|
+
Click to increment counter
|
|
14
|
+
<span class="badge badge-system">{{ counter.counter }}</span>
|
|
15
|
+
</button>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
import { ServiceRegistry } from '@c8y/ngx-components';
|
|
3
|
+
import { ICounterService } from '../counter/counter.model';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'tut-counter-component',
|
|
7
|
+
templateUrl: './counter.component.html',
|
|
8
|
+
standalone: true
|
|
9
|
+
})
|
|
10
|
+
export class CounterComponent {
|
|
11
|
+
counter: ICounterService;
|
|
12
|
+
|
|
13
|
+
constructor(registry: ServiceRegistry) {
|
|
14
|
+
/**
|
|
15
|
+
* To retrieve an instance of a service injected by `hookService` you can use ServiceRegistry.get(key) method.
|
|
16
|
+
* It will return all injected service instances in a type-safe manner if there is a typed extension key declared.
|
|
17
|
+
* For an example of such declaration check the declarations in `counter/counter.model.ts`.
|
|
18
|
+
*/
|
|
19
|
+
this.counter = registry.get('counter').at(0);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NgModule } from '@angular/core';
|
|
2
|
+
import { NavigatorNode, hookComponent, hookNavigator, hookRoute } from '@c8y/ngx-components';
|
|
3
|
+
|
|
4
|
+
@NgModule({
|
|
5
|
+
providers: [
|
|
6
|
+
/* Hook the Service hook demo view */
|
|
7
|
+
hookRoute({
|
|
8
|
+
path: 'hooks/service',
|
|
9
|
+
loadComponent: () => import('./basic-view.component').then(m => m.BasicViewComponent)
|
|
10
|
+
}),
|
|
11
|
+
hookNavigator(
|
|
12
|
+
new NavigatorNode({
|
|
13
|
+
priority: 40,
|
|
14
|
+
path: 'hooks/service',
|
|
15
|
+
icon: 'gears',
|
|
16
|
+
label: 'Service',
|
|
17
|
+
parent: 'Hooks'
|
|
18
|
+
})
|
|
19
|
+
),
|
|
20
|
+
/* Hook a client component for the service provided via `hookService` */
|
|
21
|
+
hookComponent({
|
|
22
|
+
id: 'counter.component',
|
|
23
|
+
label: 'Counter component',
|
|
24
|
+
description: 'This component can count',
|
|
25
|
+
loadComponent: () => import('./counter.component').then(m => m.CounterComponent)
|
|
26
|
+
})
|
|
27
|
+
]
|
|
28
|
+
})
|
|
29
|
+
export class ServiceHookModule {}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declare the contract of the service that will be injected via `hookService` in an interface.
|
|
3
|
+
* This interface will be used also to bind a type to the key used for providing and retrieving the service.
|
|
4
|
+
* This interface should be declared in a module that is shared between modules that use the service and those that implement and inject it.
|
|
5
|
+
*/
|
|
6
|
+
export interface ICounterService {
|
|
7
|
+
/**
|
|
8
|
+
* Current counter state.
|
|
9
|
+
*/
|
|
10
|
+
counter: number;
|
|
11
|
+
/**
|
|
12
|
+
* Increment counter value.
|
|
13
|
+
*/
|
|
14
|
+
count: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare global {
|
|
18
|
+
/**
|
|
19
|
+
* The `CumulocityServiceRegistry` namespaces is declared in `@c8y/ngx-components` as part of the global scope.
|
|
20
|
+
* This allows you to augment the service registry by adding your typed extension keys.
|
|
21
|
+
*/
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
23
|
+
namespace CumulocityServiceRegistry {
|
|
24
|
+
interface ExtensionKeys {
|
|
25
|
+
/**
|
|
26
|
+
* The extension key used for injecting and retrieving hooked services.
|
|
27
|
+
* To provide a service with a key provide the service using `hookService`:
|
|
28
|
+
*
|
|
29
|
+
* ```typescript
|
|
30
|
+
* @NgModule({
|
|
31
|
+
* providers: [hookService('counter', CounterService)]
|
|
32
|
+
* })
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* In your client code you can use `ServiceRegistry` to retrieve an instance of the injected service:
|
|
36
|
+
* ```typescript
|
|
37
|
+
* ServiceRegistry.get(key)
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
counter: ICounterService;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NgModule } from '@angular/core';
|
|
2
|
+
import { hookService } from '@c8y/ngx-components';
|
|
3
|
+
import { CounterService } from './counter.service';
|
|
4
|
+
|
|
5
|
+
@NgModule({
|
|
6
|
+
/**
|
|
7
|
+
* To provide a service using `hookService`, you pass a key that clients will use to retrieve the service instance.
|
|
8
|
+
* By extending the `ExtensionKeys` interface in the `CumulocityServiceRegistry` namespace, you declare the key for your service.
|
|
9
|
+
* `hookService` then enforces type safety, ensuring only services that implement the corresponding interface can be provided for that key.
|
|
10
|
+
*/
|
|
11
|
+
providers: [hookService('counter', CounterService)]
|
|
12
|
+
})
|
|
13
|
+
export class CounterHookModule {}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NgModule } from '@angular/core';
|
|
2
|
+
import { ServiceHookModule } from './client/service-hook.module';
|
|
3
|
+
import { CounterHookModule } from './counter/counter.module';
|
|
4
|
+
|
|
5
|
+
@NgModule({
|
|
6
|
+
imports: [ServiceHookModule, CounterHookModule]
|
|
7
|
+
})
|
|
8
|
+
/**
|
|
9
|
+
* `codex-tutorial-example` component supports a single module only, hence the module providing the service (CounterHookModule)
|
|
10
|
+
* and the one consuming the service (ServiceHookModule) need to be combined in a single module.
|
|
11
|
+
* In the general case, both modules above can be added as remotes separately.
|
|
12
|
+
*/
|
|
13
|
+
export class ServiceHookCodexSampleModule {}
|
|
@@ -19,4 +19,14 @@
|
|
|
19
19
|
[properties]="specifiedPackageVersionProperties"
|
|
20
20
|
></c8y-properties-list>
|
|
21
21
|
<!-- /important -->
|
|
22
|
+
|
|
23
|
+
<!-- important -->
|
|
24
|
+
<c8y-properties-list
|
|
25
|
+
[title]="'Properties with no parse enabled'"
|
|
26
|
+
[data]="[]"
|
|
27
|
+
[emptyLabel]="'--'"
|
|
28
|
+
[noParse]="true"
|
|
29
|
+
[properties]="customPropertiesNoParse"
|
|
30
|
+
></c8y-properties-list>
|
|
31
|
+
<!-- /important -->
|
|
22
32
|
</div>
|
|
@@ -94,4 +94,25 @@ export class PropertiesListExampleComponent {
|
|
|
94
94
|
key: 'custom'
|
|
95
95
|
}
|
|
96
96
|
];
|
|
97
|
+
|
|
98
|
+
readonly customPropertiesNoParse: PropertiesListItem[] = [
|
|
99
|
+
{
|
|
100
|
+
label: 'String property',
|
|
101
|
+
key: 'string',
|
|
102
|
+
value: this.customData.string,
|
|
103
|
+
type: 'string'
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
label: 'Number property',
|
|
107
|
+
key: 'number',
|
|
108
|
+
value: this.customData.number,
|
|
109
|
+
type: 'string'
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
label: 'Array property',
|
|
113
|
+
key: 'array',
|
|
114
|
+
value: this.customData.array,
|
|
115
|
+
type: 'array'
|
|
116
|
+
}
|
|
117
|
+
];
|
|
97
118
|
}
|