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