Commit ce1a3ea56713b327a3b5c23dc4ceb5880a4415c4

Authored by Vladyslav_Prykhodko
1 parent fa3bbb28

UI: OTA updates add support external URL

@@ -98,10 +98,10 @@ export class DeviceProfileService { @@ -98,10 +98,10 @@ export class DeviceProfileService {
98 text += this.translate.instant('ota-update.change-firmware', {count: deviceFirmwareUpdate}); 98 text += this.translate.instant('ota-update.change-firmware', {count: deviceFirmwareUpdate});
99 } 99 }
100 if (deviceSoftwareUpdate > 0) { 100 if (deviceSoftwareUpdate > 0) {
101 - text += text.length ? '<br/>' : ''; 101 + text += text.length ? ' ' : '';
102 text += this.translate.instant('ota-update.change-software', {count: deviceSoftwareUpdate}); 102 text += this.translate.instant('ota-update.change-software', {count: deviceSoftwareUpdate});
103 } 103 }
104 - return text !== '' ? this.dialogService.confirm('', text) : of(true); 104 + return text !== '' ? this.dialogService.confirm('', text, null, this.translate.instant('common.proceed')) : of(true);
105 }), 105 }),
106 mergeMap((update) => update ? this.saveDeviceProfile(deviceProfile, config) : throwError('Canceled saving device profiles'))); 106 mergeMap((update) => update ? this.saveDeviceProfile(deviceProfile, config) : throwError('Canceled saving device profiles')));
107 } 107 }
@@ -35,6 +35,10 @@ import { PageLink } from '@shared/models/page/page-link'; @@ -35,6 +35,10 @@ import { PageLink } from '@shared/models/page/page-link';
35 import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component'; 35 import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component';
36 import { EntityAction } from '@home/models/entity/entity-component.models'; 36 import { EntityAction } from '@home/models/entity/entity-component.models';
37 import { FileSizePipe } from '@shared/pipe/file-size.pipe'; 37 import { FileSizePipe } from '@shared/pipe/file-size.pipe';
  38 +import { ClipboardService } from 'ngx-clipboard';
  39 +import { ActionNotificationShow } from '@core/notification/notification.actions';
  40 +import { Store } from '@ngrx/store';
  41 +import { AppState } from '@core/core.state';
