diff --git a/apps/frontend/src/components/launches/calendar.context.tsx b/apps/frontend/src/components/launches/calendar.context.tsx index 8c81c99e..fb5bea36 100644 --- a/apps/frontend/src/components/launches/calendar.context.tsx +++ b/apps/frontend/src/components/launches/calendar.context.tsx @@ -26,6 +26,8 @@ import { expandPostsList, expandPosts } from '@gitroom/helpers/utils/posts.list. extend(isoWeek); extend(weekOfYear); +export type ListStateFilter = 'all' | 'scheduled' | 'draft' | 'published'; + export const CalendarContext = createContext({ startDate: newDayjs().startOf('isoWeek').format('YYYY-MM-DD'), endDate: newDayjs().endOf('isoWeek').format('YYYY-MM-DD'), @@ -78,6 +80,10 @@ export const CalendarContext = createContext({ setListPage: (page: number) => { /** empty **/ }, + listState: 'all' as ListStateFilter, + setListState: (state: ListStateFilter) => { + /** empty **/ + }, }); export interface Integrations { @@ -144,6 +150,11 @@ export const CalendarWeekProvider: FC<{ // List view state const [listPage, setListPage] = useState(0); + const [listState, setListStateRaw] = useState('all'); + const setListState = useCallback((next: ListStateFilter) => { + setListStateRaw(next); + setListPage(0); + }, []); // Initialize with current date range based on URL params or defaults const initStartDate = searchParams.get('startDate'); @@ -190,8 +201,9 @@ export const CalendarWeekProvider: FC<{ page: listPage.toString(), limit: '100', customer: filters?.customer?.toString() || '', + state: listState, }).toString(); - }, [listPage, filters.customer]); + }, [listPage, filters.customer, listState]); const loadListData = useCallback(async () => { const response = await fetch(`/posts/list?${listParams}`); @@ -341,6 +353,8 @@ export const CalendarWeekProvider: FC<{ listPage, listTotalPages, setListPage, + listState, + setListState, }} > {children} diff --git a/apps/frontend/src/components/launches/calendar.tsx b/apps/frontend/src/components/launches/calendar.tsx index 1cb3f40b..75c60a90 100644 --- a/apps/frontend/src/components/launches/calendar.tsx +++ b/apps/frontend/src/components/launches/calendar.tsx @@ -493,7 +493,15 @@ export const MonthView = () => { export const ListView = () => { const t = useT(); const user = useUser(); - const { integrations, loading, listPosts } = useCalendar(); + const { integrations, loading, listPosts, listState } = useCalendar(); + const emptyMessage = + listState === 'scheduled' + ? t('no_upcoming_posts', 'No upcoming posts scheduled') + : listState === 'draft' + ? t('no_draft_posts', 'No draft posts') + : listState === 'published' + ? t('no_published_posts', 'No published posts') + : t('no_posts', 'No posts'); // Use shared post actions hook const { editPost, deletePost, copyDebugJson, openStatistics, openMissingRelease } = usePostActions(); @@ -522,9 +530,7 @@ export const ListView = () => { if (listPosts.length === 0) { return (
-
- {t('no_upcoming_posts', 'No upcoming posts scheduled')} -
+
{emptyMessage}
); } diff --git a/apps/frontend/src/components/launches/filters.tsx b/apps/frontend/src/components/launches/filters.tsx index c6b534c8..5fd690f6 100644 --- a/apps/frontend/src/components/launches/filters.tsx +++ b/apps/frontend/src/components/launches/filters.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useCalendar } from '@gitroom/frontend/components/launches/calendar.context'; +import { useCalendar, ListStateFilter } from '@gitroom/frontend/components/launches/calendar.context'; import clsx from 'clsx'; import dayjs from 'dayjs'; import { useCallback } from 'react'; @@ -259,6 +259,21 @@ export const Filters = () => { const isListView = calendar.display === 'list'; + const setListStateFilter = useCallback( + (next: ListStateFilter) => () => { + if (calendar.listState === next) return; + calendar.setListState(next); + }, + [calendar] + ); + + const listStateOptions: { value: ListStateFilter; label: string }[] = [ + { value: 'all', label: t('all', 'All') }, + { value: 'scheduled', label: t('scheduled', 'Scheduled') }, + { value: 'draft', label: t('draft', 'Draft') }, + { value: 'published', label: t('published', 'Published') }, + ]; + const previousPage = useCallback(() => { if (calendar.listPage > 0) { calendar.setListPage(calendar.listPage - 1); @@ -393,6 +408,21 @@ export const Filters = () => { +
+ {listStateOptions.map((option) => ( +
+ {option.label} +
+ ))} +
)} diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts index 84767937..c11a3166 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts @@ -224,6 +224,26 @@ export class PostsRepository { const limit = query.limit || 20; const skip = page * limit; + const stateFilter = query.state || 'all'; + const stateAndDate = + stateFilter === 'scheduled' + ? { + state: State.QUEUE, + publishDate: { gte: dayjs.utc().toDate() }, + } + : stateFilter === 'draft' + ? { state: State.DRAFT } + : stateFilter === 'published' + ? { state: State.PUBLISHED } + : { + state: { + in: [State.QUEUE, State.DRAFT, State.PUBLISHED, State.ERROR], + }, + }; + + const orderDirection: 'asc' | 'desc' = + stateFilter === 'published' ? 'desc' : 'asc'; + const where = { AND: [ { @@ -233,12 +253,8 @@ export class PostsRepository { }, ], }, - { - publishDate: { - gte: dayjs.utc().toDate(), - }, - }, ], + ...stateAndDate, deletedAt: null as Date | null, parentPostId: null as string | null, intervalInDays: null as number | null, @@ -257,7 +273,7 @@ export class PostsRepository { skip, take: limit, orderBy: { - publishDate: 'asc', + publishDate: orderDirection, }, select: { id: true, diff --git a/libraries/nestjs-libraries/src/dtos/posts/get.posts.list.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/get.posts.list.dto.ts index 89ef94dd..70e764e8 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/get.posts.list.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/get.posts.list.dto.ts @@ -4,9 +4,12 @@ import { IsNumber, Min, Max, + IsIn, } from 'class-validator'; import { Transform } from 'class-transformer'; +export type PostListStateFilter = 'all' | 'scheduled' | 'draft' | 'published'; + export class GetPostsListDto { @IsOptional() @IsNumber() @@ -24,4 +27,8 @@ export class GetPostsListDto { @IsOptional() @IsString() customer?: string; + + @IsOptional() + @IsIn(['all', 'scheduled', 'draft', 'published']) + state?: PostListStateFilter = 'all'; }