feat: add page stats to page menu (#876)

This commit is contained in:
Philip Okugbe 2025-03-13 14:54:18 +00:00 committed by GitHub
parent f7a14e23cd
commit f45d9dc5a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 111 additions and 7 deletions

View File

@ -24,6 +24,7 @@
"@mantine/spotlight": "^7.17.0",
"@tabler/icons-react": "^3.22.0",
"@tanstack/react-query": "^5.61.4",
"@tiptap/extension-character-count": "^2.11.5",
"axios": "^1.7.9",
"clsx": "^2.1.1",
"emoji-mart": "^5.6.0",

View File

@ -346,5 +346,10 @@
"Space deleted successfully": "Space deleted successfully",
"Members added successfully": "Members added successfully",
"Member removed successfully": "Member removed successfully",
"Member role updated successfully": "Member role updated successfully"
"Member role updated successfully": "Member role updated successfully",
"Created by: <b>{{creatorName}}</b>": "Created by: <b>{{creatorName}}</b>",
"Created at: {{time}}": "Created at: {{time}}",
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
"Character count: {{characterCount}}": "Character count: {{characterCount}}"
}

View File

@ -71,6 +71,7 @@ import MentionView from "@/features/editor/components/mention/mention-view.tsx";
import i18n from "@/i18n.ts";
import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboard.ts";
import EmojiCommand from "./emoji-command";
import { CharacterCount } from "@tiptap/extension-character-count";
const lowlight = createLowlight(common);
lowlight.register("mermaid", plaintext);
@ -211,6 +212,7 @@ export const mainExtensions = [
MarkdownClipboard.configure({
transformPastedText: true,
}),
CharacterCount
] as any;
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];

View File

