diff --git a/client/src/app/give-feedback/give-feedback/give-feedback.component.html b/client/src/app/give-feedback/give-feedback/give-feedback.component.html index 97520b70..dab6c1f7 100644 --- a/client/src/app/give-feedback/give-feedback/give-feedback.component.html +++ b/client/src/app/give-feedback/give-feedback/give-feedback.component.html @@ -39,7 +39,11 @@

- + @for (result of queryResults$ | async; track result.email) { -
+
{{ result.displayName }} -
{{ result.email }}
+
{{ result.email }}
+ + @if (result.managerEmail && showManagerEmail()) { +
Manager: {{ result.managerEmail }}
+ }
diff --git a/client/src/app/shared/autocomplete-email/autocomplete-email/autocomplete-email.component.ts b/client/src/app/shared/autocomplete-email/autocomplete-email/autocomplete-email.component.ts index b893bcd3..034b4f75 100644 --- a/client/src/app/shared/autocomplete-email/autocomplete-email/autocomplete-email.component.ts +++ b/client/src/app/shared/autocomplete-email/autocomplete-email/autocomplete-email.component.ts @@ -29,6 +29,8 @@ import { ValidationErrorMessagePipe } from '../../validation/validation-error-me export class AutocompleteEmailComponent { forManager = input(false, { transform: booleanAttribute }); + showManagerEmail = input(false, { transform: booleanAttribute }); + private allowedEmailDomainsValidator = allowedEmailDomainsValidatorFactory(inject(ALLOWED_EMAIL_DOMAINS)); email = input( diff --git a/client/src/app/shared/autocomplete-email/multi-autocomplete-email/multi-autocomplete-email.component.html b/client/src/app/shared/autocomplete-email/multi-autocomplete-email/multi-autocomplete-email.component.html index 35f8d340..69e088bd 100644 --- a/client/src/app/shared/autocomplete-email/multi-autocomplete-email/multi-autocomplete-email.component.html +++ b/client/src/app/shared/autocomplete-email/multi-autocomplete-email/multi-autocomplete-email.component.html @@ -25,13 +25,19 @@ @for (result of queryResults$ | async; track result.email) { -
+
{{ 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; + }); };