+
{{ result.displayName }}
-
{{ result.email }}
+
{{ result.email }}
+
+ @if (result.managerEmail && showManagerEmail()) {
+
+ Manager: {{ result.managerEmail }}
+
+ }
diff --git a/client/src/app/shared/autocomplete-email/multi-autocomplete-email/multi-autocomplete-email.component.ts b/client/src/app/shared/autocomplete-email/multi-autocomplete-email/multi-autocomplete-email.component.ts
index ff3cf667..0e2f88cb 100644
--- a/client/src/app/shared/autocomplete-email/multi-autocomplete-email/multi-autocomplete-email.component.ts
+++ b/client/src/app/shared/autocomplete-email/multi-autocomplete-email/multi-autocomplete-email.component.ts
@@ -1,6 +1,15 @@
import { COMMA } from '@angular/cdk/keycodes';
import { AsyncPipe } from '@angular/common';
-import { Component, DestroyRef, ViewEncapsulation, afterNextRender, inject, input, viewChild } from '@angular/core';
+import {
+ Component,
+ DestroyRef,
+ ViewEncapsulation,
+ afterNextRender,
+ booleanAttribute,
+ inject,
+ input,
+ viewChild,
+} from '@angular/core';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatAutocomplete, MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipEditedEvent, MatChipInput, MatChipsModule } from '@angular/material/chips';
@@ -55,6 +64,8 @@ export class MultiAutocompleteEmailComponent {
isInvalidEmail = input<((email: string) => boolean) | undefined>(undefined);
+ showManagerEmail = input(false, { transform: booleanAttribute });
+
matChipInput = viewChild.required(MatChipInput);
matAutocomplete = viewChild.required(MatAutocomplete);
diff --git a/client/src/app/shared/people/people.types.ts b/client/src/app/shared/people/people.types.ts
index 5b2c5ac6..52799ebb 100644
--- a/client/src/app/shared/people/people.types.ts
+++ b/client/src/app/shared/people/people.types.ts
@@ -2,4 +2,5 @@ export type Person = {
email: string;
displayName?: string;
photoUrl?: string;
+ managerEmail?: string;
};
diff --git a/server/src/core/google-apis/google-apis.service.ts b/server/src/core/google-apis/google-apis.service.ts
index eae636cb..ef058a41 100644
--- a/server/src/core/google-apis/google-apis.service.ts
+++ b/server/src/core/google-apis/google-apis.service.ts
@@ -4,6 +4,7 @@ import { ConfigService } from '@nestjs/config';
import { AppConfig } from '../config';
import { JWT_SCOPES, JWT_SUBJECT } from './google-apis.config';
import { Person } from './google-apis.types';
+import { mapGoogleUserRelationsToZenikaManagerEmail } from './google-apis.utils';
@Injectable()
export class GoogleApisService {
@@ -30,7 +31,7 @@ export class GoogleApisService {
const { data } = await admin('directory_v1').users.list({
access_token: accessToken,
- fields: 'users(primaryEmail, name, thumbnailPhotoUrl), nextPageToken',
+ fields: 'users(primaryEmail, name, thumbnailPhotoUrl, relations), nextPageToken',
viewType: 'domain_public',
domain: 'zenika.com',
pageToken,
@@ -47,6 +48,7 @@ export class GoogleApisService {
email: user.primaryEmail,
displayName: user.name?.fullName ?? undefined,
photoUrl: user.thumbnailPhotoUrl ?? undefined,
+ managerEmail: mapGoogleUserRelationsToZenikaManagerEmail(user.relations),
});
}
return _persons;
diff --git a/server/src/core/google-apis/google-apis.types.ts b/server/src/core/google-apis/google-apis.types.ts
index 5b2c5ac6..52799ebb 100644
--- a/server/src/core/google-apis/google-apis.types.ts
+++ b/server/src/core/google-apis/google-apis.types.ts
@@ -2,4 +2,5 @@ export type Person = {
email: string;
displayName?: string;
photoUrl?: string;
+ managerEmail?: string;
};
diff --git a/server/src/core/google-apis/google-apis.utils.ts b/server/src/core/google-apis/google-apis.utils.ts
new file mode 100644
index 00000000..0841ef79
--- /dev/null
+++ b/server/src/core/google-apis/google-apis.utils.ts
@@ -0,0 +1,23 @@
+/**
+ * Inside Zenika's organization, the `Schema$User['relations']` Google API field, follows a particular interface.
+ *
+ * import type { admin_directory_v1 } from '@googleapis/admin';
+ *
+ * `admin_directory_v1.Schema$User['relations']`
+ * is equal to:
+ * `{ type: 'manager'; value: string }[]`
+ */
+
+type ManagerRelation = {
+ type: 'manager';
+ /**
+ * The manager email.
+ */
+ value: string;
+};
+
+const isManagerRelation = (relation: unknown): relation is ManagerRelation =>
+ Object.prototype.toString.call(relation) === '[object Object]' && (relation as ManagerRelation).type === 'manager';
+
+export const mapGoogleUserRelationsToZenikaManagerEmail = (relations: unknown) =>
+ Array.isArray(relations) ? relations.find(isManagerRelation)?.value : undefined;
diff --git a/server/src/people/people-cache.utils.ts b/server/src/people/people-cache.utils.ts
index 5b3d2b90..bdb480b4 100644
--- a/server/src/people/people-cache.utils.ts
+++ b/server/src/people/people-cache.utils.ts
@@ -43,5 +43,8 @@ export const searchPersons = (query: string, searchablePersons: SearchablePerson
searchTokens.some((searchToken) => searchToken.startsWith(querySearchToken)),
),
)
- .map(({ email, displayName, photoUrl }) => ({ email, displayName, photoUrl }));
+ .map((searchablePerson) => {
+ const { searchTokens, ...person } = searchablePerson; // eslint-disable-line @typescript-eslint/no-unused-vars
+ return person;
+ });
};