fix(settings): bind the M3U group management result to its playlist

The groups view captures the playlist before the PIN prompt and drops
the management dialog's hidden and locked group lists once another
playlist is open, so they cannot be saved under that playlist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-27 15:36:34 +02:00
1 parent 4c64b798e1
commit 0f225d8f95
5 files changed
+38 -5

No files matched your search

+4 -1
View File
@@ -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
@@ -62,6 +62,7 @@
[activeChannelUrl]="activeChannelUrl()"
[favoriteIds]="favoriteIds()"
[hiddenGroupTitles]="hiddenGroupTitles()"
[playlistId]="activePlaylist()?._id ?? null"
[lockedGroupTitles]="lockedGroupTitles()"
[withheldGroupCount]="withheldGroupCount()"
[managementGroups]="managementGroups()"
@@ -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<string, EpgProgram | null>());
@@ -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<unknown>();
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);
@@ -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<string | null>(null);
readonly lockedGroupTitles = input<string[] | null>(null);
readonly managementGroups = input<GroupManagementDialogGroup[] | null>(
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;
}