[ACS-8433] ACA: User Profile Service (#3957)

This commit is contained in:
Denys Vuika 2024-07-22 10:07:43 -04:00 committed by GitHub
parent f23f5edd08
commit b5568d43fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 183 additions and 153 deletions

View File

@ -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:

View File

@ -1,2 +1 @@
<h1 class="aca-sr-only" title="{{pageHeading | async | translate}}">{{ pageHeading | async | translate }}</h1>
<router-outlet></router-outlet>

View File

@ -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<boolean> = new Subject<boolean>();
pageHeading: Observable<string>;
constructor(private appService: AppService) {
this.pageHeading = this.appService.pageHeading$;
this.appService.init();
}
}

View File

@ -58,7 +58,7 @@ module.exports = () => {
check: {
global: {
statements: 75,
branches: 67,
branches: 65,
functions: 71,
lines: 74
}

View File

@ -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<AppStore>>(Store<AppStore>);
user$ = this.store.select(getUserProfile);
private userProfileService = inject(UserProfileService);
user$ = this.userProfileService.userProfile$;
}

View File

@ -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<AppStore>>(Store<AppStore>);
user$ = this.store.select(getUserProfile);
private userProfileService = inject(UserProfileService);
user$ = this.userProfileService.userProfile$;
@Input()
actionRef: ContentActionRef;

View File

@ -22,17 +22,10 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
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<SelectionState>;
profile$: Observable<ProfileState>;
profile$ = this.userProfileService.userProfile$;
constructor(private store: Store<AppStore>, private appHookService: AppHookService) {
this.selection$ = this.store.select(getAppSelection);
this.profile$ = this.store.select(getUserProfile);
}
onToggleEvent(event: LibraryMembershipToggleEvent) {

View File

@ -22,10 +22,16 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
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() {

View File

@ -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) {

View File

@ -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();
});
});

View File

@ -22,39 +22,51 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
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<boolean>;
ready$: Observable<boolean>;
pageHeading$: Observable<string>;
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(
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))
);
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 {

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ProfileState>(null);
userProfile$ = this.userProfile.asObservable();
/**
* Load user profile.
*/
async loadUserProfile(): Promise<ProfileState> {
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;
}
}

View File

@ -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';

View File

@ -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 {

View File

@ -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);