diff --git a/package-lock.json b/package-lock.json index 34531d6464dfc0e530ce4aee021d586399b207aa..cdb64c940fcc90e54bfa3ee7be7aa2f2d3d8126e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9246,9 +9246,9 @@ } }, "solidify-frontend": { - "version": "0.1.6", - "resolved": "https://packages.dlcm.ch/repository/npm-group/solidify-frontend/-/solidify-frontend-0.1.6.tgz", - "integrity": "sha512-USVsE/KsGlKa0HGB57wOag1QUG+g8lTAmqL2+moeqdySv0qytcvyAxCQIjFYftaRn8xG3vf7vUcmMrAloucaTQ==", + "version": "0.1.7", + "resolved": "https://packages.dlcm.ch/repository/npm-group/solidify-frontend/-/solidify-frontend-0.1.7.tgz", + "integrity": "sha512-MxuGeRq+VvwWCNHKPIxTGLHTtkU2nfWy8eTIEDlSXibQNpbDJ/D5EVzhg+3laPS18ChSNtz1zk2Vx4BPAriLTw==", "requires": { "tslib": "^1.9.0" } diff --git a/package.json b/package.json index 5c13f98282355539ea585f7d984bdc640b0b71dd..5964b038381a5c615cb82302a56ccf18c8441386 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "primeicons": "1.0.0", "primeng": "8.0.1", "rxjs": "6.5.2", - "solidify-frontend": "0.1.6", + "solidify-frontend": "0.1.7", "sync-pom-version-to-package": "1.3.1", "ts-key-enum": "2.0.0", "tslib": "1.10.0", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ffeaafda8cfcaaa3b5acf975a0fdcba5093ebbed..c0a87a565aa28372b66a52cc3b7cd19dbe23dd3b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -28,7 +28,6 @@ import {UserDialog} from "@app/components/dialogs/user/user.dialog"; import {AvatarPresentational} from "@app/components/presentationals/avatar/avatar.presentational"; import {MainToolbarDesktopVerticalPresentational} from "@app/components/presentationals/main-toolbar/main-toolbar-desktop-vertical/main-toolbar-desktop-vertical.presentational"; import {MainToolbarMobilePresentational} from "@app/components/presentationals/main-toolbar/main-toolbar-mobile/main-toolbar-mobile.presentational"; -import {PersonForm} from "@app/components/presentationals/person-form/person-form"; import {ThemeSelectorPresentational} from "@app/components/presentationals/theme-selector/theme-selector.presentational"; import {UserForm} from "@app/components/presentationals/user-form/user-form"; import {InMemoryStorage} from "@app/in-memory.storage"; @@ -72,7 +71,6 @@ const presentationals = [ ThemeSelectorPresentational, AvatarPresentational, UserForm, - PersonForm, ]; const dialogs = [ diff --git a/src/app/components/dialogs/user/user.dialog.html b/src/app/components/dialogs/user/user.dialog.html index b05408c8622661b8608ea96568f6d9ffb07343f0..a73f22581c02ea97b97014ace21fb5dd6143e599 100644 --- a/src/app/components/dialogs/user/user.dialog.html +++ b/src/app/components/dialogs/user/user.dialog.html @@ -18,14 +18,13 @@ <mat-spinner></mat-spinner> </div> <ng-template #finishLoading> - <dlcm-person-form #formPresentational - *ngIf="(currentPersonObs | async)" - [model]="currentPersonObs | async" - (submitChange)="savePersonInfo($event)" - ></dlcm-person-form> + <dlcm-shared-person-form #formPresentational + *ngIf="(currentPersonObs | async)" + [model]="currentPersonObs | async" + (submitChange)="savePersonInfo($event)" + ></dlcm-shared-person-form> </ng-template> </div> - </div> <div mat-dialog-actions class="footer" diff --git a/src/app/components/dialogs/user/user.dialog.ts b/src/app/components/dialogs/user/user.dialog.ts index 5d58f9e2517ea395aac07b24bcc880a40ca56028..6fa21941dcc8209c3668303a63b86661a628ebe7 100644 --- a/src/app/components/dialogs/user/user.dialog.ts +++ b/src/app/components/dialogs/user/user.dialog.ts @@ -10,7 +10,6 @@ import { MAT_DIALOG_DATA, MatDialogRef, } from "@angular/material/dialog"; -import {PersonForm} from "@app/components/presentationals/person-form/person-form"; import {Person} from "@app/generated-api"; import {AppPersonAction} from "@app/stores/person/app-person.action"; import { @@ -18,6 +17,7 @@ import { Store, } from "@ngxs/store"; import {SharedAbstractContainer} from "@shared/components/containers/shared-abstract/shared-abstract.container"; +import {SharedPersonForm} from "@shared/components/presentationals/shared-person-form/shared-person-form"; import {UserExtended} from "@shared/models/business/user-extended.model"; import {LocalStateModel} from "@shared/models/local-state.model"; import {Observable} from "rxjs"; @@ -37,7 +37,7 @@ export class UserDialog extends SharedAbstractContainer implements OnInit { @Select((state: LocalStateModel) => StoreUtil.isLoadingState(state.application.application_person)) isLoadingPersonObs: Observable<boolean>; @ViewChild("formPresentational", {static: false}) - readonly formPresentational: PersonForm; + readonly formPresentational: SharedPersonForm; constructor(protected store: Store, protected dialogRef: MatDialogRef<UserDialog>, diff --git a/src/app/features/deposit/components/dialogs/deposit-person-alternative/deposit-person-alternative.dialog.html b/src/app/features/deposit/components/dialogs/deposit-person-alternative/deposit-person-alternative.dialog.html new file mode 100644 index 0000000000000000000000000000000000000000..4c9e05d1038e44f1a5c578a2308234f9b7d3a84e --- /dev/null +++ b/src/app/features/deposit/components/dialogs/deposit-person-alternative/deposit-person-alternative.dialog.html @@ -0,0 +1,45 @@ +<h1 mat-dialog-title + class="header" +>{{'deposit.popup.person.alternative.title' | translate}}</h1> +<div mat-dialog-content + class="content" +> + <div class="explanation">{{'deposit.popup.person.alternative.explanation' | translate}}</div> + <div class="warning">{{'deposit.popup.person.alternative.warning' | translate}}</div> + + <mat-list> + <h3 mat-subheader>{{'deposit.popup.person.alternative.subtitle.yourInput' | translate}}</h3> + <mat-list-item (click)="select(data.newPerson, true)" + [class.is-active]="isSelected(data.newPerson)" + > + <mat-icon mat-list-icon>person_add</mat-icon> + <h4 mat-line><span class="firstName">{{data.newPerson.firstName}}</span> {{data.newPerson.lastName}}</h4> + <p mat-line>{{data.newPerson.orcid}}</p> + </mat-list-item> + <mat-divider></mat-divider> + <h3 mat-subheader>{{'deposit.popup.person.alternative.subtitle.matchingPersons' | translate}}</h3> + <mat-list-item *ngFor="let person of data.listExistingPerson" + (click)="select(person, false)" + [class.is-active]="isSelected(person)" + > + <mat-icon mat-list-icon>person</mat-icon> + <h4 mat-line><span class="firstName">{{person.firstName}}</span> {{person.lastName}}</h4> + <p mat-line>{{person.orcid}}</p> + </mat-list-item> + </mat-list> + +</div> +<div mat-dialog-actions + class="footer" +> + <button mat-button + [mat-dialog-close]="" + cdkFocusInitial + >{{'deposit.popup.person.alternative.button.close' | translate}}</button> + <button mat-flat-button + color="primary" + [disabled]="!selectedDepositPerson" + (click)="submit()" + >{{'deposit.popup.person.alternative.button.submit' | translate}} + </button> +</div> diff --git a/src/app/features/deposit/components/dialogs/deposit-person-alternative/deposit-person-alternative.dialog.scss b/src/app/features/deposit/components/dialogs/deposit-person-alternative/deposit-person-alternative.dialog.scss new file mode 100644 index 0000000000000000000000000000000000000000..2613d05b7d1bd86f00ef8d91b6376f380989a652 --- /dev/null +++ b/src/app/features/deposit/components/dialogs/deposit-person-alternative/deposit-person-alternative.dialog.scss @@ -0,0 +1,38 @@ +@import "../sass/abstracts/variables"; +@import "../sass/abstracts/mixins"; + +:host { + .header { + text-align: center; + } + + .content { + .explanation { + padding-bottom: 10px; + white-space: pre; + } + + .warning { + color: $warning; + padding-bottom: 20px; + } + + ::ng-deep .mat-list-item { + cursor: pointer; + + &.is-active { + background-color: $extra-light-grey; + } + } + + .firstName { + font-style: italic; + padding-right: 6px; + } + } + + .footer { + display: flex; + justify-content: space-between; + } +} diff --git a/src/app/features/deposit/components/dialogs/deposit-person-alternative/deposit-person-alternative.dialog.ts b/src/app/features/deposit/components/dialogs/deposit-person-alternative/deposit-person-alternative.dialog.ts new file mode 100644 index 0000000000000000000000000000000000000000..1539c5c1ce6f2de21a3b01f2ee8bf545f001764b --- /dev/null +++ b/src/app/features/deposit/components/dialogs/deposit-person-alternative/deposit-person-alternative.dialog.ts @@ -0,0 +1,55 @@ +import {PersonExtended} from "@admin/models/person-extended.model"; +import { + ChangeDetectionStrategy, + Component, + Inject, +} from "@angular/core"; +import { + MAT_DIALOG_DATA, + MatDialogRef, +} from "@angular/material/dialog"; +import {SharedAbstractContainer} from "@shared/components/containers/shared-abstract/shared-abstract.container"; +import {isNullOrUndefined} from "solidify-frontend"; + +@Component({ + selector: "dlcm-deposit-alternative-dialog", + templateUrl: "./deposit-person-alternative.dialog.html", + styleUrls: ["./deposit-person-alternative.dialog.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DepositPersonAlternativeDialog extends SharedAbstractContainer { + selectedDepositPerson: AlternativeDepositPersonSelected; + + constructor(private readonly dialogRef: MatDialogRef<DepositPersonAlternativeDialog>, + @Inject(MAT_DIALOG_DATA) public data: AlternativeDepositPersonWrapper) { + super(); + } + + submit(): void { + this.dialogRef.close(this.selectedDepositPerson); + } + + select(selected: PersonExtended, isNew: boolean): void { + this.selectedDepositPerson = { + selected: selected, + isNew: isNew, + }; + } + + isSelected(current: PersonExtended): boolean { + if (isNullOrUndefined(this.selectedDepositPerson)) { + return false; + } + return current === this.selectedDepositPerson.selected; + } +} + +export interface AlternativeDepositPersonWrapper { + newPerson: PersonExtended; + listExistingPerson: PersonExtended[]; +} + +export interface AlternativeDepositPersonSelected { + selected: PersonExtended; + isNew: boolean; +} diff --git a/src/app/features/deposit/components/dialogs/deposit-person/deposit-person.dialog.html b/src/app/features/deposit/components/dialogs/deposit-person/deposit-person.dialog.html new file mode 100644 index 0000000000000000000000000000000000000000..eae2b72811ffd288282dce05f0b2c4802dd0930b --- /dev/null +++ b/src/app/features/deposit/components/dialogs/deposit-person/deposit-person.dialog.html @@ -0,0 +1,34 @@ +<h1 mat-dialog-title + class="header" +>{{'deposit.popup.person.title' | translate}}</h1> +<div mat-dialog-content + class="content" +> + <div class="warning">{{'deposit.popup.person.warning' | translate}}</div> + <div class="wrapper"> + <div class="spinner-wrapper" + *ngIf="isLoadingPersonObs | async" + > + <mat-spinner></mat-spinner> + </div> + + <dlcm-shared-person-form #formPresentational + (submitChange)="savePerson($event)" + ></dlcm-shared-person-form> + + </div> +</div> +<div mat-dialog-actions + class="footer" +> + <button mat-button + [mat-dialog-close]="" + cdkFocusInitial + >{{'deposit.popup.person.button.close' | translate}}</button> + <button mat-flat-button + color="primary" + [disabled]="!formPresentational?.form.valid || !formPresentational?.form.dirty || (isLoadingPersonObs | async)" + (click)="formPresentational?.onSubmit()" + >{{'deposit.popup.person.button.submit' | translate}} + </button> +</div> diff --git a/src/app/features/deposit/components/dialogs/deposit-person/deposit-person.dialog.scss b/src/app/features/deposit/components/dialogs/deposit-person/deposit-person.dialog.scss new file mode 100644 index 0000000000000000000000000000000000000000..480b6369c10296599cfb083cfbac400fd344e97a --- /dev/null +++ b/src/app/features/deposit/components/dialogs/deposit-person/deposit-person.dialog.scss @@ -0,0 +1,28 @@ +@import "../sass/abstracts/variables"; +@import "../sass/abstracts/mixins"; + +:host { + .header { + text-align: center; + } + + .content { + .warning { + text-align: center; + color: $warning; + padding-bottom: 20px; + } + + .wrapper { + position: relative; + min-height: 150px; + } + + @include spinner-wrapper(); + } + + .footer { + display: flex; + justify-content: space-between; + } +} diff --git a/src/app/features/deposit/components/dialogs/deposit-person/deposit-person.dialog.ts b/src/app/features/deposit/components/dialogs/deposit-person/deposit-person.dialog.ts new file mode 100644 index 0000000000000000000000000000000000000000..e48d2156cbe0a3ce50cc628532fbe29a8e5a870e --- /dev/null +++ b/src/app/features/deposit/components/dialogs/deposit-person/deposit-person.dialog.ts @@ -0,0 +1,110 @@ +import {PersonExtended} from "@admin/models/person-extended.model"; +import { + ChangeDetectionStrategy, + Component, + ViewChild, +} from "@angular/core"; +import { + MatDialog, + MatDialogRef, +} from "@angular/material/dialog"; +import { + AlternativeDepositPersonSelected, + AlternativeDepositPersonWrapper, + DepositPersonAlternativeDialog, +} from "@deposit/components/dialogs/deposit-person-alternative/deposit-person-alternative.dialog"; +import { + Select, + Store, +} from "@ngxs/store"; +import {SharedAbstractContainer} from "@shared/components/containers/shared-abstract/shared-abstract.container"; +import {SharedPersonForm} from "@shared/components/presentationals/shared-person-form/shared-person-form"; +import {LocalStateModel} from "@shared/models/local-state.model"; +import {SharedPersonAction} from "@shared/stores/person/shared-person.action"; +import {Observable} from "rxjs"; +import {tap} from "rxjs/operators"; +import { + isEmptyArray, + isNullOrUndefined, + ModelFormControlEvent, + NotificationService, + StoreUtil, + TRANSLATE, +} from "solidify-frontend"; + +@Component({ + selector: "dlcm-deposit-person-dialog", + templateUrl: "./deposit-person.dialog.html", + styleUrls: ["./deposit-person.dialog.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DepositPersonDialog extends SharedAbstractContainer { + @Select((state: LocalStateModel) => StoreUtil.isLoadingState(state.shared.shared_person)) isLoadingPersonObs: Observable<boolean>; + + @ViewChild("formPresentational", {static: false}) + readonly formPresentational: SharedPersonForm; + + constructor(protected readonly store: Store, + protected readonly dialogRef: MatDialogRef<DepositPersonDialog>, + private readonly notificationService: NotificationService, + private readonly dialog: MatDialog) { + super(); + } + + savePerson($event: ModelFormControlEvent<PersonExtended>): void { + this.checkSimilarPerson($event); + } + + private checkSimilarPerson($event: ModelFormControlEvent<PersonExtended>): void { + this.subscribe(this.store.dispatch(new SharedPersonAction.Search($event.model)).pipe( + tap((state: LocalStateModel) => { + const listPersonMatching = state.shared.shared_person.listPersonMatching; + if (isNullOrUndefined(listPersonMatching) || isEmptyArray(listPersonMatching)) { + this.createPerson($event); + } else { + this.openAlternativeDialog($event, listPersonMatching); + } + }), + )); + } + + private createPerson($event: ModelFormControlEvent<PersonExtended>): void { + const obs = this.store.dispatch(new SharedPersonAction.Create($event)); + + this.subscribe(obs.pipe( + tap((state: LocalStateModel) => { + const currentPersonInModal = $event.model; + const currentPersonInState = state.shared.shared_person.current; + if (this.isSamePerson(currentPersonInModal, currentPersonInState)) { + this.dialogRef.close(currentPersonInState); + } else { + this.notificationService.showError(TRANSLATE("deposit.addPerson.dialog.error"), true); + } + }), + )); + } + + private isSamePerson(currentPersonInModal: PersonExtended, currentPersonInState: PersonExtended): boolean { + return currentPersonInModal.firstName === currentPersonInState.firstName && currentPersonInModal.lastName === currentPersonInState.lastName && currentPersonInModal.orcid === currentPersonInState.orcid; + } + + private openAlternativeDialog(newPerson: ModelFormControlEvent<PersonExtended>, listMatchingPerson: PersonExtended[]): void { + this.subscribe(this.dialog.open(DepositPersonAlternativeDialog, { + data: { + listExistingPerson: listMatchingPerson, + newPerson: newPerson.model, + } as AlternativeDepositPersonWrapper, + }).afterClosed().pipe( + tap((depositPersonSelected: AlternativeDepositPersonSelected) => { + if (isNullOrUndefined(depositPersonSelected)) { + return; + } + if (depositPersonSelected.isNew) { + this.createPerson(newPerson); + } else { + this.dialogRef.close(depositPersonSelected.selected); + } + }), + )); + } +} diff --git a/src/app/features/deposit/components/presentationals/deposit-form/deposit-form.presentational.html b/src/app/features/deposit/components/presentationals/deposit-form/deposit-form.presentational.html index cb8388c415e6e4ca11f7bb96452c3653758b0e7f..6e9a5c2a3b1b5ab80a83a3114ae9b548ebccb1b3 100644 --- a/src/app/features/deposit/components/presentationals/deposit-form/deposit-form.presentational.html +++ b/src/app/features/deposit/components/presentationals/deposit-form/deposit-form.presentational.html @@ -114,20 +114,46 @@ <mat-error>{{formValidationHelper.getFormError(fd)}}</mat-error> </mat-form-field> - <ng-container *ngIf="getFormControl(formDefinition.authors) as fd"> - <dlcm-shared-multi-select [matTooltip]="'deposit.tooltips.authors' | translate" - [matTooltipPosition]="'left'" - aria-label="author tooltip" - matTooltipClass="tooltip" - [list]="listPersons" - [formControl]="fd" - [labelKey]="'fullName'" - [valueKey]="'resId'" - [placeholder]="'deposit.authors' | translate" - [required]="formValidationHelper.hasRequiredField(fd)" - > - </dlcm-shared-multi-select> - </ng-container> + + <div class="author-wrapper"> + <div class="author-wrapper-firstline"> + <ng-container *ngIf="getFormControl(formDefinition.authors) as fd"> + <dlcm-shared-multi-select [matTooltip]="'deposit.tooltips.authors' | translate" + [matTooltipPosition]="'left'" + aria-label="author tooltip" + matTooltipClass="tooltip" + class="input" + [list]="listPersons" + [formControl]="fd" + [labelKey]="'fullName'" + [valueKey]="'resId'" + [placeholder]="'deposit.authors' | translate" + [required]="formValidationHelper.hasRequiredField(fd)" + > + </dlcm-shared-multi-select> + </ng-container> + + <button mat-icon-button + mat-button + (click)="addPeople()" + type="button" + aria-label="Add missing person" + [disabled]="readonly" + [matTooltip]="'deposit.tooltips.addMissingPerson' | translate" + [matTooltipPosition]="'above'" + > + <mat-icon>person_add</mat-icon> + </button> + </div> + + <div class="author-add-me-wrapper"> + <a *ngIf="!readonly && personConnected && !personConnectedAlreadyInAuthor()" + class="author-add-me" + (click)="addMeAsAuthor()" + >{{'deposit.addMeAsAuthor' | translate }}</a> + </div> + </div> + <mat-form-field [matTooltip]="'deposit.tooltips.accessLevel' | translate" [matTooltipPosition]="'left'" diff --git a/src/app/features/deposit/components/presentationals/deposit-form/deposit-form.presentational.scss b/src/app/features/deposit/components/presentationals/deposit-form/deposit-form.presentational.scss new file mode 100644 index 0000000000000000000000000000000000000000..5e355aa367397dd67364fd82b1e1b2229d92c140 --- /dev/null +++ b/src/app/features/deposit/components/presentationals/deposit-form/deposit-form.presentational.scss @@ -0,0 +1,19 @@ +@import "../../../../../shared/components/presentationals/shared-abstract-form/shared-abstract-form.presentational.scss"; + +.author-wrapper { + display: flex; + flex-direction: column; + + .author-wrapper-firstline { + display: flex; + + .input { + width: 100%; + } + } + + .author-add-me-wrapper { + padding-bottom: 20px; + text-align: right; + } +} diff --git a/src/app/features/deposit/components/presentationals/deposit-form/deposit-form.presentational.ts b/src/app/features/deposit/components/presentationals/deposit-form/deposit-form.presentational.ts index ea069a0c9f61ead73de7dc284d5556586625f54f..cfebdbb3bc8ae62b2469a27433a8f3cfb6768b2e 100644 --- a/src/app/features/deposit/components/presentationals/deposit-form/deposit-form.presentational.ts +++ b/src/app/features/deposit/components/presentationals/deposit-form/deposit-form.presentational.ts @@ -9,6 +9,7 @@ import { FormBuilder, Validators, } from "@angular/forms"; +import {MatDialog} from "@angular/material/dialog"; import {DepositExtended} from "@app/features/deposit/models/deposits-extended.model"; import {OrganizationalUnitExtended} from "@app/features/deposit/models/organizational-unit-extended.model"; import { @@ -22,6 +23,7 @@ import { import {SharedAbstractFormPresentational} from "@app/shared/components/presentationals/shared-abstract-form/shared-abstract-form.presentational"; import {LocalModelAttributeEnum} from "@app/shared/enums/model-attribute.enum"; import {BaseFormDefinition} from "@app/shared/models/base-form-definition.model"; +import {DepositPersonDialog} from "@deposit/components/dialogs/deposit-person/deposit-person.dialog"; import _ from "lodash"; import { distinctUntilChanged, @@ -29,6 +31,8 @@ import { } from "rxjs/operators"; import { DateUtil, + isEmptyString, + isNullOrUndefined, PropertyName, SolidifyValidator, } from "solidify-frontend"; @@ -37,7 +41,7 @@ import AccessEnum = Deposit.AccessEnum; @Component({ selector: "dlcm-deposit-form", templateUrl: "./deposit-form.presentational.html", - styleUrls: ["../../../../../shared/components/presentationals/shared-abstract-form/shared-abstract-form.presentational.scss"], + styleUrls: ["./deposit-form.presentational.scss"], changeDetection: ChangeDetectionStrategy.OnPush, }) export class DepositFormPresentational extends SharedAbstractFormPresentational<DepositExtended> implements OnInit { @@ -69,8 +73,12 @@ export class DepositFormPresentational extends SharedAbstractFormPresentational< @Input() listPersons: Person[]; + @Input() + personConnected: Person; + constructor(protected readonly _changeDetectorRef: ChangeDetectorRef, - private readonly _fb: FormBuilder) { + private readonly _fb: FormBuilder, + private readonly dialog: MatDialog) { super(_changeDetectorRef); } @@ -138,6 +146,42 @@ export class DepositFormPresentational extends SharedAbstractFormPresentational< deposit.collectionEnd = DateUtil.convertToOffsetDateTimeIso8601(deposit.collectionEnd); return deposit; } + + addPeople(): void { + this.subscribe( + this.dialog.open(DepositPersonDialog, { + width: "90%", + }).afterClosed().pipe( + tap((person: Person) => { + if (isNullOrUndefined(person)) { + return; + } + this.listPersons = [...this.listPersons, person]; + this.addAuthor(person.resId); + }), + ), + ); + } + + addMeAsAuthor(): void { + this.addAuthor(this.personConnected.resId); + } + + addAuthor(personResId: string): void { + const formControl = this.form.get(this.formDefinition.authors); + let currentValue = formControl.value; + if (isNullOrUndefined(currentValue) || isEmptyString(currentValue)) { + currentValue = []; + } + if (currentValue.includes(personResId)) { + return; + } + formControl.setValue([...currentValue, personResId]); + } + + personConnectedAlreadyInAuthor(): boolean { + return this.form.get(this.formDefinition.authors).value.includes(this.personConnected.resId); + } } class FormComponentFormDefinition extends BaseFormDefinition { diff --git a/src/app/features/deposit/components/routables/deposit-create/deposit-create.routable.html b/src/app/features/deposit/components/routables/deposit-create/deposit-create.routable.html index c0c05fa468a9ea7cb58aecca0d73dbabe597eef5..d7f599ba2172f44b3435a607a5266d7b775770df 100644 --- a/src/app/features/deposit/components/routables/deposit-create/deposit-create.routable.html +++ b/src/app/features/deposit/components/routables/deposit-create/deposit-create.routable.html @@ -11,6 +11,7 @@ [submissionPolicies]="submissionPoliciesObs | async" [preservationPolicies]="preservationPoliciesObs | async" [organizationalUnits]="organizationalUnitsObs | async" + [personConnected]="personConnectedObs | async" [listPersons]="listPersonObs | async" ></dlcm-deposit-form> </div> diff --git a/src/app/features/deposit/components/routables/deposit-create/deposit-create.routable.ts b/src/app/features/deposit/components/routables/deposit-create/deposit-create.routable.ts index bd7488fcaddd44d0c3d7695ba4a2aa9fd8827dcc..ad3ad4e2e56cab81988a73c8c10a4b8077402f3b 100644 --- a/src/app/features/deposit/components/routables/deposit-create/deposit-create.routable.ts +++ b/src/app/features/deposit/components/routables/deposit-create/deposit-create.routable.ts @@ -41,6 +41,7 @@ export class DepositCreateRoutable extends SharedAbstractCreateRoutable<DepositE @Select((state: LocalStateModel) => state.shared.shared_preservationPolicy.list) preservationPoliciesObs: Observable<PreservationPolicy[]>; @Select((state: LocalStateModel) => state.shared.shared_person.list) listPersonObs: Observable<Person[]>; @Select((state: LocalStateModel) => state.shared.shared_organizationalUnit.list) organizationalUnitsObs: Observable<AccessOrganizationalUnit[]>; + @Select((state: LocalStateModel) => state.application.application_person.current) personConnectedObs: Observable<Person>; @ViewChild("formPresentational", {static: false}) readonly formPresentational: SharedAbstractFormPresentational<DepositExtended>; diff --git a/src/app/features/deposit/components/routables/deposit-edit/deposit-edit.routable.html b/src/app/features/deposit/components/routables/deposit-edit/deposit-edit.routable.html index 5449234afb4dd2c9d2ada4e7b57f06ca09d7a135..f551699b12b064dcb52a4fe3cefd3c859f5a032e 100644 --- a/src/app/features/deposit/components/routables/deposit-edit/deposit-edit.routable.html +++ b/src/app/features/deposit/components/routables/deposit-edit/deposit-edit.routable.html @@ -10,7 +10,8 @@ > <mat-tab [label]="'deposit.tab.details' | translate"> <div class="tab-content"> - <dlcm-deposit-form #formPresentational *ngIf="(currentObs | async) && !(isLoadingObs | async) && !(isLoadingPersonObs | async)" + <dlcm-deposit-form #formPresentational + *ngIf="(currentObs | async) && !(isLoadingObs | async) && !(isLoadingPersonObs | async)" [model]="currentObs | async" [languages]="languagesObs | async" [licenses]="licensesObs | async" @@ -19,6 +20,7 @@ [organizationalUnits]="organizationalUnitsObs | async" [listPersons]="listPersonObs | async" [selectedPersons]="selectedPersonObs | async" + [personConnected]="personConnectedObs | async" (submitChange)="update($event)" ></dlcm-deposit-form> </div> diff --git a/src/app/features/deposit/components/routables/deposit-edit/deposit-edit.routable.ts b/src/app/features/deposit/components/routables/deposit-edit/deposit-edit.routable.ts index f028684120b4b6937b7dcdd852e8a68c420deca7..461c3ccdbaa76e220276cdb2824687c08a0a10e1 100644 --- a/src/app/features/deposit/components/routables/deposit-edit/deposit-edit.routable.ts +++ b/src/app/features/deposit/components/routables/deposit-edit/deposit-edit.routable.ts @@ -45,6 +45,7 @@ export class DepositEditRoutable extends SharedAbstractEditRoutable<DepositExten @Select((state: LocalStateModel) => state.shared.shared_person.list) listPersonObs: Observable<Person[]>; @Select((state: LocalStateModel) => state.shared.shared_organizationalUnit.list) organizationalUnitsObs: Observable<AccessOrganizationalUnit[]>; @Select((state: LocalStateModel) => state.deposit.deposit_person.selected) selectedPersonObs: Observable<Person[]>; + @Select((state: LocalStateModel) => state.application.application_person.current) personConnectedObs: Observable<Person>; @ViewChild("formPresentational", {static: false}) readonly formPresentational: DepositFormPresentational; diff --git a/src/app/features/deposit/deposit.module.ts b/src/app/features/deposit/deposit.module.ts index da477e54eb85154da967d40d1395d7525dc3dcfc..16ddc94952f80dc42d3274557cf3161df7e22f43 100644 --- a/src/app/features/deposit/deposit.module.ts +++ b/src/app/features/deposit/deposit.module.ts @@ -12,6 +12,8 @@ import {DepositDataFileState} from "@app/features/deposit/stores/data-file/depos import {DepositState} from "@app/features/deposit/stores/deposit.state"; import {DepositPeopleState} from "@app/features/deposit/stores/people/deposit-people.state"; import {SharedModule} from "@app/shared/shared.module"; +import {DepositPersonAlternativeDialog} from "@deposit/components/dialogs/deposit-person-alternative/deposit-person-alternative.dialog"; +import {DepositPersonDialog} from "@deposit/components/dialogs/deposit-person/deposit-person.dialog"; import {TranslateModule} from "@ngx-translate/core"; import {NgxsModule} from "@ngxs/store"; import {DepositFileTreePresentational} from "./components/presentationals/deposit-file-tree/deposit-file-tree.presentational"; @@ -32,6 +34,8 @@ const dialogs = [ DepositDeleteDialog, DepositFileDetailDialog, DepositFileUploadDialog, + DepositPersonDialog, + DepositPersonAlternativeDialog, ]; const presentationals = [ DepositFormPresentational, diff --git a/src/app/components/presentationals/person-form/person-form.html b/src/app/shared/components/presentationals/shared-person-form/shared-person-form.html similarity index 100% rename from src/app/components/presentationals/person-form/person-form.html rename to src/app/shared/components/presentationals/shared-person-form/shared-person-form.html diff --git a/src/app/components/presentationals/person-form/person-form.ts b/src/app/shared/components/presentationals/shared-person-form/shared-person-form.ts similarity index 85% rename from src/app/components/presentationals/person-form/person-form.ts rename to src/app/shared/components/presentationals/shared-person-form/shared-person-form.ts index 7dd579ef8d731e937bcc69b73260285d63f305d1..1284a8f8dde8ce9f83b18cad27ebc3cdf3be455d 100644 --- a/src/app/components/presentationals/person-form/person-form.ts +++ b/src/app/shared/components/presentationals/shared-person-form/shared-person-form.ts @@ -16,12 +16,12 @@ import { } from "solidify-frontend"; @Component({ - selector: "dlcm-person-form", - templateUrl: "./person-form.html", - styleUrls: ["../../../shared/components/presentationals/shared-abstract-form/shared-abstract-form.presentational.scss"], + selector: "dlcm-shared-person-form", + templateUrl: "./shared-person-form.html", + styleUrls: ["../../../../shared/components/presentationals/shared-abstract-form/shared-abstract-form.presentational.scss"], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class PersonForm extends SharedAbstractFormPresentational<PersonExtended> { +export class SharedPersonForm extends SharedAbstractFormPresentational<PersonExtended> { formDefinition: FormComponentFormDefinition = new FormComponentFormDefinition(); constructor(protected readonly _changeDetectorRef: ChangeDetectorRef, diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 13b408915f8684cefc6c337d44cccf268f312b05..cf60934d8a584c0183d3a14e95658785b16cace0 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -28,6 +28,7 @@ import {TranslateModule} from "@ngx-translate/core"; import {NgxsModule} from "@ngxs/store"; import {SharedHistoryDialog} from "@shared/components/dialogs/shared-history/shared-history.dialog"; import {SharedButtonStatusHistoryPresentational} from "@shared/components/presentationals/shared-button-status-history/shared-button-status-history.presentational"; +import {SharedPersonForm} from "@shared/components/presentationals/shared-person-form/shared-person-form"; import {DatetimePipe} from "@shared/pipes/datetime.pipe"; import {TableModule} from "primeng/table"; import {SolidifyFrontendModule} from "solidify-frontend"; @@ -47,6 +48,7 @@ const presentationals = [ SharedFieldErrorPresentational, SharedErrorPresentational, SharedButtonStatusHistoryPresentational, + SharedPersonForm, ]; const directives = [SharedValidationDirective]; const pipes = [ diff --git a/src/app/shared/stores/person/shared-person.action.ts b/src/app/shared/stores/person/shared-person.action.ts index bd5f6b162709931ecb7eac0cc6d461d31e2fb6e7..1b3a08b00242b542a831275da9a0521062397bb0 100644 --- a/src/app/shared/stores/person/shared-person.action.ts +++ b/src/app/shared/stores/person/shared-person.action.ts @@ -1,3 +1,4 @@ +import {PersonExtended} from "@admin/models/person-extended.model"; import {Person} from "@app/generated-api"; import { ResourceAction, @@ -30,11 +31,11 @@ export namespace SharedPersonAction { } @TypeDefaultAction(state) - export class GetAllSuccess extends ResourceAction.GetAllSuccess<Person> { + export class GetAllSuccess extends ResourceAction.GetAllSuccess<PersonExtended> { } @TypeDefaultAction(state) - export class GetAllFail extends ResourceAction.GetAllFail<Person> { + export class GetAllFail extends ResourceAction.GetAllFail<PersonExtended> { } @TypeDefaultAction(state) @@ -42,35 +43,35 @@ export namespace SharedPersonAction { } @TypeDefaultAction(state) - export class GetByIdSuccess extends ResourceAction.GetByIdSuccess<Person> { + export class GetByIdSuccess extends ResourceAction.GetByIdSuccess<PersonExtended> { } @TypeDefaultAction(state) - export class GetByIdFail extends ResourceAction.GetByIdFail<Person> { + export class GetByIdFail extends ResourceAction.GetByIdFail<PersonExtended> { } @TypeDefaultAction(state) - export class Create extends ResourceAction.Create<Person> { + export class Create extends ResourceAction.Create<PersonExtended> { } @TypeDefaultAction(state) - export class CreateSuccess extends ResourceAction.CreateSuccess<Person> { + export class CreateSuccess extends ResourceAction.CreateSuccess<PersonExtended> { } @TypeDefaultAction(state) - export class CreateFail extends ResourceAction.CreateFail<Person> { + export class CreateFail extends ResourceAction.CreateFail<PersonExtended> { } @TypeDefaultAction(state) - export class Update extends ResourceAction.Update<Person> { + export class Update extends ResourceAction.Update<PersonExtended> { } @TypeDefaultAction(state) - export class UpdateSuccess extends ResourceAction.UpdateSuccess<Person> { + export class UpdateSuccess extends ResourceAction.UpdateSuccess<PersonExtended> { } @TypeDefaultAction(state) - export class UpdateFail extends ResourceAction.UpdateFail<Person> { + export class UpdateFail extends ResourceAction.UpdateFail<PersonExtended> { } @TypeDefaultAction(state) @@ -84,6 +85,27 @@ export namespace SharedPersonAction { @TypeDefaultAction(state) export class DeleteFail extends ResourceAction.DeleteFail { } + + export class Search { + static readonly type: string = `[${state}] Search`; + + constructor(public person: Person) { + } + } + + export class SearchSuccess { + static readonly type: string = `[${state}] Search Success`; + + constructor(public list: PersonExtended[]) { + } + } + + export class SearchFail { + static readonly type: string = `[${state}] Search Fail`; + + constructor() { + } + } } export const sharedPersonActionNameSpace: ResourceNameSpace = SharedPersonAction; diff --git a/src/app/shared/stores/person/shared-person.state.ts b/src/app/shared/stores/person/shared-person.state.ts index 3dc6d73d167abb732c50dd80c7e888ea8697bb52..5f3266bfba5132de007c3bbdae8bf2e7aa503d72 100644 --- a/src/app/shared/stores/person/shared-person.state.ts +++ b/src/app/shared/stores/person/shared-person.state.ts @@ -1,15 +1,25 @@ -import {Person} from "@app/generated-api"; +import {PersonExtended} from "@admin/models/person-extended.model"; import {AdminResourceApiEnum} from "@app/shared/enums/api.enum"; import {LocalStateEnum} from "@app/shared/enums/local-state.enum"; -import {RoutesEnum} from "@app/shared/enums/routes.enum"; -import {sharedPersonActionNameSpace} from "@app/shared/stores/person/shared-person.action"; import { + SharedPersonAction, + sharedPersonActionNameSpace, +} from "@app/shared/stores/person/shared-person.action"; +import { + Action, Actions, State, + StateContext, Store, } from "@ngxs/store"; +import {Observable} from "rxjs"; +import { + catchError, + tap, +} from "rxjs/operators"; import { ApiService, + CollectionTyped, defaultResourceStateInitValue, NotificationService, QueryParameters, @@ -19,17 +29,19 @@ import { } from "solidify-frontend"; import {environment} from "../../../../environments/environment"; -export interface SharedPersonStateModel extends ResourceStateModel<Person> { +export interface SharedPersonStateModel extends ResourceStateModel<PersonExtended> { + listPersonMatching: PersonExtended[]; } @State<SharedPersonStateModel>({ name: LocalStateEnum.shared_person, defaults: { ...defaultResourceStateInitValue, + listPersonMatching: [], queryParameters: new QueryParameters(environment.defaultEnumValuePageSizeOption), }, }) -export class SharedPersonState extends ResourceState<Person> { +export class SharedPersonState extends ResourceState<PersonExtended> { constructor(protected apiService: ApiService, protected store: Store, protected notificationService: NotificationService, @@ -37,10 +49,47 @@ export class SharedPersonState extends ResourceState<Person> { super(apiService, store, notificationService, actions$, { nameSpace: sharedPersonActionNameSpace, urlResource: AdminResourceApiEnum.people, - routeRedirectUrlAfterSuccessAction: RoutesEnum.homePage, + routeRedirectUrlAfterSuccessAction: undefined, notificationResourceCreateTextToTranslate: TRANSLATE("shared.person.notification.resource.create"), notificationResourceDeleteTextToTranslate: TRANSLATE("shared.person.notification.resource.delete"), notificationResourceUpdateTextToTranslate: TRANSLATE("shared.person.notification.resource.update"), }); } + + @Action(SharedPersonAction.Search) + search(ctx: StateContext<SharedPersonStateModel>, action: SharedPersonAction.Search): Observable<CollectionTyped<PersonExtended>> { + ctx.patchState({ + isLoadingCounter: ctx.getState().isLoadingCounter + 1, + }); + const map = new Map<string, string>(); + map.set("firstName", action.person.firstName); + map.set("lastName", action.person.lastName); + const queryParameters = new QueryParameters(); + queryParameters.search = { + searchItems: map, + }; + return this.apiService.get<PersonExtended>(this._urlResource, queryParameters) + .pipe( + tap(collection => ctx.dispatch(new SharedPersonAction.SearchSuccess(collection._data))), + catchError(error => { + ctx.dispatch(new SharedPersonAction.SearchFail()); + throw error; + }), + ); + } + + @Action(SharedPersonAction.SearchSuccess) + searchSuccess(ctx: StateContext<SharedPersonStateModel>, action: SharedPersonAction.SearchSuccess): void { + ctx.patchState({ + isLoadingCounter: ctx.getState().isLoadingCounter - 1, + listPersonMatching: action.list, + }); + } + + @Action(SharedPersonAction.SearchFail) + searchFail(ctx: StateContext<SharedPersonStateModel>, action: SharedPersonAction.SearchFail): void { + ctx.patchState({ + isLoadingCounter: ctx.getState().isLoadingCounter - 1, + }); + } } diff --git a/src/app/stores/person/app-person.action.ts b/src/app/stores/person/app-person.action.ts index cb916c2ffa0c263f9a6d95f51b1726eb3a41d1b7..d5a766b567bd25f5e50116017a6a5db9f353e6c1 100644 --- a/src/app/stores/person/app-person.action.ts +++ b/src/app/stores/person/app-person.action.ts @@ -84,6 +84,13 @@ export namespace AppPersonAction { @TypeDefaultAction(state) export class DeleteFail extends ResourceAction.DeleteFail { } + + export class AddCurrentPerson { + static readonly type: string = `[${state}] Add Current Person`; + + constructor(public model: Person) { + } + } } export const appPersonActionNameSpace: ResourceNameSpace = AppPersonAction; diff --git a/src/app/stores/person/app-person.state.ts b/src/app/stores/person/app-person.state.ts index ac0c6990958f6a0076f8b9e0e4147b8a17462dfa..c35713941346b5c6dab3e3bd2916b32c0eea80f1 100644 --- a/src/app/stores/person/app-person.state.ts +++ b/src/app/stores/person/app-person.state.ts @@ -2,10 +2,15 @@ import {Person} from "@app/generated-api"; import {AdminResourceApiEnum} from "@app/shared/enums/api.enum"; import {LocalStateEnum} from "@app/shared/enums/local-state.enum"; import {RoutesEnum} from "@app/shared/enums/routes.enum"; -import {appPersonActionNameSpace} from "@app/stores/person/app-person.action"; import { + AppPersonAction, + appPersonActionNameSpace, +} from "@app/stores/person/app-person.action"; +import { + Action, Actions, State, + StateContext, Store, } from "@ngxs/store"; import {UserExtended} from "@shared/models/business/user-extended.model"; @@ -44,4 +49,11 @@ export class AppPersonState extends ResourceState<UserExtended> { notificationResourceUpdateTextToTranslate: TRANSLATE("app.person.notification.resource.update"), }); } + + @Action(AppPersonAction.AddCurrentPerson) + getCurrentUserSuccess(ctx: StateContext<AppPersonStateModel>, action: AppPersonAction.AddCurrentPerson): void { + ctx.patchState({ + current: action.model, + }); + } } diff --git a/src/app/stores/user/app-user.state.ts b/src/app/stores/user/app-user.state.ts index a513b980f03f1da1573851c50bbd313e24ae429d..1056f1c2f7eb2b147821c0fdc4457cf2a8d581fd 100644 --- a/src/app/stores/user/app-user.state.ts +++ b/src/app/stores/user/app-user.state.ts @@ -1,6 +1,7 @@ import {AdminResourceApiEnum} from "@app/shared/enums/api.enum"; import {LocalStateEnum} from "@app/shared/enums/local-state.enum"; import {RoutesEnum} from "@app/shared/enums/routes.enum"; +import {AppPersonAction} from "@app/stores/person/app-person.action"; import { AppUserAction, appUserActionNameSpace, @@ -93,5 +94,7 @@ export class AppUserState extends ResourceState<UserExtended> { ctx.patchState({ current: action.user, }); + + ctx.dispatch(new AppPersonAction.AddCurrentPerson(action.user.person)); } } diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 1af6604a013aae3fe73fc0e6e5cedb79515a02f5..14c256b867406872397a0410a93e1dc0e993eb7e 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -619,6 +619,12 @@ }, "deposit": { "accessLevel": "Access Level", + "addMeAsAuthor": "add me as author", + "addPerson": { + "dialog": { + "error": "Can not retrieve the added person" + } + }, "authors": "Authors", "collectionBegin": "Data Collection Start Date", "collectionEnd": "Data Collection End Date", @@ -722,6 +728,29 @@ } }, "organizationUnit": "Organizational Unit", + "popup": { + "person": { + "alternative": { + "button": { + "close": "Close", + "submit": "Validate" + }, + "explanation": "You may be referring to an additional person below.\nPlease select the author to add to the deposit.", + "subtitle": { + "matchingPersons": "Matching persons", + "yourInput": "Your input" + }, + "title": "Existing people match!", + "warning": "Please do not create a duplicate." + }, + "button": { + "close": "Close", + "submit": "Add" + }, + "title": "Add missing person", + "warning": "Please check beforehand that the new person does not already exist before creating it" + } + }, "preservationPolicy": "Preservation Policy", "publicationDate": "Publication Date", "refresh": "Refresh", @@ -747,6 +776,7 @@ "title": "Title", "tooltips": { "accessLevel": "Sets the archive's access level: \n\n1) Public (open access) \n2) Restricted (Access limited to members of the organizational unit) \n3) Closed (Customized access)", + "addMissingPerson": "Add existing person", "authors": "Follows recommendations on authorship for scientific publications", "collectionBegin": "The date at which data collection began", "collectionEnd": "The date at which data collection ended", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index d575fb6c144572412a55b7049789367573f891be..14c256b867406872397a0410a93e1dc0e993eb7e 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -619,6 +619,12 @@ }, "deposit": { "accessLevel": "Access Level", + "addMeAsAuthor": "add me as author", + "addPerson": { + "dialog": { + "error": "Can not retrieve the added person" + } + }, "authors": "Authors", "collectionBegin": "Data Collection Start Date", "collectionEnd": "Data Collection End Date", @@ -722,6 +728,29 @@ } }, "organizationUnit": "Organizational Unit", + "popup": { + "person": { + "alternative": { + "button": { + "close": "Close", + "submit": "Validate" + }, + "explanation": "You may be referring to an additional person below.\nPlease select the author to add to the deposit.", + "subtitle": { + "matchingPersons": "Matching persons", + "yourInput": "Your input" + }, + "title": "Existing people match!", + "warning": "Please do not create a duplicate." + }, + "button": { + "close": "Close", + "submit": "Add" + }, + "title": "Add missing person", + "warning": "Please check beforehand that the new person does not already exist before creating it" + } + }, "preservationPolicy": "Preservation Policy", "publicationDate": "Publication Date", "refresh": "Refresh", @@ -747,6 +776,7 @@ "title": "Title", "tooltips": { "accessLevel": "Sets the archive's access level: \n\n1) Public (open access) \n2) Restricted (Access limited to members of the organizational unit) \n3) Closed (Customized access)", + "addMissingPerson": "Add existing person", "authors": "Follows recommendations on authorship for scientific publications", "collectionBegin": "The date at which data collection began", "collectionEnd": "The date at which data collection ended", @@ -804,7 +834,8 @@ "browsing": { "archive": { "noArchive": "No archive for the selected organizational unit", - "noOrgUnitSelected": "Select an organizational unit"} + "noOrgUnitSelected": "Select an organizational unit" + } } }, "invalid ORCID": "invalid ORCID", diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json index bac61fa1cee751f884caa02893584329783e5360..03e1f9a304ae8e15407ad5b4e44a6e4254558308 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -619,6 +619,12 @@ }, "deposit": { "accessLevel": "Niveau d'accès", + "addMeAsAuthor": "m'ajouter en tant qu'auteur", + "addPerson": { + "dialog": { + "error": "Impossible de récupérer la personne ajoutée" + } + }, "authors": "Auteurs", "collectionBegin": "Début de la collecte des données", "collectionEnd": "Fin de la collecte des données", @@ -722,6 +728,29 @@ } }, "organizationUnit": "Unité organisationnelle", + "popup": { + "person": { + "alternative": { + "button": { + "close": "Annuler", + "submit": "Valider" + }, + "explanation": "Vous faites peut-être référence à une personne figurante ci-dessous.\nMerci de séléctionner l'auteur souhaité à ajouter au dépôt.", + "subtitle": { + "matchingPersons": "Personnes pouvant correspondre", + "yourInput": "Votre saisie" + }, + "title": "Des personnes existantes correspondent !", + "warning": "Merci de ne pas créer de doublon." + }, + "button": { + "close": "Fermer", + "submit": "Ajouter" + }, + "title": "Ajout d'une personne manquante", + "warning": "Veuillez vérifier au préalable que la personne recherchée n'existe pas déjà avant de la créer." + } + }, "preservationPolicy": "Politique de préservation", "publicationDate": "Date de publication", "refresh": "Rafraichir", @@ -747,6 +776,7 @@ "title": "Titre", "tooltips": { "accessLevel": "Niveau d'accès de l'archive: \n\n1) Public (open access)\n2) Accès restreint (accès limité aux membres de l'unité organisationnelle)\n3) Fermé (accès personnalisé)", + "addMissingPerson": "Ajouter une personne existante", "authors": "Suivre les recommandations sur la paternité d'une publication scientifique", "collectionBegin": "Début de la période de collecte des données", "collectionEnd": "Fin de la période de collecte des données",