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 (
- <>
+
- >
+
);
}
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.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 {
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 {
- const result = await this.pageRepository.update(pageId, updatePageDto);
+ async update(
+ pageId: string,
+ updatePageDto: UpdatePageDto,
+ userId: string,
+ ): Promise {
+ 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 {
+ async updateState(
+ pageId: string,
+ content: any,
+ ydoc: any,
+ userId?: string, // TODO: fix this
+ ): Promise {
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 {
- return await this.pageRepository.find({
- order: {
- createdAt: 'DESC',
- },
- take: limit,
- });
- }
-
- async getByWorkspaceId(
+ async getSidebarPagesByWorkspaceId(
workspaceId: string,
limit = 200,
): Promise {
@@ -224,4 +231,38 @@ export class PageService {
return transformPageResult(pages);
}
+
+ async getRecentWorkspacePages(
+ workspaceId: string,
+ limit = 20,
+ offset = 0,
+ ): Promise {
+ 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;
+ }
}