@ -1,4 +1,4 @@
import { ActionIcon, Group, Menu, Tooltip } from "@mantine/core";
import { ActionIcon, Group, Menu, Text, Tooltip } from "@mantine/core";
import {
IconArrowsHorizontal,
IconDots,
@ -24,9 +24,13 @@ import { extractPageSlugId } from "@/lib";
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import ExportModal from "@/components/common/export-modal";
import { yjsConnectionStatusAtom } from "@/features/editor/atoms/editor-atoms.ts";
import {
pageEditorAtom,
yjsConnectionStatusAtom,
} from "@/features/editor/atoms/editor-atoms.ts";
import { formattedDate, timeAgo } from "@/lib/time.ts";
interface PageHeaderMenuProps {
readOnly?: boolean;
@ -79,6 +83,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
const [tree] = useAtom(treeApiAtom);
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
useDisclosure(false);
const [pageEditor] = useAtom(pageEditorAtom);
const handleCopyLink = () => {
const pageUrl =
@ -108,7 +113,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
shadow="xl"
position="bottom-end"
offset={20}
width={200}
width={230}
withArrow
arrowPosition="center"
>
@ -168,6 +173,41 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
</Menu.Item>
</>
)}
<Menu.Divider />
<>
<Group px="sm" wrap="nowrap" style={{ cursor: "pointer" }}>
<Tooltip
label={t("Edited by {{name}} {{time}}", {
name: page.lastUpdatedBy.name,
time: timeAgo(page.updatedAt),
})}
position="left-start"
>
<div>
<Text size="xs" c="dimmed" truncate="end">
{t("Word count: {{wordCount}}", {
wordCount: pageEditor?.storage?.characterCount?.words(),
})}
</Text>
<Text size="xs" c="dimmed" lineClamp={1}>
<Trans
defaults="Created by: <b>{{creatorName}}</b>"
values={{ creatorName: page?.creator?.name }}
components={{ b: <Text span fw={500} /> }}
/>
</Text>
<Text size="xs" c="dimmed" truncate="end">
{t("Created at: {{time}}", {
time: formattedDate(page.createdAt),
})}
</Text>
</div>
</Tooltip>
</Group>
</>
</Menu.Dropdown>
</Menu>

View File

@ -12,16 +12,28 @@ export interface IPage {
spaceId: string;
workspaceId: string;
isLocked: boolean;
isPublic: boolean;
lastModifiedById: Date;
lastUpdatedById: Date;
createdAt: Date;
updatedAt: Date;
deletedAt: Date;
position: string;
hasChildren: boolean;
creator: ICreator;
lastUpdatedBy: ILastUpdatedBy;
space: Partial<ISpace>;
}
interface ICreator {
id: string;
name: string;
avatarUrl: string;
}
interface ILastUpdatedBy {
id: string;
name: string;
avatarUrl: string;
}
export interface IMovePage {
pageId: string;
position?: string;

View File

@ -44,6 +44,8 @@ export class PageController {
const page = await this.pageRepo.findById(dto.pageId, {
includeSpace: true,
includeContent: true,
includeCreator: true,
includeLastUpdatedBy: true,
});
if (!page) {

View File

@ -46,6 +46,8 @@ export class PageRepo {
includeContent?: boolean;
includeYdoc?: boolean;
includeSpace?: boolean;
includeCreator?: boolean;
includeLastUpdatedBy?: boolean;
withLock?: boolean;
trx?: KyselyTransaction;
},
@ -58,6 +60,14 @@ export class PageRepo {
.$if(opts?.includeContent, (qb) => qb.select('content'))
.$if(opts?.includeYdoc, (qb) => qb.select('ydoc'));
if (opts?.includeCreator) {
query = query.select((eb) => this.withCreator(eb));
}
if (opts?.includeLastUpdatedBy) {
query = query.select((eb) => this.withLastUpdatedBy(eb));
}
if (opts?.includeSpace) {
query = query.select((eb) => this.withSpace(eb));
}
@ -161,6 +171,24 @@ export class PageRepo {
).as('space');
}
withCreator(eb: ExpressionBuilder<DB, 'pages'>) {
return jsonObjectFrom(
eb
.selectFrom('users')
.select(['users.id', 'users.name', 'users.avatarUrl'])
.whereRef('users.id', '=', 'pages.creatorId'),
).as('creator');
}
withLastUpdatedBy(eb: ExpressionBuilder<DB, 'pages'>) {
return jsonObjectFrom(
eb
.selectFrom('users')
.select(['users.id', 'users.name', 'users.avatarUrl'])
.whereRef('users.id', '=', 'pages.lastUpdatedById'),
).as('lastUpdatedBy');
}
async getPageAndDescendants(parentPageId: string) {
return this.db
.withRecursive('page_hierarchy', (db) =>

14
pnpm-lock.yaml generated
View File

@ -242,6 +242,9 @@ importers:
'@tanstack/react-query':
specifier: ^5.61.4
version: 5.61.4(react@18.3.1)
'@tiptap/extension-character-count':
specifier: ^2.11.5
version: 2.11.5(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)
axios:
specifier: ^1.7.9
version: 1.7.9
@ -3654,6 +3657,12 @@ packages:
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-character-count@2.11.5':
resolution: {integrity: sha512-Da2VGb7ClmKwXdQdQC2735qylYD8/MQAPA0skPEcHxcDTDuI8ibyIDnMPnczgS/hR5g0TYE2DQp/dkhJXeovkQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-code-block-lowlight@2.10.3':
resolution: {integrity: sha512-ieRSdfDW06pmKcsh73N506/EWNJrpMrZzyuFx3YGJtfM+Os0a9hMLy2TSuNleyRsihBi5mb+zvdeqeGdaJm7Ng==}
peerDependencies:
@ -12802,6 +12811,11 @@ snapshots:
dependencies:
'@tiptap/core': 2.10.3(@tiptap/pm@2.10.3)
'@tiptap/extension-character-count@2.11.5(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)':
dependencies:
'@tiptap/core': 2.10.3(@tiptap/pm@2.10.3)
'@tiptap/pm': 2.10.3
'@tiptap/extension-code-block-lowlight@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/extension-code-block@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)(highlight.js@11.10.0)(lowlight@3.2.0)':
dependencies:
'@tiptap/core': 2.10.3(@tiptap/pm@2.10.3)