From b5568d43fa2908e1d5b0c7f9e12aa3b0fa1afb70 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Mon, 22 Jul 2024 10:07:43 -0400 Subject: [PATCH] [ACS-8433] ACA: User Profile Service (#3957) --- .github/workflows/pull-request.yml | 9 +- app/src/app/app.component.html | 1 - app/src/app/app.components.ts | 4 +- karma.conf.js | 2 +- .../common/user-info/user-info.component.ts | 8 +- .../sidenav/user-menu/user-menu.component.ts | 9 +- .../toggle-join-library-button.component.ts | 20 ++-- .../components/trashcan/trashcan.component.ts | 16 +++- .../src/lib/store/reducers/app.reducer.ts | 27 +----- .../src/lib/services/app.service.spec.ts | 91 +++++++------------ .../src/lib/services/app.service.ts | 61 +++++++------ .../src/lib/services/user-profile.service.ts | 81 +++++++++++++++++ projects/aca-shared/src/public-api.ts | 1 + .../store/src/actions/app.actions.ts | 5 +- .../store/src/selectors/app.selectors.ts | 1 + 15 files changed, 183 insertions(+), 153 deletions(-) create mode 100644 projects/aca-shared/src/lib/services/user-profile.service.ts diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 3c23368e84..9e17bb15a0 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -70,20 +70,13 @@ jobs: unit-tests: needs: [lint, build] - name: "Unit tests: ${{ matrix.unit-tests.name }}" + name: "Unit tests" runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - unit-tests: - - name: "aca-content" - - name: "aca-shared" steps: - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 - - name: node uses: actions/setup-node@v3 with: diff --git a/app/src/app/app.component.html b/app/src/app/app.component.html index 862e06c534..0680b43f9c 100644 --- a/app/src/app/app.component.html +++ b/app/src/app/app.component.html @@ -1,2 +1 @@ -

{{ pageHeading | async | translate }}

diff --git a/app/src/app/app.components.ts b/app/src/app/app.components.ts index 94d6e40038..9c6b90a6dd 100644 --- a/app/src/app/app.components.ts +++ b/app/src/app/app.components.ts @@ -23,7 +23,7 @@ */ import { Component, ViewEncapsulation } from '@angular/core'; -import { Observable, Subject } from 'rxjs'; +import { Subject } from 'rxjs'; import { AppService } from '@alfresco/aca-shared'; @Component({ @@ -34,10 +34,8 @@ import { AppService } from '@alfresco/aca-shared'; }) export class AppComponent { onDestroy$: Subject = new Subject(); - pageHeading: Observable; constructor(private appService: AppService) { - this.pageHeading = this.appService.pageHeading$; this.appService.init(); } } diff --git a/karma.conf.js b/karma.conf.js index cfc277142e..99002e4f87 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -58,7 +58,7 @@ module.exports = () => { check: { global: { statements: 75, - branches: 67, + branches: 65, functions: 71, lines: 74 } diff --git a/projects/aca-content/src/lib/components/common/user-info/user-info.component.ts b/projects/aca-content/src/lib/components/common/user-info/user-info.component.ts index 101c69a459..98d4a246a8 100644 --- a/projects/aca-content/src/lib/components/common/user-info/user-info.component.ts +++ b/projects/aca-content/src/lib/components/common/user-info/user-info.component.ts @@ -27,8 +27,7 @@ import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { MatMenuModule } from '@angular/material/menu'; import { TranslateModule } from '@ngx-translate/core'; -import { AppStore, getUserProfile } from '@alfresco/aca-shared/store'; -import { Store } from '@ngrx/store'; +import { UserProfileService } from '@alfresco/aca-shared'; @Component({ standalone: true, @@ -39,6 +38,7 @@ import { Store } from '@ngrx/store'; encapsulation: ViewEncapsulation.None }) export class UserInfoComponent { - private store = inject>(Store); - user$ = this.store.select(getUserProfile); + private userProfileService = inject(UserProfileService); + + user$ = this.userProfileService.userProfile$; } diff --git a/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.ts b/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.ts index 70f064a914..d010671132 100644 --- a/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.ts +++ b/projects/aca-content/src/lib/components/sidenav/user-menu/user-menu.component.ts @@ -28,9 +28,7 @@ import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { TranslateModule } from '@ngx-translate/core'; import { MatMenuModule } from '@angular/material/menu'; -import { ToolbarMenuItemComponent } from '@alfresco/aca-shared'; -import { AppStore, getUserProfile } from '@alfresco/aca-shared/store'; -import { Store } from '@ngrx/store'; +import { ToolbarMenuItemComponent, UserProfileService } from '@alfresco/aca-shared'; @Component({ standalone: true, @@ -42,8 +40,9 @@ import { Store } from '@ngrx/store'; host: { class: 'aca-user-menu' } }) export class UserMenuComponent implements OnInit { - private store = inject>(Store); - user$ = this.store.select(getUserProfile); + private userProfileService = inject(UserProfileService); + + user$ = this.userProfileService.userProfile$; @Input() actionRef: ContentActionRef; diff --git a/projects/aca-content/src/lib/components/toolbar/toggle-join-library/toggle-join-library-button.component.ts b/projects/aca-content/src/lib/components/toolbar/toggle-join-library/toggle-join-library-button.component.ts index 97f7ac0e85..2205991630 100644 --- a/projects/aca-content/src/lib/components/toolbar/toggle-join-library/toggle-join-library-button.component.ts +++ b/projects/aca-content/src/lib/components/toolbar/toggle-join-library/toggle-join-library-button.component.ts @@ -22,17 +22,10 @@ * from Hyland Software. If not, see . */ -import { - AppStore, - SetSelectedNodesAction, - SnackbarErrorAction, - SnackbarInfoAction, - getAppSelection, - getUserProfile -} from '@alfresco/aca-shared/store'; -import { AppHookService } from '@alfresco/aca-shared'; -import { ProfileState, SelectionState } from '@alfresco/adf-extensions'; -import { Component, ViewEncapsulation } from '@angular/core'; +import { AppStore, SetSelectedNodesAction, SnackbarErrorAction, SnackbarInfoAction, getAppSelection } from '@alfresco/aca-shared/store'; +import { AppHookService, UserProfileService } from '@alfresco/aca-shared'; +import { SelectionState } from '@alfresco/adf-extensions'; +import { Component, inject, ViewEncapsulation } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { LibraryMembershipDirective, LibraryMembershipErrorEvent, LibraryMembershipToggleEvent } from '@alfresco/adf-content-services'; @@ -64,12 +57,13 @@ import { MatIconModule } from '@angular/material/icon'; host: { class: 'app-toggle-join-library' } }) export class ToggleJoinLibraryButtonComponent { + private userProfileService = inject(UserProfileService); + selection$: Observable; - profile$: Observable; + profile$ = this.userProfileService.userProfile$; constructor(private store: Store, private appHookService: AppHookService) { this.selection$ = this.store.select(getAppSelection); - this.profile$ = this.store.select(getUserProfile); } onToggleEvent(event: LibraryMembershipToggleEvent) { diff --git a/projects/aca-content/src/lib/components/trashcan/trashcan.component.ts b/projects/aca-content/src/lib/components/trashcan/trashcan.component.ts index d1b4cc7fb7..31cdab9943 100644 --- a/projects/aca-content/src/lib/components/trashcan/trashcan.component.ts +++ b/projects/aca-content/src/lib/components/trashcan/trashcan.component.ts @@ -22,10 +22,16 @@ * from Hyland Software. If not, see . */ -import { getUserProfile } from '@alfresco/aca-shared/store'; import { DocumentListPresetRef, DynamicColumnComponent } from '@alfresco/adf-extensions'; -import { Component, OnInit, ViewEncapsulation } from '@angular/core'; -import { ContextActionsDirective, PageComponent, PageLayoutComponent, PaginationDirective, ToolbarComponent } from '@alfresco/aca-shared'; +import { Component, inject, OnInit, ViewEncapsulation } from '@angular/core'; +import { + ContextActionsDirective, + PageComponent, + PageLayoutComponent, + PaginationDirective, + ToolbarComponent, + UserProfileService +} from '@alfresco/aca-shared'; import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; import { DocumentListModule } from '@alfresco/adf-content-services'; @@ -52,7 +58,9 @@ import { DocumentListDirective } from '../../directives/document-list.directive' encapsulation: ViewEncapsulation.None }) export class TrashcanComponent extends PageComponent implements OnInit { - user$ = this.store.select(getUserProfile); + private userProfileService = inject(UserProfileService); + + user$ = this.userProfileService.userProfile$; columns: DocumentListPresetRef[] = []; ngOnInit() { diff --git a/projects/aca-content/src/lib/store/reducers/app.reducer.ts b/projects/aca-content/src/lib/store/reducers/app.reducer.ts index 73cf64ef71..192d8398d4 100644 --- a/projects/aca-content/src/lib/store/reducers/app.reducer.ts +++ b/projects/aca-content/src/lib/store/reducers/app.reducer.ts @@ -92,32 +92,7 @@ export function appReducer(state: AppState = INITIAL_APP_STATE, action: Action): } function updateUser(state: AppState, action: SetUserProfileAction): AppState { - const newState = { ...state }; - const user = action.payload.person; - const groups = [...(action.payload.groups || [])]; - - const id = user.id; - const firstName = user.firstName || ''; - const lastName = user.lastName || ''; - const userName = `${firstName} ${lastName}`; - const initials = [firstName[0], lastName[0]].join(''); - const email = user.email; - - const capabilities = user.capabilities; - const isAdmin = capabilities ? capabilities.isAdmin : true; - - newState.user = { - firstName, - lastName, - userName, - initials, - isAdmin, - id, - groups, - email - }; - - return newState; + return { ...state, user: { ...action.payload } }; } function updateCurrentFolder(state: AppState, action: SetCurrentFolderAction) { diff --git a/projects/aca-shared/src/lib/services/app.service.spec.ts b/projects/aca-shared/src/lib/services/app.service.spec.ts index acedcac914..b3be7a3963 100644 --- a/projects/aca-shared/src/lib/services/app.service.spec.ts +++ b/projects/aca-shared/src/lib/services/app.service.spec.ts @@ -26,20 +26,19 @@ import { AppService } from './app.service'; import { TestBed } from '@angular/core/testing'; import { AuthenticationService, - AppConfigService, AlfrescoApiService, PageTitleService, AlfrescoApiServiceMock, TranslationMock, TranslationService, - UserPreferencesService + UserPreferencesService, + NotificationService } from '@alfresco/adf-core'; import { BehaviorSubject, Observable, of, Subject } from 'rxjs'; import { HttpClientModule } from '@angular/common/http'; import { DiscoveryApiService, FileUploadErrorEvent, - GroupService, SearchQueryBuilderService, SharedLinksApiService, UploadService @@ -53,25 +52,26 @@ import { MatDialogModule } from '@angular/material/dialog'; import { TranslateModule } from '@ngx-translate/core'; import { Store } from '@ngrx/store'; import { ContentApiService } from './content-api.service'; -import { SetRepositoryInfoAction, SetUserProfileAction, SnackbarErrorAction } from '@alfresco/aca-shared/store'; -import { AppSettingsService } from '@alfresco/aca-shared'; +import { AppSettingsService, UserProfileService } from '@alfresco/aca-shared'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; describe('AppService', () => { let service: AppService; let auth: AuthenticationService; - let appConfig: AppConfigService; let searchQueryBuilderService: SearchQueryBuilderService; let uploadService: UploadService; let store: Store; let sharedLinksApiService: SharedLinksApiService; let contentApi: ContentApiService; - let groupService: GroupService; let preferencesService: UserPreferencesService; let appSettingsService: AppSettingsService; + let userProfileService: UserProfileService; + let notificationService: NotificationService; + let loadUserProfileSpy: jasmine.Spy; beforeEach(() => { TestBed.configureTestingModule({ - imports: [CommonModule, HttpClientModule, TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), MatDialogModule], + imports: [CommonModule, HttpClientModule, TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), MatDialogModule, MatSnackBarModule], providers: [ SearchQueryBuilderService, provideMockStore({}), @@ -118,31 +118,18 @@ describe('AppService', () => { }); appSettingsService = TestBed.inject(AppSettingsService); - appConfig = TestBed.inject(AppConfigService); auth = TestBed.inject(AuthenticationService); searchQueryBuilderService = TestBed.inject(SearchQueryBuilderService); uploadService = TestBed.inject(UploadService); store = TestBed.inject(Store); sharedLinksApiService = TestBed.inject(SharedLinksApiService); contentApi = TestBed.inject(ContentApiService); - groupService = TestBed.inject(GroupService); + spyOn(contentApi, 'getRepositoryInformation').and.returnValue(of({} as any)); service = TestBed.inject(AppService); preferencesService = TestBed.inject(UserPreferencesService); - }); - - it('should be ready if [withCredentials] mode is used', (done) => { - appConfig.config = { - auth: { - withCredentials: true - } - }; - - const instance = TestBed.inject(AppService); - expect(instance.withCredentials).toBeTruthy(); - - instance.ready$.subscribe(() => { - done(); - }); + userProfileService = TestBed.inject(UserProfileService); + loadUserProfileSpy = spyOn(userProfileService, 'loadUserProfile').and.returnValue(Promise.resolve({} as any)); + notificationService = TestBed.inject(NotificationService); }); it('should be ready after login', async () => { @@ -170,45 +157,46 @@ describe('AppService', () => { }); it('should raise notification on share link error', () => { + const showError = spyOn(notificationService, 'showError').and.stub(); spyOn(store, 'select').and.returnValue(of('')); service.init(); - const dispatch = spyOn(store, 'dispatch'); sharedLinksApiService.error.next({ message: 'Error Message', statusCode: 1 }); - expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('Error Message')); + expect(showError).toHaveBeenCalledWith('Error Message'); }); it('should raise notification on upload error', async () => { spyOn(store, 'select').and.returnValue(of('')); service.init(); - const dispatch = spyOn(store, 'dispatch'); + + const showError = spyOn(notificationService, 'showError').and.stub(); uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 403 })); - expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.403')); - dispatch.calls.reset(); + expect(showError).toHaveBeenCalledWith('APP.MESSAGES.UPLOAD.ERROR.403'); + showError.calls.reset(); uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 404 })); - expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.404')); - dispatch.calls.reset(); + expect(showError).toHaveBeenCalledWith('APP.MESSAGES.UPLOAD.ERROR.404'); + showError.calls.reset(); uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 409 })); - expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.CONFLICT')); - dispatch.calls.reset(); + expect(showError).toHaveBeenCalledWith('APP.MESSAGES.UPLOAD.ERROR.CONFLICT'); + showError.calls.reset(); uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 500 })); - expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.500')); - dispatch.calls.reset(); + expect(showError).toHaveBeenCalledWith('APP.MESSAGES.UPLOAD.ERROR.500'); + showError.calls.reset(); uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 504 })); - expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.504')); - dispatch.calls.reset(); + expect(showError).toHaveBeenCalledWith('APP.MESSAGES.UPLOAD.ERROR.504'); + showError.calls.reset(); uploadService.fileUploadError.next(new FileUploadErrorEvent(null, { status: 403 })); - expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.403')); - dispatch.calls.reset(); + expect(showError).toHaveBeenCalledWith('APP.MESSAGES.UPLOAD.ERROR.403'); + showError.calls.reset(); uploadService.fileUploadError.next(new FileUploadErrorEvent(null, {})); - expect(dispatch).toHaveBeenCalledWith(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.GENERIC')); + expect(showError).toHaveBeenCalledWith('APP.MESSAGES.UPLOAD.ERROR.GENERIC'); }); it('should load custom css', () => { @@ -225,34 +213,19 @@ describe('AppService', () => { }); it('should load repository status on login', () => { - const repository: any = {}; - spyOn(contentApi, 'getRepositoryInformation').and.returnValue(of({ entry: { repository } })); - spyOn(store, 'select').and.returnValue(of('')); service.init(); - - const dispatch = spyOn(store, 'dispatch'); auth.onLogin.next(true); - - expect(dispatch).toHaveBeenCalledWith(new SetRepositoryInfoAction(repository)); + expect(contentApi.getRepositoryInformation).toHaveBeenCalled(); }); it('should load user profile on login', async () => { const person: any = { id: 'person' }; - const group: any = { entry: {} }; - const groups: any[] = [group]; - - spyOn(contentApi, 'getRepositoryInformation').and.returnValue(of({} as any)); - spyOn(groupService, 'listAllGroupMembershipsForPerson').and.returnValue(Promise.resolve(groups)); - spyOn(contentApi, 'getPerson').and.returnValue(of({ entry: person })); - + loadUserProfileSpy.and.returnValue(Promise.resolve(person)); spyOn(store, 'select').and.returnValue(of('')); service.init(); - - const dispatch = spyOn(store, 'dispatch'); auth.onLogin.next(true); - await expect(groupService.listAllGroupMembershipsForPerson).toHaveBeenCalled(); - await expect(dispatch).toHaveBeenCalledWith(new SetUserProfileAction({ person, groups: [group.entry] })); + expect(loadUserProfileSpy).toHaveBeenCalled(); }); }); diff --git a/projects/aca-shared/src/lib/services/app.service.ts b/projects/aca-shared/src/lib/services/app.service.ts index f0a2238ff8..b2eeecebd0 100644 --- a/projects/aca-shared/src/lib/services/app.service.ts +++ b/projects/aca-shared/src/lib/services/app.service.ts @@ -22,39 +22,51 @@ * from Hyland Software. If not, see . */ -import { Injectable, OnDestroy } from '@angular/core'; -import { AuthenticationService, AppConfigService, AlfrescoApiService, PageTitleService, UserPreferencesService } from '@alfresco/adf-core'; +import { inject, Injectable, OnDestroy } from '@angular/core'; +import { + AuthenticationService, + AppConfigService, + AlfrescoApiService, + PageTitleService, + UserPreferencesService, + NotificationService +} from '@alfresco/adf-core'; import { Observable, BehaviorSubject, Subject } from 'rxjs'; -import { GroupService, SearchQueryBuilderService, SharedLinksApiService, UploadService, FileUploadErrorEvent } from '@alfresco/adf-content-services'; +import { SearchQueryBuilderService, SharedLinksApiService, UploadService, FileUploadErrorEvent } from '@alfresco/adf-content-services'; import { OverlayContainer } from '@angular/cdk/overlay'; import { ActivatedRoute, ActivationEnd, NavigationStart, Router } from '@angular/router'; -import { filter, map, tap } from 'rxjs/operators'; +import { filter, map } from 'rxjs/operators'; import { AppStore, CloseModalDialogsAction, SetCurrentUrlAction, SetRepositoryInfoAction, SetUserProfileAction, - SnackbarErrorAction, ResetSelectionAction } from '@alfresco/aca-shared/store'; import { ContentApiService } from './content-api.service'; import { RouterExtensionService } from './router.extension.service'; import { Store } from '@ngrx/store'; -import { DiscoveryEntry, GroupEntry, Group } from '@alfresco/js-api'; +import { DiscoveryEntry } from '@alfresco/js-api'; import { AcaMobileAppSwitcherService } from './aca-mobile-app-switcher.service'; import { ShellAppService } from '@alfresco/adf-core/shell'; import { AppSettingsService } from './app-settings.service'; +import { UserProfileService } from './user-profile.service'; @Injectable({ providedIn: 'root' }) // After moving shell to ADF to core, AppService will implement ShellAppService export class AppService implements ShellAppService, OnDestroy { + private notificationService = inject(NotificationService); private ready: BehaviorSubject; ready$: Observable; - pageHeading$: Observable; + + private pageHeading = new BehaviorSubject(''); + /** @deprecated page title is updated automatically */ + pageHeading$ = this.pageHeading.asObservable(); + appNavNarMode$: Subject<'collapsed' | 'expanded'> = new BehaviorSubject('expanded'); toggleAppNavBar$ = new Subject(); @@ -84,11 +96,11 @@ export class AppService implements ShellAppService, OnDestroy { private routerExtensionService: RouterExtensionService, private contentApi: ContentApiService, private sharedLinksApiService: SharedLinksApiService, - private groupService: GroupService, private overlayContainer: OverlayContainer, searchQueryBuilderService: SearchQueryBuilderService, private acaMobileAppSwitcherService: AcaMobileAppSwitcherService, - private appSettingsService: AppSettingsService + private appSettingsService: AppSettingsService, + private userProfileService: UserProfileService ) { this.ready = new BehaviorSubject(this.authenticationService.isLoggedIn() || this.withCredentials); this.ready$ = this.ready.asObservable(); @@ -104,11 +116,15 @@ export class AppService implements ShellAppService, OnDestroy { acaMobileAppSwitcherService.closeDialog(); }); - this.pageHeading$ = this.router.events.pipe( - filter((event) => event instanceof ActivationEnd && event.snapshot.children.length === 0), - map((event: ActivationEnd) => event.snapshot?.data?.title ?? ''), - tap((title) => this.pageTitle.setTitle(title)) - ); + this.router.events + .pipe( + filter((event) => event instanceof ActivationEnd && event.snapshot.children.length === 0), + map((event: ActivationEnd) => event.snapshot?.data?.title ?? '') + ) + .subscribe((title) => { + this.pageHeading.next(title); + this.pageTitle.setTitle(title); + }); } ngOnDestroy(): void { @@ -153,7 +169,7 @@ export class AppService implements ShellAppService, OnDestroy { this.sharedLinksApiService.error.subscribe((err: { message: string }) => { if (err?.message) { - this.store.dispatch(new SnackbarErrorAction(err.message)); + this.notificationService.showError(err.message); } }); @@ -184,17 +200,8 @@ export class AppService implements ShellAppService, OnDestroy { } private async loadUserProfile() { - const groupsEntries: GroupEntry[] = await this.groupService.listAllGroupMembershipsForPerson('-me-', { maxItems: 250 }); - - const groups: Group[] = []; - - if (groupsEntries) { - groups.push(...groupsEntries.map((obj) => obj.entry)); - } - - this.contentApi.getPerson('-me-').subscribe((person) => { - this.store.dispatch(new SetUserProfileAction({ person: person.entry, groups })); - }); + const profile = await this.userProfileService.loadUserProfile(); + this.store.dispatch(new SetUserProfileAction(profile)); } onFileUploadedError(error: FileUploadErrorEvent) { @@ -220,7 +227,7 @@ export class AppService implements ShellAppService, OnDestroy { message = 'APP.MESSAGES.UPLOAD.ERROR.504'; } - this.store.dispatch(new SnackbarErrorAction(message)); + this.notificationService.showError(message); } private loadCustomCss(): void { diff --git a/projects/aca-shared/src/lib/services/user-profile.service.ts b/projects/aca-shared/src/lib/services/user-profile.service.ts new file mode 100644 index 0000000000..c36452caca --- /dev/null +++ b/projects/aca-shared/src/lib/services/user-profile.service.ts @@ -0,0 +1,81 @@ +/*! + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { inject, Injectable } from '@angular/core'; +import { ProfileState } from '@alfresco/adf-extensions'; +import { GroupService } from '@alfresco/adf-content-services'; +import { BehaviorSubject } from 'rxjs'; +import { AlfrescoApiService } from '@alfresco/adf-core'; +import { PeopleApi } from '@alfresco/js-api'; + +@Injectable({ providedIn: 'root' }) +export class UserProfileService { + private api = inject(AlfrescoApiService); + private groupService = inject(GroupService); + + private get peopleApi(): PeopleApi { + return new PeopleApi(this.api.getInstance()); + } + + private userProfile = new BehaviorSubject(null); + userProfile$ = this.userProfile.asObservable(); + + /** + * Load user profile. + */ + async loadUserProfile(): Promise { + const groupsEntries = await this.groupService.listAllGroupMembershipsForPerson('-me-', { maxItems: 250 }); + + const groups = []; + if (groupsEntries) { + groups.push(...groupsEntries.map((obj) => obj.entry)); + } + + const { entry: user } = await this.peopleApi.getPerson('-me-'); + + const id = user.id; + const firstName = user.firstName || ''; + const lastName = user.lastName || ''; + const userName = `${firstName} ${lastName}`; + const initials = [firstName[0], lastName[0]].join(''); + const email = user.email; + + const capabilities = user.capabilities; + const isAdmin = capabilities ? capabilities.isAdmin : true; + + const profile: ProfileState = { + firstName, + lastName, + userName, + initials, + isAdmin, + id, + groups, + email + }; + + this.userProfile.next(profile); + return profile; + } +} diff --git a/projects/aca-shared/src/public-api.ts b/projects/aca-shared/src/public-api.ts index fd2e7196cd..6ac2a49269 100644 --- a/projects/aca-shared/src/public-api.ts +++ b/projects/aca-shared/src/public-api.ts @@ -57,6 +57,7 @@ export * from './lib/services/router.extension.service'; export * from './lib/services/app-hook.service'; export * from './lib/services/aca-file-auto-download.service'; export * from './lib/services/app-settings.service'; +export * from './lib/services/user-profile.service'; export * from './lib/utils/node.utils'; export * from './lib/testing/lib-testing-module'; diff --git a/projects/aca-shared/store/src/actions/app.actions.ts b/projects/aca-shared/store/src/actions/app.actions.ts index 402216d4ca..22cc211a92 100644 --- a/projects/aca-shared/store/src/actions/app.actions.ts +++ b/projects/aca-shared/store/src/actions/app.actions.ts @@ -23,8 +23,9 @@ */ import { Action } from '@ngrx/store'; -import { Node, Person, Group, RepositoryInfo, VersionEntry } from '@alfresco/js-api'; +import { Node, RepositoryInfo, VersionEntry } from '@alfresco/js-api'; import { AppActionTypes } from './app-action-types'; +import { ProfileState } from '@alfresco/adf-extensions'; export class SetCurrentFolderAction implements Action { readonly type = AppActionTypes.SetCurrentFolder; @@ -47,7 +48,7 @@ export class SetCurrentUrlAction implements Action { export class SetUserProfileAction implements Action { readonly type = AppActionTypes.SetUserProfile; - constructor(public payload: { person: Person; groups: Group[] }) {} + constructor(public payload: ProfileState) {} } export class ToggleInfoDrawerAction implements Action { diff --git a/projects/aca-shared/store/src/selectors/app.selectors.ts b/projects/aca-shared/store/src/selectors/app.selectors.ts index 3271dfabb7..54639b422e 100644 --- a/projects/aca-shared/store/src/selectors/app.selectors.ts +++ b/projects/aca-shared/store/src/selectors/app.selectors.ts @@ -28,6 +28,7 @@ import { createSelector } from '@ngrx/store'; const HXI_CONNECTOR = 'alfresco-hxinsight-connector-prediction-applier-extension'; export const selectApp = (state: AppStore) => state.app; +/** @deprecated use `UserProfileService` instead */ export const getUserProfile = createSelector(selectApp, (state) => state.user); export const getCurrentFolder = createSelector(selectApp, (state) => state.navigation.currentFolder); export const getCurrentVersion = createSelector(selectApp, (state) => state.currentNodeVersion);