From a6a58949bba9fbd8aa21bf7caedd6aafbd3bd116 Mon Sep 17 00:00:00 2001
From: Nicolas Rod <Nicolas.Rod@unige.ch>
Date: Mon, 10 Feb 2025 15:29:51 +0100
Subject: [PATCH 1/2] feat(ORCID): [DLCM-2761] Show links to external websites
 supporting ORCID

---
 src/app/models/index.ts                       |  9 ++++-
 .../shared-person-overlay.presentational.html | 16 ++++++++
 .../shared-person-overlay.presentational.scss | 24 ++++++++++++
 .../shared-person-overlay.presentational.ts   | 23 +++++++++++
 .../shared/enums/api-resource-name.enum.ts    |  1 +
 src/app/shared/enums/label-translate.enum.ts  |  1 +
 .../stores/person/shared-person.action.ts     | 21 ++++++++++
 .../stores/person/shared-person.state.ts      | 38 +++++++++++++++++++
 src/assets/i18n/de.json                       |  1 +
 src/assets/i18n/en.json                       |  1 +
 src/assets/i18n/fr.json                       |  1 +
 11 files changed, 135 insertions(+), 1 deletion(-)

diff --git a/src/app/models/index.ts b/src/app/models/index.ts
index f123f397b..616d5b368 100644
--- a/src/app/models/index.ts
+++ b/src/app/models/index.ts
@@ -86,6 +86,7 @@ import {FacetResult as FacetResultPartial} from "../generated-api/model/facet-re
 import {FileFormat as FileFormatPartial} from "../generated-api/model/file-format.partial.model";
 import {FileList as FileListPartial} from "../generated-api/model/file-list.partial.model";
 import {FundingAgency as FundingAgencyPartial} from "../generated-api/model/funding-agency.partial.model";
+import {I18nLink as I18nLinkPartial} from "../generated-api/model/i18n-link.partial.model";
 import {Institution as InstitutionPartial} from "../generated-api/model/institution.partial.model";
 import {JobExecutionReportLine as JobExecutionReportLinePartial} from "../generated-api/model/job-execution-report-line.partial.model";
 import {JobExecutionReport as JobExecutionReportPartial} from "../generated-api/model/job-execution-report.partial.model";
@@ -95,12 +96,12 @@ import {Language as LanguagePartial} from "../generated-api/model/language.parti
 import {License as LicensePartial} from "../generated-api/model/license.partial.model";
 import {Link as LinkPartial} from "../generated-api/model/link.partial.model";
 import {LoginInfo as LoginInfoPartial} from "../generated-api/model/login-info.partial.model";
-
 import {MetadataType as MetadataTypePartial} from "../generated-api/model/metadata-type.partial.model";
 import {NotificationType as NotificationTypePartial} from "../generated-api/model/notification-type.partial.model";
 import {Notification as NotificationPartial} from "../generated-api/model/notification.partial.model";
 import {OAIMetadataPrefix as OaiMetadataPrefixPartial} from "../generated-api/model/oai-metadata-prefix.partial.model";
 import {OAISet as OaiSetPartial} from "../generated-api/model/oai-set.partial.model";
+import {OrcidWebsiteDTO as OrcidWebsiteDTOPartial} from "../generated-api/model/orcid-website-dto.partial.model";
 import {OrderArchive as OrderArchivePartial} from "../generated-api/model/order-archive.partial.model";
 import {OrderSubsetItem as OrderSubsetItemPartial} from "../generated-api/model/order-subset-item.partial.model";
 import {Order as OrderPartial} from "../generated-api/model/order.partial.model";
@@ -132,6 +133,7 @@ import {VirusCheck as AipVirusCheckPartial} from "../generated-api/model/virus-c
 import RatingTypeEnum = Enums.Archive.RatingTypeEnum;
 import IdentifiersEnum = Enums.IdentifiersEnum;
 
