mirror of
https://github.com/docmost/docmost.git
synced 2025-11-12 15:32:38 +10:00
Compare commits
2 Commits
1c200dbd0f
...
e3ba817723
| Author | SHA1 | Date | |
|---|---|---|---|
| e3ba817723 | |||
| b0491d5da4 |
@ -1,4 +1,4 @@
|
|||||||
import { Button, Group } from "@mantine/core";
|
import { Button, Group, Tooltip } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type CommentActionsProps = {
|
type CommentActionsProps = {
|
||||||
@ -15,7 +15,7 @@ function CommentActions({
|
|||||||
isCommentEditor,
|
isCommentEditor,
|
||||||
}: CommentActionsProps) {
|
}: CommentActionsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group justify="flex-end" pt="sm" wrap="nowrap">
|
<Group justify="flex-end" pt="sm" wrap="nowrap">
|
||||||
{isCommentEditor && (
|
{isCommentEditor && (
|
||||||
|
|||||||
@ -124,6 +124,7 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) {
|
|||||||
|
|
||||||
<CommentEditor
|
<CommentEditor
|
||||||
onUpdate={handleCommentEditorChange}
|
onUpdate={handleCommentEditorChange}
|
||||||
|
onSave={handleAddComment}
|
||||||
placeholder={t("Write a comment")}
|
placeholder={t("Write a comment")}
|
||||||
editable={true}
|
editable={true}
|
||||||
autofocus={true}
|
autofocus={true}
|
||||||
|
|||||||
@ -8,10 +8,12 @@ import { useFocusWithin } from "@mantine/hooks";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { forwardRef, useEffect, useImperativeHandle } from "react";
|
import { forwardRef, useEffect, useImperativeHandle } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import EmojiCommand from "@/features/editor/extensions/emoji-command";
|
||||||
|
|
||||||
interface CommentEditorProps {
|
interface CommentEditorProps {
|
||||||
defaultContent?: any;
|
defaultContent?: any;
|
||||||
onUpdate?: any;
|
onUpdate?: any;
|
||||||
|
onSave?: any;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
@ -22,6 +24,7 @@ const CommentEditor = forwardRef(
|
|||||||
{
|
{
|
||||||
defaultContent,
|
defaultContent,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
|
onSave,
|
||||||
editable,
|
editable,
|
||||||
placeholder,
|
placeholder,
|
||||||
autofocus,
|
autofocus,
|
||||||
@ -42,7 +45,35 @@ const CommentEditor = forwardRef(
|
|||||||
}),
|
}),
|
||||||
Underline,
|
Underline,
|
||||||
Link,
|
Link,
|
||||||
|
EmojiCommand,
|
||||||
],
|
],
|
||||||
|
editorProps: {
|
||||||
|
handleDOMEvents: {
|
||||||
|
keydown: (_view, event) => {
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
"ArrowUp",
|
||||||
|
"ArrowDown",
|
||||||
|
"ArrowLeft",
|
||||||
|
"ArrowRight",
|
||||||
|
"Enter",
|
||||||
|
].includes(event.key)
|
||||||
|
) {
|
||||||
|
const emojiCommand = document.querySelector("#emoji-command");
|
||||||
|
if (emojiCommand) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (onSave) onSave();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
onUpdate({ editor }) {
|
onUpdate({ editor }) {
|
||||||
if (onUpdate) onUpdate(editor.getJSON());
|
if (onUpdate) onUpdate(editor.getJSON());
|
||||||
},
|
},
|
||||||
|
|||||||
@ -140,6 +140,7 @@ function CommentListItem({ comment, pageId }: CommentListItemProps) {
|
|||||||
defaultContent={content}
|
defaultContent={content}
|
||||||
editable={true}
|
editable={true}
|
||||||
onUpdate={(newContent: any) => setContent(newContent)}
|
onUpdate={(newContent: any) => setContent(newContent)}
|
||||||
|
onSave={handleUpdateComment}
|
||||||
autofocus={true}
|
autofocus={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -151,6 +151,7 @@ const CommentEditorWithActions = ({ commentId, onSave, isLoading }) => {
|
|||||||
<CommentEditor
|
<CommentEditor
|
||||||
ref={commentEditorRef}
|
ref={commentEditorRef}
|
||||||
onUpdate={setContent}
|
onUpdate={setContent}
|
||||||
|
onSave={handleSave}
|
||||||
editable={true}
|
editable={true}
|
||||||
/>
|
/>
|
||||||
{focused && <CommentActions onSave={handleSave} isLoading={isLoading} />}
|
{focused && <CommentActions onSave={handleSave} isLoading={isLoading} />}
|
||||||
|
|||||||
@ -17,17 +17,19 @@
|
|||||||
.commentEditor {
|
.commentEditor {
|
||||||
|
|
||||||
.focused {
|
.focused {
|
||||||
|
border-radius: var(--mantine-radius-sm);
|
||||||
box-shadow: 0 0 0 2px var(--mantine-color-blue-3);
|
box-shadow: 0 0 0 2px var(--mantine-color-blue-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror :global(.ProseMirror){
|
.ProseMirror :global(.ProseMirror){
|
||||||
|
border-radius: var(--mantine-radius-sm);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
max-height: 20vh;
|
max-height: 20vh;
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
margin-top: 2px;
|
margin-top: 10px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
overflow: hidden auto;
|
overflow: hidden auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const renderEmojiItems = () => {
|
|||||||
showOnCreate: true,
|
showOnCreate: true,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
trigger: "manual",
|
trigger: "manual",
|
||||||
placement: "bottom-start",
|
placement: "bottom",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onStart: (props: {
|
onStart: (props: {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
@ -18,7 +19,7 @@ import {
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import classes from "./mention.module.css";
|
import classes from "./mention.module.css";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
import { IconFileDescription } from "@tabler/icons-react";
|
import { IconFileDescription, IconPlus } from "@tabler/icons-react";
|
||||||
import { useSpaceQuery } from "@/features/space/queries/space-query.ts";
|
import { useSpaceQuery } from "@/features/space/queries/space-query.ts";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { v7 as uuid7 } from "uuid";
|
import { v7 as uuid7 } from "uuid";
|
||||||
@ -28,14 +29,28 @@ import {
|
|||||||
MentionListProps,
|
MentionListProps,
|
||||||
MentionSuggestionItem,
|
MentionSuggestionItem,
|
||||||
} from "@/features/editor/components/mention/mention.type.ts";
|
} from "@/features/editor/components/mention/mention.type.ts";
|
||||||
|
import { IPage } from "@/features/page/types/page.types";
|
||||||
|
import { useCreatePageMutation, usePageQuery } from "@/features/page/queries/page-query";
|
||||||
|
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom";
|
||||||
|
import { SimpleTree } from "react-arborist";
|
||||||
|
import { SpaceTreeNode } from "@/features/page/tree/types";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useQueryEmit } from "@/features/websocket/use-query-emit";
|
||||||
|
import { extractPageSlugId } from "@/lib";
|
||||||
|
|
||||||
const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||||
const [selectedIndex, setSelectedIndex] = useState(1);
|
const [selectedIndex, setSelectedIndex] = useState(1);
|
||||||
const viewportRef = useRef<HTMLDivElement>(null);
|
const viewportRef = useRef<HTMLDivElement>(null);
|
||||||
const { spaceSlug } = useParams();
|
const { pageSlug, spaceSlug } = useParams();
|
||||||
|
const { data: page } = usePageQuery({ pageId: extractPageSlugId(pageSlug) });
|
||||||
const { data: space } = useSpaceQuery(spaceSlug);
|
const { data: space } = useSpaceQuery(spaceSlug);
|
||||||
const [currentUser] = useAtom(currentUserAtom);
|
const [currentUser] = useAtom(currentUserAtom);
|
||||||
const [renderItems, setRenderItems] = useState<MentionSuggestionItem[]>([]);
|
const [renderItems, setRenderItems] = useState<MentionSuggestionItem[]>([]);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [data, setData] = useAtom(treeDataAtom);
|
||||||
|
const tree = useMemo(() => new SimpleTree<SpaceTreeNode>(data), [data]);
|
||||||
|
const createPageMutation = useCreatePageMutation();
|
||||||
|
const emit = useQueryEmit();
|
||||||
|
|
||||||
const { data: suggestion, isLoading } = useSearchSuggestionsQuery({
|
const { data: suggestion, isLoading } = useSearchSuggestionsQuery({
|
||||||
query: props.query,
|
query: props.query,
|
||||||
@ -45,12 +60,23 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
|||||||
limit: 10,
|
limit: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createPageItem = (label: string) : MentionSuggestionItem => {
|
||||||
|
return {
|
||||||
|
id: null,
|
||||||
|
label: label,
|
||||||
|
entityType: "page",
|
||||||
|
entityId: null,
|
||||||
|
slugId: null,
|
||||||
|
icon: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (suggestion && !isLoading) {
|
if (suggestion && !isLoading) {
|
||||||
let items: MentionSuggestionItem[] = [];
|
let items: MentionSuggestionItem[] = [];
|
||||||
|
|
||||||
if (suggestion?.users?.length > 0) {
|
if (suggestion?.users?.length > 0) {
|
||||||
items.push({ entityType: "header", label: "Users" });
|
items.push({ entityType: "header", label: t("Users") });
|
||||||
|
|
||||||
items = items.concat(
|
items = items.concat(
|
||||||
suggestion.users.map((user) => ({
|
suggestion.users.map((user) => ({
|
||||||
@ -64,7 +90,7 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (suggestion?.pages?.length > 0) {
|
if (suggestion?.pages?.length > 0) {
|
||||||
items.push({ entityType: "header", label: "Pages" });
|
items.push({ entityType: "header", label: t("Pages") });
|
||||||
items = items.concat(
|
items = items.concat(
|
||||||
suggestion.pages.map((page) => ({
|
suggestion.pages.map((page) => ({
|
||||||
id: uuid7(),
|
id: uuid7(),
|
||||||
@ -76,6 +102,7 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
items.push(createPageItem(props.query));
|
||||||
|
|
||||||
setRenderItems(items);
|
setRenderItems(items);
|
||||||
// update editor storage
|
// update editor storage
|
||||||
@ -96,7 +123,7 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
|||||||
creatorId: currentUser?.user.id,
|
creatorId: currentUser?.user.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (item.entityType === "page") {
|
if (item.entityType === "page" && item.id!==null) {
|
||||||
props.command({
|
props.command({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
label: item.label || "Untitled",
|
label: item.label || "Untitled",
|
||||||
@ -106,6 +133,9 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
|||||||
creatorId: currentUser?.user.id,
|
creatorId: currentUser?.user.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (item.entityType === "page" && item.id===null) {
|
||||||
|
createPage(item.label);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[renderItems],
|
[renderItems],
|
||||||
@ -167,6 +197,58 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const createPage = async (title: string) => {
|
||||||
|
const payload: { spaceId: string; parentPageId?: string; title: string } = {
|
||||||
|
spaceId: space.id,
|
||||||
|
parentPageId: page.id || null,
|
||||||
|
title: title
|
||||||
|
};
|
||||||
|
|
||||||
|
let createdPage: IPage;
|
||||||
|
try {
|
||||||
|
createdPage = await createPageMutation.mutateAsync(payload);
|
||||||
|
const parentId = page.id || null;
|
||||||
|
const data = {
|
||||||
|
id: createdPage.id,
|
||||||
|
slugId: createdPage.slugId,
|
||||||
|
name: createdPage.title,
|
||||||
|
position: createdPage.position,
|
||||||
|
spaceId: createdPage.spaceId,
|
||||||
|
parentPageId: createdPage.parentPageId,
|
||||||
|
children: [],
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const lastIndex = tree.data.length;
|
||||||
|
|
||||||
|
tree.create({ parentId, index: lastIndex, data });
|
||||||
|
setData(tree.data);
|
||||||
|
|
||||||
|
props.command({
|
||||||
|
id: uuid7(),
|
||||||
|
label: createdPage.title || "Untitled",
|
||||||
|
entityType: "page",
|
||||||
|
entityId: createdPage.id,
|
||||||
|
slugId: createdPage.slugId,
|
||||||
|
creatorId: currentUser?.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
emit({
|
||||||
|
operation: "addTreeNode",
|
||||||
|
spaceId: space.id,
|
||||||
|
payload: {
|
||||||
|
parentId,
|
||||||
|
index: lastIndex,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error("Failed to create page");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if no results and enter what to do?
|
// if no results and enter what to do?
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -178,7 +260,7 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
|||||||
if (renderItems.length === 0) {
|
if (renderItems.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Paper shadow="md" p="xs" withBorder>
|
<Paper shadow="md" p="xs" withBorder>
|
||||||
No results
|
{ t("No results") }
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -248,14 +330,14 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
|||||||
color="gray"
|
color="gray"
|
||||||
size={18}
|
size={18}
|
||||||
>
|
>
|
||||||
<IconFileDescription size={18} />
|
{ (item.id) ? <IconFileDescription size={18} /> : <IconPlus size={18} /> }
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
)}
|
)}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<Text size="sm" fw={500}>
|
<Text size="sm" fw={500}>
|
||||||
{item.label}
|
{ (item.id) ? item.label : t("Create page") + ': ' + item.label }
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
Reference in New Issue
Block a user