mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(settings): close lock editors on relock and clear withheld details opened from All
The Xtream, Stalker and M3U category editors are gated by the PIN only when they open; an idle relock left them on screen listing locked names with a lock-rewriting Save. Each now closes itself when the session relocks. ParentalLockEnforcementService also judges the selected Xtream/Stalker item by its own category: a detail opened from All, recently added or search has no selected category to vanish with, so it stayed open after a relock. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
45661e311e
commit
bc1b0eef41
9 files changed
+438
-21
No files matched your search
@@ -0,0 +1,169 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
|
||||
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
|
||||
import { ParentalLockService } from '@iptvnator/services';
|
||||
import { ParentalLockEnforcementService } from './parental-lock-enforcement.service';
|
||||
import { PlaybackKeepAwakeService } from './playback-keep-awake.service';
|
||||
|
||||
interface Applier {
|
||||
applyXtream(): Promise<void>;
|
||||
applyStalker(): Promise<void>;
|
||||
}
|
||||
|
||||
describe('ParentalLockEnforcementService', () => {
|
||||
const router = { url: '/', navigate: jest.fn() };
|
||||
const lockedStalkerIds = new Set<string>();
|
||||
const parentalLock = {
|
||||
version: signal(0),
|
||||
registerBusyProbe: jest.fn(),
|
||||
isStalkerCategoryLocked: jest.fn(
|
||||
(_playlistId: string, _type: string, id: unknown) =>
|
||||
id !== null &&
|
||||
id !== undefined &&
|
||||
lockedStalkerIds.has(String(id))
|
||||
),
|
||||
isM3uGroupLocked: jest.fn(() => false),
|
||||
};
|
||||
const stalkerStore = {
|
||||
currentPlaylist: signal<{ _id: string } | null>({ _id: 'stalker-1' }),
|
||||
selectedContentType: signal<string>('vod'),
|
||||
selectedCategoryId: signal<string | null>('*'),
|
||||
selectedItem: signal<{ category_id?: string } | null>(null),
|
||||
clearSelectedItem: jest.fn(),
|
||||
setSelectedCategory: jest.fn(),
|
||||
};
|
||||
const xtreamStore = {
|
||||
playlistId: signal<string | null>('xtream-1'),
|
||||
selectedCategoryId: signal<number | null>(null),
|
||||
selectedItem: signal<{ category_id?: number } | null>(null),
|
||||
reloadCategories: jest.fn(async () => undefined),
|
||||
reloadCachedContent: jest.fn(async () => undefined),
|
||||
getCategoriesBySelectedType: jest.fn(() => [{ id: 7 }, { id: 8 }]),
|
||||
setSelectedItem: jest.fn(),
|
||||
setSelectedCategory: jest.fn(),
|
||||
};
|
||||
let service: Applier;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
lockedStalkerIds.clear();
|
||||
router.url = '/';
|
||||
stalkerStore.selectedCategoryId.set('*');
|
||||
stalkerStore.selectedItem.set(null);
|
||||
xtreamStore.selectedCategoryId.set(null);
|
||||
xtreamStore.selectedItem.set(null);
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ParentalLockService, useValue: parentalLock },
|
||||
{ provide: XtreamStore, useValue: xtreamStore },
|
||||
{ provide: StalkerStore, useValue: stalkerStore },
|
||||
{ provide: Router, useValue: router },
|
||||
{
|
||||
provide: Store,
|
||||
useValue: {
|
||||
selectSignal: () => signal(null),
|
||||
dispatch: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PlaybackKeepAwakeService,
|
||||
useValue: { hasPlayingVideo: () => false },
|
||||
},
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(
|
||||
ParentalLockEnforcementService
|
||||
) as unknown as Applier;
|
||||
});
|
||||
|
||||
describe('Stalker', () => {
|
||||
it('clears a detail opened from All whose own genre is withheld', async () => {
|
||||
router.url = '/workspace/stalker/stalker-1/vod/42';
|
||||
lockedStalkerIds.add('9');
|
||||
stalkerStore.selectedItem.set({ category_id: '9' });
|
||||
|
||||
await service.applyStalker();
|
||||
|
||||
expect(stalkerStore.clearSelectedItem).toHaveBeenCalled();
|
||||
// "All" itself is not locked, so the category stays selected.
|
||||
expect(stalkerStore.setSelectedCategory).not.toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith([
|
||||
'/workspace',
|
||||
'stalker',
|
||||
'stalker-1',
|
||||
'vod',
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves a detail from All alone when its genre is not locked', async () => {
|
||||
router.url = '/workspace/stalker/stalker-1/vod/42';
|
||||
lockedStalkerIds.add('9');
|
||||
stalkerStore.selectedItem.set({ category_id: '3' });
|
||||
|
||||
await service.applyStalker();
|
||||
|
||||
expect(stalkerStore.clearSelectedItem).not.toHaveBeenCalled();
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('steps off a locked selected category', async () => {
|
||||
router.url = '/workspace/stalker/stalker-1/vod';
|
||||
lockedStalkerIds.add('9');
|
||||
stalkerStore.selectedCategoryId.set('9');
|
||||
|
||||
await service.applyStalker();
|
||||
|
||||
expect(stalkerStore.clearSelectedItem).toHaveBeenCalled();
|
||||
expect(stalkerStore.setSelectedCategory).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Xtream', () => {
|
||||
it('clears a selected item whose category is no longer readable', async () => {
|
||||
router.url = '/workspace/xtreams/xtream-1/vod/42';
|
||||
xtreamStore.selectedItem.set({ category_id: 99 });
|
||||
|
||||
await service.applyXtream();
|
||||
|
||||
expect(xtreamStore.reloadCategories).toHaveBeenCalled();
|
||||
expect(xtreamStore.setSelectedItem).toHaveBeenCalledWith(null);
|
||||
expect(xtreamStore.setSelectedCategory).not.toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith([
|
||||
'/workspace',
|
||||
'xtreams',
|
||||
'xtream-1',
|
||||
'vod',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps a selected item whose category survived the lock', async () => {
|
||||
router.url = '/workspace/xtreams/xtream-1/vod/42';
|
||||
xtreamStore.selectedCategoryId.set(7);
|
||||
xtreamStore.selectedItem.set({ category_id: 7 });
|
||||
|
||||
await service.applyXtream();
|
||||
|
||||
expect(xtreamStore.setSelectedItem).not.toHaveBeenCalled();
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('steps off a selected category that vanished', async () => {
|
||||
router.url = '/workspace/xtreams/xtream-1/live';
|
||||
xtreamStore.selectedCategoryId.set(99);
|
||||
|
||||
await service.applyXtream();
|
||||
|
||||
expect(xtreamStore.setSelectedItem).toHaveBeenCalledWith(null);
|
||||
expect(xtreamStore.setSelectedCategory).toHaveBeenCalledWith(null);
|
||||
expect(router.navigate).toHaveBeenCalledWith([
|
||||
'/workspace',
|
||||
'xtreams',
|
||||
'xtream-1',
|
||||
'live',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -72,20 +72,42 @@ export class ParentalLockEnforcementService {
|
||||
await this.xtreamStore.reloadCachedContent();
|
||||
|
||||
const match = XTREAM_ROUTE.exec(this.router.url);
|
||||
const selectedCategoryId = this.xtreamStore.selectedCategoryId();
|
||||
if (selectedCategoryId === null) {
|
||||
return;
|
||||
}
|
||||
const categoryType = toParentalLockXtreamCategoryType(match?.[2]);
|
||||
const categories = this.xtreamStore.getCategoriesBySelectedType();
|
||||
const stillVisible = categories.some(
|
||||
(category) =>
|
||||
Number(
|
||||
(category as { id?: number | string }).id ??
|
||||
(category as { category_id?: string }).category_id
|
||||
) === Number(selectedCategoryId)
|
||||
);
|
||||
if (stillVisible) {
|
||||
const isVisibleCategory = (categoryId: unknown): boolean =>
|
||||
categories.some(
|
||||
(category) =>
|
||||
Number(
|
||||
(category as { id?: number | string }).id ??
|
||||
(category as { category_id?: string }).category_id
|
||||
) === Number(categoryId)
|
||||
);
|
||||
const selectedCategoryId = this.xtreamStore.selectedCategoryId();
|
||||
// The selected ITEM is judged on its own category: opened from
|
||||
// "All", recently added or search it has no selected category to
|
||||
// vanish with, yet its detail must not outlive the lock.
|
||||
const selectedItem = this.xtreamStore.selectedItem?.() as {
|
||||
category_id?: string | number;
|
||||
} | null;
|
||||
const itemWithheld =
|
||||
selectedItem?.category_id !== undefined &&
|
||||
selectedItem?.category_id !== null &&
|
||||
!isVisibleCategory(selectedItem.category_id);
|
||||
if (itemWithheld) {
|
||||
this.xtreamStore.setSelectedItem(null);
|
||||
}
|
||||
if (
|
||||
selectedCategoryId === null ||
|
||||
isVisibleCategory(selectedCategoryId)
|
||||
) {
|
||||
if (itemWithheld && match && match[1] === playlistId) {
|
||||
void this.router.navigate([
|
||||
'/workspace',
|
||||
'xtreams',
|
||||
match[1],
|
||||
match[2],
|
||||
]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.xtreamStore.setSelectedItem(null);
|
||||
@@ -109,20 +131,36 @@ export class ParentalLockEnforcementService {
|
||||
const contentType = toParentalLockStalkerCategoryType(
|
||||
this.stalkerStore.selectedContentType()
|
||||
);
|
||||
if (!contentType) {
|
||||
return;
|
||||
}
|
||||
const selectedCategoryId = this.stalkerStore.selectedCategoryId();
|
||||
if (
|
||||
!contentType ||
|
||||
!selectedCategoryId ||
|
||||
!this.parentalLock.isStalkerCategoryLocked(
|
||||
const categoryWithheld =
|
||||
!!selectedCategoryId &&
|
||||
this.parentalLock.isStalkerCategoryLocked(
|
||||
playlistId,
|
||||
contentType,
|
||||
selectedCategoryId
|
||||
)
|
||||
) {
|
||||
);
|
||||
// An item opened from "All" (`*`) or search has its own genre to be
|
||||
// judged by; the list dropping its row is not enough.
|
||||
const selectedItem = this.stalkerStore.selectedItem?.() as {
|
||||
category_id?: string | number;
|
||||
} | null;
|
||||
const itemWithheld =
|
||||
!!selectedItem &&
|
||||
this.parentalLock.isStalkerCategoryLocked(
|
||||
playlistId,
|
||||
contentType,
|
||||
selectedItem.category_id
|
||||
);
|
||||
if (!categoryWithheld && !itemWithheld) {
|
||||
return;
|
||||
}
|
||||
this.stalkerStore.clearSelectedItem();
|
||||
this.stalkerStore.setSelectedCategory(null);
|
||||
if (categoryWithheld) {
|
||||
this.stalkerStore.setSelectedCategory(null);
|
||||
}
|
||||
const match = STALKER_ROUTE.exec(this.router.url);
|
||||
if (match && match[1] === playlistId) {
|
||||
void this.router.navigate([
|
||||
|
||||
@@ -105,7 +105,18 @@ locked default.
|
||||
`ParentalLockEnforcementService` (`apps/web/src/app/services/`) calls
|
||||
`XtreamStore.reloadCategories()` + `reloadCachedContent()` and, if the
|
||||
selected category vanished, clears the selection and navigates to the
|
||||
section root.
|
||||
section root. The selected ITEM is judged separately, on its own
|
||||
`category_id` against the reloaded category list: a detail opened from
|
||||
"All", recently added or search has no selected category to vanish
|
||||
with, so its row disappearing from a list is not enough — the item is
|
||||
cleared and the route returns to the section root while the (still
|
||||
readable) selected category stays.
|
||||
- **Stalker:** the same service checks the selected genre through
|
||||
`isStalkerCategoryLocked` and, independently, the selected item's own
|
||||
`category_id` — an item opened from `*` (All) or search is withheld by
|
||||
its genre even though `*` itself can never be locked. A withheld item is
|
||||
cleared and the section root is navigated to; the category selection is
|
||||
reset only when the genre itself is locked.
|
||||
- **Xtream (PWA):** `PwaXtreamDataSource` drops withheld categories,
|
||||
streams and search hits at read time; the same reloads apply.
|
||||
- **Warm-cache detection (Electron):** the filtered category/content reads
|
||||
@@ -167,7 +178,11 @@ locked default.
|
||||
`StalkerCategoryLockDialogComponent` reached from a lock button above the
|
||||
categories rail; it offers "Lock adult (18+)" for genres the portal flags
|
||||
`censored`. All three list the locked names and can rewrite the locks, so
|
||||
each opens only after `requestUnlock()` succeeds. The Xtream dialog loads
|
||||
each opens only after `requestUnlock()` succeeds — and closes itself,
|
||||
discarding the draft, the moment `active` becomes true again (idle relock,
|
||||
Lock now), since the PIN gate covers only the opening. The M3U group
|
||||
dialog does this only while it carries lock toggles; the plain hide/show
|
||||
editor is not behind the PIN. The Xtream dialog loads
|
||||
its candidates through the capability-selected data source
|
||||
(`IXtreamDataSource.getAllCategories`, which the PWA source answers from
|
||||
its session cache or the API), so PWA users can set locks too; the
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ParentalLockService } from '@iptvnator/services';
|
||||
import {
|
||||
StalkerCategoryLockDialogComponent,
|
||||
StalkerCategoryLockDialogData,
|
||||
} from './stalker-category-lock-dialog.component';
|
||||
|
||||
describe('StalkerCategoryLockDialogComponent', () => {
|
||||
let fixture: ComponentFixture<StalkerCategoryLockDialogComponent>;
|
||||
let component: StalkerCategoryLockDialogComponent;
|
||||
const dialogRef = { close: jest.fn() };
|
||||
const parentalLock = {
|
||||
active: signal(false),
|
||||
lockedStalkerIds: jest.fn(() => ['2']),
|
||||
setStalkerLocks: jest.fn(),
|
||||
};
|
||||
const data: StalkerCategoryLockDialogData = {
|
||||
playlistId: 'stalker-1',
|
||||
contentType: 'itv',
|
||||
categories: [
|
||||
{ category_id: '1', category_name: 'News', censored: false },
|
||||
{ category_id: '2', category_name: 'Adult', censored: true },
|
||||
] as StalkerCategoryLockDialogData['categories'],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
parentalLock.active.set(false);
|
||||
parentalLock.setStalkerLocks.mockResolvedValue(true);
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StalkerCategoryLockDialogComponent,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: dialogRef },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: data },
|
||||
{ provide: ParentalLockService, useValue: parentalLock },
|
||||
{ provide: MatSnackBar, useValue: { open: jest.fn() } },
|
||||
],
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(StalkerCategoryLockDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('seeds the draft from the stored locks', () => {
|
||||
expect(component.categories().map((c) => c.locked)).toEqual([
|
||||
false,
|
||||
true,
|
||||
]);
|
||||
expect(component.lockedCount()).toBe(1);
|
||||
});
|
||||
|
||||
it('closes without saving when the session relocks while it is open', () => {
|
||||
expect(dialogRef.close).not.toHaveBeenCalled();
|
||||
|
||||
parentalLock.active.set(true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(dialogRef.close).toHaveBeenCalledWith(false);
|
||||
expect(parentalLock.setStalkerLocks).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+11
@@ -2,6 +2,7 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
@@ -82,6 +83,16 @@ export class StalkerCategoryLockDialogComponent {
|
||||
() => this.categories().filter((category) => category.censored).length
|
||||
);
|
||||
|
||||
constructor() {
|
||||
// Same relock rule as the Xtream editor: the dialog outliving the
|
||||
// unlocked session would keep the locked names and Save reachable.
|
||||
effect(() => {
|
||||
if (this.parentalLock.active()) {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private buildDraft(): LockableCategory[] {
|
||||
const locked = new Set(
|
||||
this.parentalLock.lockedStalkerIds(
|
||||
|
||||
+24
@@ -1,9 +1,11 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { XTREAM_DATA_SOURCE } from '@iptvnator/portal/xtream/data-access';
|
||||
import {
|
||||
DatabaseService,
|
||||
ParentalLockService,
|
||||
RuntimeCapabilitiesService,
|
||||
XCategoryFromDb,
|
||||
} from '@iptvnator/services';
|
||||
@@ -35,6 +37,12 @@ describe('CategoryManagementDialogComponent', () => {
|
||||
getAllCategories: jest.fn(),
|
||||
};
|
||||
const dialogRef = { close: jest.fn() };
|
||||
const parentalLock = {
|
||||
enabled: signal(false),
|
||||
active: signal(false),
|
||||
lockedXtreamIds: jest.fn(() => [] as number[]),
|
||||
setXtreamLocks: jest.fn(),
|
||||
};
|
||||
const data: CategoryManagementDialogData = {
|
||||
playlistId: 'mock-playlist',
|
||||
contentType: 'live',
|
||||
@@ -45,6 +53,8 @@ describe('CategoryManagementDialogComponent', () => {
|
||||
jest.clearAllMocks();
|
||||
dataSource.getAllCategories.mockResolvedValue(categories);
|
||||
db.updateCategoryVisibility.mockResolvedValue(undefined);
|
||||
parentalLock.enabled.set(false);
|
||||
parentalLock.active.set(false);
|
||||
data.contentType = 'live';
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -60,6 +70,7 @@ describe('CategoryManagementDialogComponent', () => {
|
||||
},
|
||||
{ provide: MatDialogRef, useValue: dialogRef },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: data },
|
||||
{ provide: ParentalLockService, useValue: parentalLock },
|
||||
],
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(CategoryManagementDialogComponent);
|
||||
@@ -184,6 +195,19 @@ describe('CategoryManagementDialogComponent', () => {
|
||||
expect(dialogRef.close).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('closes without saving when the session relocks while it is open', () => {
|
||||
parentalLock.enabled.set(true);
|
||||
fixture.detectChanges();
|
||||
expect(dialogRef.close).not.toHaveBeenCalled();
|
||||
|
||||
parentalLock.active.set(true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(dialogRef.close).toHaveBeenCalledWith(false);
|
||||
expect(db.updateCategoryVisibility).not.toHaveBeenCalled();
|
||||
expect(parentalLock.setXtreamLocks).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('discards pending bulk changes on cancel', () => {
|
||||
component.searchTerm.set('FR');
|
||||
component.selectAll();
|
||||
|
||||
+12
@@ -2,6 +2,7 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
OnInit,
|
||||
signal,
|
||||
@@ -107,6 +108,17 @@ export class CategoryManagementDialogComponent implements OnInit {
|
||||
this.filteredSelectedCount() === this.filteredCategories().length
|
||||
);
|
||||
|
||||
constructor() {
|
||||
// The PIN gate only covers opening: a relock (idle timer, Lock now)
|
||||
// while the editor is open must take its locked names and its
|
||||
// lock-rewriting Save away too.
|
||||
effect(() => {
|
||||
if (this.parentalLock.active()) {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await this.loadCategories();
|
||||
}
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ParentalLockService } from '@iptvnator/services';
|
||||
import {
|
||||
GroupManagementDialogComponent,
|
||||
GroupManagementDialogData,
|
||||
} from './group-management-dialog.component';
|
||||
|
||||
describe('GroupManagementDialogComponent', () => {
|
||||
const dialogRef = { close: jest.fn() };
|
||||
const parentalLock = { active: signal(false) };
|
||||
|
||||
async function create(
|
||||
data: GroupManagementDialogData
|
||||
): Promise<ComponentFixture<GroupManagementDialogComponent>> {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
GroupManagementDialogComponent,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: dialogRef },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: data },
|
||||
{ provide: ParentalLockService, useValue: parentalLock },
|
||||
],
|
||||
}).compileComponents();
|
||||
const fixture = TestBed.createComponent(GroupManagementDialogComponent);
|
||||
fixture.detectChanges();
|
||||
return fixture;
|
||||
}
|
||||
|
||||
const groups = [
|
||||
{ key: 'News', count: 3 },
|
||||
{ key: 'Adult', count: 2 },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
parentalLock.active.set(false);
|
||||
});
|
||||
|
||||
it('closes without a result when the session relocks while locks are shown', async () => {
|
||||
const fixture = await create({
|
||||
groups,
|
||||
hiddenGroupTitles: [],
|
||||
lockedGroupTitles: ['Adult'],
|
||||
});
|
||||
expect(dialogRef.close).not.toHaveBeenCalled();
|
||||
|
||||
parentalLock.active.set(true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(dialogRef.close).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it('keeps the plain hide/show editor open when the lock feature is off', async () => {
|
||||
const fixture = await create({ groups, hiddenGroupTitles: ['News'] });
|
||||
|
||||
parentalLock.active.set(true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(dialogRef.close).not.toHaveBeenCalled();
|
||||
expect(fixture.componentInstance.showLocks).toBe(false);
|
||||
});
|
||||
});
|
||||
+13
@@ -3,6 +3,7 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { ParentalLockService } from '@iptvnator/services';
|
||||
import { foldSearchText } from '@iptvnator/shared/interfaces';
|
||||
|
||||
export interface GroupManagementDialogGroup {
|
||||
@@ -64,6 +66,7 @@ export class GroupManagementDialogComponent {
|
||||
>
|
||||
);
|
||||
readonly data = inject<GroupManagementDialogData>(MAT_DIALOG_DATA);
|
||||
private readonly parentalLock = inject(ParentalLockService);
|
||||
|
||||
readonly showLocks = this.data.lockedGroupTitles !== undefined;
|
||||
readonly searchTerm = signal('');
|
||||
@@ -100,6 +103,16 @@ export class GroupManagementDialogComponent {
|
||||
this.groups().every((group) => group.selected)
|
||||
);
|
||||
|
||||
constructor() {
|
||||
// A relock while the editor is open: the locked group names it
|
||||
// lists and the lock list it can rewrite are behind the PIN.
|
||||
effect(() => {
|
||||
if (this.showLocks && this.parentalLock.active()) {
|
||||
this.dialogRef.close(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearSearch(): void {
|
||||
this.searchTerm.set('');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user