+
 /* eslint-enable no-restricted-imports */
 
 export type BaseResourceExtended = {
@@ -403,6 +405,8 @@ export type FundingAgency = OverrideType<FundingAgencyPartial, {
   logo?: SolidifyFile;
 }> & BaseResourceLogo & BaseResourceExtended;
 
+export type I18nLink = OverrideType<I18nLinkPartial>;
+
 export type Institution = OverrideType<InstitutionPartial, {
   organizationalUnits?: OrganizationalUnit[];
   identifiers?: MappingObject<IdentifiersEnum, string>;
@@ -481,6 +485,8 @@ export type Order = OverrideType<OrderPartial, {
   orderStatus?: Enums.Order.StatusEnum;
 }> & BaseResourceExtended;
 
+export type OrcidWebsiteDTO = OverrideType<OrcidWebsiteDTOPartial>;
+
 export type OrderArchive = OverrideType<OrderArchivePartial, {
   archive?: Aip;
   metadata?: ArchiveMetadata;
@@ -517,6 +523,7 @@ export type Person = OverrideType<PersonPartial, {
   notificationTypes?: NotificationType[];
   avatar?: PersonAvatar;
   searchCriterias?: SearchCriteria[];
+  orcidLinks: I18nLink[];
 }> & BaseResourceAvatar & BaseResourceExtended;
 
 export type PreservationJob = OverrideType<PreservationJobPartial, {
diff --git a/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.html b/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.html
index 50be37421..8ad20b04c 100644
--- a/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.html
+++ b/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.html
@@ -48,5 +48,21 @@
         ></solidify-icon>
       </li>
     </ul>
+
+    <div *ngIf="data.orcidLinks | isNonEmptyArray"
+         class="orcid-websites-zone"
+    >
+      <div class="title">{{ labelTranslateEnum.orcidExternalWebsites | translate }}:</div>
+
+      <ul class="websites">
+        <li *ngFor="let orcidLink of data.orcidLinks"
+            class="website"
+        >
+          <a [href]="orcidLink.url" target="_blank" class="website-name"
+             solidifyTooltipOnEllipsis
+          >{{ orcidLink.text }}</a>
+        </li>
+      </ul>
+    </div>
   </div>
 </div>
diff --git a/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.scss b/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.scss
index b1c91d306..b5436f245 100644
--- a/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.scss
+++ b/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.scss
@@ -55,5 +55,29 @@
         }
       }
     }
+
+    .orcid-websites-zone {
+      .title {
+        font-size: 14px;
+      }
+
+      .websites {
+        margin-left: 5px;
+        display: grid;
+        grid-gap: 2px;
+
+        .website {
+          display: grid;
+          grid-template-columns: 1fr;
+          grid-gap: 5px;
+          align-items: center;
+
+          .website-name {
+            font-size: 12px;
+            @include truncate-with-ellipsis;
+          }
+        }
+      }
+    }
   }
 }
diff --git a/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.ts b/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.ts
index 39724cfe6..523177579 100644
--- a/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.ts
+++ b/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.ts
@@ -31,6 +31,7 @@ import {
 import {Enums} from "@enums";
 import {environment} from "@environments/environment";
 import {
+  I18nLink,
   Institution,
   Person,
   User,
@@ -51,6 +52,7 @@ import {
   isNotNullNorUndefined,
   isNullOrUndefined,
   isNullOrUndefinedOrWhiteString,
+  isNotNullNorUndefinedNorWhiteString,
   isTrue,
   MappingObjectUtil,
   ObjectUtil,
@@ -58,6 +60,7 @@ import {
   ResourceFileNameSpace,
   SsrUtil,
   StoreUtil,
+  MemoizedUtil,
 } from "solidify-frontend";
 
 @Component({
@@ -131,6 +134,26 @@ export class SharedPersonOverlayPresentational extends AbstractOverlayPresentati
         },
       ));
     }
+
+    if (isNotNullNorUndefinedNorWhiteString(this.data.orcid)) {
+      this.subscribe(StoreUtil.dispatchActionAndWaitForSubActionCompletion(this._store, this._actions$,
+        new SharedPersonAction.GetExternalOrcidWebsites(this.data.orcid), SharedPersonAction.GetExternalOrcidWebsitesSuccess,
+        result => {
+          this.data = ObjectUtil.clone(this.data);
+          const currentLanguage = MemoizedUtil.selectSnapshot(this._store, environment.appState, state => state.appLanguage);
+          const orcidLinks: I18nLink[] = [];
+          for (const website of result.orcidWebsites) {
+            for (const link of website.links) {
+              if (link.languageCode === currentLanguage) {
+                orcidLinks.push(link);
+              }
+            }
+          }
+          this.data.orcidLinks = orcidLinks;
+          this._changeDetector.detectChanges();
+        },
+      ));
+    }
   }
 }
 
