diff --git a/src/backend/common/database/drizzle/repositories/PlayRepository.ts b/src/backend/common/database/drizzle/repositories/PlayRepository.ts index 09e4ff0a..138ba1fa 100644 --- a/src/backend/common/database/drizzle/repositories/PlayRepository.ts +++ b/src/backend/common/database/drizzle/repositories/PlayRepository.ts @@ -3,9 +3,9 @@ import { DbConcrete, runTransaction } from "../drizzleUtils.js"; import clone from 'clone'; import { Traverse, TraverseContext } from 'neotraverse/modern'; import { loggerNoop } from "../../../MaybeLogger.js"; -import { DateLike, DeepReplaceValue, ErrorLike, PlayObject, PlayState, REGEX_ISO8601_LOOSE, TA_CLOSE, TA_DEFAULT_ACCURACY, TA_EXACT, TemporalAccuracy } from "../../../../../core/Atomic.js"; +import { DateLike, DeepReplaceValue, ErrorLike, PlayObject, PlayState, QueueName, REGEX_ISO8601_LOOSE, TA_CLOSE, TA_DEFAULT_ACCURACY, TA_EXACT, TemporalAccuracy } from "../../../../../core/Atomic.js"; import { generateInputEntity, generatePlayEntity, PlayEntityOpts, hydratePlaySelect, PlayHydateOptions } from "../entityUtils.js"; -import { playInputs, plays, queueStates, relations } from "../schema/schema.js"; +import { playInputs, plays, queueStates, relations, schema } from "../schema/schema.js"; import { PlayNew, PlaySelect, PlayInputNew, FindWhere, FindMany, QueueStateSelect, FindWith, PlaySelectWithQueueStates, WhereClause, PlayWith } from "../drizzleTypes.js";; import { MarkOptional, MarkRequired, PathValue } from "ts-essentials"; import { genGroupIdStrFromPlay, removeEmptyArrays, removeUndefinedKeys } from "../../../../utils.js"; @@ -23,7 +23,7 @@ import { SourceType } from "../../../infrastructure/config/source/sources.js"; // https://github.com/drizzle-team/drizzle-orm/issues/695 may be useful for typing models with relations? export interface QueueCriteria { - queueName: string + queueName: QueueName queueStatus: QueueStateSelect['queueStatus'][] | QueueStateSelect['queueStatus'] } @@ -252,8 +252,9 @@ export class DrizzlePlayRepository extends DrizzleBaseRepository<'plays'> { const where = buildPlayWhere({componentId: args.componentId ?? this.componentId, ...rest}); - // @ts-expect-error - const filter = relationsFilterToSQL(plays, where); + + // https://github.com/drizzle-team/drizzle-orm/issues/5218#issuecomment-3854900241 + const filter = relationsFilterToSQL(plays, where, relations.plays.relations, relations); // https://github.com/drizzle-team/drizzle-orm/discussions/3119#discussioncomment-16379557 const count = await this.db.$count(plays, filter); diff --git a/src/backend/common/database/drizzle/schema/schema.ts b/src/backend/common/database/drizzle/schema/schema.ts index 1d3c7a21..69a1f4ce 100644 --- a/src/backend/common/database/drizzle/schema/schema.ts +++ b/src/backend/common/database/drizzle/schema/schema.ts @@ -290,7 +290,7 @@ export const getConfigByTableName = (name: T) => { } } -const schema = {playInputs, plays, components, componentMigrations, queueStates, jobs}; +export const schema = {playInputs, plays, components, componentMigrations, queueStates, jobs}; export type TSchema = typeof relations; export type Schema = typeof schema; diff --git a/src/client/components/Badges.tsx b/src/client/components/Badges.tsx index 8384a98a..0b65f720 100644 --- a/src/client/components/Badges.tsx +++ b/src/client/components/Badges.tsx @@ -1,17 +1,17 @@ import { Badge, Separator, HStack } from "@chakra-ui/react"; -import React, { ComponentProps, useState, useCallback, useEffect } from "react"; +import React, { ComponentProps, useState, useCallback, useEffect, PropsWithChildren } from "react"; import { COMPONENT_STATE, ComponentCommonApiJson, componentStateToFriendly, MsSseEvent, PlayApiCommon } from "../../core/Api"; -import { capitalize } from "../../core/StringUtils"; +import { capitalizeWords } from "../../core/StringUtils"; import {useSSEContext, useSSEEvent} from "@flamefrontend/sse-runtime-react"; import { Second } from "../../core/Atomic"; import { useTimeout } from 'react-use-timeout'; -export const PlayStateBadge = (props: ComponentProps & { state: PlayApiCommon['state'], suffix?: React.JSX.Element, hasDeadQueue?: boolean }) => { +export const PlayStateBadge = (props: PropsWithChildren> & { state: PlayApiCommon['state'], suffix?: React.JSX.Element, hasDeadQueue?: boolean }) => { - const { state, suffix, ...rest } = props; + const { state, suffix, children, ...rest } = props; let badgeColor = undefined; - let badgeText = capitalize(state); + let badgeText = capitalizeWords(state); switch (state) { case 'queued': @@ -22,6 +22,7 @@ export const PlayStateBadge = (props: ComponentProps & { state: Pl badgeColor = 'green'; break; case 'failed': + case ('dead queued' as PlayApiCommon['state']): badgeColor = 'red'; if(props.hasDeadQueue) { badgeText = 'Dead Queued'; @@ -35,7 +36,7 @@ export const PlayStateBadge = (props: ComponentProps & { state: Pl break; } - return {badgeText}{suffix} + return {children ?? badgeText}{suffix} } const DEFAULT_EXPIRES = 10000; diff --git a/src/client/components/playActivity/ActivityList.tsx b/src/client/components/playActivity/ActivityList.tsx index 4500a57c..0dcc7b77 100644 --- a/src/client/components/playActivity/ActivityList.tsx +++ b/src/client/components/playActivity/ActivityList.tsx @@ -121,13 +121,15 @@ export const ListContainerFetchable = (props: { componentId: number, componentTy if ('componentId' in (payload.data as object) && (payload.data as Record).componentId === props.componentId) { switch (payload.type) { case 'playInsert': - const componentData = payload.data as MsSseEventPayload; + { + const componentData = payload.data as MsSseEventPayload; console.debug(`[Insert Check ${componentData.data.uid}] Recieved playInsert for Component ${componentId}, checking if Play can be inserted...`); if(playInWindow(componentData.data, query)) { queryClient.setQueryData(tanQueries.activities.list(componentId, query).queryKey, (old: InfiniteData, unknown>) => { return insertInfinitePlay(componentData.data, old); }); - } + } + } } } }); @@ -189,7 +191,7 @@ const insertInfinitePlay = (data: PlayApiCommonDetailed, queryData: InfiniteData data: [...playData.slice(0, beforeIndex), { ...data, - // @ts-expect-error + // @ts-expect-error only used for inserts in this context isNew: true } , ...playData.slice(beforeIndex)] diff --git a/src/client/components/playActivity/ListFilters.tsx b/src/client/components/playActivity/ListFilters.tsx index 8479a5c2..ac2241d6 100644 --- a/src/client/components/playActivity/ListFilters.tsx +++ b/src/client/components/playActivity/ListFilters.tsx @@ -4,8 +4,6 @@ import React, { ComponentProps, Fragment, useMemo, useCallback, useState } from import dayjs, { Dayjs } from 'dayjs'; import doy from 'dayjs/plugin/dayOfYear.js'; import "./PlayList.scss"; -import { ToggleButtonVariant } from '../ToggleButton.js'; -import { capitalize } from '../../../core/StringUtils.js'; import { PlayStateBadge } from '../Badges.js'; import { DateFormatter, @@ -25,9 +23,10 @@ import { QueryPlaysOptsJson } from '../../../backend/common/database/drizzle/rep import { cardHeaderSeparator } from '../../utils/ComponentUtils.js'; import { CompareDateBetween } from '../../../backend/common/database/drizzle/repositories/BaseRepository.js'; import { CalendarButton, RefreshButton } from '../icons/ChakraIcons.js'; -import { nanoid } from 'nanoid'; import { QueryPlaysOptsJsonRefreshable, tanQueries, useQueryWatcher } from '../../queries/index.js'; import { useQueryClient } from '@tanstack/react-query'; +import { PlayStateUI } from '../../../core/Api.js'; +import { capitalizeWords } from '../../../core/StringUtils.js'; const noop = (_) => null; @@ -45,16 +44,16 @@ const SelectValue = () => { interface PlayStateFilterProps { mode: ComponentType - onChange?: (states: PlayState[]) => void + onChange?: (states: PlayStateUI[]) => void } -export const PlayStateFilter = (props: PlayStateFilterProps & {value?: PlayState[] | undefined}) => { +export const PlayStateFilter = (props: PlayStateFilterProps & {value?: PlayStateUI[] | undefined}) => { const { mode, onChange = noop, value } = props; - const availableStates = isComponentTypeSource(mode) ? PLAY_SOURCE_STATE : PLAY_CLIENT_STATE; - const selectOptions = createListCollection({ items: availableStates.map(x => ({ label: capitalize(x), value: x })) }); + const availableStates = ['dead queued', ...(isComponentTypeSource(mode) ? PLAY_SOURCE_STATE : PLAY_CLIENT_STATE)]; + const selectOptions = createListCollection({ items: availableStates.map(x => ({ label: capitalizeWords(x), value: x })) }); //const [enabledStates, setEnabledStates] = useState([]); // maxW="420px" return ( @@ -273,7 +272,7 @@ export const ListFilters = (props: { const queryClient = useQueryClient(); - const setState = useCallback((val: PlayState[]) => { + const setState = useCallback((val: PlayStateUI[]) => { const { state, ...rest @@ -283,7 +282,7 @@ export const ListFilters = (props: { ...rest, state: val }); - }, [onchange, filters]); + }, [onChange, filters]); const setDateRange = useCallback((val: [string, string]) => { const { playedAt, diff --git a/src/client/queries/index.ts b/src/client/queries/index.ts index 431a0dab..0e5e061f 100644 --- a/src/client/queries/index.ts +++ b/src/client/queries/index.ts @@ -6,10 +6,10 @@ import { QueryPlaysOpts, QueryPlaysOptsJson } from "../../backend/common/databas import qs from 'qs'; import { baseUrl } from "../utils"; import { PaginatedResponse } from "../../backend/common/database/drizzle/repositories/BaseRepository"; -import { ComponentsApiJson, PlayApiCommonDetailed } from "../../core/Api"; -import { SourcePlayerJson } from "../../core/Atomic"; +import { ComponentsApiJson, PlayApiCommonDetailed, PlayStateUI } from "../../core/Api"; +import { CLIENT_DEAD_QUEUE, CLIENT_INGRESS_QUEUE, isPlayState, SourcePlayerJson } from "../../core/Atomic"; -export type QueryPlaysOptsJsonRefreshable = QueryPlaysOptsJson & {nonce?: string}; +export type QueryPlaysOptsJsonRefreshable = Omit & {nonce?: string, state?: PlayStateUI[]}; const components = createQueryKeys('components', { list: () => ({ @@ -32,11 +32,26 @@ const activities = createQueryKeys('activities', { queryFn: (ctx) => { const { nonce, + state, ...rest } = filters; + const derived: QueryPlaysOptsJson = rest; + if(state !== undefined) { + derived.state = state.filter(x => isPlayState(x)); + + // remove 'dead queued' derived play state and replace with filter for queue = 'dead' & state = 'queued' + if(state.includes('dead queued') && !rest.queues?.some(x => x.queueName === CLIENT_DEAD_QUEUE)) { + derived.queues = [...(rest.queues ?? []), {queueName: CLIENT_DEAD_QUEUE, queueStatus: 'queued'}]; + } + // remove 'queued' play state and replace with filter for queue = 'ingress' & state = 'queued' + if(state.includes('queued') && !rest.queues?.some(x => x.queueName === CLIENT_INGRESS_QUEUE)) { + derived.queues = [...(derived.queues ?? []), {queueName: CLIENT_INGRESS_QUEUE, queueStatus: 'queued'}]; + derived.state = derived.state.filter(x => x !== 'queued'); + } + } return ky.get(`components/${componentId}/plays`, { baseUrl: baseUrl, - searchParams: qs.stringify({...rest, offset: ctx.pageParam}) + searchParams: qs.stringify({...derived, offset: ctx.pageParam}) }).json>() } }), diff --git a/src/core/Api.ts b/src/core/Api.ts index a13ec267..0860559f 100644 --- a/src/core/Api.ts +++ b/src/core/Api.ts @@ -151,4 +151,6 @@ export type MsSseEvent> = { export type SortPlaysBy = 'played' | 'seen'; export interface SortPlaysByProps { sortBy: SortPlaysBy -} \ No newline at end of file +} + +export type PlayStateUI = PlayState | 'dead queued'; \ No newline at end of file diff --git a/src/core/Atomic.ts b/src/core/Atomic.ts index 7c16aae9..39f5d3c8 100644 --- a/src/core/Atomic.ts +++ b/src/core/Atomic.ts @@ -658,6 +658,7 @@ export type PlayClientState = PlayStateCommon | 'duped' | 'scrobbled'; export const PLAY_CLIENT_STATE = [...PLAY_STATE_COMMON, 'duped', 'scrobbled']; export type PlayState = PlaySourceState | PlayClientState; export const PLAY_STATES = Array.from(new Set([...PLAY_CLIENT_STATE, ...PLAY_SOURCE_STATE])); +export const isPlayState = (val: string): val is PlayState => PLAY_STATES.includes(val); export type QueueStatus = 'queued' | 'completed' | 'failed'; diff --git a/src/core/StringUtils.ts b/src/core/StringUtils.ts index b07bb4fe..e1762fb3 100644 --- a/src/core/StringUtils.ts +++ b/src/core/StringUtils.ts @@ -191,9 +191,10 @@ export const slice = (str: string, index: number, count: number, add?: string): return str.slice(0, index) + (add || "") + str.slice(index + count); } -export const capitalize = (str: any) => { - return str.charAt(0).toUpperCase() + str.slice(1) -} + +export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1) + +export const capitalizeWords = (str: string, delimiter = ' ') => str.split(delimiter).map(x => capitalize(x)).join(delimiter); /** * Split a string-ish variable by a list of deliminators and return the first actually split array or default to returning the string as the first element.