diff --git a/docs/architecture/parental-lock.md b/docs/architecture/parental-lock.md index 507689e8c..df651bae2 100644 --- a/docs/architecture/parental-lock.md +++ b/docs/architecture/parental-lock.md @@ -372,7 +372,10 @@ on either side. lock store, failure snackbar); the M3U rail emits the toggled group to `ChannelListContainerComponent`, which captures the playlist, asks for the PIN, and persists the edit only if that playlist is still open (two - playlists can share a group name). Bulk actions stay in the dialog, and while the session is locked + playlists can share a group name). The M3U management dialog is bound + the same way: `GroupsViewComponent` captures its `playlistId` input + before the PIN and drops the dialog's result once another playlist is + open. Bulk actions stay in the dialog, and while the session is locked a locked category is not in the rail, so unlocking always goes through the "N locked · Enter PIN to show" row or the dialog. The M3U dialog hands its lock list back to `ChannelListContainerComponent`, which diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html index 0134bbead..f3ccb20bf 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html @@ -62,6 +62,7 @@ [activeChannelUrl]="activeChannelUrl()" [favoriteIds]="favoriteIds()" [hiddenGroupTitles]="hiddenGroupTitles()" + [playlistId]="activePlaylist()?._id ?? null" [lockedGroupTitles]="lockedGroupTitles()" [withheldGroupCount]="withheldGroupCount()" [managementGroups]="managementGroups()" diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts index 4d5addf82..ba8f8681f 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts @@ -133,7 +133,7 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy { /** Route-aware playlist ID for recent-item mutations */ private readonly resolvedPlaylistId = this.playlistContext.resolvedPlaylistId; - private readonly activePlaylist = this.playlistContext.activePlaylist; + protected readonly activePlaylist = this.playlistContext.activePlaylist; /** Map of channel ID to current EPG program */ readonly channelEpgMap = signal(new Map()); diff --git a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.spec.ts b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.spec.ts index ab8dfd8b9..957cab029 100644 --- a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.spec.ts +++ b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.spec.ts @@ -4,7 +4,7 @@ import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MatDialog } from '@angular/material/dialog'; import { TranslateModule } from '@ngx-translate/core'; -import { of } from 'rxjs'; +import { of, Subject } from 'rxjs'; import { ParentalLockService } from '@iptvnator/services'; import { Channel } from '@iptvnator/shared/interfaces'; import { ChannelDetailsDialogComponent } from '../channel-details-dialog/channel-details-dialog.component'; @@ -537,6 +537,25 @@ describe('GroupsViewComponent', () => { ]); }); + it('drops the manage-groups result once another playlist is open', async () => { + const hiddenGroupTitlesChanged = jest.fn(); + const lockedGroupTitlesChanged = jest.fn(); + component.hiddenGroupTitlesChanged.subscribe(hiddenGroupTitlesChanged); + component.lockedGroupTitlesChanged.subscribe(lockedGroupTitlesChanged); + fixture.componentRef.setInput('playlistId', 'playlist-a'); + fixture.componentRef.setInput('lockedGroupTitles', ['News']); + fixture.detectChanges(); + const closed = new Subject(); + dialog.open.mockReturnValue({ afterClosed: () => closed }); + + await component.openGroupManagement(); + fixture.componentRef.setInput('playlistId', 'playlist-b'); + closed.next({ hiddenGroupTitles: [], lockedGroupTitles: [] }); + + expect(hiddenGroupTitlesChanged).not.toHaveBeenCalled(); + expect(lockedGroupTitlesChanged).not.toHaveBeenCalled(); + }); + it('emits a single-group toggle from the right-click menu (the host asks for the PIN)', () => { const groupLockToggled = jest.fn(); component.groupLockToggled.subscribe(groupLockToggled); diff --git a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.ts b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.ts index 282839055..a4d4f6369 100644 --- a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.ts @@ -153,6 +153,12 @@ export class GroupsViewComponent { * of the playlist, locked ones included, because `groupedChannels` * arrives with the withheld groups already removed. */ + /** + * The playlist the groups belong to. The management dialog's result is + * applied only while it is still the open one: the host saves hidden + * and locked groups under the CURRENT playlist. + */ + readonly playlistId = input(null); readonly lockedGroupTitles = input(null); readonly managementGroups = input( null @@ -535,7 +541,11 @@ export class GroupsViewComponent { // The dialog lists every group by name, locked ones included, and can // rewrite the locks — so it sits behind the PIN like the portal // dialogs do. - if (!(await this.parentalLock.requestUnlock())) { + const playlistId = this.playlistId(); + if ( + !(await this.parentalLock.requestUnlock()) || + this.playlistId() !== playlistId + ) { return; } const groups = @@ -558,7 +568,7 @@ export class GroupsViewComponent { }); dialogRef.afterClosed().subscribe((result) => { - if (result === undefined) { + if (result === undefined || this.playlistId() !== playlistId) { return; }