From aead89e3e61045772410b3f5245d2d52f050b659 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 9 May 2026 18:34:57 +0200 Subject: [PATCH] fix(xtream): keep category management available Fixes #913 --- .../src/category-management.e2e.ts | 92 ++++++++++++++++++- docs/architecture/category-management.md | 19 ++-- .../workspace-context-panel.component.ts | 1 - 3 files changed, 102 insertions(+), 10 deletions(-) diff --git a/apps/electron-backend-e2e/src/category-management.e2e.ts b/apps/electron-backend-e2e/src/category-management.e2e.ts index a9de2a5e4..49382b099 100644 --- a/apps/electron-backend-e2e/src/category-management.e2e.ts +++ b/apps/electron-backend-e2e/src/category-management.e2e.ts @@ -1,7 +1,6 @@ import { Locator, Page } from '@playwright/test'; import { addXtreamPortal, - clickCategoryById, closeElectronApp, expect, launchElectronApp, @@ -205,6 +204,86 @@ test.describe('Electron Xtream Category Management', () => { await closeElectronApp(app); } }); + + test('allows restoring live categories after every category is hidden', async ({ + dataDir, + request, + }) => { + await resetMockServers(request, ['xtream']); + const portalName = 'All Hidden Categories Xtream'; + const app = await launchElectronApp(dataDir); + + try { + await addXtreamPortal(app.mainWindow, { + name: portalName, + }); + await waitForXtreamWorkspaceReady(app.mainWindow); + await openWorkspaceSection(app.mainWindow, 'Live TV'); + await app.mainWindow.waitForURL( + /\/workspace\/xtreams\/[^/]+\/live/ + ); + + await expect + .poll(() => readVisibleSidebarCategoryNames(app.mainWindow), { + timeout: 15000, + }) + .not.toEqual([]); + const initialCategoryNames = await readVisibleSidebarCategoryNames( + app.mainWindow + ); + + let dialog = await openManageCategoriesDialog(app.mainWindow); + await dialog + .getByRole('button', { + name: 'Deselect All', + exact: true, + }) + .click(); + await expect( + dialog.locator('.category-item mat-checkbox input:checked') + ).toHaveCount(0); + await dialog + .getByRole('button', { name: 'Save', exact: true }) + .click(); + await app.mainWindow.waitForSelector('mat-dialog-container', { + state: 'detached', + }); + + await expect + .poll(() => readVisibleSidebarCategoryNames(app.mainWindow), { + timeout: 15000, + }) + .toEqual([]); + + await expect( + app.mainWindow.getByRole('button', { + name: 'Manage categories', + exact: true, + }) + ).toBeEnabled(); + + dialog = await openManageCategoriesDialog(app.mainWindow); + await dialog + .getByRole('button', { name: 'Select All', exact: true }) + .click(); + await expect( + dialog.locator('.category-item mat-checkbox input:checked') + ).toHaveCount(initialCategoryNames.length); + await dialog + .getByRole('button', { name: 'Save', exact: true }) + .click(); + await app.mainWindow.waitForSelector('mat-dialog-container', { + state: 'detached', + }); + + await expectVisibleSidebarCategoryNames( + app.mainWindow, + initialCategoryNames + ); + } finally { + await closeElectronApp(app); + } + }); }); async function openManageCategoriesDialog(page: Page) { @@ -266,6 +345,10 @@ async function expectVisibleSidebarCategoryNames( .toEqual(expectedNames); } catch (error) { const actualNames = await readVisibleSidebarCategoryNames(page); + if (stringArraysEqual(actualNames, expectedNames)) { + return; + } + throw new Error( `Expected visible sidebar categories ${JSON.stringify( expectedNames @@ -300,6 +383,13 @@ async function readVisibleSidebarCategoryNames(page: Page): Promise { return actualNames; } +function stringArraysEqual(left: string[], right: string[]): boolean { + return ( + left.length === right.length && + left.every((value, index) => value === right[index]) + ); +} + async function pickSidebarCategory( page: Page ): Promise<{ id: string; itemCount: number; name: string }> { diff --git a/docs/architecture/category-management.md b/docs/architecture/category-management.md index 177378954..9bc35bd82 100644 --- a/docs/architecture/category-management.md +++ b/docs/architecture/category-management.md @@ -66,14 +66,12 @@ ALTER TABLE categories ADD COLUMN hidden INTEGER DEFAULT 0 **Integration Points** -- `xtream-main-container.component.ts` - Movies & Series sections -- `live-stream-layout.component.ts` - Live TV section - -Both components: - -1. Add a "tune" icon button in the sidebar header -2. Open the dialog with playlist ID and content type -3. Call `xtreamStore.reloadCategories()` after dialog closes with changes +- `libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.ts` + renders the tune icon button for Xtream Live TV, Movies, and Series sidebars. +- The workspace context panel lazy-loads `CategoryManagementDialogComponent` + from `@iptvnator/portal/xtream/feature`, opens it with playlist ID, content + type, and item counts, then calls `xtreamStore.reloadCategories()` after a + successful dialog save. ### Store @@ -87,6 +85,11 @@ Added `reloadCategories()` method to refresh categories from database after visi - **Persistence**: Visibility settings survive playlist refresh (see below) - **Per-playlist, per-type**: Categories are managed per playlist and per content type (live/movies/series) - **No content deletion**: Hiding a category only affects sidebar visibility; the category and its content remain in the database +- **All-hidden recovery**: Once the selected Xtream type is loaded, the manage + categories button remains available even if every visible category has been + hidden. The sidebar category list is filtered, but the dialog reads all + categories through `getAllXtreamCategories()` so users can select categories + again. ### Visibility Preservation During Refresh diff --git a/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.ts b/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.ts index b8103669e..c510f81e3 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.ts @@ -101,7 +101,6 @@ export class WorkspaceContextPanelComponent { readonly canManageXtreamCategories = computed( () => this.isXtreamCategories() && - this.xtreamCategories().length > 0 && this.xtreamSelectedTypeCountsReady() ); readonly xtreamStatusText = computed(() => {