UI: Implement complex version create action.

This commit is contained in:
Igor Kulikov 2022-05-30 19:05:17 +03:00
parent 0be0df6015
commit b6f964a5eb
15 changed files with 787 additions and 34 deletions

View File

@ -156,9 +156,11 @@ import { WidgetSettingsComponent } from '@home/components/widget/widget-settings
import { VersionControlSettingsComponent } from '@home/components/vc/version-control-settings.component';
import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { EntityVersionsTableComponent } from '@home/components/vc/entity-versions-table.component';
import { EntityVersionExportComponent } from '@home/components/vc/entity-version-export.component';
import { EntityVersionCreateComponent } from '@home/components/vc/entity-version-create.component';
import { EntityVersionRestoreComponent } from '@home/components/vc/entity-version-restore.component';
import { EntityVersionDiffComponent } from '@home/components/vc/entity-version-diff.component';
import { ComplexVersionCreateComponent } from '@home/components/vc/complex-version-create.component';
import { EntityTypesVersionCreateComponent } from '@home/components/vc/entity-types-version-create.component';
@NgModule({
declarations:
@ -286,9 +288,11 @@ import { EntityVersionDiffComponent } from '@home/components/vc/entity-version-d
VersionControlSettingsComponent,
VersionControlComponent,
EntityVersionsTableComponent,
EntityVersionExportComponent,
EntityVersionCreateComponent,
EntityVersionRestoreComponent,
EntityVersionDiffComponent
EntityVersionDiffComponent,
ComplexVersionCreateComponent,
EntityTypesVersionCreateComponent
],
imports: [
CommonModule,
@ -410,9 +414,11 @@ import { EntityVersionDiffComponent } from '@home/components/vc/entity-version-d
VersionControlSettingsComponent,
VersionControlComponent,
EntityVersionsTableComponent,
EntityVersionExportComponent,
EntityVersionCreateComponent,
EntityVersionRestoreComponent,
EntityVersionDiffComponent
EntityVersionDiffComponent,
ComplexVersionCreateComponent,
EntityTypesVersionCreateComponent
],
providers: [
WidgetComponentService,

View File

@ -0,0 +1,82 @@
<!--
Copyright © 2016-2022 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 style="min-width: 800px;">
<section *ngIf="!resultMessage">
<mat-toolbar>
<h2>{{ 'version-control.create-entities-version' | translate }}</h2>
<span fxFlex></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="isLoading$ | async">
</mat-progress-bar>
<form [formGroup]="createVersionFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<tb-branch-autocomplete
fxFlex
required
formControlName="branch">
</tb-branch-autocomplete>
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>version-control.version-name</mat-label>
<input required matInput formControlName="versionName">
<mat-error *ngIf="createVersionFormGroup.get('versionName').hasError('required')">
{{ 'version-control.version-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>version-control.default-sync-strategy</mat-label>
<mat-select required formControlName="syncStrategy">
<mat-option *ngFor="let strategy of syncStrategies" [value]="strategy">
{{syncStrategyTranslations.get(strategy) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<tb-entity-types-version-create
formControlName="entityTypes">
</tb-entity-types-version-create>
</div>
</fieldset>
</form>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="export()"
[disabled]="(isLoading$ | async) || createVersionFormGroup.invalid || !createVersionFormGroup.dirty">
{{ 'action.create' | translate }}
</button>
</div>
</section>
<section *ngIf="resultMessage">
<div class="mat-title create-result-message">{{ resultMessage }}</div>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
</section>
</section>

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
:host {
.export-result-message {
.create-result-message {
padding: 48px 8px 8px;
text-align: center;
}

View File

@ -0,0 +1,104 @@
///
/// Copyright © 2016-2022 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, Input, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import {
ComplexVersionCreateRequest,
createDefaultEntityTypesVersionCreate,
SyncStrategy, syncStrategyTranslationMap,
VersionCreateRequestType,
VersionCreationResult
} from '@shared/models/vc.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-complex-version-create',
templateUrl: './complex-version-create.component.html',
styleUrls: ['./complex-version-create.component.scss']
})
export class ComplexVersionCreateComponent extends PageComponent implements OnInit {
@Input()
branch: string;
@Input()
onClose: (result: VersionCreationResult | null, branch: string | null) => void;
@Input()
onContentUpdated: () => void;
createVersionFormGroup: FormGroup;
syncStrategies = Object.values(SyncStrategy);
syncStrategyTranslations = syncStrategyTranslationMap;
resultMessage: string;
versionCreateResult: VersionCreationResult = null;
versionCreateBranch: string = null;
constructor(protected store: Store<AppState>,
private entitiesVersionControlService: EntitiesVersionControlService,
private translate: TranslateService,
private fb: FormBuilder) {
super(store);
}
ngOnInit(): void {
this.createVersionFormGroup = this.fb.group({
branch: [this.branch, [Validators.required]],
versionName: [null, [Validators.required]],
syncStrategy: [SyncStrategy.MERGE, Validators.required],
entityTypes: [createDefaultEntityTypesVersionCreate(), []],
});
}
cancel(): void {
if (this.onClose) {
this.onClose(this.versionCreateResult, this.versionCreateBranch);
}
}
export(): void {
const request: ComplexVersionCreateRequest = {
branch: this.createVersionFormGroup.get('branch').value,
versionName: this.createVersionFormGroup.get('versionName').value,
syncStrategy: this.createVersionFormGroup.get('syncStrategy').value,
entityTypes: this.createVersionFormGroup.get('entityTypes').value,
type: VersionCreateRequestType.COMPLEX
};
this.entitiesVersionControlService.saveEntitiesVersion(request).subscribe((result) => {
if (!result.added && !result.modified && !result.removed) {
this.resultMessage = this.translate.instant('version-control.nothing-to-commit');
} else {
this.resultMessage = this.translate.instant('version-control.version-create-result',
{added: result.added, modified: result.modified, removed: result.removed});
}
this.versionCreateResult = result;
this.versionCreateBranch = request.branch;
if (this.onContentUpdated) {
this.onContentUpdated();
}
});
}
}

View File

@ -0,0 +1,111 @@
<!--
Copyright © 2016-2022 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 class="entity-types-version-create" [formGroup]="entityTypesVersionCreateFormGroup" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>version-control.entity-types</legend>
<div fxLayout="column">
<div class="tb-control-list">
<div *ngFor="let entityTypeFormGroup of entityTypesFormGroupArray(); trackBy: trackByEntityType;
let $index = index; last as isLast;"
fxLayout="row" fxLayoutAlign="start center" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<mat-expansion-panel class="entity-type-config" fxFlex [formGroup]="entityTypeFormGroup" [expanded]="entityTypesFormGroupExpanded(entityTypeFormGroup)">
<mat-expansion-panel-header>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<mat-panel-title>
<div fxLayout="row" fxFlex fxLayoutAlign="start center">
<div>{{ entityTypeText(entityTypeFormGroup) }}</div>
</div>
</mat-panel-title>
<span fxFlex></span>
<button *ngIf="!disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeEntityType($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="entity-type-config-content" fxLayout="column" fxLayoutGap="0.5em">
<mat-divider></mat-divider>
<div fxLayout="row" fxLayoutGap="16px">
<tb-entity-type-select
showLabel
formControlName="entityType"
required
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)">
</tb-entity-type-select>
<div fxFlex fxLayout="row" fxLayoutGap="16px" fxLayoutAlign="start center" formGroupName="config">
<mat-form-field class="mat-block">
<mat-label translate>version-control.sync-strategy</mat-label>
<mat-select formControlName="syncStrategy">
<mat-option [value]="'default'">
{{ 'version-control.default' | translate }}
</mat-option>
<mat-option *ngFor="let strategy of syncStrategies" [value]="strategy">
{{syncStrategyTranslations.get(strategy) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox formControlName="saveRelations">
{{ 'version-control.export-entity-relations' | translate }}
</mat-checkbox>
</div>
</div>
<div fxLayout="row" fxLayoutGap="16px" fxLayoutAlign="start center"
formGroupName="config" style="height: 60px;">
<mat-slide-toggle formControlName="allEntities">
{{ 'version-control.all-entities' | translate }}
</mat-slide-toggle>
<tb-entity-list
fxFlex
[fxShow]="!entityTypeFormGroup.get('config').get('allEntities').value"
[entityType]="entityTypeFormGroup.get('entityType').value"
required
formControlName="entityIds">
</tb-entity-list>
</div>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</div>
<div *ngIf="!entityTypesFormGroupArray().length">
<span translate fxLayoutAlign="center center"
class="tb-prompt">version-control.no-entity-types</span>
</div>
<div style="padding-top: 16px;" fxLayout="row">
<button mat-raised-button color="primary"
type="button"
[disabled]="!addEnabled()"
(click)="addEntityType()">
<span translate>version-control.add-entity-type</span>
</button>
<span fxFlex></span>
<button mat-raised-button color="primary"
type="button"
[disabled]="!entityTypesFormGroupArray().length"
(click)="removeAll()">
<span translate>version-control.remove-all</span>
</button>
</div>
</div>
</fieldset>
</section>

View File

@ -0,0 +1,64 @@
/**
* Copyright © 2016-2022 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 {
.entity-types-version-create {
.fields-group {
padding: 0 16px 8px;
margin-bottom: 10px;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
legend {
color: rgba(0, 0, 0, .7);
width: fit-content;
}
legend + * {
display: block;
margin-top: 16px;
}
}
.tb-control-list {
overflow-y: auto;
max-height: 600px;
}
.tb-prompt {
margin: 30px 0;
}
mat-expansion-panel.entity-type-config {
box-shadow: none;
border: 1px groove rgba(0, 0, 0, .25);
.mat-expansion-panel-header {
padding: 0 24px 0 8px;
height: 48px;
}
.entity-type-config-content {
padding: 0 8px 8px;
}
}
}
}
:host ::ng-deep {
.mat-expansion-panel.entity-type-config {
.mat-expansion-panel-body {
padding: 0;
}
}
}

View File

@ -0,0 +1,247 @@
///
/// Copyright © 2016-2022 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, forwardRef, Input, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormArray,
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator,
Validators
} from '@angular/forms';
import { PageComponent } from '@shared/components/page.component';
import {
EntityTypeVersionCreateConfig,
exportableEntityTypes,
SyncStrategy,
syncStrategyTranslationMap
} from '@shared/models/vc.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-entity-types-version-create',
templateUrl: './entity-types-version-create.component.html',
styleUrls: ['./entity-types-version-create.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EntityTypesVersionCreateComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => EntityTypesVersionCreateComponent),
multi: true
}
]
})
export class EntityTypesVersionCreateComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator {
@Input()
disabled: boolean;
private modelValue: {[entityType: string]: EntityTypeVersionCreateConfig};
private propagateChange = null;
public entityTypesVersionCreateFormGroup: FormGroup;
syncStrategies = Object.values(SyncStrategy);
syncStrategyTranslations = syncStrategyTranslationMap;
constructor(protected store: Store<AppState>,
private translate: TranslateService,
private fb: FormBuilder) {
super(store);
}
ngOnInit(): void {
this.entityTypesVersionCreateFormGroup = this.fb.group({
entityTypes: [this.fb.array([]), [Validators.min(1)]]
});
this.entityTypesVersionCreateFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.entityTypesVersionCreateFormGroup.disable({emitEvent: false});
} else {
this.entityTypesVersionCreateFormGroup.enable({emitEvent: false});
}
}
writeValue(value: {[entityType: string]: EntityTypeVersionCreateConfig} | undefined): void {
this.modelValue = value;
this.entityTypesVersionCreateFormGroup.setControl('entityTypes',
this.prepareEntityTypesFormArray(value), {emitEvent: false});
}
public validate(c: FormControl) {
return this.entityTypesVersionCreateFormGroup.valid && this.entityTypesFormGroupArray().length ? null : {
entityTypes: {
valid: false,
},
};
}
private prepareEntityTypesFormArray(entityTypes: {[entityType: string]: EntityTypeVersionCreateConfig} | undefined): FormArray {
const entityTypesControls: Array<AbstractControl> = [];
if (entityTypes) {
for (const entityType of Object.keys(entityTypes)) {
const config = entityTypes[entityType];
entityTypesControls.push(this.createEntityTypeControl(entityType as EntityType, config));
}
}
return this.fb.array(entityTypesControls);
}
private createEntityTypeControl(entityType: EntityType, config: EntityTypeVersionCreateConfig): AbstractControl {
const entityTypeControl = this.fb.group(
{
entityType: [entityType, [Validators.required]],
config: this.fb.group({
syncStrategy: [config.syncStrategy === null ? 'default' : config.syncStrategy, []],
saveRelations: [config.saveRelations, []],
allEntities: [config.allEntities, []],
entityIds: [config.entityIds, [Validators.required]]
})
}
);
this.updateEntityTypeValidators(entityTypeControl);
entityTypeControl.get('config').get('allEntities').valueChanges.subscribe(() => {
this.updateEntityTypeValidators(entityTypeControl);
});
return entityTypeControl;
}
private updateEntityTypeValidators(entityTypeControl: AbstractControl): void {
const allEntities: boolean = entityTypeControl.get('config').get('allEntities').value;
if (allEntities) {
entityTypeControl.get('config').get('entityIds').disable({emitEvent: false});
} else {
entityTypeControl.get('config').get('entityIds').enable({emitEvent: false});
}
entityTypeControl.get('config').get('entityIds').updateValueAndValidity({emitEvent: false});
}
entityTypesFormGroupArray(): FormGroup[] {
return (this.entityTypesVersionCreateFormGroup.get('entityTypes') as FormArray).controls as FormGroup[];
}
entityTypesFormGroupExpanded(entityTypeControl: AbstractControl): boolean {
return !!(entityTypeControl as any).expanded;
}
public trackByEntityType(index: number, entityTypeControl: AbstractControl): any {
return entityTypeControl;
}
public removeEntityType(index: number) {
(this.entityTypesVersionCreateFormGroup.get('entityTypes') as FormArray).removeAt(index);
}
public addEnabled(): boolean {
const entityTypesArray = this.entityTypesVersionCreateFormGroup.get('entityTypes') as FormArray;
return entityTypesArray.length < exportableEntityTypes.length;
}
public addEntityType() {
const entityTypesArray = this.entityTypesVersionCreateFormGroup.get('entityTypes') as FormArray;
const config: EntityTypeVersionCreateConfig = {
syncStrategy: null,
saveRelations: false,
allEntities: true,
entityIds: []
};
const allowed = this.allowedEntityTypes();
let entityType: EntityType = null;
if (allowed.length) {
entityType = allowed[0];
}
const entityTypeControl = this.createEntityTypeControl(entityType, config);
(entityTypeControl as any).expanded = true;
entityTypesArray.push(entityTypeControl);
this.entityTypesVersionCreateFormGroup.updateValueAndValidity();
}
public removeAll() {
const entityTypesArray = this.entityTypesVersionCreateFormGroup.get('entityTypes') as FormArray;
entityTypesArray.clear();
this.entityTypesVersionCreateFormGroup.updateValueAndValidity();
}
entityTypeText(entityTypeControl: AbstractControl): string {
const entityType: EntityType = entityTypeControl.get('entityType').value;
const config: EntityTypeVersionCreateConfig = entityTypeControl.get('config').value;
let count = config?.entityIds?.length;
if (!isDefinedAndNotNull(count)) {
count = 0;
}
if (entityType) {
return this.translate.instant((config?.allEntities ? entityTypeTranslations.get(entityType).typePlural
: entityTypeTranslations.get(entityType).list), { count });
} else {
return 'Undefined';
}
}
allowedEntityTypes(entityTypeControl?: AbstractControl): Array<EntityType> {
let res = [...exportableEntityTypes];
const currentEntityType: EntityType = entityTypeControl?.get('entityType')?.value;
const value: [{entityType: string, config: EntityTypeVersionCreateConfig}] =
this.entityTypesVersionCreateFormGroup.get('entityTypes').value || [];
const usedEntityTypes = value.map(val => val.entityType).filter(val => val);
res = res.filter(entityType => !usedEntityTypes.includes(entityType) || entityType === currentEntityType);
return res;
}
private updateModel() {
const value: [{entityType: string, config: EntityTypeVersionCreateConfig}] =
this.entityTypesVersionCreateFormGroup.get('entityTypes').value || [];
let modelValue: {[entityType: string]: EntityTypeVersionCreateConfig} = null;
if (value && value.length) {
modelValue = {};
value.forEach((val) => {
modelValue[val.entityType] = val.config;
if ((modelValue[val.entityType].syncStrategy as any) === 'default') {
modelValue[val.entityType].syncStrategy = null;
}
});
}
this.modelValue = modelValue;
this.propagateChange(this.modelValue);
}
}

View File

@ -24,7 +24,7 @@
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="isLoading$ | async">
</mat-progress-bar>
<form [formGroup]="exportFormGroup" style="padding-top: 16px;">
<form [formGroup]="createVersionFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<tb-branch-autocomplete
@ -34,7 +34,7 @@
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>version-control.version-name</mat-label>
<input required matInput formControlName="versionName">
<mat-error *ngIf="exportFormGroup.get('versionName').hasError('required')">
<mat-error *ngIf="createVersionFormGroup.get('versionName').hasError('required')">
{{ 'version-control.version-name-required' | translate }}
</mat-error>
</mat-form-field>
@ -54,13 +54,13 @@
<button mat-raised-button color="primary"
type="button"
(click)="export()"
[disabled]="(isLoading$ | async) || exportFormGroup.invalid || !exportFormGroup.dirty">
[disabled]="(isLoading$ | async) || createVersionFormGroup.invalid || !createVersionFormGroup.dirty">
{{ 'action.create' | translate }}
</button>
</div>
</section>
<section *ngIf="resultMessage">
<div class="mat-title export-result-message">{{ resultMessage }}</div>
<div class="mat-title create-result-message">{{ resultMessage }}</div>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"

View File

@ -0,0 +1,21 @@
/**
* Copyright © 2016-2022 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 {
.create-result-message {
padding: 48px 8px 8px;
text-align: center;
}
}

View File

@ -29,11 +29,11 @@ import { EntityId } from '@shared/models/id/entity-id';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-entity-version-export',
templateUrl: './entity-version-export.component.html',
styleUrls: ['./entity-version-export.component.scss']
selector: 'tb-entity-version-create',
templateUrl: './entity-version-create.component.html',
styleUrls: ['./entity-version-create.component.scss']
})
export class EntityVersionExportComponent extends PageComponent implements OnInit {
export class EntityVersionCreateComponent extends PageComponent implements OnInit {
@Input()
branch: string;
@ -47,7 +47,7 @@ export class EntityVersionExportComponent extends PageComponent implements OnIni
@Input()
onContentUpdated: () => void;
exportFormGroup: FormGroup;
createVersionFormGroup: FormGroup;
resultMessage: string;
@ -59,7 +59,7 @@ export class EntityVersionExportComponent extends PageComponent implements OnIni
}
ngOnInit(): void {
this.exportFormGroup = this.fb.group({
this.createVersionFormGroup = this.fb.group({
branch: [this.branch, [Validators.required]],
versionName: [null, [Validators.required]],
saveRelations: [false, []]
@ -75,10 +75,10 @@ export class EntityVersionExportComponent extends PageComponent implements OnIni
export(): void {
const request: SingleEntityVersionCreateRequest = {
entityId: this.entityId,
branch: this.exportFormGroup.get('branch').value,
versionName: this.exportFormGroup.get('versionName').value,
branch: this.createVersionFormGroup.get('branch').value,
versionName: this.createVersionFormGroup.get('versionName').value,
config: {
saveRelations: this.exportFormGroup.get('saveRelations').value
saveRelations: this.createVersionFormGroup.get('saveRelations').value
},
type: VersionCreateRequestType.SINGLE_ENTITY
};

View File

@ -32,12 +32,19 @@
</div>
<span fxFlex></span>
<button *ngIf="singleEntityMode" mat-stroked-button color="primary"
#exportButton
#createVersionButton
[disabled]="(isLoading$ | async)"
(click)="toggleVcExport($event, exportButton)">
(click)="toggleCreateVersion($event, createVersionButton)">
<mat-icon>update</mat-icon>
{{'version-control.create-version' | translate }}
</button>
<button *ngIf="!singleEntityMode" mat-stroked-button color="primary"
#complexCreateVersionButton
[disabled]="(isLoading$ | async)"
(click)="toggleComplexCreateVersion($event, complexCreateVersionButton)">
<mat-icon>update</mat-icon>
{{'version-control.create-entities-version' | translate }}
</button>
<button mat-icon-button
[disabled]="isLoading$ | async"
(click)="enterFilterMode()"

View File

@ -43,11 +43,11 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { BranchAutocompleteComponent } from '@shared/components/vc/branch-autocomplete.component';
import { isNotEmptyStr } from '@core/utils';
import { TbPopoverService } from '@shared/components/popover.service';
import { EntityVersionExportComponent } from '@home/components/vc/entity-version-export.component';
import { EntityVersionCreateComponent } from '@home/components/vc/entity-version-create.component';
import { MatButton } from '@angular/material/button';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { EntityVersionRestoreComponent } from '@home/components/vc/entity-version-restore.component';
import { EntityVersionDiffComponent } from '@home/components/vc/entity-version-diff.component';
import { ComplexVersionCreateComponent } from '@home/components/vc/complex-version-create.component';
@Component({
selector: 'tb-entity-versions-table',
@ -177,21 +177,21 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
}
}
toggleVcExport($event: Event, exportButton: MatButton) {
toggleCreateVersion($event: Event, createVersionButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = exportButton._elementRef.nativeElement;
const trigger = createVersionButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const vcExportPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, EntityVersionExportComponent, 'left', true, null,
const createVersionPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, EntityVersionCreateComponent, 'left', true, null,
{
branch: this.branch,
entityId: this.entityId,
onClose: (result: VersionCreationResult | null, branch: string | null) => {
vcExportPopover.hide();
createVersionPopover.hide();
if (result) {
if (this.branch !== branch) {
this.branchChanged(branch);
@ -201,9 +201,41 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
}
},
onContentUpdated: () => {
vcExportPopover.updatePosition();
createVersionPopover.updatePosition();
setTimeout(() => {
vcExportPopover.updatePosition();
createVersionPopover.updatePosition();
});
}
}, {}, {}, {}, false);
}
}
toggleComplexCreateVersion($event: Event, complexCreateVersionButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = complexCreateVersionButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const complexCreateVersionPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ComplexVersionCreateComponent, 'leftTop', true, null,
{
branch: this.branch,
onClose: (result: VersionCreationResult | null, branch: string | null) => {
complexCreateVersionPopover.hide();
if (result) {
if (this.branch !== branch) {
this.branchChanged(branch);
} else {
this.updateData();
}
}
},
onContentUpdated: () => {
complexCreateVersionPopover.updatePosition();
setTimeout(() => {
complexCreateVersionPopover.updatePosition();
});
}
}, {}, {}, {}, false);

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { AfterViewInit, Component, forwardRef, Input, OnInit } from '@angular/core';
import { AfterViewInit, Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
@ -33,7 +33,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
multi: true
}]
})
export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit {
export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges {
entityTypeFormGroup: FormGroup;
@ -45,6 +45,9 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit,
@Input()
useAliasEntityTypes: boolean;
@Input()
filterAllowedEntityTypes = true;
private showLabelValue: boolean;
get showLabel(): boolean {
return this.showLabelValue;
@ -87,7 +90,8 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit,
}
ngOnInit() {
this.entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, this.useAliasEntityTypes);
this.entityTypes = this.filterAllowedEntityTypes ?
this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, this.useAliasEntityTypes) : this.allowedEntityTypes;
this.entityTypeFormGroup.get('entityType').valueChanges.subscribe(
(value) => {
let modelValue;
@ -101,6 +105,22 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit,
);
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'allowedEntityTypes') {
this.entityTypes = this.filterAllowedEntityTypes ?
this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, this.useAliasEntityTypes) : this.allowedEntityTypes;
const currentEntityType: EntityType | AliasEntityType = this.entityTypeFormGroup.get('entityType').value;
if (currentEntityType && !this.entityTypes.includes(currentEntityType)) {
this.entityTypeFormGroup.get('entityType').patchValue(null, {emitEvent: true});
}
}
}
}
}
ngAfterViewInit(): void {
}

View File

@ -21,6 +21,15 @@ import { EntityRelation } from '@shared/models/relation.models';
import { Device, DeviceCredentials } from '@shared/models/device.models';
import { RuleChain, RuleChainMetaData } from '@shared/models/rule-chain.models';
export const exportableEntityTypes: Array<EntityType> = [
EntityType.ASSET,
EntityType.DEVICE,
EntityType.DASHBOARD,
EntityType.CUSTOMER,
EntityType.DEVICE_PROFILE,
EntityType.RULE_CHAIN
];
export interface VersionCreateConfig {
saveRelations: boolean;
}
@ -51,6 +60,43 @@ export interface SingleEntityVersionCreateRequest extends VersionCreateRequest {
type: VersionCreateRequestType.SINGLE_ENTITY;
}
export enum SyncStrategy {
MERGE = 'MERGE',
OVERWRITE = 'OVERWRITE'
}
export const syncStrategyTranslationMap = new Map<SyncStrategy, string>(
[
[SyncStrategy.MERGE, 'version-control.sync-strategy-merge'],
[SyncStrategy.OVERWRITE, 'version-control.sync-strategy-overwrite']
]
);
export interface EntityTypeVersionCreateConfig extends VersionCreateConfig {
syncStrategy: SyncStrategy;
entityIds: string[];
allEntities: boolean;
}
export interface ComplexVersionCreateRequest extends VersionCreateRequest {
syncStrategy: SyncStrategy;
entityTypes: {[entityType: string]: EntityTypeVersionCreateConfig};
type: VersionCreateRequestType.COMPLEX;
}
export function createDefaultEntityTypesVersionCreate(): {[entityType: string]: EntityTypeVersionCreateConfig} {
const res: {[entityType: string]: EntityTypeVersionCreateConfig} = {};
for (const entityType of exportableEntityTypes) {
res[entityType] = {
syncStrategy: null,
saveRelations: false,
allEntities: true,
entityIds: []
};
}
return res;
}
export interface VersionLoadRequest {
branch: string;
versionId: string;
@ -63,6 +109,7 @@ export interface SingleEntityVersionLoadRequest extends VersionLoadRequest {
type: VersionLoadRequestType.SINGLE_ENTITY;
}
export interface BranchInfo {
name: string;
default: boolean;

View File

@ -3137,7 +3137,19 @@
"previous-difference": "Previous Difference",
"next-difference": "Next Difference",
"current": "Current",
"differences": "{ count, plural, 1 {1 difference} other {# differences} }"
"differences": "{ count, plural, 1 {1 difference} other {# differences} }",
"create-entities-version": "Create entities version",
"default-sync-strategy": "Default sync strategy",
"sync-strategy-merge": "Merge",
"sync-strategy-overwrite": "Overwrite",
"entity-types": "Entity types",
"sync-strategy": "Sync strategy",
"default": "Default",
"all-entities": "All entities",
"no-entity-types": "No entity types configured",
"add-entity-type": "Add entity type",
"remove-all": "Remove all",
"version-create-result": "{ added, plural, 0 {No entities} 1 {1 entity} other {# entities} } added.\n{ modified, plural, 0 {No entities} 1 {1 entity} other {# entities} } modified.\n{ removed, plural, 0 {No entities} 1 {1 entity} other {# entities} } removed."
},
"widget": {
"widget-library": "Widgets Library",