mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
fix(playlist): keep the pasted provider message when switching import methods
The auto-detect surface is destroyed by the dialog's method switch, so inspecting a prefilled form and coming back lost the paste. The text now lives on the dialog and is fed back into the surface on re-entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
This commit is contained in:
4 files changed
+133
-3
No files matched your search
+2
@@ -59,6 +59,8 @@
|
||||
}
|
||||
@case ('auto') {
|
||||
<app-auto-import
|
||||
[initialText]="autoDetectText()"
|
||||
(textChanged)="autoDetectText.set($event)"
|
||||
(candidateSelected)="onCandidateSelected($event)"
|
||||
/>
|
||||
}
|
||||
|
||||
+6
@@ -101,6 +101,12 @@ export class AddPlaylistDialogComponent {
|
||||
private readonly pendingPrefill =
|
||||
signal<ProviderImportCandidate | null>(null);
|
||||
|
||||
/**
|
||||
* Survives the auto-detect surface being destroyed by a method switch, so
|
||||
* returning to it after inspecting a prefilled form keeps the paste.
|
||||
*/
|
||||
readonly autoDetectText = signal('');
|
||||
|
||||
// Order matches the v0.22 mockup left-to-right: URL first (Most common),
|
||||
// then File, Xtream credentials, Stalker portal, raw text paste. Each
|
||||
// entry stands on its own — no nested subtypes. Labels are short and
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { provideNoopAnimations } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ProviderImportCandidate } from '@iptvnator/shared/interfaces';
|
||||
import { AutoImportComponent } from './auto-import.component';
|
||||
|
||||
describe('AutoImportComponent', () => {
|
||||
let component: AutoImportComponent;
|
||||
let fixture: ComponentFixture<AutoImportComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [AutoImportComponent, TranslateModule.forRoot()],
|
||||
providers: [provideNoopAnimations()],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AutoImportComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('computes candidates from the pasted text', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
component.textControl.setValue(
|
||||
'https://lists.example.com/main.m3u'
|
||||
);
|
||||
|
||||
expect(component.candidates()).toHaveLength(1);
|
||||
expect(component.candidates()[0].kind).toBe('m3u-url');
|
||||
});
|
||||
|
||||
it('restores the dialog-held text so a method switch does not lose the paste', () => {
|
||||
fixture.componentRef.setInput(
|
||||
'initialText',
|
||||
'MAC: 00:1A:79:12:34:56'
|
||||
);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.textControl.value).toBe('MAC: 00:1A:79:12:34:56');
|
||||
expect(component.candidates()[0]?.kind).toBe('stalker');
|
||||
});
|
||||
|
||||
it('reports edits back to the dialog', () => {
|
||||
fixture.detectChanges();
|
||||
const emitted: string[] = [];
|
||||
component.textChanged.subscribe((value) => emitted.push(value));
|
||||
|
||||
component.textControl.setValue('some text');
|
||||
|
||||
expect(emitted).toEqual(['some text']);
|
||||
});
|
||||
|
||||
it('clears the textarea and the candidates', () => {
|
||||
fixture.detectChanges();
|
||||
component.textControl.setValue('https://lists.example.com/main.m3u');
|
||||
|
||||
component.clearForm();
|
||||
|
||||
expect(component.textControl.value).toBe('');
|
||||
expect(component.candidates()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('emits the picked candidate', () => {
|
||||
fixture.detectChanges();
|
||||
const emitted: ProviderImportCandidate[] = [];
|
||||
component.candidateSelected.subscribe((candidate) =>
|
||||
emitted.push(candidate)
|
||||
);
|
||||
const candidate: ProviderImportCandidate = {
|
||||
kind: 'xtream',
|
||||
confidence: 'high',
|
||||
username: 'alice',
|
||||
};
|
||||
|
||||
component.selectCandidate(candidate);
|
||||
|
||||
expect(emitted).toEqual([candidate]);
|
||||
});
|
||||
|
||||
it('masks the password in the summary rows', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const rows = component.summaryRows({
|
||||
kind: 'xtream',
|
||||
confidence: 'high',
|
||||
serverUrl: 'http://tv.example.com:8080',
|
||||
username: 'alice',
|
||||
password: 's3cret',
|
||||
});
|
||||
|
||||
const passwordRow = rows.find(
|
||||
(row) => row.labelKey === 'HOME.XTREAM_PLAYLIST.PASSWORD'
|
||||
);
|
||||
expect(passwordRow?.value).toBe('••••••');
|
||||
expect(JSON.stringify(rows)).not.toContain('s3cret');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, computed, output } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { Component, computed, input, OnInit, output } from '@angular/core';
|
||||
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -59,15 +59,37 @@ const CONFIDENCE_LABEL_KEYS: Record<ProviderImportConfidence, string> = {
|
||||
TranslatePipe,
|
||||
],
|
||||
})
|
||||
export class AutoImportComponent {
|
||||
export class AutoImportComponent implements OnInit {
|
||||
readonly candidateSelected = output<ProviderImportCandidate>();
|
||||
|
||||
/**
|
||||
* The pasted text lives in the DIALOG, not here: switching to a prefilled
|
||||
* form destroys this component (`@switch`), and coming back to compare or
|
||||
* pick another candidate must not cost the user their paste. The dialog
|
||||
* feeds the last text back in and listens for edits.
|
||||
*/
|
||||
readonly initialText = input('');
|
||||
readonly textChanged = output<string>();
|
||||
|
||||
readonly textControl = new FormControl('', { nonNullable: true });
|
||||
|
||||
private readonly textValue = toSignal(this.textControl.valueChanges, {
|
||||
initialValue: '',
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.textControl.valueChanges
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe((value) => this.textChanged.emit(value));
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const initial = this.initialText();
|
||||
if (initial) {
|
||||
this.textControl.setValue(initial);
|
||||
}
|
||||
}
|
||||
|
||||
readonly candidates = computed(() =>
|
||||
detectProviderImportCandidates(this.textValue())
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user