38 42
39 @Injectable() 43 @Injectable()
40 export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<OtaPackage, PageLink, OtaPackageInfo>> { 44 export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<OtaPackage, PageLink, OtaPackageInfo>> {
@@ -44,8 +48,10 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot @@ -44,8 +48,10 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot
44 48
45 constructor(private translate: TranslateService, 49 constructor(private translate: TranslateService,
46 private datePipe: DatePipe, 50 private datePipe: DatePipe,
  51 + private store: Store<AppState>,
47 private otaPackageService: OtaPackageService, 52 private otaPackageService: OtaPackageService,
48 - private fileSize: FileSizePipe) { 53 + private fileSize: FileSizePipe,
  54 + private clipboardService: ClipboardService) {
49 this.config.entityType = EntityType.OTA_PACKAGE; 55 this.config.entityType = EntityType.OTA_PACKAGE;
50 this.config.entityComponent = OtaUpdateComponent; 56 this.config.entityComponent = OtaUpdateComponent;
51 this.config.entityTranslations = entityTypeTranslations.get(EntityType.OTA_PACKAGE); 57 this.config.entityTranslations = entityTypeTranslations.get(EntityType.OTA_PACKAGE);
@@ -62,15 +68,21 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot @@ -62,15 +68,21 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot
62 }), 68 }),
63 new EntityTableColumn<OtaPackageInfo>('fileName', 'ota-update.file-name', '25%'), 69 new EntityTableColumn<OtaPackageInfo>('fileName', 'ota-update.file-name', '25%'),
64 new EntityTableColumn<OtaPackageInfo>('dataSize', 'ota-update.file-size', '70px', entity => { 70 new EntityTableColumn<OtaPackageInfo>('dataSize', 'ota-update.file-size', '70px', entity => {
65 - return this.fileSize.transform(entity.dataSize || 0); 71 + return entity.dataSize ? this.fileSize.transform(entity.dataSize) : '';
66 }), 72 }),
67 new EntityTableColumn<OtaPackageInfo>('checksum', 'ota-update.checksum', '540px', entity => { 73 new EntityTableColumn<OtaPackageInfo>('checksum', 'ota-update.checksum', '540px', entity => {
68 - return `${ChecksumAlgorithmTranslationMap.get(entity.checksumAlgorithm)}: ${entity.checksum}`; 74 + return entity.checksum ? `${ChecksumAlgorithmTranslationMap.get(entity.checksumAlgorithm)}: ${entity.checksum}` : '';
69 }, () => ({}), false) 75 }, () => ({}), false)
70 ); 76 );
71 77
72 this.config.cellActionDescriptors.push( 78 this.config.cellActionDescriptors.push(
73 { 79 {
  80 + name: this.translate.instant('ota-update.copy-checksum'),
  81 + icon: 'content_copy',
  82 + isEnabled: (otaPackage) => !!otaPackage.checksum,
  83 + onAction: ($event, entity) => this.copyPackageChecksum($event, entity)
  84 + },
  85 + {
74 name: this.translate.instant('ota-update.download'), 86 name: this.translate.instant('ota-update.download'),
75 icon: 'file_download', 87 icon: 'file_download',
76 isEnabled: (otaPackage) => otaPackage.hasData, 88 isEnabled: (otaPackage) => otaPackage.hasData,
@@ -101,7 +113,26 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot @@ -101,7 +113,26 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot
101 if ($event) { 113 if ($event) {
102 $event.stopPropagation(); 114 $event.stopPropagation();
103 } 115 }
104 - this.otaPackageService.downloadOtaPackage(otaPackageInfo.id.id).subscribe(); 116 + if (otaPackageInfo.url) {
  117 + window.open(otaPackageInfo.url, '_blank');
  118 + } else {
  119 + this.otaPackageService.downloadOtaPackage(otaPackageInfo.id.id).subscribe();
  120 + }
  121 + }
  122 +
  123 + copyPackageChecksum($event: Event, otaPackageInfo: OtaPackageInfo) {
  124 + if ($event) {
  125 + $event.stopPropagation();
  126 + }
  127 + this.clipboardService.copy(otaPackageInfo?.checksum);
  128 + this.store.dispatch(new ActionNotificationShow(
  129 + {
  130 + message: this.translate.instant('ota-update.checksum-copied-message'),
  131 + type: 'success',
  132 + duration: 750,
  133 + verticalPosition: 'bottom',
  134 + horizontalPosition: 'right'
  135 + }));
105 } 136 }
106 137
107 onPackageAction(action: EntityAction<OtaPackageInfo>): boolean { 138 onPackageAction(action: EntityAction<OtaPackageInfo>): boolean {
@@ -109,6 +140,9 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot @@ -109,6 +140,9 @@ export class OtaUpdateTableConfigResolve implements Resolve<EntityTableConfig<Ot
109 case 'uploadPackage': 140 case 'uploadPackage':
110 this.exportPackage(action.event, action.entity); 141 this.exportPackage(action.event, action.entity);
111 return true; 142 return true;
  143 + case 'copyChecksum':
  144 + this.copyPackageChecksum(action.event, action.entity);
  145 + return true;
112 } 146 }
113 return false; 147 return false;
114 } 148 }
@@ -31,6 +31,7 @@ @@ -31,6 +31,7 @@
31 <div fxLayout="row" fxLayout.xs="column"> 31 <div fxLayout="row" fxLayout.xs="column">
32 <button mat-raised-button 32 <button mat-raised-button
33 ngxClipboard 33 ngxClipboard
  34 + [disabled]="(isLoading$ | async)"
34 (cbOnSuccess)="onPackageIdCopied()" 35 (cbOnSuccess)="onPackageIdCopied()"
35 [cbContent]="entity?.id?.id" 36 [cbContent]="entity?.id?.id"
36 [fxShow]="!isEdit"> 37 [fxShow]="!isEdit">
@@ -38,10 +39,9 @@ @@ -38,10 +39,9 @@
38 <span translate>ota-update.copyId</span> 39 <span translate>ota-update.copyId</span>
39 </button> 40 </button>
40 <button mat-raised-button 41 <button mat-raised-button
41 - ngxClipboard  
42 - (cbOnSuccess)="onPackageChecksumCopied()"  
43 - [cbContent]="entity?.checksum"  
44 - [fxShow]="!isEdit"> 42 + [disabled]="(isLoading$ | async)"
  43 + (click)="onEntityAction($event, 'copyChecksum')"
  44 + [fxShow]="!isEdit && entity?.checksum">
