[ACS-10165] create all libraries component (#4826)

* [ACS-10165] create all libraries component

* [ACS-10165] move translations, apply cr comments

* [ACS-10165] fix column tracking

* [ACS-10165] remove flag

* [ACS-10165] create base component for library pages

* [ACS-10165] fix build issue

* [ACS-10165] code review fixes
This commit is contained in:
Grzegorz Jaśkowski
2025-10-24 09:57:05 +02:00
committed by GitHub
parent 7e6b43653e
commit 3386af55dd
33 changed files with 981 additions and 443 deletions
@@ -175,6 +175,13 @@
"description": "APP.BROWSE.LIBRARIES.MENU.FAVORITE_LIBRARIES.SIDENAV_LINK.TOOLTIP",
"route": "favorite/libraries"
},
{
"id": "app.navbar.libraries.all",
"order": 350,
"title": "APP.BROWSE.LIBRARIES.MENU.ALL_LIBRARIES.SIDENAV_LINK.LABEL",
"description": "APP.BROWSE.LIBRARIES.MENU.ALL_LIBRARIES.SIDENAV_LINK.TOOLTIP",
"route": "all/libraries"
},
{
"id": "app.navbar.recentFiles",
"order": 400,
@@ -504,8 +511,7 @@
"component": "app.toolbar.toggleInfoDrawer",
"rules": {
"visible": [
"app.selection.library",
"isLibraryManager"
"app.selection.library"
]
}
},
@@ -1583,8 +1589,7 @@
"component": "app.components.tabs.library.metadata",
"rules": {
"visible": [
"app.selection.library",
"isLibraryManager"
"app.selection.library"
]
}
}
+10
View File
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "لا توجد مكتبات مفضلة",
"TEXT": "حدد مكتبة تريد العثور عليها بسهولة لاحقًا كمفضلة."
},
"ALL_LIBRARIES":{
"TITLE": "لم يتم العثور على مكتبة",
"TEXT": "أنشئ مكتبة جديدة للبدء"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "المكتبات المفضلة",
"TOOLTIP": "الوصول إلى مكتباتي المفضلة"
}
},
"ALL_LIBRARIES": {
"TITLE": "كل المكتبات",
"SIDENAV_LINK": {
"LABEL": "كل المكتبات"
}
}
},
"ERRORS": {
+10
View File
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "Žádné oblíbené knihovny souborů",
"TEXT": "Pro snadnější dostupnost můžete označit vybrané knihovny za oblíbené."
},
"ALL_LIBRARIES": {
"TITLE": "Nebyla nalezena žádná knihovna",
"TEXT": "Začněte vytvořením nové knihovny"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "Oblíbené knihovny",
"TOOLTIP": "Přejít do oblíbených knihoven"
}
},
"ALL_LIBRARIES": {
"TITLE": "Všechny knihovny",
"SIDENAV_LINK": {
"LABEL": "Všechny knihovny"
}
}
},
"ERRORS": {
+10
View File
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "Du har ikke angivet nogen biblioteker som favorit",
"TEXT": "Angiv et bibliotek som favorit for at gøre det nemt at finde biblioteket igen."
},
"ALL_LIBRARIES": {
"TITLE": "Intet bibliotek fundet",
"TEXT": "Opret et ny bibliotek, og kom i gang"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "Biblioteker, der er angivet som favorit",
"TOOLTIP": "Adgang til de biblioteker, jeg har angivet som favorit"
}
},
"ALL_LIBRARIES": {
"TITLE": "Alle biblioteker",
"SIDENAV_LINK": {
"LABEL": "Alle biblioteker"
}
}
},
"ERRORS": {
+10
View File
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "Keine Bibliotheken als Favoriten markiert",
"TEXT": "Markieren Sie eine Bibliothek als Favorit, um sie später leichter zu finden."
},
"ALL_LIBRARIES": {
"TITLE": "Keine Bibliothek gefunden",
"TEXT": "Erstellen Sie zu Beginn eine neue Bibliothek."
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "Als Favoriten markierte Bibliotheken",
"TOOLTIP": "Auf meine als Favoriten markierten Bibliotheken zugreifen"
}
},
"ALL_LIBRARIES": {
"TITLE": "Alle Bibliotheken",
"SIDENAV_LINK": {
"LABEL": "Alle Bibliotheken"
}
}
},
"ERRORS": {
+11
View File
@@ -135,6 +135,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "No Favorite Libraries",
"TEXT": "Favorite a library that you want to find easily later."
},
"ALL_LIBRARIES": {
"TITLE": "No library found",
"TEXT": "Create a new library to get started."
}
},
"MENU": {
@@ -151,6 +155,13 @@
"LABEL": "Favorite Libraries",
"TOOLTIP": "Access my favorite libraries"
}
},
"ALL_LIBRARIES": {
"TITLE": "All Libraries",
"SIDENAV_LINK": {
"LABEL": "All Libraries",
"TOOLTIP": "Access all libraries"
}
}
},
"ERRORS": {
+10
View File
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "No hay bibliotecas favoritas",
"TEXT": "Marcar como favorita una biblioteca para encontrarla más rápidamente."
},
"ALL_LIBRARIES": {
"TITLE": "No se ha encontrado ninguna biblioteca",
"TEXT ": "Crear una biblioteca nueva para empezar"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "Bibliotecas favoritas",
"TOOLTIP": "Acceder a mis bibliotecas favoritas"
}
},
"ALL_LIBRARIES": {
"TITLE": "Todas las bibliotecas",
"SIDENAV_LINK": {
"LABEL": "Todas las bibliotecas"
}
}
},
"ERRORS": {
+10
View File
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "Ei suosikkikirjastoja",
"TEXT": "Voit lisätä suosikkeihin kirjastoja, jotka haluat löytää helposti myöhemmin."
},
"ALL_LIBRARIES": {
"TITLE": "Yhtään kirjastoa ei löytynyt",
"TEXT": "Luo uusi kirjasto aloittaaksesi"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "Suosikkikirjastot",
"TOOLTIP": "Siirry omiin suosikkikirjastoihin"
}
},
"ALL_LIBRARIES": {
"TITLE": "Kaikki kirjastot",
"SIDENAV_LINK": {
"LABEL": "Kaikki kirjastot"
}
}
},
"ERRORS": {
+10
View File
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "Pas de bibliothèques favorites",
"TEXT": "Ajouter aux favoris une bibliothèque pour la retrouver facilement par la suite."
},
"ALL_LIBRARIES": {
"TITLE": "Aucune bibliothèque trouvée",
"TEXT": "Créez une nouvelle bibliothèque pour commencer"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "Bibliothèques favorites",
"TOOLTIP": "Accéder à mes bibliothèques favorites"
}
},
"ALL_LIBRARIES": {
"TITLE": "Toutes les bibliothèques",
"SIDENAV_LINK": {
"LABEL": "Toutes les bibliothèques"
}
}
},
"ERRORS": {
+10
View File
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "Nessuna libreria preferita",
"TEXT": "Imposta una libreria come preferita per trovarla facilmente in seguito."
},
"ALL_LIBRARIES": {
"TITLE": "Nessuna libreria trovata",
"TEXT": "Crea una nuova libreria per iniziare"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "Librerie preferite",
"TOOLTIP": "Accedi alle librerie preferite"
}
},
"ALL_LIBRARIES": {
"TITLE": "Tutte le librerie",
"SIDENAV_LINK": {
"LABEL": "Tutte le librerie"
}
}
},
"ERRORS": {
+10
View File
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "お気に入りのライブラリはありません",
"TEXT": "後で簡単に見つけられるようにライブラリをお気に入りに追加します。"
},
"ALL_LIBRARIES": {
"TITLE": "ライブラリが見つかりません",
"TEXT": "最初に新しいライブラリを作成してください"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "お気に入りのライブラリ",
"TOOLTIP": "お気に入りのライブラリにアクセス"
}
},
"ALL_LIBRARIES": {
"TITLE": "すべてのライブラリ",
"SIDENAV_LINK": {
"LABEL": "すべてのライブラリ"
}
}
},
"ERRORS": {
+10
View File
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "Ingen favorittbiblioteker",
"TEXT": "Legg et bibliotek til som favoritt, slik at du lett kan finne det senere."
},
"ALL_LIBRARIES": {
"TITLE": "Fant ingen biblioteker",
"TEXT": "Opprett et nytt bibliotek for å komme i gang"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "Favorittbiblioteker",
"TOOLTIP": "Gå inn i mine favorittbiblioteker"
}
},
"ALL_LIBRARIES": {
"TITLE": "Alle biblioteker",
"SIDENAV_LINK": {
"LABEL": "Alle biblioteker"
}
}
}
},
+10
View File
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "Geen favoriete bibliotheken",
"TEXT": "Voeg een bibliotheek toe aan uw favorieten zodat u deze later gemakkelijk kunt vinden."
},
"ALL_LIBRARIES": {
"TITLE": "Geen bibliotheek gevonden",
"TEXT": "Maak een nieuwe bibliotheek om aan de slag te gaan"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "Favoriete bibliotheken",
"TOOLTIP": "Toegang krijgen tot mijn favoriete bibliotheken"
}
},
"ALL_LIBRARIES": {
"TITLE": "Alle bibliotheken",
"SIDENAV_LINK": {
"LABEL": "Alle bibliotheken"
}
}
},
"ERRORS": {
+10
View File
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "Ingen favorittbiblioteker",
"TEXT": "Merk et bibliotek som favoritt for å enkelt finne det senere."
},
"ALL_LIBRARIES": {
"TITLE": "Fant ingen bibliotek",
"TEXT": "Opprett et nytt bibliotek for å komme i gang"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "Favorittbiblioteker",
"TOOLTIP": "Få tilgang til mine favorittbiblioteker"
}
},
"ALL_LIBRARIES": {
"TITLE": "Alle biblioteker",
"SIDENAV_LINK": {
"LABEL": "Alle biblioteker"
}
}
},
"ERRORS": {
+10
View File
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "Brak ulubionych bibliotek",
"TEXT": "Dodaj do ulubionych bibliotekę, którą później chcesz łatwo znaleźć."
},
"ALL_LIBRARIES": {
"TITLE": "Nie znaleziono biblioteki",
"TEXT": "Utwórz nową bibliotekę, aby rozpocząć"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "Ulubione biblioteki",
"TOOLTIP": "Uzyskaj dostęp do moich ulubionych bibliotek"
}
},
"ALL_LIBRARIES": {
"TITLE": "Wszystkie biblioteki",
"SIDENAV_LINK": {
"LABEL": "Wszystkie biblioteki"
}
}
},
"ERRORS": {
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "Nenhuma biblioteca favorita",
"TEXT": "Adicione uma biblioteca como favorita para facilitar o acesso a ela."
},
"ALL_LIBRARIES": {
"TITLE": "Nenhuma biblioteca encontrada",
"TEXT": "Crie uma nova biblioteca para iniciar"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "Bibliotecas favoritas",
"TOOLTIP": "Acessar minhas bibliotecas favoritas"
}
},
"ALL_LIBRARIES": {
"TITLE": "Todas as bibliotecas",
"SIDENAV_LINK": {
"LABEL": "Todas as bibliotecas"
}
}
},
"ERRORS": {
+10
View File
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "Нет избранных библиотек",
"TEXT": "Добавляйте библиотеки в избранные, чтобы быстро находить их, когда они понадобятся снова."
},
"ALL_LIBRARIES": {
"TITLE": "Библиотеки не найдены",
"TEXT": "Создайте новую библиотеку, чтобы приступить к работе"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "Избранные библиотеки",
"TOOLTIP": "Доступ к моим избранным библиотекам"
}
},
"ALL_LIBRARIES": {
"TITLE": "Все библиотеки",
"SIDENAV_LINK": {
"LABEL": "Все библиотеки"
}
}
},
"ERRORS": {
+10
View File
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "Inga favoritbibliotek",
"TEXT": "Favoritmarkera ett bibliotek som du enkelt vill hitta senare."
},
"ALL_LIBRARIES": {
"TITLE": "Inget bibliotek hittades",
"TEXT": "Skapa ett nytt bibliotek för att komma igång"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "Favoritbibliotek",
"TOOLTIP": "Kom åt mina favoritbibliotek"
}
},
"ALL_LIBRARIES": {
"TITLE": "Alla bibliotek",
"SIDENAV_LINK": {
"LABEL": "Alla bibliotek"
}
}
},
"ERRORS": {
@@ -133,6 +133,10 @@
"FAVORITE_LIBRARIES": {
"TITLE": "无收藏库",
"TEXT": "收藏您希望以后轻松查找的库"
},
"ALL_LIBRARIES": {
"TITLE": "未发现任何库",
"TEXT": "创建新库以开始使用"
}
},
"MENU": {
@@ -149,6 +153,12 @@
"LABEL": "收藏库",
"TOOLTIP": "访问我的收藏库"
}
},
"ALL_LIBRARIES": {
"TITLE": "所有库",
"SIDENAV_LINK": {
"LABEL": "所有库"
}
}
},
"ERRORS": {
@@ -42,6 +42,7 @@ import { TrashcanComponent } from './components/trashcan/trashcan.component';
import { ShellLayoutComponent } from '@alfresco/adf-core/shell';
import { SearchAiResultsComponent } from './components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component';
import { SavedSearchesSmartListComponent } from './components/search/search-save/list/smart-list/saved-searches-smart-list.component';
import { LibraryListComponent } from './components/library-list/library-list.component';
export const CONTENT_ROUTES: ExtensionRoute[] = [
{
@@ -230,6 +231,24 @@ export const CONTENT_LAYOUT_ROUTES: Route[] = [
...createViewRoutes('libraries')
]
},
{
path: 'all',
children: [
{
path: '',
pathMatch: 'full',
redirectTo: 'libraries'
},
{
path: 'libraries',
component: LibraryListComponent,
data: {
title: 'APP.BROWSE.LIBRARIES.MENU.ALL_LIBRARIES.TITLE',
sortingPreferenceKey: 'all-libraries'
}
}
]
},
{
path: 'favorites',
data: {
@@ -1,94 +1,14 @@
<aca-page-layout>
<div class="aca-page-layout-header">
<h1 class="aca-page-title">
{{ (selectedRowItemsCount < 1 ? 'APP.BROWSE.LIBRARIES.MENU.FAVORITE_LIBRARIES.TITLE' : 'APP.HEADER.SELECTED') | translate: { count: selectedRowItemsCount } }}
</h1>
<aca-toolbar [items]="actions" />
</div>
<div class="aca-page-layout-content">
<div class="aca-main-content">
<adf-document-list
#documentList
acaDocumentList
acaContextActions
[node]="$any(list)"
[loading]="isLoading"
[selectionMode]="'multiple'"
[multiselect]="true"
[navigate]="false"
[sorting]="['title', 'asc']"
[sortingMode]="'client'"
[displayCheckboxesOnHover]="true"
[preselectNodes]="selectedNodesState?.nodes"
(node-dblclick)="handleNodeClick($event)"
[imageResolver]="imageResolver"
(selectedItemsCountChanged)="onSelectedItemsCountChanged($event)"
[isResizingEnabled]="true"
[blurOnResize]="false"
(name-click)="handleNodeClick($event)"
>
<adf-custom-empty-content-template>
<adf-empty-content
icon="library_books"
[title]="'APP.BROWSE.LIBRARIES.EMPTY_STATE.FAVORITE_LIBRARIES.TITLE'"
subtitle="APP.BROWSE.LIBRARIES.EMPTY_STATE.FAVORITE_LIBRARIES.TEXT"
/>
</adf-custom-empty-content-template>
<data-columns>
<ng-container *ngFor="let column of columns; trackBy: trackByColumnId">
<ng-container *ngIf="column.template && !(column.desktopOnly && isSmallScreen)">
<data-column
[id]="column.id"
[draggable]="column.draggable"
[resizable]="column.resizable"
[key]="column.key"
[title]="column.title"
[type]="column.type"
[format]="column.format"
[class]="column.class"
[sortable]="column.sortable"
[isHidden]="column.isHidden"
[sortingKey]="column.sortingKey || column.key"
>
<ng-template let-context>
<adf-dynamic-column [id]="column.template" [context]="context" />
</ng-template>
</data-column>
</ng-container>
<ng-container *ngIf="!column.template && !(column.desktopOnly && isSmallScreen)">
<data-column
[id]="column.id"
[key]="column.key"
[draggable]="column.draggable"
[resizable]="column.resizable"
[title]="column.title"
[type]="column.type"
[format]="column.format"
[class]="column.class"
[sortable]="column.sortable"
[isHidden]="column.isHidden"
[sortingKey]="column.sortingKey || column.key"
/>
</ng-container>
</ng-container>
</data-columns>
</adf-document-list>
<adf-pagination
[target]="documentList"
[pagination]="pagination"
(changePageSize)="onChangePageSize($event)"
(changePageNumber)="onChange($event)"
(nextPage)="onChange($event)"
(prevPage)="onChange($event)"
/>
</div>
<div class="aca-sidebar" *ngIf="infoDrawerOpened$ | async">
<aca-info-drawer [node]="selection.last" />
</div>
</div>
</aca-page-layout>
<aca-libraries-base
[list]="list"
[isLoading]="isLoading"
[pagination]="pagination"
[columns]="columns"
[navigateRoute]="'favorite/libraries'"
[titleKey]="'APP.BROWSE.LIBRARIES.MENU.FAVORITE_LIBRARIES.TITLE'"
[emptyTitleKey]="'APP.BROWSE.LIBRARIES.EMPTY_STATE.FAVORITE_LIBRARIES.TITLE'"
[emptySubtitleKey]="'APP.BROWSE.LIBRARIES.EMPTY_STATE.FAVORITE_LIBRARIES.TEXT'"
(changePageSize)="onChangePageSize($event)"
(changePageNumber)="getList($event)"
(nextPage)="getList($event)"
(prevPage)="getList($event)"
/>
@@ -23,189 +23,129 @@
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { UserPreferencesService } from '@alfresco/adf-core';
import { AlfrescoApiService } from '@alfresco/adf-content-services';
import { FavoriteLibrariesComponent } from './favorite-libraries.component';
import { AppTestingModule } from '../../testing/app-testing.module';
import { AppHookService, ContentApiService } from '@alfresco/aca-shared';
import { AppExtensionService, AppHookService, ContentApiService } from '@alfresco/aca-shared';
import { provideEffects } from '@ngrx/effects';
import { RouterEffects } from '@alfresco/aca-shared/store';
import { of, throwError } from 'rxjs';
import { LibraryEffects } from '../../store/effects';
import { NodeEntry } from '@alfresco/js-api';
import { getTitleElementText } from '../../testing/test-utils';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { SiteEntry } from '@alfresco/js-api/typings';
import { libraryColumnsPresetMock, favoriteLibrariesMock, libraryPaginationMock } from '../../mock/libraries-mock';
describe('FavoriteLibrariesComponent', () => {
let fixture: ComponentFixture<FavoriteLibrariesComponent>;
let component: FavoriteLibrariesComponent;
let alfrescoApi: AlfrescoApiService;
let userPreference: UserPreferencesService;
let contentApiService: ContentApiService;
let router: Router;
let page;
let appHookService: AppHookService;
beforeEach(() => {
page = {
list: {
entries: [{ entry: { id: 1 } }, { entry: { id: 2 } }],
pagination: { data: 'data' }
}
};
});
let appExtensionService: AppExtensionService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule, FavoriteLibrariesComponent, MatSnackBarModule],
providers: [provideEffects([RouterEffects, LibraryEffects])]
providers: [provideEffects([LibraryEffects])]
});
fixture = TestBed.createComponent(FavoriteLibrariesComponent);
component = fixture.componentInstance;
alfrescoApi = TestBed.inject(AlfrescoApiService);
contentApiService = TestBed.inject(ContentApiService);
userPreference = TestBed.inject(UserPreferencesService);
appHookService = TestBed.inject(AppHookService);
alfrescoApi.reset();
router = TestBed.inject(Router);
appExtensionService = TestBed.inject(AppExtensionService);
spyOn(contentApiService, 'getNode').and.returnValue(of({ entry: { id: 'libraryId' } } as NodeEntry));
});
describe('on initialization', () => {
it('should set data', () => {
spyOn(contentApiService, 'getFavoriteLibraries').and.returnValue(of(page));
fixture.detectChanges();
it('should set data', () => {
spyOn(contentApiService, 'getFavoriteLibraries').and.returnValue(of(favoriteLibrariesMock));
fixture.detectChanges();
expect(component.list).toBe(page);
expect(component.pagination).toBe(page.list.pagination);
});
expect(component.list).toBe(favoriteLibrariesMock);
expect(component.pagination).toBe(favoriteLibrariesMock.list.pagination);
});
it('should get data with user preference pagination size', () => {
userPreference.paginationSize = 1;
spyOn(contentApiService, 'getFavoriteLibraries').and.returnValue(of(page));
it('should get data with user preference pagination size', () => {
userPreference.paginationSize = 1;
spyOn(contentApiService, 'getFavoriteLibraries').and.returnValue(of(favoriteLibrariesMock));
fixture.detectChanges();
fixture.detectChanges();
expect(contentApiService.getFavoriteLibraries).toHaveBeenCalledWith('-me-', {
maxItems: userPreference.paginationSize
});
});
it('should set data on error', () => {
spyOn(contentApiService, 'getFavoriteLibraries').and.returnValue(throwError('error'));
fixture.detectChanges();
expect(component.list).toBe(null);
expect(component.pagination).toBe(null);
expect(component.isLoading).toBe(false);
});
it('should set title based on selectedRowItemsCount', () => {
fixture.detectChanges();
expect(getTitleElementText(fixture)).toBe('APP.BROWSE.LIBRARIES.MENU.FAVORITE_LIBRARIES.TITLE');
component.selectedRowItemsCount = 5;
fixture.detectChanges();
expect(getTitleElementText(fixture)).toBe('APP.HEADER.SELECTED');
expect(contentApiService.getFavoriteLibraries).toHaveBeenCalledWith('-me-', {
maxItems: userPreference.paginationSize
});
});
describe('Node navigation', () => {
it('does not navigate when id is not passed', () => {
spyOn(router, 'navigate').and.stub();
component.navigateTo(null);
it('should set data on error', () => {
spyOn(contentApiService, 'getFavoriteLibraries').and.returnValue(throwError(() => 'error'));
fixture.detectChanges();
expect(router.navigate).not.toHaveBeenCalled();
});
it('does not navigate when id is not passed', () => {
spyOn(router, 'navigate').and.stub();
component.navigateTo({
entry: {
guid: 'test-guid',
visibility: 'PUBLIC',
role: 'SiteConsumer'
}
} as SiteEntry);
expect(router.navigate).toHaveBeenCalledWith(['favorite/libraries', 'libraryId']);
});
expect(component.list).toBe(null);
expect(component.pagination).toBe(null);
expect(component.isLoading).toBe(false);
});
describe('Reload on actions', () => {
it('should set columns from extensions on init', () => {
appExtensionService.documentListPresets.libraries = libraryColumnsPresetMock;
fixture.detectChanges();
expect(component.columns).toEqual(appExtensionService.documentListPresets.favoriteLibraries);
});
it('should handle no columns preset in extensions', () => {
appExtensionService.documentListPresets.favoriteLibraries = undefined;
component.ngOnInit();
expect(component.columns.length).toBe(0);
});
describe('Library hooks', () => {
beforeEach(() => {
spyOn(contentApiService, 'getFavoriteLibraries').and.returnValue(of(page));
spyOn(contentApiService, 'getFavoriteLibraries').and.returnValue(of(favoriteLibrariesMock));
fixture.detectChanges();
});
it('should reload on libraryDeleted action', () => {
it('should reload on libraryDeleted hook', () => {
appHookService.libraryDeleted.next('');
expect(contentApiService.getFavoriteLibraries).toHaveBeenCalled();
});
it('should reload on libraryUpdated action', () => {
it('should reload on libraryUpdated hook', () => {
appHookService.libraryUpdated.next({} as any);
expect(contentApiService.getFavoriteLibraries).toHaveBeenCalled();
});
it('should reload on favoriteLibraryToggle action', () => {
it('should reload on favoriteLibraryToggle hook', () => {
appHookService.favoriteLibraryToggle.next();
expect(contentApiService.getFavoriteLibraries).toHaveBeenCalled();
});
it('should reload on libraryJoined action', () => {
it('should reload on libraryJoined hook', () => {
appHookService.libraryJoined.next();
expect(contentApiService.getFavoriteLibraries).toHaveBeenCalled();
});
it('should reload on libraryLeft action', () => {
it('should reload on libraryLeft hook', () => {
appHookService.libraryLeft.next({} as any);
expect(contentApiService.getFavoriteLibraries).toHaveBeenCalled();
});
});
describe('Pagination', () => {
let pagination;
beforeEach(() => {
pagination = {
count: 100,
hasMoreItems: true,
totalItems: 300,
skipCount: 25,
maxItems: 25
};
});
it('should get list with pagination data onChange event', () => {
spyOn(contentApiService, 'getFavoriteLibraries').and.returnValue(of(page));
component.onChange(pagination);
expect(contentApiService.getFavoriteLibraries).toHaveBeenCalledWith('-me-', pagination);
});
it('should get list with pagination data onChangePageSize event', () => {
spyOn(contentApiService, 'getFavoriteLibraries').and.returnValue(of(page));
spyOn(contentApiService, 'getFavoriteLibraries').and.returnValue(of(favoriteLibrariesMock));
component.onChangePageSize(pagination);
component.onChangePageSize(libraryPaginationMock);
expect(contentApiService.getFavoriteLibraries).toHaveBeenCalledWith('-me-', pagination);
expect(contentApiService.getFavoriteLibraries).toHaveBeenCalledWith('-me-', libraryPaginationMock);
});
it('should set preference page size onChangePageSize event', () => {
spyOn(contentApiService, 'getFavoriteLibraries').and.returnValue(of(page));
spyOn(contentApiService, 'getFavoriteLibraries').and.returnValue(of(favoriteLibrariesMock));
component.onChangePageSize(pagination);
component.onChangePageSize(libraryPaginationMock);
expect(userPreference.paginationSize).toBe(pagination.maxItems);
expect(userPreference.paginationSize).toBe(libraryPaginationMock.maxItems);
});
});
});
@@ -23,63 +23,30 @@
*/
import { ChangeDetectorRef, Component, OnInit, ViewEncapsulation } from '@angular/core';
import { FavoritePaging, Pagination, SiteEntry } from '@alfresco/js-api';
import {
AppHookService,
ContentApiService,
ContextActionsDirective,
InfoDrawerComponent,
PageComponent,
PageLayoutComponent,
ToolbarComponent
} from '@alfresco/aca-shared';
import { NavigateLibraryAction } from '@alfresco/aca-shared/store';
import {
CustomEmptyContentTemplateDirective,
DataColumnComponent,
DataColumnListComponent,
EmptyContentComponent,
PaginationComponent,
UserPreferencesService
} from '@alfresco/adf-core';
import { DocumentListPresetRef, DynamicColumnComponent } from '@alfresco/adf-extensions';
import { CommonModule } from '@angular/common';
import { DocumentListDirective } from '../../directives/document-list.directive';
import { TranslatePipe } from '@ngx-translate/core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { FavoritePaging, Pagination } from '@alfresco/js-api';
import { ContentApiService } from '@alfresco/aca-shared';
import { UserPreferencesService } from '@alfresco/adf-core';
import { DocumentListPresetRef } from '@alfresco/adf-extensions';
import { LibrariesBaseComponent } from '../libraries-base/libraries-base.component';
@Component({
imports: [
CommonModule,
DocumentListDirective,
ContextActionsDirective,
PaginationComponent,
InfoDrawerComponent,
PageLayoutComponent,
TranslatePipe,
ToolbarComponent,
EmptyContentComponent,
DynamicColumnComponent,
DataColumnListComponent,
DataColumnComponent,
DocumentListComponent,
CustomEmptyContentTemplateDirective
],
selector: 'aca-favorite-libraries',
standalone: true,
templateUrl: './favorite-libraries.component.html',
imports: [LibrariesBaseComponent],
encapsulation: ViewEncapsulation.None
})
export class FavoriteLibrariesComponent extends PageComponent implements OnInit {
export class FavoriteLibrariesComponent extends LibrariesBaseComponent implements OnInit {
pagination: Pagination = new Pagination({
skipCount: 0,
maxItems: 25,
totalItems: 0
});
isLoading = false;
list: FavoritePaging;
list: FavoritePaging = null;
columns: DocumentListPresetRef[] = [];
constructor(
private appHookService: AppHookService,
private contentApiService: ContentApiService,
private preferences: UserPreferencesService,
private changeDetectorRef: ChangeDetectorRef
@@ -92,36 +59,22 @@ export class FavoriteLibrariesComponent extends PageComponent implements OnInit
this.getList({ maxItems: this.preferences.paginationSize });
this.subscriptions = this.subscriptions.concat([
this.subscriptions.push(
this.appHookService.libraryDeleted.subscribe(() => this.reloadList()),
this.appHookService.libraryUpdated.subscribe(() => this.reloadList()),
this.appHookService.libraryJoined.subscribe(() => this.reloadList()),
this.appHookService.libraryLeft.subscribe(() => this.reloadList()),
this.appHookService.favoriteLibraryToggle.subscribe(() => this.reloadList())
]);
);
this.columns = this.extensions.documentListPresets.favoriteLibraries || [];
}
navigateTo(node: SiteEntry) {
if (node?.entry?.guid) {
this.store.dispatch(new NavigateLibraryAction(node.entry, 'favorite/libraries'));
}
}
handleNodeClick(event: Event) {
this.navigateTo((event as CustomEvent).detail?.node);
}
onChangePageSize(pagination: Pagination) {
this.preferences.paginationSize = pagination.maxItems;
this.getList(pagination);
}
onChange(pagination: Pagination) {
this.getList(pagination);
}
private getList(pagination: Pagination) {
getList(pagination: Pagination) {
this.isLoading = true;
this.contentApiService.getFavoriteLibraries('-me-', pagination).subscribe(
(favoriteLibraries: FavoritePaging) => {
@@ -0,0 +1,96 @@
<aca-page-layout>
<div class="aca-page-layout-header">
<h1 class="aca-page-title">
@if (selectedRowItemsCount < 1) {
{{ titleKey | translate: { count: selectedRowItemsCount } }}
} @else {
{{ 'APP.HEADER.SELECTED' | translate: { count: selectedRowItemsCount } }}
}
</h1>
<aca-toolbar [items]="actions" />
</div>
<div class="aca-page-layout-content">
<div class="aca-main-content">
<adf-document-list
#documentList
acaDocumentList
acaContextActions
[currentFolderId]="currentFolderId"
[node]="$any(list)"
[loading]="isLoading"
[selectionMode]="'multiple'"
[multiselect]="true"
[navigate]="false"
[sorting]="['title', 'asc']"
[sortingMode]="'client'"
[displayCheckboxesOnHover]="true"
[preselectNodes]="selectedNodesState?.nodes"
(node-dblclick)="handleNodeClick($event)"
[imageResolver]="imageResolver"
[isResizingEnabled]="true"
[blurOnResize]="false"
(name-click)="handleNodeClick($event)"
>
<adf-custom-empty-content-template>
<adf-empty-content
icon="library_books"
[title]="emptyTitleKey | translate"
[subtitle]="emptySubtitleKey | translate"
/>
</adf-custom-empty-content-template>
<data-columns>
@for (column of columns; track trackByColumnId($index, column)) {
@if (column.template && !(column.desktopOnly && isSmallScreen)) {
<data-column
[id]="column.id"
[draggable]="column.draggable"
[resizable]="column.resizable"
[key]="column.key"
[title]="column.title"
[type]="column.type"
[format]="column.format"
[class]="column.class"
[sortable]="column.sortable"
[isHidden]="column.isHidden"
[sortingKey]="column.sortingKey || column.key"
>
<ng-template let-context>
<adf-dynamic-column [id]="column.template" [context]="context" />
</ng-template>
</data-column>
} @else if (!column.template && !(column.desktopOnly && isSmallScreen)) {
<data-column
[id]="column.id"
[key]="column.key"
[draggable]="column.draggable"
[resizable]="column.resizable"
[title]="column.title"
[type]="column.type"
[format]="column.format"
[class]="column.class"
[sortable]="column.sortable"
[isHidden]="column.isHidden"
[sortingKey]="column.sortingKey || column.key"
/>
}
}
</data-columns>
</adf-document-list>
<adf-pagination
[target]="documentList"
[pagination]="pagination"
(changePageSize)="changePageSize.emit($event)"
(changePageNumber)="changePageNumber.emit($event)"
(nextPage)="nextPage.emit($event)"
(prevPage)="prevPage.emit($event)"
/>
</div>
@if (infoDrawerOpened$ | async) {
<aca-info-drawer class="aca-sidebar" [node]="$any(selection?.last)" />
}
</div>
</aca-page-layout>
@@ -0,0 +1,145 @@
/*!
* Copyright © 2005-2025 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { LibrariesBaseComponent } from './libraries-base.component';
import { AppTestingModule } from '../../testing/app-testing.module';
import { CustomEmptyContentTemplateDirective, PaginationComponent, UnitTestingUtils } from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { provideEffects } from '@ngrx/effects';
import { NavigateLibraryAction, RouterEffects } from '@alfresco/aca-shared/store';
import { LibraryEffects } from '../../store/effects';
import { getTitleElementText } from '../../testing/test-utils';
import { librariesMock, libraryPaginationMock } from '../../mock/libraries-mock';
import { DebugElement } from '@angular/core';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { initialState } from '@alfresco/aca-shared';
describe('LibrariesBaseComponent', () => {
let component: LibrariesBaseComponent;
let fixture: ComponentFixture<LibrariesBaseComponent>;
let unitTestingUtils: UnitTestingUtils;
let store: MockStore;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppTestingModule, LibrariesBaseComponent],
providers: [provideEffects([RouterEffects, LibraryEffects]), provideMockStore({ initialState })]
}).compileComponents();
fixture = TestBed.createComponent(LibrariesBaseComponent);
component = fixture.componentInstance;
unitTestingUtils = new UnitTestingUtils(fixture.debugElement);
store = TestBed.inject(MockStore);
component.list = librariesMock;
fixture.detectChanges();
});
it('should set title based on selectedRowItemsCount', () => {
component.titleKey = 'TITLE_TRANSLATION_KEY';
fixture.detectChanges();
expect(getTitleElementText(fixture)).toBe('TITLE_TRANSLATION_KEY');
component.selectedRowItemsCount = 5;
fixture.detectChanges();
expect(getTitleElementText(fixture)).toBe('APP.HEADER.SELECTED');
});
it('should show empty state when list is empty', () => {
component.list = null;
component.emptyTitleKey = 'EMPTY_TITLE';
component.emptySubtitleKey = 'EMPTY_SUBTITLE';
fixture.detectChanges();
const emptyContent = unitTestingUtils.getByDirective(CustomEmptyContentTemplateDirective);
expect(emptyContent.nativeElement.textContent).toContain('EMPTY_TITLE');
expect(emptyContent.nativeElement.textContent).toContain('EMPTY_SUBTITLE');
});
describe('Pagination events', () => {
let paginationComponent: DebugElement;
beforeEach(() => {
paginationComponent = unitTestingUtils.getByDirective(PaginationComponent);
});
it('should emit changePageSize event', () => {
spyOn(component.changePageSize, 'emit');
paginationComponent.triggerEventHandler('changePageSize', libraryPaginationMock);
expect(component.changePageSize.emit).toHaveBeenCalledWith(libraryPaginationMock);
});
it('should emit changePageNumber event', () => {
spyOn(component.changePageNumber, 'emit');
paginationComponent.triggerEventHandler('changePageNumber', libraryPaginationMock);
expect(component.changePageNumber.emit).toHaveBeenCalledWith(libraryPaginationMock);
});
it('should emit nextPage event', () => {
spyOn(component.nextPage, 'emit');
paginationComponent.triggerEventHandler('nextPage', libraryPaginationMock);
expect(component.nextPage.emit).toHaveBeenCalledWith(libraryPaginationMock);
});
it('should emit prevPage event', () => {
spyOn(component.prevPage, 'emit');
paginationComponent.triggerEventHandler('prevPage', libraryPaginationMock);
expect(component.prevPage.emit).toHaveBeenCalledWith(libraryPaginationMock);
});
});
describe('Node navigation', () => {
let documentListComponent: DebugElement;
beforeEach(() => {
documentListComponent = unitTestingUtils.getByDirective(DocumentListComponent);
});
it('should not navigate when node is null or missing guid', () => {
spyOn(store, 'dispatch').and.stub();
documentListComponent.triggerEventHandler('node-dblclick', { detail: { node: null } });
expect(store.dispatch).not.toHaveBeenCalled();
documentListComponent.triggerEventHandler('node-dblclick', { detail: { node: { entry: {} } } });
expect(store.dispatch).not.toHaveBeenCalled();
});
it('should handle node double click', () => {
spyOn(store, 'dispatch').and.stub();
const siteEntry = librariesMock.list.entries[0].entry;
documentListComponent.triggerEventHandler('node-dblclick', { detail: { node: { entry: siteEntry } } });
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new NavigateLibraryAction(siteEntry) }));
});
it('should handle name click', () => {
spyOn(store, 'dispatch').and.stub();
const siteEntry = librariesMock.list.entries[0].entry;
documentListComponent.triggerEventHandler('name-click', { detail: { node: { entry: siteEntry } } });
expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new NavigateLibraryAction(siteEntry) }));
});
});
});
@@ -0,0 +1,98 @@
/*!
* Copyright © 2005-2025 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 { CommonModule } from '@angular/common';
import { Component, Input, Output, EventEmitter, ViewEncapsulation, inject } from '@angular/core';
import { TranslatePipe } from '@ngx-translate/core';
import {
ContextActionsDirective,
ToolbarComponent,
InfoDrawerComponent,
PageLayoutComponent,
PageComponent,
AppHookService
} from '@alfresco/aca-shared';
import { DocumentListDirective } from '../../directives/document-list.directive';
import {
CustomEmptyContentTemplateDirective,
DataColumnComponent,
DataColumnListComponent,
EmptyContentComponent,
PaginationComponent
} from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { DocumentListPresetRef, DynamicColumnComponent } from '@alfresco/adf-extensions';
import { NavigateLibraryAction } from '@alfresco/aca-shared/store';
import { FavoritePaging, Pagination, SiteEntry, SitePaging } from '@alfresco/js-api';
@Component({
selector: 'aca-libraries-base',
standalone: true,
templateUrl: './libraries-base.component.html',
imports: [
CommonModule,
TranslatePipe,
ToolbarComponent,
InfoDrawerComponent,
PageLayoutComponent,
DocumentListDirective,
ContextActionsDirective,
DocumentListComponent,
DataColumnListComponent,
DataColumnComponent,
DynamicColumnComponent,
CustomEmptyContentTemplateDirective,
EmptyContentComponent,
PaginationComponent
],
encapsulation: ViewEncapsulation.None
})
export class LibrariesBaseComponent extends PageComponent {
@Input() titleKey: string;
@Input() list: SitePaging | FavoritePaging;
@Input() isLoading: boolean;
@Input() emptyTitleKey: string;
@Input() emptySubtitleKey: string;
@Input() pagination: Pagination;
@Input() currentFolderId: string;
@Input() columns: DocumentListPresetRef[];
@Input() navigateRoute: string;
@Output() changePageSize = new EventEmitter<Pagination>();
@Output() changePageNumber = new EventEmitter<Pagination>();
@Output() nextPage = new EventEmitter<Pagination>();
@Output() prevPage = new EventEmitter<Pagination>();
protected appHookService = inject(AppHookService);
private navigateTo(node: SiteEntry) {
if (node?.entry?.guid) {
this.store.dispatch(new NavigateLibraryAction(node.entry, this.navigateRoute));
}
}
handleNodeClick(event: Event) {
this.navigateTo((event as CustomEvent).detail?.node);
}
}
@@ -1,86 +1,7 @@
<aca-page-layout>
<div class="aca-page-layout-header">
<h1 class="aca-page-title">
{{ (selectedRowItemsCount < 1 ? 'APP.BROWSE.LIBRARIES.MENU.MY_LIBRARIES.TITLE' : 'APP.HEADER.SELECTED') | translate: { count: selectedRowItemsCount } }}
</h1>
<aca-toolbar [items]="actions" />
</div>
<div class="aca-page-layout-content">
<div class="aca-main-content">
<adf-document-list
#documentList
acaDocumentList
acaContextActions
[currentFolderId]="'-mysites-'"
[selectionMode]="'multiple'"
[multiselect]="true"
[navigate]="false"
[sorting]="['title', 'asc']"
[sortingMode]="'client'"
[preselectNodes]="selectedNodesState?.nodes"
[imageResolver]="imageResolver"
[displayCheckboxesOnHover]="true"
[isResizingEnabled]="true"
(selectedItemsCountChanged)="onSelectedItemsCountChanged($event)"
[blurOnResize]="false"
(node-dblclick)="handleNodeClick($event)"
(name-click)="handleNodeClick($event)"
>
<adf-custom-empty-content-template>
<adf-empty-content
icon="library_books"
[title]="'APP.BROWSE.LIBRARIES.EMPTY_STATE.FILE_LIBRARIES.TITLE'"
subtitle="APP.BROWSE.LIBRARIES.EMPTY_STATE.FILE_LIBRARIES.TEXT"
/>
</adf-custom-empty-content-template>
<data-columns>
<ng-container *ngFor="let column of columns; trackBy: trackByColumnId">
<ng-container *ngIf="column.template && !(column.desktopOnly && isSmallScreen)">
<data-column
[id]="column.id"
[key]="column.key"
[title]="column.title"
[type]="column.type"
[format]="column.format"
[draggable]="column.draggable"
[resizable]="column.resizable"
[class]="column.class"
[sortable]="column.sortable"
[isHidden]="column.isHidden"
[sortingKey]="column.sortingKey || column.key"
>
<ng-template let-context>
<adf-dynamic-column [id]="column.template" [context]="context" />
</ng-template>
</data-column>
</ng-container>
<ng-container *ngIf="!column.template && !(column.desktopOnly && isSmallScreen)">
<data-column
[id]="column.id"
[key]="column.key"
[title]="column.title"
[type]="column.type"
[format]="column.format"
[class]="column.class"
[draggable]="column.draggable"
[resizable]="column.resizable"
[sortable]="column.sortable"
[isHidden]="column.isHidden"
[sortingKey]="column.sortingKey || column.key"
/>
</ng-container>
</ng-container>
</data-columns>
</adf-document-list>
<adf-pagination acaPagination [target]="documentList" />
</div>
<div class="aca-sidebar" *ngIf="infoDrawerOpened$ | async">
<aca-info-drawer [node]="$any(selection).library" />
</div>
</div>
</aca-page-layout>
<aca-libraries-base
[currentFolderId]="'-mysites-'"
[columns]="columns"
[titleKey]="'APP.BROWSE.LIBRARIES.MENU.MY_LIBRARIES.TITLE'"
[emptyTitleKey]="'APP.BROWSE.LIBRARIES.EMPTY_STATE.FILE_LIBRARIES.TITLE'"
[emptySubtitleKey]="'APP.BROWSE.LIBRARIES.EMPTY_STATE.FILE_LIBRARIES.TEXT'"
/>
@@ -23,32 +23,20 @@
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { AlfrescoApiService } from '@alfresco/adf-content-services';
import { LibrariesComponent } from './libraries.component';
import { AppTestingModule } from '../../testing/app-testing.module';
import { provideEffects } from '@ngrx/effects';
import { LibraryEffects } from '../../store/effects';
import { ContentApiService } from '@alfresco/aca-shared';
import { getTitleElementText } from '../../testing/test-utils';
import { AppExtensionService, AppHookService, ContentApiService } from '@alfresco/aca-shared';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { libraryColumnsPresetMock, librariesMock } from '../../mock/libraries-mock';
describe('LibrariesComponent', () => {
let fixture: ComponentFixture<LibrariesComponent>;
let component: LibrariesComponent;
let alfrescoApi: AlfrescoApiService;
let contentApiService: ContentApiService;
let router: Router;
let page;
beforeEach(() => {
page = {
list: {
entries: [{ entry: { id: 1 } }, { entry: { id: 2 } }],
pagination: { data: 'data' }
}
};
});
let appHookService: AppHookService;
let appExtensionService: AppExtensionService;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -59,34 +47,49 @@ describe('LibrariesComponent', () => {
fixture = TestBed.createComponent(LibrariesComponent);
component = fixture.componentInstance;
alfrescoApi = TestBed.inject(AlfrescoApiService);
contentApiService = TestBed.inject(ContentApiService);
alfrescoApi.reset();
router = TestBed.inject(Router);
appHookService = TestBed.inject(AppHookService);
appExtensionService = TestBed.inject(AppExtensionService);
const sitesApi = contentApiService.sitesApi;
spyOn(sitesApi, 'listSites').and.returnValue(Promise.resolve(page));
spyOn(sitesApi, 'listSites').and.returnValue(Promise.resolve(librariesMock));
spyOn(sitesApi, 'listSiteMembershipsForPerson').and.returnValue(Promise.resolve({}));
});
describe('Initialization', () => {
it('should set title to MY_LIBRARIES.TITLE based on selectedRowItemsCount', () => {
fixture.detectChanges();
expect(getTitleElementText(fixture)).toBe('APP.BROWSE.LIBRARIES.MENU.MY_LIBRARIES.TITLE');
component.selectedRowItemsCount = 2;
fixture.detectChanges();
expect(getTitleElementText(fixture)).toBe('APP.HEADER.SELECTED');
});
it('should set columns from extensions on init', () => {
appExtensionService.documentListPresets.libraries = libraryColumnsPresetMock;
fixture.detectChanges();
expect(component.columns).toEqual(appExtensionService.documentListPresets.libraries);
});
describe('Node navigation', () => {
it('does not navigate when id is not passed', () => {
spyOn(router, 'navigate').and.stub();
component.navigateTo(null);
it('should handle no columns preset in extensions', () => {
appExtensionService.documentListPresets.libraries = undefined;
component.ngOnInit();
expect(component.columns.length).toBe(0);
});
expect(router.navigate).not.toHaveBeenCalled();
describe('Library hooks', () => {
let reloadSpy: jasmine.Spy<() => void>;
beforeEach(() => {
reloadSpy = spyOn(component, 'reload');
fixture.detectChanges();
});
it('should reload on libraryDeleted hook', () => {
appHookService.libraryDeleted.next('');
expect(reloadSpy).toHaveBeenCalled();
});
it('should reload on libraryUpdated hook', () => {
appHookService.libraryUpdated.next(librariesMock.list.entries[0]);
expect(reloadSpy).toHaveBeenCalled();
});
it('should reload on libraryLeft hook', () => {
appHookService.libraryLeft.next('');
expect(reloadSpy).toHaveBeenCalled();
});
});
});
@@ -22,59 +22,17 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { NavigateLibraryAction } from '@alfresco/aca-shared/store';
import { SiteEntry } from '@alfresco/js-api';
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import {
AppHookService,
ContextActionsDirective,
InfoDrawerComponent,
PageComponent,
PageLayoutComponent,
PaginationDirective,
ToolbarComponent
} from '@alfresco/aca-shared';
import { DocumentListPresetRef, DynamicColumnComponent } from '@alfresco/adf-extensions';
import { CommonModule } from '@angular/common';
import {
CustomEmptyContentTemplateDirective,
DataColumnComponent,
DataColumnListComponent,
EmptyContentComponent,
PaginationComponent
} from '@alfresco/adf-core';
import { DocumentListDirective } from '../../directives/document-list.directive';
import { TranslatePipe } from '@ngx-translate/core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { LibrariesBaseComponent } from '../libraries-base/libraries-base.component';
@Component({
imports: [
CommonModule,
DocumentListDirective,
ContextActionsDirective,
PaginationComponent,
PaginationDirective,
InfoDrawerComponent,
PageLayoutComponent,
TranslatePipe,
ToolbarComponent,
EmptyContentComponent,
DynamicColumnComponent,
DocumentListComponent,
DataColumnComponent,
DataColumnListComponent,
CustomEmptyContentTemplateDirective
],
selector: 'aca-libraries',
standalone: true,
templateUrl: './libraries.component.html',
imports: [LibrariesBaseComponent],
encapsulation: ViewEncapsulation.None
})
export class LibrariesComponent extends PageComponent implements OnInit {
columns: DocumentListPresetRef[] = [];
constructor(private appHookService: AppHookService) {
super();
}
export class LibrariesComponent extends LibrariesBaseComponent implements OnInit {
ngOnInit() {
super.ngOnInit();
@@ -86,14 +44,4 @@ export class LibrariesComponent extends PageComponent implements OnInit {
this.columns = this.extensions.documentListPresets.libraries || [];
}
navigateTo(node: SiteEntry) {
if (node?.entry?.guid) {
this.store.dispatch(new NavigateLibraryAction(node.entry));
}
}
handleNodeClick(event: Event) {
this.navigateTo((event as CustomEvent).detail?.node);
}
}
@@ -0,0 +1,13 @@
<aca-libraries-base
[list]="list"
[isLoading]="isLoading"
[pagination]="pagination"
[columns]="columns"
[titleKey]="'APP.BROWSE.LIBRARIES.MENU.ALL_LIBRARIES.TITLE'"
[emptyTitleKey]="'APP.BROWSE.LIBRARIES.EMPTY_STATE.ALL_LIBRARIES.TITLE'"
[emptySubtitleKey]="'APP.BROWSE.LIBRARIES.EMPTY_STATE.ALL_LIBRARIES.TEXT'"
(changePageSize)="onChangePageSize($event)"
(changePageNumber)="getList($event)"
(nextPage)="getList($event)"
(prevPage)="getList($event)"
/>
@@ -0,0 +1,136 @@
/*!
* Copyright © 2005-2025 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserPreferencesService } from '@alfresco/adf-core';
import { SitesService } from '@alfresco/adf-content-services';
import { LibraryListComponent } from './library-list.component';
import { AppTestingModule } from '../../testing/app-testing.module';
import { AppExtensionService, AppHookService } from '@alfresco/aca-shared';
import { provideEffects } from '@ngrx/effects';
import { Observable, of, throwError } from 'rxjs';
import { LibraryEffects } from '../../store/effects';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { SitePaging } from '@alfresco/js-api';
import { libraryColumnsPresetMock, librariesMock, libraryPaginationMock } from '../../mock/libraries-mock';
describe('LibraryListComponent', () => {
let fixture: ComponentFixture<LibraryListComponent>;
let component: LibraryListComponent;
let userPreference: UserPreferencesService;
let sitesService: SitesService;
let appHookService: AppHookService;
let getSitesSpy: jasmine.Spy<(options?: any) => Observable<SitePaging>>;
let appExtensionService: AppExtensionService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule, LibraryListComponent, MatSnackBarModule],
providers: [provideEffects([LibraryEffects])]
});
fixture = TestBed.createComponent(LibraryListComponent);
component = fixture.componentInstance;
sitesService = TestBed.inject(SitesService);
userPreference = TestBed.inject(UserPreferencesService);
appHookService = TestBed.inject(AppHookService);
appExtensionService = TestBed.inject(AppExtensionService);
getSitesSpy = spyOn(sitesService, 'getSites');
getSitesSpy.and.returnValue(of(librariesMock));
fixture.detectChanges();
});
it('should set data', () => {
expect(component.list).toBe(librariesMock);
expect(component.pagination).toBe(librariesMock.list.pagination);
});
it('should get data with user preference pagination size', () => {
userPreference.paginationSize = 1;
component.ngOnInit();
expect(sitesService.getSites).toHaveBeenCalledWith({ maxItems: 1 });
});
it('should set data on error', () => {
getSitesSpy.and.returnValue(throwError(() => 'error'));
component.ngOnInit();
expect(component.list).toBeNull();
expect(component.pagination).toBeNull();
expect(component.isLoading).toBe(false);
});
it('should set columns from extensions on init', () => {
appExtensionService.documentListPresets.libraries = libraryColumnsPresetMock;
component.ngOnInit();
expect(component.columns).toEqual(appExtensionService.documentListPresets.libraries);
});
it('should handle no columns preset in extensions', () => {
appExtensionService.documentListPresets.libraries = undefined;
component.ngOnInit();
expect(component.columns.length).toBe(0);
});
describe('Library hooks', () => {
it('should reload on libraryDeleted action', () => {
appHookService.libraryDeleted.next('');
expect(sitesService.getSites).toHaveBeenCalled();
});
it('should reload on libraryUpdated action', () => {
appHookService.libraryUpdated.next(librariesMock.list.entries[0]);
expect(sitesService.getSites).toHaveBeenCalled();
});
it('should reload on libraryJoined action', () => {
appHookService.libraryJoined.next();
expect(sitesService.getSites).toHaveBeenCalled();
});
it('should reload on libraryLeft action', () => {
appHookService.libraryLeft.next('');
expect(sitesService.getSites).toHaveBeenCalled();
});
});
describe('Pagination', () => {
it('should get list with pagination data onChange event', () => {
component.getList(libraryPaginationMock);
expect(sitesService.getSites).toHaveBeenCalledWith(libraryPaginationMock);
});
it('should get list with pagination data onChangePageSize event', () => {
component.onChangePageSize(libraryPaginationMock);
expect(sitesService.getSites).toHaveBeenCalledWith(libraryPaginationMock);
});
it('should set preference page size onChangePageSize event', () => {
component.onChangePageSize(libraryPaginationMock);
expect(userPreference.paginationSize).toBe(libraryPaginationMock.maxItems);
});
});
});
@@ -0,0 +1,94 @@
/*!
* Copyright © 2005-2025 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 { ChangeDetectorRef, Component, OnInit, ViewEncapsulation } from '@angular/core';
import { Pagination, SitePaging } from '@alfresco/js-api';
import { UserPreferencesService } from '@alfresco/adf-core';
import { SitesService } from '@alfresco/adf-content-services';
import { LibrariesBaseComponent } from '../libraries-base/libraries-base.component';
@Component({
selector: 'aca-library-list',
standalone: true,
templateUrl: './library-list.component.html',
imports: [LibrariesBaseComponent],
encapsulation: ViewEncapsulation.None
})
export class LibraryListComponent extends LibrariesBaseComponent implements OnInit {
pagination = new Pagination({
skipCount: 0,
maxItems: 25,
totalItems: 0
});
isLoading = false;
list: SitePaging = null;
constructor(
private readonly preferences: UserPreferencesService,
private readonly changeDetectorRef: ChangeDetectorRef,
private readonly sitesService: SitesService
) {
super();
}
ngOnInit() {
super.ngOnInit();
this.getList({ maxItems: this.preferences.paginationSize });
this.subscriptions.push(
this.appHookService.libraryDeleted.subscribe(() => this.reloadList()),
this.appHookService.libraryUpdated.subscribe(() => this.reloadList()),
this.appHookService.libraryJoined.subscribe(() => this.reloadList()),
this.appHookService.libraryLeft.subscribe(() => this.reloadList())
);
this.columns = this.extensions.documentListPresets.libraries || [];
}
onChangePageSize(pagination: Pagination) {
this.preferences.paginationSize = pagination.maxItems;
this.getList(pagination);
}
getList(pagination: Pagination) {
this.isLoading = true;
this.sitesService.getSites(pagination).subscribe({
next: (libraryList: SitePaging) => {
this.list = libraryList;
this.pagination = libraryList.list.pagination;
this.isLoading = false;
this.changeDetectorRef.detectChanges();
},
error: () => {
this.list = null;
this.pagination = null;
this.isLoading = false;
}
});
}
private reloadList() {
this.reload();
this.getList(this.pagination);
}
}
@@ -0,0 +1,66 @@
/*!
* Copyright © 2005-2025 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 { DocumentListPresetRef } from '@alfresco/adf-extensions';
import { FavoritePaging, Pagination, SitePaging } from '@alfresco/js-api';
export const librariesMock: SitePaging = {
list: {
entries: [
{ entry: { id: '1', guid: '1', title: 'Library 1', visibility: 'public' } },
{ entry: { id: '2', guid: '2', title: 'Library 2', visibility: 'private' } }
],
pagination: { count: 25, skipCount: 0 }
}
};
export const favoriteLibrariesMock: FavoritePaging = {
list: {
entries: [
{ entry: { targetGuid: '1', target: { id: '1', guid: '1', title: 'Favorite Library 1', visibility: 'public' } } },
{ entry: { targetGuid: '2', target: { id: '2', guid: '2', title: 'Favorite Library 2', visibility: 'private' } } }
],
pagination: { count: 25, skipCount: 0 }
}
};
export const libraryColumnsPresetMock: DocumentListPresetRef[] = [
{
key: 'name',
type: 'text',
sortable: false,
template: '',
desktopOnly: false,
sortingKey: '',
id: ''
}
];
export const libraryPaginationMock: Pagination = {
count: 100,
hasMoreItems: true,
totalItems: 300,
skipCount: 25,
maxItems: 25
};