diff --git a/src/client/components/icons/ChakraIcons.tsx b/src/client/components/icons/ChakraIcons.tsx index 40d68696..dfd22e25 100644 --- a/src/client/components/icons/ChakraIcons.tsx +++ b/src/client/components/icons/ChakraIcons.tsx @@ -18,20 +18,27 @@ import { LuPowerOff, LuEye, LuEyeClosed, - LuCalendar + LuCalendar, + LuRefreshCw } from "react-icons/lu" import { VscDebugRestart } from 'react-icons/vsc'; import { RiZzzFill } from "react-icons/ri"; import { SiGoogledocs } from "react-icons/si"; -import { IconButton, Clipboard, useClipboard } from "@chakra-ui/react" +import { IconButton, Clipboard, useClipboard, Spinner } from "@chakra-ui/react" import { ComponentProps, PropsWithChildren } from 'react'; import { IconBaseProps, IconType } from "react-icons/lib"; -export const makeIconButton = (Icon: IconType) => (props: PropsWithChildren> & { iconProps?: IconBaseProps }) => { - const { iconProps = {}, children, ...rest } = props; +export const makeIconButton = (Icon: IconType) => (props: PropsWithChildren> & { iconProps?: IconBaseProps, loading?: boolean }) => { + const { + iconProps = {}, + children, + loading = false, + size = 'xs', + ...rest + } = props; return ( - - {children} + + {loading ? : }{children} ); } @@ -142,4 +149,7 @@ export const EyeClosedIcon = LuEyeClosed; export const EyeClosedButton = makeIconButton(LuEyeClosed); export const CalendarIcon = LuCalendar; -export const CalendarButton = makeIconButton(CalendarIcon); \ No newline at end of file +export const CalendarButton = makeIconButton(CalendarIcon); + +export const RefreshIcon = LuRefreshCw; +export const RefreshButton = makeIconButton(RefreshIcon); \ No newline at end of file diff --git a/src/client/components/playActivity/ActivityList.tsx b/src/client/components/playActivity/ActivityList.tsx index 33bcab21..4500a57c 100644 --- a/src/client/components/playActivity/ActivityList.tsx +++ b/src/client/components/playActivity/ActivityList.tsx @@ -8,7 +8,7 @@ import "./PlayList.scss"; import { MsSseEvent, MsSseEventPayload, PlayApiCommon, PlayApiCommonDetailed } from '../../../core/Api.js'; import { useQuery, useInfiniteQuery, UseInfiniteQueryResult, InfiniteData, useQueryClient } from '@tanstack/react-query'; import { ErrorAlert } from '../ErrorAlert.js'; -import { tanQueries } from '../../queries/index.js'; +import { QueryPlaysOptsJsonRefreshable, tanQueries } from '../../queries/index.js'; import { VirtualizedListNormal } from './VirtualListNormal.js'; import { NoPlayResults, VirtualizedListDynamic } from './VirtualListDynamic.js'; import { VirtualizedListExp } from './VirtualListExperimental.js'; @@ -67,7 +67,7 @@ const PlainAccordian = (props: ActivityLogProps) => { - {live ? : } + {live ? : } @@ -86,12 +86,12 @@ export const ListContainer = (props?: ComponentProps) => { return } -export const ListContainerFetchable = (props: { componentId: number, componentType: ComponentType, filters?: QueryPlaysOptsJson } & Pick, 'render'>) => { +export const ListContainerFetchable = (props: { componentId: number, componentType: ComponentType, filters?: QueryPlaysOptsJsonRefreshable } & Pick, 'render'>) => { const { componentId, filters = {} } = props; - const query: QueryPlaysOptsJson = { ...filters, order: 'desc', sort: 'playedAt' }; + const query: QueryPlaysOptsJsonRefreshable = filters; // { ...filters, order: 'desc', sort: 'playedAt' }; const { isPending, isError, @@ -133,8 +133,8 @@ export const ListContainerFetchable = (props: { componentId: number, componentTy }); let rendered; - if (status === 'pending' && data === undefined) { - rendered = ; + if (isPending) { + rendered = ; } else if (isError || status === 'error') { rendered = } else if(!isFetching && allPlays.length === 0) { @@ -263,16 +263,18 @@ const playInWindow = (data: PlayApiCommonDetailed, query: QueryPlaysOptsJson): b } export const ListContainerFilterable = (props: { componentId: number, componentType: ComponentType } & Pick, 'render'>) => { - const [filters, setFilter] = useState({ + const [filters, setFilter] = useState({ playedAt: { type: 'between', range: todayRange, inclusive: true - } + }, + order: 'desc', + sort: 'playedAt' }); return ( - + ) diff --git a/src/client/components/playActivity/ListFilters.tsx b/src/client/components/playActivity/ListFilters.tsx index b7a4e26f..8479a5c2 100644 --- a/src/client/components/playActivity/ListFilters.tsx +++ b/src/client/components/playActivity/ListFilters.tsx @@ -1,4 +1,4 @@ -import { Span, Stack, Text, Box, HStack, Wrap, Flex, Container, Select, Portal, createListCollection, useSelectContext, DatePicker, VStack, Button, Spacer, TagsInput, Card } from '@chakra-ui/react'; +import { Span, Stack, Text, Box, Spinner, HStack, Wrap, Flex, Container, Select, Portal, createListCollection, useSelectContext, DatePicker, VStack, Button, Spacer, TagsInput, Card } from '@chakra-ui/react'; import { ComponentType, isComponentTypeSource, PLAY_CLIENT_STATE, PLAY_SOURCE_STATE, PlayState } from '../../../core/Atomic.js'; import React, { ComponentProps, Fragment, useMemo, useCallback, useState } from "react" import dayjs, { Dayjs } from 'dayjs'; @@ -24,7 +24,10 @@ import { import { QueryPlaysOptsJson } from '../../../backend/common/database/drizzle/repositories/PlayRepository.js'; import { cardHeaderSeparator } from '../../utils/ComponentUtils.js'; import { CompareDateBetween } from '../../../backend/common/database/drizzle/repositories/BaseRepository.js'; -import { CalendarButton } from '../icons/ChakraIcons.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'; const noop = (_) => null; @@ -256,15 +259,20 @@ export const PhraseFilter = (props: PhraseFilterProps) => { } export const ListFilters = (props: { - onChange: (e: QueryPlaysOptsJson) => void - filters: QueryPlaysOptsJson + onChange: (e: QueryPlaysOptsJsonRefreshable) => void, + loading?: boolean + filters: QueryPlaysOptsJsonRefreshable componentType: ComponentType, + componentId: number }) => { const { filters, onChange, + componentId, } = props; + const queryClient = useQueryClient(); + const setState = useCallback((val: PlayState[]) => { const { state, @@ -299,10 +307,24 @@ export const ListFilters = (props: { onChange({...rest, text: val}); }, [onChange, filters]); + const onRefresh = useCallback(() => { + queryClient.invalidateQueries({ + queryKey: tanQueries.activities.list(componentId, filters).queryKey + }); + // const nonce = nanoid(); + // const { + // ...rest + // } = filters; + // console.log(`Adding nonce for refresh ${nonce}`); + // onChange({...rest, nonce}); + }, [componentId, filters]); + + const { isFetching } = useQueryWatcher(tanQueries.activities.list(componentId, filters).queryKey) + return ( - Filters + Filters onRefresh()}/> diff --git a/src/client/queries/index.ts b/src/client/queries/index.ts index d442f54f..0b416f33 100644 --- a/src/client/queries/index.ts +++ b/src/client/queries/index.ts @@ -1,4 +1,6 @@ import { createQueryKeys, mergeQueryKeys } from "@lukemorales/query-key-factory"; +import { useQueryClient, hashKey, QueryObserver } from '@tanstack/react-query' +import { useEffect, useState, useMemo } from 'react'; import ky from 'ky'; import { QueryPlaysOpts, QueryPlaysOptsJson } from "../../backend/common/database/drizzle/repositories/PlayRepository"; import qs from 'qs'; @@ -7,6 +9,8 @@ import { PaginatedResponse } from "../../backend/common/database/drizzle/reposit import { ComponentsApiJson, PlayApiCommonDetailed } from "../../core/Api"; import { SourcePlayerJson } from "../../core/Atomic"; +export type QueryPlaysOptsJsonRefreshable = QueryPlaysOptsJson & {nonce?: string}; + const components = createQueryKeys('components', { list: () => ({ queryKey: ['components'], @@ -23,14 +27,18 @@ const components = createQueryKeys('components', { }) const activities = createQueryKeys('activities', { - list: (componentId: number, filters: QueryPlaysOptsJson) => ({ + list: (componentId: number, filters: QueryPlaysOptsJsonRefreshable) => ({ queryKey: ['components', componentId, 'plays', filters], queryFn: (ctx) => { + const { + nonce, + ...rest + } = filters; return ky.get(`components/${componentId}/plays`, { - baseUrl: baseUrl, - searchParams: qs.stringify({...filters, offset: ctx.pageParam}) - }).json>() - } + baseUrl: baseUrl, + searchParams: qs.stringify({...rest, offset: ctx.pageParam}) + }).json>() + } }), single: (componentId: number, activityUid: string) => ({ queryKey: ['components', componentId, 'play', activityUid], @@ -53,4 +61,41 @@ const players = createQueryKeys('players', { }) }) -export const tanQueries = mergeQueryKeys(components, activities, players); \ No newline at end of file +export const tanQueries = mergeQueryKeys(components, activities, players); + +export const useQueryState = (queryKey: Readonly) => { + const queryClient = useQueryClient() + const [state, setState] = useState(() => queryClient.getQueryState(queryKey)) + + useEffect(() => { + const targetHash = hashKey(queryKey) + return queryClient.getQueryCache().subscribe((event) => { + if (event.query.queryHash === targetHash) { + setState(event.query.state) + } + }) + }, [queryClient, queryKey]) + + return state // { status, data, error, fetchStatus, ... } +} + +export const useQueryWatcher = (queryKey: Readonly) => { + const queryClient = useQueryClient() + + const observer = useMemo( + () => + new QueryObserver(queryClient, { + queryKey, + enabled: false, // never triggers its own fetch + }), + [queryClient, queryKey] + ) + + const [result, setResult] = useState(() => observer.getCurrentResult()) + + useEffect(() => { + return observer.subscribe(setResult) + }, [observer]) + + return result // { status, data, error, isPending, isSuccess, ... } +} \ No newline at end of file