45 <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> 45 <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
46 <span translate>ota-update.copy-checksum</span> 46 <span translate>ota-update.copy-checksum</span>
47 </button> 47 </button>
@@ -49,7 +49,7 @@ @@ -49,7 +49,7 @@
49 </div> 49 </div>
50 <div class="mat-padding" fxLayout="column" fxLayoutGap="8px"> 50 <div class="mat-padding" fxLayout="column" fxLayoutGap="8px">
51 <form [formGroup]="entityForm"> 51 <form [formGroup]="entityForm">
52 - <fieldset [disabled]="(isLoading$ | async) || !isEdit" fxLayout="column" fxLayoutGap="8px"> 52 + <fieldset [disabled]="(isLoading$ | async) || !isEdit" fxLayout="column">
53 <div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column"> 53 <div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
54 <mat-form-field class="mat-block" fxFlex="45"> 54 <mat-form-field class="mat-block" fxFlex="45">
55 <mat-label translate>ota-update.title</mat-label> 55 <mat-label translate>ota-update.title</mat-label>
@@ -83,44 +83,68 @@ @@ -83,44 +83,68 @@
83 </mat-option> 83 </mat-option>
84 </mat-select> 84 </mat-select>
85 </mat-form-field> 85 </mat-form-field>
86 - <div class="mat-caption" translate *ngIf="isAdd">ota-update.warning-after-save-no-edit</div>  
87 - <div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayoutGap.sm="8px" fxLayout.xs="column" fxLayout.md="column">  
88 - <mat-form-field class="mat-block" fxFlex="33">  
89 - <mat-label translate>ota-update.checksum-algorithm</mat-label>  
90 - <mat-select formControlName="checksumAlgorithm">  
91 - <mat-option *ngFor="let checksumAlgorithm of checksumAlgorithms" [value]="checksumAlgorithm">  
92 - {{ checksumAlgorithmTranslationMap.get(checksumAlgorithm) }}  
93 - </mat-option>  
94 - </mat-select>  
95 - </mat-form-field>  
96 - <mat-form-field class="mat-block" fxFlex>  
97 - <mat-label translate>ota-update.checksum</mat-label>  
98 - <input matInput formControlName="checksum" type="text" [readonly]="!isAdd">  
99 - </mat-form-field>  
100 - </div>  
101 <section *ngIf="isAdd"> 86 <section *ngIf="isAdd">
102 - <tb-file-input  
103 - formControlName="file"  
104 - workFromFileObj="true"  
105 - required  
106 - dropLabel="{{'ota-update.drop-file' | translate}}">  
107 - </tb-file-input> 87 + <div class="mat-caption" style="margin: -8px 0 8px;" translate>ota-update.warning-after-save-no-edit</div>
  88 + <mat-radio-group formControlName="resource" fxLayoutGap="16px">
  89 + <mat-radio-button value="file">Upload binary file</mat-radio-button>
  90 + <mat-radio-button value="url">Use external URL</mat-radio-button>
  91 + </mat-radio-group>
108 </section> 92 </section>
109 - <section *ngIf="!isAdd">  
110 - <div fxLayout="row" fxLayoutGap.gt-md="8px" fxLayoutGap.sm="8px" fxLayout.xs="column" fxLayout.md="column"> 93 + <section *ngIf="entityForm.get('resource').value === 'file'">
  94 + <section *ngIf="isAdd">
  95 + <tb-file-input
  96 + formControlName="file"
  97 + workFromFileObj="true"
  98 + [required]="entityForm.get('resource').value === 'file'"
  99 + dropLabel="{{'ota-update.drop-file' | translate}}">
  100 + </tb-file-input>
  101 + <mat-checkbox formControlName="generateChecksum" style="margin-top: 16px">
  102 + {{ 'ota-update.auto-generate-checksum' | translate }}
  103 + </mat-checkbox>
  104 + </section>
  105 + <div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayoutGap.sm="8px"
  106 + fxLayout.xs="column" fxLayout.md="column" *ngIf="!(isAdd && this.entityForm.get('generateChecksum').value)">
