add annotation status on annotation dialog

This commit is contained in:
amentis 2024-07-02 16:03:27 +03:00
parent 0e96ced54c
commit 9ec2ba3081
12 changed files with 239 additions and 4 deletions

View File

@ -102,6 +102,20 @@
<button mat-button (click)="enableReply(thread)" class="action-button" [ngClass]="this.replyEnabledPerThread[thread] ? 'active-action' : ''" [disabled]="this.replyEnabledPerThread[thread]">{{ 'ANNOTATION-DIALOG.THREADS.REPLY' | translate}}</button>
</div>
</div>
<div class="col-5">
<mat-form-field class="w-100" *ngIf="listingStatuses.length > 0">
<mat-label>{{'ANNOTATION-DIALOG.ANNOTATION-STATUS.TITLE' | translate}}</mat-label>
<mat-select [formControl]="getAnnotationStatusFormControl(annotationStatusFormGroup.get('annotationsStatusArray'), getParentAnnotation(thread).id).get('statusId')" (selectionChange)="changeIcon(annotationStatusFormGroup.get('annotationsStatusArray'), getParentAnnotation(thread).id)">
<mat-option *ngFor="let status of listingStatuses" [value]="status.id">{{status.label}}</mat-option>
</mat-select>
<mat-error *ngIf="getAnnotationStatusFormControl(annotationStatusFormGroup.get('annotationsStatusArray'), getParentAnnotation(thread).id).get('statusId').hasError('backendError')">{{getAnnotationStatusFormControl(annotationStatusFormGroup.get('annotationsStatusArray'), getParentAnnotation(thread).id).get('statusId').getError('backendError').message}}</mat-error>
<mat-error *ngIf="getAnnotationStatusFormControl(annotationStatusFormGroup.get('annotationsStatusArray'), getParentAnnotation(thread).id).get('statusId').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
<button mat-icon-button color="primary" type="submit" [disabled]="!canSaveAnnotationStatus(annotationStatusFormGroup.get('annotationsStatusArray'), getParentAnnotation(thread).id)" (click)="persistAnnotationStatus(annotationStatusFormGroup.get('annotationsStatusArray'), getParentAnnotation(thread).id)">
<mat-icon *ngIf="getAnnotationStatusFormControl(annotationStatusFormGroup.get('annotationsStatusArray'), getParentAnnotation(thread).id).get('isCheckIcon').value == false" class="mat-24" >save</mat-icon>
<mat-icon *ngIf="getAnnotationStatusFormControl(annotationStatusFormGroup.get('annotationsStatusArray'), getParentAnnotation(thread).id).get('isCheckIcon').value == true" class="mat-24" >check</mat-icon>
</button>
</div>
<div class="row">
<div class="col-12">
<ng-container *ngIf="annotationsPerThread[thread].length === 1 && !this.showRepliesPerThread[thread]">

View File

