From 5d60960d9de5567013bfe48817cd83ba372d275a Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 4 Oct 2026 10:25:02 +0200 Subject: [PATCH] perf(playlist): make the playlist import and shared UI components OnPush Plan item C6 step 3 for libs/playlist (import/feature and shared/ui): the twelve Eager components switch to OnPush. Two of them rendered plain fields written after an await, outside any template event, which only an Eager check on the next zone tick picked up: - playlist-item's portal status dot (PWA, after the async portal check) now reads a signal; - playlist-info's playlist is backed by a signal behind its existing getter/setter name, so the EPG source list follows removals and file picks that land after awaited cleanup and dialogs. A regression test for each fails on OnPush with the plain field and passes with the signal. The Stalker import's post-await patchValue needs no change (see the zoneless checklist). The m3u feature-player components stay Eager for the playback PR. Co-Authored-By: Claude Opus 5.5 --- docs/architecture/zoneless-migration.md | 30 +++--- .../add-playlist-dialog.component.ts | 2 +- .../lib/auto-import/auto-import.component.ts | 2 +- .../lib/file-upload/file-upload.component.ts | 2 +- .../stalker-portal-import.component.ts | 2 +- .../lib/text-import/text-import.component.ts | 2 +- .../lib/url-upload/url-upload.component.ts | 2 +- .../xtream-code-import.component.ts | 2 +- .../empty-state/empty-state.component.ts | 2 +- ...laylist-info-epg-sources.component.spec.ts | 95 +++++++++++++++++++ .../playlist-info/playlist-info.component.ts | 19 +++- .../playlist-item.component.html | 4 +- .../playlist-item.component.spec.ts | 44 ++++++--- .../playlist-item/playlist-item.component.ts | 14 +-- .../source-cleanup-dialog.component.ts | 2 +- .../source-health-indicator.component.ts | 2 +- 16 files changed, 177 insertions(+), 49 deletions(-) create mode 100644 libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info-epg-sources.component.spec.ts diff --git a/docs/architecture/zoneless-migration.md b/docs/architecture/zoneless-migration.md index 630d51ff2..5603c434a 100644 --- a/docs/architecture/zoneless-migration.md +++ b/docs/architecture/zoneless-migration.md @@ -124,20 +124,20 @@ files that still contain `ChangeDetectionStrategy.Eager`. ### libs/playlist (14) -- [ ] `libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts` -- [ ] `libs/playlist/import/feature/src/lib/auto-import/auto-import.component.ts` -- [ ] `libs/playlist/import/feature/src/lib/file-upload/file-upload.component.ts` -- [ ] `libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts` -- [ ] `libs/playlist/import/feature/src/lib/text-import/text-import.component.ts` -- [ ] `libs/playlist/import/feature/src/lib/url-upload/url-upload.component.ts` -- [ ] `libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts` +- [x] `libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts` +- [x] `libs/playlist/import/feature/src/lib/auto-import/auto-import.component.ts` +- [x] `libs/playlist/import/feature/src/lib/file-upload/file-upload.component.ts` +- [x] `libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts` +- [x] `libs/playlist/import/feature/src/lib/text-import/text-import.component.ts` +- [x] `libs/playlist/import/feature/src/lib/url-upload/url-upload.component.ts` +- [x] `libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts` - [ ] `libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.ts` - [ ] `libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts` -- [ ] `libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.component.ts` -- [ ] `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts` -- [ ] `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.ts` -- [ ] `libs/playlist/shared/ui/src/lib/source-health/source-cleanup-dialog.component.ts` -- [ ] `libs/playlist/shared/ui/src/lib/source-health/source-health-indicator.component.ts` +- [x] `libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.component.ts` +- [x] `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts` +- [x] `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.ts` +- [x] `libs/playlist/shared/ui/src/lib/source-health/source-cleanup-dialog.component.ts` +- [x] `libs/playlist/shared/ui/src/lib/source-health/source-health-indicator.component.ts` `libs/playlist/m3u/feature-player` (2) goes with the playback PR. @@ -167,9 +167,9 @@ the field a signal (or a `computed`), or writes it through one. | --- | --- | --- | --- | | [ ] | `libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts` `onChannelNumberInput`/`clearChannelNumberInput` | 2 s `window.setTimeout` hides the channel-number overlay through plain `showChannelNumberOverlay`/`channelNumberInput` | playback | | [ ] | same file, `applySettings` and the settings `effect()` | IndexedDB `storage.get(...).subscribe` and an effect assign plain `playerSettings`, which picks the player in the template | playback | -| [ ] | `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.ts` `checkPortalStatus` | plain `portalStatus` assigned after `await` in `ngOnInit` (PWA only: skipped when source health is supported) | playlist | -| [ ] | `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts` (EPG clear and EPG file pick handlers) | plain `playlist` reassigned after `await` | playlist | -| [ ] | `libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts` (device-id derivation) | `form.patchValue` after `await`; template getters read `control.value`, which is not signal-backed | playlist | +| [x] | `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.ts` `checkPortalStatus` | plain `portalStatus` assigned after `await` in `ngOnInit` (PWA only: skipped when source health is supported) | playlist | +| [x] | `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts` (EPG clear and EPG file pick handlers) | plain `playlist` reassigned after `await` | playlist | +| [x] | `libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts` (device-id derivation) | `form.patchValue` after `await`; no fix needed: while derivation is on `hasManualDeviceIds` is false whatever the IDs are, the controls write their own DOM values, and typing goes through value-accessor listeners that mark the view | playlist | | [ ] | `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` (favorites load) | `favorites` Map filled in a `subscribe` without `markForCheck`; the component is OnPush already, so this is a latent bug today | portal | | [ ] | `libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts` (favorites load) | same pattern; the neighbouring `favoriteMarks.changes$` handler does call `markForCheck` | portal | | [ ] | same file, programme dialog `afterClosed` | deletes from `epgPrograms`/`currentProgramsProgress` after `await` without marking | portal | diff --git a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts index 05090e988..88ded3ec6 100644 --- a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts +++ b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts @@ -72,7 +72,7 @@ const METHOD_BY_CANDIDATE_KIND: Record< selector: 'app-add-playlist', templateUrl: './add-playlist-dialog.component.html', styleUrl: './add-playlist-dialog.component.scss', - changeDetection: ChangeDetectionStrategy.Eager, + changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) export class AddPlaylistDialogComponent { diff --git a/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.ts b/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.ts index 1dcf80b52..0b86cb5af 100644 --- a/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.ts +++ b/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.ts @@ -147,7 +147,7 @@ function maskUrlQueryPasswords(url: string): string { selector: 'app-auto-import', templateUrl: './auto-import.component.html', styleUrl: './auto-import.component.scss', - changeDetection: ChangeDetectionStrategy.Eager, + changeDetection: ChangeDetectionStrategy.OnPush, imports: [ MatButtonModule, MatFormFieldModule, diff --git a/libs/playlist/import/feature/src/lib/file-upload/file-upload.component.ts b/libs/playlist/import/feature/src/lib/file-upload/file-upload.component.ts index ae51aa9c2..d621b9410 100644 --- a/libs/playlist/import/feature/src/lib/file-upload/file-upload.component.ts +++ b/libs/playlist/import/feature/src/lib/file-upload/file-upload.component.ts @@ -18,7 +18,7 @@ const KB = 1024; imports: [DragDropFileUploadDirective, MatIconModule, TranslatePipe], selector: 'app-file-upload', templateUrl: './file-upload.component.html', - changeDetection: ChangeDetectionStrategy.Eager, + changeDetection: ChangeDetectionStrategy.OnPush, styleUrls: ['./file-upload.component.scss'], }) export class FileUploadComponent { diff --git a/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts b/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts index e373891b4..8cd65d5fd 100644 --- a/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts +++ b/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts @@ -75,7 +75,7 @@ interface StalkerSettledIdentity { ], selector: 'app-stalker-portal-import', templateUrl: './stalker-portal-import.component.html', - changeDetection: ChangeDetectionStrategy.Eager, + changeDetection: ChangeDetectionStrategy.OnPush, styles: [ ` :host { diff --git a/libs/playlist/import/feature/src/lib/text-import/text-import.component.ts b/libs/playlist/import/feature/src/lib/text-import/text-import.component.ts index 5eb4f7fd5..f767cffb7 100644 --- a/libs/playlist/import/feature/src/lib/text-import/text-import.component.ts +++ b/libs/playlist/import/feature/src/lib/text-import/text-import.component.ts @@ -17,7 +17,7 @@ import { TranslatePipe } from '@ngx-translate/core'; selector: 'app-text-import', templateUrl: './text-import.component.html', styleUrls: ['./text-import.component.scss'], - changeDetection: ChangeDetectionStrategy.Eager, + changeDetection: ChangeDetectionStrategy.OnPush, imports: [MatInputModule, ReactiveFormsModule, TranslatePipe], }) export class TextImportComponent { diff --git a/libs/playlist/import/feature/src/lib/url-upload/url-upload.component.ts b/libs/playlist/import/feature/src/lib/url-upload/url-upload.component.ts index ce06b6834..59338b8bf 100644 --- a/libs/playlist/import/feature/src/lib/url-upload/url-upload.component.ts +++ b/libs/playlist/import/feature/src/lib/url-upload/url-upload.component.ts @@ -19,7 +19,7 @@ import { RuntimeCapabilitiesService } from '@iptvnator/services'; @Component({ selector: 'app-url-upload', templateUrl: './url-upload.component.html', - changeDetection: ChangeDetectionStrategy.Eager, + changeDetection: ChangeDetectionStrategy.OnPush, imports: [ MatFormFieldModule, MatInputModule, diff --git a/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts b/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts index 83870b92d..ca7c148f9 100644 --- a/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts +++ b/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts @@ -60,7 +60,7 @@ function xtreamServerUrlValidator( ], selector: 'app-xtream-code-import', templateUrl: './xtream-code-import.component.html', - changeDetection: ChangeDetectionStrategy.Eager, + changeDetection: ChangeDetectionStrategy.OnPush, styles: [ ` :host { diff --git a/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.component.ts b/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.component.ts index 6dc6cf9a4..d5de21666 100644 --- a/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.component.ts +++ b/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.component.ts @@ -103,7 +103,7 @@ const SOURCE_CARDS: readonly SourceCard[] = [ './empty-state.responsive.scss', './empty-state.themes.scss', ], - changeDetection: ChangeDetectionStrategy.Eager, + changeDetection: ChangeDetectionStrategy.OnPush, imports: [MatButtonModule, MatIcon, TranslatePipe], }) export class EmptyStateComponent { diff --git a/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info-epg-sources.component.spec.ts b/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info-epg-sources.component.spec.ts new file mode 100644 index 000000000..5a200d2d9 --- /dev/null +++ b/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info-epg-sources.component.spec.ts @@ -0,0 +1,95 @@ +import { TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { Store } from '@ngrx/store'; +import { TranslateModule } from '@ngx-translate/core'; +import { of, Subject } from 'rxjs'; +import { EpgRuntimeBridgeService } from '@iptvnator/epg/data-access'; +import { + DatabaseService, + PlaylistsService, + RuntimeCapabilitiesService, + SettingsStore, +} from '@iptvnator/services'; +import { Playlist } from '@iptvnator/shared/interfaces'; +import { PlaylistInfoComponent } from './playlist-info.component'; +import { STALKER_PLAYLIST_CONNECTION_EDITOR } from './stalker-playlist-connection-editor.token'; + +/** + * The component is OnPush: the playlist is replaced after the awaited EPG + * cleanup, outside any template event, so the rendered source list has to + * follow without a zone-triggered tick. + */ +describe('PlaylistInfoComponent playlist EPG source rows', () => { + const keepUrl = 'https://playlist.example.com/keep.xml'; + const removeUrl = 'https://playlist.example.com/remove.xml'; + const playlist = { + id: 'playlist-1', + _id: 'playlist-1', + title: 'My Playlist', + count: 1, + importDate: '2026-04-01T00:00:00.000Z', + autoRefresh: false, + url: 'https://example.com/playlist.m3u', + epgUrls: [keepUrl, removeUrl], + detectedEpgUrls: [keepUrl, removeUrl], + } as Playlist & { id: string }; + + function render() { + TestBed.configureTestingModule({ + imports: [PlaylistInfoComponent, TranslateModule.forRoot()], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: playlist }, + { + provide: PlaylistsService, + useValue: { getPlaylistById: jest.fn(() => of(playlist)) }, + }, + { provide: DatabaseService, useValue: {} }, + { + provide: EpgRuntimeBridgeService, + useValue: { + supportsDataManagement: true, + clearEpgDataForSource: jest + .fn() + .mockResolvedValue({ success: true }), + }, + }, + { provide: Store, useValue: { dispatch: jest.fn() } }, + { + provide: SettingsStore, + useValue: { getSettings: jest.fn(() => ({ epgUrl: [] })) }, + }, + { provide: MatSnackBar, useValue: { open: jest.fn() } }, + { + provide: MatDialogRef, + useValue: { beforeClosed: () => new Subject() }, + }, + { + provide: RuntimeCapabilitiesService, + useValue: { supportsDesktopFileSave: false }, + }, + { provide: STALKER_PLAYLIST_CONNECTION_EDITOR, useValue: {} }, + ], + }); + const fixture = TestBed.createComponent(PlaylistInfoComponent); + fixture.detectChanges(); + return fixture; + } + + it('drops a removed source row once its cleanup resolves', async () => { + const fixture = render(); + const renderedSources = () => + Array.from( + (fixture.nativeElement as HTMLElement).querySelectorAll( + '.playlist-epg-source-row input[readonly]' + ), + (input) => (input as HTMLInputElement).value + ); + expect(renderedSources()).toEqual([keepUrl, removeUrl]); + + await fixture.componentInstance.removePlaylistEpgSource(removeUrl); + fixture.detectChanges(); + + expect(renderedSources()).toEqual([keepUrl]); + }); +}); diff --git a/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts b/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts index 97192e68c..35a010ded 100644 --- a/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts +++ b/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts @@ -166,7 +166,7 @@ type DesktopFileSaveBridge = Pick< `, ], providers: [DatePipe], - changeDetection: ChangeDetectionStrategy.Eager, + changeDetection: ChangeDetectionStrategy.OnPush, imports: [ ClipboardModule, MatButton, @@ -300,8 +300,20 @@ export class PlaylistInfoComponent { ) as UntypedFormArray; } - /** Playlist object */ - playlist: Playlist & { id: string }; + /** + * Playlist object. Signal-backed because it is replaced after awaited + * hydration and EPG source edits, which no template event marks for an + * OnPush check. + */ + private readonly playlistState = signal( + this.playlistData + ); + get playlist(): Playlist & { id: string } { + return this.playlistState(); + } + set playlist(value: Playlist & { id: string }) { + this.playlistState.set(value); + } /** Form group with playlist details */ playlistDetails!: UntypedFormGroup; @@ -311,7 +323,6 @@ export class PlaylistInfoComponent { this.dialogRef?.beforeClosed().subscribe(() => { this.dialogClosing = true; }); - this.playlist = this.playlistData; this.createForm(); this.connectionTest = createXtreamConnectionTestState( this.playlistDetails diff --git a/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.html b/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.html index 844252b24..f97919a39 100644 --- a/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.html +++ b/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.html @@ -64,8 +64,8 @@ } @else if (showsAutoRefresh) { { fixture.detectChanges(); expect( - (fixture.nativeElement as HTMLElement).querySelector( - '.refresh-btn' - ) + (fixture.nativeElement as HTMLElement).querySelector('.refresh-btn') ).toBeNull(); }); @@ -149,9 +147,7 @@ describe('PlaylistItemComponent', () => { fixture.detectChanges(); expect( - (fixture.nativeElement as HTMLElement).querySelector( - '.refresh-btn' - ) + (fixture.nativeElement as HTMLElement).querySelector('.refresh-btn') ).not.toBeNull(); }); @@ -173,9 +169,7 @@ describe('PlaylistItemComponent', () => { fixture.detectChanges(); expect( - (fixture.nativeElement as HTMLElement).querySelector( - '.refresh-btn' - ) + (fixture.nativeElement as HTMLElement).querySelector('.refresh-btn') ).not.toBeNull(); fixture.destroy(); @@ -195,12 +189,38 @@ describe('PlaylistItemComponent', () => { fixture.detectChanges(); expect( - (fixture.nativeElement as HTMLElement).querySelector( - '.refresh-btn' - ) + (fixture.nativeElement as HTMLElement).querySelector('.refresh-btn') ).toBeNull(); }); + // OnPush: the status arrives after an await outside any template event, + // so it must reach the view without a zone-triggered tick. + it('renders the portal status that resolves after the first render', async () => { + fixture.destroy(); + fixture = TestBed.createComponent(PlaylistItemComponent); + component = fixture.componentInstance; + component.item = { + title: 'Xtream Source', + _id: 'xtream-source', + count: 10, + importDate: Date.now().toString(), + autoRefresh: false, + serverUrl: 'https://example.com', + username: 'demo', + password: 'secret', + }; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const statusDot = (fixture.nativeElement as HTMLElement).querySelector( + '.status-dot' + ); + expect(statusDot?.getAttribute('aria-label')).toBe( + 'Portal status: active' + ); + }); + it('renders cancel and progress UI for long-running playlist actions', () => { fixture.componentRef.setInput('isDeleting', true); fixture.componentRef.setInput( diff --git a/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.ts b/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.ts index 033de62c5..8cbe29077 100644 --- a/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.ts +++ b/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.ts @@ -17,6 +17,7 @@ import { inject, input, output, + signal, ChangeDetectionStrategy, } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; @@ -39,7 +40,7 @@ import type { PlaylistMeta } from '@iptvnator/shared/interfaces'; selector: 'app-playlist-item', templateUrl: './playlist-item.component.html', styleUrls: ['./playlist-item.component.scss'], - changeDetection: ChangeDetectionStrategy.Eager, + changeDetection: ChangeDetectionStrategy.OnPush, imports: [ SourceHealthIndicatorComponent, DatePipe, @@ -70,7 +71,7 @@ export class PlaylistItemComponent implements OnInit { readonly removeClicked = output(); readonly cancelBusyActionClicked = output(); - portalStatus: PortalStatus = 'unavailable'; + readonly portalStatus = signal('unavailable'); private readonly portalStatusService = inject(PortalStatusService); readonly runtime = inject(RuntimeCapabilitiesService); readonly sourceIcons = SOURCE_TYPE_ICONS; @@ -125,21 +126,22 @@ export class PlaylistItemComponent implements OnInit { private async checkPortalStatus() { if (this.item.serverUrl && this.item.username && this.item.password) { - this.portalStatus = + this.portalStatus.set( await this.portalStatusService.checkPortalStatus( this.item.serverUrl, this.item.username, this.item.password - ); + ) + ); } } getStatusClass(): string { - return this.portalStatusService.getStatusClass(this.portalStatus); + return this.portalStatusService.getStatusClass(this.portalStatus()); } getStatusIcon(): string { - return this.portalStatusService.getStatusIcon(this.portalStatus); + return this.portalStatusService.getStatusIcon(this.portalStatus()); } onPlaylistClick(): void { diff --git a/libs/playlist/shared/ui/src/lib/source-health/source-cleanup-dialog.component.ts b/libs/playlist/shared/ui/src/lib/source-health/source-cleanup-dialog.component.ts index b23f785c1..43558e4f4 100644 --- a/libs/playlist/shared/ui/src/lib/source-health/source-cleanup-dialog.component.ts +++ b/libs/playlist/shared/ui/src/lib/source-health/source-cleanup-dialog.component.ts @@ -181,7 +181,7 @@ export interface SourceCleanupDialogData extends SourceCleanupContext { } `, - changeDetection: ChangeDetectionStrategy.Eager, + changeDetection: ChangeDetectionStrategy.OnPush, styles: [ ` :host { diff --git a/libs/playlist/shared/ui/src/lib/source-health/source-health-indicator.component.ts b/libs/playlist/shared/ui/src/lib/source-health/source-health-indicator.component.ts index 3637c056a..4573a2b26 100644 --- a/libs/playlist/shared/ui/src/lib/source-health/source-health-indicator.component.ts +++ b/libs/playlist/shared/ui/src/lib/source-health/source-health-indicator.component.ts @@ -29,7 +29,7 @@ import { PlaylistMeta, sourceHealthType } from '@iptvnator/shared/interfaces'; role="img" > }`, - changeDetection: ChangeDetectionStrategy.Eager, + changeDetection: ChangeDetectionStrategy.OnPush, styles: [ ` :host {