diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0a0020360..5f78916f9 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -70,6 +70,8 @@ import { AppSearchResultsModule } from './components/search/search-results.modul import { AppLoginModule } from './components/login/login.module'; import { AppHeaderModule } from './components/header/header.module'; import { environment } from '../environments/environment'; +import { LibraryMembershipDirective } from './directives/library-membership.directive'; +import { ToggleJoinLibraryComponent } from './components/toolbar/toggle-join-library/toggle-join-library.component'; @NgModule({ imports: [ @@ -110,7 +112,9 @@ import { environment } from '../environments/environment'; LibrariesComponent, FavoriteLibrariesComponent, NodeVersionsDialogComponent, - LibraryDialogComponent + LibraryDialogComponent, + LibraryMembershipDirective, + ToggleJoinLibraryComponent ], providers: [ { provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy }, @@ -124,7 +128,11 @@ import { environment } from '../environments/environment'; } } ], - entryComponents: [LibraryDialogComponent, NodeVersionsDialogComponent], + entryComponents: [ + LibraryDialogComponent, + NodeVersionsDialogComponent, + ToggleJoinLibraryComponent + ], bootstrap: [AppComponent] }) export class AppModule {} diff --git a/src/app/components/layout/app-layout/app-layout.component.ts b/src/app/components/layout/app-layout/app-layout.component.ts index 1bf756966..42d5c2d38 100644 --- a/src/app/components/layout/app-layout/app-layout.component.ts +++ b/src/app/components/layout/app-layout/app-layout.component.ts @@ -44,6 +44,8 @@ import { currentFolder } from '../../../store/selectors/app.selectors'; import { AppStore } from '../../../store/states'; import { BreakpointObserver } from '@angular/cdk/layout'; import { SetSelectedNodesAction } from '../../../store/actions'; +import { MatIconRegistry } from '@angular/material'; +import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-layout', @@ -68,14 +70,25 @@ export class AppLayoutComponent implements OnInit, OnDestroy { private minimizeConditions: string[] = ['search']; private hideConditions: string[] = ['preview']; + customIcon: any = { + join_library: './assets/images/join-library.svg' + }; + constructor( protected store: Store, private permission: NodePermissionService, private router: Router, private userPreferenceService: UserPreferencesService, private appConfigService: AppConfigService, - private breakpointObserver: BreakpointObserver - ) {} + private breakpointObserver: BreakpointObserver, + matIconRegistry: MatIconRegistry, + sanitizer: DomSanitizer + ) { + matIconRegistry.addSvgIcon( + 'join_library', + sanitizer.bypassSecurityTrustResourceUrl(this.customIcon['join_library']) + ); + } ngOnInit() { this.isSmallScreen$ = this.breakpointObserver diff --git a/src/app/components/layout/layout.module.ts b/src/app/components/layout/layout.module.ts index 75299a967..78959e455 100644 --- a/src/app/components/layout/layout.module.ts +++ b/src/app/components/layout/layout.module.ts @@ -36,6 +36,7 @@ import { PageLayoutComponent } from './page-layout/page-layout.component'; import { PageLayoutHeaderComponent } from './page-layout/page-layout-header.component'; import { PageLayoutContentComponent } from './page-layout/page-layout-content.component'; import { PageLayoutErrorComponent } from './page-layout/page-layout-error.component'; +import { HttpClientModule } from '@angular/common/http'; @NgModule({ imports: [ @@ -45,7 +46,8 @@ import { PageLayoutErrorComponent } from './page-layout/page-layout-error.compon ContentModule.forChild(), AppCommonModule, AppSidenavModule, - AppHeaderModule + AppHeaderModule, + HttpClientModule ], declarations: [ AppLayoutComponent, 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 de321e28c..e2a21ef81 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 @@ -73,6 +73,13 @@ export class SearchLibrariesResultsComponent extends PageComponent this.columns = this.extensions.documentListPresets.searchLibraries || []; this.subscriptions.push( + this.content.libraryJoined.subscribe(() => + this.librariesQueryBuilder.update() + ), + this.content.libraryDeleted.subscribe(() => + this.librariesQueryBuilder.update() + ), + this.librariesQueryBuilder.updated.subscribe(() => { this.isLoading = true; diff --git a/src/app/components/toolbar/toggle-join-library/toggle-join-library.component.spec.ts b/src/app/components/toolbar/toggle-join-library/toggle-join-library.component.spec.ts new file mode 100644 index 000000000..99861f487 --- /dev/null +++ b/src/app/components/toolbar/toggle-join-library/toggle-join-library.component.spec.ts @@ -0,0 +1,131 @@ +/*! + * @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 { ToggleJoinLibraryComponent } from './toggle-join-library.component'; +import { of } from 'rxjs'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-core'; +import { LibraryMembershipDirective } from '../../../directives/library-membership.directive'; +import { Store } from '@ngrx/store'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + SnackbarErrorAction, + SnackbarInfoAction +} from '../../../store/actions/snackbar.actions'; +import { AppTestingModule } from '../../../testing/app-testing.module'; +import { ContentManagementService } from '../../../services/content-management.service'; + +describe('ToggleJoinLibraryComponent', () => { + let component: ToggleJoinLibraryComponent; + let fixture: ComponentFixture; + let alfrescoApi: AlfrescoApiService; + let contentManagementService: ContentManagementService; + let entry; + + const storeMock = { + select: () => of({ library: { entry, isLibrary: true } }), + dispatch: jasmine.createSpy('dispatch') + }; + + beforeEach(() => { + entry = { + id: 'lib-id', + joinRequested: true, + title: 'test', + visibility: 'MODERATED' + }; + + TestBed.configureTestingModule({ + imports: [AppTestingModule], + declarations: [ToggleJoinLibraryComponent, LibraryMembershipDirective], + providers: [ + { provide: Store, useValue: storeMock }, + { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock } + ], + schemas: [NO_ERRORS_SCHEMA] + }); + + fixture = TestBed.createComponent(ToggleJoinLibraryComponent); + component = fixture.componentInstance; + alfrescoApi = TestBed.get(AlfrescoApiService); + contentManagementService = TestBed.get(ContentManagementService); + + spyOn(alfrescoApi.peopleApi, 'getSiteMembershipRequest').and.stub(); + }); + + afterEach(() => { + fixture.destroy(); + storeMock.dispatch.calls.reset(); + }); + + it('should get Store selection entry on initialization', done => { + component.selection$.subscribe(selection => { + expect(selection.library.entry).toEqual(entry); + done(); + }); + }); + + it('should dispatch `SnackbarErrorAction` action on error', () => { + const event = { event: {}, i18nKey: 'ERROR_i18nKey' }; + component.onErrorEvent(event); + + expect(storeMock.dispatch).toHaveBeenCalledWith( + new SnackbarErrorAction(event.i18nKey) + ); + }); + + it('should dispatch `SnackbarInfoAction` action on onToggleEvent', () => { + const event = { shouldReload: true, i18nKey: 'SOME_i18nKey' }; + component.onToggleEvent(event); + + expect(storeMock.dispatch).toHaveBeenCalledWith( + new SnackbarInfoAction(event.i18nKey) + ); + }); + + it('should call libraryJoined.next on contentManagementService onToggleEvent', done => { + spyOn(contentManagementService.libraryJoined, 'next').and.callThrough(); + + contentManagementService.libraryJoined.subscribe(() => { + expect(contentManagementService.libraryJoined.next).toHaveBeenCalled(); + done(); + }); + const event = { shouldReload: true }; + component.onToggleEvent(event); + }); + + it('should call joinLibraryToggle.next on contentManagementService onToggleEvent', done => { + spyOn(contentManagementService.joinLibraryToggle, 'next').and.callThrough(); + + contentManagementService.joinLibraryToggle.subscribe(() => { + expect( + contentManagementService.joinLibraryToggle.next + ).toHaveBeenCalled(); + done(); + }); + const event = { shouldReload: false }; + component.onToggleEvent(event); + }); +}); diff --git a/src/app/components/toolbar/toggle-join-library/toggle-join-library.component.ts b/src/app/components/toolbar/toggle-join-library/toggle-join-library.component.ts new file mode 100644 index 000000000..06fdbd9f4 --- /dev/null +++ b/src/app/components/toolbar/toggle-join-library/toggle-join-library.component.ts @@ -0,0 +1,88 @@ +/*! + * @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 { Component, ViewEncapsulation } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../../store/states'; +import { appSelection } from '../../../store/selectors/app.selectors'; +import { Observable } from 'rxjs'; +import { SelectionState } from '@alfresco/adf-extensions'; +import { ContentManagementService } from '../../../services/content-management.service'; +import { + SnackbarErrorAction, + SnackbarInfoAction +} from '../../../store/actions/snackbar.actions'; + +@Component({ + selector: 'app-toggle-join-library', + template: ` + + `, + encapsulation: ViewEncapsulation.None, + host: { class: 'app-toggle-join-library' } +}) +export class ToggleJoinLibraryComponent { + selection$: Observable; + + constructor( + private store: Store, + private content: ContentManagementService + ) { + this.selection$ = this.store.select(appSelection); + } + + onToggleEvent(event) { + this.store.dispatch(new SnackbarInfoAction(event.i18nKey)); + + if (event.shouldReload) { + this.content.libraryJoined.next(); + } else { + this.content.joinLibraryToggle.next(); + } + } + + onErrorEvent(event) { + this.store.dispatch(new SnackbarErrorAction(event.i18nKey)); + } +} diff --git a/src/app/directives/document-list.directive.ts b/src/app/directives/document-list.directive.ts index a1730c463..2825637af 100644 --- a/src/app/directives/document-list.directive.ts +++ b/src/app/directives/document-list.directive.ts @@ -58,7 +58,8 @@ export class DocumentListDirective implements OnInit, OnDestroy { this.isLibrary = this.documentList.currentFolderId === '-mysites-' || // workaround for custom node list - this.router.url.endsWith('/libraries'); + this.router.url.endsWith('/libraries') || + this.router.url.startsWith('/search-libraries'); if (this.sortingPreferenceKey) { const current = this.documentList.sorting; diff --git a/src/app/directives/library-membership.directive.ts b/src/app/directives/library-membership.directive.ts new file mode 100644 index 000000000..5ea4e19de --- /dev/null +++ b/src/app/directives/library-membership.directive.ts @@ -0,0 +1,185 @@ +/*! + * @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 { + Directive, + EventEmitter, + HostListener, + Input, + OnChanges, + Output +} from '@angular/core'; +import { SiteEntry, SiteMembershipRequestBody } from 'alfresco-js-api'; +import { AlfrescoApiService } from '@alfresco/adf-core'; +import { BehaviorSubject, from } from 'rxjs'; + +@Directive({ + selector: '[acaLibraryMembership]', + exportAs: 'libraryMembership' +}) +export class LibraryMembershipDirective implements OnChanges { + targetSite: any = null; + + isJoinRequested: BehaviorSubject = new BehaviorSubject( + false + ); + + /** Site for which to toggle the membership request. */ + @Input('acaLibraryMembership') + selection: SiteEntry = null; + + @Output() toggle: EventEmitter = new EventEmitter(); + @Output() error: EventEmitter = new EventEmitter(); + + @HostListener('click') + onClick() { + this.toggleMembershipRequest(); + } + + constructor(private alfrescoApiService: AlfrescoApiService) {} + + ngOnChanges(changes) { + if ( + !changes.selection.currentValue || + !changes.selection.currentValue.entry + ) { + this.targetSite = null; + + return; + } + this.targetSite = changes.selection.currentValue.entry; + this.markMembershipRequest(); + } + + toggleMembershipRequest() { + if (!this.targetSite) { + return; + } + + if (this.targetSite.joinRequested) { + this.cancelJoinRequest().subscribe( + () => { + this.targetSite.joinRequested = false; + this.isJoinRequested.next(false); + const info = { + shouldReload: false, + i18nKey: 'APP.MESSAGES.INFO.JOIN_CANCELED' + }; + this.toggle.emit(info); + }, + error => { + const errWitMessage = { + error, + i18nKey: 'APP.MESSAGES.ERRORS.JOIN_CANCEL_FAILED' + }; + this.error.emit(errWitMessage); + } + ); + } + + if (!this.targetSite.joinRequested) { + this.joinLibraryRequest().subscribe( + createdMembership => { + this.targetSite.joinRequested = true; + this.isJoinRequested.next(true); + + if ( + createdMembership.entry && + createdMembership.entry.site && + createdMembership.entry.site.role + ) { + const info = { + shouldReload: true, + i18nKey: 'APP.MESSAGES.INFO.JOINED' + }; + this.toggle.emit(info); + } else { + const info = { + shouldReload: false, + i18nKey: 'APP.MESSAGES.INFO.JOIN_REQUESTED' + }; + this.toggle.emit(info); + } + }, + error => { + const errWitMessage = { + error, + i18nKey: 'APP.MESSAGES.ERRORS.JOIN_REQUEST_FAILED' + }; + this.error.emit(errWitMessage); + } + ); + } + } + + markMembershipRequest() { + if (!this.targetSite) { + return; + } + + this.getMembershipRequest().subscribe( + data => { + if (data.entry.id === this.targetSite.id) { + this.targetSite.joinRequested = true; + this.isJoinRequested.next(true); + } + }, + () => { + this.targetSite.joinRequested = false; + this.isJoinRequested.next(false); + } + ); + } + + private joinLibraryRequest() { + const memberBody = { + id: this.targetSite.id + }; + return from( + this.alfrescoApiService.peopleApi.addSiteMembershipRequest( + '-me-', + memberBody + ) + ); + } + + private cancelJoinRequest() { + return from( + this.alfrescoApiService.peopleApi.removeSiteMembershipRequest( + '-me-', + this.targetSite.id + ) + ); + } + + private getMembershipRequest() { + return from( + this.alfrescoApiService.peopleApi.getSiteMembershipRequest( + '-me-', + this.targetSite.id + ) + ); + } +} diff --git a/src/app/extensions/core.extensions.module.ts b/src/app/extensions/core.extensions.module.ts index 08ed74b84..65ae8adc6 100644 --- a/src/app/extensions/core.extensions.module.ts +++ b/src/app/extensions/core.extensions.module.ts @@ -47,6 +47,7 @@ import { LibraryStatusColumnComponent } from '../components/common/library-statu import { TrashcanNameColumnComponent } from '../components/common/trashcan-name-column/trashcan-name-column.component'; import { LocationLinkComponent } from '../components/common/location-link/location-link.component'; import { DocumentDisplayModeComponent } from '../components/toolbar/document-display-mode/document-display-mode.component'; +import { ToggleJoinLibraryComponent } from '../components/toolbar/toggle-join-library/toggle-join-library.component'; export function setupExtensions(service: AppExtensionService): Function { return () => service.load(); @@ -85,6 +86,7 @@ export class CoreExtensionsModule { 'app.components.tabs.versions': VersionsTabComponent, 'app.toolbar.toggleInfoDrawer': ToggleInfoDrawerComponent, 'app.toolbar.toggleFavorite': ToggleFavoriteComponent, + 'app.toolbar.toggleJoinLibrary': ToggleJoinLibraryComponent, 'app.toolbar.cardView': DocumentDisplayModeComponent, 'app.shared-link.toggleSharedLink': ToggleSharedComponent, 'app.columns.name': NameColumnComponent, @@ -110,6 +112,9 @@ export class CoreExtensionsModule { 'app.selection.file': app.hasFileSelected, 'app.selection.file.canShare': app.canShareFile, 'app.selection.library': app.hasLibrarySelected, + 'app.selection.isPrivateLibrary': app.isPrivateLibrary, + 'app.selection.hasLibraryRole': app.hasLibraryRole, + 'app.selection.hasNoLibraryRole': app.hasNoLibraryRole, 'app.selection.folder': app.hasFolderSelected, 'app.selection.folder.canUpdate': app.canUpdateSelectedFolder, diff --git a/src/app/extensions/evaluators/app.evaluators.ts b/src/app/extensions/evaluators/app.evaluators.ts index 8ae860f2c..03ae9f8e5 100644 --- a/src/app/extensions/evaluators/app.evaluators.ts +++ b/src/app/extensions/evaluators/app.evaluators.ts @@ -178,6 +178,35 @@ export function hasLibrarySelected( return library ? true : false; } +export function isPrivateLibrary( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + const library = context.selection.library; + return library + ? !!( + library.entry && + library.entry.visibility && + library.entry.visibility === 'PRIVATE' + ) + : false; +} + +export function hasLibraryRole( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + const library = context.selection.library; + return library ? !!(library.entry && library.entry.role) : false; +} + +export function hasNoLibraryRole( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + return !hasLibraryRole(context, ...args); +} + export function hasFileSelected( context: RuleContext, ...args: RuleParameter[] diff --git a/src/app/extensions/evaluators/navigation.evaluators.ts b/src/app/extensions/evaluators/navigation.evaluators.ts index 8775f300d..8955864f1 100644 --- a/src/app/extensions/evaluators/navigation.evaluators.ts +++ b/src/app/extensions/evaluators/navigation.evaluators.ts @@ -75,7 +75,9 @@ export function isLibraries( ...args: RuleParameter[] ): boolean { const { url } = context.navigation; - return url && url.endsWith('/libraries'); + return ( + url && (url.endsWith('/libraries') || url.startsWith('/search-libraries')) + ); } export function isNotLibraries( diff --git a/src/app/services/content-management.service.ts b/src/app/services/content-management.service.ts index 213394124..3dabf49ff 100644 --- a/src/app/services/content-management.service.ts +++ b/src/app/services/content-management.service.ts @@ -82,6 +82,8 @@ export class ContentManagementService { libraryDeleted = new Subject(); libraryCreated = new Subject(); libraryUpdated = new Subject(); + libraryJoined = new Subject(); + joinLibraryToggle = new Subject(); linksUnshared = new Subject(); favoriteAdded = new Subject>(); favoriteRemoved = new Subject>(); diff --git a/src/app/store/actions/library.actions.ts b/src/app/store/actions/library.actions.ts index 655e4040f..3ab9f1e40 100644 --- a/src/app/store/actions/library.actions.ts +++ b/src/app/store/actions/library.actions.ts @@ -45,6 +45,7 @@ export class NavigateLibraryAction implements Action { readonly type = NAVIGATE_LIBRARY; constructor(public payload?: string) {} } + export class UpdateLibraryAction implements Action { readonly type = UPDATE_LIBRARY; constructor(public payload?: SiteBody) {} diff --git a/src/app/store/effects/library.effects.ts b/src/app/store/effects/library.effects.ts index 3c9388946..8c1e6c034 100644 --- a/src/app/store/effects/library.effects.ts +++ b/src/app/store/effects/library.effects.ts @@ -33,8 +33,8 @@ import { CREATE_LIBRARY, NavigateLibraryAction, NAVIGATE_LIBRARY, - UPDATE_LIBRARY, - UpdateLibraryAction + UpdateLibraryAction, + UPDATE_LIBRARY } from '../actions'; import { ContentManagementService } from '../../services/content-management.service'; import { Store } from '@ngrx/store'; diff --git a/src/assets/app.extensions.json b/src/assets/app.extensions.json index 21b17f5a2..cfbea4a8b 100644 --- a/src/assets/app.extensions.json +++ b/src/assets/app.extensions.json @@ -101,6 +101,20 @@ { "type": "rule", "value": "app.navigation.isLibraries" } ] }, + { + "id": "app.libraries.toolbar.canToggleJoin", + "type": "core.every", + "parameters": [ + { "type": "rule", "value": "app.selection.library" }, + { "type": "rule", + "value": "core.not", + "parameters": [ + { "type": "rule", "value": "app.selection.isPrivateLibrary" } + ] + }, + { "type": "rule", "value": "app.selection.hasNoLibraryRole" } + ] + }, { "id": "app.toolbar.canCopyNode", "type": "core.every", @@ -387,6 +401,15 @@ "visible": "app.libraries.toolbar.info" } }, + { + "id": "app.toolbar.joinLibrary", + "type": "custom", + "order": 704, + "component": "app.toolbar.toggleJoinLibrary", + "rules": { + "visible": "app.libraries.toolbar.canToggleJoin" + } + }, { "id": "app.toolbar.more", "type": "menu", @@ -1277,7 +1300,8 @@ "title": "APP.DOCUMENT_LIST.COLUMNS.ROLE", "type": "text", "sortable": true, - "desktopOnly": true + "template": "app.columns.libraryRole", + "desktopOnly": false }, { "id": "app.libraries.visibility", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 2e32739fe..1f64c8240 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -162,7 +162,9 @@ "SHARE": "Share", "SHARE_EDIT": "Shared link settings", "PRINT": "Print", - "FULLSCREEN": "Activate full-screen mode" + "FULLSCREEN": "Activate full-screen mode", + "JOIN": "Join", + "CANCEL_JOIN": "Cancel join request" }, "DIALOGS": { "CONFIRM_PURGE": { @@ -227,7 +229,9 @@ "GENERIC": "There was a problem restoring {{ name }}" } }, - "DELETE_LIBRARY_FAILED": "Cannot delete the library" + "DELETE_LIBRARY_FAILED": "Cannot delete the library", + "JOIN_REQUEST_FAILED": "Cannot join the library", + "JOIN_CANCEL_FAILED": "Cannot cancel the request to join the library" }, "UPLOAD": { "ERROR": { @@ -272,7 +276,10 @@ "FAIL": "{{ failed }} couldn't be moved." } }, - "LIBRARY_DELETED": "Library deleted" + "LIBRARY_DELETED": "Library deleted", + "JOINED": "Library joined", + "JOIN_REQUESTED": "Join library request sent", + "JOIN_CANCELED": "Canceled the request to join the library" } }, "CONTENT_METADATA": { diff --git a/src/assets/images/join-library.svg b/src/assets/images/join-library.svg new file mode 100644 index 000000000..1a6186a5a --- /dev/null +++ b/src/assets/images/join-library.svg @@ -0,0 +1,35 @@ + + + + join-library + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +