@sumaris-net/ngx-components 18.17.3 → 18.17.5
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/doc/changelog.md +3 -0
- package/esm2022/public_api.mjs +3 -1
- package/esm2022/src/app/admin/users/person.filter.mjs +76 -2
- package/esm2022/src/app/admin/users/person.service.mjs +2 -2
- package/esm2022/src/app/admin/users/users-select.modal.mjs +167 -0
- package/esm2022/src/app/admin/users/users.mjs +130 -39
- package/esm2022/src/app/admin/users/users.module.mjs +7 -4
- package/esm2022/src/app/admin/users/users.utils.mjs +29 -0
- package/esm2022/src/app/core/services/local-settings.service.mjs +5 -1
- package/esm2022/src/app/core/table/async-table.class.mjs +6 -3
- package/esm2022/src/app/core/table/table.class.mjs +7 -3
- package/esm2022/src/app/shared/toolbar/toolbar.mjs +12 -7
- package/esm2022/src/environments/environment.mjs +2 -1
- package/fesm2022/sumaris-net.ngx-components.mjs +417 -49
- package/fesm2022/sumaris-net.ngx-components.mjs.map +1 -1
- package/package.json +1 -1
- package/public_api.d.ts +2 -0
- package/src/app/admin/users/person.filter.d.ts +11 -0
- package/src/app/admin/users/users-select.modal.d.ts +73 -0
- package/src/app/admin/users/users.d.ts +29 -4
- package/src/app/admin/users/users.module.d.ts +9 -8
- package/src/app/admin/users/users.utils.d.ts +6 -0
- package/src/app/shared/inputs.d.ts +1 -1
- package/src/app/shared/toolbar/toolbar.d.ts +3 -3
- package/src/assets/i18n/en-US.json +5 -0
- package/src/assets/i18n/en.json +5 -0
- package/src/assets/i18n/fr.json +5 -0
- package/src/assets/manifest.json +1 -1
package/package.json
CHANGED
package/public_api.d.ts
CHANGED
|
@@ -297,6 +297,8 @@ export * from './src/app/admin/users/person.validator';
|
|
|
297
297
|
export * from './src/app/admin/users/person.service';
|
|
298
298
|
export * from './src/app/admin/users/users.module';
|
|
299
299
|
export * from './src/app/admin/users/users';
|
|
300
|
+
export * from './src/app/admin/users/users.utils';
|
|
301
|
+
export * from './src/app/admin/users/users-select.modal';
|
|
300
302
|
export * from './src/app/shared/testing/tests.page';
|
|
301
303
|
export * from './src/app/shared/material/material.testing.module';
|
|
302
304
|
export * from './src/app/shared/material/autocomplete/testing/autocomplete.test';
|
|
@@ -4,6 +4,7 @@ import { EntityAsObjectOptions } from '../../core/services/model/entity.model';
|
|
|
4
4
|
import { StoreObject } from '@apollo/client/core';
|
|
5
5
|
import { FilterFn } from '../../shared/types';
|
|
6
6
|
export declare class PersonFilter extends EntityFilter<PersonFilter, Person> {
|
|
7
|
+
static FIELDS: (keyof PersonFilter)[];
|
|
7
8
|
static fromObject: (source: any, opts?: any) => PersonFilter;
|
|
8
9
|
static searchFilter(source: any): FilterFn<Person>;
|
|
9
10
|
email: string;
|
|
@@ -15,8 +16,18 @@ export declare class PersonFilter extends EntityFilter<PersonFilter, Person> {
|
|
|
15
16
|
excludedIds: number[];
|
|
16
17
|
searchAttribute: string;
|
|
17
18
|
searchAttributes: string[];
|
|
19
|
+
additionalFields: AdditionalFields;
|
|
18
20
|
constructor();
|
|
19
21
|
fromObject(source: any, opts?: EntityAsObjectOptions): void;
|
|
20
22
|
asObject(opts?: EntityAsObjectOptions): StoreObject;
|
|
21
23
|
protected buildFilter(): FilterFn<Person>[];
|
|
22
24
|
}
|
|
25
|
+
export declare class AdditionalFields {
|
|
26
|
+
[key: string]: any;
|
|
27
|
+
static fromObject(source: any, opts?: any): {
|
|
28
|
+
[key: string]: any;
|
|
29
|
+
};
|
|
30
|
+
static asObject(source: any, target?: any, opts?: EntityAsObjectOptions): {
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { AfterViewInit, ChangeDetectorRef, OnInit } from '@angular/core';
|
|
2
|
+
import { ModalController } from '@ionic/angular';
|
|
3
|
+
import { Person } from '../../core/services/model/person.model';
|
|
4
|
+
import { LocalSettingsService } from '../../core/services/local-settings.service';
|
|
5
|
+
import { RxState } from '@rx-angular/state';
|
|
6
|
+
import { TableElement } from '@e-is/ngx-material-table';
|
|
7
|
+
import * as i0 from "@angular/core";
|
|
8
|
+
export interface AppSelectUsersModalOptions {
|
|
9
|
+
debug?: boolean;
|
|
10
|
+
title?: string;
|
|
11
|
+
showFilter?: boolean;
|
|
12
|
+
canEdit?: boolean;
|
|
13
|
+
hideFooter?: boolean;
|
|
14
|
+
showMessageButton?: boolean;
|
|
15
|
+
mobile?: boolean;
|
|
16
|
+
multiple?: boolean;
|
|
17
|
+
selectedUsers?: Person[];
|
|
18
|
+
}
|
|
19
|
+
interface AppSelectUsersModalState {
|
|
20
|
+
showFilter: boolean;
|
|
21
|
+
canEdit: boolean;
|
|
22
|
+
showMessageButton: boolean;
|
|
23
|
+
mobile: boolean;
|
|
24
|
+
multiple: boolean;
|
|
25
|
+
selectedUsers: Person[];
|
|
26
|
+
showEmailColumn: boolean;
|
|
27
|
+
showPubkeyColumn: boolean;
|
|
28
|
+
showUpdateDateColumn: boolean;
|
|
29
|
+
showCreationDateColumn: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare class AppSelectUsersModal implements OnInit, AfterViewInit, AppSelectUsersModalOptions {
|
|
32
|
+
protected settings: LocalSettingsService;
|
|
33
|
+
protected viewCtrl: ModalController;
|
|
34
|
+
protected cd: ChangeDetectorRef;
|
|
35
|
+
protected state: RxState<AppSelectUsersModalState>;
|
|
36
|
+
debug: boolean;
|
|
37
|
+
title: string;
|
|
38
|
+
settingsId: string;
|
|
39
|
+
showEmailColumn: boolean;
|
|
40
|
+
showPubkeyColumn: boolean;
|
|
41
|
+
showUpdateDateColumn: boolean;
|
|
42
|
+
showCreationDateColumn: boolean;
|
|
43
|
+
private usersTable;
|
|
44
|
+
constructor(settings: LocalSettingsService, viewCtrl: ModalController, cd: ChangeDetectorRef, state: RxState<AppSelectUsersModalState>);
|
|
45
|
+
get showFilter(): boolean;
|
|
46
|
+
set showFilter(value: boolean);
|
|
47
|
+
get canEdit(): boolean;
|
|
48
|
+
set canEdit(value: boolean);
|
|
49
|
+
get showMessageButton(): boolean;
|
|
50
|
+
set showMessageButton(value: boolean);
|
|
51
|
+
get mobile(): boolean;
|
|
52
|
+
set mobile(value: boolean);
|
|
53
|
+
get multiple(): boolean;
|
|
54
|
+
set multiple(value: boolean);
|
|
55
|
+
get selectedUsers(): Person[];
|
|
56
|
+
set selectedUsers(value: Person[]);
|
|
57
|
+
get hasSelection(): boolean;
|
|
58
|
+
ngOnInit(): void;
|
|
59
|
+
ngAfterViewInit(): void;
|
|
60
|
+
cancel(): void;
|
|
61
|
+
validate(): Promise<any>;
|
|
62
|
+
protected markForCheck(): void;
|
|
63
|
+
protected clickRow(row: TableElement<Person>): void;
|
|
64
|
+
protected onSelectionChange(): void;
|
|
65
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<AppSelectUsersModal, never>;
|
|
66
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<AppSelectUsersModal, "app-select-users-modal", never, { "debug": { "alias": "debug"; "required": false; }; "title": { "alias": "title"; "required": false; }; "settingsId": { "alias": "settingsId"; "required": false; }; "showEmailColumn": { "alias": "showEmailColumn"; "required": false; }; "showPubkeyColumn": { "alias": "showPubkeyColumn"; "required": false; }; "showUpdateDateColumn": { "alias": "showUpdateDateColumn"; "required": false; }; "showCreationDateColumn": { "alias": "showCreationDateColumn"; "required": false; }; "showFilter": { "alias": "showFilter"; "required": false; }; "canEdit": { "alias": "canEdit"; "required": false; }; "showMessageButton": { "alias": "showMessageButton"; "required": false; }; "mobile": { "alias": "mobile"; "required": false; }; "multiple": { "alias": "multiple"; "required": false; }; "selectedUsers": { "alias": "selectedUsers"; "required": false; }; }, {}, never, never, false, never>;
|
|
67
|
+
static ngAcceptInputType_showFilter: unknown;
|
|
68
|
+
static ngAcceptInputType_canEdit: unknown;
|
|
69
|
+
static ngAcceptInputType_showMessageButton: unknown;
|
|
70
|
+
static ngAcceptInputType_mobile: unknown;
|
|
71
|
+
static ngAcceptInputType_multiple: unknown;
|
|
72
|
+
}
|
|
73
|
+
export {};
|
|
@@ -12,6 +12,7 @@ import { ConfigService } from '../../core/services/config.service';
|
|
|
12
12
|
import { MatExpansionPanel } from '@angular/material/expansion';
|
|
13
13
|
import { MessageService } from '../../social/message/message.service';
|
|
14
14
|
import { MenuService } from '../../core/menu/menu.service';
|
|
15
|
+
import { ModalController } from '@ionic/angular';
|
|
15
16
|
import * as i0 from "@angular/core";
|
|
16
17
|
export declare class UsersPage extends AppTable<Person, PersonFilter> implements OnInit {
|
|
17
18
|
protected accountService: AccountService;
|
|
@@ -20,8 +21,8 @@ export declare class UsersPage extends AppTable<Person, PersonFilter> implements
|
|
|
20
21
|
protected dataService: PersonService;
|
|
21
22
|
protected messageService: MessageService;
|
|
22
23
|
protected menuService: MenuService;
|
|
23
|
-
|
|
24
|
-
readonly
|
|
24
|
+
protected modalCtrl: ModalController;
|
|
25
|
+
readonly canDebug: boolean;
|
|
25
26
|
readonly filterForm: UntypedFormGroup;
|
|
26
27
|
readonly profiles: import("../../core/services/model/person.model").UserProfileLabel[];
|
|
27
28
|
readonly statusList: readonly import("../../core/services/model/referential.model").IStatus[];
|
|
@@ -30,16 +31,33 @@ export declare class UsersPage extends AppTable<Person, PersonFilter> implements
|
|
|
30
31
|
protected additionalFields: FormFieldDefinition[];
|
|
31
32
|
protected filterCriteriaCount: number;
|
|
32
33
|
protected defaultCompact: boolean;
|
|
34
|
+
get filterableAdditionalFields(): FormFieldDefinition[];
|
|
33
35
|
set showUsernameColumn(value: boolean);
|
|
34
36
|
get showUsernameColumn(): boolean;
|
|
35
37
|
set showUsernameExtranetColumn(value: boolean);
|
|
36
38
|
get showUsernameExtranetColumn(): boolean;
|
|
39
|
+
set showEmailColumn(value: boolean);
|
|
40
|
+
get showEmailColumn(): boolean;
|
|
41
|
+
set showPubkeyColumn(value: boolean);
|
|
42
|
+
get showPubkeyColumn(): boolean;
|
|
43
|
+
set showCreationDateColumn(value: boolean);
|
|
44
|
+
get showCreationDateColumn(): boolean;
|
|
45
|
+
set showUpdateDateColumn(value: boolean);
|
|
46
|
+
get showUpdateDateColumn(): boolean;
|
|
47
|
+
get showFormButtons(): boolean;
|
|
48
|
+
title: string;
|
|
49
|
+
canSendMessage: boolean;
|
|
50
|
+
canEdit: boolean;
|
|
37
51
|
compact: boolean;
|
|
38
52
|
usePageSettings: boolean;
|
|
39
53
|
sticky: boolean;
|
|
40
54
|
stickyEnd: boolean;
|
|
41
55
|
canDownload: boolean;
|
|
42
|
-
|
|
56
|
+
inModal: boolean;
|
|
57
|
+
showFooter: boolean;
|
|
58
|
+
showToolbar: boolean;
|
|
59
|
+
showPaginator: boolean;
|
|
60
|
+
constructor(injector: Injector, formBuilder: UntypedFormBuilder, accountService: AccountService, validatorService: ValidatorService, configService: ConfigService, dataService: PersonService, messageService: MessageService, menuService: MenuService, modalCtrl: ModalController, environment: any);
|
|
43
61
|
filterExpansionPanel: MatExpansionPanel;
|
|
44
62
|
get firstUserColumn(): string;
|
|
45
63
|
ngOnInit(): Promise<void>;
|
|
@@ -53,11 +71,18 @@ export declare class UsersPage extends AppTable<Person, PersonFilter> implements
|
|
|
53
71
|
}): Promise<void>;
|
|
54
72
|
toggleCompactMode(): Promise<void>;
|
|
55
73
|
exportToCsv(event: Event): Promise<void>;
|
|
74
|
+
protected devToggleDebug(): Promise<void>;
|
|
56
75
|
static ɵfac: i0.ɵɵFactoryDeclaration<UsersPage, never>;
|
|
57
|
-
static ɵcmp: i0.ɵɵComponentDeclaration<UsersPage, "app-users-table", never, { "compact": { "alias": "compact"; "required": false; }; "usePageSettings": { "alias": "usePageSettings"; "required": false; }; "sticky": { "alias": "sticky"; "required": false; }; "stickyEnd": { "alias": "stickyEnd"; "required": false; }; "canDownload": { "alias": "canDownload"; "required": false; }; }, {}, never, never, false, never>;
|
|
76
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<UsersPage, "app-users-table", never, { "showUsernameColumn": { "alias": "showUsernameColumn"; "required": false; }; "showUsernameExtranetColumn": { "alias": "showUsernameExtranetColumn"; "required": false; }; "showEmailColumn": { "alias": "showEmailColumn"; "required": false; }; "showPubkeyColumn": { "alias": "showPubkeyColumn"; "required": false; }; "showCreationDateColumn": { "alias": "showCreationDateColumn"; "required": false; }; "showUpdateDateColumn": { "alias": "showUpdateDateColumn"; "required": false; }; "title": { "alias": "title"; "required": false; }; "canSendMessage": { "alias": "canSendMessage"; "required": false; }; "canEdit": { "alias": "canEdit"; "required": false; }; "compact": { "alias": "compact"; "required": false; }; "usePageSettings": { "alias": "usePageSettings"; "required": false; }; "sticky": { "alias": "sticky"; "required": false; }; "stickyEnd": { "alias": "stickyEnd"; "required": false; }; "canDownload": { "alias": "canDownload"; "required": false; }; "inModal": { "alias": "inModal"; "required": false; }; "showFooter": { "alias": "showFooter"; "required": false; }; "showToolbar": { "alias": "showToolbar"; "required": false; }; "showPaginator": { "alias": "showPaginator"; "required": false; }; }, {}, never, never, false, never>;
|
|
77
|
+
static ngAcceptInputType_canSendMessage: unknown;
|
|
78
|
+
static ngAcceptInputType_canEdit: unknown;
|
|
58
79
|
static ngAcceptInputType_compact: unknown;
|
|
59
80
|
static ngAcceptInputType_usePageSettings: unknown;
|
|
60
81
|
static ngAcceptInputType_sticky: unknown;
|
|
61
82
|
static ngAcceptInputType_stickyEnd: unknown;
|
|
62
83
|
static ngAcceptInputType_canDownload: unknown;
|
|
84
|
+
static ngAcceptInputType_inModal: unknown;
|
|
85
|
+
static ngAcceptInputType_showFooter: unknown;
|
|
86
|
+
static ngAcceptInputType_showToolbar: unknown;
|
|
87
|
+
static ngAcceptInputType_showPaginator: unknown;
|
|
63
88
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import * as i0 from "@angular/core";
|
|
2
2
|
import * as i1 from "./users";
|
|
3
|
-
import * as i2 from "
|
|
4
|
-
import * as i3 from "
|
|
5
|
-
import * as i4 from "
|
|
6
|
-
import * as i5 from "../../
|
|
7
|
-
import * as i6 from "../../
|
|
8
|
-
import * as i7 from "../../shared/
|
|
9
|
-
import * as i8 from "
|
|
3
|
+
import * as i2 from "./users-select.modal";
|
|
4
|
+
import * as i3 from "ngx-jdenticon";
|
|
5
|
+
import * as i4 from "@ngx-translate/core";
|
|
6
|
+
import * as i5 from "../../core/core.module";
|
|
7
|
+
import * as i6 from "../../social/social.module";
|
|
8
|
+
import * as i7 from "../../shared/material/material.module";
|
|
9
|
+
import * as i8 from "../../shared/debug/debug.module";
|
|
10
|
+
import * as i9 from "@angular/material/form-field";
|
|
10
11
|
export declare class AdminUsersModule {
|
|
11
12
|
static ɵfac: i0.ɵɵFactoryDeclaration<AdminUsersModule, never>;
|
|
12
|
-
static ɵmod: i0.ɵɵNgModuleDeclaration<AdminUsersModule, [typeof i1.UsersPage], [typeof
|
|
13
|
+
static ɵmod: i0.ɵɵNgModuleDeclaration<AdminUsersModule, [typeof i1.UsersPage, typeof i2.AppSelectUsersModal], [typeof i3.NgxJdenticonModule, typeof i4.TranslateModule, typeof i5.CoreModule, typeof i6.SocialModule, typeof i7.SharedMaterialModule, typeof i8.SharedDebugModule, typeof i9.MatFormFieldModule], [typeof i6.SocialModule, typeof i4.TranslateModule, typeof i1.UsersPage, typeof i2.AppSelectUsersModal]>;
|
|
13
14
|
static ɵinj: i0.ɵɵInjectorDeclaration<AdminUsersModule>;
|
|
14
15
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AppSelectUsersModalOptions } from './users-select.modal';
|
|
2
|
+
import { ModalController } from '@ionic/angular';
|
|
3
|
+
import { Person } from '../../core/services/model/person.model';
|
|
4
|
+
export declare class UsersUtils {
|
|
5
|
+
static openSelectUsersModal(modalCtrl: ModalController, opts?: Partial<AppSelectUsersModalOptions>): Promise<Person[]>;
|
|
6
|
+
}
|
|
@@ -17,7 +17,7 @@ export interface InputElement extends FocusableElement {
|
|
|
17
17
|
}
|
|
18
18
|
export declare function isInputElement(object: any): object is InputElement;
|
|
19
19
|
export declare function asInputElement<T = any>(object: ElementRef<T>): InputElement | undefined;
|
|
20
|
-
export declare function tabindexComparator(a: InputElement, b: InputElement):
|
|
20
|
+
export declare function tabindexComparator(a: InputElement, b: InputElement): 1 | -1 | 0;
|
|
21
21
|
export interface CanGainFocusOptions {
|
|
22
22
|
minTabindex?: number;
|
|
23
23
|
maxTabindex?: number;
|
|
@@ -19,8 +19,8 @@ export declare class ToolbarComponent implements ToolbarToken, OnInit, OnDestroy
|
|
|
19
19
|
private route;
|
|
20
20
|
private router;
|
|
21
21
|
private navController;
|
|
22
|
-
private routerOutlet;
|
|
23
22
|
private cd;
|
|
23
|
+
private routerOutlet;
|
|
24
24
|
protected progressBarService: IProgressBarService;
|
|
25
25
|
private _closeTapCount;
|
|
26
26
|
private _validateTapCount;
|
|
@@ -50,7 +50,7 @@ export declare class ToolbarComponent implements ToolbarToken, OnInit, OnDestroy
|
|
|
50
50
|
onSearch: EventEmitter<CustomEvent<ISearchbarSearchbarChangeEventDetail>>;
|
|
51
51
|
searchbar: IonSearchbar;
|
|
52
52
|
menuToggle: IonMenuToggle;
|
|
53
|
-
constructor(route: ActivatedRoute, router: Router, navController: NavController,
|
|
53
|
+
constructor(route: ActivatedRoute, router: Router, navController: NavController, cd: ChangeDetectorRef, routerOutlet: IonRouterOutlet, progressBarService: IProgressBarService);
|
|
54
54
|
ngOnInit(): void;
|
|
55
55
|
ngOnDestroy(): void;
|
|
56
56
|
toggleSearchBar(): Promise<void>;
|
|
@@ -60,7 +60,7 @@ export declare class ToolbarComponent implements ToolbarToken, OnInit, OnDestroy
|
|
|
60
60
|
tapValidate(event: HammerTapEvent): void;
|
|
61
61
|
protected ionSearchBarChanged(event: Event): void;
|
|
62
62
|
protected markForCheck(): void;
|
|
63
|
-
static ɵfac: i0.ɵɵFactoryDeclaration<ToolbarComponent, [null, null, null, null,
|
|
63
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ToolbarComponent, [null, null, null, null, { optional: true; }, { optional: true; }]>;
|
|
64
64
|
static ɵcmp: i0.ɵɵComponentDeclaration<ToolbarComponent, "app-toolbar", never, { "progressBarMode": { "alias": "progressBarMode"; "required": false; }; "title": { "alias": "title"; "required": false; }; "color": { "alias": "color"; "required": false; }; "class": { "alias": "class"; "required": false; }; "id": { "alias": "id"; "required": false; }; "backHref": { "alias": "backHref"; "required": false; }; "defaultBackHref": { "alias": "defaultBackHref"; "required": false; }; "hasValidate": { "alias": "hasValidate"; "required": false; }; "hasClose": { "alias": "hasClose"; "required": false; }; "hasSearch": { "alias": "hasSearch"; "required": false; }; "canGoBack": { "alias": "canGoBack"; "required": false; }; "canShowMenu": { "alias": "canShowMenu"; "required": false; }; }, { "onValidate": "onValidate"; "onClose": "onClose"; "onValidateAndClose": "onValidateAndClose"; "onBackClick": "onBackClick"; "onSearch": "onSearch"; }, never, ["[slot=start]", "ion-title, ion-segment", "ion-buttons[slot=end]", "[slot=end]"], false, never>;
|
|
65
65
|
static ngAcceptInputType_hasValidate: unknown;
|
|
66
66
|
static ngAcceptInputType_hasClose: unknown;
|
package/src/assets/i18n/en.json
CHANGED
package/src/assets/i18n/fr.json
CHANGED
|
@@ -488,6 +488,11 @@
|
|
|
488
488
|
"FILTER": {
|
|
489
489
|
"SEARCH": "Recherche : nom, prénom..."
|
|
490
490
|
}
|
|
491
|
+
},
|
|
492
|
+
"SELECT": {
|
|
493
|
+
"TITLE": "Sélectionner des utilisateurs",
|
|
494
|
+
"BTN_TEST_MODAL": "Tester la modale de sélection",
|
|
495
|
+
"SELECTED_COUNT": "{{count}} utilisateur(s) sélectionné(s)"
|
|
491
496
|
}
|
|
492
497
|
},
|
|
493
498
|
"NETWORK": {
|
package/src/assets/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "ngx-sumaris-components",
|
|
3
3
|
"short_name": "ngx-sumaris-components",
|
|
4
4
|
"manifest_version": 1,
|
|
5
|
-
"version": "18.17.
|
|
5
|
+
"version": "18.17.5",
|
|
6
6
|
"default_locale": "fr",
|
|
7
7
|
"description": "Angular components for building beautiful and responsive Apps",
|
|
8
8
|
"icons": [{
|