diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts index c213ab9ee6..a3a05d6a50 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/applications.module.ts @@ -21,11 +21,13 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { HomeComponentsModule } from '@home/components/home-components.module'; import { ApplicationsRoutingModule } from '@home/pages/mobile/applications/applications-routing.module'; +import { ReleaseNotesPanelComponent } from '@home/pages/mobile/applications/release-notes-panel.component'; @NgModule({ declarations: [ MobileAppComponent, - MobileAppTableHeaderComponent + MobileAppTableHeaderComponent, + ReleaseNotesPanelComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts index 2c86f6124c..ef513728a9 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts @@ -120,7 +120,7 @@ export class MobileAppTableConfigResolver { backgroundColor = 'rgba(209, 39, 48, 0.06)'; break; case MobileAppStatus.DRAFT: - backgroundColor = 'rgba(160, 160, 160, 0.06)'; + backgroundColor = 'rgba(0, 148, 255, 0.06)'; break; } return `
+ {{ 'mobile.application-secret-required' | translate }} + mobile.status @@ -80,14 +83,36 @@
mobile.version-information
- - mobile.min-version - - - - mobile.latest-version - - +
+ + mobile.min-version + + + +
+
+ + mobile.latest-version + + + +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts index e801a8966e..c4eb358a22 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts @@ -14,7 +14,7 @@ /// 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 { AppState } from '@core/core.state'; 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 { PlatformType, platformTypeTranslations } from '@shared/models/oauth2.models'; 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({ selector: 'tb-mobile-app', @@ -51,7 +54,10 @@ export class MobileAppComponent extends EntityComponent { @Inject('entity') protected entityValue: MobileApp, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, protected cd: ChangeDetectorRef, - public fb: FormBuilder) { + public fb: FormBuilder, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef) { super(store, fb, entityValue, entitiesTableConfigValue, cd); } @@ -64,7 +70,9 @@ export class MobileAppComponent extends EntityComponent { status: [entity?.status ? entity.status : MobileAppStatus.DRAFT], versionInfo: this.fb.group({ minVersion: [entity?.versionInfo?.minVersion ? entity.versionInfo.minVersion : ''], + minVersionReleaseNotes: [entity?.versionInfo?.minVersionReleaseNotes ? entity.versionInfo.minVersionReleaseNotes : ''], latestVersion: [entity?.versionInfo?.latestVersion ? entity.versionInfo.latestVersion : ''], + latestVersionReleaseNotes: [entity?.versionInfo?.latestVersionReleaseNotes ? entity.versionInfo.latestVersionReleaseNotes : ''], }), storeInfo: this.fb.group({ storeLink: [entity?.storeInfo?.storeLink ? entity.storeInfo.storeLink : ''], @@ -89,7 +97,7 @@ export class MobileAppComponent extends EntityComponent { form.get('status').valueChanges.pipe( takeUntilDestroyed() ).subscribe((value: MobileAppStatus) => { - if (value === MobileAppStatus.PUBLISHED) { + if (value !== MobileAppStatus.DRAFT) { form.get('storeInfo.storeLink').addValidators(Validators.required); form.get('storeInfo.sha256CertFingerprints').addValidators(Validators.required); form.get('storeInfo.appId').addValidators(Validators.required); @@ -113,6 +121,7 @@ export class MobileAppComponent extends EntityComponent { override updateFormState(): void { super.updateFormState(); if (this.isEdit && this.entityForm && !this.isAdd) { + this.entityForm.get('status').updateValueAndValidity({onlySelf: false}); this.entityForm.get('platformType').disable({emitEvent: false}); if (this.entityForm.get('platformType').value === PlatformType.ANDROID) { this.entityForm.get('storeInfo.appId').disable({emitEvent: false}); @@ -136,6 +145,40 @@ export class MobileAppComponent extends EntityComponent { 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 { if (control.value === '') { return null; diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.html new file mode 100644 index 0000000000..d667a54b01 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.html @@ -0,0 +1,36 @@ + +
+
{{ title | translate }}
+ +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.scss new file mode 100644 index 0000000000..f70487a708 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.scss @@ -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; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.ts new file mode 100644 index 0000000000..935e949f3d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/release-notes-panel.component.ts @@ -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; + + @Output() + releaseNotesApplied = new EventEmitter(); + + title: string; + + releaseNotesControl: FormControl; + + tinyMceOptions: Record = { + 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); + } + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 61305d3b77..d600843dd6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4067,6 +4067,7 @@ "application-details": "Application details", "application-package": "Application Package", "application-secret": "Application Secret", + "application-secret-required": "Application Secret is required", "applications": "Applications", "copy-app-id": "Copy App ID", "copy-app-store-link": "Copy App Store link", @@ -4100,7 +4101,9 @@ "suspended": "Suspended" }, "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": { "action-button": "Action button",