diff --git a/projects/sonar/src/app/app-routing.module.ts b/projects/sonar/src/app/app-routing.module.ts index e33eac3c..59b07b9d 100644 --- a/projects/sonar/src/app/app-routing.module.ts +++ b/projects/sonar/src/app/app-routing.module.ts @@ -156,7 +156,7 @@ export const typeResolver: ResolveFn = (route) => { export class AppRoutingModule { private translateService: TranslateService = inject(TranslateService); - private router: Router= inject(Router); + private router: Router = inject(Router); private route: ActivatedRoute = inject(ActivatedRoute); private userService: UserService = inject(UserService); private httpClient: HttpClient = inject(HttpClient); @@ -279,7 +279,8 @@ export class AppRoutingModule { Accept: 'application/rero+json' } }, - files: {...fileConfig, + files: { + ...fileConfig, filterList: (item: any) => { return ( item.metadata && @@ -396,6 +397,29 @@ export class AppRoutingModule { editorSettings: { longMode: true, }, + preCreateRecord: (record: any) => { + const user = this.userService.currentUser(); + // add organisation reference to the new record + const organisationCode = user.organisation.code; + if (organisationCode) { + record.metadata.organisation = { + $ref: this.apiService.getRefEndpoint( + 'organisations', + organisationCode + ) + }; + } + const userPid = user.pid; + if (userPid) { + record.metadata.user = { + $ref: this.apiService.getRefEndpoint( + 'users', + userPid + ) + }; + } + return record; + }, exportFormats: [ { label: 'CSV', @@ -474,8 +498,8 @@ export class AppRoutingModule { if (user) { /** Removes collections and subdivisions routes on the organisation shared */ if (!('isDedicated' in user.organisation) || !(user.organisation.isDedicated)) { - recordsRoutesConfiguration = recordsRoutesConfiguration - .filter(route => !(['collections', 'subdivisions'].includes(route.type))); + recordsRoutesConfiguration = recordsRoutesConfiguration + .filter(route => !(['collections', 'subdivisions'].includes(route.type))); } recordsRoutesConfiguration.forEach((config: any) => { @@ -521,6 +545,7 @@ export class AppRoutingModule { recordResource: config.recordResource || null, exportFormats: config.exportFormats || null, sortOptions: config.sortOptions || null, + preCreateRecord: config.preCreateRecord || null, canAdd: () => this._can(config.type, 'add'), canUpdate: (record: any) => this._can(config.type, 'update', record), canDelete: (record: any) => this._can(config.type, 'delete', record), @@ -634,7 +659,7 @@ export class AppRoutingModule { private _documentAggregationsOrder(): Observable { return of(null).pipe( switchMap(() => { - const {view} = this.route.snapshot.children[0].params; + const { view } = this.route.snapshot.children[0].params; let params = new HttpParams(); if (view) { diff --git a/projects/sonar/src/app/app.module.ts b/projects/sonar/src/app/app.module.ts index 24393cad..a25094ad 100644 --- a/projects/sonar/src/app/app.module.ts +++ b/projects/sonar/src/app/app.module.ts @@ -95,6 +95,7 @@ import { BriefViewComponent as ProjectBriefViewComponent } from './record/projec import { DetailComponent as ProjectDetailComponent } from './record/project/detail/detail.component'; import { BriefViewComponent as SubdivisionBriefViewComponent } from './record/subdivision/brief-view/brief-view.component'; import { UserComponent } from './record/user/user.component'; +import { ValidationComponent } from './record/validation/validation.component'; import { UserService } from './user.service'; import { LicensePipe } from './record/document/license.pipe'; import { BucketNameService } from './bucket-name.service'; @@ -145,7 +146,8 @@ export function minElementError(err: any, field: FormlyFieldConfig) { MetadataComponent, FilesComponent, SwisscoveryComponent, - LicensePipe + LicensePipe, + ValidationComponent ], bootstrap: [AppComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], diff --git a/projects/sonar/src/app/deposit/brief-view/brief-view.component.html b/projects/sonar/src/app/deposit/brief-view/brief-view.component.html index b7b0809d..5673f4b2 100644 --- a/projects/sonar/src/app/deposit/brief-view/brief-view.component.html +++ b/projects/sonar/src/app/deposit/brief-view/brief-view.component.html @@ -62,22 +62,8 @@

@if(!record.metadata.logs || record.metadata.logs.length === 1) { } - @switch(record.metadata.status) { - @case('in_progress') { - - } - @case('to_validate') { - - } - @case('ask_for_changes') { - - } - @case('validated') { - - } - @case('rejected') { - - } + @if(statusSeverity) { + } diff --git a/projects/sonar/src/app/deposit/brief-view/brief-view.component.ts b/projects/sonar/src/app/deposit/brief-view/brief-view.component.ts index a82da60a..95a18c17 100644 --- a/projects/sonar/src/app/deposit/brief-view/brief-view.component.ts +++ b/projects/sonar/src/app/deposit/brief-view/brief-view.component.ts @@ -16,6 +16,7 @@ */ import { Component, computed, inject, OnDestroy, OnInit, signal } from '@angular/core'; import { Subscription } from 'rxjs'; +import { VALIDATION_STATUS_SEVERITY } from '../../enum/validation'; import { UserService } from '../../user.service'; @Component({ @@ -48,6 +49,12 @@ export class BriefViewComponent implements OnInit, OnDestroy { this.userService.hasRole(['moderator', 'admin', 'superuser']) ); + get statusSeverity(): string | null { + return this.record?.metadata?.status + ? VALIDATION_STATUS_SEVERITY[this.record.metadata.status] + : null; + } + private subscription: Subscription = new Subscription(); ngOnInit(): void { diff --git a/projects/sonar/src/app/enum/validation.ts b/projects/sonar/src/app/enum/validation.ts index 56173edd..5d61cc3e 100644 --- a/projects/sonar/src/app/enum/validation.ts +++ b/projects/sonar/src/app/enum/validation.ts @@ -30,3 +30,11 @@ export enum validation_action { 'REJECT' = 'reject', 'ASK_FOR_CHANGES' = 'ask_for_changes' } + +export const VALIDATION_STATUS_SEVERITY: Record = { + [validation_status.IN_PROGRESS]: 'primary', + [validation_status.TO_VALIDATE]: 'info', + [validation_status.ASK_FOR_CHANGES]: 'warn', + [validation_status.VALIDATED]: 'success', + [validation_status.REJECTED]: 'danger' +}; diff --git a/projects/sonar/src/app/record/project/brief-view/brief-view.component.html b/projects/sonar/src/app/record/project/brief-view/brief-view.component.html index 2050a0d6..2894f4c8 100644 --- a/projects/sonar/src/app/record/project/brief-view/brief-view.component.html +++ b/projects/sonar/src/app/record/project/brief-view/brief-view.component.html @@ -21,8 +21,8 @@
- @if(record.metadata?.validation?.status !== validationStatus.VALIDATED) { - + @if(validationSeverity) { + }
diff --git a/projects/sonar/src/app/record/project/brief-view/brief-view.component.ts b/projects/sonar/src/app/record/project/brief-view/brief-view.component.ts index 0292887c..72271928 100644 --- a/projects/sonar/src/app/record/project/brief-view/brief-view.component.ts +++ b/projects/sonar/src/app/record/project/brief-view/brief-view.component.ts @@ -16,16 +16,13 @@ */ import { Component } from '@angular/core'; import { ResultItem } from '@rero/ng-core'; -import { validation_status } from '../../../enum/validation'; +import { VALIDATION_STATUS_SEVERITY } from '../../../enum/validation'; @Component({ templateUrl: './brief-view.component.html', standalone: false }) export class BriefViewComponent implements ResultItem { - // Constant for validation status. - readonly validationStatus = validation_status; - // Record data. record: any; @@ -34,4 +31,10 @@ export class BriefViewComponent implements ResultItem { // Detail URL object. detailUrl: { link: string, external: boolean }; + + get validationSeverity(): string | null { + return this.record?.metadata?.validation?.status + ? VALIDATION_STATUS_SEVERITY[this.record.metadata.validation.status] + : null; + } } diff --git a/projects/sonar/src/app/record/validation/validation.component.html b/projects/sonar/src/app/record/validation/validation.component.html new file mode 100644 index 00000000..df8d1b55 --- /dev/null +++ b/projects/sonar/src/app/record/validation/validation.component.html @@ -0,0 +1,106 @@ + +@if(validation && user && (isModerator() || isOwner())) { + +
+ @if(validation.status === validationStatus.IN_PROGRESS || validation.status === validationStatus.ASK_FOR_CHANGES) { + + {{ "The record is currently in status \"\{\{ status \}\}\". It is not visible in public views." | translate: { status: (validation.status) | translate } }} + @if(isOwner()) { + + Submit it + . + } + + } + @if(validation.status === validationStatus.TO_VALIDATE) { + + {{"The record is in validation. It will be reviewed by a moderator before publishing." | translate}} + + } + @if(validation.status === validationStatus.REJECTED) { + + {{ "The record has been rejected after a review from a moderator." | translate}} + + } + @if(validation.status === validationStatus.VALIDATED) { + + {{ "The record is currently published. It is visible in public views." | translate }} + + + } + @if(validation.status === validationStatus.TO_VALIDATE && isModerator()) { + +
+ + + + + +
+ } + @if(validation.logs) { + + @if(showLogs) { + + + + Status + Date + User + Comment + + + + + {{ (log.status) | translate }} + {{ log.date | dateTranslate: 'medium' }} + {{ log.user.name }} + + + + + } + } +
+
+} diff --git a/projects/sonar/src/app/record/validation/validation.component.ts b/projects/sonar/src/app/record/validation/validation.component.ts new file mode 100644 index 00000000..9df84eef --- /dev/null +++ b/projects/sonar/src/app/record/validation/validation.component.ts @@ -0,0 +1,132 @@ +/* + * SONAR User Interface + * Copyright (C) 2021 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Component, ElementRef, inject, Input, OnInit, ViewChild } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CONFIG, RecordService } from '@rero/ng-core'; +import { NgxSpinnerService } from 'ngx-spinner'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { UserService } from '../../user.service'; +import { validation_action, validation_status } from '../../enum/validation'; + +/** + * Component to manage validation on a record. + */ +@Component({ + selector: 'sonar-record-validation', + templateUrl: './validation.component.html', + standalone: false, +}) +export class ValidationComponent implements OnInit { + + private userService: UserService = inject(UserService); + private recordService: RecordService = inject(RecordService); + private translateService: TranslateService = inject(TranslateService); + private messageService: MessageService = inject(MessageService); + private confirmationService: ConfirmationService = inject(ConfirmationService); + private spinner: NgxSpinnerService = inject(NgxSpinnerService); + + // Constant for validation status. + readonly validationStatus = validation_status; + + // Constant for validation actions. + readonly validationAction = validation_action; + + // Record object. + @Input() record: any; + + // Resource type. + @Input() type: string; + + // Current logged user. + user: any; + + // Validation metadata of the record. + validation: any; + + // Wether to show logs table or not. + showLogs = false; + + /** Used to retrieve value for the comment */ + @ViewChild('comment') comment: ElementRef; + + ngOnInit(): void { + this.validation = this.record.metadata.validation; + + this.userService.user$.subscribe((user) => { + this.user = user; + }); + } + + /** + * Check if current user is the creator of the record. + * + * @returns True if current user is the creator of the record. + */ + isOwner(): boolean { + return this.userService.getUserRefEndpoint() === this.validation.user.$ref; + } + + /** + * Check if current user is moderator. + * + * @returns True if current user is moderator. + */ + isModerator(): boolean { + return this.user?.is_moderator ?? false; + } + + /** + * Update the validation, depending on the action. + * + * @param action Action done. + */ + updateValidation(action: string): void { + this.confirmationService.confirm({ + header: this.translateService.instant('validation_action_' + action), + message: this.translateService.instant( + 'Do you really want to do this action?' + ), + closable: false, + rejectButtonStyleClass: 'p-button-text', + accept: () => { + this.spinner.show(); + + this.validation.action = action; + + // Store the comment + if (this.comment && this.comment.nativeElement.value) { + this.validation.comment = this.comment.nativeElement.value; + } else { + delete this.validation.comment; + } + + this.recordService + .update(this.type, this.record.id, this.record) + .subscribe((record: any) => { + this.record = record; + this.validation = this.record.metadata.validation; + this.spinner.hide(); + this.messageService.add({ + severity: 'success', + detail: this.translateService.instant('Review has been done successfully!'), + life: CONFIG.MESSAGE_LIFE, + }); + }); + }, + }); + } +} diff --git a/projects/sonar/src/app/user.service.ts b/projects/sonar/src/app/user.service.ts index 2e748616..c8ebd9cd 100644 --- a/projects/sonar/src/app/user.service.ts +++ b/projects/sonar/src/app/user.service.ts @@ -135,6 +135,10 @@ export class UserService { && this._user.organisation.isDedicated; } + currentUser(): any { + return this._user; + } + /** * Return the link to public interface, depending on user's organisation. * @returns Link to public interface.