fix: fix selecting items from local libraries when editing channels (#1592)

This commit is contained in:
Christian Benincasa
2026-01-12 14:35:39 -05:00
committed by GitHub
parent 74ed6f76c8
commit b48e54b65c
14 changed files with 91 additions and 41 deletions

File diff suppressed because one or more lines are too long

View File

@@ -217,7 +217,8 @@ export const programmingApi: RouterPluginAsyncCallback = async (fastify) => {
}),
querystring: z.object({
facetQuery: z.string().optional(),
libraryId: z.string().uuid().optional(),
mediaSourceId: z.uuid().optional(),
libraryId: z.uuid().optional(),
}),
body: z.object({
filter: SearchFilterQuerySchema.optional(),
@@ -237,6 +238,7 @@ export const programmingApi: RouterPluginAsyncCallback = async (fastify) => {
facetName: req.params.facetName,
libraryId: req.query.libraryId,
filter: req.body.filter,
mediaSourceId: req.query.mediaSourceId,
},
);

View File

@@ -51,7 +51,8 @@ export const ImportedLibrarySelector = ({ initialLibraryId }: Props) => {
const { data: libraries } = useMediaSourceLibraries(
selectedServer?.id ?? '',
{
enabled: isNonEmptyString(selectedServer?.id),
enabled:
isNonEmptyString(selectedServer?.id) && selectedServer.type !== 'local',
},
);

View File

@@ -13,7 +13,9 @@ import {
ToggleButtonGroup,
Typography,
} from '@mui/material';
import type { MediaSourceId } from '@tunarr/shared';
import { isNonEmptyString } from '@tunarr/shared/util';
import { tag } from '@tunarr/types';
import type { SearchRequest } from '@tunarr/types/api';
import {
capitalize,
@@ -154,7 +156,13 @@ export const ProgrammingSelector = ({
if (selectedServer?.type === 'local') {
return (
<Box sx={{ mt: 2 }}>
<SearchInput />
<SearchInput mediaSourceId={tag<MediaSourceId>(selectedServer.id)} />
<SelectedProgrammingActions
toggleOrSetSelectedProgramsDrawer={
toggleOrSetSelectedProgramsDrawer
}
/>
<LibraryProgramGrid mediaSource={selectedServer} />
</Box>
);
}
@@ -197,10 +205,8 @@ export const ProgrammingSelector = ({
return (
<Stack gap={2}>
<SearchInput
library={{
...selectedLibrary.view,
mediaSource: selectedServer!,
}}
mediaSourceId={tag<MediaSourceId>(selectedServer!.id)}
libraryId={selectedLibrary.view.id}
/>
<SelectedProgrammingActions
toggleOrSetSelectedProgramsDrawer={

View File

@@ -72,7 +72,9 @@ const ProgramGridItemInner = <T extends ProgramOrFolder>(
},
aspectRatio: isMusicItem(item)
? 'square'
: isEpisode(item)
: isEpisode(item) ||
item.type === 'other_video' ||
item.type === 'music_video'
? 'landscape'
: 'portrait',
isPlaylist: item.type === 'playlist',

View File

@@ -1,17 +1,15 @@
import { Stack } from '@mui/material';
import { DatePicker } from '@mui/x-date-pickers';
import type { MediaSourceLibrary } from '@tunarr/types';
import { type DateSearchField } from '@tunarr/types/api';
import dayjs from 'dayjs';
import { isNumber } from 'lodash-es';
import { Controller, useFormContext } from 'react-hook-form';
import type { FieldKey, FieldPrefix } from '../../types/SearchBuilder.ts';
import { SearchForm } from './SearchInput.tsx';
import type { SearchForm } from './SearchInput.tsx';
type Props = {
field: DateSearchField;
formKey: FieldKey<FieldPrefix, 'fieldSpec'>;
library?: MediaSourceLibrary;
};
export function DateSearchValueNode({ field, formKey }: Props) {

View File

@@ -1,7 +1,7 @@
import { Autocomplete, CircularProgress, TextField } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import type { MediaSourceId } from '@tunarr/shared';
import { search } from '@tunarr/shared/util';
import type { MediaSourceLibrary } from '@tunarr/types';
import type { FactedStringSearchField } from '@tunarr/types/api';
import { useMemo } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
@@ -9,16 +9,18 @@ import { useDebounceValue } from 'usehooks-ts';
import { postApiProgramsFacetsByFacetNameOptions } from '../../generated/@tanstack/react-query.gen.ts';
import { isNonEmptyString } from '../../helpers/util.ts';
import type { FieldKey, FieldPrefix } from '../../types/SearchBuilder.ts';
import { SearchForm } from './SearchInput.tsx';
import type { SearchForm } from './SearchInput.tsx';
export function FacetStringValueSearchNode({
formKey,
library,
mediaSourceId,
libraryId,
field,
}: {
field: FactedStringSearchField;
formKey: FieldKey<FieldPrefix, 'fieldSpec'>;
library?: MediaSourceLibrary;
mediaSourceId?: MediaSourceId;
libraryId?: string;
}) {
const { control } = useFormContext<SearchForm>();
const [facetSearchInputValue, setFacetSearchInputValue] = useDebounceValue(
@@ -32,7 +34,8 @@ export function FacetStringValueSearchNode({
facetName: search.virtualFieldToIndexField[field.key] ?? field.key,
},
query: {
libraryId: library?.id,
mediaSourceId,
libraryId,
facetQuery: isNonEmptyString(facetSearchInputValue)
? facetSearchInputValue
: undefined,
@@ -45,7 +48,7 @@ export function FacetStringValueSearchNode({
return facetQuery.data?.facetValues
? Object.keys(facetQuery.data.facetValues)
: [];
}, [facetQuery.data?.facetValues]);
}, [facetQuery.data]);
return (
<Controller

View File

@@ -1,5 +1,5 @@
import { Stack, TextField } from '@mui/material';
import type { MediaSourceLibrary } from '@tunarr/types';
import type { MediaSourceId } from '@tunarr/shared';
import { FormProvider, useFormContext } from 'react-hook-form';
import { searchFilterToString } from '../../../../shared/dist/src/util/searchUtil';
import { SearchGroupNode } from './SearchGroupNode.tsx';
@@ -7,10 +7,14 @@ import type { SearchForm } from './SearchInput.tsx';
import { SearchInputToggle } from './SearchInputToggle.tsx';
type Props = {
library?: MediaSourceLibrary;
mediaSourceId?: MediaSourceId;
libraryId?: string;
};
export const PointAndClickSearchBuilder = ({ library }: Props) => {
export const PointAndClickSearchBuilder = ({
mediaSourceId,
libraryId,
}: Props) => {
const form = useFormContext<SearchForm>();
const filter = form.watch('filter');
@@ -36,7 +40,8 @@ export const PointAndClickSearchBuilder = ({ library }: Props) => {
formKey="filter.filter"
index={0}
remove={() => {}}
library={library}
mediaSourceId={mediaSourceId}
libraryId={libraryId}
/>
</FormProvider>
);

View File

@@ -9,7 +9,7 @@ import {
TextField,
Tooltip,
} from '@mui/material';
import type { MediaSourceLibrary } from '@tunarr/types';
import type { MediaSourceId } from '@tunarr/shared';
import type { SearchRequest } from '@tunarr/types/api';
import { useToggle } from '@uidotdev/usehooks';
import { isEmpty, isNil } from 'lodash-es';
@@ -25,10 +25,14 @@ import { SearchInputToggle } from './SearchInputToggle.tsx';
type SearchBuilderProps = {
onSearch: (query: SearchRequest) => void;
library?: MediaSourceLibrary;
mediaSourceId?: MediaSourceId;
libraryId?: string;
};
export function SearchFilterBuilder({ library }: SearchBuilderProps) {
export function SearchFilterBuilder({
libraryId,
mediaSourceId,
}: SearchBuilderProps) {
const [smartCollectionModalOpen, toggleSmartCollectionModal] =
useToggle(false);
@@ -147,7 +151,10 @@ export function SearchFilterBuilder({ library }: SearchBuilderProps) {
/>
</Stack>
) : (
<PointAndClickSearchBuilder library={library} />
<PointAndClickSearchBuilder
mediaSourceId={mediaSourceId}
libraryId={libraryId}
/>
)}
</Stack>
<CreateSmartCollectionDialog

View File

@@ -11,6 +11,7 @@ import {
Stack,
Tooltip,
} from '@mui/material';
import type { MediaSourceId } from '@tunarr/shared';
import { isNonEmptyString } from '@tunarr/shared/util';
import type { MediaSourceLibrary } from '@tunarr/types';
import type { SearchFilter } from '@tunarr/types/api';
@@ -21,7 +22,9 @@ import { useGetFieldName } from '../../hooks/searchBuilderHooks.ts';
import { SearchValueNode } from './SearchValueNode.tsx';
export type GroupNodeProps = {
library?: MediaSourceLibrary;
mediaSourceId?: MediaSourceId;
libraryId?: string;
mediaTypeFilter?: MediaSourceLibrary['mediaType'];
index: number;
depth: number;
formKey: FieldPrefix;
@@ -33,7 +36,9 @@ export function SearchGroupNode({
formKey,
remove: removeSelf,
index,
library,
libraryId,
mediaSourceId,
mediaTypeFilter,
}: GroupNodeProps) {
const { control } = useFormContext();
const prefix = isNonEmptyString(formKey)
@@ -120,7 +125,9 @@ export function SearchGroupNode({
index={index}
only={fields.length === 1}
remove={removeChild}
library={library}
libraryId={libraryId}
mediaSourceId={mediaSourceId}
mediaTypeFilter={mediaTypeFilter}
/>
) : (
<SearchGroupNode
@@ -129,7 +136,9 @@ export function SearchGroupNode({
formKey={`${prefix}children.${index}` as FieldPrefix}
remove={removeChild}
index={index}
library={library}
libraryId={libraryId}
mediaSourceId={mediaSourceId}
mediaTypeFilter={mediaTypeFilter}
/>
),
)}

View File

@@ -8,8 +8,8 @@ import {
Tooltip,
} from '@mui/material';
import { useMatches } from '@tanstack/react-router';
import type { MediaSourceId } from '@tunarr/shared';
import { isNonEmptyString, search as tunarrSearch } from '@tunarr/shared/util';
import type { MediaSourceLibrary } from '@tunarr/types';
import type { SearchFilter, SearchRequest } from '@tunarr/types/api';
import { difference, isEmpty, last } from 'lodash-es';
import { useCallback, useState } from 'react';
@@ -26,7 +26,8 @@ import {
import { SearchFilterBuilder } from './SearchFilterBuilder.tsx';
type Props = {
library?: MediaSourceLibrary;
mediaSourceId?: MediaSourceId;
libraryId?: string;
initialSearchFilter?: SearchFilter;
};
@@ -48,7 +49,11 @@ export type SearchForm = {
queryBuilderType: QueryBuilderType;
};
export const SearchInput = ({ library, initialSearchFilter }: Props) => {
export const SearchInput = ({
libraryId,
initialSearchFilter,
mediaSourceId,
}: Props) => {
const routeMatch = useMatches();
const formMethods = useForm<SearchForm>({
defaultValues: {
@@ -198,7 +203,8 @@ export const SearchInput = ({ library, initialSearchFilter }: Props) => {
)}
/>
<SearchFilterBuilder
library={library}
libraryId={libraryId}
mediaSourceId={mediaSourceId}
onSearch={handleSearchChange}
/>
<Box sx={{ width: '100%' }}>

View File

@@ -14,8 +14,8 @@ import { OperatorsByType } from '@tunarr/types/api';
import { find, flatten, isArray, isNumber, map } from 'lodash-es';
import { useCallback } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import type { SearchFieldSpec } from '../../helpers/searchBuilderConstants.ts';
import {
SearchFieldSpec,
SearchFieldSpecs,
getOperatorLabel,
} from '../../helpers/searchBuilderConstants.ts';
@@ -33,7 +33,16 @@ type ValueNodeProps = GroupNodeProps & {
};
export function SearchValueNode(props: ValueNodeProps) {
const { library, depth, index, formKey, only, remove } = props;
const {
libraryId,
mediaSourceId,
mediaTypeFilter,
depth,
index,
formKey,
only,
remove,
} = props;
const { control, watch, setValue } = useFormContext<SearchForm>();
const selfValue = watch(formKey) as SearchFilterValueNode;
const getFieldName = useGetFieldName(formKey);
@@ -113,7 +122,7 @@ export function SearchValueNode(props: ValueNodeProps) {
shouldDirty: true,
});
},
[getFieldName, setValue],
[dayjs, getFieldName, setValue],
);
const handleOpChange = useCallback(
@@ -185,7 +194,8 @@ export function SearchValueNode(props: ValueNodeProps) {
return (
<FacetStringValueSearchNode
formKey={getFieldName('fieldSpec')}
library={library}
libraryId={libraryId}
mediaSourceId={mediaSourceId}
field={fieldSpec}
/>
);
@@ -193,7 +203,6 @@ export function SearchValueNode(props: ValueNodeProps) {
return (
<DateSearchValueNode
formKey={getFieldName('fieldSpec')}
library={library}
field={fieldSpec}
/>
);
@@ -237,9 +246,9 @@ export function SearchValueNode(props: ValueNodeProps) {
>
{seq.collect(SearchFieldSpecs, (spec) => {
if (
library &&
mediaTypeFilter &&
isArray(spec.visibleForLibraryTypes) &&
!spec.visibleForLibraryTypes.includes(library.mediaType)
!spec.visibleForLibraryTypes.includes(mediaTypeFilter)
) {
return null;
}

View File

@@ -12460,6 +12460,7 @@ export type PostApiProgramsFacetsByFacetNameData = {
};
query?: {
facetQuery?: string;
mediaSourceId?: string;
libraryId?: string;
};
url: '/api/programs/facets/{facetName}';

View File

@@ -36,7 +36,7 @@ function MediaSourceBrowserPage() {
</Typography>
</Box>
<Stack gap={2}>
<SearchInput library={library} />
<SearchInput libraryId={libraryId} />
<LibraryProgramGrid
mediaSource={library.mediaSource}
library={library}