fix(search): address global search review feedback

This commit is contained in:
4gray committed 2026-06-20 20:33:16 +02:00
1 parent 12d931f4cc
commit 357d1de050
11 files changed
+216 -44

No files matched your search

@@ -0,0 +1,22 @@
import { PlaylistMeta } from '@iptvnator/shared/interfaces';
type SearchablePlaylistMeta = Pick<
PlaylistMeta,
'count' | 'filePath' | 'macAddress' | 'serverUrl' | 'url'
>;
export function isWorkspaceGlobalSearchablePlaylist(
playlist: SearchablePlaylistMeta
): boolean {
if (playlist.serverUrl) {
return true;
}
if (playlist.macAddress) {
return false;
}
return Boolean(
playlist.filePath || playlist.url || Number(playlist.count ?? 0) > 0
);
}
@@ -1,4 +1,10 @@
import { computed, DestroyRef, inject, Injectable, signal } from '@angular/core';
import {
computed,
DestroyRef,
inject,
Injectable,
signal,
} from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { NavigationEnd, Router } from '@angular/router';
import { Store } from '@ngrx/store';
@@ -84,12 +90,16 @@ export class WorkspaceShellRouteStateService {
path: ['/workspace/sources'],
});
links.push({
icon: 'search',
tooltip: this.translateText('WORKSPACE.SHELL.RAIL_GLOBAL_SEARCH'),
path: ['/workspace/search'],
exact: true,
});
if (this.runtime.isElectron) {
links.push({
icon: 'search',
tooltip: this.translateText(
'WORKSPACE.SHELL.RAIL_GLOBAL_SEARCH'
),
path: ['/workspace/search'],
exact: true,
});
}
links.push({
icon: 'favorite',
@@ -5,6 +5,7 @@ import { TranslateService } from '@ngx-translate/core';
import { startWith } from 'rxjs';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import { RuntimeCapabilitiesService } from '@iptvnator/services';
import { WorkspaceSearchCapability } from '@iptvnator/workspace/shell/util';
import {
SEARCH_LOADED_ONLY_STATUS,
@@ -14,6 +15,7 @@ import {
resolveSearchPlaceholderKey,
resolveSearchScopeLabel,
} from './helpers/workspace-shell-search-labels';
import { isWorkspaceGlobalSearchablePlaylist } from './helpers/workspace-shell-searchable-playlists';
import { WorkspaceShellRouteStateService } from './workspace-shell-route-state.service';
import { WorkspaceShellSearchSyncService } from './workspace-shell-search-sync.service';
@@ -23,6 +25,7 @@ export class WorkspaceShellSearchService {
private readonly xtreamStore = inject(XtreamStore);
private readonly stalkerStore = inject(StalkerStore);
private readonly translate = inject(TranslateService);
private readonly runtime = inject(RuntimeCapabilitiesService);
private readonly routeState = inject(WorkspaceShellRouteStateService);
private readonly searchSync = inject(WorkspaceShellSearchSyncService);
@@ -83,25 +86,18 @@ export class WorkspaceShellSearchService {
}
if (route.kind === 'global-search') {
const hasSearchablePlaylists = this.routeState.playlists().some(
(playlist) =>
!!playlist.serverUrl ||
(!playlist.macAddress &&
(!!playlist.filePath ||
!!playlist.url ||
!playlist.serverUrl))
);
const hasSearchablePlaylists =
this.runtime.isElectron &&
this.routeState
.playlists()
.some(isWorkspaceGlobalSearchablePlaylist);
return {
enabled: hasSearchablePlaylists,
behavior: hasSearchablePlaylists
? 'remote-search'
: 'disabled',
behavior: hasSearchablePlaylists ? 'remote-search' : 'disabled',
context: null,
section: null,
searchMode: hasSearchablePlaylists
? 'remote-search'
: 'none',
searchMode: hasSearchablePlaylists ? 'remote-search' : 'none',
placeholderKey: 'WORKSPACE.SHELL.SEARCH_GLOBAL_PLACEHOLDER',
scopeLabel: this.translateText(
'WORKSPACE.SHELL.RAIL_GLOBAL_SEARCH'
@@ -675,6 +675,17 @@ describe('WorkspaceShellFacade', () => {
expect(facade.brandLink()).toBe('/workspace/sources');
});
it('hides the Electron-only global search rail link in the web runtime', () => {
runtime.isElectron = false;
showDashboardSignal.set(false);
expect(facade.workspaceLinks().map((link) => link.path)).toEqual([
['/workspace/sources'],
['/workspace/global-favorites'],
['/workspace/global-recent'],
]);
});
it('persists the last restorable route from navigation events', () => {
expect(
startupPreferences.persistLastRestorablePath
@@ -731,9 +742,42 @@ describe('WorkspaceShellFacade', () => {
]);
facade.currentUrl.set('/workspace/dashboard');
expect(facade.commandPaletteCommands().map((command) => command.id)).toContain(
'global-search'
);
expect(
facade.commandPaletteCommands().map((command) => command.id)
).toContain('global-search');
});
it('does not enable global search for empty invalid playlist metadata', () => {
activePlaylistSignal.set(null);
playlistsSignal.set([
{
_id: 'empty-playlist',
title: 'Empty playlist',
} as PlaylistSignalMeta,
]);
facade.currentUrl.set('/workspace/search');
expect(facade.canUseSearch()).toBe(false);
expect(
facade.commandPaletteCommands().map((command) => command.id)
).not.toContain('global-search');
});
it('hides the global search command and header capability in the web runtime', () => {
runtime.isElectron = false;
playlistsSignal.set([
{
_id: 'pl-m3u',
title: 'Playlist M3U',
count: 10,
} as PlaylistSignalMeta,
]);
facade.currentUrl.set('/workspace/search');
expect(facade.canUseSearch()).toBe(false);
expect(
facade.commandPaletteCommands().map((command) => command.id)
).not.toContain('global-search');
});
it('hides the downloads command when downloads are unsupported', () => {
@@ -17,6 +17,7 @@ import {
CommandBuilderActions,
CommandBuilderContext,
} from './helpers/workspace-shell-command-builders';
import { isWorkspaceGlobalSearchablePlaylist } from './helpers/workspace-shell-searchable-playlists';
import { WorkspaceShellCommandPaletteService } from './workspace-shell-command-palette.service';
import { WorkspaceShellHeaderService } from './workspace-shell-header.service';
import { WorkspaceShellRouteStateService } from './workspace-shell-route-state.service';
@@ -219,14 +220,9 @@ export class WorkspaceShellFacade {
context: this.currentContext(),
section: this.currentSection(),
hasActivePlaylist: !!this.activePlaylist(),
hasSearchablePlaylists: this.playlists().some(
(playlist) =>
!!playlist.serverUrl ||
(!playlist.macAddress &&
(!!playlist.filePath ||
!!playlist.url ||
!playlist.serverUrl))
),
hasSearchablePlaylists:
this.isElectron &&
this.playlists().some(isWorkspaceGlobalSearchablePlaylist),
canRefreshPlaylist: this.canRefreshPlaylist(),
supportsDownloads: this.supportsDownloads,
showDashboard: this.showDashboard(),