48 changed files with 3002 additions and 62 deletions
@ -0,0 +1,49 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<form (ngSubmit)="save()" style="width: 850px;max-height: 100vh;display: grid;grid-template-rows: min-content 4px minmax(auto, 1fr) min-content;"> |
|||
<mat-toolbar color="primary"> |
|||
<h2>{{ 'mobile.add-application' | translate }}</h2> |
|||
<span fxFlex></span> |
|||
<div tb-help="mobileApplication"></div> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div *ngIf="!(isLoading$ | async)"></div> |
|||
<div mat-dialog-content style="padding: 0"> |
|||
<tb-mobile-app #mobileAppComponent></tb-mobile-app> |
|||
</div> |
|||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
cdkFocusInitial |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || mobileAppComponent.entityForm?.invalid || !mobileAppComponent.entityForm?.dirty"> |
|||
{{ 'action.add' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,81 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { AfterViewInit, Component, Inject, OnDestroy, SkipSelf, ViewChild } from '@angular/core'; |
|||
import { ErrorStateMatcher } from '@angular/material/core'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms'; |
|||
import { MobileApp } from '@shared/models/mobile-app.models'; |
|||
import { MobileAppComponent } from '@home/pages/mobile/applications/mobile-app.component'; |
|||
import { PlatformType } from '@shared/models/oauth2.models'; |
|||
import { MobileAppService } from '@core/http/mobile-app.service'; |
|||
|
|||
export interface MobileAppDialogData { |
|||
platformType: PlatformType; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-mobile-app-dialog', |
|||
templateUrl: './mobile-app-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: MobileAppDialogComponent}], |
|||
styleUrls: [] |
|||
}) |
|||
export class MobileAppDialogComponent extends DialogComponent<MobileAppDialogComponent, MobileApp> implements OnDestroy, AfterViewInit, ErrorStateMatcher { |
|||
|
|||
submitted = false; |
|||
|
|||
@ViewChild('mobileAppComponent', {static: true}) mobileAppComponent: MobileAppComponent; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
protected dialogRef: MatDialogRef<MobileAppDialogComponent, MobileApp>, |
|||
private mobileAppService: MobileAppService, |
|||
@Inject(MAT_DIALOG_DATA) public data: MobileAppDialogData, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher) { |
|||
super(store, router, dialogRef); |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
setTimeout(() => { |
|||
this.mobileAppComponent.entityForm.markAsDirty(); |
|||
this.mobileAppComponent.entityForm.patchValue({platformType: this.data.platformType}); |
|||
this.mobileAppComponent.entityForm.get('platformType').disable({emitEvent: false}); |
|||
}, 0); |
|||
} |
|||
|
|||
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
|||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
|||
const customErrorState = !!(control && control.invalid && this.submitted); |
|||
return originalErrorState || customErrorState; |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
save() { |
|||
this.submitted = true; |
|||
if (this.mobileAppComponent.entityForm.valid) { |
|||
this.mobileAppService.saveMobileApp(this.mobileAppComponent.entityFormValue()).subscribe( |
|||
app => this.dialogRef.close(app) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Routes } from '@angular/router'; |
|||
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; |
|||
import { Authority } from '@shared/models/authority.enum'; |
|||
import { MenuId } from '@core/services/menu.models'; |
|||
import { MobileBundleTableConfigResolver } from '@home/pages/mobile/bundes/mobile-bundle-table-config.resolve'; |
|||
import { NgModule } from '@angular/core'; |
|||
|
|||
export const bundlesRoutes: Routes = [ |
|||
{ |
|||
path: 'bundles', |
|||
component: EntitiesTableComponent, |
|||
data: { |
|||
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], |
|||
title: 'mobile.bundles', |
|||
breadcrumb: { |
|||
menuId: MenuId.mobile_bundles |
|||
} |
|||
}, |
|||
resolve: { |
|||
entitiesTableConfig: MobileBundleTableConfigResolver |
|||
} |
|||
} |
|||
]; |
|||
|
|||
@NgModule({ |
|||
providers: [ |
|||
MobileBundleTableConfigResolver |
|||
], |
|||
imports: [], |
|||
exports: [] |
|||
}) |
|||
export class MobileBundleRoutingModule { } |
|||
@ -0,0 +1,49 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { HomeComponentsModule } from '@home/components/home-components.module'; |
|||
import { MobileBundleRoutingModule } from '@home/pages/mobile/bundes/bundles-routing.module'; |
|||
import { MobileBundleTableHeaderComponent } from '@home/pages/mobile/bundes/mobile-bundle-table-header.component'; |
|||
import { MobileBundleDialogComponent } from '@home/pages/mobile/bundes/mobile-bundle-dialog.component'; |
|||
import { MobileLayoutComponent } from '@home/pages/mobile/bundes/layout/mobile-layout.component'; |
|||
import { MobilePageItemRowComponent } from '@home/pages/mobile/bundes/layout/mobile-page-item-row.component'; |
|||
import { AddMobilePageDialogComponent } from '@home/pages/mobile/bundes/layout/add-mobile-page-dialog.component'; |
|||
import { CustomMobilePageComponent } from '@home/pages/mobile/bundes/layout/custom-mobile-page.component'; |
|||
import { CustomMobilePagePanelComponent } from '@home/pages/mobile/bundes/layout/custom-mobile-page-panel.component'; |
|||
import { DefaultMobilePagePanelComponent } from '@home/pages/mobile/bundes/layout/default-mobile-page-panel.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: [ |
|||
MobileBundleTableHeaderComponent, |
|||
MobileBundleDialogComponent, |
|||
MobileLayoutComponent, |
|||
MobilePageItemRowComponent, |
|||
AddMobilePageDialogComponent, |
|||
CustomMobilePageComponent, |
|||
CustomMobilePagePanelComponent, |
|||
DefaultMobilePagePanelComponent, |
|||
], |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
HomeComponentsModule, |
|||
MobileBundleRoutingModule, |
|||
] |
|||
}) |
|||
export class MobileBundlesModule { } |
|||
@ -0,0 +1,44 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<mat-toolbar color="primary"> |
|||
<h2 translate>mobile.add-specific-page</h2> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<div mat-dialog-content fxLayout="column"> |
|||
<tb-mobile-page-item [formControl]="customMobilePage"> |
|||
</tb-mobile-page-item> |
|||
</div> |
|||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async) || customMobilePage.invalid || !customMobilePage.dirty" |
|||
(click)="save()"> |
|||
{{ 'action.add' | translate }} |
|||
</button> |
|||
</div> |
|||
@ -0,0 +1,29 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
width: 600px; |
|||
height: 100%; |
|||
max-width: 100%; |
|||
max-height: 100vh; |
|||
display: grid; |
|||
grid-template-rows: min-content minmax(auto, 1fr) min-content; |
|||
|
|||
.mobile-page-form { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 16px; |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component } from '@angular/core'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { CustomMobilePage } from '@shared/models/mobile-app.models'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { MatDialogRef } from '@angular/material/dialog'; |
|||
import { FormBuilder } from '@angular/forms'; |
|||
import { deepTrim } from '@core/utils'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-add-mobile-page-dialog', |
|||
templateUrl: './add-mobile-page-dialog.component.html', |
|||
styleUrls: ['./add-mobile-page-dialog.component.scss'] |
|||
}) |
|||
export class AddMobilePageDialogComponent extends DialogComponent<AddMobilePageDialogComponent, CustomMobilePage> { |
|||
|
|||
customMobilePage = this.fb.control<CustomMobilePage>(null); |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
public dialogRef: MatDialogRef<AddMobilePageDialogComponent, CustomMobilePage>, |
|||
private fb: FormBuilder) { |
|||
super(store, router, dialogRef); |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
save() { |
|||
if (this.customMobilePage.valid) { |
|||
const pageItem = deepTrim(this.customMobilePage.value); |
|||
this.dialogRef.close(pageItem); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-custom-mobile-page-panel"> |
|||
<div class="tb-custom-mobile-page-title" translate>mobile.edit-custom-page</div> |
|||
<div class="tb-custom-mobile-page-panel-content"> |
|||
<tb-mobile-page-item [formControl]="mobilePageControl"> |
|||
</tb-mobile-page-item> |
|||
</div> |
|||
<div class="tb-custom-mobile-page-panel-buttons"> |
|||
<button mat-button |
|||
color="primary" |
|||
type="button" |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button |
|||
color="primary" |
|||
type="button" |
|||
(click)="apply()" |
|||
[disabled]="mobilePageControl.invalid || !mobilePageControl.dirty"> |
|||
{{ 'action.apply' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,49 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
@import '../scss/constants'; |
|||
|
|||
.tb-custom-mobile-page-panel { |
|||
width: 500px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 16px; |
|||
@media #{$mat-lt-md} { |
|||
width: 90vw; |
|||
} |
|||
.tb-custom-mobile-page-title { |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
line-height: 24px; |
|||
letter-spacing: 0.25px; |
|||
color: rgba(0, 0, 0, 0.87); |
|||
} |
|||
.tb-custom-mobile-page-panel-content { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 16px; |
|||
overflow: auto; |
|||
margin: -10px; |
|||
padding: 10px; |
|||
} |
|||
.tb-custom-mobile-page-panel-buttons { |
|||
height: 40px; |
|||
display: flex; |
|||
flex-direction: row; |
|||
gap: 16px; |
|||
justify-content: flex-end; |
|||
align-items: flex-end; |
|||
} |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; |
|||
import { FormBuilder } from '@angular/forms'; |
|||
import { CustomMobilePage } from '@shared/models/mobile-app.models'; |
|||
import { TbPopoverComponent } from '@shared/components/popover.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-custom-menu-item-panel', |
|||
templateUrl: './custom-mobile-page-panel.component.html', |
|||
styleUrls: ['./custom-mobile-page-panel.component.scss'], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class CustomMobilePagePanelComponent implements OnInit { |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
pageItem: CustomMobilePage; |
|||
|
|||
@Input() |
|||
popover: TbPopoverComponent<CustomMobilePagePanelComponent>; |
|||
|
|||
@Output() |
|||
customMobilePageApplied = new EventEmitter<CustomMobilePage>(); |
|||
|
|||
mobilePageControl = this.fb.control<CustomMobilePage>(null); |
|||
|
|||
constructor(private fb: FormBuilder) { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.mobilePageControl.setValue(this.pageItem, {emitEvent: false}); |
|||
if (this.disabled) { |
|||
this.mobilePageControl.disable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
cancel() { |
|||
this.popover?.hide(); |
|||
} |
|||
|
|||
apply() { |
|||
if (this.mobilePageControl.valid) { |
|||
const menuItem = this.mobilePageControl.value; |
|||
this.customMobilePageApplied.emit(menuItem); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<section [formGroup]="customMobilePageForm" class="mobile-page-form"> |
|||
<div class="tb-form-row space-between"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="visible"> |
|||
{{ 'mobile.visible' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="center center"> |
|||
<tb-material-icon-select asBoxInput |
|||
required |
|||
color="#305680" |
|||
formControlName="icon"> |
|||
</tb-material-icon-select> |
|||
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label translate>mobile.page-name</mat-label> |
|||
<input required matInput formControlName="label"> |
|||
</mat-form-field> |
|||
</div> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label translate>mobile.page-type</mat-label> |
|||
<mat-select formControlName="type"> |
|||
<mat-option *ngFor="let pageType of mobilePagesTypes" [value]="pageType"> |
|||
{{ mobilePageTypeTranslations.get(pageType) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<ng-container *ngIf="customMobilePageForm.get('type').value === MobilePageType.DASHBOARD"> |
|||
<tb-dashboard-autocomplete fxFlex |
|||
formControlName="dashboardId" |
|||
required |
|||
label="{{ 'dashboard.dashboard' | translate }}" |
|||
floatLabel="always" |
|||
appearance="outline" |
|||
subscriptSizing="dynamic" |
|||
requiredText="dashboard.dashboard-required" |
|||
placeholder="{{ 'dashboard.select-dashboard' | translate }}" |
|||
[selectFirstDashboard]="false" |
|||
></tb-dashboard-autocomplete> |
|||
</ng-container> |
|||
<ng-container *ngIf="customMobilePageForm.get('type').value === MobilePageType.WEB_VIEW"> |
|||
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label translate>mobile.url</mat-label> |
|||
<input required matInput formControlName="url"> |
|||
</mat-form-field> |
|||
</ng-container> |
|||
<ng-container *ngIf="customMobilePageForm.get('type').value === MobilePageType.CUSTOM"> |
|||
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label translate>mobile.path</mat-label> |
|||
<input required matInput formControlName="path"> |
|||
</mat-form-field> |
|||
</ng-container> |
|||
</section> |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host{ |
|||
.mobile-page-form { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 16px; |
|||
} |
|||
} |
|||
@ -0,0 +1,132 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { booleanAttribute, Component, forwardRef, Input } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
ValidationErrors, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { CustomMobilePage, MobilePageType, mobilePageTypeTranslations } from '@shared/models/mobile-app.models'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
|||
import { Authority } from '@shared/models/authority.enum'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-mobile-page-item', |
|||
templateUrl: './custom-mobile-page.component.html', |
|||
styleUrls: ['./custom-mobile-page.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => CustomMobilePageComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => CustomMobilePageComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class CustomMobilePageComponent implements ControlValueAccessor, Validator { |
|||
|
|||
@Input({transform: booleanAttribute}) |
|||
disabled: boolean; |
|||
|
|||
mobilePagesTypes = [MobilePageType.DASHBOARD, MobilePageType.WEB_VIEW, MobilePageType.CUSTOM]; |
|||
MobilePageType = MobilePageType; |
|||
mobilePageTypeTranslations = mobilePageTypeTranslations; |
|||
|
|||
customMobilePageForm = this.fb.group({ |
|||
visible: [true], |
|||
icon: ['star'], |
|||
label: ['', Validators.required], |
|||
type: [MobilePageType.DASHBOARD], |
|||
dashboardId: ['', Validators.required], |
|||
url: [{value:'', disabled: true}, [Validators.required]], |
|||
path: [{value:'', disabled: true}, [Validators.required, Validators.pattern(/^((?!\s).)*$/)]] |
|||
}); |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
|
|||
constructor(private fb: FormBuilder, |
|||
private store: Store<AppState>) { |
|||
this.customMobilePageForm.get('type').valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe(type => { |
|||
this.customMobilePageForm.get('dashboardId').disable({emitEvent: false}); |
|||
this.customMobilePageForm.get('url').disable({emitEvent: false}); |
|||
this.customMobilePageForm.get('path').disable({emitEvent: false}); |
|||
switch (type) { |
|||
case MobilePageType.DASHBOARD: |
|||
this.customMobilePageForm.get('dashboardId').enable({emitEvent: false}); |
|||
break; |
|||
case MobilePageType.WEB_VIEW: |
|||
this.customMobilePageForm.get('url').enable({emitEvent: false}); |
|||
break; |
|||
case MobilePageType.CUSTOM: |
|||
this.customMobilePageForm.get('path').enable({emitEvent: false}); |
|||
break; |
|||
} |
|||
}); |
|||
|
|||
if (getCurrentAuthUser(this.store).authority === Authority.SYS_ADMIN) { |
|||
this.mobilePagesTypes.shift(); |
|||
this.customMobilePageForm.get('type').setValue(MobilePageType.WEB_VIEW); |
|||
} |
|||
|
|||
this.customMobilePageForm.valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe((value) => this.propagateChange(value)) |
|||
} |
|||
|
|||
registerOnChange(fn: any) { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(_fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean) { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.customMobilePageForm.disable({emitEvent: false}); |
|||
} else { |
|||
this.customMobilePageForm.enable({emitEvent: false}); |
|||
this.customMobilePageForm.get('type').updateValueAndValidity({onlySelf: true}); |
|||
} |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
if (!this.customMobilePageForm.valid) { |
|||
return { |
|||
invalidCustomMobilePageForm: true |
|||
}; |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
writeValue(value: CustomMobilePage) { |
|||
this.customMobilePageForm.patchValue(value, {emitEvent: false}); |
|||
} |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-default-mobile-page-panel" [formGroup]="mobilePageFormGroup"> |
|||
<div class="tb-default-mobile-page-title-container"> |
|||
<div class="tb-default-mobile-page-title" translate>mobile.edit-page</div> |
|||
<button *ngIf="!disabled" |
|||
[disabled]="!isCleanupEnabled" |
|||
type="button" |
|||
mat-stroked-button |
|||
class="tb-box-button" |
|||
matTooltip="{{ 'mobile.reset-to-page-default' | translate }}" |
|||
matTooltipPosition="above" |
|||
(click)="cleanup()"> |
|||
<tb-icon [color]="isCleanupEnabled ? 'primary' : null" matButtonIcon>mdi:broom</tb-icon> |
|||
</button> |
|||
</div> |
|||
<div class="tb-default-mobile-page-panel-content"> |
|||
<div class="tb-form-row space-between"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="visible"> |
|||
{{ 'mobile.visible' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="center center"> |
|||
<tb-material-icon-select asBoxInput |
|||
color="#305680" |
|||
formControlName="icon"> |
|||
</tb-material-icon-select> |
|||
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" floatLabel="always"> |
|||
<mat-label translate>mobile.page-name</mat-label> |
|||
<input matInput formControlName="label" placeholder="{{ defaultItemName }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-default-mobile-page-panel-buttons"> |
|||
<button mat-button |
|||
color="primary" |
|||
type="button" |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button |
|||
color="primary" |
|||
type="button" |
|||
(click)="apply()" |
|||
[disabled]="mobilePageFormGroup.invalid || !mobilePageFormGroup.dirty"> |
|||
{{ 'action.apply' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,55 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
@import '../scss/constants'; |
|||
|
|||
.tb-default-mobile-page-panel { |
|||
width: 500px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 16px; |
|||
@media #{$mat-lt-md} { |
|||
width: 90vw; |
|||
} |
|||
.tb-default-mobile-page-title-container { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
.tb-default-mobile-page-title { |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
line-height: 24px; |
|||
letter-spacing: 0.25px; |
|||
color: rgba(0, 0, 0, 0.87); |
|||
} |
|||
.tb-default-mobile-page-panel-content { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 16px; |
|||
overflow: auto; |
|||
margin: -10px; |
|||
padding: 10px; |
|||
} |
|||
.tb-default-mobile-page-panel-buttons { |
|||
height: 40px; |
|||
display: flex; |
|||
flex-direction: row; |
|||
gap: 16px; |
|||
justify-content: flex-end; |
|||
align-items: flex-end; |
|||
} |
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, DestroyRef, EventEmitter, inject, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; |
|||
import { DefaultMobilePage, defaultMobilePageMap, hideDefaultMenuItems } from '@shared/models/mobile-app.models'; |
|||
import { TbPopoverComponent } from '@shared/components/popover.component'; |
|||
import { FormBuilder } from '@angular/forms'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-default-mobile-page-panel', |
|||
templateUrl: './default-mobile-page-panel.component.html', |
|||
styleUrls: ['./default-mobile-page-panel.component.scss'], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class DefaultMobilePagePanelComponent implements OnInit { |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
pageItem: DefaultMobilePage; |
|||
|
|||
@Input() |
|||
popover: TbPopoverComponent<DefaultMobilePagePanelComponent>; |
|||
|
|||
@Output() |
|||
defaultMobilePageApplied = new EventEmitter<DefaultMobilePage>(); |
|||
|
|||
mobilePageFormGroup = this.fb.group({ |
|||
visible: [true], |
|||
icon: [''], |
|||
label: [''], |
|||
}); |
|||
|
|||
isCleanupEnabled = false; |
|||
defaultItemName: string; |
|||
|
|||
private defaultMobilePages: Omit<DefaultMobilePage, 'type' | 'visible'>; |
|||
private destroyRef = inject(DestroyRef); |
|||
|
|||
constructor(private fb: FormBuilder) { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.defaultMobilePages = defaultMobilePageMap.get(this.pageItem.id); |
|||
this.defaultItemName = this.defaultMobilePages.label; |
|||
|
|||
this.mobilePageFormGroup.patchValue({ |
|||
label: this.pageItem.label, |
|||
icon: this.pageItem.icon ? this.pageItem.icon : this.defaultMobilePages.icon, |
|||
visible: this.pageItem.visible |
|||
}, {emitEvent: false}); |
|||
|
|||
if (this.disabled) { |
|||
this.mobilePageFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.mobilePageFormGroup.valueChanges.pipe( |
|||
takeUntilDestroyed(this.destroyRef) |
|||
).subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
this.updateCleanupState(); |
|||
} |
|||
} |
|||
|
|||
cancel() { |
|||
this.popover?.hide(); |
|||
} |
|||
|
|||
apply() { |
|||
this.defaultMobilePageApplied.emit(this.pageItem); |
|||
} |
|||
|
|||
cleanup() { |
|||
this.mobilePageFormGroup.patchValue({ |
|||
visible: !hideDefaultMenuItems.includes(this.pageItem.id), |
|||
icon: this.defaultMobilePages.icon, |
|||
label: null |
|||
}, {emitEvent: false}); |
|||
this.mobilePageFormGroup.markAsDirty(); |
|||
this.updateModel(); |
|||
} |
|||
|
|||
private updateCleanupState() { |
|||
this.isCleanupEnabled = (hideDefaultMenuItems.includes(this.pageItem.id) ? this.pageItem.visible : !this.pageItem.visible) || |
|||
!!this.pageItem.label || |
|||
!!this.pageItem.icon; |
|||
} |
|||
|
|||
private updateModel() { |
|||
this.pageItem.visible = this.mobilePageFormGroup.get('visible').value; |
|||
const label = this.mobilePageFormGroup.get('label').value; |
|||
if (label) { |
|||
this.pageItem.label = label; |
|||
} else { |
|||
delete this.pageItem.label; |
|||
} |
|||
const icon = this.mobilePageFormGroup.get('icon').value; |
|||
if (icon !== this.defaultMobilePages.icon) { |
|||
this.pageItem.icon = icon; |
|||
} else { |
|||
delete this.pageItem.icon; |
|||
} |
|||
this.updateCleanupState(); |
|||
} |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="mobile-layout-container"> |
|||
<div class="header"> |
|||
<div class="title-section"> |
|||
<span tbTruncateWithTooltip class="title-container" translate>mobile.pages</span> |
|||
</div> |
|||
<span fxFlex></span> |
|||
<button *ngIf="!readonly" class="tb-mobile-layout-button" |
|||
fxHide.lt-lg |
|||
mat-stroked-button |
|||
color="primary" |
|||
(click)="hideAll()"> |
|||
<tb-icon matButtonIcon>visibility_off</tb-icon> |
|||
<div tbTruncateWithTooltip>{{ 'mobile.hide-all-pages' | translate }}</div> |
|||
</button> |
|||
<button *ngIf="!readonly" class="tb-mobile-layout-button" |
|||
fxHide.lt-lg |
|||
mat-stroked-button |
|||
color="primary" |
|||
(click)="resetToDefault()"> |
|||
<tb-icon matButtonIcon>mdi:broom</tb-icon> |
|||
<div tbTruncateWithTooltip>{{ 'mobile.reset-to-default-pages' | translate }}</div> |
|||
</button> |
|||
<button *ngIf="!readonly" |
|||
fxHide.gt-md |
|||
type="button" |
|||
mat-stroked-button |
|||
class="tb-box-button" |
|||
matTooltip="{{ 'mobile.hide-all-pages' | translate }}" |
|||
matTooltipPosition="above" |
|||
(click)="hideAll()"> |
|||
<tb-icon color="primary" matButtonIcon>visibility_off</tb-icon> |
|||
</button> |
|||
<button *ngIf="!readonly" |
|||
fxHide.gt-md |
|||
type="button" |
|||
mat-stroked-button |
|||
class="tb-box-button" |
|||
matTooltip="{{ 'mobile.reset-to-default-pages' | translate }}" |
|||
matTooltipPosition="above" |
|||
(click)="resetToDefault()"> |
|||
<tb-icon color="primary" matButtonIcon>mdi:broom</tb-icon> |
|||
</button> |
|||
<div tb-help="mobileLayout"></div> |
|||
</div> |
|||
<div #mobilePagesContainer class="tb-drop-list tb-mobile-items-container" |
|||
cdkDropList |
|||
cdkDropListOrientation="vertical" |
|||
(cdkDropListDropped)="mobileItemDrop($event)" |
|||
[cdkDropListDisabled]="!dragEnabled"> |
|||
<div class="tb-mobile-item-row-container" *ngFor="let menuItemControl of visibleMobilePagesControls(); trackBy: trackByMenuItem; |
|||
let $index = index; last as isLast;"> |
|||
<div cdkDrag [cdkDragDisabled]="!dragEnabled" |
|||
class="tb-draggable tb-mobile-item-row"> |
|||
<div *ngIf="isCustom(menuItemControl)" class="tb-mobile-item-label"> |
|||
{{ 'mobile.custom-page' | translate }} |
|||
</div> |
|||
<button cdkDragHandle *ngIf="dragEnabled" |
|||
mat-icon-button |
|||
matTooltip="{{ 'action.drag' | translate }}" |
|||
matTooltipPosition="above" |
|||
class="tb-mat-40 tb-drag-handle"> |
|||
<tb-icon>drag_indicator</tb-icon> |
|||
</button> |
|||
<tb-mobile-menu-item-row style="flex: 1; min-width: 0;" |
|||
[formControl]="menuItemControl" |
|||
[maxIconNameBlockWidth]="maxIconNameBlockWidth" |
|||
[hideItems]="hideItems$" |
|||
(pageRemoved)="removeCustomPage($index)"> |
|||
</tb-mobile-menu-item-row> |
|||
</div> |
|||
<button *ngIf="!isLast && !readonly" |
|||
(click)="addCustomMobilePage($index)" |
|||
class="tb-add-mobile-item-button" |
|||
[class.divider]="showMenuDivider($index)" |
|||
mat-button |
|||
matTooltip="{{ 'mobile.add-specific-page' | translate }}" |
|||
matTooltipPosition="above" |
|||
color="primary"> |
|||
<div class="tb-add-mobile-label-container tb-primary-fill"> |
|||
<mat-icon class="tb-mat-18" color="primary">add</mat-icon> |
|||
<span class="tb-mobile-divider-label" *ngIf="showMenuDivider($index)">{{ getDividerLabel($index) | translate }}</span> |
|||
</div> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<button *ngIf="!readonly" (click)="addCustomMobilePage()" |
|||
mat-button color="primary" class="tb-primary-fill" style="min-height: 52px;"> |
|||
<mat-icon color="primary">add</mat-icon> |
|||
<span translate>mobile.add-specific-page</span> |
|||
</button> |
|||
</div> |
|||
@ -0,0 +1,190 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
@import './../scss/constants'; |
|||
|
|||
:host{ |
|||
.mobile-layout-container { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
|
|||
.header { |
|||
z-index: 1; |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
gap: 8px; |
|||
|
|||
.title-section { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
gap: 24px; |
|||
min-width: 0; |
|||
.title-container { |
|||
font-size: 20px; |
|||
letter-spacing: .1px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-mobile-items-container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 4px; |
|||
min-height: 0; |
|||
overflow: auto; |
|||
padding-top: 8px; |
|||
padding-bottom: 8px; |
|||
} |
|||
} |
|||
|
|||
.tb-mobile-layout-button.mdc-button { |
|||
min-width: 120px; |
|||
tb-icon { |
|||
min-width: 18px; |
|||
} |
|||
.mdc-button__label { |
|||
overflow: hidden; |
|||
} |
|||
} |
|||
|
|||
::ng-deep { |
|||
.mat-mdc-button.mat-mdc-button-base.tb-add-mobile-item-button { |
|||
height: 24px; |
|||
opacity: 0; |
|||
transition: all 200ms; |
|||
&.divider { |
|||
opacity: 1; |
|||
.mat-mdc-button-persistent-ripple { |
|||
border-bottom-style: dashed; |
|||
border-bottom-width: 1px; |
|||
} |
|||
.tb-add-mobile-label-container { |
|||
vertical-align: middle; |
|||
.mat-icon { |
|||
display: none; |
|||
} |
|||
} |
|||
&:hover { |
|||
.mat-mdc-button-persistent-ripple { |
|||
border-bottom-style: inherit; |
|||
border-bottom-width: inherit; |
|||
} |
|||
.tb-add-mobile-label-container { |
|||
vertical-align: sub; |
|||
.mat-icon { |
|||
display: inline-block; |
|||
} |
|||
.tb-mobile-divider-label { |
|||
display: none; |
|||
} |
|||
} |
|||
} |
|||
.mat-mdc-button-persistent-ripple { |
|||
top: 11px; |
|||
bottom: 11px; |
|||
&:before { |
|||
opacity: 0.32; |
|||
} |
|||
} |
|||
} |
|||
&:hover { |
|||
opacity: 1; |
|||
} |
|||
.mat-mdc-button-persistent-ripple { |
|||
top: 11px; |
|||
bottom: 11px; |
|||
&:before { |
|||
opacity: 0.32; |
|||
} |
|||
} |
|||
&:active { |
|||
.mat-mdc-button-persistent-ripple { |
|||
&:before { |
|||
opacity: 0.48; |
|||
} |
|||
} |
|||
.tb-add-mobile-label-container { |
|||
&:after { |
|||
opacity: 0.48; |
|||
} |
|||
} |
|||
} |
|||
.tb-add-mobile-label-container { |
|||
display: inline-flex; |
|||
padding: 3px; |
|||
vertical-align: sub; |
|||
border-radius: 6px; |
|||
background: #fff; |
|||
.mat-icon { |
|||
margin: 0; |
|||
} |
|||
.tb-mobile-divider-label { |
|||
line-height: 12px; |
|||
font-size: 10px; |
|||
font-weight: 400; |
|||
padding: 0 6px; |
|||
} |
|||
&:after { |
|||
display: block; |
|||
height: auto; |
|||
content: ""; |
|||
position: absolute; |
|||
inset: 0; |
|||
opacity: 0.32; |
|||
border-radius: 6px; |
|||
border: 1px solid $tb-primary-color; |
|||
pointer-events: none; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-mobile-item-row-container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 4px; |
|||
} |
|||
|
|||
.tb-mobile-item-row { |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
padding: 12px 12px 12px 0; |
|||
background: #fff; |
|||
gap: 0; |
|||
border-radius: 10px; |
|||
border: 1px solid rgba(0, 0, 0, 0.05); |
|||
box-shadow: 0 5px 16px 0 rgba(0, 0, 0, 0.04); |
|||
.tb-mobile-item-label { |
|||
position: absolute; |
|||
left: 8px; |
|||
top: -8px; |
|||
padding-left: 8px; |
|||
padding-right: 8px; |
|||
background: #fff; |
|||
font-size: 12px; |
|||
font-weight: 400; |
|||
line-height: 16px; |
|||
letter-spacing: 0.4px; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,245 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { booleanAttribute, Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'; |
|||
import { |
|||
AbstractControl, |
|||
ControlValueAccessor, |
|||
FormArray, |
|||
FormBuilder, |
|||
FormControl, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
Validator |
|||
} from '@angular/forms'; |
|||
import { |
|||
CustomMobilePage, |
|||
getDefaultMobileMenuItem, |
|||
isDefaultMobilePagesConfig, |
|||
MobileLayoutConfig, |
|||
mobileMenuDividers, |
|||
MobilePage, |
|||
MobilePageType |
|||
} from '@shared/models/mobile-app.models'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { CdkDragDrop } from '@angular/cdk/drag-drop'; |
|||
import { deepClone, isDefined } from '@core/utils'; |
|||
import { Subject } from 'rxjs'; |
|||
import { BreakpointObserver } from '@angular/cdk/layout'; |
|||
import { MediaBreakpoints } from '@shared/models/constants'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { AddMobilePageDialogComponent } from '@home/pages/mobile/bundes/layout/add-mobile-page-dialog.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-mobile-layout', |
|||
templateUrl: './mobile-layout.component.html', |
|||
styleUrls: ['./mobile-layout.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => MobileLayoutComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => MobileLayoutComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
}) |
|||
export class MobileLayoutComponent implements ControlValueAccessor, Validator { |
|||
|
|||
@Input({transform: booleanAttribute}) |
|||
readonly!: boolean |
|||
|
|||
@ViewChild('mobilePagesContainer') |
|||
mobilePagesContainer: ElementRef<HTMLElement>; |
|||
|
|||
pagesForm = this.fb.group({ |
|||
pages: this.fb.array([]) |
|||
}); |
|||
|
|||
maxIconNameBlockWidth = 256; |
|||
|
|||
private hideItemsSubject = new Subject<void>(); |
|||
hideItems$ = this.hideItemsSubject.asObservable(); |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
|
|||
constructor(private fb: FormBuilder, |
|||
private breakpointObserver: BreakpointObserver, |
|||
private dialog: MatDialog, |
|||
) { |
|||
|
|||
this.pagesForm.valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe( |
|||
() => this.updateModel() |
|||
); |
|||
|
|||
this.breakpointObserver.observe([MediaBreakpoints.xs, MediaBreakpoints['gt-xs'], MediaBreakpoints['gt-sm']]).pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe(() => { |
|||
this.computeMaxIconNameBlockWidth(); |
|||
}); |
|||
this.computeMaxIconNameBlockWidth(); |
|||
} |
|||
|
|||
registerOnChange(fn: any) { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(_fn: any) { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean) { |
|||
if (isDisabled) { |
|||
this.pagesForm.disable({emitEvent: false}); |
|||
} else { |
|||
this.pagesForm.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(layout: MobileLayoutConfig) { |
|||
const processLayout = this.prepareMobilePages(layout); |
|||
this.pagesForm.setControl('pages', this.prepareMobilePagesFormArray(processLayout)); |
|||
} |
|||
|
|||
validate(_c: FormControl) { |
|||
if (!this.pagesForm.valid) { |
|||
return { |
|||
invalidLayoutForm: true |
|||
}; |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
hideAll() { |
|||
this.hideItemsSubject.next(); |
|||
} |
|||
|
|||
resetToDefault() { |
|||
if (!isDefaultMobilePagesConfig(this.pagesForm.value.pages as MobilePage[])) { |
|||
const processLayout = this.prepareMobilePages(null); |
|||
this.pagesForm.setControl('pages', this.prepareMobilePagesFormArray(processLayout)); |
|||
} |
|||
} |
|||
|
|||
get dragEnabled(): boolean { |
|||
return !this.readonly && this.visibleMobilePagesControls().length > 1; |
|||
} |
|||
|
|||
visibleMobilePagesControls(): Array<AbstractControl> { |
|||
return this.pagesFormArray().controls; |
|||
} |
|||
|
|||
mobileItemDrop(event: CdkDragDrop<string[]>) { |
|||
const menuItemsArray = this.pagesFormArray(); |
|||
const menuItem = this.visibleMobilePagesControls()[event.previousIndex]; |
|||
const previousIndex = this.actualMobilePageIndex(event.previousIndex); |
|||
const currentIndex = this.actualMobilePageIndex(event.currentIndex); |
|||
menuItemsArray.removeAt(previousIndex); |
|||
menuItemsArray.insert(currentIndex, menuItem); |
|||
this.pagesForm.markAsDirty(); |
|||
} |
|||
|
|||
trackByMenuItem(_index: number, menuItemControl: AbstractControl): any { |
|||
return menuItemControl; |
|||
} |
|||
|
|||
addCustomMobilePage(index?: number) { |
|||
this.dialog.open<AddMobilePageDialogComponent, null, |
|||
CustomMobilePage>(AddMobilePageDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] |
|||
}).afterClosed().subscribe((menuItem) => { |
|||
if (menuItem) { |
|||
const menuItemsArray = this.pagesFormArray(); |
|||
const menuItemControl = this.fb.control(menuItem, []); |
|||
if (isDefined(index)) { |
|||
const insertIndex = this.actualMobilePageIndex(index) + 1; |
|||
menuItemsArray.insert(insertIndex, menuItemControl); |
|||
} else { |
|||
menuItemsArray.push(menuItemControl); |
|||
setTimeout(() => { |
|||
this.mobilePagesContainer.nativeElement.scrollTop = this.mobilePagesContainer.nativeElement.scrollHeight; |
|||
}, 0); |
|||
} |
|||
this.pagesForm.markAsDirty(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
isCustom(menuItemControl: AbstractControl): boolean { |
|||
return menuItemControl.value.type !== MobilePageType.DEFAULT; |
|||
} |
|||
|
|||
removeCustomPage(index: number) { |
|||
this.pagesFormArray().removeAt(this.actualMobilePageIndex(index)); |
|||
this.pagesForm.markAsDirty(); |
|||
} |
|||
|
|||
showMenuDivider(index: number): boolean { |
|||
return mobileMenuDividers.has(index); |
|||
} |
|||
|
|||
getDividerLabel(index: number): string { |
|||
return mobileMenuDividers.get(index); |
|||
} |
|||
|
|||
private updateModel() { |
|||
if (isDefaultMobilePagesConfig(this.pagesForm.value.pages as MobilePage[])) { |
|||
this.propagateChange({pages: []}); |
|||
} else { |
|||
this.propagateChange(this.pagesForm.value); |
|||
} |
|||
} |
|||
|
|||
private prepareMobilePages(layout: MobileLayoutConfig) { |
|||
if (!layout?.pages?.length) { |
|||
return getDefaultMobileMenuItem(); |
|||
} |
|||
return layout.pages; |
|||
} |
|||
|
|||
private prepareMobilePagesFormArray(items: MobilePage[]): FormArray { |
|||
const menuItemsControls: Array<AbstractControl> = []; |
|||
items.forEach((item) => { |
|||
menuItemsControls.push(this.fb.control(deepClone(item))); |
|||
}); |
|||
return this.fb.array(menuItemsControls); |
|||
} |
|||
|
|||
private computeMaxIconNameBlockWidth() { |
|||
if (this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm'])) { |
|||
this.maxIconNameBlockWidth = 256; |
|||
} else if (this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs'])) { |
|||
this.maxIconNameBlockWidth = 200; |
|||
} else { |
|||
this.maxIconNameBlockWidth = 0; |
|||
} |
|||
} |
|||
|
|||
private pagesFormArray(): FormArray { |
|||
return this.pagesForm.get('pages') as FormArray; |
|||
} |
|||
|
|||
private actualMobilePageIndex(index: number): number { |
|||
const menuItem = this.visibleMobilePagesControls()[index]; |
|||
return this.pagesFormArray().controls.indexOf(menuItem); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-mobile-page-item-row-component-container"> |
|||
<div class="tb-mobile-page-item-row-component"> |
|||
<div class="tb-form-row" [formGroup]="mobilePageRowForm"> |
|||
<div class="tb-mobile-page-item-start-block"> |
|||
<ng-content select=".tb-custom-menu-item-row-prefix"></ng-content> |
|||
<div class="tb-mobile-page-item-icon-name-block" [style.width]="iconNameBlockWidth"> |
|||
<mat-slide-toggle matTooltip="{{ (mobilePageRowForm.get('visible').value ? 'mobile.visible' : 'mobile.hidden') | translate }}" |
|||
matTooltipPosition="above" |
|||
formControlName="visible"></mat-slide-toggle> |
|||
<tb-material-icon-select asBoxInput |
|||
color="#305680" |
|||
formControlName="icon"> |
|||
</tb-material-icon-select> |
|||
<div tbTruncateWithTooltip> {{ itemName }} </div> |
|||
</div> |
|||
</div> |
|||
<div fxHide.xs class="tb-mobile-page-item-input-block"> |
|||
<mat-form-field class="flex tb-suffix-absolute" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput [required]="isCustomMenuItem" formControlName="label" placeholder="{{ itemNamePlaceholder }}"> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
matTooltip="{{ 'mobile.page-name-required' | translate }}" |
|||
*ngIf="mobilePageRowForm.get('label').hasError('required') && mobilePageRowForm.get('label').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field *ngIf="isCustomMenuItem" |
|||
class="flex tb-mobile-page-item-info" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput readonly [placeholder]="itemInfo"> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<button *ngIf="!disabled" |
|||
type="button" |
|||
mat-icon-button |
|||
#editButton |
|||
(click)="edit($event, editButton)" |
|||
matTooltip="{{ 'mobile.edit-page' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>edit</mat-icon> |
|||
</button> |
|||
<button mat-icon-button *ngIf="!disabled && isDefaultMenuItem" |
|||
[disabled]="!isCleanupEnabled" |
|||
matTooltip="{{ 'mobile.reset-to-page-default' | translate }}" |
|||
matTooltipPosition="above" |
|||
(click)="cleanup()"> |
|||
<tb-icon>mdi:broom</tb-icon> |
|||
</button> |
|||
<button mat-icon-button *ngIf="!disabled && isCustomMenuItem" |
|||
matTooltip="{{ 'mobile.delete-page' | translate }}" |
|||
matTooltipPosition="above" |
|||
(click)="delete()"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,84 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
@import "../scss/constants"; |
|||
@import '../scss/mixins'; |
|||
|
|||
.tb-mobile-page-item-row-component-container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 0; |
|||
} |
|||
|
|||
.tb-mobile-page-item-row-component { |
|||
display: flex; |
|||
gap: 8px; |
|||
align-items: center; |
|||
.mat-mdc-icon-button.mat-mdc-button-base { |
|||
&:not(.tb-drag-handle) { |
|||
@media #{$mat-xs} { |
|||
@include tb-mat-icon-button-size(24); |
|||
} |
|||
@media #{$mat-sm} { |
|||
@include tb-mat-icon-button-size(32); |
|||
} |
|||
} |
|||
&:not(:disabled) { |
|||
.mat-icon { |
|||
color: rgba(0, 0, 0, 0.54); |
|||
} |
|||
} |
|||
&:disabled { |
|||
.mat-icon { |
|||
color: rgba(0, 0, 0, 0.12); |
|||
} |
|||
} |
|||
} |
|||
.tb-form-row { |
|||
flex: 1; |
|||
min-width: 0; |
|||
.tb-mobile-page-item-start-block { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 0; |
|||
min-width: 0; |
|||
} |
|||
.tb-mobile-page-item-icon-name-block { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 12px; |
|||
min-width: 0; |
|||
} |
|||
.tb-mobile-page-item-input-block { |
|||
display: flex; |
|||
gap: 8px; |
|||
flex: 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-mobile-page-item-info { |
|||
.mdc-text-field:not(.mdc-text-field--disabled) { |
|||
input.mdc-text-field__input { |
|||
cursor: default; |
|||
&::placeholder { |
|||
color: $tb-primary-color; |
|||
} |
|||
&:-ms-input-placeholder { |
|||
color: $tb-primary-color; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,323 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
booleanAttribute, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
DestroyRef, |
|||
EventEmitter, |
|||
forwardRef, |
|||
inject, |
|||
Input, |
|||
numberAttribute, |
|||
OnInit, |
|||
Output, |
|||
Renderer2, |
|||
ViewContainerRef, |
|||
ViewEncapsulation |
|||
} from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
ValidationErrors, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { Observable } from 'rxjs'; |
|||
import { |
|||
CustomMobilePage, |
|||
DefaultMobilePage, |
|||
defaultMobilePageMap, |
|||
hideDefaultMenuItems, |
|||
MobilePageType, |
|||
mobilePageTypeTranslations |
|||
} from '@shared/models/mobile-app.models'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { MatButton } from '@angular/material/button'; |
|||
import { deepClone } from '@core/utils'; |
|||
import { TbPopoverService } from '@shared/components/popover.service'; |
|||
import { CustomMobilePagePanelComponent } from '@home/pages/mobile/bundes/layout/custom-mobile-page-panel.component'; |
|||
import { DefaultMobilePagePanelComponent } from '@home/pages/mobile/bundes/layout/default-mobile-page-panel.component'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-mobile-menu-item-row', |
|||
templateUrl: './mobile-page-item-row.component.html', |
|||
styleUrls: ['./mobile-page-item-row.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => MobilePageItemRowComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => MobilePageItemRowComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class MobilePageItemRowComponent implements ControlValueAccessor, OnInit, Validator { |
|||
|
|||
@Input({transform: booleanAttribute}) |
|||
disabled: boolean; |
|||
|
|||
@Input({transform: numberAttribute}) |
|||
maxIconNameBlockWidth = 256; |
|||
|
|||
@Input() |
|||
hideItems: Observable<void>; |
|||
|
|||
@Output() |
|||
pageRemoved = new EventEmitter(); |
|||
|
|||
iconNameBlockWidth = '256px'; |
|||
itemInfo: string; |
|||
|
|||
isDefaultMenuItem = false; |
|||
isCustomMenuItem = false; |
|||
|
|||
isCleanupEnabled = false; |
|||
|
|||
mobilePageRowForm = this.fb.group({ |
|||
visible: [true, []], |
|||
icon: ['', []], |
|||
label: ['', []], |
|||
type: [MobilePageType.DEFAULT] |
|||
}); |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
private destroyRef = inject(DestroyRef); |
|||
|
|||
private defaultMobilePages: Omit<DefaultMobilePage, 'type' | 'visible'>; |
|||
private defaultItemName: string; |
|||
|
|||
private modelValue: DefaultMobilePage | CustomMobilePage; |
|||
|
|||
constructor(private fb: FormBuilder, |
|||
private cd: ChangeDetectorRef, |
|||
private popoverService: TbPopoverService, |
|||
private renderer: Renderer2, |
|||
private viewContainerRef: ViewContainerRef, |
|||
private translate: TranslateService) { |
|||
this.mobilePageRowForm.valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe( |
|||
() => this.updateModel() |
|||
); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.updateIconNameBlockWidth(); |
|||
if (this.hideItems) { |
|||
this.hideItems.pipe( |
|||
takeUntilDestroyed(this.destroyRef) |
|||
).subscribe(() => { |
|||
if (!this.disabled && this.modelValue.visible) { |
|||
this.mobilePageRowForm.patchValue( |
|||
{visible: false}, {emitEvent: true} |
|||
); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(_fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean) { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.mobilePageRowForm.disable({emitEvent: false}); |
|||
} else { |
|||
this.mobilePageRowForm.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
if (!this.mobilePageRowForm.valid) { |
|||
return { |
|||
invalidMobileMenuItem: true |
|||
}; |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
writeValue(value: DefaultMobilePage | CustomMobilePage) { |
|||
this.modelValue = value; |
|||
this.mobilePageRowForm.patchValue( |
|||
{ |
|||
visible: value.visible, |
|||
icon: value.icon, |
|||
label: value.label |
|||
}, {emitEvent: false} |
|||
); |
|||
if (this.modelValue.type === MobilePageType.DEFAULT) { |
|||
this.isDefaultMenuItem = true; |
|||
const defaultPage = this.modelValue as DefaultMobilePage; |
|||
if (defaultMobilePageMap.has(defaultPage.id)) { |
|||
this.defaultMobilePages = defaultMobilePageMap.get(defaultPage.id); |
|||
this.defaultItemName = this.defaultMobilePages.label; |
|||
if (!value.icon) { |
|||
this.mobilePageRowForm.patchValue({ |
|||
icon: this.defaultMobilePages.icon |
|||
}, {emitEvent: false}); |
|||
} |
|||
} |
|||
} else { |
|||
this.isCustomMenuItem = true; |
|||
this.mobilePageRowForm.get('label').setValidators([Validators.required]); |
|||
this.mobilePageRowForm.get('label').updateValueAndValidity({emitEvent: false}); |
|||
} |
|||
this.updateCleanupState(); |
|||
this.updateItemInfo(); |
|||
this.cd.markForCheck(); |
|||
} |
|||
|
|||
cleanup() { |
|||
const visible = !hideDefaultMenuItems.includes((this.modelValue as DefaultMobilePage).id) |
|||
this.mobilePageRowForm.patchValue( |
|||
{ |
|||
visible: visible, |
|||
icon: this.defaultMobilePages.icon, |
|||
label: null |
|||
}, {emitEvent: false} |
|||
); |
|||
this.modelValue.visible = visible; |
|||
delete this.modelValue.icon; |
|||
delete this.modelValue.label; |
|||
this.updateModel(); |
|||
} |
|||
|
|||
delete() { |
|||
this.pageRemoved.emit(); |
|||
} |
|||
|
|||
edit($event: Event, matButton: MatButton) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const trigger = matButton._elementRef.nativeElement; |
|||
if (this.popoverService.hasPopover(trigger)) { |
|||
this.popoverService.hidePopover(trigger); |
|||
} else { |
|||
const ctx: any = { |
|||
disabled: this.disabled, |
|||
pageItem: deepClone(this.modelValue) |
|||
}; |
|||
if (this.isDefaultMenuItem) { |
|||
const defaultMobilePagePanelPopover = this.popoverService.displayPopover(trigger, this.renderer, |
|||
this.viewContainerRef, DefaultMobilePagePanelComponent, ['right', 'bottom', 'top'], true, null, |
|||
ctx, |
|||
{}, |
|||
{}, {}, false, () => {}, {padding: '16px 24px'}); |
|||
defaultMobilePagePanelPopover.tbComponentRef.instance.popover = defaultMobilePagePanelPopover; |
|||
defaultMobilePagePanelPopover.tbComponentRef.instance.defaultMobilePageApplied.subscribe((menuItem) => { |
|||
defaultMobilePagePanelPopover.hide(); |
|||
this.afterPageEdit(menuItem); |
|||
}); |
|||
} else { |
|||
const customMobilePagePanelPopover = this.popoverService.displayPopover(trigger, this.renderer, |
|||
this.viewContainerRef, CustomMobilePagePanelComponent, ['right', 'bottom', 'top'], true, null, |
|||
ctx, |
|||
{}, |
|||
{}, {}, false, () => {}, {padding: '16px 24px'}); |
|||
customMobilePagePanelPopover.tbComponentRef.instance.popover = customMobilePagePanelPopover; |
|||
customMobilePagePanelPopover.tbComponentRef.instance.customMobilePageApplied.subscribe((page) => { |
|||
customMobilePagePanelPopover.hide(); |
|||
this.afterPageEdit(page); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
get itemName(): string { |
|||
return this.isDefaultMenuItem ? this.defaultItemName : this.mobilePageRowForm.get('label').value; |
|||
} |
|||
|
|||
get itemNamePlaceholder(): string { |
|||
return this.isDefaultMenuItem ? this.defaultItemName : ''; |
|||
} |
|||
|
|||
private updateIconNameBlockWidth() { |
|||
if (this.maxIconNameBlockWidth) { |
|||
this.iconNameBlockWidth = `${this.maxIconNameBlockWidth}px`; |
|||
} else { |
|||
this.iconNameBlockWidth = '100%'; |
|||
} |
|||
} |
|||
|
|||
private updateModel() { |
|||
this.modelValue.visible = this.mobilePageRowForm.get('visible').value; |
|||
const label = this.mobilePageRowForm.get('label').value; |
|||
if (label) { |
|||
this.modelValue.label = label; |
|||
} else { |
|||
delete this.modelValue.label; |
|||
} |
|||
let icon = this.mobilePageRowForm.get('icon').value; |
|||
if (this.isDefaultMenuItem) { |
|||
if (this.defaultMobilePages.icon === icon) { |
|||
icon = null; |
|||
} |
|||
} |
|||
if (icon) { |
|||
this.modelValue.icon = icon; |
|||
} else { |
|||
delete this.modelValue.icon; |
|||
} |
|||
this.updateCleanupState(); |
|||
this.updateItemInfo(); |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
|
|||
private updateCleanupState() { |
|||
if (this.isDefaultMenuItem) { |
|||
this.isCleanupEnabled = |
|||
(hideDefaultMenuItems.includes((this.modelValue as DefaultMobilePage).id) ? this.modelValue.visible : !this.modelValue.visible) || |
|||
!!this.modelValue.label || |
|||
!!this.modelValue.icon; |
|||
} |
|||
} |
|||
|
|||
private updateItemInfo() { |
|||
if (this.isCustomMenuItem) { |
|||
this.itemInfo = this.translate.instant(mobilePageTypeTranslations.get(this.modelValue.type)); |
|||
} else { |
|||
this.itemInfo = ''; |
|||
} |
|||
} |
|||
|
|||
private afterPageEdit(page: DefaultMobilePage | CustomMobilePage) { |
|||
this.mobilePageRowForm.patchValue({ |
|||
visible: page.visible, |
|||
icon: page.icon || (this.isDefaultMenuItem ? this.defaultMobilePages.icon : null), |
|||
label: page.label |
|||
}, {emitEvent: false}); |
|||
if (this.isCustomMenuItem) { |
|||
this.modelValue = page; |
|||
} |
|||
this.updateModel(); |
|||
} |
|||
} |
|||
@ -0,0 +1,109 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<mat-toolbar color="primary"> |
|||
<h2>{{ dialogTitle | translate }}</h2> |
|||
<span fxFlex></span> |
|||
<div tb-help="ruleNotifications"></div> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div *ngIf="!(isLoading$ | async)"></div> |
|||
<div mat-dialog-content ngStyle.xs="padding: 0;"> |
|||
<mat-horizontal-stepper [linear]="true" labelPosition="end" #addMobileBundle [orientation]="(stepperOrientation | async)" |
|||
(selectionChange)="changeStep($event)"> |
|||
<ng-template matStepperIcon="edit"> |
|||
<mat-icon>check</mat-icon> |
|||
</ng-template> |
|||
<mat-step [stepControl]="bundlesForms"> |
|||
<ng-template matStepLabel>{{ 'mobile.basic-settings' | translate }}</ng-template> |
|||
<form [formGroup]="bundlesForms" style="margin-top: 4px;"> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label translate>mobile.title</mat-label> |
|||
<input matInput required formControlName="title"> |
|||
<mat-hint> </mat-hint> |
|||
<mat-error *ngIf="bundlesForms.get('title').hasError('required')"> |
|||
{{ 'mobile.title-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<tb-entity-autocomplete |
|||
allowCreateNew |
|||
useFullEntityId |
|||
appearance="outline" |
|||
labelText="{{ 'mobile.android-application' | translate }}" |
|||
[entityType]="entityType.MOBILE_APP" |
|||
[entitySubtype]="platformType.ANDROID" |
|||
(createNew)="createApplication('androidAppId', platformType.ANDROID)" |
|||
formControlName="androidAppId"> |
|||
</tb-entity-autocomplete> |
|||
<tb-entity-autocomplete |
|||
allowCreateNew |
|||
useFullEntityId |
|||
appearance="outline" |
|||
labelText="{{ 'mobile.ios-application' | translate }}" |
|||
[entityType]="entityType.MOBILE_APP" |
|||
[entitySubtype]="platformType.IOS" |
|||
(createNew)="createApplication('iosAppId', platformType.IOS)" |
|||
formControlName="iosAppId"> |
|||
</tb-entity-autocomplete> |
|||
<mat-form-field appearance="outline"> |
|||
<mat-label translate>mobile.description</mat-label> |
|||
<textarea matInput formControlName="description" rows="1"></textarea> |
|||
</mat-form-field> |
|||
</form> |
|||
</mat-step> |
|||
<mat-step [stepControl]="oauthForms"> |
|||
<ng-template matStepLabel>{{ 'admin.oauth2.oauth2' | translate }}</ng-template> |
|||
<form [formGroup]="oauthForms" style="margin-top: 8px;"> |
|||
<mat-slide-toggle formControlName="oauth2Enabled"> |
|||
{{ 'mobile.enable-oauth' | translate }} |
|||
</mat-slide-toggle> |
|||
<tb-entity-list formControlName="oauth2ClientIds" |
|||
[entityType]="entityType.OAUTH2_CLIENT" |
|||
appearance="outline" |
|||
labelText="{{ 'admin.oauth2.clients' | translate }}" |
|||
placeholderText="{{ 'admin.oauth2.add-client' | translate }}"> |
|||
<button mat-button color="primary" matSuffix |
|||
[disabled]="this.oauthForms.get('oauth2ClientIds').disabled" |
|||
(click)="createClient($event)"> |
|||
<span style="white-space: nowrap">{{ 'admin.oauth2.create-new' | translate }}</span> |
|||
</button> |
|||
</tb-entity-list> |
|||
</form> |
|||
</mat-step> |
|||
<mat-step [stepControl]="layoutForms"> |
|||
<ng-template matStepLabel>{{ 'mobile.layout' | translate }}</ng-template> |
|||
<form [formGroup]="layoutForms" class="layout-form"> |
|||
<tb-mobile-layout formControlName="layoutConfig"> |
|||
</tb-mobile-layout> |
|||
</form> |
|||
</mat-step> |
|||
</mat-horizontal-stepper> |
|||
</div> |
|||
<div mat-dialog-actions fxLayout="row"> |
|||
<button mat-stroked-button *ngIf="selectedIndex > 0" |
|||
(click)="backStep()">{{ 'action.back' | translate }}</button> |
|||
<span fxFlex></span> |
|||
<button mat-raised-button |
|||
color="primary" |
|||
(click)="nextStep()">{{ nextStepLabel() | translate }}</button> |
|||
</div> |
|||
@ -0,0 +1,73 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
@import "../../../../../../scss/constants"; |
|||
|
|||
:host { |
|||
width: 850px; |
|||
height: 100%; |
|||
max-width: 100%; |
|||
max-height: 100vh; |
|||
display: grid; |
|||
grid-template-rows: min-content 4px minmax(auto, 1fr) min-content; |
|||
|
|||
.mat-mdc-slide-toggle { |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
form.layout-form { |
|||
display: block; |
|||
height: 100%; |
|||
max-height: 100%; |
|||
} |
|||
|
|||
::ng-deep { |
|||
|
|||
.mat-mdc-dialog-content { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
padding: 0 !important; |
|||
color: rgba(0, 0, 0, 0.87); |
|||
|
|||
.mat-stepper-horizontal { |
|||
display: flex; |
|||
height: 100%; |
|||
overflow: hidden; |
|||
|
|||
.mat-horizontal-stepper-wrapper { |
|||
flex: 1 1 100%; |
|||
max-width: 100%; |
|||
} |
|||
|
|||
.mat-horizontal-content-container { |
|||
height: 700px; |
|||
max-height: 100%; |
|||
width: 100%;; |
|||
overflow-y: auto; |
|||
scrollbar-gutter: stable; |
|||
@media #{$mat-gt-sm} { |
|||
min-width: 500px; |
|||
} |
|||
.mat-horizontal-stepper-content:not(.mat-horizontal-stepper-content-inactive):nth-child(3) { |
|||
height: 100%; |
|||
max-height: 100%; |
|||
overflow: hidden; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,226 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, Inject, ViewChild } from '@angular/core'; |
|||
import { MobileApp, MobileAppBundle, MobileAppBundleInfo } from '@shared/models/mobile-app.models'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; |
|||
import { MediaBreakpoints } from '@shared/models/constants'; |
|||
import { map } from 'rxjs/operators'; |
|||
import { forkJoin, Observable } from 'rxjs'; |
|||
import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper'; |
|||
import { BreakpointObserver } from '@angular/cdk/layout'; |
|||
import { FormBuilder, Validators } from '@angular/forms'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
import { PlatformType } from '@shared/models/oauth2.models'; |
|||
import { MatStepper } from '@angular/material/stepper'; |
|||
import { |
|||
MobileAppDialogComponent, |
|||
MobileAppDialogData |
|||
} from '@home/pages/mobile/applications/mobile-app-dialog.component'; |
|||
import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { MobileAppService } from '@core/http/mobile-app.service'; |
|||
import { deepClone, deepTrim } from '@core/utils'; |
|||
|
|||
export interface MobileBundleDialogData { |
|||
bundle?: MobileAppBundleInfo; |
|||
isAdd?: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-mobile-bundle-dialog', |
|||
templateUrl: './mobile-bundle-dialog.component.html', |
|||
styleUrls: ['./mobile-bundle-dialog.component.scss'] |
|||
}) |
|||
export class MobileBundleDialogComponent extends DialogComponent<MobileBundleDialogComponent, MobileAppBundle> { |
|||
|
|||
@ViewChild('addMobileBundle', {static: true}) addMobileBundle: MatStepper; |
|||
|
|||
readonly entityType = EntityType; |
|||
|
|||
selectedIndex = 0; |
|||
|
|||
dialogTitle = 'mobile.edit-bundle'; |
|||
|
|||
stepperOrientation: Observable<StepperOrientation>; |
|||
|
|||
platformType = PlatformType; |
|||
|
|||
bundlesForms = this.fb.group({ |
|||
title: ['', Validators.required], |
|||
androidAppId: [null], |
|||
iosAppId: [null], |
|||
description: [''], |
|||
}); |
|||
|
|||
oauthForms = this.fb.group({ |
|||
oauth2Enabled: [true], |
|||
oauth2ClientIds: [null] |
|||
}); |
|||
|
|||
layoutForms = this.fb.group({ |
|||
layoutConfig: [null] |
|||
}); |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
protected dialogRef: MatDialogRef<MobileBundleDialogComponent, MobileAppBundle>, |
|||
@Inject(MAT_DIALOG_DATA) public data: MobileBundleDialogData, |
|||
private breakpointObserver: BreakpointObserver, |
|||
private fb: FormBuilder, |
|||
private dialog: MatDialog, |
|||
private mobileAppService: MobileAppService) { |
|||
super(store, router, dialogRef); |
|||
|
|||
if (this.data.isAdd) { |
|||
this.dialogTitle = 'mobile.add-bundle' |
|||
} |
|||
|
|||
this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs']) |
|||
.pipe(map(({matches}) => matches ? 'horizontal' : 'vertical')); |
|||
|
|||
this.oauthForms.get('oauth2Enabled').valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe(value => { |
|||
if (value) { |
|||
this.oauthForms.get('oauth2ClientIds').enable({emitEvent: false}); |
|||
} else { |
|||
this.oauthForms.get('oauth2ClientIds').disable({emitEvent: false}); |
|||
} |
|||
}) |
|||
|
|||
if (!this.data.isAdd && this.data.bundle) { |
|||
this.bundlesForms.patchValue(this.data.bundle, {emitEvent: false}); |
|||
this.oauthForms.get('oauth2Enabled').setValue(this.data.bundle.oauth2Enabled, {onlySelf: true}); |
|||
this.oauthForms.get('oauth2ClientIds') |
|||
.setValue(deepClone(this.data.bundle.oauth2ClientInfos.map(item => item.id.id)), {emitEvent: false}); |
|||
this.layoutForms.patchValue(this.data.bundle, {emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
backStep() { |
|||
this.addMobileBundle.previous(); |
|||
} |
|||
|
|||
nextStep() { |
|||
if (this.selectedIndex >= this.maxStepperIndex) { |
|||
this.add(); |
|||
} else { |
|||
this.addMobileBundle.next(); |
|||
} |
|||
} |
|||
|
|||
nextStepLabel(): string { |
|||
if (this.selectedIndex >= this.maxStepperIndex) { |
|||
return this.data.isAdd ? 'action.add' : 'action.save'; |
|||
} |
|||
return 'action.next'; |
|||
} |
|||
|
|||
changeStep($event: StepperSelectionEvent) { |
|||
this.selectedIndex = $event.selectedIndex; |
|||
if ($event.previouslySelectedIndex > $event.selectedIndex) { |
|||
$event.previouslySelectedStep.interacted = false; |
|||
} |
|||
} |
|||
|
|||
createApplication(formControl: string, platformType: PlatformType) { |
|||
this.dialog.open<MobileAppDialogComponent, MobileAppDialogData, MobileApp>(MobileAppDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
platformType |
|||
} |
|||
}).afterClosed() |
|||
.subscribe((app) => { |
|||
if (app) { |
|||
this.bundlesForms.get(formControl).patchValue(app.id); |
|||
this.bundlesForms.get(formControl).markAsDirty(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
createClient($event: Event) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
$event.preventDefault(); |
|||
} |
|||
this.dialog.open<ClientDialogComponent>(ClientDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: {} |
|||
}).afterClosed() |
|||
.subscribe((client) => { |
|||
if (client) { |
|||
const formValue = this.oauthForms.get('oauth2ClientIds').value ? |
|||
[...this.oauthForms.get('oauth2ClientIds').value] : []; |
|||
formValue.push(client.id.id); |
|||
this.oauthForms.get('oauth2ClientIds').patchValue(formValue); |
|||
this.oauthForms.get('oauth2ClientIds').markAsDirty(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private get maxStepperIndex(): number { |
|||
return this.addMobileBundle?._steps?.length - 1; |
|||
} |
|||
|
|||
private add(): void { |
|||
if (this.allValid()) { |
|||
let task = { |
|||
mobileBundle: this.mobileAppService.saveMobileAppBundle(this.mobileAppBundleFormValue, this.oauthForms.value.oauth2ClientIds), |
|||
oath2Clients: null |
|||
} |
|||
if (this.data.isAdd) { |
|||
delete task.oath2Clients; |
|||
} else { |
|||
const mobileBundle: MobileAppBundle = {...this.data.bundle, ...this.mobileAppBundleFormValue}; |
|||
task.mobileBundle = this.mobileAppService.saveMobileAppBundle(mobileBundle); |
|||
task.oath2Clients = this.mobileAppService.updateOauth2Clients(mobileBundle.id.id, this.oauthForms.get('oauth2ClientIds').value); |
|||
} |
|||
forkJoin(task).subscribe( |
|||
(res) => this.dialogRef.close(res.mobileBundle) |
|||
); |
|||
} |
|||
} |
|||
|
|||
private allValid(): boolean { |
|||
return !this.addMobileBundle.steps.find((item, index) => { |
|||
if (item.stepControl?.invalid) { |
|||
item.interacted = true; |
|||
this.addMobileBundle.selectedIndex = index; |
|||
return true; |
|||
} else { |
|||
return false; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private get mobileAppBundleFormValue(): MobileAppBundle { |
|||
const formValue = deepTrim(this.bundlesForms.value) as MobileAppBundle; |
|||
formValue.layoutConfig = deepTrim(this.layoutForms.value.layoutConfig); |
|||
formValue.oauth2Enabled = this.oauthForms.value.oauth2Enabled; |
|||
return formValue; |
|||
} |
|||
} |
|||
@ -0,0 +1,142 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
import { |
|||
CellActionDescriptor, |
|||
checkBoxCell, |
|||
DateEntityTableColumn, |
|||
EntityChipsEntityTableColumn, |
|||
EntityTableColumn, |
|||
EntityTableConfig |
|||
} from '@home/models/entity/entities-table-config.models'; |
|||
import { MobileAppBundleInfo } from '@shared/models/mobile-app.models'; |
|||
import { ActivatedRouteSnapshot } from '@angular/router'; |
|||
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; |
|||
import { Direction } from '@shared/models/page/sort-order'; |
|||
import { MobileBundleTableHeaderComponent } from '@home/pages/mobile/bundes/mobile-bundle-table-header.component'; |
|||
import { DatePipe } from '@angular/common'; |
|||
import { MobileAppService } from '@core/http/mobile-app.service'; |
|||
import { map } from 'rxjs/operators'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { EntityAction } from '@home/models/entity/entity-component.models'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { |
|||
MobileBundleDialogComponent, |
|||
MobileBundleDialogData |
|||
} from '@home/pages/mobile/bundes/mobile-bundle-dialog.component'; |
|||
import { NotificationTemplate } from '@shared/models/notification.models'; |
|||
|
|||
@Injectable() |
|||
export class MobileBundleTableConfigResolver { |
|||
|
|||
private readonly config: EntityTableConfig<MobileAppBundleInfo> = new EntityTableConfig<MobileAppBundleInfo>(); |
|||
|
|||
constructor( |
|||
private datePipe: DatePipe, |
|||
private mobileAppService: MobileAppService, |
|||
private translate : TranslateService, |
|||
private dialog: MatDialog, |
|||
) { |
|||
this.config.selectionEnabled = false; |
|||
this.config.entityType = EntityType.MOBILE_APP_BUNDLE; |
|||
this.config.addEnabled = false; |
|||
this.config.rowPointer = true; |
|||
this.config.detailsPanelEnabled = false; |
|||
this.config.entityTranslations = entityTypeTranslations.get(EntityType.MOBILE_APP_BUNDLE); |
|||
this.config.entityResources = entityTypeResources.get(EntityType.MOBILE_APP_BUNDLE); |
|||
this.config.headerComponent = MobileBundleTableHeaderComponent; |
|||
this.config.onEntityAction = action => this.onBundleAction(action); |
|||
this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'}; |
|||
this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; |
|||
|
|||
this.config.columns.push( |
|||
new DateEntityTableColumn<MobileAppBundleInfo>('createdTime', 'common.created-time', this.datePipe, '170px'), |
|||
new EntityTableColumn<MobileAppBundleInfo>('title', 'mobile.title', '25%'), |
|||
new EntityChipsEntityTableColumn<MobileAppBundleInfo>('oauth2ClientInfos', 'mobile.oauth-clients', '35%'), |
|||
new EntityChipsEntityTableColumn<MobileAppBundleInfo>('androidPkg', 'mobile.android-app', '20%'), |
|||
new EntityChipsEntityTableColumn<MobileAppBundleInfo>('iosPkg', 'mobile.ios-app', '20%'), |
|||
new EntityTableColumn<MobileAppBundleInfo>('oauth2Enabled', 'mobile.enable-oauth', '140px', |
|||
entity => checkBoxCell(entity.oauth2Enabled)) |
|||
) |
|||
|
|||
this.config.deleteEnabled = bundle => !(bundle.iosAppId || bundle.androidAppId); |
|||
this.config.deleteEntityTitle = (bundle) => this.translate.instant('mobile.delete-applications-bundle-title', {bundleName: bundle.name}); |
|||
this.config.deleteEntityContent = () => this.translate.instant('mobile.delete-applications-bundle-text'); |
|||
this.config.deleteEntity = id => this.mobileAppService.deleteMobileAppBundle(id.id); |
|||
|
|||
this.config.entitiesFetchFunction = pageLink => this.mobileAppService.getTenantMobileAppBundleInfos(pageLink).pipe( |
|||
map(bundles => { |
|||
bundles.data.map(data => { |
|||
if (data.androidPkgName) { |
|||
data.androidPkg = { |
|||
id: data.androidAppId, |
|||
name: data.androidPkgName |
|||
} |
|||
} |
|||
if (data.iosPkgName) { |
|||
data.iosPkg = { |
|||
id: data.iosAppId, |
|||
name: data.iosPkgName |
|||
} |
|||
} |
|||
}) |
|||
return bundles; |
|||
}) |
|||
); |
|||
|
|||
this.config.handleRowClick = ($event, bundle) => { |
|||
$event?.stopPropagation(); |
|||
this.mobileAppService.getMobileAppBundleInfoById(bundle.id.id).subscribe(appBundleInfo => { |
|||
this.editBundle($event, appBundleInfo); |
|||
}) |
|||
return true; |
|||
}; |
|||
} |
|||
|
|||
resolve(_route: ActivatedRouteSnapshot): EntityTableConfig<MobileAppBundleInfo> { |
|||
return this.config; |
|||
} |
|||
|
|||
private editBundle($event: Event, bundle: MobileAppBundleInfo, isAdd = false) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.dialog.open<MobileBundleDialogComponent, MobileBundleDialogData, |
|||
MobileAppBundleInfo>(MobileBundleDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
isAdd, |
|||
bundle |
|||
} |
|||
}).afterClosed() |
|||
.subscribe((res) => { |
|||
if (res) { |
|||
this.config.updateData(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private onBundleAction(action: EntityAction<MobileAppBundleInfo>): boolean { |
|||
switch (action.action) { |
|||
case 'add': |
|||
this.editBundle(action.event, action.entity, true); |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div fxFlex="100" fxLayout="row" fxLayoutAlign="space-between center" fxLayoutAlign.xs="start center"> |
|||
<section fxHide fxShow.gt-xs class="tb-entity-title-container"> |
|||
<div tbTruncateWithTooltip translate>mobile.bundles</div> |
|||
<div tb-help="oauth2Settings"></div> |
|||
</section> |
|||
<button mat-stroked-button color="primary" (click)="createBundle($event)"> |
|||
<div fxLayout="row" fxLayoutAlign="start center"> |
|||
<mat-icon>add</mat-icon>{{ 'mobile.add-bundle' | translate }} |
|||
</div> |
|||
</button> |
|||
</div> |
|||
@ -0,0 +1,23 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host{ |
|||
width: 100000px; |
|||
|
|||
.tb-entity-title-container { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component } from '@angular/core'; |
|||
import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; |
|||
import { MobileAppBundleInfo } from '@shared/models/mobile-app.models'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-mobile-bundle-table-header', |
|||
templateUrl: './mobile-bundle-table-header.component.html', |
|||
styleUrls: ['./mobile-bundle-table-header.component.scss'] |
|||
}) |
|||
export class MobileBundleTableHeaderComponent extends EntityTableHeaderComponent<MobileAppBundleInfo> { |
|||
|
|||
constructor(protected store: Store<AppState>) { |
|||
super(store); |
|||
} |
|||
|
|||
createBundle($event: Event) { |
|||
this.entitiesTableConfig.onEntityAction({event: $event, action: 'add', entity: null}); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue