diff --git a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html index 8923055af..f81bd6022 100644 --- a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html +++ b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.html @@ -102,6 +102,20 @@ +
+ + {{'ANNOTATION-DIALOG.ANNOTATION-STATUS.TITLE' | translate}} + + {{status.label}} + + {{getAnnotationStatusFormControl(annotationStatusFormGroup.get('annotationsStatusArray'), getParentAnnotation(thread).id).get('statusId').getError('backendError').message}} + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + + +
diff --git a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts index 80fb51011..967e691d7 100644 --- a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts +++ b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.component.ts @@ -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; 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(x => x.author.name), nameof(x => x.payload), nameof(x => x.protectionType), + [nameof(x => x.annotationStatuses), nameof(x => x.id)].join('.'), + [nameof(x => x.annotationStatuses), nameof(x => x.annotation), nameof(x => x.id)].join('.'), + [nameof(x => x.annotationStatuses), nameof(x => x.status), nameof(x => x.id)].join('.'), + [nameof(x => x.annotationStatuses), nameof(x => x.createdAt)].join('.'), + [nameof(x => x.annotationStatuses), nameof(x => x.updatedAt)].join('.'), + [nameof(x => x.annotationStatuses), nameof(x => x.isActive)].join('.'), + [nameof(x => x.annotationStatuses), nameof(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(x => x.id), + nameof(x => x.label), + nameof(x => x.internalStatus) + ] + }; + + lookup.order = { items: [nameof(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(); + } + + + } diff --git a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.module.ts b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.module.ts index 92b80ffb6..b2e256cde 100644 --- a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.module.ts +++ b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-dialog.module.ts @@ -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, diff --git a/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-status-editor.model.ts b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-status-editor.model.ts new file mode 100644 index 000000000..86ba12190 --- /dev/null +++ b/dmp-frontend/src/app/ui/annotations/annotation-dialog-component/annotation-status-editor.model.ts @@ -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(); + 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(); + 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; + } +} diff --git a/dmp-frontend/src/app/ui/description/editor/description-editor.component.html b/dmp-frontend/src/app/ui/description/editor/description-editor.component.html index a072f3dbd..4f05f9a68 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-editor.component.html +++ b/dmp-frontend/src/app/ui/description/editor/description-editor.component.html @@ -178,6 +178,7 @@ [validationErrorModel]="editorModel.validationErrorModel" [isNew]="isNew || isCopy" [canReview]="canReview" + [dmpUsers]="item?.dmp?.dmpUsers ?? []" >
diff --git a/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts b/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts index 4fc6c4da9..f0acdfcb8 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts +++ b/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts @@ -210,6 +210,7 @@ export class DescriptionEditorComponent extends BaseEditor 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) { diff --git a/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.ts b/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.ts index a4822ec3b..c48cb95a2 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.ts +++ b/dmp-frontend/src/app/ui/description/editor/description-form/components/form-field-set/form-field-set.component.ts @@ -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 => { diff --git a/dmp-frontend/src/app/ui/description/editor/description-form/components/form-section/form-section.component.html b/dmp-frontend/src/app/ui/description/editor/description-form/components/form-section/form-section.component.html index 2360e1ad2..a22461f6c 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-form/components/form-section/form-section.component.html +++ b/dmp-frontend/src/app/ui/description/editor/description-form/components/form-section/form-section.component.html @@ -25,6 +25,7 @@ [isChild]="false" [hideAnnotations]="isNew" [canReview]="canReview" + [dmpUsers]="dmpUsers" > diff --git a/dmp-frontend/src/app/ui/description/editor/description-form/components/form-section/form-section.component.ts b/dmp-frontend/src/app/ui/description/editor/description-form/components/form-section/form-section.component.ts index a88fb4631..e0814b3a1 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-form/components/form-section/form-section.component.ts +++ b/dmp-frontend/src/app/ui/description/editor/description-form/components/form-section/form-section.component.ts @@ -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; diff --git a/dmp-frontend/src/app/ui/description/editor/description-form/description-form.component.html b/dmp-frontend/src/app/ui/description/editor/description-form/description-form.component.html index 6dc8589bc..f29f4c18e 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-form/description-form.component.html +++ b/dmp-frontend/src/app/ui/description/editor/description-form/description-form.component.html @@ -15,7 +15,7 @@
- +
diff --git a/dmp-frontend/src/app/ui/description/editor/description-form/description-form.component.ts b/dmp-frontend/src/app/ui/description/editor/description-form/description-form.component.ts index cb4399975..828647825 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-form/description-form.component.ts +++ b/dmp-frontend/src/app/ui/description/editor/description-form/description-form.component.ts @@ -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 = new EventEmitter(); diff --git a/dmp-frontend/src/app/ui/description/editor/resolvers/description-editor-entity.resolver.ts b/dmp-frontend/src/app/ui/description/editor/resolvers/description-editor-entity.resolver.ts index 95096a3be..e50bb0028 100644 --- a/dmp-frontend/src/app/ui/description/editor/resolvers/description-editor-entity.resolver.ts +++ b/dmp-frontend/src/app/ui/description/editor/resolvers/description-editor-entity.resolver.ts @@ -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(x => x.descriptions), nameof(x => x.dmpDescriptionTemplate), nameof(x => x.descriptionTemplateGroupId)].join('.'), (prefix ? prefix + '.' : '') + [nameof(x => x.descriptions), nameof(x => x.dmpDescriptionTemplate), nameof(x => x.sectionId)].join('.'), (prefix ? prefix + '.' : '') + [nameof(x => x.descriptions), nameof(x => x.dmpDescriptionTemplate), nameof(x => x.isActive)].join('.'), + (prefix ? prefix + '.' : '') + [nameof(x => x.dmpUsers), nameof(x => x.id)].join('.'), + (prefix ? prefix + '.' : '') + [nameof(x => x.dmpUsers), nameof(x => x.sectionId)].join('.'), + (prefix ? prefix + '.' : '') + [nameof(x => x.dmpUsers), nameof(x => x.user.id)].join('.'), + (prefix ? prefix + '.' : '') + [nameof(x => x.dmpUsers), nameof(x => x.user.name)].join('.'), + (prefix ? prefix + '.' : '') + [nameof(x => x.dmpUsers), nameof(x => x.role)].join('.'), + (prefix ? prefix + '.' : '') + [nameof(x => x.dmpUsers), nameof(x => x.isActive)].join('.'), ] }