UI: Add release note to mobile application

This commit is contained in:
Vladyslav_Prykhodko 2024-10-16 10:54:26 +03:00
parent 119354a441
commit ed24383e2a
8 changed files with 247 additions and 15 deletions

View File

@ -21,11 +21,13 @@ import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { HomeComponentsModule } from '@home/components/home-components.module'; import { HomeComponentsModule } from '@home/components/home-components.module';
import { ApplicationsRoutingModule } from '@home/pages/mobile/applications/applications-routing.module'; import { ApplicationsRoutingModule } from '@home/pages/mobile/applications/applications-routing.module';
import { ReleaseNotesPanelComponent } from '@home/pages/mobile/applications/release-notes-panel.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
MobileAppComponent, MobileAppComponent,
MobileAppTableHeaderComponent MobileAppTableHeaderComponent,
ReleaseNotesPanelComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

View File

@ -120,7 +120,7 @@ export class MobileAppTableConfigResolver {
backgroundColor = 'rgba(209, 39, 48, 0.06)'; backgroundColor = 'rgba(209, 39, 48, 0.06)';
break; break;
case MobileAppStatus.DRAFT: case MobileAppStatus.DRAFT:
backgroundColor = 'rgba(160, 160, 160, 0.06)'; backgroundColor = 'rgba(0, 148, 255, 0.06)';
break; break;
} }
return `<div style="border-radius: 14px; height: 28px; line-height: 20px; padding: 4px 10px; return `<div style="border-radius: 14px; height: 28px; line-height: 20px; padding: 4px 10px;
@ -142,7 +142,7 @@ export class MobileAppTableConfigResolver {
styleObj.color = '#D12730'; styleObj.color = '#D12730';
break; break;
case MobileAppStatus.DRAFT: case MobileAppStatus.DRAFT:
styleObj.color = '#A0A0A0'; styleObj.color = '#0094FF';
break; break;
} }
return styleObj; return styleObj;

View File

