fix: add tags and show_tags fields to PnC builder (#1626)

This commit is contained in:
Christian Benincasa
2026-01-28 17:38:18 -05:00
committed by GitHub
parent 5a400bf8ed
commit e13ceb514a
12 changed files with 129 additions and 77 deletions

View File

@@ -3,7 +3,8 @@ import { useQuery } from '@tanstack/react-query';
import type { MediaSourceId } from '@tunarr/shared';
import { search } from '@tunarr/shared/util';
import type { FactedStringSearchField } from '@tunarr/types/schemas';
import { useMemo } from 'react';
import { isArray } from 'lodash-es';
import { useCallback, useMemo } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { useDebounceValue } from 'usehooks-ts';
import { postApiProgramsFacetsByFacetNameOptions } from '../../generated/@tanstack/react-query.gen.ts';
@@ -22,11 +23,12 @@ export function FacetStringValueSearchNode({
mediaSourceId?: MediaSourceId;
libraryId?: string;
}) {
const { control } = useFormContext<SearchForm>();
const { control, setValue, watch } = useFormContext<SearchForm>();
const [facetSearchInputValue, setFacetSearchInputValue] = useDebounceValue(
'',
500,
);
const [lastValue, op] = watch([`${formKey}.value`, `${formKey}.op`]);
const facetQuery = useQuery({
...postApiProgramsFacetsByFacetNameOptions({
@@ -50,6 +52,24 @@ export function FacetStringValueSearchNode({
: [];
}, [facetQuery.data]);
const handleValueChange = useCallback(
(newValue: string[], originalOnChange: (...args: unknown[]) => void) => {
if (
(!isArray(lastValue) || lastValue.length < 2) &&
newValue.length > 1 &&
op !== 'in' &&
op !== 'not in'
) {
setValue(`${formKey}.op`, 'in', {
shouldDirty: true,
shouldTouch: true,
});
}
originalOnChange([...newValue]);
},
[formKey, lastValue, op, setValue],
);
return (
<Controller
control={control}
@@ -64,7 +84,7 @@ export function FacetStringValueSearchNode({
multiple
filterOptions={(x) => x}
onChange={(_, newValue) => {
field.onChange([...newValue.values()]);
handleValueChange(newValue, field.onChange);
}}
filterSelectedOptions
autoComplete

View File

@@ -11,8 +11,9 @@ import {
import { search, seq } from '@tunarr/shared/util';
import type { SearchField, SearchFilterValueNode } from '@tunarr/types/schemas';
import { OperatorsByType } from '@tunarr/types/schemas';
import { find, flatten, head, isArray, isNumber, map } from 'lodash-es';
import { find, head, isArray, isNumber } from 'lodash-es';
import { useCallback } from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { SearchFieldSpec } from '../../helpers/searchBuilderConstants.ts';
import {
@@ -48,25 +49,6 @@ export function SearchValueNode(props: ValueNodeProps) {
const getFieldName = useGetFieldName(formKey);
const dayjs = useDayjs();
// useEffect(() => {
// const sub = watch((value, { name }) => {
// if (name === getFieldName('fieldSpec.value')) {
// const fieldValue = get(
// value,
// getFieldName('fieldSpec').split('.'),
// ) as SearchField;
// if (
// (fieldValue.type === 'facted_string' ||
// fieldValue.type === 'string') &&
// fieldValue.value.length > 1
// ) {
// setValue(getFieldName('fieldSpec.op'), 'in');
// }
// }
// });
// return () => sub.unsubscribe();
// }, [formKey, getFieldName, setValue, watch]);
const handleFieldChange = useCallback(
(newField: string) => {
const spec = find(
@@ -89,7 +71,7 @@ export function SearchValueNode(props: ValueNodeProps) {
value: [],
};
break;
case 'facted_string':
case 'faceted_string':
fieldSpec = {
key: spec.alias ?? spec.key,
type: spec.type,
@@ -159,7 +141,6 @@ export function SearchValueNode(props: ValueNodeProps) {
value: string,
originalOnChange: (...args: unknown[]) => void,
) => {
console.log(value);
if (selfValue.fieldSpec.type === 'numeric') {
if (spec.normalizer) {
originalOnChange(spec.normalizer(value));
@@ -190,7 +171,7 @@ export function SearchValueNode(props: ValueNodeProps) {
return;
}
if (fieldSpec.type === 'facted_string') {
if (fieldSpec.type === 'faceted_string') {
return (
<FacetStringValueSearchNode
formKey={getFieldName('fieldSpec')}
@@ -226,6 +207,41 @@ export function SearchValueNode(props: ValueNodeProps) {
}
};
const renderOperatorInput = useCallback(
(
field: ControllerRenderProps<SearchForm, `${FieldPrefix}.fieldSpec.op`>,
) => {
const multiple =
isArray(selfValue.fieldSpec.value) &&
selfValue.fieldSpec.value.length > 1;
const operators = seq.collect(
OperatorsByType[selfValue.fieldSpec.type],
(operator) => {
if (multiple && operator !== 'in' && operator !== 'not in') {
return;
}
return (
<MenuItem key={operator} value={operator}>
{getOperatorLabel(selfValue.fieldSpec.type, operator)}
</MenuItem>
);
},
);
return (
<Select
label="Operator"
{...field}
value={field.value}
onChange={(ev) => handleOpChange(ev.target.value, field.onChange)}
>
{operators}
</Select>
);
},
[handleOpChange, selfValue.fieldSpec.type, selfValue.fieldSpec.value],
);
return (
<Stack
gap={1}
@@ -274,22 +290,7 @@ export function SearchValueNode(props: ValueNodeProps) {
<Controller
control={control}
name={getFieldName('fieldSpec.op')}
render={({ field }) => (
<Select
label="Operator"
{...field}
value={field.value}
onChange={(ev) => handleOpChange(ev.target.value, field.onChange)}
>
{flatten(
map(OperatorsByType[selfValue.fieldSpec.type], (op) => (
<MenuItem key={op} value={op}>
{getOperatorLabel(selfValue.fieldSpec.type, op)}
</MenuItem>
)),
)}
</Select>
)}
render={({ field }) => renderOperatorInput(field)}
/>
</FormControl>
{renderValueInput()}

View File

@@ -849,7 +849,7 @@ export type SearchFilterInput = {
} | {
key: string;
name: string;
type: 'facted_string';
type: 'faceted_string';
op: '=' | '!=' | 'contains' | 'starts with' | 'in' | 'not in';
value: Array<string>;
} | {
@@ -1722,7 +1722,7 @@ export type SearchFilter = {
} | {
key: string;
name: string;
type: 'facted_string';
type: 'faceted_string';
op: '=' | '!=' | 'contains' | 'starts with' | 'in' | 'not in';
value: Array<string>;
} | {

View File

@@ -47,13 +47,13 @@ const SecondsField = {
} satisfies SearchFieldSpec<'numeric'>;
export const SearchFieldSpecs: NonEmptyArray<
| SearchFieldSpec<'string' | 'facted_string'>
| SearchFieldSpec<'string' | 'faceted_string'>
| SearchFieldSpec<'numeric' | 'date'>
> = [
TitleSearchFieldSpec,
{
key: 'type',
type: 'facted_string' as const,
type: 'faceted_string' as const,
name: 'Type',
uiVisible: true,
visibleForLibraryTypes: 'all',
@@ -71,14 +71,14 @@ export const SearchFieldSpecs: NonEmptyArray<
{
key: 'genres.name',
alias: 'genre',
type: 'facted_string' as const,
type: 'faceted_string' as const,
name: 'Genre',
uiVisible: true,
visibleForLibraryTypes: 'all',
},
{
key: 'rating',
type: 'facted_string' as const,
type: 'faceted_string' as const,
name: 'Content Rating',
uiVisible: true,
visibleForLibraryTypes: ['movies', 'shows'],
@@ -86,7 +86,7 @@ export const SearchFieldSpecs: NonEmptyArray<
{
key: 'actors.name',
alias: 'actor',
type: 'facted_string' as const,
type: 'faceted_string' as const,
name: 'Actors',
uiVisible: true,
visibleForLibraryTypes: 'all',
@@ -94,7 +94,7 @@ export const SearchFieldSpecs: NonEmptyArray<
{
key: 'writer.name',
alias: 'writer',
type: 'facted_string' as const,
type: 'faceted_string' as const,
name: 'Writers',
uiVisible: true,
visibleForLibraryTypes: 'all',
@@ -102,7 +102,7 @@ export const SearchFieldSpecs: NonEmptyArray<
{
key: 'director.name',
alias: 'director',
type: 'facted_string' as const,
type: 'faceted_string' as const,
name: 'Directors',
uiVisible: true,
visibleForLibraryTypes: 'all',
@@ -110,7 +110,7 @@ export const SearchFieldSpecs: NonEmptyArray<
{
key: 'studio.name',
alias: 'studio',
type: 'facted_string' as const,
type: 'faceted_string' as const,
name: 'Studios',
uiVisible: true,
visibleForLibraryTypes: 'all',
@@ -134,7 +134,7 @@ export const SearchFieldSpecs: NonEmptyArray<
{
key: 'videoCodec',
alias: 'video_codec',
type: 'facted_string' as const,
type: 'faceted_string' as const,
name: 'Video Codec',
uiVisible: true,
visibleForLibraryTypes: 'all',
@@ -166,7 +166,7 @@ export const SearchFieldSpecs: NonEmptyArray<
{
key: 'audioCodec',
alias: 'audio_codec',
type: 'facted_string' as const,
type: 'faceted_string' as const,
name: 'Audio Codec',
uiVisible: true,
visibleForLibraryTypes: 'all',
@@ -181,6 +181,23 @@ export const SearchFieldSpecs: NonEmptyArray<
},
MinutesField,
SecondsField,
{
key: 'tags',
type: 'faceted_string',
name: 'Tags',
uiVisible: true,
visibleForLibraryTypes: 'all',
},
{
key: 'grandparent.tags',
alias: 'show_tags',
type: 'faceted_string' as const,
name: 'Show Tags',
uiVisible: true,
visibleForLibraryTypes: ['shows'] as NoInfer<
ReadonlyArray<MediaSourceLibrary['mediaType']>
>,
} satisfies SearchFieldSpec<'faceted_string'>,
];
interface Bij<In, Out = In> {
@@ -243,7 +260,7 @@ const OperatorLabelByFieldType = {
'>=': 'greater than or equal',
to: 'between',
},
facted_string: {
faceted_string: {
'!=': '!=',
'=': '=',
'starts with': 'starts with',

View File

@@ -58,7 +58,7 @@ export const TrashPage = () => {
key: 'state',
name: '',
op: '=',
type: 'facted_string',
type: 'faceted_string',
value: ['missing'],
},
} satisfies SearchFilter;
@@ -79,7 +79,7 @@ export const TrashPage = () => {
name: '',
op: 'in',
value: itemTypes,
type: 'facted_string',
type: 'faceted_string',
},
},
],