feat(sources): rebuild the Sources page on the shared source row

- Side list (UI-kit side list): title with the playlist count and an Add
  button, a Type group, and on desktop a Status group (Any / Responding /
  Not responding). The two filters combine and persist; each group counts
  within the other's choice. The chosen row uses one selected-row token
  (soft tint, outlined in light theme).
- Page: a 28px title with the visible count instead of the bordered
  header bar; Clean up and the sort menu are secondary buttons. Clean up
  shows how many sources do not respond. Sort adds Last updated, Most
  channels and (desktop) Not responding first, and keeps Name Z-A and
  Custom order.
- List: app-source-row with hover actions (Refresh, Edit, ⋯); a failing
  row keeps Retry in view. "No matches" offers Reset filters. Dragging in
  custom order no longer reuses positions of rows a filter hides.
- Delete with Undo: no confirmation; the row leaves at once and the delete
  runs when the toast closes (its close button commits at once). Deleting
  the active source switches to the next row. A large Xtream delete shows
  its row as "Removing…" with progress and Cancel, as before.
- The list's refresh flows moved into SourceListRefreshService; E2E and
  performance drivers follow the new markup and delete flow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-12 22:34:22 +02:00
1 parent 028a3db880
commit 401295c092
70 files changed
+3837 -2023

No files matched your search

+2 -6
View File
@@ -111,16 +111,12 @@ test('@a11y icon-only buttons are named in Sources and the playlist info dialog'
}) => {
await addM3uUrlPlaylist(page, M3U_TITLE);
// Sources list: refresh, details and remove on each row.
// Sources list: refresh, edit and the actions menu on each row.
await openSources(page);
const row = sourceRowByTitle(page, M3U_TITLE);
await expect(row).toBeVisible({ timeout: 15_000 });
await row.hover();
for (const name of [
'Refresh playlist',
'Show playlist details',
'Remove playlist',
]) {
for (const name of ['Refresh playlist', 'Edit…', 'Playlist actions']) {
await expect(row.getByRole('button', { name })).toBeAttached();
}
await expectNamedButtons(page, 3);
+1 -1
View File
@@ -259,7 +259,7 @@ test('@self-hosted M3U User-Agent reaches the provider on import and refresh', a
channel = 'Refreshed UA Channel';
const row = sourceRowByTitle(page, 'Protected M3U');
await row.hover();
await row.locator('.refresh-btn').click();
await row.locator('.source-row__refresh').click();
await expect(
page.locator('.mat-mdc-snack-bar-label').last()
).toContainText('updated');
+5 -2
View File
@@ -190,7 +190,10 @@ export async function openSources(page: Page): Promise<void> {
}
export function sourceRowByTitle(page: Page, title: string): Locator {
return page.locator('app-playlist-item').filter({ hasText: title }).first();
return page
.locator('app-workspace-sources app-source-row')
.filter({ hasText: title })
.first();
}
export async function openSourceEditor(
@@ -200,7 +203,7 @@ export async function openSourceEditor(
const row = sourceRowByTitle(page, title);
await expect(row).toBeVisible({ timeout: 15_000 });
await row.hover();
await row.locator('.edit-btn').click();
await row.locator('.source-row__edit').click();
const dialog = page.locator('mat-dialog-container').last();
await expect(dialog).toBeVisible();
return dialog;