1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Merge pull request #4544 from thornbill/fix-eslint-warnings-ts

This commit is contained in:
Bill Thornton 2023-04-26 23:56:33 -04:00 committed by GitHub
commit f5e292bf29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 342 additions and 364 deletions

View file

@ -1,4 +1,4 @@
import React, { FunctionComponent, useEffect, useState } from 'react'; import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import type { ConnectResponse } from 'jellyfin-apiclient'; import type { ConnectResponse } from 'jellyfin-apiclient';
@ -35,116 +35,119 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
const [ isLoading, setIsLoading ] = useState(true); const [ isLoading, setIsLoading ] = useState(true);
useEffect(() => { const bounce = useCallback(async (connectionResponse: ConnectResponse) => {
const bounce = async (connectionResponse: ConnectResponse) => { switch (connectionResponse.State) {
switch (connectionResponse.State) { case ConnectionState.SignedIn:
case ConnectionState.SignedIn: // Already logged in, bounce to the home page
// Already logged in, bounce to the home page console.debug('[ConnectionRequired] already logged in, redirecting to home');
console.debug('[ConnectionRequired] already logged in, redirecting to home'); navigate(BounceRoutes.Home);
navigate(BounceRoutes.Home); return;
return; case ConnectionState.ServerSignIn:
case ConnectionState.ServerSignIn: // Bounce to the login page
// Bounce to the login page if (location.pathname === BounceRoutes.Login) {
if (location.pathname === BounceRoutes.Login) { setIsLoading(false);
setIsLoading(false); } else {
} else { console.debug('[ConnectionRequired] not logged in, redirecting to login page');
console.debug('[ConnectionRequired] not logged in, redirecting to login page'); navigate(`${BounceRoutes.Login}?serverid=${connectionResponse.ApiClient.serverId()}`);
navigate(`${BounceRoutes.Login}?serverid=${connectionResponse.ApiClient.serverId()}`);
}
return;
case ConnectionState.ServerSelection:
// Bounce to select server page
console.debug('[ConnectionRequired] redirecting to select server page');
navigate(BounceRoutes.SelectServer);
return;
case ConnectionState.ServerUpdateNeeded:
// Show update needed message and bounce to select server page
try {
await alert({
text: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin'),
html: globalize.translate('ServerUpdateNeeded', '<a href="https://github.com/jellyfin/jellyfin">https://github.com/jellyfin/jellyfin</a>')
});
} catch (ex) {
console.warn('[ConnectionRequired] failed to show alert', ex);
}
console.debug('[ConnectionRequired] server update required, redirecting to select server page');
navigate(BounceRoutes.SelectServer);
return;
}
console.warn('[ConnectionRequired] unhandled connection state', connectionResponse.State);
};
const validateConnection = async () => {
// Check connection status on initial page load
const firstConnection = appRouter.firstConnectionResult;
appRouter.firstConnectionResult = null;
if (firstConnection && firstConnection.State !== ConnectionState.SignedIn) {
if (firstConnection.State === ConnectionState.ServerSignIn) {
// Verify the wizard is complete
try {
const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`);
if (!infoResponse.ok) {
throw new Error('Public system info request failed');
}
const systemInfo = await infoResponse.json();
if (!systemInfo?.StartupWizardCompleted) {
// Update the current ApiClient
// TODO: Is there a better place to handle this?
ServerConnections.setLocalApiClient(firstConnection.ApiClient);
// Bounce to the wizard
console.info('[ConnectionRequired] startup wizard is not complete, redirecting there');
navigate(BounceRoutes.StartWizard);
return;
}
} catch (ex) {
console.error('[ConnectionRequired] checking wizard status failed', ex);
return;
}
} }
return;
case ConnectionState.ServerSelection:
// Bounce to select server page
console.debug('[ConnectionRequired] redirecting to select server page');
navigate(BounceRoutes.SelectServer);
return;
case ConnectionState.ServerUpdateNeeded:
// Show update needed message and bounce to select server page
try {
await alert({
text: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin'),
html: globalize.translate('ServerUpdateNeeded', '<a href="https://github.com/jellyfin/jellyfin">https://github.com/jellyfin/jellyfin</a>')
});
} catch (ex) {
console.warn('[ConnectionRequired] failed to show alert', ex);
}
console.debug('[ConnectionRequired] server update required, redirecting to select server page');
navigate(BounceRoutes.SelectServer);
return;
}
// Bounce to the correct page in the login flow console.warn('[ConnectionRequired] unhandled connection state', connectionResponse.State);
bounce(firstConnection); }, [location.pathname, navigate]);
const handleIncompleteWizard = useCallback(async (firstConnection: ConnectResponse) => {
if (firstConnection.State === ConnectionState.ServerSignIn) {
// Verify the wizard is complete
try {
const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`);
if (!infoResponse.ok) {
throw new Error('Public system info request failed');
}
const systemInfo = await infoResponse.json();
if (!systemInfo?.StartupWizardCompleted) {
// Update the current ApiClient
// TODO: Is there a better place to handle this?
ServerConnections.setLocalApiClient(firstConnection.ApiClient);
// Bounce to the wizard
console.info('[ConnectionRequired] startup wizard is not complete, redirecting there');
navigate(BounceRoutes.StartWizard);
return;
}
} catch (ex) {
console.error('[ConnectionRequired] checking wizard status failed', ex);
return; return;
} }
}
// TODO: appRouter will call appHost.exit() if navigating back when you are already at the default route. // Bounce to the correct page in the login flow
// This case will need to be handled elsewhere before appRouter can be killed. bounce(firstConnection);
}, [bounce, navigate]);
const client = ServerConnections.currentApiClient(); const validateUserAccess = useCallback(async () => {
const client = ServerConnections.currentApiClient();
// If this is a user route, ensure a user is logged in // If this is a user route, ensure a user is logged in
if ((isAdminRequired || isUserRequired) && !client?.isLoggedIn()) { if ((isAdminRequired || isUserRequired) && !client?.isLoggedIn()) {
try { try {
console.warn('[ConnectionRequired] unauthenticated user attempted to access user route'); console.warn('[ConnectionRequired] unauthenticated user attempted to access user route');
bounce(await ServerConnections.connect());
} catch (ex) {
console.warn('[ConnectionRequired] error bouncing from user route', ex);
}
return;
}
// If this is an admin route, ensure the user has access
if (isAdminRequired) {
try {
const user = await client?.getCurrentUser();
if (!user?.Policy?.IsAdministrator) {
console.warn('[ConnectionRequired] normal user attempted to access admin route');
bounce(await ServerConnections.connect()); bounce(await ServerConnections.connect());
} catch (ex) {
console.warn('[ConnectionRequired] error bouncing from user route', ex);
}
return;
}
// If this is an admin route, ensure the user has access
if (isAdminRequired) {
try {
const user = await client?.getCurrentUser();
if (!user?.Policy?.IsAdministrator) {
console.warn('[ConnectionRequired] normal user attempted to access admin route');
bounce(await ServerConnections.connect());
return;
}
} catch (ex) {
console.warn('[ConnectionRequired] error bouncing from admin route', ex);
return; return;
} }
} catch (ex) {
console.warn('[ConnectionRequired] error bouncing from admin route', ex);
return;
} }
}
setIsLoading(false); setIsLoading(false);
}; }, [bounce, isAdminRequired, isUserRequired]);
validateConnection(); useEffect(() => {
}, [ isAdminRequired, isUserRequired, location.pathname, navigate ]); // TODO: appRouter will call appHost.exit() if navigating back when you are already at the default route.
// This case will need to be handled elsewhere before appRouter can be killed.
// Check connection status on initial page load
const firstConnection = appRouter.firstConnectionResult;
appRouter.firstConnectionResult = null;
if (firstConnection && firstConnection.State !== ConnectionState.SignedIn) {
handleIncompleteWizard(firstConnection);
} else {
validateUserAccess();
}
}, [handleIncompleteWizard, validateUserAccess]);
if (isLoading) { if (isLoading) {
return <Loading />; return <Loading />;

View file

@ -4,6 +4,7 @@ import { useLocation } from 'react-router-dom';
import ServerConnections from './ServerConnections'; import ServerConnections from './ServerConnections';
import viewManager from './viewManager/viewManager'; import viewManager from './viewManager/viewManager';
import globalize from '../scripts/globalize'; import globalize from '../scripts/globalize';
import type { RestoreViewFailResponse } from '../types/viewManager';
interface ServerContentPageProps { interface ServerContentPageProps {
view: string view: string
@ -29,7 +30,7 @@ const ServerContentPage: FunctionComponent<ServerContentPageProps> = ({ view })
}; };
viewManager.tryRestoreView(viewOptions) viewManager.tryRestoreView(viewOptions)
.catch(async (result?: any) => { .catch(async (result?: RestoreViewFailResponse) => {
if (!result || !result.cancelled) { if (!result || !result.cancelled) {
const apiClient = ServerConnections.currentApiClient(); const apiClient = ServerConnections.currentApiClient();
@ -46,12 +47,13 @@ const ServerContentPage: FunctionComponent<ServerContentPageProps> = ({ view })
}; };
loadPage(); loadPage();
}, [ },
// location.state is NOT included as a dependency here since dialogs will update state while the current view stays the same
// eslint-disable-next-line react-hooks/exhaustive-deps
[
view, view,
location.pathname, location.pathname,
location.search location.search
// location.state is NOT included as a dependency here since dialogs will update state while the current view
// stays the same
]); ]);
return <></>; return <></>;

View file

@ -1,4 +1,4 @@
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; import { type BaseItemDtoQueryResult, ItemFields, ItemFilter } from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import loading from '../loading/loading'; import loading from '../loading/loading';
@ -33,6 +33,41 @@ const getDefaultSortBy = () => {
return 'SortName'; return 'SortName';
}; };
const getFields = (viewQuerySettings: ViewQuerySettings) => {
const fields: ItemFields[] = [
ItemFields.BasicSyncInfo,
ItemFields.MediaSourceCount
];
if (viewQuerySettings.imageType === 'primary') {
fields.push(ItemFields.PrimaryImageAspectRatio);
}
return fields.join(',');
};
const getFilters = (viewQuerySettings: ViewQuerySettings) => {
const filters: ItemFilter[] = [];
if (viewQuerySettings.IsPlayed) {
filters.push(ItemFilter.IsPlayed);
}
if (viewQuerySettings.IsUnplayed) {
filters.push(ItemFilter.IsUnplayed);
}
if (viewQuerySettings.IsFavorite) {
filters.push(ItemFilter.IsFavorite);
}
if (viewQuerySettings.IsResumable) {
filters.push(ItemFilter.IsResumable);
}
return filters;
};
const getVisibleViewSettings = () => { const getVisibleViewSettings = () => {
return [ return [
'showTitle', 'showTitle',
@ -228,33 +263,7 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
}, [getCardOptions, getContext, itemsResult.Items, getNoItemsMessage, viewQuerySettings.imageType]); }, [getCardOptions, getContext, itemsResult.Items, getNoItemsMessage, viewQuerySettings.imageType]);
const getQuery = useCallback(() => { const getQuery = useCallback(() => {
let fields = 'BasicSyncInfo,MediaSourceCount'; const queryFilters = getFilters(viewQuerySettings);
if (viewQuerySettings.imageType === 'primary') {
fields += ',PrimaryImageAspectRatio';
}
if (viewQuerySettings.showYear) {
fields += ',ProductionYear';
}
const queryFilters: string[] = [];
if (viewQuerySettings.IsPlayed) {
queryFilters.push('IsPlayed');
}
if (viewQuerySettings.IsUnplayed) {
queryFilters.push('IsUnplayed');
}
if (viewQuerySettings.IsFavorite) {
queryFilters.push('IsFavorite');
}
if (viewQuerySettings.IsResumable) {
queryFilters.push('IsResumable');
}
let queryIsHD; let queryIsHD;
@ -271,7 +280,7 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
SortOrder: viewQuerySettings.SortOrder, SortOrder: viewQuerySettings.SortOrder,
IncludeItemTypes: getItemTypes().join(','), IncludeItemTypes: getItemTypes().join(','),
Recursive: true, Recursive: true,
Fields: fields, Fields: getFields(viewQuerySettings),
ImageTypeLimit: 1, ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo', EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo',
Limit: userSettings.libraryPageSize(undefined) || undefined, Limit: userSettings.libraryPageSize(undefined) || undefined,
@ -293,28 +302,7 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
ParentId: topParentId ParentId: topParentId
}; };
}, [ }, [
viewQuerySettings.imageType, viewQuerySettings,
viewQuerySettings.showYear,
viewQuerySettings.IsPlayed,
viewQuerySettings.IsUnplayed,
viewQuerySettings.IsFavorite,
viewQuerySettings.IsResumable,
viewQuerySettings.IsHD,
viewQuerySettings.IsSD,
viewQuerySettings.SortBy,
viewQuerySettings.SortOrder,
viewQuerySettings.VideoTypes,
viewQuerySettings.GenreIds,
viewQuerySettings.Is4K,
viewQuerySettings.Is3D,
viewQuerySettings.HasSubtitles,
viewQuerySettings.HasTrailer,
viewQuerySettings.HasSpecialFeature,
viewQuerySettings.HasThemeSong,
viewQuerySettings.HasThemeVideo,
viewQuerySettings.StartIndex,
viewQuerySettings.NameLessThan,
viewQuerySettings.NameStartsWith,
getItemTypes, getItemTypes,
getBasekey, getBasekey,
topParentId topParentId

View file

@ -1,4 +1,3 @@
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
import Dashboard from '../../../utils/dashboard'; import Dashboard from '../../../utils/dashboard';
import globalize from '../../../scripts/globalize'; import globalize from '../../../scripts/globalize';
@ -17,7 +16,7 @@ type IProps = {
const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => { const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
const loadUser = useCallback(() => { const loadUser = useCallback(async () => {
const page = element.current; const page = element.current;
if (!page) { if (!page) {
@ -25,61 +24,48 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
return; return;
} }
window.ApiClient.getUser(userId).then(function (user) { const user = await window.ApiClient.getUser(userId);
Dashboard.getCurrentUser().then(function (loggedInUser: UserDto) { const loggedInUser = await Dashboard.getCurrentUser();
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
}
if (!user.Configuration) { if (!user.Policy || !user.Configuration) {
throw new Error('Unexpected null user.Configuration'); throw new Error('Unexpected null user policy or configuration');
} }
LibraryMenu.setTitle(user.Name); LibraryMenu.setTitle(user.Name);
let showLocalAccessSection = false; let showLocalAccessSection = false;
if (user.HasConfiguredPassword) { if (user.HasConfiguredPassword) {
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide'); (page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide');
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide'); (page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide');
showLocalAccessSection = true; showLocalAccessSection = true;
} else { } else {
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide'); (page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide');
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide'); (page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide');
} }
if (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess) { const canChangePassword = loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess;
(page.querySelector('.passwordSection') as HTMLDivElement).classList.remove('hide'); (page.querySelector('.passwordSection') as HTMLDivElement).classList.toggle('hide', !canChangePassword);
} else { (page.querySelector('.localAccessSection') as HTMLDivElement).classList.toggle('hide', !(showLocalAccessSection && canChangePassword));
(page.querySelector('.passwordSection') as HTMLDivElement).classList.add('hide');
}
if (showLocalAccessSection && (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) { const txtEasyPassword = page.querySelector('#txtEasyPassword') as HTMLInputElement;
(page.querySelector('.localAccessSection') as HTMLDivElement).classList.remove('hide'); txtEasyPassword.value = '';
} else {
(page.querySelector('.localAccessSection') as HTMLDivElement).classList.add('hide');
}
const txtEasyPassword = page.querySelector('#txtEasyPassword') as HTMLInputElement; if (user.HasConfiguredEasyPassword) {
txtEasyPassword.value = ''; txtEasyPassword.placeholder = '******';
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.remove('hide');
} else {
txtEasyPassword.removeAttribute('placeholder');
txtEasyPassword.placeholder = '';
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.add('hide');
}
if (user.HasConfiguredEasyPassword) { const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement;
txtEasyPassword.placeholder = '******';
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.remove('hide');
} else {
txtEasyPassword.removeAttribute('placeholder');
txtEasyPassword.placeholder = '';
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.add('hide');
}
const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement; chkEnableLocalEasyPassword.checked = user.Configuration.EnableLocalPassword || false;
chkEnableLocalEasyPassword.checked = user.Configuration.EnableLocalPassword || false; import('../../autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
import('../../autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
});
});
}); });
(page.querySelector('#txtCurrentPassword') as HTMLInputElement).value = ''; (page.querySelector('#txtCurrentPassword') as HTMLInputElement).value = '';

View file

@ -1,7 +1,7 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; import type { BaseItemDto, BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
import type { ApiClient } from 'jellyfin-apiclient'; import type { ApiClient } from 'jellyfin-apiclient';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { FunctionComponent, useEffect, useState } from 'react'; import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import ServerConnections from '../ServerConnections'; import ServerConnections from '../ServerConnections';
@ -14,6 +14,17 @@ type SearchResultsProps = {
query?: string; query?: string;
} }
const ensureNonNullItems = (result: BaseItemDtoQueryResult) => ({
...result,
Items: result.Items || []
});
const isMovies = (collectionType: string) => collectionType === 'movies';
const isMusic = (collectionType: string) => collectionType === 'music';
const isTVShows = (collectionType: string) => collectionType === 'tvshows' || collectionType === 'tv';
/* /*
* React component to display search result rows for global search and non-live tv library search * React component to display search result rows for global search and non-live tv library search
*/ */
@ -35,55 +46,55 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
const [ people, setPeople ] = useState<BaseItemDto[]>([]); const [ people, setPeople ] = useState<BaseItemDto[]>([]);
const [ collections, setCollections ] = useState<BaseItemDto[]>([]); const [ collections, setCollections ] = useState<BaseItemDto[]>([]);
useEffect(() => { const getDefaultParameters = useCallback(() => ({
const getDefaultParameters = () => ({ ParentId: parentId,
ParentId: parentId, searchTerm: query,
searchTerm: query, Limit: 24,
Limit: 24, Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount',
Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount', Recursive: true,
Recursive: true, EnableTotalRecordCount: false,
EnableTotalRecordCount: false, ImageTypeLimit: 1,
ImageTypeLimit: 1, IncludePeople: false,
IncludePeople: false, IncludeMedia: false,
IncludeMedia: false, IncludeGenres: false,
IncludeGenres: false, IncludeStudios: false,
IncludeStudios: false, IncludeArtists: false
IncludeArtists: false }), [parentId, query]);
});
const fetchArtists = (apiClient: ApiClient, params = {}) => apiClient?.getArtists( const fetchArtists = useCallback((apiClient: ApiClient, params = {}) => (
apiClient?.getCurrentUserId(), apiClient?.getArtists(
apiClient.getCurrentUserId(),
{ {
...getDefaultParameters(), ...getDefaultParameters(),
IncludeArtists: true, IncludeArtists: true,
...params ...params
} }
); ).then(ensureNonNullItems)
), [getDefaultParameters]);
const fetchItems = (apiClient: ApiClient, params = {}) => apiClient?.getItems( const fetchItems = useCallback((apiClient: ApiClient, params = {}) => (
apiClient?.getCurrentUserId(), apiClient?.getItems(
apiClient.getCurrentUserId(),
{ {
...getDefaultParameters(), ...getDefaultParameters(),
IncludeMedia: true, IncludeMedia: true,
...params ...params
} }
); ).then(ensureNonNullItems)
), [getDefaultParameters]);
const fetchPeople = (apiClient: ApiClient, params = {}) => apiClient?.getPeople( const fetchPeople = useCallback((apiClient: ApiClient, params = {}) => (
apiClient?.getCurrentUserId(), apiClient?.getPeople(
apiClient.getCurrentUserId(),
{ {
...getDefaultParameters(), ...getDefaultParameters(),
IncludePeople: true, IncludePeople: true,
...params ...params
} }
); ).then(ensureNonNullItems)
), [getDefaultParameters]);
const isMovies = () => collectionType === 'movies';
const isMusic = () => collectionType === 'music';
const isTVShows = () => collectionType === 'tvshows' || collectionType === 'tv';
useEffect(() => {
// Reset state // Reset state
setMovies([]); setMovies([]);
setShows([]); setShows([]);
@ -102,78 +113,80 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
setPeople([]); setPeople([]);
setCollections([]); setCollections([]);
if (query) { if (!query) {
const apiClient = ServerConnections.getApiClient(serverId); return;
// Movie libraries
if (!collectionType || isMovies()) {
// Movies row
fetchItems(apiClient, { IncludeItemTypes: 'Movie' })
.then(result => setMovies(result.Items || []));
}
// TV Show libraries
if (!collectionType || isTVShows()) {
// Shows row
fetchItems(apiClient, { IncludeItemTypes: 'Series' })
.then(result => setShows(result.Items || []));
// Episodes row
fetchItems(apiClient, { IncludeItemTypes: 'Episode' })
.then(result => setEpisodes(result.Items || []));
}
// People are included for Movies and TV Shows
if (!collectionType || isMovies() || isTVShows()) {
// People row
fetchPeople(apiClient).then(result => setPeople(result.Items || []));
}
// Music libraries
if (!collectionType || isMusic()) {
// Playlists row
fetchItems(apiClient, { IncludeItemTypes: 'Playlist' })
.then(results => setPlaylists(results.Items || []));
// Artists row
fetchArtists(apiClient).then(result => setArtists(result.Items || []));
// Albums row
fetchItems(apiClient, { IncludeItemTypes: 'MusicAlbum' })
.then(result => setAlbums(result.Items || []));
// Songs row
fetchItems(apiClient, { IncludeItemTypes: 'Audio' })
.then(result => setSongs(result.Items || []));
}
// Other libraries do not support in-library search currently
if (!collectionType) {
// Videos row
fetchItems(apiClient, {
MediaTypes: 'Video',
ExcludeItemTypes: 'Movie,Episode,TvChannel'
}).then(result => setVideos(result.Items || []));
// Programs row
fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' })
.then(result => setPrograms(result.Items || []));
// Channels row
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
.then(result => setChannels(result.Items || []));
// Photo Albums row
fetchItems(apiClient, { IncludeItemTypes: 'PhotoAlbum' })
.then(results => setPhotoAlbums(results.Items || []));
// Photos row
fetchItems(apiClient, { IncludeItemTypes: 'Photo' })
.then(results => setPhotos(results.Items || []));
// Audio Books row
fetchItems(apiClient, { IncludeItemTypes: 'AudioBook' })
.then(results => setAudioBooks(results.Items || []));
// Books row
fetchItems(apiClient, { IncludeItemTypes: 'Book' })
.then(results => setBooks(results.Items || []));
// Collections row
fetchItems(apiClient, { IncludeItemTypes: 'BoxSet' })
.then(result => setCollections(result.Items || []));
}
} }
}, [collectionType, parentId, query, serverId]);
const apiClient = ServerConnections.getApiClient(serverId);
// Movie libraries
if (!collectionType || isMovies(collectionType)) {
// Movies row
fetchItems(apiClient, { IncludeItemTypes: 'Movie' })
.then(result => setMovies(result.Items));
}
// TV Show libraries
if (!collectionType || isTVShows(collectionType)) {
// Shows row
fetchItems(apiClient, { IncludeItemTypes: 'Series' })
.then(result => setShows(result.Items));
// Episodes row
fetchItems(apiClient, { IncludeItemTypes: 'Episode' })
.then(result => setEpisodes(result.Items));
}
// People are included for Movies and TV Shows
if (!collectionType || isMovies(collectionType) || isTVShows(collectionType)) {
// People row
fetchPeople(apiClient).then(result => setPeople(result.Items));
}
// Music libraries
if (!collectionType || isMusic(collectionType)) {
// Playlists row
fetchItems(apiClient, { IncludeItemTypes: 'Playlist' })
.then(results => setPlaylists(results.Items));
// Artists row
fetchArtists(apiClient).then(result => setArtists(result.Items));
// Albums row
fetchItems(apiClient, { IncludeItemTypes: 'MusicAlbum' })
.then(result => setAlbums(result.Items));
// Songs row
fetchItems(apiClient, { IncludeItemTypes: 'Audio' })
.then(result => setSongs(result.Items));
}
// Other libraries do not support in-library search currently
if (!collectionType) {
// Videos row
fetchItems(apiClient, {
MediaTypes: 'Video',
ExcludeItemTypes: 'Movie,Episode,TvChannel'
}).then(result => setVideos(result.Items));
// Programs row
fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' })
.then(result => setPrograms(result.Items));
// Channels row
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
.then(result => setChannels(result.Items));
// Photo Albums row
fetchItems(apiClient, { IncludeItemTypes: 'PhotoAlbum' })
.then(result => setPhotoAlbums(result.Items));
// Photos row
fetchItems(apiClient, { IncludeItemTypes: 'Photo' })
.then(result => setPhotos(result.Items));
// Audio Books row
fetchItems(apiClient, { IncludeItemTypes: 'AudioBook' })
.then(result => setAudioBooks(result.Items));
// Books row
fetchItems(apiClient, { IncludeItemTypes: 'Book' })
.then(result => setBooks(result.Items));
// Collections row
fetchItems(apiClient, { IncludeItemTypes: 'BoxSet' })
.then(result => setCollections(result.Items));
}
}, [collectionType, fetchArtists, fetchItems, fetchPeople, query, serverId]);
return ( return (
<div <div

View file

@ -2,6 +2,7 @@ import React, { FunctionComponent, useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import type { RestoreViewFailResponse } from '../../types/viewManager';
import viewManager from './viewManager'; import viewManager from './viewManager';
export interface ViewManagerPageProps { export interface ViewManagerPageProps {
@ -45,7 +46,7 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
}; };
viewManager.tryRestoreView(viewOptions) viewManager.tryRestoreView(viewOptions)
.catch(async (result?: any) => { .catch(async (result?: RestoreViewFailResponse) => {
if (!result || !result.cancelled) { if (!result || !result.cancelled) {
const [ controllerFactory, viewHtml ] = await Promise.all([ const [ controllerFactory, viewHtml ] = await Promise.all([
import(/* webpackChunkName: "[request]" */ `../../controllers/${controller}`), import(/* webpackChunkName: "[request]" */ `../../controllers/${controller}`),
@ -63,7 +64,10 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
}; };
loadPage(); loadPage();
}, [ },
// location.state is NOT included as a dependency here since dialogs will update state while the current view stays the same
// eslint-disable-next-line react-hooks/exhaustive-deps
[
controller, controller,
view, view,
type, type,
@ -73,8 +77,6 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
transition, transition,
location.pathname, location.pathname,
location.search location.search
// location.state is NOT included as a dependency here since dialogs will update state while the current view
// stays the same
]); ]);
return <></>; return <></>;

View file

@ -126,7 +126,7 @@ const Scroller: FC<ScrollerProps> = ({
}, [getScrollPosition, getScrollSize, getScrollWidth]); }, [getScrollPosition, getScrollSize, getScrollWidth]);
const initCenterFocus = useCallback((elem: EventTarget, scrollerInstance: scrollerFactory) => { const initCenterFocus = useCallback((elem: EventTarget, scrollerInstance: scrollerFactory) => {
dom.addEventListener(elem, 'focus', function (e: { target: any; }) { dom.addEventListener(elem, 'focus', function (e: FocusEvent) {
const focused = focusManager.focusableParent(e.target); const focused = focusManager.focusableParent(e.target);
if (focused) { if (focused) {
scrollerInstance.toCenter(focused, false); scrollerInstance.toCenter(focused, false);

View file

@ -25,6 +25,17 @@ type AuthProvider = {
Id?: string; Id?: string;
} }
const getCheckedElementDataIds = (elements: NodeListOf<Element>) => (
Array.prototype.filter.call(elements, e => e.checked)
.map(e => e.getAttribute('data-id'))
);
function onSaveComplete() {
Dashboard.navigate('userprofiles.html');
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
const UserEdit: FunctionComponent = () => { const UserEdit: FunctionComponent = () => {
const [ userName, setUserName ] = useState(''); const [ userName, setUserName ] = useState('');
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ResetProvider[]>([]); const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ResetProvider[]>([]);
@ -56,7 +67,7 @@ const UserEdit: FunctionComponent = () => {
} }
const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement; const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement;
providers.length > 1 ? fldSelectLoginProvider.classList.remove('hide') : fldSelectLoginProvider.classList.add('hide'); fldSelectLoginProvider.classList.toggle('hide', providers.length <= 1);
setAuthProviders(providers); setAuthProviders(providers);
@ -73,7 +84,7 @@ const UserEdit: FunctionComponent = () => {
} }
const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement; const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement;
providers.length > 1 ? fldSelectPasswordResetProvider.classList.remove('hide') : fldSelectPasswordResetProvider.classList.add('hide'); fldSelectPasswordResetProvider.classList.toggle('hide', providers.length <= 1);
setPasswordResetProviders(providers); setPasswordResetProviders(providers);
@ -145,7 +156,7 @@ const UserEdit: FunctionComponent = () => {
}); });
const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement; const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement;
user.Policy.IsDisabled ? disabledUserBanner.classList.remove('hide') : disabledUserBanner.classList.add('hide'); disabledUserBanner.classList.toggle('hide', !user.Policy.IsDisabled);
const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement; const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement;
txtUserName.disabled = false; txtUserName.disabled = false;
@ -198,19 +209,9 @@ const UserEdit: FunctionComponent = () => {
loadData(); loadData();
function onSaveComplete() {
Dashboard.navigate('userprofiles.html');
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
const saveUser = (user: UserDto) => { const saveUser = (user: UserDto) => {
if (!user.Id) { if (!user.Id || !user.Policy) {
throw new Error('Unexpected null user.Id'); throw new Error('Unexpected null user id or policy');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
} }
user.Name = (page.querySelector('#txtUserName') as HTMLInputElement).value; user.Name = (page.querySelector('#txtUserName') as HTMLInputElement).value;
@ -235,19 +236,15 @@ const UserEdit: FunctionComponent = () => {
user.Policy.AuthenticationProviderId = (page.querySelector('#selectLoginProvider') as HTMLSelectElement).value; user.Policy.AuthenticationProviderId = (page.querySelector('#selectLoginProvider') as HTMLSelectElement).value;
user.Policy.PasswordResetProviderId = (page.querySelector('#selectPasswordResetProvider') as HTMLSelectElement).value; user.Policy.PasswordResetProviderId = (page.querySelector('#selectPasswordResetProvider') as HTMLSelectElement).value;
user.Policy.EnableContentDeletion = (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).checked; user.Policy.EnableContentDeletion = (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).checked;
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (c) { user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : getCheckedElementDataIds(page.querySelectorAll('.chkFolder'));
return c.checked; user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType;
}).map(function (c) {
return c.getAttribute('data-id'); window.ApiClient.updateUser(user)
}); .then(() => (
if (window.ApiClient.isMinServerVersion('10.6.0')) { window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {})
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType; )).then(() => {
}
window.ApiClient.updateUser(user).then(function () {
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {}).then(function () {
onSaveComplete(); onSaveComplete();
}); });
});
}; };
const onSubmit = (e: Event) => { const onSubmit = (e: Event) => {
@ -261,16 +258,11 @@ const UserEdit: FunctionComponent = () => {
}; };
(page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) { (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
if (this.checked) { (page.querySelector('.deleteAccess') as HTMLDivElement).classList.toggle('hide', this.checked);
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.add('hide');
} else {
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.remove('hide');
}
}); });
window.ApiClient.getNamedConfiguration('network').then(function (config) { window.ApiClient.getNamedConfiguration('network').then(function (config) {
const fldRemoteAccess = page.querySelector('.fldRemoteAccess') as HTMLDivElement; (page.querySelector('.fldRemoteAccess') as HTMLDivElement).classList.toggle('hide', !config.EnableRemoteAccess);
config.EnableRemoteAccess ? fldRemoteAccess.classList.remove('hide') : fldRemoteAccess.classList.add('hide');
}); });
(page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit); (page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);

View file

@ -111,12 +111,8 @@ const UserNew: FunctionComponent = () => {
userInput.Name = (page.querySelector('#txtUsername') as HTMLInputElement).value; userInput.Name = (page.querySelector('#txtUsername') as HTMLInputElement).value;
userInput.Password = (page.querySelector('#txtPassword') as HTMLInputElement).value; userInput.Password = (page.querySelector('#txtPassword') as HTMLInputElement).value;
window.ApiClient.createUser(userInput).then(function (user) { window.ApiClient.createUser(userInput).then(function (user) {
if (!user.Id) { if (!user.Id || !user.Policy) {
throw new Error('Unexpected null user.Id'); throw new Error('Unexpected null user id or policy');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
} }
user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked; user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked;

View file

@ -215,12 +215,8 @@ const UserParentalControl: FunctionComponent = () => {
}; };
const saveUser = (user: UserDto) => { const saveUser = (user: UserDto) => {
if (!user.Id) { if (!user.Id || !user.Policy) {
throw new Error('Unexpected null user.Id'); throw new Error('Unexpected null user id or policy');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
} }
const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10); const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10);

View file

@ -30,12 +30,8 @@ const UserProfile: FunctionComponent = () => {
loading.show(); loading.show();
window.ApiClient.getUser(userId).then(function (user) { window.ApiClient.getUser(userId).then(function (user) {
if (!user.Name) { if (!user.Name || !user.Id) {
throw new Error('Unexpected null user.Name'); throw new Error('Unexpected null user name or id');
}
if (!user.Id) {
throw new Error('Unexpected null user.Id');
} }
setUserName(user.Name); setUserName(user.Name);

3
src/types/viewManager.ts Normal file
View file

@ -0,0 +1,3 @@
export interface RestoreViewFailResponse {
cancelled?: boolean
}

View file

@ -31,25 +31,26 @@ function getItemsSplit(apiClient: ApiClient, userId: string, options: GetItemsRe
function mergeResults(results: BaseItemDtoQueryResult[]) { function mergeResults(results: BaseItemDtoQueryResult[]) {
const merged: BaseItemDtoQueryResult = { const merged: BaseItemDtoQueryResult = {
Items: [], Items: [],
TotalRecordCount: 0,
StartIndex: 0 StartIndex: 0
}; };
// set TotalRecordCount separately so TS knows it is defined
merged.TotalRecordCount = 0;
for (const result of results) { for (const result of results) {
if (result.Items == null) { if (!result.Items) {
console.log('[getItems] Retrieved Items array is invalid', result.Items); console.log('[getItems] Retrieved Items array is invalid', result.Items);
continue; continue;
} }
if (result.TotalRecordCount == null) { if (!result.TotalRecordCount) {
console.log('[getItems] Retrieved TotalRecordCount is invalid', result.TotalRecordCount); console.log('[getItems] Retrieved TotalRecordCount is invalid', result.TotalRecordCount);
continue; continue;
} }
if (result.StartIndex == null) { if (typeof result.StartIndex === 'undefined') {
console.log('[getItems] Retrieved StartIndex is invalid', result.StartIndex); console.log('[getItems] Retrieved StartIndex is invalid', result.StartIndex);
continue; continue;
} }
merged.Items = merged.Items?.concat(result.Items); merged.Items = merged.Items?.concat(result.Items);
merged.TotalRecordCount! += result.TotalRecordCount; merged.TotalRecordCount += result.TotalRecordCount;
merged.StartIndex = Math.min(merged.StartIndex || 0, result.StartIndex); merged.StartIndex = Math.min(merged.StartIndex || 0, result.StartIndex);
} }
return merged; return merged;