mirror of
https://github.com/docmost/docmost.git
synced 2025-11-13 10:22:36 +10:00
feat: add page stats to page menu (#876)
This commit is contained in:
@ -24,6 +24,7 @@
|
|||||||
"@mantine/spotlight": "^7.17.0",
|
"@mantine/spotlight": "^7.17.0",
|
||||||
"@tabler/icons-react": "^3.22.0",
|
"@tabler/icons-react": "^3.22.0",
|
||||||
"@tanstack/react-query": "^5.61.4",
|
"@tanstack/react-query": "^5.61.4",
|
||||||
|
"@tiptap/extension-character-count": "^2.11.5",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"emoji-mart": "^5.6.0",
|
"emoji-mart": "^5.6.0",
|
||||||
|
|||||||
@ -346,5 +346,10 @@
|
|||||||
"Space deleted successfully": "Space deleted successfully",
|
"Space deleted successfully": "Space deleted successfully",
|
||||||
"Members added successfully": "Members added successfully",
|
"Members added successfully": "Members added successfully",
|
||||||
"Member removed successfully": "Member removed 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}}"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,6 +71,7 @@ import MentionView from "@/features/editor/components/mention/mention-view.tsx";
|
|||||||
import i18n from "@/i18n.ts";
|
import i18n from "@/i18n.ts";
|
||||||
import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboard.ts";
|
import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboard.ts";
|
||||||
import EmojiCommand from "./emoji-command";
|
import EmojiCommand from "./emoji-command";
|
||||||
|
import { CharacterCount } from "@tiptap/extension-character-count";
|
||||||
|
|
||||||
const lowlight = createLowlight(common);
|
const lowlight = createLowlight(common);
|
||||||
lowlight.register("mermaid", plaintext);
|
lowlight.register("mermaid", plaintext);
|
||||||
@ -211,6 +212,7 @@ export const mainExtensions = [
|
|||||||
MarkdownClipboard.configure({
|
MarkdownClipboard.configure({
|
||||||
transformPastedText: true,
|
transformPastedText: true,
|
||||||
}),
|
}),
|
||||||
|
CharacterCount
|
||||||
] as any;
|
] as any;
|
||||||
|
|
||||||
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];
|
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ActionIcon, Group, Menu, Tooltip } from "@mantine/core";
|
import { ActionIcon, Group, Menu, Text, Tooltip } from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconArrowsHorizontal,
|
IconArrowsHorizontal,
|
||||||
IconDots,
|
IconDots,
|
||||||
@ -24,9 +24,13 @@ import { extractPageSlugId } from "@/lib";
|
|||||||
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
|
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
|
||||||
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
||||||
import { PageWidthToggle } from "@/features/user/components/page-width-pref.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 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 {
|
interface PageHeaderMenuProps {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@ -79,6 +83,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
|||||||
const [tree] = useAtom(treeApiAtom);
|
const [tree] = useAtom(treeApiAtom);
|
||||||
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
||||||
useDisclosure(false);
|
useDisclosure(false);
|
||||||
|
const [pageEditor] = useAtom(pageEditorAtom);
|
||||||
|
|
||||||
const handleCopyLink = () => {
|
const handleCopyLink = () => {
|
||||||
const pageUrl =
|
const pageUrl =
|
||||||
@ -108,7 +113,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
|||||||
shadow="xl"
|
shadow="xl"
|
||||||
position="bottom-end"
|
position="bottom-end"
|
||||||
offset={20}
|
offset={20}
|
||||||
width={200}
|
width={230}
|
||||||
withArrow
|
withArrow
|
||||||
arrowPosition="center"
|
arrowPosition="center"
|
||||||
>
|
>
|
||||||
@ -168,6 +173,41 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
|||||||
</Menu.Item>
|
</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.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
|
|||||||
@ -12,16 +12,28 @@ export interface IPage {
|
|||||||
spaceId: string;
|
spaceId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
isPublic: boolean;
|
lastUpdatedById: Date;
|
||||||
lastModifiedById: Date;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
deletedAt: Date;
|
deletedAt: Date;
|
||||||
position: string;
|
position: string;
|
||||||
hasChildren: boolean;
|
hasChildren: boolean;
|
||||||
|
creator: ICreator;
|
||||||
|
lastUpdatedBy: ILastUpdatedBy;
|
||||||
space: Partial<ISpace>;
|
space: Partial<ISpace>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ICreator {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
}
|
||||||
|
interface ILastUpdatedBy {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IMovePage {
|
export interface IMovePage {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
position?: string;
|
position?: string;
|
||||||
|
|||||||
@ -44,6 +44,8 @@ export class PageController {
|
|||||||
const page = await this.pageRepo.findById(dto.pageId, {
|
const page = await this.pageRepo.findById(dto.pageId, {
|
||||||
includeSpace: true,
|
includeSpace: true,
|
||||||
includeContent: true,
|
includeContent: true,
|
||||||
|
includeCreator: true,
|
||||||
|
includeLastUpdatedBy: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
|
|||||||
@ -46,6 +46,8 @@ export class PageRepo {
|
|||||||
includeContent?: boolean;
|
includeContent?: boolean;
|
||||||
includeYdoc?: boolean;
|
includeYdoc?: boolean;
|
||||||
includeSpace?: boolean;
|
includeSpace?: boolean;
|
||||||
|
includeCreator?: boolean;
|
||||||
|
includeLastUpdatedBy?: boolean;
|
||||||
withLock?: boolean;
|
withLock?: boolean;
|
||||||
trx?: KyselyTransaction;
|
trx?: KyselyTransaction;
|
||||||
},
|
},
|
||||||
@ -58,6 +60,14 @@ export class PageRepo {
|
|||||||
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
.$if(opts?.includeContent, (qb) => qb.select('content'))
|
||||||
.$if(opts?.includeYdoc, (qb) => qb.select('ydoc'));
|
.$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) {
|
if (opts?.includeSpace) {
|
||||||
query = query.select((eb) => this.withSpace(eb));
|
query = query.select((eb) => this.withSpace(eb));
|
||||||
}
|
}
|
||||||
@ -161,6 +171,24 @@ export class PageRepo {
|
|||||||
).as('space');
|
).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) {
|
async getPageAndDescendants(parentPageId: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.withRecursive('page_hierarchy', (db) =>
|
.withRecursive('page_hierarchy', (db) =>
|
||||||
|
|||||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@ -242,6 +242,9 @@ importers:
|
|||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.61.4
|
specifier: ^5.61.4
|
||||||
version: 5.61.4(react@18.3.1)
|
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:
|
axios:
|
||||||
specifier: ^1.7.9
|
specifier: ^1.7.9
|
||||||
version: 1.7.9
|
version: 1.7.9
|
||||||
@ -3654,6 +3657,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.7.0
|
'@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':
|
'@tiptap/extension-code-block-lowlight@2.10.3':
|
||||||
resolution: {integrity: sha512-ieRSdfDW06pmKcsh73N506/EWNJrpMrZzyuFx3YGJtfM+Os0a9hMLy2TSuNleyRsihBi5mb+zvdeqeGdaJm7Ng==}
|
resolution: {integrity: sha512-ieRSdfDW06pmKcsh73N506/EWNJrpMrZzyuFx3YGJtfM+Os0a9hMLy2TSuNleyRsihBi5mb+zvdeqeGdaJm7Ng==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -12802,6 +12811,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.10.3(@tiptap/pm@2.10.3)
|
'@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)':
|
'@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:
|
dependencies:
|
||||||
'@tiptap/core': 2.10.3(@tiptap/pm@2.10.3)
|
'@tiptap/core': 2.10.3(@tiptap/pm@2.10.3)
|
||||||
|
|||||||
Reference in New Issue
Block a user