111 <mat-form-field class="mat-block" fxFlex="33"> 107 <mat-form-field class="mat-block" fxFlex="33">
112 - <mat-label translate>ota-update.file-name</mat-label>  
113 - <input matInput formControlName="fileName" type="text" readonly> 108 + <mat-label translate>ota-update.checksum-algorithm</mat-label>
  109 + <mat-select formControlName="checksumAlgorithm">
  110 + <mat-option *ngFor="let checksumAlgorithm of checksumAlgorithms" [value]="checksumAlgorithm">
  111 + {{ checksumAlgorithmTranslationMap.get(checksumAlgorithm) }}
  112 + </mat-option>
  113 + </mat-select>
114 </mat-form-field> 114 </mat-form-field>
115 <mat-form-field class="mat-block" fxFlex> 115 <mat-form-field class="mat-block" fxFlex>
116 - <mat-label translate>ota-update.file-size-bytes</mat-label>  
117 - <input matInput formControlName="dataSize" type="text" readonly>  
118 - </mat-form-field>  
119 - <mat-form-field class="mat-block" fxFlex>  
120 - <mat-label translate>ota-update.content-type</mat-label>  
121 - <input matInput formControlName="contentType" type="text" readonly> 116 + <mat-label translate>ota-update.checksum</mat-label>
  117 + <input matInput formControlName="checksum" type="text">
  118 + <mat-hint *ngIf="isAdd" translate>ota-update.checksum-hint</mat-hint>
122 </mat-form-field> 119 </mat-form-field>
123 </div> 120 </div>
  121 + <section *ngIf="!isAdd">
  122 + <div fxLayout="row" fxLayoutGap.gt-md="8px" fxLayoutGap.sm="8px" fxLayout.xs="column" fxLayout.md="column">
  123 + <mat-form-field class="mat-block" fxFlex="33">
  124 + <mat-label translate>ota-update.file-name</mat-label>
  125 + <input matInput formControlName="fileName" type="text">
  126 + </mat-form-field>
  127 + <mat-form-field class="mat-block" fxFlex>
  128 + <mat-label translate>ota-update.file-size-bytes</mat-label>
  129 + <input matInput formControlName="dataSize" type="text">
  130 + </mat-form-field>
  131 + <mat-form-field class="mat-block" fxFlex>
  132 + <mat-label translate>ota-update.content-type</mat-label>
  133 + <input matInput formControlName="contentType" type="text">
  134 + </mat-form-field>
  135 + </div>
  136 + </section>
  137 + </section>
  138 + <section *ngIf="entityForm.get('resource').value === 'url'" style="margin-top: 8px">
  139 + <mat-form-field class="mat-block">
  140 + <mat-label translate>ota-update.url</mat-label>
  141 + <input matInput formControlName="url"
  142 + type="text"
  143 + [required]="entityForm.get('resource').value === 'url'">
  144 + <mat-error *ngIf="entityForm.get('url').hasError('required')" translate>
  145 + ota-update.url-required
  146 + </mat-error>
  147 + </mat-form-field>
124 </section> 148 </section>
125 <div formGroupName="additionalInfo"> 149 <div formGroupName="additionalInfo">
126 <mat-form-field class="mat-block"> 150 <mat-form-field class="mat-block">
@@ -30,6 +30,8 @@ import { @@ -30,6 +30,8 @@ import {
30 OtaUpdateTypeTranslationMap 30 OtaUpdateTypeTranslationMap
31 } from '@shared/models/ota-package.models'; 31 } from '@shared/models/ota-package.models';
32 import { ActionNotificationShow } from '@core/notification/notification.actions'; 32 import { ActionNotificationShow } from '@core/notification/notification.actions';
  33 +import { filter, takeUntil } from 'rxjs/operators';
  34 +import { isNotEmptyStr } from '@core/utils';
