From 9314ef722d8770a1f728fd34f5af3bf25f453883 Mon Sep 17 00:00:00 2001 From: Matus Kasak Date: Wed, 2 Sep 2026 15:59:49 +0200 Subject: [PATCH 1/6] =?UTF-8?q?ZCU-PUB/feat(community):=20show=20"=C4=8Cl?= =?UTF-8?q?=C3=A1nky"=20collection=203rd,=20after=20"Kapitoly=20v=20knih?= =?UTF-8?q?=C3=A1ch"=20(#953)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hardcode the display order for the ZCU publications community so that the "Články" collection appears in 3rd position, immediately after "Kapitoly v knihách", while all other collections keep their existing alphabetical order. Approach: the base sub-collection-list component gains a no-op `applyCustomCollectionOrder` seam applied to each fetched page, keeping behavior identical for every other theme/customer. The custom theme overrides it to pin the two named collections via a small, unit-tested pure `reorderCollections` helper. Assumption: only the "Články -> 3rd" constraint is encoded here; a full configurable custom ordering would be the separate Option B feature. Co-Authored-By: Claude Opus 4.8 --- ...nity-page-sub-collection-list.component.ts | 16 ++- ...page-sub-collection-list.component.spec.ts | 97 +++++++++++++++++++ ...nity-page-sub-collection-list.component.ts | 54 ++++++++++- 3 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts 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..57594971036 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,24 @@ 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. + * + * The default implementation is a no-op that returns the RemoteData unchanged, so behavior + * stays identical for every theme and customer. Themes that need a bespoke display order + * (e.g. a hardcoded pinned position for a specific collection) override this method. + * + * @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/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..310dbc79aa8 --- /dev/null +++ b/src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts @@ -0,0 +1,97 @@ +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 SECOND_COLLECTION_NAME = 'Kapitoly v knihách'; +const THIRD_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('pins "Články" to the 3rd position, directly after "Kapitoly v knihách"', () => { + const input = [ + fakeCollection('Alfa'), + fakeCollection(SECOND_COLLECTION_NAME), + fakeCollection(THIRD_COLLECTION_NAME), + fakeCollection('Zeta'), + ]; + + const result = reorder(input); + + expect(names(result)).toEqual(['Alfa', SECOND_COLLECTION_NAME, THIRD_COLLECTION_NAME, 'Zeta']); + expect(result[2].name).toBe(THIRD_COLLECTION_NAME); + expect(result[1].name).toBe(SECOND_COLLECTION_NAME); + expect(names(result).indexOf(THIRD_COLLECTION_NAME)) + .toBe(names(result).indexOf(SECOND_COLLECTION_NAME) + 1); + }); + + it('leaves the list unchanged when "Kapitoly v knihách" is missing', () => { + const input = [ + fakeCollection('Alfa'), + fakeCollection(THIRD_COLLECTION_NAME), + fakeCollection('Zeta'), + ]; + + expect(names(reorder(input))).toEqual(['Alfa', THIRD_COLLECTION_NAME, 'Zeta']); + }); + + it('leaves the list unchanged when "Články" is missing', () => { + const input = [ + fakeCollection('Alfa'), + fakeCollection(SECOND_COLLECTION_NAME), + fakeCollection('Zeta'), + ]; + + expect(names(reorder(input))).toEqual(['Alfa', SECOND_COLLECTION_NAME, 'Zeta']); + }); + + it('leaves the list unchanged when there are no other collections', () => { + const input = [ + fakeCollection(SECOND_COLLECTION_NAME), + fakeCollection(THIRD_COLLECTION_NAME), + ]; + + expect(names(reorder(input))).toEqual([SECOND_COLLECTION_NAME, THIRD_COLLECTION_NAME]); + }); + + it('uses the exact diacritic collection names', () => { + expect(SECOND_COLLECTION_NAME).toBe('Kapitoly v knihách'); + expect(THIRD_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..1f08d7f00ba 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,19 @@ 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'; + +/** + * Name of the collection pinned to the 2nd position for the ZCU publications community. + */ +const SECOND_COLLECTION_NAME = 'Kapitoly v knihách'; + +/** + * Name of the collection pinned to the 3rd position for the ZCU publications community. + */ +const THIRD_COLLECTION_NAME = 'Články'; @Component({ selector: 'ds-community-page-sub-collection-list', @@ -9,4 +22,43 @@ 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 "Kapitoly v knihách" is shown 2nd and + * "Články" 3rd, while all other collections keep their existing alphabetical order. + * + * @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; + } + + /** + * Pure helper that reorders a list of collections to pin "Kapitoly v knihách" to the 2nd + * position and "Články" to the 3rd, keeping every other collection in its original order. + * + * The rule only applies when both pinned collections are present and there is at least one + * other collection; otherwise the list is returned unchanged. The result is + * `[others[0], second, third, ...others.slice(1)]`. + * + * @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[] { + const second = collections.find((collection: Collection) => collection.name === SECOND_COLLECTION_NAME); + const third = collections.find((collection: Collection) => collection.name === THIRD_COLLECTION_NAME); + const others = collections.filter((collection: Collection) => collection !== second && collection !== third); + + if (second && third && others.length >= 1) { + return [others[0], second, third, ...others.slice(1)]; + } + return collections; + } + +} From 11c5865bd3c31a6e5de0b37c4425f5545c42686f Mon Sep 17 00:00:00 2001 From: Matus Kasak Date: Thu, 3 Sep 2026 10:18:51 +0200 Subject: [PATCH 2/6] =?UTF-8?q?ZCU-PUB/feat(community):=20also=20apply=20"?= =?UTF-8?q?=C4=8Cl=C3=A1nky"=203rd=20order=20in=20the=20community=20browse?= =?UTF-8?q?=20tree=20(#953)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The initial fix only reordered the community *detail* page (the themed sub-collection list). The "Communities & Collections" browse tree (/community-list) renders collections through CommunityListService and was still showing the default alphabetical order (Články last). Extract the ordering rule into a shared pure helper (shared/zcu-collection-order.ts) and apply it in both places: - the custom-theme sub-collection-list override (delegates to the helper), - CommunityListService (reorders each fetched collection page in the tree). Co-Authored-By: Claude Opus 4.8 --- .../community-list-service.ts | 4 +- src/app/shared/zcu-collection-order.spec.ts | 70 +++++++++++++++++++ src/app/shared/zcu-collection-order.ts | 38 ++++++++++ ...nity-page-sub-collection-list.component.ts | 28 ++------ 4 files changed, 115 insertions(+), 25 deletions(-) create mode 100644 src/app/shared/zcu-collection-order.spec.ts create mode 100644 src/app/shared/zcu-collection-order.ts diff --git a/src/app/community-list-page/community-list-service.ts b/src/app/community-list-page/community-list-service.ts index bbf1c7cdb5d..db0bb5c569c 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 + // issue #953: 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/shared/zcu-collection-order.spec.ts b/src/app/shared/zcu-collection-order.spec.ts new file mode 100644 index 00000000000..4058320ed25 --- /dev/null +++ b/src/app/shared/zcu-collection-order.spec.ts @@ -0,0 +1,70 @@ +import { Collection } from '../core/shared/collection.model'; +import { + reorderZcuPublicationCollections, + ZCU_SECOND_COLLECTION_NAME, + ZCU_THIRD_COLLECTION_NAME, +} from './zcu-collection-order'; + +/** + * Builds a minimal Collection-like stub that only exposes the `name` getter used by the ordering. + * + * @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); +} + +describe('reorderZcuPublicationCollections', () => { + it('uses the exact diacritic pinned names', () => { + expect(ZCU_SECOND_COLLECTION_NAME).toBe('Kapitoly v knihách'); + expect(ZCU_THIRD_COLLECTION_NAME).toBe('Články'); + }); + + it('pins "Články" to the 3rd position, directly after "Kapitoly v knihách"', () => { + const input = [ + fakeCollection('Knihy'), + fakeCollection(ZCU_SECOND_COLLECTION_NAME), + fakeCollection('Zprávy'), + fakeCollection(ZCU_THIRD_COLLECTION_NAME), + ]; + + const result = reorderZcuPublicationCollections(input); + + expect(result[2].name).toBe(ZCU_THIRD_COLLECTION_NAME); + expect(names(result).indexOf(ZCU_THIRD_COLLECTION_NAME)) + .toBe(names(result).indexOf(ZCU_SECOND_COLLECTION_NAME) + 1); + expect(names(result)).toEqual(['Knihy', ZCU_SECOND_COLLECTION_NAME, ZCU_THIRD_COLLECTION_NAME, 'Zprávy']); + }); + + it('leaves the list unchanged when a pinned collection is missing', () => { + expect(names(reorderZcuPublicationCollections([ + fakeCollection('Knihy'), fakeCollection(ZCU_THIRD_COLLECTION_NAME), + ]))).toEqual(['Knihy', ZCU_THIRD_COLLECTION_NAME]); + + expect(names(reorderZcuPublicationCollections([ + fakeCollection('Knihy'), fakeCollection(ZCU_SECOND_COLLECTION_NAME), + ]))).toEqual(['Knihy', ZCU_SECOND_COLLECTION_NAME]); + }); + + it('leaves the list unchanged when there are no other collections', () => { + expect(names(reorderZcuPublicationCollections([ + fakeCollection(ZCU_SECOND_COLLECTION_NAME), fakeCollection(ZCU_THIRD_COLLECTION_NAME), + ]))).toEqual([ZCU_SECOND_COLLECTION_NAME, ZCU_THIRD_COLLECTION_NAME]); + }); + + 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..49ffdbc1b87 --- /dev/null +++ b/src/app/shared/zcu-collection-order.ts @@ -0,0 +1,38 @@ +import { Collection } from '../core/shared/collection.model'; + +/** + * Name of the collection pinned to the 2nd position for the ZCU publications community (issue #953). + */ +export const ZCU_SECOND_COLLECTION_NAME = 'Kapitoly v knihách'; + +/** + * Name of the collection pinned to the 3rd position for the ZCU publications community (issue #953). + */ +export const ZCU_THIRD_COLLECTION_NAME = 'Články'; + +/** + * Reorders a list of collections so that "Kapitoly v knihách" is shown 2nd and "Články" 3rd, while + * every other collection keeps its original (alphabetical) order. This is the ZCU-specific hardcode + * for issue #953, shared by every place that lists a community's collections (the community page and + * the community browse tree). + * + * The rule only applies when both pinned collections are present and there is at least one other + * collection; otherwise the list is returned unchanged. The result is + * `[others[0], second, third, ...others.slice(1)]`. + * + * @param collections the collections to reorder (as fetched, alphabetical by dc.title) + * @returns a new, reordered array, or the original array when the rule does not apply + */ +export function reorderZcuPublicationCollections(collections: Collection[]): Collection[] { + if (!Array.isArray(collections) || collections.length === 0) { + return collections; + } + const second = collections.find((collection: Collection) => collection.name === ZCU_SECOND_COLLECTION_NAME); + const third = collections.find((collection: Collection) => collection.name === ZCU_THIRD_COLLECTION_NAME); + const others = collections.filter((collection: Collection) => collection !== second && collection !== third); + + if (second && third && others.length >= 1) { + return [others[0], second, third, ...others.slice(1)]; + } + return collections; +} 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 1f08d7f00ba..9cc86b5bb61 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 @@ -4,16 +4,7 @@ import { CommunityPageSubCollectionListComponent as BaseComponent } 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'; - -/** - * Name of the collection pinned to the 2nd position for the ZCU publications community. - */ -const SECOND_COLLECTION_NAME = 'Kapitoly v knihách'; - -/** - * Name of the collection pinned to the 3rd position for the ZCU publications community. - */ -const THIRD_COLLECTION_NAME = 'Články'; +import { reorderZcuPublicationCollections } from '../../../../../app/shared/zcu-collection-order'; @Component({ selector: 'ds-community-page-sub-collection-list', @@ -40,25 +31,14 @@ export class CommunityPageSubCollectionListComponent extends BaseComponent { } /** - * Pure helper that reorders a list of collections to pin "Kapitoly v knihách" to the 2nd - * position and "Články" to the 3rd, keeping every other collection in its original order. - * - * The rule only applies when both pinned collections are present and there is at least one - * other collection; otherwise the list is returned unchanged. The result is - * `[others[0], second, third, ...others.slice(1)]`. + * Reorders the current page of collections using the shared ZCU ordering rule (issue #953), + * pinning "Kapitoly v knihách" to the 2nd position and "Články" to the 3rd. * * @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[] { - const second = collections.find((collection: Collection) => collection.name === SECOND_COLLECTION_NAME); - const third = collections.find((collection: Collection) => collection.name === THIRD_COLLECTION_NAME); - const others = collections.filter((collection: Collection) => collection !== second && collection !== third); - - if (second && third && others.length >= 1) { - return [others[0], second, third, ...others.slice(1)]; - } - return collections; + return reorderZcuPublicationCollections(collections); } } From 224f37eb7c124a8095c1c86b88caf86d6cd94856 Mon Sep 17 00:00:00 2001 From: Matus Kasak Date: Fri, 4 Sep 2026 10:06:22 +0200 Subject: [PATCH 3/6] =?UTF-8?q?ZCU-PUB/fix(community):=20pin=20"=C4=8Cl?= =?UTF-8?q?=C3=A1nky"=20directly=20after=20"Kapitoly=20v=20knih=C3=A1ch"?= =?UTF-8?q?=20(#953)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous #953 hardcode force-pinned "Kapitoly v knihách" to the 2nd slot and "Články" to the 3rd, which is wrong whenever book parts is not naturally 2nd. Per the refined requirement, "Články" must simply follow book parts wherever it sits: book parts 1st -> articles 2nd, book parts 2nd -> articles 3rd, etc. Book parts and every other collection keep their natural (alphabetical) order; only "Články" moves. reorderZcuPublicationCollections now finds book parts, removes "Články", and re-inserts it immediately after book parts (no-op when either is absent or they are already adjacent). Constants renamed ZCU_{SECOND,THIRD} -> ZCU_{BOOKPARTS,ARTICLES}_COLLECTION_NAME to stop implying fixed positions. The shared helper is used by both the community page (themed sub-collection-list) and the community browse tree (CommunityListService), so both are fixed. Verified end-to-end on a seeded Publikační činnost hierarchy: a department with book parts 2nd shows Články 3rd, and one with book parts 1st shows Články 2nd, in both the /community-list tree and the community detail page. Co-Authored-By: Claude Opus 4.8 --- src/app/shared/zcu-collection-order.spec.ts | 78 +++++++++++++------ src/app/shared/zcu-collection-order.ts | 58 +++++++++----- ...page-sub-collection-list.component.spec.ts | 49 ++++++------ ...nity-page-sub-collection-list.component.ts | 7 +- 4 files changed, 123 insertions(+), 69 deletions(-) diff --git a/src/app/shared/zcu-collection-order.spec.ts b/src/app/shared/zcu-collection-order.spec.ts index 4058320ed25..1c235ec8ae7 100644 --- a/src/app/shared/zcu-collection-order.spec.ts +++ b/src/app/shared/zcu-collection-order.spec.ts @@ -1,8 +1,8 @@ import { Collection } from '../core/shared/collection.model'; import { reorderZcuPublicationCollections, - ZCU_SECOND_COLLECTION_NAME, - ZCU_THIRD_COLLECTION_NAME, + ZCU_ARTICLES_COLLECTION_NAME, + ZCU_BOOKPARTS_COLLECTION_NAME, } from './zcu-collection-order'; /** @@ -26,41 +26,73 @@ function names(collections: Collection[]): string[] { } describe('reorderZcuPublicationCollections', () => { - it('uses the exact diacritic pinned names', () => { - expect(ZCU_SECOND_COLLECTION_NAME).toBe('Kapitoly v knihách'); - expect(ZCU_THIRD_COLLECTION_NAME).toBe('Články'); + it('uses the exact diacritic anchor names', () => { + expect(ZCU_BOOKPARTS_COLLECTION_NAME).toBe('Kapitoly v knihách'); + expect(ZCU_ARTICLES_COLLECTION_NAME).toBe('Články'); }); - it('pins "Články" to the 3rd position, directly after "Kapitoly v knihách"', () => { + it('moves "Články" directly after "Kapitoly v knihách" when book parts is 2nd (-> articles 3rd)', () => { const input = [ - fakeCollection('Knihy'), - fakeCollection(ZCU_SECOND_COLLECTION_NAME), + fakeCollection('Habilitace'), + fakeCollection(ZCU_BOOKPARTS_COLLECTION_NAME), + fakeCollection('Sborníky'), + fakeCollection(ZCU_ARTICLES_COLLECTION_NAME), + ]; + + const result = reorderZcuPublicationCollections(input); + + expect(names(result)).toEqual(['Habilitace', ZCU_BOOKPARTS_COLLECTION_NAME, ZCU_ARTICLES_COLLECTION_NAME, 'Sborníky']); + // articles is exactly one slot after book parts + expect(names(result).indexOf(ZCU_ARTICLES_COLLECTION_NAME)) + .toBe(names(result).indexOf(ZCU_BOOKPARTS_COLLECTION_NAME) + 1); + }); + + it('moves "Články" to 2nd when "Kapitoly v knihách" is 1st (book parts stays 1st)', () => { + const input = [ + fakeCollection(ZCU_BOOKPARTS_COLLECTION_NAME), + fakeCollection('Sborníky'), fakeCollection('Zprávy'), - fakeCollection(ZCU_THIRD_COLLECTION_NAME), + fakeCollection(ZCU_ARTICLES_COLLECTION_NAME), ]; const result = reorderZcuPublicationCollections(input); - expect(result[2].name).toBe(ZCU_THIRD_COLLECTION_NAME); - expect(names(result).indexOf(ZCU_THIRD_COLLECTION_NAME)) - .toBe(names(result).indexOf(ZCU_SECOND_COLLECTION_NAME) + 1); - expect(names(result)).toEqual(['Knihy', ZCU_SECOND_COLLECTION_NAME, ZCU_THIRD_COLLECTION_NAME, 'Zprávy']); + expect(names(result)).toEqual([ZCU_BOOKPARTS_COLLECTION_NAME, ZCU_ARTICLES_COLLECTION_NAME, 'Sborníky', 'Zprávy']); }); - it('leaves the list unchanged when a pinned collection is missing', () => { - expect(names(reorderZcuPublicationCollections([ - fakeCollection('Knihy'), fakeCollection(ZCU_THIRD_COLLECTION_NAME), - ]))).toEqual(['Knihy', ZCU_THIRD_COLLECTION_NAME]); + it('never moves "Kapitoly v knihách" itself out of its natural position', () => { + const input = [ + fakeCollection('Abstrakty'), + fakeCollection('Habilitace'), + fakeCollection(ZCU_BOOKPARTS_COLLECTION_NAME), + fakeCollection(ZCU_ARTICLES_COLLECTION_NAME), + ]; - expect(names(reorderZcuPublicationCollections([ - fakeCollection('Knihy'), fakeCollection(ZCU_SECOND_COLLECTION_NAME), - ]))).toEqual(['Knihy', ZCU_SECOND_COLLECTION_NAME]); + const result = reorderZcuPublicationCollections(input); + + // book parts stays 3rd; articles follows it as 4th; unchanged here since already adjacent + expect(names(result)).toEqual(['Abstrakty', 'Habilitace', ZCU_BOOKPARTS_COLLECTION_NAME, ZCU_ARTICLES_COLLECTION_NAME]); + }); + + it('leaves the list unchanged when "Články" is already directly after book parts', () => { + const input = [ + fakeCollection('Habilitace'), + fakeCollection(ZCU_BOOKPARTS_COLLECTION_NAME), + fakeCollection(ZCU_ARTICLES_COLLECTION_NAME), + fakeCollection('Sborníky'), + ]; + + expect(reorderZcuPublicationCollections(input)).toBe(input); }); - it('leaves the list unchanged when there are no other collections', () => { + it('leaves the list unchanged when an anchor collection is missing', () => { + expect(names(reorderZcuPublicationCollections([ + fakeCollection('Knihy'), fakeCollection(ZCU_ARTICLES_COLLECTION_NAME), + ]))).toEqual(['Knihy', ZCU_ARTICLES_COLLECTION_NAME]); + expect(names(reorderZcuPublicationCollections([ - fakeCollection(ZCU_SECOND_COLLECTION_NAME), fakeCollection(ZCU_THIRD_COLLECTION_NAME), - ]))).toEqual([ZCU_SECOND_COLLECTION_NAME, ZCU_THIRD_COLLECTION_NAME]); + fakeCollection('Knihy'), fakeCollection(ZCU_BOOKPARTS_COLLECTION_NAME), + ]))).toEqual(['Knihy', ZCU_BOOKPARTS_COLLECTION_NAME]); }); it('handles empty and single-element input without error', () => { diff --git a/src/app/shared/zcu-collection-order.ts b/src/app/shared/zcu-collection-order.ts index 49ffdbc1b87..84950198ccf 100644 --- a/src/app/shared/zcu-collection-order.ts +++ b/src/app/shared/zcu-collection-order.ts @@ -1,38 +1,60 @@ import { Collection } from '../core/shared/collection.model'; /** - * Name of the collection pinned to the 2nd position for the ZCU publications community (issue #953). + * Name of the "book parts" collection that anchors the ordering for the ZCU publications + * community (issue #953). "Články" is always shown directly after this collection. */ -export const ZCU_SECOND_COLLECTION_NAME = 'Kapitoly v knihách'; +export const ZCU_BOOKPARTS_COLLECTION_NAME = 'Kapitoly v knihách'; /** - * Name of the collection pinned to the 3rd position for the ZCU publications community (issue #953). + * Name of the "articles" collection that is pinned directly after "Kapitoly v knihách" + * for the ZCU publications community (issue #953). */ -export const ZCU_THIRD_COLLECTION_NAME = 'Články'; +export const ZCU_ARTICLES_COLLECTION_NAME = 'Články'; /** - * Reorders a list of collections so that "Kapitoly v knihách" is shown 2nd and "Články" 3rd, while - * every other collection keeps its original (alphabetical) order. This is the ZCU-specific hardcode - * for issue #953, shared by every place that lists a community's collections (the community page and - * the community browse tree). + * Reorders a list of collections so that "Články" (articles) is shown immediately after + * "Kapitoly v knihách" (book parts), while every other collection — including "Kapitoly v + * knihách" itself — keeps its original (alphabetical) position. This is the ZCU-specific + * hardcode for issue #953, shared by every place that lists a community's collections (the + * community page and the community browse tree). * - * The rule only applies when both pinned collections are present and there is at least one other - * collection; otherwise the list is returned unchanged. The result is - * `[others[0], second, third, ...others.slice(1)]`. + * The articles collection tracks the book-parts collection wherever it naturally sits: + * - book parts 1st -> articles 2nd + * - book parts 2nd -> articles 3rd + * and so on. The rule only applies when BOTH collections are present in the given list; + * otherwise (or if they are already adjacent in the right order) the list is returned + * unchanged. Nothing but the position of "Články" is ever moved. + * + * Note: ordering is applied per fetched page. In the ZCU publications community a department + * has only a handful of collections, so both always land on the same page. * * @param collections the collections to reorder (as fetched, alphabetical by dc.title) * @returns a new, reordered array, or the original array when the rule does not apply */ export function reorderZcuPublicationCollections(collections: Collection[]): Collection[] { - if (!Array.isArray(collections) || collections.length === 0) { + if (!Array.isArray(collections) || collections.length < 2) { + return collections; + } + + const bookPartsIndex = collections.findIndex((collection: Collection) => collection.name === ZCU_BOOKPARTS_COLLECTION_NAME); + const articlesIndex = collections.findIndex((collection: Collection) => collection.name === ZCU_ARTICLES_COLLECTION_NAME); + + // Both anchor collections must be present for the rule to apply. + if (bookPartsIndex === -1 || articlesIndex === -1) { return collections; } - const second = collections.find((collection: Collection) => collection.name === ZCU_SECOND_COLLECTION_NAME); - const third = collections.find((collection: Collection) => collection.name === ZCU_THIRD_COLLECTION_NAME); - const others = collections.filter((collection: Collection) => collection !== second && collection !== third); - if (second && third && others.length >= 1) { - return [others[0], second, third, ...others.slice(1)]; + // Already directly after book parts -> nothing to do. + if (articlesIndex === bookPartsIndex + 1) { + return collections; } - return collections; + + const articles = collections[articlesIndex]; + // Remove "Články" while preserving every other collection's relative order (incl. book parts). + const withoutArticles = collections.filter((_: Collection, index: number) => index !== articlesIndex); + // Re-locate book parts in the reduced list and insert "Články" right after it. + const insertAt = withoutArticles.findIndex((collection: Collection) => collection.name === ZCU_BOOKPARTS_COLLECTION_NAME) + 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 index 310dbc79aa8..bc24c142386 100644 --- 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 @@ -22,8 +22,8 @@ function names(collections: Collection[]): string[] { return collections.map((collection: Collection) => collection.name); } -const SECOND_COLLECTION_NAME = 'Kapitoly v knihách'; -const THIRD_COLLECTION_NAME = 'Články'; +const BOOKPARTS_COLLECTION_NAME = 'Kapitoly v knihách'; +const ARTICLES_COLLECTION_NAME = 'Články'; describe('CommunityPageSubCollectionListComponent (custom theme) reorderCollections', () => { let component: CommunityPageSubCollectionListComponent; @@ -35,55 +35,54 @@ describe('CommunityPageSubCollectionListComponent (custom theme) reorderCollecti component = new CommunityPageSubCollectionListComponent(null, null, null); }); - it('pins "Články" to the 3rd position, directly after "Kapitoly v knihách"', () => { + it('shows "Články" directly after "Kapitoly v knihách" (book parts 2nd -> articles 3rd)', () => { const input = [ fakeCollection('Alfa'), - fakeCollection(SECOND_COLLECTION_NAME), - fakeCollection(THIRD_COLLECTION_NAME), - fakeCollection('Zeta'), + fakeCollection(BOOKPARTS_COLLECTION_NAME), + fakeCollection('Sborníky'), + fakeCollection(ARTICLES_COLLECTION_NAME), ]; const result = reorder(input); - expect(names(result)).toEqual(['Alfa', SECOND_COLLECTION_NAME, THIRD_COLLECTION_NAME, 'Zeta']); - expect(result[2].name).toBe(THIRD_COLLECTION_NAME); - expect(result[1].name).toBe(SECOND_COLLECTION_NAME); - expect(names(result).indexOf(THIRD_COLLECTION_NAME)) - .toBe(names(result).indexOf(SECOND_COLLECTION_NAME) + 1); + 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('leaves the list unchanged when "Kapitoly v knihách" is missing', () => { + it('shows "Články" 2nd when "Kapitoly v knihách" is 1st', () => { const input = [ - fakeCollection('Alfa'), - fakeCollection(THIRD_COLLECTION_NAME), - fakeCollection('Zeta'), + fakeCollection(BOOKPARTS_COLLECTION_NAME), + fakeCollection('Sborníky'), + fakeCollection(ARTICLES_COLLECTION_NAME), ]; - expect(names(reorder(input))).toEqual(['Alfa', THIRD_COLLECTION_NAME, 'Zeta']); + expect(names(reorder(input))).toEqual([BOOKPARTS_COLLECTION_NAME, ARTICLES_COLLECTION_NAME, 'Sborníky']); }); - it('leaves the list unchanged when "Články" is missing', () => { + it('leaves the list unchanged when "Kapitoly v knihách" is missing', () => { const input = [ fakeCollection('Alfa'), - fakeCollection(SECOND_COLLECTION_NAME), + fakeCollection(ARTICLES_COLLECTION_NAME), fakeCollection('Zeta'), ]; - expect(names(reorder(input))).toEqual(['Alfa', SECOND_COLLECTION_NAME, 'Zeta']); + expect(names(reorder(input))).toEqual(['Alfa', ARTICLES_COLLECTION_NAME, 'Zeta']); }); - it('leaves the list unchanged when there are no other collections', () => { + it('leaves the list unchanged when "Články" is missing', () => { const input = [ - fakeCollection(SECOND_COLLECTION_NAME), - fakeCollection(THIRD_COLLECTION_NAME), + fakeCollection('Alfa'), + fakeCollection(BOOKPARTS_COLLECTION_NAME), + fakeCollection('Zeta'), ]; - expect(names(reorder(input))).toEqual([SECOND_COLLECTION_NAME, THIRD_COLLECTION_NAME]); + expect(names(reorder(input))).toEqual(['Alfa', BOOKPARTS_COLLECTION_NAME, 'Zeta']); }); it('uses the exact diacritic collection names', () => { - expect(SECOND_COLLECTION_NAME).toBe('Kapitoly v knihách'); - expect(THIRD_COLLECTION_NAME).toBe('Články'); + expect(BOOKPARTS_COLLECTION_NAME).toBe('Kapitoly v knihách'); + expect(ARTICLES_COLLECTION_NAME).toBe('Články'); const input = [ fakeCollection('Alfa'), 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 9cc86b5bb61..c4d7f01b313 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 @@ -16,8 +16,9 @@ import { reorderZcuPublicationCollections } from '../../../../../app/shared/zcu- export class CommunityPageSubCollectionListComponent extends BaseComponent { /** - * Reorders the current page of collections so that "Kapitoly v knihách" is shown 2nd and - * "Články" 3rd, while all other collections keep their existing alphabetical order. + * Reorders the current page of collections so that "Články" is shown directly after + * "Kapitoly v knihách" (issue #953), while every other collection — book parts included — + * 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 @@ -32,7 +33,7 @@ export class CommunityPageSubCollectionListComponent extends BaseComponent { /** * Reorders the current page of collections using the shared ZCU ordering rule (issue #953), - * pinning "Kapitoly v knihách" to the 2nd position and "Články" to the 3rd. + * 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 From 0bf2c9bd9fe5bc880ae5ee801b85d26855840ea8 Mon Sep 17 00:00:00 2001 From: Matus Kasak Date: Fri, 4 Sep 2026 10:36:25 +0200 Subject: [PATCH 4/6] Update docd --- .../community-list-service.ts | 2 +- ...nity-page-sub-collection-list.component.ts | 4 ---- src/app/shared/zcu-collection-order.ts | 20 +++---------------- ...nity-page-sub-collection-list.component.ts | 5 ++--- 4 files changed, 6 insertions(+), 25 deletions(-) diff --git a/src/app/community-list-page/community-list-service.ts b/src/app/community-list-page/community-list-service.ts index db0bb5c569c..8a505d28c7e 100644 --- a/src/app/community-list-page/community-list-service.ts +++ b/src/app/community-list-page/community-list-service.ts @@ -256,7 +256,7 @@ export class CommunityListService { getFirstCompletedRemoteData(), map((rd: RemoteData>) => { if (hasValue(rd) && hasValue(rd.payload)) { - // issue #953: apply the ZCU collection display order in the community browse tree + // 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) { 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 57594971036..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 @@ -95,10 +95,6 @@ export class CommunityPageSubCollectionListComponent implements OnInit, OnDestro /** * Extension point for theme/customer-specific ordering of the current page of collections. * - * The default implementation is a no-op that returns the RemoteData unchanged, so behavior - * stays identical for every theme and customer. Themes that need a bespoke display order - * (e.g. a hardcoded pinned position for a specific collection) override this method. - * * @param rd the RemoteData holding the current page of collections * @returns the RemoteData to emit, potentially with a reordered page */ diff --git a/src/app/shared/zcu-collection-order.ts b/src/app/shared/zcu-collection-order.ts index 84950198ccf..a109a9487ec 100644 --- a/src/app/shared/zcu-collection-order.ts +++ b/src/app/shared/zcu-collection-order.ts @@ -1,33 +1,19 @@ import { Collection } from '../core/shared/collection.model'; /** - * Name of the "book parts" collection that anchors the ordering for the ZCU publications - * community (issue #953). "Články" is always shown directly after this collection. + * Name of the collection that anchors the ordering for the ZCU publications community. */ export const ZCU_BOOKPARTS_COLLECTION_NAME = 'Kapitoly v knihách'; /** - * Name of the "articles" collection that is pinned directly after "Kapitoly v knihách" - * for the ZCU publications community (issue #953). + * Name of the collection that is pinned directly after "Kapitoly v knihách". */ export const ZCU_ARTICLES_COLLECTION_NAME = 'Články'; /** * Reorders a list of collections so that "Články" (articles) is shown immediately after * "Kapitoly v knihách" (book parts), while every other collection — including "Kapitoly v - * knihách" itself — keeps its original (alphabetical) position. This is the ZCU-specific - * hardcode for issue #953, shared by every place that lists a community's collections (the - * community page and the community browse tree). - * - * The articles collection tracks the book-parts collection wherever it naturally sits: - * - book parts 1st -> articles 2nd - * - book parts 2nd -> articles 3rd - * and so on. The rule only applies when BOTH collections are present in the given list; - * otherwise (or if they are already adjacent in the right order) the list is returned - * unchanged. Nothing but the position of "Články" is ever moved. - * - * Note: ordering is applied per fetched page. In the ZCU publications community a department - * has only a handful of collections, so both always land on the same page. + * knihách" itself — keeps its original position. This is the ZCU-specific hardcode. * * @param collections the collections to reorder (as fetched, alphabetical by dc.title) * @returns a new, reordered array, or the original array when the rule does not apply 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 c4d7f01b313..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 @@ -17,8 +17,7 @@ export class CommunityPageSubCollectionListComponent extends BaseComponent { /** * Reorders the current page of collections so that "Články" is shown directly after - * "Kapitoly v knihách" (issue #953), while every other collection — book parts included — - * keeps its existing alphabetical position. + * "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 @@ -32,7 +31,7 @@ export class CommunityPageSubCollectionListComponent extends BaseComponent { } /** - * Reorders the current page of collections using the shared ZCU ordering rule (issue #953), + * 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) From 0a784adb9975714846bc61bbfd262e262c53016f Mon Sep 17 00:00:00 2001 From: Matus Kasak Date: Fri, 4 Sep 2026 10:49:52 +0200 Subject: [PATCH 5/6] fix(community): match book parts / articles collections by Czech title prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Departments name these collections inconsistently ("Články", "Články / Articles", "Články / Articles (KAE)", and the same pattern for "Kapitoly v knihách / Bookparts (KAE)"). The exact-name match only handled the plain Czech variant, so the reorder never fired for most departments. Match by Czech title prefix (trimmed, case-insensitive) so every bilingual/suffixed variant is handled regardless of UI language. --- src/app/shared/zcu-collection-order.spec.ts | 102 +++++++++++--------- src/app/shared/zcu-collection-order.ts | 37 +++---- 2 files changed, 76 insertions(+), 63 deletions(-) diff --git a/src/app/shared/zcu-collection-order.spec.ts b/src/app/shared/zcu-collection-order.spec.ts index 1c235ec8ae7..788c805aa08 100644 --- a/src/app/shared/zcu-collection-order.spec.ts +++ b/src/app/shared/zcu-collection-order.spec.ts @@ -1,84 +1,96 @@ import { Collection } from '../core/shared/collection.model'; import { reorderZcuPublicationCollections, - ZCU_ARTICLES_COLLECTION_NAME, - ZCU_BOOKPARTS_COLLECTION_NAME, + ZCU_ARTICLES_COLLECTION_PREFIX, + ZCU_BOOKPARTS_COLLECTION_PREFIX, } from './zcu-collection-order'; -/** - * Builds a minimal Collection-like stub that only exposes the `name` getter used by the ordering. - * - * @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); } describe('reorderZcuPublicationCollections', () => { - it('uses the exact diacritic anchor names', () => { - expect(ZCU_BOOKPARTS_COLLECTION_NAME).toBe('Kapitoly v knihách'); - expect(ZCU_ARTICLES_COLLECTION_NAME).toBe('Články'); + 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 "Články" directly after "Kapitoly v knihách" when book parts is 2nd (-> articles 3rd)', () => { + it('moves articles directly after book parts when book parts is 2nd (-> articles 3rd)', () => { const input = [ fakeCollection('Habilitace'), - fakeCollection(ZCU_BOOKPARTS_COLLECTION_NAME), + fakeCollection('Kapitoly v knihách'), fakeCollection('Sborníky'), - fakeCollection(ZCU_ARTICLES_COLLECTION_NAME), + fakeCollection('Články'), ]; - const result = reorderZcuPublicationCollections(input); - - expect(names(result)).toEqual(['Habilitace', ZCU_BOOKPARTS_COLLECTION_NAME, ZCU_ARTICLES_COLLECTION_NAME, 'Sborníky']); - // articles is exactly one slot after book parts - expect(names(result).indexOf(ZCU_ARTICLES_COLLECTION_NAME)) - .toBe(names(result).indexOf(ZCU_BOOKPARTS_COLLECTION_NAME) + 1); + expect(names(reorderZcuPublicationCollections(input))) + .toEqual(['Habilitace', 'Kapitoly v knihách', 'Články', 'Sborníky']); }); - it('moves "Články" to 2nd when "Kapitoly v knihách" is 1st (book parts stays 1st)', () => { + it('moves articles to 2nd when book parts is 1st', () => { const input = [ - fakeCollection(ZCU_BOOKPARTS_COLLECTION_NAME), + fakeCollection('Kapitoly v knihách'), fakeCollection('Sborníky'), fakeCollection('Zprávy'), - fakeCollection(ZCU_ARTICLES_COLLECTION_NAME), + fakeCollection('Články'), ]; - const result = reorderZcuPublicationCollections(input); + expect(names(reorderZcuPublicationCollections(input))) + .toEqual(['Kapitoly v knihách', 'Články', 'Sborníky', 'Zprávy']); + }); - expect(names(result)).toEqual([ZCU_BOOKPARTS_COLLECTION_NAME, ZCU_ARTICLES_COLLECTION_NAME, '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('never moves "Kapitoly v knihách" itself out of its natural position', () => { + 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(ZCU_BOOKPARTS_COLLECTION_NAME), - fakeCollection(ZCU_ARTICLES_COLLECTION_NAME), + fakeCollection('Kapitoly v knihách'), + fakeCollection('Články'), ]; - const result = reorderZcuPublicationCollections(input); - - // book parts stays 3rd; articles follows it as 4th; unchanged here since already adjacent - expect(names(result)).toEqual(['Abstrakty', 'Habilitace', ZCU_BOOKPARTS_COLLECTION_NAME, ZCU_ARTICLES_COLLECTION_NAME]); + expect(names(reorderZcuPublicationCollections(input))) + .toEqual(['Abstrakty', 'Habilitace', 'Kapitoly v knihách', 'Články']); }); - it('leaves the list unchanged when "Články" is already directly after book parts', () => { + it('leaves the list unchanged when articles is already directly after book parts', () => { const input = [ fakeCollection('Habilitace'), - fakeCollection(ZCU_BOOKPARTS_COLLECTION_NAME), - fakeCollection(ZCU_ARTICLES_COLLECTION_NAME), + fakeCollection('Kapitoly v knihách'), + fakeCollection('Články'), fakeCollection('Sborníky'), ]; @@ -87,12 +99,12 @@ describe('reorderZcuPublicationCollections', () => { it('leaves the list unchanged when an anchor collection is missing', () => { expect(names(reorderZcuPublicationCollections([ - fakeCollection('Knihy'), fakeCollection(ZCU_ARTICLES_COLLECTION_NAME), - ]))).toEqual(['Knihy', ZCU_ARTICLES_COLLECTION_NAME]); + fakeCollection('Knihy'), fakeCollection('Články'), + ]))).toEqual(['Knihy', 'Články']); expect(names(reorderZcuPublicationCollections([ - fakeCollection('Knihy'), fakeCollection(ZCU_BOOKPARTS_COLLECTION_NAME), - ]))).toEqual(['Knihy', ZCU_BOOKPARTS_COLLECTION_NAME]); + fakeCollection('Knihy'), fakeCollection('Kapitoly v knihách'), + ]))).toEqual(['Knihy', 'Kapitoly v knihách']); }); it('handles empty and single-element input without error', () => { diff --git a/src/app/shared/zcu-collection-order.ts b/src/app/shared/zcu-collection-order.ts index a109a9487ec..b91e4b81fd1 100644 --- a/src/app/shared/zcu-collection-order.ts +++ b/src/app/shared/zcu-collection-order.ts @@ -1,46 +1,47 @@ import { Collection } from '../core/shared/collection.model'; /** - * Name of the collection that anchors the ordering for the ZCU publications community. + * Title prefix of the "book parts" collection that anchors the ordering. + * Departments name it "Kapitoly v knihách", "Kapitoly v knihách / Bookparts" or + * "Kapitoly v knihách / Bookparts (KAE)", so we match on the common Czech prefix. */ -export const ZCU_BOOKPARTS_COLLECTION_NAME = 'Kapitoly v knihách'; +export const ZCU_BOOKPARTS_COLLECTION_PREFIX = 'Kapitoly v knihách'; /** - * Name of the collection that is pinned directly after "Kapitoly v knihách". + * Title prefix of the "articles" collection pinned directly after book parts. + * Departments name it "Články", "Články / Articles" or "Články / Articles (KAE)". */ -export const ZCU_ARTICLES_COLLECTION_NAME = 'Články'; +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 "Články" (articles) is shown immediately after - * "Kapitoly v knihách" (book parts), while every other collection — including "Kapitoly v - * knihách" itself — keeps its original position. This is the ZCU-specific hardcode. - * - * @param collections the collections to reorder (as fetched, alphabetical by dc.title) - * @returns a new, reordered array, or the original array when the rule does not apply + * Reorders a list of collections so that the articles collection is shown immediately after + * the book parts collection, while every other collection — book parts included — keeps its + * original position. Both are matched by title prefix, so the bilingual and department-suffixed + * variants ("Články / Articles (KAE)", ...) are handled regardless of the UI language. */ export function reorderZcuPublicationCollections(collections: Collection[]): Collection[] { if (!Array.isArray(collections) || collections.length < 2) { return collections; } - const bookPartsIndex = collections.findIndex((collection: Collection) => collection.name === ZCU_BOOKPARTS_COLLECTION_NAME); - const articlesIndex = collections.findIndex((collection: Collection) => collection.name === ZCU_ARTICLES_COLLECTION_NAME); + const bookPartsIndex = collections.findIndex((collection: Collection) => nameStartsWith(collection, ZCU_BOOKPARTS_COLLECTION_PREFIX)); + const articlesIndex = collections.findIndex((collection: Collection) => nameStartsWith(collection, ZCU_ARTICLES_COLLECTION_PREFIX)); - // Both anchor collections must be present for the rule to apply. if (bookPartsIndex === -1 || articlesIndex === -1) { return collections; } - - // Already directly after book parts -> nothing to do. if (articlesIndex === bookPartsIndex + 1) { return collections; } const articles = collections[articlesIndex]; - // Remove "Články" while preserving every other collection's relative order (incl. book parts). const withoutArticles = collections.filter((_: Collection, index: number) => index !== articlesIndex); - // Re-locate book parts in the reduced list and insert "Články" right after it. - const insertAt = withoutArticles.findIndex((collection: Collection) => collection.name === ZCU_BOOKPARTS_COLLECTION_NAME) + 1; + const insertAt = withoutArticles.findIndex((collection: Collection) => nameStartsWith(collection, ZCU_BOOKPARTS_COLLECTION_PREFIX)) + 1; return [...withoutArticles.slice(0, insertAt), articles, ...withoutArticles.slice(insertAt)]; } From 5a0c871b544cbc0bb882a03f0f46783be0cd2ecf Mon Sep 17 00:00:00 2001 From: Matus Kasak Date: Fri, 4 Sep 2026 11:22:00 +0200 Subject: [PATCH 6/6] Update docs --- src/app/shared/zcu-collection-order.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/app/shared/zcu-collection-order.ts b/src/app/shared/zcu-collection-order.ts index b91e4b81fd1..7d3a89d3402 100644 --- a/src/app/shared/zcu-collection-order.ts +++ b/src/app/shared/zcu-collection-order.ts @@ -1,15 +1,12 @@ import { Collection } from '../core/shared/collection.model'; /** - * Title prefix of the "book parts" collection that anchors the ordering. - * Departments name it "Kapitoly v knihách", "Kapitoly v knihách / Bookparts" or - * "Kapitoly v knihách / Bookparts (KAE)", so we match on the common Czech prefix. + * Title prefix of the collection that anchors the ordering. */ export const ZCU_BOOKPARTS_COLLECTION_PREFIX = 'Kapitoly v knihách'; /** - * Title prefix of the "articles" collection pinned directly after book parts. - * Departments name it "Články", "Články / Articles" or "Články / Articles (KAE)". + * Title prefix of the collection pinned directly after book parts. */ export const ZCU_ARTICLES_COLLECTION_PREFIX = 'Články'; @@ -20,9 +17,7 @@ function nameStartsWith(collection: Collection, prefix: string): boolean { /** * Reorders a list of collections so that the articles collection is shown immediately after - * the book parts collection, while every other collection — book parts included — keeps its - * original position. Both are matched by title prefix, so the bilingual and department-suffixed - * variants ("Články / Articles (KAE)", ...) are handled regardless of the UI language. + * 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) {