diff --git a/src/app/shared/enums/api-resource-name.enum.ts b/src/app/shared/enums/api-resource-name.enum.ts
index 6842080a6..36569e4fa 100644
--- a/src/app/shared/enums/api-resource-name.enum.ts
+++ b/src/app/shared/enums/api-resource-name.enum.ts
@@ -79,6 +79,7 @@ enum ApiResourceNameExtendEnum {
   SCHEDULED_TASK = "scheduled-tasks",
   GLOBAL_BANNERS = "global-banners",
   ORCID_SYNCHRONIZATION = "orcid-synchronizations",
+  ORCID_EXTERNAL_WEBSITES = "external-websites",
 
   // Miscellaneous
   SCHEMA = "schema",
diff --git a/src/app/shared/enums/label-translate.enum.ts b/src/app/shared/enums/label-translate.enum.ts
index adab2083f..0400e039f 100644
--- a/src/app/shared/enums/label-translate.enum.ts
+++ b/src/app/shared/enums/label-translate.enum.ts
@@ -385,6 +385,7 @@ export class LabelTranslateEnum {
   static desiredRole: string = MARK_AS_TRANSLATABLE("general.label.desiredRole");
   static orcidAuthenticated: string = MARK_AS_TRANSLATABLE("general.label.orcidAuthenticated");
   static orcidNotAuthenticated: string = MARK_AS_TRANSLATABLE("general.label.orcidNotAuthenticated");
+  static orcidExternalWebsites: string = MARK_AS_TRANSLATABLE("general.label.orcidExternalWebsites");
   static data: string = MARK_AS_TRANSLATABLE("general.label.data");
   static reason: string = MARK_AS_TRANSLATABLE("general.label.reason");
   static reasonForRejection: string = MARK_AS_TRANSLATABLE("general.label.reasonForRejection");
diff --git a/src/app/shared/stores/person/shared-person.action.ts b/src/app/shared/stores/person/shared-person.action.ts
index 9336b5754..e8fc90d32 100644
--- a/src/app/shared/stores/person/shared-person.action.ts
+++ b/src/app/shared/stores/person/shared-person.action.ts
@@ -23,6 +23,7 @@
 
 import {
   Institution,
+  OrcidWebsiteDTO,
   Person,
 } from "@models";
 import {StateEnum} from "@shared/enums/state.enum";
@@ -298,6 +299,26 @@ export namespace SharedPersonAction {
   export class SearchPersonInstitutionFail extends BaseSubActionFail<SearchPersonInstitution> {
     static readonly type: string = `[${state}] Search Person Institution Fail`;
   }
+
+  export class GetExternalOrcidWebsites extends BaseAction {
+    static readonly type: string = `[${state}] GetExternalOrcidWebsites`;
+
+    constructor(public orcid: string) {
+      super();
+    }
+  }
+
+  export class GetExternalOrcidWebsitesSuccess extends BaseSubActionSuccess<GetExternalOrcidWebsites> {
+    static readonly type: string = `[${state}] Get External Orcid Websites Success`;
+
+    constructor(public parentAction: GetExternalOrcidWebsites, public orcidWebsites: OrcidWebsiteDTO[]) {
+      super(parentAction);
+    }
+  }
+
+  export class GetExternalOrcidWebsitesFail extends BaseSubActionFail<GetExternalOrcidWebsites> {
+    static readonly type: string = `[${state}] Get External Orcid Websites Fail`;
+  }
 }
 
 export const sharedPersonActionNameSpace: ResourceFileNameSpace = SharedPersonAction;
diff --git a/src/app/shared/stores/person/shared-person.state.ts b/src/app/shared/stores/person/shared-person.state.ts
index 76b1708dc..858529803 100644
--- a/src/app/shared/stores/person/shared-person.state.ts
+++ b/src/app/shared/stores/person/shared-person.state.ts
@@ -30,6 +30,7 @@ import {
 import {environment} from "@environments/environment";
 import {
   Institution,
+  OrcidWebsiteDTO,
   Person,
 } from "@models";
 import {
@@ -79,6 +80,7 @@ import {ApiActionNameEnum} from "../../enums/api-action-name.enum";
 
 export interface SharedPersonStateModel extends ResourceFileStateModel<Person> {
   listPersonMatching: Person[];
+  orcidWebsites: OrcidWebsiteDTO[];
 }
 
 @Injectable()
@@ -87,6 +89,7 @@ export interface SharedPersonStateModel extends ResourceFileStateModel<Person> {
   defaults: {
     ...defaultResourceFileStateInitValue(),
     listPersonMatching: [],
+    orcidWebsites: [],
     queryParameters: new QueryParameters(environment.defaultEnumValuePageSizeOption),
   },
   children: [
@@ -111,6 +114,10 @@ export class SharedPersonState extends ResourceFileState<SharedPersonStateModel,
     return ApiEnum.adminPeople;
   }
 
+  protected get _urlOrcid(): string {
+    return ApiEnum.adminOrcid;
+  }
+
   protected get _urlFileResource(): string {
     return this._urlResource;
   }
@@ -304,4 +311,35 @@ export class SharedPersonState extends ResourceFileState<SharedPersonStateModel,
         }),
       );
   }
+
+  @Action(SharedPersonAction.GetExternalOrcidWebsites)
+  getExternalOrcidWebsites(ctx: SolidifyStateContext<SharedPersonStateModel>, action: SharedPersonAction.GetExternalOrcidWebsites): Observable<any> {
+    ctx.patchState({
+      isLoadingCounter: ctx.getState().isLoadingCounter + 1,
+    });
+    return this._apiService.getList<OrcidWebsiteDTO>(`${this._urlOrcid}/${action.orcid}/${ApiResourceNameEnum.ORCID_EXTERNAL_WEBSITES}`, null)
+      .pipe(
+        tap(result => {
+          ctx.dispatch(new SharedPersonAction.GetExternalOrcidWebsitesSuccess(action, result));
+        }),
+        catchError((error: SolidifyHttpErrorResponseModel) => {
+          ctx.dispatch(new SharedPersonAction.GetExternalOrcidWebsitesFail(action));
+          throw new SolidifyStateError(this, error);
+        }),
+      );
+  }
+
+  @Action(SharedPersonAction.GetExternalOrcidWebsitesSuccess)
+  getExternalOrcidWebsitesSuccess(ctx: SolidifyStateContext<SharedPersonStateModel>, action: SharedPersonAction.GetExternalOrcidWebsitesSuccess): void {
+    ctx.patchState({
+      orcidWebsites: action.orcidWebsites,
+    });
+  }
+
+  @Action(SharedPersonAction.GetExternalOrcidWebsitesFail)
+  getExternalOrcidWebsitesFail(ctx: SolidifyStateContext<SharedPersonStateModel>, action: SharedPersonAction.GetExternalOrcidWebsitesFail): void {
+    ctx.patchState({
+      orcidWebsites: undefined,
+    });
+  }
 }
diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json
index 1903967a2..6510b5464 100644
--- a/src/assets/i18n/de.json
+++ b/src/assets/i18n/de.json
@@ -1552,6 +1552,7 @@
       "optional": "Optional",
       "orcid": "ORCID",
       "orcidAuthenticated": "ORCID-authentifiziert",
+      "orcidExternalWebsites": "general.label.orcidExternalWebsites",
       "orcidNotAuthenticated": "ORCID nicht authentifiziert",
       "orderInError": "Fehlerhafte Reihenfolge",
       "orderInProgress": "Datenanfrage in Bearbeitung",
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index a0eb43673..a21aa9d52 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -1552,6 +1552,7 @@
       "optional": "Optional",
       "orcid": "ORCID",
       "orcidAuthenticated": "ORCID Authenticated",
+      "orcidExternalWebsites": "Affiliated websites supporting ORCID",
       "orcidNotAuthenticated": "ORCID not authenticated",
       "orderInError": "Order in error",
       "orderInProgress": "Order in progress",
diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json
index 224bbcc0e..ac050455c 100644
--- a/src/assets/i18n/fr.json
+++ b/src/assets/i18n/fr.json
@@ -1552,6 +1552,7 @@
       "optional": "Facultatif",
       "orcid": "ORCID",
       "orcidAuthenticated": "Authentifié par l'ORCID",
+      "orcidExternalWebsites": "Site affiliés supportant ORCID",
       "orcidNotAuthenticated": "ORCID non authentifié",
       "orderInError": "La demande est en erreur",
       "orderInProgress": "La demande est en cours",
-- 
GitLab


From 3fc975ad33c2d66a8d430e41ec1f3bf5d8edb310 Mon Sep 17 00:00:00 2001
From: Florent Poittevin <florent.poittevin@unige.ch>
Date: Tue, 11 Feb 2025 16:29:38 +0100
Subject: [PATCH 2/2] chore: fix MR

---
 src/app/models/index.ts                               |  3 +--
 .../shared-person-overlay.presentational.html         | 11 +++++++----
 .../shared-person-overlay.presentational.ts           | 11 ++++++-----
 src/app/shared/stores/person/shared-person.action.ts  |  2 +-
 4 files changed, 15 insertions(+), 12 deletions(-)

