diff --git a/docs/extending/application-features.md b/docs/extending/application-features.md
index 40a0324b3..cecaac05f 100644
--- a/docs/extending/application-features.md
+++ b/docs/extending/application-features.md
@@ -307,6 +307,7 @@ The content actions are applied to the toolbars for the following Views:
- Favorites
- Trash
- Search Results
+- Libraries Search Results
## Context Menu
diff --git a/docs/extending/components.md b/docs/extending/components.md
index e94fa0011..e978a69d9 100644
--- a/docs/extending/components.md
+++ b/docs/extending/components.md
@@ -17,6 +17,7 @@ The components are used to create custom:
| app.toolbar.toggleInfoDrawer | ToggleInfoDrawerComponent | The toolbar button component that toggles Info Drawer for the selection. |
| app.toolbar.toggleFavorite | ToggleFavoriteComponent | The toolbar button component that toggles Favorite state for the selection. |
| app.toolbar.toggleFavoriteLibrary | ToggleFavoriteLibraryComponent | The toolbar button component that toggles Favorite library state for the selection. |
+| app.toolbar.toggleJoinLibrary | ToggleJoinLibraryComponent | The toolbar button component that toggles Join/Cancel Join request for the selected library. |
See [Registration](/extending/registration) section for more details
on how to register your own entries to be re-used at runtime.
diff --git a/docs/extending/rules.md b/docs/extending/rules.md
index 4fd4f27e6..0a23b56fc 100644
--- a/docs/extending/rules.md
+++ b/docs/extending/rules.md
@@ -143,6 +143,9 @@ The button will be visible only when the linked rule evaluates to `true`.
| app.selection.file | A single File node is selected. |
| app.selection.file.canShare | User is able to share the selected file. |
| app.selection.library | A single Library node is selected. |
+| app.selection.isPrivateLibrary | A private Library node is selected. |
+| app.selection.hasLibraryRole | The selected Library node has a role property. |
+| app.selection.hasNoLibraryRole | The selected Library node has no role property. |
| app.selection.folder | A single Folder node is selected. |
| app.selection.folder.canUpdate | User has permissions to update the selected folder. |
| repository.isQuickShareEnabled | Whether the quick share repository option is enabled or not. |
@@ -163,18 +166,18 @@ You can also negate any rule by utilizing a `!` prefix:
| --------------------------------- | ------------------------------------------------------- |
| app.navigation.folder.canCreate | User can create content in the currently opened folder. |
| app.navigation.folder.canUpload | User can upload content to the currently opened folder. |
-| app.navigation.isTrashcan | User is using the **Trashcan** page. |
+| app.navigation.isTrashcan | User is using the **Trashcan** page. |
| app.navigation.isNotTrashcan | Current page is not a **Trashcan**. |
-| app.navigation.isLibraries | User is using the **Libraries** page. |
-| app.navigation.isNotLibraries | Current page is not **Libraries**. |
-| app.navigation.isSharedFiles | User is using the **Shared Files** page. |
+| app.navigation.isLibraries | User is using a **Libraries** page. |
+| app.navigation.isNotLibraries | Current page is not a **Libraries** page. |
+| app.navigation.isSharedFiles | User is using the **Shared Files** page. |
| app.navigation.isNotSharedFiles | Current page is not **Shared Files**. |
-| app.navigation.isFavorites | User is using the **Favorites** page. |
+| app.navigation.isFavorites | User is using the **Favorites** page. |
| app.navigation.isNotFavorites | Current page is not **Favorites** |
-| app.navigation.isRecentFiles | User is using the **Recent Files** page. |
+| app.navigation.isRecentFiles | User is using the **Recent Files** page. |
| app.navigation.isNotRecentFiles | Current page is not **Recent Files**. |
-| app.navigation.isSearchResults | User is using the **Search Results** page. |
-| app.navigation.isNotSearchResults | Current page is not the **Search Results**. |
+| app.navigation.isSearchResults | User is using the **Search Results** page. |
+| app.navigation.isNotSearchResults | Current page is not the **Search Results**. |
See [Registration](/extending/registration) section for more details
@@ -187,7 +190,7 @@ The rule in the example below evaluates to `true` if all the conditions are met:
- user has selected node(s)
- user is not using the **Trashcan** page
-- user is not using the **Libraries** page
+- user is not using a **Libraries** page (**My Libraries**, **Favorite Libraries** or **Libraries Search Results** pages)
```json
{
diff --git a/src/app/components/search/search-input-control/search-input-control.component.spec.ts b/src/app/components/search/search-input-control/search-input-control.component.spec.ts
index 3d6085803..fa24e52f1 100644
--- a/src/app/components/search/search-input-control/search-input-control.component.spec.ts
+++ b/src/app/components/search/search-input-control/search-input-control.component.spec.ts
@@ -24,9 +24,73 @@
*/
import { SearchInputControlComponent } from './search-input-control.component';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { AppTestingModule } from '../../../testing/app-testing.module';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('SearchInputControlComponent', () => {
- it('should be defined', () => {
- expect(SearchInputControlComponent).toBeDefined();
+ let fixture: ComponentFixture;
+ let component: SearchInputControlComponent;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [AppTestingModule],
+ declarations: [SearchInputControlComponent],
+ schemas: [NO_ERRORS_SCHEMA]
+ })
+ .compileComponents()
+ .then(() => {
+ fixture = TestBed.createComponent(SearchInputControlComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+ }));
+
+ it('should emit submit event on searchSubmit', async () => {
+ const keyboardEvent = { target: { value: 'a' } };
+
+ let eventArgs = null;
+ component.submit.subscribe(args => (eventArgs = args));
+
+ await component.searchSubmit(keyboardEvent);
+ expect(eventArgs).toBe(keyboardEvent);
+ });
+
+ it('should emit searchChange event on inputChange', async () => {
+ const searchTerm = 'b';
+
+ let eventArgs = null;
+ component.searchChange.subscribe(args => (eventArgs = args));
+
+ await component.inputChange(searchTerm);
+ expect(eventArgs).toBe(searchTerm);
+ });
+
+ it('should emit searchChange event on clear', async () => {
+ let eventArgs = null;
+ component.searchChange.subscribe(args => (eventArgs = args));
+
+ await component.clear();
+ expect(eventArgs).toBe('');
+ });
+
+ it('should clear searchTerm', async () => {
+ component.searchTerm = 'c';
+ fixture.detectChanges();
+
+ await component.clear();
+ expect(component.searchTerm).toBe('');
+ });
+
+ it('should check if searchTerm has a length less than 2', () => {
+ expect(component.isTermTooShort()).toBe(false);
+
+ component.searchTerm = 'd';
+ fixture.detectChanges();
+ expect(component.isTermTooShort()).toBe(true);
+
+ component.searchTerm = 'dd';
+ fixture.detectChanges();
+ expect(component.isTermTooShort()).toBe(false);
});
});
diff --git a/src/app/components/search/search-input-control/search-input-control.component.ts b/src/app/components/search/search-input-control/search-input-control.component.ts
index 68490b28b..66a2093cd 100644
--- a/src/app/components/search/search-input-control/search-input-control.component.ts
+++ b/src/app/components/search/search-input-control/search-input-control.component.ts
@@ -87,8 +87,6 @@ export class SearchInputControlComponent implements OnDestroy {
}
isTermTooShort() {
- const alphanumericTerm = this.searchTerm.replace(/[^0-9a-z]/gi, '');
-
- return this.searchTerm.length && alphanumericTerm.length < 2;
+ return !!(this.searchTerm && this.searchTerm.length < 2);
}
}
diff --git a/src/app/components/search/search-input/search-input.component.spec.ts b/src/app/components/search/search-input/search-input.component.spec.ts
index 6fad90428..b0d4e290e 100644
--- a/src/app/components/search/search-input/search-input.component.spec.ts
+++ b/src/app/components/search/search-input/search-input.component.spec.ts
@@ -39,11 +39,13 @@ import { SEARCH_BY_TERM, SearchByTermAction } from '../../../store/actions';
import { map } from 'rxjs/operators';
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
import { SearchLibrariesQueryBuilderService } from '../search-libraries-results/search-libraries-query-builder.service';
+import { ContentManagementService } from '../../../services/content-management.service';
describe('SearchInputComponent', () => {
let fixture: ComponentFixture;
let component: SearchInputComponent;
let actions$: Actions;
+ let content: ContentManagementService;
beforeEach(async(() => {
TestBed.configureTestingModule({
@@ -56,11 +58,33 @@ describe('SearchInputComponent', () => {
.then(() => {
actions$ = TestBed.get(Actions);
fixture = TestBed.createComponent(SearchInputComponent);
+ content = TestBed.get(ContentManagementService);
component = fixture.componentInstance;
fixture.detectChanges();
});
}));
+ it('should change flag on library400Error event', () => {
+ expect(component.has400LibraryError).toBe(false);
+ content.library400Error.next();
+
+ expect(component.has400LibraryError).toBe(true);
+ });
+
+ it('should have no library constraint by default', () => {
+ expect(component.hasLibraryConstraint()).toBe(false);
+ });
+
+ it('should have library constraint on 400 error received', () => {
+ const libItem = component.searchOptions.find(
+ item => item.key.toLowerCase().indexOf('libraries') > 0
+ );
+ libItem.value = true;
+ content.library400Error.next();
+
+ expect(component.hasLibraryConstraint()).toBe(true);
+ });
+
describe('onSearchSubmit()', () => {
it('should call search action with correct search options', fakeAsync(done => {
const searchedTerm = 's';
diff --git a/src/app/components/search/search-input/search-input.component.ts b/src/app/components/search/search-input/search-input.component.ts
index a767c0e1f..deae299e6 100644
--- a/src/app/components/search/search-input/search-input.component.ts
+++ b/src/app/components/search/search-input/search-input.component.ts
@@ -23,7 +23,13 @@
* along with Alfresco. If not, see .
*/
-import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
+import {
+ Component,
+ OnDestroy,
+ OnInit,
+ ViewChild,
+ ViewEncapsulation
+} from '@angular/core';
import {
NavigationEnd,
PRIMARY_OUTLET,
@@ -37,9 +43,11 @@ import { SearchInputControlComponent } from '../search-input-control/search-inpu
import { Store } from '@ngrx/store';
import { AppStore } from '../../../store/states/app.state';
import { SearchByTermAction } from '../../../store/actions';
-import { filter } from 'rxjs/operators';
+import { filter, takeUntil } from 'rxjs/operators';
import { SearchLibrariesQueryBuilderService } from '../search-libraries-results/search-libraries-query-builder.service';
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
+import { ContentManagementService } from '../../../services/content-management.service';
+import { Subject } from 'rxjs';
export enum SearchOptionIds {
Files = 'files',
@@ -53,10 +61,12 @@ export enum SearchOptionIds {
encapsulation: ViewEncapsulation.None,
host: { class: 'aca-search-input' }
})
-export class SearchInputComponent implements OnInit {
+export class SearchInputComponent implements OnInit, OnDestroy {
+ onDestroy$: Subject = new Subject();
hasOneChange = false;
hasNewChange = false;
navigationTimer: any;
+ has400LibraryError = false;
searchedWord = null;
searchOptions: Array = [
@@ -86,6 +96,7 @@ export class SearchInputComponent implements OnInit {
constructor(
private librariesQueryBuilder: SearchLibrariesQueryBuilderService,
private queryBuilder: SearchQueryBuilderService,
+ private content: ContentManagementService,
private router: Router,
private store: Store
) {}
@@ -94,15 +105,23 @@ export class SearchInputComponent implements OnInit {
this.showInputValue();
this.router.events
+ .pipe(takeUntil(this.onDestroy$))
.pipe(filter(e => e instanceof RouterEvent))
.subscribe(event => {
if (event instanceof NavigationEnd) {
this.showInputValue();
}
});
+
+ this.content.library400Error
+ .pipe(takeUntil(this.onDestroy$))
+ .subscribe(() => {
+ this.has400LibraryError = true;
+ });
}
showInputValue() {
+ this.has400LibraryError = false;
this.searchedWord = '';
if (this.onSearchResults || this.onLibrariesSearchResults) {
@@ -121,12 +140,18 @@ export class SearchInputComponent implements OnInit {
}
}
+ ngOnDestroy(): void {
+ this.onDestroy$.next(true);
+ this.onDestroy$.complete();
+ }
+
/**
* Called when the user submits the search, e.g. hits enter or clicks submit
*
* @param event Parameters relating to the search
*/
onSearchSubmit(event: KeyboardEvent) {
+ this.has400LibraryError = false;
const searchTerm = (event.target as HTMLInputElement).value;
if (searchTerm) {
this.store.dispatch(
@@ -136,6 +161,7 @@ export class SearchInputComponent implements OnInit {
}
onSearchChange(searchTerm: string) {
+ this.has400LibraryError = false;
if (this.hasOneChange) {
this.hasNewChange = true;
} else {
@@ -158,6 +184,7 @@ export class SearchInputComponent implements OnInit {
}
onOptionChange() {
+ this.has400LibraryError = false;
if (this.searchedWord) {
if (this.isLibrariesChecked()) {
if (this.onLibrariesSearchResults) {
@@ -213,7 +240,9 @@ export class SearchInputComponent implements OnInit {
hasLibraryConstraint(): boolean {
if (this.isLibrariesChecked()) {
- return this.searchInputControl.isTermTooShort();
+ return (
+ this.has400LibraryError || this.searchInputControl.isTermTooShort()
+ );
}
return false;
}
diff --git a/src/app/components/search/search-libraries-results/search-libraries-query-builder.service.spec.ts b/src/app/components/search/search-libraries-results/search-libraries-query-builder.service.spec.ts
index 3445b9001..a6ad36dfc 100644
--- a/src/app/components/search/search-libraries-results/search-libraries-query-builder.service.spec.ts
+++ b/src/app/components/search/search-libraries-results/search-libraries-query-builder.service.spec.ts
@@ -93,4 +93,18 @@ describe('SearchLibrariesQueryBuilderService', () => {
const compiled = builder.buildQuery();
expect(compiled.opts).toEqual({ maxItems: 5, skipCount: 5 });
});
+
+ it('should raise an event on error', async () => {
+ const err = '{"error": {"statusCode": 400}}';
+ spyOn(queriesApi, 'findSites').and.returnValue(Promise.reject(err));
+
+ const query = {};
+ spyOn(builder, 'buildQuery').and.returnValue(query);
+
+ let eventArgs = null;
+ builder.hadError.subscribe(args => (eventArgs = args));
+
+ await builder.execute();
+ expect(eventArgs).toBe(err);
+ });
});
diff --git a/src/app/components/search/search-libraries-results/search-libraries-query-builder.service.ts b/src/app/components/search/search-libraries-results/search-libraries-query-builder.service.ts
index 86008e73d..02b697dc2 100644
--- a/src/app/components/search/search-libraries-results/search-libraries-query-builder.service.ts
+++ b/src/app/components/search/search-libraries-results/search-libraries-query-builder.service.ts
@@ -36,6 +36,7 @@ export class SearchLibrariesQueryBuilderService {
updated: Subject = new Subject();
executed: Subject = new Subject();
+ hadError: Subject = new Subject();
paging: { maxItems?: number; skipCount?: number } = null;
@@ -77,10 +78,13 @@ export class SearchLibrariesQueryBuilderService {
return null;
}
- private findLibraries(libraryQuery: { term; opts }): Promise {
+ private findLibraries(libraryQuery): Promise {
return this.alfrescoApiService
.getInstance()
.core.queriesApi.findSites(libraryQuery.term, libraryQuery.opts)
- .catch(() => ({ list: { pagination: { totalItems: 0 }, entries: [] } }));
+ .catch(err => {
+ this.hadError.next(err);
+ return { list: { pagination: { totalItems: 0 }, entries: [] } };
+ });
}
}
diff --git a/src/app/components/search/search-libraries-results/search-libraries-results.component.ts b/src/app/components/search/search-libraries-results/search-libraries-results.component.ts
index e2a21ef81..c33c96a24 100644
--- a/src/app/components/search/search-libraries-results/search-libraries-results.component.ts
+++ b/src/app/components/search/search-libraries-results/search-libraries-results.component.ts
@@ -93,6 +93,17 @@ export class SearchLibrariesResultsComponent extends PageComponent
this.isLoading = false;
}),
+ this.librariesQueryBuilder.hadError.subscribe(err => {
+ try {
+ const {
+ error: { statusCode }
+ } = JSON.parse(err.message);
+ if (statusCode === 400) {
+ this.content.library400Error.next();
+ }
+ } catch (e) {}
+ }),
+
this.breakpointObserver
.observe([Breakpoints.HandsetPortrait, Breakpoints.HandsetLandscape])
.subscribe(result => {
diff --git a/src/app/directives/library-membership.directive.spec.ts b/src/app/directives/library-membership.directive.spec.ts
new file mode 100644
index 000000000..d6c2abe8f
--- /dev/null
+++ b/src/app/directives/library-membership.directive.spec.ts
@@ -0,0 +1,169 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2018 Alfresco Software Limited
+ *
+ * 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
+ * along with Alfresco. If not, see .
+ */
+
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+import {
+ AlfrescoApiService,
+ AlfrescoApiServiceMock,
+ AppConfigService,
+ CoreModule,
+ StorageService
+} from '@alfresco/adf-core';
+import { AppTestingModule } from '../testing/app-testing.module';
+import { DirectivesModule } from './directives.module';
+import { LibraryMembershipDirective } from './library-membership.directive';
+import { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core';
+
+describe('LibraryMembershipDirective', () => {
+ let alfrescoApiService: AlfrescoApiService;
+ let directive: LibraryMembershipDirective;
+ let peopleApi;
+ let addMembershipSpy;
+ let getMembershipSpy;
+ let deleteMembershipSpy;
+
+ const testSiteEntry = {
+ id: 'id-1',
+ guid: 'site-1',
+ title: 'aa t m',
+ visibility: 'MODERATED'
+ };
+ const requestedMembershipResponse = {
+ id: testSiteEntry.id,
+ createdAt: '2018-11-14',
+ site: testSiteEntry
+ };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [AppTestingModule, DirectivesModule, CoreModule.forRoot()],
+ schemas: [NO_ERRORS_SCHEMA]
+ });
+ alfrescoApiService = new AlfrescoApiServiceMock(
+ new AppConfigService(null),
+ new StorageService()
+ );
+ peopleApi = alfrescoApiService.getInstance().core.peopleApi;
+ directive = new LibraryMembershipDirective(alfrescoApiService);
+ });
+
+ describe('markMembershipRequest', () => {
+ beforeEach(() => {
+ getMembershipSpy = spyOn(
+ peopleApi,
+ 'getSiteMembershipRequest'
+ ).and.returnValue(
+ Promise.resolve({ entry: requestedMembershipResponse })
+ );
+ });
+
+ it('should not check membership requests if no entry is selected', fakeAsync(() => {
+ const selection = {};
+ const change = new SimpleChange(null, selection, true);
+ directive.ngOnChanges({ selection: change });
+ tick();
+ expect(getMembershipSpy).not.toHaveBeenCalled();
+ }));
+
+ it('should check if a membership request exists for the selected library', fakeAsync(() => {
+ const selection = { entry: {} };
+ const change = new SimpleChange(null, selection, true);
+ directive.ngOnChanges({ selection: change });
+ tick();
+ expect(getMembershipSpy.calls.count()).toBe(1);
+ }));
+
+ it('should remember when a membership request exists for selected library', fakeAsync(() => {
+ const selection = { entry: testSiteEntry };
+ const change = new SimpleChange(null, selection, true);
+ directive.ngOnChanges({ selection: change });
+ tick();
+ expect(directive.targetSite.joinRequested).toBe(true);
+ }));
+
+ it('should remember when a membership request is not found for selected library', fakeAsync(() => {
+ getMembershipSpy.and.returnValue(Promise.reject());
+
+ const selection = { entry: testSiteEntry };
+ const change = new SimpleChange(null, selection, true);
+ directive.ngOnChanges({ selection: change });
+ tick();
+ expect(directive.targetSite.joinRequested).toBe(false);
+ }));
+ });
+
+ describe('toggleMembershipRequest', () => {
+ beforeEach(() => {
+ getMembershipSpy = spyOn(
+ peopleApi,
+ 'getSiteMembershipRequest'
+ ).and.returnValue(
+ Promise.resolve({ entry: requestedMembershipResponse })
+ );
+ addMembershipSpy = spyOn(
+ peopleApi,
+ 'addSiteMembershipRequest'
+ ).and.returnValue(
+ Promise.resolve({ entry: requestedMembershipResponse })
+ );
+ deleteMembershipSpy = spyOn(
+ peopleApi,
+ 'removeSiteMembershipRequest'
+ ).and.returnValue(Promise.resolve({}));
+ });
+
+ it('should do nothing if there is no selected library ', fakeAsync(() => {
+ const selection = {};
+ const change = new SimpleChange(null, selection, true);
+ directive.ngOnChanges({ selection: change });
+ tick();
+ directive.toggleMembershipRequest();
+ tick();
+ expect(addMembershipSpy).not.toHaveBeenCalled();
+ expect(deleteMembershipSpy).not.toHaveBeenCalled();
+ }));
+
+ it('should delete membership request if there is one', fakeAsync(() => {
+ const selection = { entry: testSiteEntry };
+ const change = new SimpleChange(null, selection, true);
+ directive.ngOnChanges({ selection: change });
+ tick();
+ directive.toggleMembershipRequest();
+ tick();
+ expect(deleteMembershipSpy).toHaveBeenCalled();
+ }));
+
+ it('should call API to make a membership request if there is none', fakeAsync(() => {
+ const selection = { entry: { id: 'no-membership-requested' } };
+ const change = new SimpleChange(null, selection, true);
+ directive.ngOnChanges({ selection: change });
+ tick();
+ directive.toggleMembershipRequest();
+ tick();
+ expect(addMembershipSpy).toHaveBeenCalled();
+ expect(deleteMembershipSpy).not.toHaveBeenCalled();
+ }));
+ });
+});
diff --git a/src/app/directives/library-membership.directive.ts b/src/app/directives/library-membership.directive.ts
index 5ea4e19de..1d2ae0957 100644
--- a/src/app/directives/library-membership.directive.ts
+++ b/src/app/directives/library-membership.directive.ts
@@ -90,11 +90,11 @@ export class LibraryMembershipDirective implements OnChanges {
this.toggle.emit(info);
},
error => {
- const errWitMessage = {
+ const errWithMessage = {
error,
i18nKey: 'APP.MESSAGES.ERRORS.JOIN_CANCEL_FAILED'
};
- this.error.emit(errWitMessage);
+ this.error.emit(errWithMessage);
}
);
}
@@ -124,11 +124,11 @@ export class LibraryMembershipDirective implements OnChanges {
}
},
error => {
- const errWitMessage = {
+ const errWithMessage = {
error,
i18nKey: 'APP.MESSAGES.ERRORS.JOIN_REQUEST_FAILED'
};
- this.error.emit(errWitMessage);
+ this.error.emit(errWithMessage);
}
);
}
diff --git a/src/app/services/content-management.service.ts b/src/app/services/content-management.service.ts
index f6a19e1c4..4069413b2 100644
--- a/src/app/services/content-management.service.ts
+++ b/src/app/services/content-management.service.ts
@@ -83,6 +83,7 @@ export class ContentManagementService {
libraryCreated = new Subject();
libraryUpdated = new Subject();
libraryJoined = new Subject();
+ library400Error = new Subject();
joinLibraryToggle = new Subject();
linksUnshared = new Subject();
favoriteAdded = new Subject>();