@ -1,9 +1,12 @@
import { Component, Inject } from '@angular/core';
import { FormBuilder, FormControl, UntypedFormGroup, Validators } from '@angular/forms';
import { FormBuilder, FormControl, UntypedFormArray, UntypedFormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { AnnotationStatus, AnnotationStatusPersist } from '@annotation-service/core/model/annotation-status.model';
import { Annotation, AnnotationPersist } from '@annotation-service/core/model/annotation.model';
import { Status } from '@annotation-service/core/model/status.model';
import { AnnotationLookup } from '@annotation-service/core/query/annotation.lookup';
import { StatusLookup } from '@annotation-service/core/query/status.lookup';
import { AnnotationService } from '@annotation-service/services/http/annotation.service';
import { AnnotationProtectionType } from '@app/core/common/enum/annotation-protection-type.enum';
import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service';
@ -14,8 +17,12 @@ import { BaseComponent } from '@common/base/base.component';
import { FormService } from '@common/forms/form-service';
import { Guid } from '@common/types/guid';
import { TranslateService } from '@ngx-translate/core';
import { IsActive } from '@notification-service/core/enum/is-active.enum';
import { takeUntil } from 'rxjs/operators';
import { nameof } from 'ts-simple-nameof';
import { AnnotationStatusArrayEditorModel } from './annotation-status-editor.model';
import { StatusService } from '@annotation-service/services/http/status.service';
import { InternalStatus } from '@annotation-service/core/enum/internal-status.enum';
@Component({
selector: 'app-annotation-dialog',
@ -43,6 +50,9 @@ export class AnnotationDialogComponent extends BaseComponent {
threadReplyTextsFG: Array<UntypedFormGroup>;
threadFormGroup: UntypedFormGroup;
private formBuilder: FormBuilder = new FormBuilder();
public annotationStatusFormGroup: UntypedFormGroup;
public listingStatuses: Status[] = [];
public dmpUsersMentionNames: string[] = [];
constructor(
@ -55,12 +65,14 @@ export class AnnotationDialogComponent extends BaseComponent {
private annotationService: AnnotationService,
private formService: FormService,
public enumUtils: EnumUtils,
private statusService: StatusService,
protected routerUtils: RouterUtilsService,
) {
super();
this.entityId = data.entityId;
this.anchor = data.anchor;
this.entityType = data.entityType;
this.dmpUsersMentionNames = data.dmpUsers.map(x => x.user.name);
dialogRef.backdropClick().pipe(takeUntil(this._destroyed)).subscribe(() => dialogRef.close(this.changesMade));
}
@ -72,6 +84,7 @@ export class AnnotationDialogComponent extends BaseComponent {
if (this.entityId != null) {
this.loadThreads();
}
this.getStatuses();
}
createThread() {
@ -139,6 +152,13 @@ export class AnnotationDialogComponent extends BaseComponent {
nameof<Annotation>(x => x.author.name),
nameof<Annotation>(x => x.payload),
nameof<Annotation>(x => x.protectionType),
[nameof<Annotation>(x => x.annotationStatuses), nameof<AnnotationStatus>(x => x.id)].join('.'),
[nameof<Annotation>(x => x.annotationStatuses), nameof<AnnotationStatus>(x => x.annotation), nameof<Annotation>(x => x.id)].join('.'),
[nameof<Annotation>(x => x.annotationStatuses), nameof<AnnotationStatus>(x => x.status), nameof<Status>(x => x.id)].join('.'),
[nameof<Annotation>(x => x.annotationStatuses), nameof<AnnotationStatus>(x => x.createdAt)].join('.'),
[nameof<Annotation>(x => x.annotationStatuses), nameof<AnnotationStatus>(x => x.updatedAt)].join('.'),
[nameof<Annotation>(x => x.annotationStatuses), nameof<AnnotationStatus>(x => x.isActive)].join('.'),
[nameof<Annotation>(x => x.annotationStatuses), nameof<AnnotationStatus>(x => x.hash)].join('.'),
]
};
@ -160,6 +180,8 @@ export class AnnotationDialogComponent extends BaseComponent {
this.parentAnnotationsPerThread[element.threadId.toString()] = data.items.filter(x => x.threadId === element.threadId && x.id === element.id)[0];
this.threads.add(element.threadId);
});
// create annotation status array to handle each annotation
this.annotationStatusFormGroup = new AnnotationStatusArrayEditorModel().fromModel(this.comments, this.listingStatuses).buildForm();
},
error => this.onCallbackError(error),
);
@ -236,4 +258,76 @@ export class AnnotationDialogComponent extends BaseComponent {
SnackBarNotificationLevel.Success
);
}
// status
private getStatuses(){
const lookup: StatusLookup = new StatusLookup();
lookup.metadata = { countAll: true };
lookup.page = { size: 100, offset: 0 };
lookup.isActive = [IsActive.Active];
lookup.project = {
fields: [
nameof<Status>(x => x.id),
nameof<Status>(x => x.label),
nameof<Status>(x => x.internalStatus)
]
};
lookup.order = { items: [nameof<Status>(x => x.label)] };
return this.statusService.query(lookup).pipe(takeUntil(this._destroyed))
.subscribe(result => {
if (result && result.items?.length > 0) this.listingStatuses.push(...result.items);
});
}
private getAnnotationStatusControl(formArray: UntypedFormArray, annotationId: Guid){
const index = formArray.controls.findIndex(x => x.get('annotationId')?.value == annotationId);
if (index < 0) return null;
return formArray.at(index);
}
getAnnotationStatusFormControl(formArray: UntypedFormArray, annotationId: Guid) {
const control = this.getAnnotationStatusControl(formArray, annotationId);
if (control == null) return;
return control;
}
persistAnnotationStatus(formArray: UntypedFormArray, annotationId: Guid){
const control = this.getAnnotationStatusControl(formArray, annotationId);
if (control && control.valid){
const formData = this.formService.getValue(control.value) as AnnotationStatusPersist;
this.annotationService.persistStatus(formData)
.pipe(takeUntil(this._destroyed)).subscribe(
complete => this.onCallbackAnnotationStatusSuccess(),
error => this.onCallbackError(error)
);
}
}
changeIcon(formArray: UntypedFormArray, annotationId: Guid){
const control = this.getAnnotationStatusControl(formArray, annotationId);
if (control == null) return;
const status = this.listingStatuses.find(x => x.id === control.get('statusId').value) || null;
if (status && status.internalStatus != null && status.internalStatus == InternalStatus.Resolved){
control.get('isCheckIcon').patchValue(true);
} else {
control.get('isCheckIcon').patchValue(false);
}
}
canSaveAnnotationStatus(formArray: UntypedFormArray, annotationId: Guid): boolean{
const control = this.getAnnotationStatusControl(formArray, annotationId);
if (control == null) return false;
return control.valid;
}
private onCallbackAnnotationStatusSuccess() {
this.uiNotificationService.snackBarNotification(this.language.instant('ANNOTATION-DIALOG.ANNOTATION-STATUS.SUCCESS'), SnackBarNotificationLevel.Success);
this.refreshAnnotations();
}
}

