From dc65fbafa46b7180a80026c95c9cd6c6805440cb Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Mon, 13 Nov 2023 12:36:56 +0000 Subject: [PATCH] add recent changes tab to home --- client/src/components/layouts/shell.tsx | 3 +- .../src/components/navbar/navbar.module.css | 2 +- client/src/components/navbar/navbar.tsx | 14 +++- .../features/home/components/home-tabs.tsx | 34 ++++++++++ .../features/home/components/home.module.css | 10 +++ .../home/components/page-list-skeleton.tsx | 21 ++++++ .../home/components/recent-changes.tsx | 46 +++++++++++++ client/src/features/page/hooks/use-page.ts | 10 ++- .../features/page/services/page-service.ts | 5 ++ client/src/pages/dashboard/home.tsx | 11 ++- client/src/pages/welcome.tsx | 6 +- .../page/entities/page-ordering.entity.ts | 2 +- server/src/core/page/entities/page.entity.ts | 14 ++++ server/src/core/page/page.controller.ts | 22 +++++- .../core/page/repositories/page.repository.ts | 1 + .../page/services/page-ordering.service.ts | 8 +-- server/src/core/page/services/page.service.ts | 67 +++++++++++++++---- 17 files changed, 240 insertions(+), 36 deletions(-) create mode 100644 client/src/features/home/components/home-tabs.tsx create mode 100644 client/src/features/home/components/home.module.css create mode 100644 client/src/features/home/components/page-list-skeleton.tsx create mode 100644 client/src/features/home/components/recent-changes.tsx diff --git a/client/src/components/layouts/shell.tsx b/client/src/components/layouts/shell.tsx index 2c689a4d..b6c83ea6 100644 --- a/client/src/components/layouts/shell.tsx +++ b/client/src/components/layouts/shell.tsx @@ -1,9 +1,8 @@ import { desktopAsideAtom, desktopSidebarAtom } from '@/components/navbar/atoms/sidebar-atom'; import { useToggleSidebar } from '@/components/navbar/hooks/use-toggle-sidebar'; import { Navbar } from '@/components/navbar/navbar'; -import { ActionIcon, UnstyledButton, ActionIconGroup, AppShell, Avatar, Burger, Group } from '@mantine/core'; +import { AppShell, Burger, Group } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; -import { IconDots } from '@tabler/icons-react'; import { useAtom } from 'jotai'; import classes from './shell.module.css'; import Header from '@/components/layouts/header'; diff --git a/client/src/components/navbar/navbar.module.css b/client/src/components/navbar/navbar.module.css index c1132209..12c6cc33 100644 --- a/client/src/components/navbar/navbar.module.css +++ b/client/src/components/navbar/navbar.module.css @@ -37,7 +37,7 @@ align-items: center; width: 100%; font-size: var(--mantine-font-size-sm); - padding: rem(8px) var(--mantine-spacing-xs); + padding: rem(4px) var(--mantine-spacing-xs); border-radius: var(--mantine-radius-sm); font-weight: 500; color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0)); diff --git a/client/src/components/navbar/navbar.tsx b/client/src/components/navbar/navbar.tsx index 041f3769..e9edc8eb 100644 --- a/client/src/components/navbar/navbar.tsx +++ b/client/src/components/navbar/navbar.tsx @@ -12,6 +12,7 @@ import { IconPlus, IconSettings, IconFilePlus, + IconHome } from '@tabler/icons-react'; import classes from './navbar.module.css'; @@ -23,6 +24,7 @@ import SettingsModal from '@/features/settings/modal/settings-modal'; import { SearchSpotlight } from '@/features/search/search-spotlight'; import { treeApiAtom } from '@/features/page/tree/atoms/tree-api-atom'; import PageTree from '@/features/page/tree/page-tree'; +import { useNavigate } from 'react-router-dom'; interface PrimaryMenuItem { icon: React.ElementType; @@ -31,16 +33,22 @@ interface PrimaryMenuItem { } const primaryMenu: PrimaryMenuItem[] = [ + { icon: IconHome, label: 'Home' }, { icon: IconSearch, label: 'Search' }, { icon: IconSettings, label: 'Settings' }, - { icon: IconFilePlus, label: 'New Page' }, + // { icon: IconFilePlus, label: 'New Page' }, ]; export function Navbar() { const [, setSettingsModalOpen] = useAtom(settingsModalAtom); const [tree] = useAtom(treeApiAtom); + const navigate = useNavigate(); const handleMenuItemClick = (label: string) => { + if (label === 'Home') { + navigate('/home'); + } + if (label === 'Search') { spotlight.open(); } @@ -62,9 +70,9 @@ export function Navbar() { >
{menuItem.label}
diff --git a/client/src/features/home/components/home-tabs.tsx b/client/src/features/home/components/home-tabs.tsx new file mode 100644 index 00000000..5e5b4d01 --- /dev/null +++ b/client/src/features/home/components/home-tabs.tsx @@ -0,0 +1,34 @@ +import { Text, Tabs, Space } from '@mantine/core'; +import { + IconClockHour3, IconStar, +} from '@tabler/icons-react'; +import RecentChanges from '@/features/home/components/recent-changes'; + +export default function HomeTabs() { + + return ( + + + }> + Recent changes + + + }> + Favorites + + + + + + + + + + + + + My favorite pages + + + ); +} diff --git a/client/src/features/home/components/home.module.css b/client/src/features/home/components/home.module.css new file mode 100644 index 00000000..e16a1bd6 --- /dev/null +++ b/client/src/features/home/components/home.module.css @@ -0,0 +1,10 @@ +.page { + display: block; + width: 100%; + padding: var(--mantine-spacing-md); + color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0)); + + @mixin hover { + background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-8)); + } +} diff --git a/client/src/features/home/components/page-list-skeleton.tsx b/client/src/features/home/components/page-list-skeleton.tsx new file mode 100644 index 00000000..7af7ea97 --- /dev/null +++ b/client/src/features/home/components/page-list-skeleton.tsx @@ -0,0 +1,21 @@ +import { Skeleton } from '@mantine/core'; + +export default function PageListSkeleton() { + return ( + <> + + + + + + + + + + + + + + + ); +} diff --git a/client/src/features/home/components/recent-changes.tsx b/client/src/features/home/components/recent-changes.tsx new file mode 100644 index 00000000..842c0421 --- /dev/null +++ b/client/src/features/home/components/recent-changes.tsx @@ -0,0 +1,46 @@ +import { Text, Group, Stack, UnstyledButton, Divider } from '@mantine/core'; +import { format } from 'date-fns'; +import classes from './home.module.css'; +import { Link } from 'react-router-dom'; +import PageListSkeleton from '@/features/home/components/page-list-skeleton'; +import usePage from '@/features/page/hooks/use-page'; + +function RecentChanges() { + const { recentPagesQuery } = usePage(); + const { data, isLoading, isError } = recentPagesQuery; + + if (isLoading) { + return ; + } + + if (isError) { + return Failed to fetch recent pages; + } + + return ( +
+ {data.map((page) => ( + <> + + + + + + {page.title || 'Untitled'} + + + + + {format(new Date(page.createdAt), 'PPP')} + + + + + + ))} +
+ ); +} + +export default RecentChanges; diff --git a/client/src/features/page/hooks/use-page.ts b/client/src/features/page/hooks/use-page.ts index a9e61b14..f6b131c0 100644 --- a/client/src/features/page/hooks/use-page.ts +++ b/client/src/features/page/hooks/use-page.ts @@ -1,5 +1,5 @@ import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query'; -import { createPage, deletePage, getPageById, updatePage } from '@/features/page/services/page-service'; +import { createPage, deletePage, getPageById, getRecentChanges, updatePage } from '@/features/page/services/page-service'; import { IPage } from '@/features/page/types/page.types'; export default function usePage(pageId?: string) { @@ -15,6 +15,11 @@ export default function usePage(pageId?: string) { }, ); + const recentPagesQuery: UseQueryResult = useQuery( + ['recentChanges'], + () => getRecentChanges() + ); + const updateMutation = useMutation( (data: Partial) => updatePage(data), ); @@ -24,8 +29,9 @@ export default function usePage(pageId?: string) { ); return { - create: createMutation.mutate, pageQuery: pageQueryResult, + recentPagesQuery: recentPagesQuery, + create: createMutation.mutate, updatePageMutation: updateMutation.mutate, remove: removeMutation.mutate, }; diff --git a/client/src/features/page/services/page-service.ts b/client/src/features/page/services/page-service.ts index 577ced1d..fac6b0c5 100644 --- a/client/src/features/page/services/page-service.ts +++ b/client/src/features/page/services/page-service.ts @@ -11,6 +11,11 @@ export async function getPageById(id: string): Promise { return req.data as IPage; } +export async function getRecentChanges(): Promise { + const req = await api.post('/pages/recent'); + return req.data as IPage[]; +} + export async function getPages(): Promise { const req = await api.post('/pages'); return req.data as IPage[]; diff --git a/client/src/pages/dashboard/home.tsx b/client/src/pages/dashboard/home.tsx index 81356ba9..a6ffea81 100644 --- a/client/src/pages/dashboard/home.tsx +++ b/client/src/pages/dashboard/home.tsx @@ -1,12 +1,17 @@ import { useAtom } from 'jotai'; import { currentUserAtom } from '@/features/user/atoms/current-user-atom'; +import { Container } from '@mantine/core'; +import HomeTabs from '@/features/home/components/home-tabs'; +// Hello {currentUser && currentUser.user.name}! export default function Home() { const [currentUser] = useAtom(currentUserAtom); return ( - <> - Hello {currentUser && currentUser.user.name}! - + + + + + ); } diff --git a/client/src/pages/welcome.tsx b/client/src/pages/welcome.tsx index 21fd5de0..6c0d0e5e 100644 --- a/client/src/pages/welcome.tsx +++ b/client/src/pages/welcome.tsx @@ -1,9 +1,9 @@ -import { Title, Text } from '@mantine/core'; +import { Title, Text, Stack } from '@mantine/core'; import { ThemeToggle } from '@/components/theme-toggle'; export function Welcome() { return ( - <> + <Text inherit @@ -18,6 +18,6 @@ export function Welcome() { Welcome to something new and interesting. </Text> <ThemeToggle /> - </> + </Stack> ); } diff --git a/server/src/core/page/entities/page-ordering.entity.ts b/server/src/core/page/entities/page-ordering.entity.ts index f05a6993..e89d332b 100644 --- a/server/src/core/page/entities/page-ordering.entity.ts +++ b/server/src/core/page/entities/page-ordering.entity.ts @@ -23,7 +23,7 @@ export class PageOrdering { @Column({ type: 'varchar', length: 50, nullable: false }) entityType: string; - @Column('uuid', { array: true, default: () => 'ARRAY[]::uuid[]' }) + @Column('uuid', { array: true }) childrenIds: string[]; @ManyToOne(() => Workspace, (workspace) => workspace.id, { diff --git a/server/src/core/page/entities/page.entity.ts b/server/src/core/page/entities/page.entity.ts index 236f044d..e9d75b33 100644 --- a/server/src/core/page/entities/page.entity.ts +++ b/server/src/core/page/entities/page.entity.ts @@ -55,6 +55,20 @@ export class Page { @JoinColumn({ name: 'creatorId' }) creator: User; + @Column({ type: 'uuid', nullable: true }) + lastUpdatedById: string; + + @ManyToOne(() => User) + @JoinColumn({ name: 'lastUpdatedById' }) + lastUpdatedBy: User; + + @Column({ type: 'uuid', nullable: true }) + deletedById: string; + + @ManyToOne(() => User) + @JoinColumn({ name: 'deletedById' }) + deletedBy: User; + @Column() workspaceId: string; diff --git a/server/src/core/page/page.controller.ts b/server/src/core/page/page.controller.ts index 7771330e..c22fb3d4 100644 --- a/server/src/core/page/page.controller.ts +++ b/server/src/core/page/page.controller.ts @@ -50,8 +50,14 @@ export class PageController { @HttpCode(HttpStatus.OK) @Post('update') - async update(@Body() updatePageDto: UpdatePageDto) { - return this.pageService.update(updatePageDto.id, updatePageDto); + async update( + @Req() req: FastifyRequest, + @Body() updatePageDto: UpdatePageDto, + ) { + const jwtPayload = req['user']; + const userId = jwtPayload.sub; + + return this.pageService.update(updatePageDto.id, updatePageDto, userId); } @HttpCode(HttpStatus.OK) @@ -72,6 +78,16 @@ export class PageController { return this.pageOrderService.movePage(movePageDto); } + @HttpCode(HttpStatus.OK) + @Post('recent') + async getRecentWorkspacePages(@Req() req: FastifyRequest) { + const jwtPayload = req['user']; + const workspaceId = ( + await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) + ).id; + return this.pageService.getRecentWorkspacePages(workspaceId); + } + @HttpCode(HttpStatus.OK) @Post() async getWorkspacePages(@Req() req: FastifyRequest) { @@ -79,7 +95,7 @@ export class PageController { const workspaceId = ( await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) ).id; - return this.pageService.getByWorkspaceId(workspaceId); + return this.pageService.getSidebarPagesByWorkspaceId(workspaceId); } @HttpCode(HttpStatus.OK) diff --git a/server/src/core/page/repositories/page.repository.ts b/server/src/core/page/repositories/page.repository.ts index 574f48ac..7e29302d 100644 --- a/server/src/core/page/repositories/page.repository.ts +++ b/server/src/core/page/repositories/page.repository.ts @@ -26,6 +26,7 @@ export class PageRepository extends Repository<Page> { 'page.shareId', 'page.parentPageId', 'page.creatorId', + 'page.lastUpdatedById', 'page.workspaceId', 'page.isLocked', 'page.status', diff --git a/server/src/core/page/services/page-ordering.service.ts b/server/src/core/page/services/page-ordering.service.ts index 895fc9c5..5bed4e37 100644 --- a/server/src/core/page/services/page-ordering.service.ts +++ b/server/src/core/page/services/page-ordering.service.ts @@ -52,8 +52,6 @@ export class PageOrderingService { orderPageList(workspaceOrdering.childrenIds, dto); - console.log(workspaceOrdering.childrenIds); - await manager.save(workspaceOrdering); } else { const parentPageId = dto.parentId; @@ -236,8 +234,8 @@ export class PageOrderingService { manager: EntityManager, ): Promise<PageOrdering> { await manager.query( - `INSERT INTO page_ordering ("entityId", "entityType", "workspaceId") - VALUES ($1, $2, $3) + `INSERT INTO page_ordering ("entityId", "entityType", "workspaceId", "childrenIds") + VALUES ($1, $2, $3, '{}') ON CONFLICT ("entityId", "entityType") DO NOTHING`, [entityId, entityType, workspaceId], ); @@ -260,7 +258,7 @@ export class PageOrderingService { const workspaceOrder = await this.getWorkspacePageOrder(workspaceId); const pageOrder = workspaceOrder ? workspaceOrder.childrenIds : undefined; - const pages = await this.pageService.getByWorkspaceId(workspaceId); + const pages = await this.pageService.getSidebarPagesByWorkspaceId(workspaceId); const pageMap: { [id: string]: PageWithOrderingDto } = {}; pages.forEach((page) => { diff --git a/server/src/core/page/services/page.service.ts b/server/src/core/page/services/page.service.ts index 2678f58a..43e152ea 100644 --- a/server/src/core/page/services/page.service.ts +++ b/server/src/core/page/services/page.service.ts @@ -47,6 +47,7 @@ export class PageService { const page = plainToInstance(Page, createPageDto); page.creatorId = userId; page.workspaceId = workspaceId; + page.lastUpdatedById = userId; if (createPageDto.parentPageId) { // TODO: make sure parent page belongs to same workspace and user has permissions @@ -69,8 +70,17 @@ export class PageService { return createdPage; } - async update(pageId: string, updatePageDto: UpdatePageDto): Promise<Page> { - const result = await this.pageRepository.update(pageId, updatePageDto); + async update( + pageId: string, + updatePageDto: UpdatePageDto, + userId: string, + ): Promise<Page> { + const updateData = { + ...updatePageDto, + lastUpdatedById: userId, + }; + + const result = await this.pageRepository.update(pageId, updateData); if (result.affected === 0) { throw new BadRequestException(`Page not found`); } @@ -78,10 +88,16 @@ export class PageService { return await this.pageRepository.findWithoutYDoc(pageId); } - async updateState(pageId: string, content: any, ydoc: any): Promise<void> { + async updateState( + pageId: string, + content: any, + ydoc: any, + userId?: string, // TODO: fix this + ): Promise<void> { await this.pageRepository.update(pageId, { content: content, ydoc: ydoc, + ...(userId && { lastUpdatedById: userId }), }); } @@ -187,16 +203,7 @@ export class PageService { return await this.pageRepository.findById(pageId); } - async getRecentPages(limit = 10): Promise<Page[]> { - return await this.pageRepository.find({ - order: { - createdAt: 'DESC', - }, - take: limit, - }); - } - - async getByWorkspaceId( + async getSidebarPagesByWorkspaceId( workspaceId: string, limit = 200, ): Promise<PageWithOrderingDto[]> { @@ -224,4 +231,38 @@ export class PageService { return transformPageResult(pages); } + + async getRecentWorkspacePages( + workspaceId: string, + limit = 20, + offset = 0, + ): Promise<Page[]> { + const pages = await this.pageRepository + .createQueryBuilder('page') + .where('page.workspaceId = :workspaceId', { workspaceId }) + .select([ + 'page.id', + 'page.title', + 'page.slug', + 'page.icon', + 'page.coverPhoto', + 'page.editor', + 'page.shareId', + 'page.parentPageId', + 'page.creatorId', + 'page.lastUpdatedById', + 'page.workspaceId', + 'page.isLocked', + 'page.status', + 'page.publishedAt', + 'page.createdAt', + 'page.updatedAt', + 'page.deletedAt', + ]) + .orderBy('page.updatedAt', 'DESC') + .offset(offset) + .take(limit) + .getMany(); + return pages; + } }