@ -68,6 +68,9 @@
</tb-copy-button> </tb-copy-button>
</div> </div>
<mat-hint> </mat-hint> <mat-hint> </mat-hint>
<mat-error *ngIf="entityForm.get('appSecret').hasError('required')">
{{ 'mobile.application-secret-required' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label translate>mobile.status</mat-label> <mat-label translate>mobile.status</mat-label>
@ -80,14 +83,36 @@
<section class="tb-form-panel stroked no-padding-bottom" formGroupName="versionInfo" style="margin-bottom: 21px;"> <section class="tb-form-panel stroked no-padding-bottom" formGroupName="versionInfo" style="margin-bottom: 21px;">
<div class="tb-form-panel-title" translate>mobile.version-information</div> <div class="tb-form-panel-title" translate>mobile.version-information</div>
<section fxLayout="column"> <section fxLayout="column">
<mat-form-field appearance="outline"> <section fxLayout="row" fxLayoutAlign="start start">
<mat-label translate>mobile.min-version</mat-label> <mat-form-field fxFlex appearance="outline">
<input matInput formControlName="minVersion"> <mat-label translate>mobile.min-version</mat-label>
</mat-form-field> <input matInput formControlName="minVersion">
<mat-form-field appearance="outline"> </mat-form-field>
<mat-label translate>mobile.latest-version</mat-label> <button mat-icon-button
<input matInput formControlName="latestVersion"> style="margin-top: 4px; color:#00000061"
</mat-form-field> type="button"
matTooltip="{{ 'mobile.min-version-release-notes' | translate }}"
matTooltipPosition="above"
#editMinReleaseNotesVersionButton
(click)="editReleaseNote($event, editMinReleaseNotesVersionButton, false)">
<tb-icon>mdi:text-box-edit</tb-icon>
</button>
</section>
<section fxLayout="row" fxLayoutAlign="start start">
<mat-form-field fxFlex appearance="outline">
<mat-label translate>mobile.latest-version</mat-label>
<input matInput formControlName="latestVersion">
</mat-form-field>
<button mat-icon-button
style="margin-top: 4px; color:#00000061"
type="button"
matTooltip="{{ 'mobile.latest-version-release-notes' | translate }}"
matTooltipPosition="above"
#editLatestReleaseNotesVersionButton
(click)="editReleaseNote($event, editLatestReleaseNotesVersionButton, true)">
<tb-icon>mdi:text-box-edit</tb-icon>
</button>
</section>
</section> </section>
</section> </section>
<section class="tb-form-panel stroked no-padding-bottom" formGroupName="storeInfo"> <section class="tb-form-panel stroked no-padding-bottom" formGroupName="storeInfo">

View File

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ChangeDetectorRef, Component, Inject } from '@angular/core'; import { ChangeDetectorRef, Component, Inject, Renderer2, ViewContainerRef } from '@angular/core';
import { EntityComponent } from '@home/components/entity/entity.component'; import { EntityComponent } from '@home/components/entity/entity.component';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
@ -26,6 +26,9 @@ import { EntityType } from '@shared/models/entity-type.models';
import { MobileApp, MobileAppStatus, mobileAppStatusTranslations } from '@shared/models/mobile-app.models'; import { MobileApp, MobileAppStatus, mobileAppStatusTranslations } from '@shared/models/mobile-app.models';
import { PlatformType, platformTypeTranslations } from '@shared/models/oauth2.models'; import { PlatformType, platformTypeTranslations } from '@shared/models/oauth2.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { ReleaseNotesPanelComponent } from '@home/pages/mobile/applications/release-notes-panel.component';
@Component({ @Component({
selector: 'tb-mobile-app', selector: 'tb-mobile-app',
@ -51,7 +54,10 @@ export class MobileAppComponent extends EntityComponent<MobileApp> {
@Inject('entity') protected entityValue: MobileApp, @Inject('entity') protected entityValue: MobileApp,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<MobileApp>, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<MobileApp>,
protected cd: ChangeDetectorRef, protected cd: ChangeDetectorRef,
public fb: FormBuilder) { public fb: FormBuilder,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd); super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
@ -64,7 +70,9 @@ export class MobileAppComponent extends EntityComponent<MobileApp> {
status: [entity?.status ? entity.status : MobileAppStatus.DRAFT], status: [entity?.status ? entity.status : MobileAppStatus.DRAFT],
versionInfo: this.fb.group({ versionInfo: this.fb.group({
minVersion: [entity?.versionInfo?.minVersion ? entity.versionInfo.minVersion : ''], minVersion: [entity?.versionInfo?.minVersion ? entity.versionInfo.minVersion : ''],
minVersionReleaseNotes: [entity?.versionInfo?.minVersionReleaseNotes ? entity.versionInfo.minVersionReleaseNotes : ''],
latestVersion: [entity?.versionInfo?.latestVersion ? entity.versionInfo.latestVersion : ''], latestVersion: [entity?.versionInfo?.latestVersion ? entity.versionInfo.latestVersion : ''],
latestVersionReleaseNotes: [entity?.versionInfo?.latestVersionReleaseNotes ? entity.versionInfo.latestVersionReleaseNotes : ''],
}), }),
storeInfo: this.fb.group({ storeInfo: this.fb.group({
storeLink: [entity?.storeInfo?.storeLink ? entity.storeInfo.storeLink : ''], storeLink: [entity?.storeInfo?.storeLink ? entity.storeInfo.storeLink : ''],
@ -89,7 +97,7 @@ export class MobileAppComponent extends EntityComponent<MobileApp> {
form.get('status').valueChanges.pipe( form.get('status').valueChanges.pipe(
takeUntilDestroyed() takeUntilDestroyed()
).subscribe((value: MobileAppStatus) => { ).subscribe((value: MobileAppStatus) => {
if (value === MobileAppStatus.PUBLISHED) { if (value !== MobileAppStatus.DRAFT) {
form.get('storeInfo.storeLink').addValidators(Validators.required); form.get('storeInfo.storeLink').addValidators(Validators.required);
form.get('storeInfo.sha256CertFingerprints').addValidators(Validators.required); form.get('storeInfo.sha256CertFingerprints').addValidators(Validators.required);
form.get('storeInfo.appId').addValidators(Validators.required); form.get('storeInfo.appId').addValidators(Validators.required);
@ -113,6 +121,7 @@ export class MobileAppComponent extends EntityComponent<MobileApp> {
override updateFormState(): void { override updateFormState(): void {
super.updateFormState(); super.updateFormState();
if (this.isEdit && this.entityForm && !this.isAdd) { if (this.isEdit && this.entityForm && !this.isAdd) {
this.entityForm.get('status').updateValueAndValidity({onlySelf: false});
this.entityForm.get('platformType').disable({emitEvent: false}); this.entityForm.get('platformType').disable({emitEvent: false});
if (this.entityForm.get('platformType').value === PlatformType.ANDROID) { if (this.entityForm.get('platformType').value === PlatformType.ANDROID) {
this.entityForm.get('storeInfo.appId').disable({emitEvent: false}); this.entityForm.get('storeInfo.appId').disable({emitEvent: false});
@ -136,6 +145,40 @@ export class MobileAppComponent extends EntityComponent<MobileApp> {
this.entityForm.get('appSecret').markAsDirty(); this.entityForm.get('appSecret').markAsDirty();
} }
editReleaseNote($event: Event, matButton: MatButton, isLatest: boolean) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
disabled: !(this.isAdd || this.isEdit),
isLatest: isLatest,
releaseNotes: isLatest
? this.entityForm.get('versionInfo.latestVersionReleaseNotes').value
: this.entityForm.get('versionInfo.minVersionReleaseNotes').value
};
const releaseNotesPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ReleaseNotesPanelComponent, ['leftOnly', 'leftBottomOnly', 'leftTopOnly'], true, null,
ctx,
{},
{}, {}, false, () => {}, {padding: '16px 24px'});
releaseNotesPanelPopover.tbComponentRef.instance.popover = releaseNotesPanelPopover;
releaseNotesPanelPopover.tbComponentRef.instance.releaseNotesApplied.subscribe((releaseNotes) => {
releaseNotesPanelPopover.hide();
if (isLatest) {
this.entityForm.get('versionInfo.latestVersionReleaseNotes').setValue(releaseNotes);
this.entityForm.get('versionInfo.latestVersionReleaseNotes').markAsDirty();
} else {
this.entityForm.get('versionInfo.minVersionReleaseNotes').setValue(releaseNotes);
this.entityForm.get('versionInfo.minVersionReleaseNotes').markAsDirty();
}
});
}
}
private base64Format(control: UntypedFormControl): { [key: string]: boolean } | null { private base64Format(control: UntypedFormControl): { [key: string]: boolean } | null {
if (control.value === '') { if (control.value === '') {
return null; return null;

View File

@ -0,0 +1,36 @@
<!--
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-release-notes-panel">
<div class="tb-release-notes-title">{{ title | translate }}</div>
<editor [init]="tinyMceOptions" [formControl]="releaseNotesControl"></editor>
<div class="tb-release-notes-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]="releaseNotesControl.invalid || !releaseNotesControl.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

View File

@ -0,0 +1,41 @@
/**
* 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-release-notes-panel {
width: 600px;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-lt-md} {
width: 90vw;
}
.tb-release-notes-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-release-notes-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

View File

@ -0,0 +1,82 @@
///
/// 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, FormControl } from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component';
@Component({
selector: 'tb-release-notes-panel',
templateUrl: './release-notes-panel.component.html',
styleUrls: ['./release-notes-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ReleaseNotesPanelComponent implements OnInit {
@Input()
disabled: boolean;
@Input()
releaseNotes: string;
@Input()
isLatest: boolean;
@Input()
popover: TbPopoverComponent<ReleaseNotesPanelComponent>;
@Output()
releaseNotesApplied = new EventEmitter<string>();
title: string;
releaseNotesControl: FormControl<string>;
tinyMceOptions: Record<string, any> = {
base_url: '/assets/tinymce',
suffix: '.min',
plugins: ['lists'],
menubar: 'edit insert view format',
toolbar: ['fontfamily fontsize | bold italic underline strikethrough forecolor backcolor',
'alignleft aligncenter alignright alignjustify | bullist'],
toolbar_mode: 'sliding',
height: 400,
autofocus: false,
branding: false,
promotion: false
};
constructor(private fb: FormBuilder) {
}
ngOnInit(): void {
this.releaseNotesControl = this.fb.control(this.releaseNotes);
this.title = this.isLatest ? 'mobile.latest-version-release-notes' : 'mobile.min-version-release-notes';
if (this.disabled) {
this.releaseNotesControl.disable({emitEvent: false});
}
}
cancel() {
this.popover?.hide();
}
apply() {
if (this.releaseNotesControl.valid) {
this.releaseNotesApplied.emit(this.releaseNotesControl.value);
}
}
}

View File

@ -4067,6 +4067,7 @@
"application-details": "Application details", "application-details": "Application details",
"application-package": "Application Package", "application-package": "Application Package",
"application-secret": "Application Secret", "application-secret": "Application Secret",
"application-secret-required": "Application Secret is required",
"applications": "Applications", "applications": "Applications",
"copy-app-id": "Copy App ID", "copy-app-id": "Copy App ID",
"copy-app-store-link": "Copy App Store link", "copy-app-store-link": "Copy App Store link",
@ -4100,7 +4101,9 @@
"suspended": "Suspended" "suspended": "Suspended"
}, },
"store-information": "Store information", "store-information": "Store information",
"version-information": "Version information" "version-information": "Version information",
"min-version-release-notes": "Min version release notes",
"latest-version-release-notes": "Latest version release notes"
}, },
"notification": { "notification": {
"action-button": "Action button", "action-button": "Action button",