View File

@ -3,12 +3,14 @@ import { FormattingModule } from '@app/core/formatting.module';
import { CommonFormsModule } from '@common/forms/common-forms.module';
import { CommonUiModule } from '@common/ui/common-ui.module';
import { AnnotationDialogComponent } from './annotation-dialog.component';
import { AutoCompleteModule } from '@app/library/auto-complete/auto-complete.module';
@NgModule({
imports: [
CommonUiModule,
CommonFormsModule,
FormattingModule,
AutoCompleteModule,
],
declarations: [
AnnotationDialogComponent,

View File

@ -0,0 +1,109 @@
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { InternalStatus } from '@annotation-service/core/enum/internal-status.enum';
import { AnnotationStatus, AnnotationStatusPersist } from '@annotation-service/core/model/annotation-status.model';
import { Annotation } from '@annotation-service/core/model/annotation.model';
import { Status } from '@annotation-service/core/model/status.model';
import { IsActive } from '@app/core/common/enum/is-active.enum';
import { BaseEditorModel } from '@common/base/base-form-editor-model';
import { BackendErrorValidator } from '@common/forms/validation/custom-validator';
import { ValidationErrorModel } from '@common/forms/validation/error-model/validation-error-model';
import { Validation, ValidationContext } from '@common/forms/validation/validation-context';
import { Guid } from '@common/types/guid';
export class AnnotationStatusArrayEditorModel {
annotationsStatusArray: AnnotationStatusEditorModel [] = [];
public validationErrorModel: ValidationErrorModel = new ValidationErrorModel();
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
constructor() { }
public fromModel(item: Annotation[], listingStatuses: Status[]): AnnotationStatusArrayEditorModel {
if (item) {
item.forEach(annotation => {
if (annotation.annotationStatuses){
annotation.annotationStatuses = annotation.annotationStatuses.filter(x => x.isActive === IsActive.Active);
annotation.annotationStatuses.forEach( x => {
this.annotationsStatusArray.push(new AnnotationStatusEditorModel(this.validationErrorModel).fromModel(x, listingStatuses))
})
} else {
const tempAnnotationStatus = {
annotation: annotation,
status: null
} as AnnotationStatus;
this.annotationsStatusArray.push(new AnnotationStatusEditorModel(this.validationErrorModel).fromModel(tempAnnotationStatus, listingStatuses))
}
})
}
return this;
}
buildForm(context: ValidationContext = null, disabled: boolean = false): UntypedFormGroup {
if (context == null) { context = this.createValidationContext(); }
return this.formBuilder.group({
annotationsStatusArray: this.formBuilder.array(
(this.annotationsStatusArray ?? []).map(
(item, index) => item.buildForm()
), context.getValidation('annotationsStatusArray').validators
),
});
}
createValidationContext(): ValidationContext {
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'annotationsStatusArray', validators: [BackendErrorValidator(this.validationErrorModel, 'annotationsStatusArray')] });
baseContext.validation = baseValidationArray;
return baseContext;
}
}
export class AnnotationStatusEditorModel implements AnnotationStatusPersist {
annotationId: Guid;
statusId: Guid;
isCheckIcon: boolean = false;
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
constructor(public validationErrorModel: ValidationErrorModel = new ValidationErrorModel()) {}
public fromModel(item: AnnotationStatus, listingStatuses: Status[]): AnnotationStatusEditorModel {
if (item) {
this.annotationId = item.annotation?.id;
this.statusId = item.status?.id;
if (this.statusId) {
const status = listingStatuses.find(x => x.id === this.statusId) || null;
if (status && status.internalStatus != null && status.internalStatus == InternalStatus.Resolved){
this.isCheckIcon = true;
} else {
this.isCheckIcon = false;
}
}
}
return this;
}
buildForm(context: ValidationContext = null, disabled: boolean = false): UntypedFormGroup {
if (context == null) { context = this.createValidationContext(); }
return this.formBuilder.group({
annotationId: [{ value: this.annotationId, disabled: disabled }, context.getValidation('annotationId').validators],
statusId: [{ value: this.statusId, disabled: disabled }, context.getValidation('statusId').validators],
isCheckIcon: [{ value: this.isCheckIcon, disabled: disabled }, context.getValidation('isCheckIcon').validators],
});
}
createValidationContext(): ValidationContext {
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'annotationId', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'label')] });
baseValidationArray.push({ key: 'statusId', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'internalStatus')] });
baseValidationArray.push({ key: 'isCheckIcon', validators: [] });
baseContext.validation = baseValidationArray;
return baseContext;
}
}