diff --git a/src/app/models/index.ts b/src/app/models/index.ts
index 616d5b368..d85d6f26d 100644
--- a/src/app/models/index.ts
+++ b/src/app/models/index.ts
@@ -133,7 +133,6 @@ import {VirusCheck as AipVirusCheckPartial} from "../generated-api/model/virus-c
 import RatingTypeEnum = Enums.Archive.RatingTypeEnum;
 import IdentifiersEnum = Enums.IdentifiersEnum;
 
-
 /* eslint-enable no-restricted-imports */
 
 export type BaseResourceExtended = {
@@ -523,7 +522,7 @@ export type Person = OverrideType<PersonPartial, {
   notificationTypes?: NotificationType[];
   avatar?: PersonAvatar;
   searchCriterias?: SearchCriteria[];
-  orcidLinks: I18nLink[];
+  orcidLinks?: I18nLink[];
 }> & BaseResourceAvatar & BaseResourceExtended;
 
 export type PreservationJob = OverrideType<PreservationJobPartial, {
diff --git a/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.html b/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.html
index 8ad20b04c..bef457fdc 100644
--- a/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.html
+++ b/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.html
@@ -17,7 +17,8 @@
   <div class="informations">
     <div class="fullname"
          solidifyTooltipOnEllipsis
-    >{{data.firstName}} {{data.lastName}}</div>
+    >{{data.firstName}} {{data.lastName}}
+    </div>
     <div *ngIf="data.orcid | isNotNullNorUndefinedNorWhiteString"
          class="orcid-wrapper"
          (click)="orcidPresentational.navigateToOrcid()"
@@ -52,15 +53,17 @@
     <div *ngIf="data.orcidLinks | isNonEmptyArray"
          class="orcid-websites-zone"
     >
-      <div class="title">{{ labelTranslateEnum.orcidExternalWebsites | translate }}:</div>
+      <div class="title">{{labelTranslateEnum.orcidExternalWebsites | translate}}:</div>
 
       <ul class="websites">
         <li *ngFor="let orcidLink of data.orcidLinks"
             class="website"
         >
-          <a [href]="orcidLink.url" target="_blank" class="website-name"
+          <a [href]="orcidLink.url"
+             target="_blank"
+             class="website-name"
              solidifyTooltipOnEllipsis
-          >{{ orcidLink.text }}</a>
+          >{{orcidLink.text}}</a>
         </li>
       </ul>
     </div>
diff --git a/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.ts b/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.ts
index 523177579..b9fd5c710 100644
--- a/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.ts
+++ b/src/app/shared/components/presentationals/shared-person-overlay/shared-person-overlay.presentational.ts
@@ -137,18 +137,19 @@ export class SharedPersonOverlayPresentational extends AbstractOverlayPresentati
 
     if (isNotNullNorUndefinedNorWhiteString(this.data.orcid)) {
       this.subscribe(StoreUtil.dispatchActionAndWaitForSubActionCompletion(this._store, this._actions$,
-        new SharedPersonAction.GetExternalOrcidWebsites(this.data.orcid), SharedPersonAction.GetExternalOrcidWebsitesSuccess,
+        new SharedPersonAction.GetExternalOrcidWebsites(this.data.orcid),
+        SharedPersonAction.GetExternalOrcidWebsitesSuccess,
         result => {
           this.data = ObjectUtil.clone(this.data);
           const currentLanguage = MemoizedUtil.selectSnapshot(this._store, environment.appState, state => state.appLanguage);
           const orcidLinks: I18nLink[] = [];
-          for (const website of result.orcidWebsites) {
-            for (const link of website.links) {
+          result.orcidWebsites.forEach(website => {
+            website.links.forEach(link => {
               if (link.languageCode === currentLanguage) {
                 orcidLinks.push(link);
               }
-            }
-          }
+            });
+          });
           this.data.orcidLinks = orcidLinks;
           this._changeDetector.detectChanges();
         },
diff --git a/src/app/shared/stores/person/shared-person.action.ts b/src/app/shared/stores/person/shared-person.action.ts
index e8fc90d32..c2a16cce7 100644
--- a/src/app/shared/stores/person/shared-person.action.ts
+++ b/src/app/shared/stores/person/shared-person.action.ts
@@ -301,7 +301,7 @@ export namespace SharedPersonAction {
   }
 
   export class GetExternalOrcidWebsites extends BaseAction {
-    static readonly type: string = `[${state}] GetExternalOrcidWebsites`;
+    static readonly type: string = `[${state}] Get External Orcid Websites`;
 
     constructor(public orcid: string) {
       super();
-- 
GitLab