33 35
34 @Component({ 36 @Component({
35 selector: 'tb-ota-update', 37 selector: 'tb-ota-update',
@@ -52,6 +54,26 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O @@ -52,6 +54,26 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
52 super(store, fb, entityValue, entitiesTableConfigValue); 54 super(store, fb, entityValue, entitiesTableConfigValue);
53 } 55 }
54 56
  57 + ngOnInit() {
  58 + super.ngOnInit();
  59 + this.entityForm.get('resource').valueChanges.pipe(
  60 + filter(() => this.isAdd),
  61 + takeUntil(this.destroy$)
  62 + ).subscribe((resource) => {
  63 + if (resource === 'file') {
  64 + this.entityForm.get('url').clearValidators();
  65 + this.entityForm.get('file').setValidators(Validators.required);
  66 + this.entityForm.get('url').updateValueAndValidity({emitEvent: false});
  67 + this.entityForm.get('file').updateValueAndValidity({emitEvent: false});
  68 + } else {
  69 + this.entityForm.get('file').clearValidators();
  70 + this.entityForm.get('url').setValidators(Validators.required);
  71 + this.entityForm.get('file').updateValueAndValidity({emitEvent: false});
  72 + this.entityForm.get('url').updateValueAndValidity({emitEvent: false});
  73 + }
  74 + });
  75 + }
  76 +
55 ngOnDestroy() { 77 ngOnDestroy() {
56 super.ngOnDestroy(); 78 super.ngOnDestroy();
57 this.destroy$.next(); 79 this.destroy$.next();
@@ -74,6 +96,8 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O @@ -74,6 +96,8 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
74 deviceProfileId: [entity ? entity.deviceProfileId : null, Validators.required], 96 deviceProfileId: [entity ? entity.deviceProfileId : null, Validators.required],
75 checksumAlgorithm: [entity && entity.checksumAlgorithm ? entity.checksumAlgorithm : ChecksumAlgorithm.SHA256], 97 checksumAlgorithm: [entity && entity.checksumAlgorithm ? entity.checksumAlgorithm : ChecksumAlgorithm.SHA256],
76 checksum: [entity ? entity.checksum : '', Validators.maxLength(1020)], 98 checksum: [entity ? entity.checksum : '', Validators.maxLength(1020)],
  99 + url: [entity ? entity.url : ''],
  100 + resource: ['file'],
77 additionalInfo: this.fb.group( 101 additionalInfo: this.fb.group(
78 { 102 {
79 description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], 103 description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
@@ -82,6 +106,7 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O @@ -82,6 +106,7 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
82 }); 106 });
83 if (this.isAdd) { 107 if (this.isAdd) {
84 form.addControl('file', this.fb.control(null, Validators.required)); 108 form.addControl('file', this.fb.control(null, Validators.required));
  109 + form.addControl('generateChecksum', this.fb.control(true));
85 } else { 110 } else {
86 form.addControl('fileName', this.fb.control(null)); 111 form.addControl('fileName', this.fb.control(null));
87 form.addControl('dataSize', this.fb.control(null)); 112 form.addControl('dataSize', this.fb.control(null));
@@ -101,6 +126,8 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O @@ -101,6 +126,8 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
101 fileName: entity.fileName, 126 fileName: entity.fileName,
102 dataSize: entity.dataSize, 127 dataSize: entity.dataSize,
103 contentType: entity.contentType, 128 contentType: entity.contentType,
  129 + url: entity.url,
  130 + resource: isNotEmptyStr(entity.url) ? 'url' : 'file',
104 additionalInfo: { 131 additionalInfo: {
105 description: entity.additionalInfo ? entity.additionalInfo.description : '' 132 description: entity.additionalInfo ? entity.additionalInfo.description : ''
106 } 133 }
@@ -108,8 +135,6 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O @@ -108,8 +135,6 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
108 if (!this.isAdd && this.entityForm.enabled) { 135 if (!this.isAdd && this.entityForm.enabled) {
109 this.entityForm.disable({emitEvent: false}); 136 this.entityForm.disable({emitEvent: false});
110 this.entityForm.get('additionalInfo').enable({emitEvent: false}); 137 this.entityForm.get('additionalInfo').enable({emitEvent: false});
111 - // this.entityForm.get('dataSize').disable({emitEvent: false});  
112 - // this.entityForm.get('contentType').disable({emitEvent: false});  
113 } 138 }
114 } 139 }
115 140
@@ -124,14 +149,9 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O @@ -124,14 +149,9 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
124 })); 149 }));
125 } 150 }
126 151
127 - onPackageChecksumCopied() {  
128 - this.store.dispatch(new ActionNotificationShow(  
129 - {  
130 - message: this.translate.instant('ota-update.checksum-copied-message'),  
131 - type: 'success',  
132 - duration: 750,  
133 - verticalPosition: 'bottom',  
134 - horizontalPosition: 'right'  
135 - })); 152 + prepareFormValue(formValue: any): any {
  153 + delete formValue.resource;
  154 + delete formValue.generateChecksum;
  155 + return super.prepareFormValue(formValue);
136 } 156 }
137 } 157 }
@@ -87,6 +87,7 @@ export interface OtaPackageInfo extends BaseData<OtaPackageId> { @@ -87,6 +87,7 @@ export interface OtaPackageInfo extends BaseData<OtaPackageId> {
87 title?: string; 87 title?: string;
88 version?: string; 88 version?: string;
89 hasData?: boolean; 89 hasData?: boolean;
  90 + url?: string;
90 fileName: string; 91 fileName: string;
91 checksum?: string; 92 checksum?: string;
92 checksumAlgorithm?: ChecksumAlgorithm; 93 checksumAlgorithm?: ChecksumAlgorithm;
@@ -575,7 +575,8 @@ @@ -575,7 +575,8 @@
575 "enter-password": "Enter password", 575 "enter-password": "Enter password",
576 "enter-search": "Enter search", 576 "enter-search": "Enter search",
577 "created-time": "Created time", 577 "created-time": "Created time",
578 - "loading": "Loading..." 578 + "loading": "Loading...",
  579 + "proceed": "Proceed"
579 }, 580 },
580 "content-type": { 581 "content-type": {
581 "json": "Json", 582 "json": "Json",
@@ -2152,12 +2153,14 @@ @@ -2152,12 +2153,14 @@
2152 "assign-firmware-required": "Assigned firmware is required", 2153 "assign-firmware-required": "Assigned firmware is required",
2153 "assign-software": "Assigned software", 2154 "assign-software": "Assigned software",
2154 "assign-software-required": "Assigned software is required", 2155 "assign-software-required": "Assigned software is required",
  2156 + "auto-generate-checksum": "Auto-generate checksum",
2155 "checksum": "Checksum", 2157 "checksum": "Checksum",
  2158 + "checksum-hint": "If checksum is empty, it will be generated automatically",
2156 "checksum-algorithm": "Checksum algorithm", 2159 "checksum-algorithm": "Checksum algorithm",
2157 "checksum-copied-message": "Package checksum has been copied to clipboard", 2160 "checksum-copied-message": "Package checksum has been copied to clipboard",
2158 - "change-firmware": "You have changed the firmware. This may cause update of the { count, plural, 1 {1 device} other {# devices} }.",  
2159 - "change-software": "You have changed the software. This may cause update of the { count, plural, 1 {1 device} other {# devices} }.",  
2160 - "chose-compatible-device-profile": "Choose compatible device profile. The uploaded package will be available only for devices with the chosen profile.", 2161 + "change-firmware": "Change of the firmware may cause update of { count, plural, 1 {1 device} other {# devices} }.",
  2162 + "change-software": "Change of the software may cause update of { count, plural, 1 {1 device} other {# devices} }.",
  2163 + "chose-compatible-device-profile": "The uploaded package will be available only for devices with the chosen profile.",
2161 "chose-firmware-distributed-device": "Choose firmware that will be distributed to the devices", 2164 "chose-firmware-distributed-device": "Choose firmware that will be distributed to the devices",
2162 "chose-software-distributed-device": "Choose software that will be distributed to the devices", 2165 "chose-software-distributed-device": "Choose software that will be distributed to the devices",
2163 "content-type": "Content type", 2166 "content-type": "Content type",
@@ -2193,6 +2196,8 @@ @@ -2193,6 +2196,8 @@
2193 "firmware": "Firmware", 2196 "firmware": "Firmware",
2194 "software": "Software" 2197 "software": "Software"
2195 }, 2198 },
  2199 + "url": "Direct URL",
  2200 + "url-required": "Direct URL is required",
2196 "version": "Version", 2201 "version": "Version",
2197 "version-required": "Version is required.", 2202 "version-required": "Version is required.",
2198 "warning-after-save-no-edit": "Once the package is uploaded, you will not be able to modify title, version, device profile and package type." 2203 "warning-after-save-no-edit": "Once the package is uploaded, you will not be able to modify title, version, device profile and package type."