View File

@ -178,6 +178,7 @@
[validationErrorModel]="editorModel.validationErrorModel"
[isNew]="isNew || isCopy"
[canReview]="canReview"
[dmpUsers]="item?.dmp?.dmpUsers ?? []"
></app-description-form>
</div>
</div>

View File

@ -210,6 +210,7 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
prepareForm(data: Description) {
try {
this.editorModel = data ? new DescriptionEditorModel().fromModel(data, data.descriptionTemplate) : new DescriptionEditorModel();
if (data && data?.dmp?.dmpUsers) data.dmp.dmpUsers = data.dmp.dmpUsers.filter(x => x.isActive === IsActive.Active);
this.item = data;
this.initialTemplateId = data?.descriptionTemplate?.id?.toString();
if (data && data.dmpDescriptionTemplate?.sectionId && data.dmp?.blueprint?.definition?.sections?.length > 0 && data.dmp?.descriptions?.length > 0) {

View File

@ -20,6 +20,7 @@ import { DescriptionTemplateFieldType } from '@app/core/common/enum/description-
import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service';
import { RouterUtilsService } from '@app/core/services/router/router-utils.service';
import { TranslateService } from '@ngx-translate/core';
import { DmpUser } from '@app/core/model/dmp/dmp';
@Component({
selector: 'app-description-form-field-set',
@ -34,6 +35,7 @@ export class DescriptionFormFieldSetComponent extends BaseComponent {
@Input() hideAnnotations: boolean = false;
@Input() canReview: boolean = false;
@Input() numbering: string;
@Input() dmpUsers: DmpUser[] = [];
get isMultiplicityEnabled() {
return this.fieldSet.hasMultiplicity && this.fieldSet.multiplicity != null;
@ -183,7 +185,8 @@ export class DescriptionFormFieldSetComponent extends BaseComponent {
data: {
entityId: this.descriptionId,
anchor: fieldSetId,
entityType: AnnotationEntityType.Description
entityType: AnnotationEntityType.Description,
dmpUsers: this.dmpUsers
}
});
dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(changesMade => {

View File

@ -25,6 +25,7 @@
[isChild]="false"
[hideAnnotations]="isNew"
[canReview]="canReview"
[dmpUsers]="dmpUsers"
></app-description-form-field-set>
</div>
</div>

View File

@ -8,6 +8,7 @@ import { ToCEntryType } from '../../../table-of-contents/models/toc-entry-type.e
import { LinkToScroll } from '../../../table-of-contents/table-of-contents.component';
import { VisibilityRulesService } from '../../visibility-rules/visibility-rules.service';
import { Guid } from '@common/types/guid';
import { DmpUser } from '@app/core/model/dmp/dmp';
@Component({
@ -25,6 +26,7 @@ export class DescriptionFormSectionComponent extends BaseComponent implements On
@Input() visibilityRulesService: VisibilityRulesService;
@Input() path: string;
@Input() descriptionId: Guid;
@Input() dmpUsers: DmpUser[] = [];
// @Input() descriptionTemplateId: String;

View File

@ -15,7 +15,7 @@
</mat-expansion-panel-header>
<ng-container *ngFor="let section of page.sections; let i = index;">
<div class="row" *ngIf="visibilityRulesService.isVisibleMap[section.id]">
<app-description-form-section class="col-12" [section]="section" [canReview]="canReview" [path]="(z+1)+'.'+(i+1)" [pathName]="'pages.'+z+'.sections.'+i" [propertiesFormGroup]="propertiesFormGroup" [descriptionId]="descriptionId" [visibilityRulesService]="visibilityRulesService" (askedToScroll)="onAskedToScroll(expansionPanel, $event)" [linkToScroll]="linkToScroll" [validationErrorModel]="validationErrorModel" [isNew]="isNew"></app-description-form-section>
<app-description-form-section class="col-12" [section]="section" [canReview]="canReview" [path]="(z+1)+'.'+(i+1)" [pathName]="'pages.'+z+'.sections.'+i" [propertiesFormGroup]="propertiesFormGroup" [descriptionId]="descriptionId" [visibilityRulesService]="visibilityRulesService" (askedToScroll)="onAskedToScroll(expansionPanel, $event)" [linkToScroll]="linkToScroll" [validationErrorModel]="validationErrorModel" [isNew]="isNew" [dmpUsers]="dmpUsers"></app-description-form-section>
</div>
</ng-container>
</mat-expansion-panel>

View File

@ -8,6 +8,7 @@ import { Guid } from '@common/types/guid';
import { LinkToScroll } from '../table-of-contents/table-of-contents.component';
import { DescriptionFormAnnotationService } from './description-form-annotation.service';
import { VisibilityRulesService } from './visibility-rules/visibility-rules.service';
import { DmpUser } from '@app/core/model/dmp/dmp';
@Component({
selector: 'app-description-form',
@ -26,6 +27,7 @@ export class DescriptionFormComponent extends BaseComponent implements OnInit, O
@Input() datasetDescription: String;
@Input() linkToScroll: LinkToScroll;
@Input() validationErrorModel: ValidationErrorModel;
@Input() dmpUsers: DmpUser[] = [];
@Output() formChanged: EventEmitter<any> = new EventEmitter();

View File

@ -5,7 +5,7 @@ import { AppPermission } from '@app/core/common/enum/permission.enum';
import { DescriptionTemplate } from '@app/core/model/description-template/description-template';
import { Description, DescriptionExternalIdentifier, DescriptionField, DescriptionPropertyDefinition, DescriptionPropertyDefinitionFieldSet, DescriptionPropertyDefinitionFieldSetItem, DescriptionReference, DescriptionReferenceData, DescriptionTag } from '@app/core/model/description/description';
import { DescriptionTemplatesInSection, DmpBlueprint, DmpBlueprintDefinition, DmpBlueprintDefinitionSection } from '@app/core/model/dmp-blueprint/dmp-blueprint';
import { Dmp, DmpDescriptionTemplate } from '@app/core/model/dmp/dmp';
import { Dmp, DmpDescriptionTemplate, DmpUser } from '@app/core/model/dmp/dmp';
import { PrefillingSource } from '@app/core/model/prefilling-source/prefilling-source';
import { ReferenceType } from '@app/core/model/reference-type/reference-type';
import { Reference } from '@app/core/model/reference/reference';
@ -149,6 +149,12 @@ export class DescriptionEditorEntityResolver extends BaseEditorResolver {
(prefix ? prefix + '.' : '') + [nameof<Dmp>(x => x.descriptions), nameof<Description>(x => x.dmpDescriptionTemplate), nameof<DmpDescriptionTemplate>(x => x.descriptionTemplateGroupId)].join('.'),
(prefix ? prefix + '.' : '') + [nameof<Dmp>(x => x.descriptions), nameof<Description>(x => x.dmpDescriptionTemplate), nameof<DmpDescriptionTemplate>(x => x.sectionId)].join('.'),
(prefix ? prefix + '.' : '') + [nameof<Dmp>(x => x.descriptions), nameof<Description>(x => x.dmpDescriptionTemplate), nameof<DmpDescriptionTemplate>(x => x.isActive)].join('.'),
(prefix ? prefix + '.' : '') + [nameof<Dmp>(x => x.dmpUsers), nameof<DmpUser>(x => x.id)].join('.'),
(prefix ? prefix + '.' : '') + [nameof<Dmp>(x => x.dmpUsers), nameof<DmpUser>(x => x.sectionId)].join('.'),
(prefix ? prefix + '.' : '') + [nameof<Dmp>(x => x.dmpUsers), nameof<DmpUser>(x => x.user.id)].join('.'),
(prefix ? prefix + '.' : '') + [nameof<Dmp>(x => x.dmpUsers), nameof<DmpUser>(x => x.user.name)].join('.'),
(prefix ? prefix + '.' : '') + [nameof<Dmp>(x => x.dmpUsers), nameof<DmpUser>(x => x.role)].join('.'),
(prefix ? prefix + '.' : '') + [nameof<Dmp>(x => x.dmpUsers), nameof<DmpUser>(x => x.isActive)].join('.'),
]
}