diff --git a/src/app/community-list-page/community-list-service.ts b/src/app/community-list-page/community-list-service.ts index bbf1c7cdb5d..8a505d28c7e 100644 --- a/src/app/community-list-page/community-list-service.ts +++ b/src/app/community-list-page/community-list-service.ts @@ -25,6 +25,7 @@ import { ShowMoreFlatNode } from './show-more-flat-node.model'; import { FindListOptions } from '../core/data/find-list-options.model'; import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface'; import { v4 as uuidv4 } from 'uuid'; +import { reorderZcuPublicationCollections } from '../shared/zcu-collection-order'; // Helper method to combine and flatten an array of observables of flatNode arrays export const combineAndFlatten = (obsList: Observable[]): Observable => @@ -255,7 +256,8 @@ export class CommunityListService { getFirstCompletedRemoteData(), map((rd: RemoteData>) => { if (hasValue(rd) && hasValue(rd.payload)) { - let nodes = rd.payload.page + // apply the ZCU collection display order in the community browse tree + let nodes = reorderZcuPublicationCollections(rd.payload.page) .map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode)); if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) { nodes = [...nodes, showMoreFlatNode(`collection-${uuidv4()}`, level + 1, communityFlatNode)]; diff --git a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts index ed14096ce03..e4721fd1d39 100644 --- a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts +++ b/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts @@ -88,10 +88,20 @@ export class CommunityPageSubCollectionListComponent implements OnInit, OnDestro }); }) ).subscribe((results) => { - this.subCollectionsRDObs.next(results); + this.subCollectionsRDObs.next(this.applyCustomCollectionOrder(results)); })); } + /** + * Extension point for theme/customer-specific ordering of the current page of collections. + * + * @param rd the RemoteData holding the current page of collections + * @returns the RemoteData to emit, potentially with a reordered page + */ + protected applyCustomCollectionOrder(rd: RemoteData>): RemoteData> { + return rd; + } + ngOnDestroy(): void { this.paginationService.clearPagination(this.config?.id); this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe()); diff --git a/src/app/shared/zcu-collection-order.spec.ts b/src/app/shared/zcu-collection-order.spec.ts new file mode 100644 index 00000000000..788c805aa08 --- /dev/null +++ b/src/app/shared/zcu-collection-order.spec.ts @@ -0,0 +1,114 @@ +import { Collection } from '../core/shared/collection.model'; +import { + reorderZcuPublicationCollections, + ZCU_ARTICLES_COLLECTION_PREFIX, + ZCU_BOOKPARTS_COLLECTION_PREFIX, +} from './zcu-collection-order'; + +function fakeCollection(name: string): Collection { + return { get name(): string { return name; } } as Collection; +} + +function names(collections: Collection[]): string[] { + return collections.map((collection: Collection) => collection.name); +} + +describe('reorderZcuPublicationCollections', () => { + it('uses the Czech title prefixes', () => { + expect(ZCU_BOOKPARTS_COLLECTION_PREFIX).toBe('Kapitoly v knihách'); + expect(ZCU_ARTICLES_COLLECTION_PREFIX).toBe('Články'); + }); + + it('moves articles directly after book parts when book parts is 2nd (-> articles 3rd)', () => { + const input = [ + fakeCollection('Habilitace'), + fakeCollection('Kapitoly v knihách'), + fakeCollection('Sborníky'), + fakeCollection('Články'), + ]; + + expect(names(reorderZcuPublicationCollections(input))) + .toEqual(['Habilitace', 'Kapitoly v knihách', 'Články', 'Sborníky']); + }); + + it('moves articles to 2nd when book parts is 1st', () => { + const input = [ + fakeCollection('Kapitoly v knihách'), + fakeCollection('Sborníky'), + fakeCollection('Zprávy'), + fakeCollection('Články'), + ]; + + expect(names(reorderZcuPublicationCollections(input))) + .toEqual(['Kapitoly v knihách', 'Články', 'Sborníky', 'Zprávy']); + }); + + it('matches the bilingual, department-suffixed names used in production', () => { + const input = [ + fakeCollection('Disertační práce'), + fakeCollection('Kapitoly v knihách / Bookparts (KAE)'), + fakeCollection('Konferenční příspěvky'), + fakeCollection('Monografie a kolektivní monografie'), + fakeCollection('Zprávy'), + fakeCollection('Články / Articles (KAE)'), + ]; + + expect(names(reorderZcuPublicationCollections(input))).toEqual([ + 'Disertační práce', + 'Kapitoly v knihách / Bookparts (KAE)', + 'Články / Articles (KAE)', + 'Konferenční příspěvky', + 'Monografie a kolektivní monografie', + 'Zprávy', + ]); + }); + + it('matches mixed variants (plain articles, bilingual book parts)', () => { + const input = [ + fakeCollection('Kapitoly v knihách / Bookparts'), + fakeCollection('Sborníky'), + fakeCollection('Články'), + ]; + + expect(names(reorderZcuPublicationCollections(input))) + .toEqual(['Kapitoly v knihách / Bookparts', 'Články', 'Sborníky']); + }); + + it('never moves book parts out of its natural position', () => { + const input = [ + fakeCollection('Abstrakty'), + fakeCollection('Habilitace'), + fakeCollection('Kapitoly v knihách'), + fakeCollection('Články'), + ]; + + expect(names(reorderZcuPublicationCollections(input))) + .toEqual(['Abstrakty', 'Habilitace', 'Kapitoly v knihách', 'Články']); + }); + + it('leaves the list unchanged when articles is already directly after book parts', () => { + const input = [ + fakeCollection('Habilitace'), + fakeCollection('Kapitoly v knihách'), + fakeCollection('Články'), + fakeCollection('Sborníky'), + ]; + + expect(reorderZcuPublicationCollections(input)).toBe(input); + }); + + it('leaves the list unchanged when an anchor collection is missing', () => { + expect(names(reorderZcuPublicationCollections([ + fakeCollection('Knihy'), fakeCollection('Články'), + ]))).toEqual(['Knihy', 'Články']); + + expect(names(reorderZcuPublicationCollections([ + fakeCollection('Knihy'), fakeCollection('Kapitoly v knihách'), + ]))).toEqual(['Knihy', 'Kapitoly v knihách']); + }); + + it('handles empty and single-element input without error', () => { + expect(reorderZcuPublicationCollections([])).toEqual([]); + expect(names(reorderZcuPublicationCollections([fakeCollection('Knihy')]))).toEqual(['Knihy']); + }); +}); diff --git a/src/app/shared/zcu-collection-order.ts b/src/app/shared/zcu-collection-order.ts new file mode 100644 index 00000000000..7d3a89d3402 --- /dev/null +++ b/src/app/shared/zcu-collection-order.ts @@ -0,0 +1,42 @@ +import { Collection } from '../core/shared/collection.model'; + +/** + * Title prefix of the collection that anchors the ordering. + */ +export const ZCU_BOOKPARTS_COLLECTION_PREFIX = 'Kapitoly v knihách'; + +/** + * Title prefix of the collection pinned directly after book parts. + */ +export const ZCU_ARTICLES_COLLECTION_PREFIX = 'Články'; + +function nameStartsWith(collection: Collection, prefix: string): boolean { + const name = collection?.name; + return typeof name === 'string' && name.trim().toLocaleLowerCase().startsWith(prefix.toLocaleLowerCase()); +} + +/** + * Reorders a list of collections so that the articles collection is shown immediately after + * the book parts collection, while every other collection keeps its original position. + */ +export function reorderZcuPublicationCollections(collections: Collection[]): Collection[] { + if (!Array.isArray(collections) || collections.length < 2) { + return collections; + } + + const bookPartsIndex = collections.findIndex((collection: Collection) => nameStartsWith(collection, ZCU_BOOKPARTS_COLLECTION_PREFIX)); + const articlesIndex = collections.findIndex((collection: Collection) => nameStartsWith(collection, ZCU_ARTICLES_COLLECTION_PREFIX)); + + if (bookPartsIndex === -1 || articlesIndex === -1) { + return collections; + } + if (articlesIndex === bookPartsIndex + 1) { + return collections; + } + + const articles = collections[articlesIndex]; + const withoutArticles = collections.filter((_: Collection, index: number) => index !== articlesIndex); + const insertAt = withoutArticles.findIndex((collection: Collection) => nameStartsWith(collection, ZCU_BOOKPARTS_COLLECTION_PREFIX)) + 1; + + return [...withoutArticles.slice(0, insertAt), articles, ...withoutArticles.slice(insertAt)]; +} diff --git a/src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts b/src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts new file mode 100644 index 00000000000..bc24c142386 --- /dev/null +++ b/src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts @@ -0,0 +1,96 @@ +import { Collection } from '../../../../../app/core/shared/collection.model'; +import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component'; + +/** + * Builds a minimal Collection-like stub that only exposes the `name` getter used by the + * reordering logic under test. + * + * @param name the collection name (dc.title) to expose + * @returns an object typed as Collection for the purposes of these tests + */ +function fakeCollection(name: string): Collection { + return { get name(): string { return name; } } as Collection; +} + +/** + * Maps a list of collections to their names for concise assertions. + * + * @param collections the collections to map + * @returns the ordered list of collection names + */ +function names(collections: Collection[]): string[] { + return collections.map((collection: Collection) => collection.name); +} + +const BOOKPARTS_COLLECTION_NAME = 'Kapitoly v knihách'; +const ARTICLES_COLLECTION_NAME = 'Články'; + +describe('CommunityPageSubCollectionListComponent (custom theme) reorderCollections', () => { + let component: CommunityPageSubCollectionListComponent; + + const reorder = (collections: Collection[]): Collection[] => + (component as any).reorderCollections(collections); + + beforeEach(() => { + component = new CommunityPageSubCollectionListComponent(null, null, null); + }); + + it('shows "Články" directly after "Kapitoly v knihách" (book parts 2nd -> articles 3rd)', () => { + const input = [ + fakeCollection('Alfa'), + fakeCollection(BOOKPARTS_COLLECTION_NAME), + fakeCollection('Sborníky'), + fakeCollection(ARTICLES_COLLECTION_NAME), + ]; + + const result = reorder(input); + + expect(names(result)).toEqual(['Alfa', BOOKPARTS_COLLECTION_NAME, ARTICLES_COLLECTION_NAME, 'Sborníky']); + expect(names(result).indexOf(ARTICLES_COLLECTION_NAME)) + .toBe(names(result).indexOf(BOOKPARTS_COLLECTION_NAME) + 1); + }); + + it('shows "Články" 2nd when "Kapitoly v knihách" is 1st', () => { + const input = [ + fakeCollection(BOOKPARTS_COLLECTION_NAME), + fakeCollection('Sborníky'), + fakeCollection(ARTICLES_COLLECTION_NAME), + ]; + + expect(names(reorder(input))).toEqual([BOOKPARTS_COLLECTION_NAME, ARTICLES_COLLECTION_NAME, 'Sborníky']); + }); + + it('leaves the list unchanged when "Kapitoly v knihách" is missing', () => { + const input = [ + fakeCollection('Alfa'), + fakeCollection(ARTICLES_COLLECTION_NAME), + fakeCollection('Zeta'), + ]; + + expect(names(reorder(input))).toEqual(['Alfa', ARTICLES_COLLECTION_NAME, 'Zeta']); + }); + + it('leaves the list unchanged when "Články" is missing', () => { + const input = [ + fakeCollection('Alfa'), + fakeCollection(BOOKPARTS_COLLECTION_NAME), + fakeCollection('Zeta'), + ]; + + expect(names(reorder(input))).toEqual(['Alfa', BOOKPARTS_COLLECTION_NAME, 'Zeta']); + }); + + it('uses the exact diacritic collection names', () => { + expect(BOOKPARTS_COLLECTION_NAME).toBe('Kapitoly v knihách'); + expect(ARTICLES_COLLECTION_NAME).toBe('Články'); + + const input = [ + fakeCollection('Alfa'), + fakeCollection('Kapitoly v knihach'), + fakeCollection('Clanky'), + fakeCollection('Zeta'), + ]; + + expect(names(reorder(input))).toEqual(['Alfa', 'Kapitoly v knihach', 'Clanky', 'Zeta']); + }); +}); diff --git a/src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts b/src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts index a228fe1071e..9c46039e093 100644 --- a/src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts +++ b/src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts @@ -1,6 +1,10 @@ import { Component } from '@angular/core'; import { CommunityPageSubCollectionListComponent as BaseComponent } from '../../../../../app/community-page/sub-collection-list/community-page-sub-collection-list.component'; +import { RemoteData } from '../../../../../app/core/data/remote-data'; +import { PaginatedList } from '../../../../../app/core/data/paginated-list.model'; +import { Collection } from '../../../../../app/core/shared/collection.model'; +import { reorderZcuPublicationCollections } from '../../../../../app/shared/zcu-collection-order'; @Component({ selector: 'ds-community-page-sub-collection-list', @@ -9,4 +13,32 @@ import { CommunityPageSubCollectionListComponent as BaseComponent } // templateUrl: './community-page-sub-collection-list.component.html', templateUrl: '../../../../../app/community-page/sub-collection-list/community-page-sub-collection-list.component.html' }) -export class CommunityPageSubCollectionListComponent extends BaseComponent {} +export class CommunityPageSubCollectionListComponent extends BaseComponent { + + /** + * Reorders the current page of collections so that "Články" is shown directly after + * "Kapitoly v knihách", while every other collection keeps its existing alphabetical position. + * + * @param rd the RemoteData holding the current page of collections + * @returns the RemoteData with a reordered page, or unchanged when the page is empty + */ + protected applyCustomCollectionOrder(rd: RemoteData>): RemoteData> { + const page = rd?.payload?.page; + if (Array.isArray(page) && page.length > 0) { + rd.payload.page = this.reorderCollections(page); + } + return rd; + } + + /** + * Reorders the current page of collections using the shared ZCU ordering rule, + * moving "Články" to sit directly after "Kapitoly v knihách". + * + * @param collections the collections of the current page (alphabetical by dc.title) + * @returns a new, reordered array, or the original array when the rule does not apply + */ + protected reorderCollections(collections: Collection[]): Collection[] { + return reorderZcuPublicationCollections(collections); + } + +}