mirror of
https://github.com/docmost/docmost.git
synced 2025-11-17 16:51:10 +10:00
Merge branch 'main' into feat/sharing
This commit is contained in:
@ -9,9 +9,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Docmost is currently in **beta**. We value your feedback as we progress towards a stable release.
|
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
To get started with Docmost, please refer to our [documentation](https://docmost.com/docs).
|
To get started with Docmost, please refer to our [documentation](https://docmost.com/docs).
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.9.0",
|
"version": "0.10.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
|
|||||||
@ -209,6 +209,7 @@ export default function PageEditor({
|
|||||||
queryClient.setQueryData(["pages", slugId], {
|
queryClient.setQueryData(["pages", slugId], {
|
||||||
...pageData,
|
...pageData,
|
||||||
content: newContent,
|
content: newContent,
|
||||||
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|||||||
@ -17,6 +17,13 @@ import {
|
|||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useSpaceAbility } from "@/features/space/permissions/use-space-ability.ts";
|
||||||
|
import { useSpaceQuery } from "@/features/space/queries/space-query.ts";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
SpaceCaslAction,
|
||||||
|
SpaceCaslSubject,
|
||||||
|
} from "@/features/space/permissions/permissions.type.ts";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@ -36,6 +43,11 @@ function HistoryList({ pageId }: Props) {
|
|||||||
const [mainEditorTitle] = useAtom(titleEditorAtom);
|
const [mainEditorTitle] = useAtom(titleEditorAtom);
|
||||||
const [, setHistoryModalOpen] = useAtom(historyAtoms);
|
const [, setHistoryModalOpen] = useAtom(historyAtoms);
|
||||||
|
|
||||||
|
const { spaceSlug } = useParams();
|
||||||
|
const { data: space } = useSpaceQuery(spaceSlug);
|
||||||
|
const spaceRules = space?.membership?.permissions;
|
||||||
|
const spaceAbility = useSpaceAbility(spaceRules);
|
||||||
|
|
||||||
const confirmModal = () =>
|
const confirmModal = () =>
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: t("Please confirm your action"),
|
title: t("Please confirm your action"),
|
||||||
@ -103,20 +115,26 @@ function HistoryList({ pageId }: Props) {
|
|||||||
))}
|
))}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
<Divider />
|
{spaceAbility.cannot(
|
||||||
|
SpaceCaslAction.Manage,
|
||||||
<Group p="xs" wrap="nowrap">
|
SpaceCaslSubject.Page,
|
||||||
<Button size="compact-md" onClick={confirmModal}>
|
) ? null : (
|
||||||
{t("Restore")}
|
<>
|
||||||
</Button>
|
<Divider />
|
||||||
<Button
|
<Group p="xs" wrap="nowrap">
|
||||||
variant="default"
|
<Button size="compact-md" onClick={confirmModal}>
|
||||||
size="compact-md"
|
{t("Restore")}
|
||||||
onClick={() => setHistoryModalOpen(false)}
|
</Button>
|
||||||
>
|
<Button
|
||||||
{t("Cancel")}
|
variant="default"
|
||||||
</Button>
|
size="compact-md"
|
||||||
</Group>
|
onClick={() => setHistoryModalOpen(false)}
|
||||||
|
>
|
||||||
|
{t("Cancel")}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
IconTrash,
|
IconTrash,
|
||||||
IconWifiOff,
|
IconWifiOff,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import useToggleAside from "@/hooks/use-toggle-aside.tsx";
|
import useToggleAside from "@/hooks/use-toggle-aside.tsx";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts";
|
import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts";
|
||||||
@ -34,6 +34,7 @@ import {
|
|||||||
} from "@/features/editor/atoms/editor-atoms.ts";
|
} from "@/features/editor/atoms/editor-atoms.ts";
|
||||||
import { formattedDate, timeAgo } from "@/lib/time.ts";
|
import { formattedDate, timeAgo } from "@/lib/time.ts";
|
||||||
import MovePageModal from "@/features/page/components/move-page-modal.tsx";
|
import MovePageModal from "@/features/page/components/move-page-modal.tsx";
|
||||||
|
import { useTimeAgo } from "@/hooks/use-time-ago.tsx";
|
||||||
|
|
||||||
interface PageHeaderMenuProps {
|
interface PageHeaderMenuProps {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@ -102,6 +103,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
|||||||
{ open: openMovePageModal, close: closeMoveSpaceModal },
|
{ open: openMovePageModal, close: closeMoveSpaceModal },
|
||||||
] = useDisclosure(false);
|
] = useDisclosure(false);
|
||||||
const [pageEditor] = useAtom(pageEditorAtom);
|
const [pageEditor] = useAtom(pageEditorAtom);
|
||||||
|
const pageUpdatedAt = useTimeAgo(page.updatedAt);
|
||||||
|
|
||||||
const handleCopyLink = () => {
|
const handleCopyLink = () => {
|
||||||
const pageUrl =
|
const pageUrl =
|
||||||
@ -208,7 +210,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
label={t("Edited by {{name}} {{time}}", {
|
label={t("Edited by {{name}} {{time}}", {
|
||||||
name: page.lastUpdatedBy.name,
|
name: page.lastUpdatedBy.name,
|
||||||
time: timeAgo(page.updatedAt),
|
time: pageUpdatedAt,
|
||||||
})}
|
})}
|
||||||
position="left-start"
|
position="left-start"
|
||||||
>
|
>
|
||||||
|
|||||||
16
apps/client/src/hooks/use-time-ago.tsx
Normal file
16
apps/client/src/hooks/use-time-ago.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { timeAgo } from "@/lib/time.ts";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function useTimeAgo(date: Date | string) {
|
||||||
|
const [value, setValue] = useState(() => timeAgo(new Date(date)));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setValue(timeAgo(new Date(date)));
|
||||||
|
}, 5 * 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [date]);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.9.0",
|
"version": "0.10.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@ -47,13 +47,18 @@ export class AuthService {
|
|||||||
includePassword: true,
|
includePassword: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const errorMessage = 'email or password does not match';
|
||||||
|
if (!user || user?.deletedAt) {
|
||||||
|
throw new UnauthorizedException(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
const isPasswordMatch = await comparePasswordHash(
|
const isPasswordMatch = await comparePasswordHash(
|
||||||
loginDto.password,
|
loginDto.password,
|
||||||
user?.password,
|
user.password,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!user || !isPasswordMatch || user.deletedAt) {
|
if (!isPasswordMatch) {
|
||||||
throw new UnauthorizedException('email or password does not match');
|
throw new UnauthorizedException(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.lastLoginAt = new Date();
|
user.lastLoginAt = new Date();
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "docmost",
|
"name": "docmost",
|
||||||
"homepage": "https://docmost.com",
|
"homepage": "https://docmost.com",
|
||||||
"version": "0.9.0",
|
"version": "0.10.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nx run-many -t build",
|
"build": "nx run-many -t build",
|
||||||
|
|||||||
@ -78,10 +78,13 @@ export const Details = Node.create<DetailsOptions>({
|
|||||||
dom.setAttribute("data-type", this.name);
|
dom.setAttribute("data-type", this.name);
|
||||||
btn.setAttribute("data-type", `${this.name}Button`);
|
btn.setAttribute("data-type", `${this.name}Button`);
|
||||||
div.setAttribute("data-type", `${this.name}Container`);
|
div.setAttribute("data-type", `${this.name}Container`);
|
||||||
if (node.attrs.open) {
|
|
||||||
dom.setAttribute("open", "true");
|
if (editor.isEditable) {
|
||||||
} else {
|
if (node.attrs.open) {
|
||||||
dom.removeAttribute("open");
|
dom.setAttribute("open", "true");
|
||||||
|
} else {
|
||||||
|
dom.removeAttribute("open");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ico.innerHTML = icon("right-line");
|
ico.innerHTML = icon("right-line");
|
||||||
@ -111,6 +114,7 @@ export const Details = Node.create<DetailsOptions>({
|
|||||||
if (updatedNode.type !== this.type) {
|
if (updatedNode.type !== this.type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!editor.isEditable) return true;
|
||||||
if (updatedNode.attrs.open) {
|
if (updatedNode.attrs.open) {
|
||||||
dom.setAttribute("open", "true");
|
dom.setAttribute("open", "true");
|
||||||
} else {
|
} else {
|
||||||
@ -132,9 +136,10 @@ export const Details = Node.create<DetailsOptions>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const slice = state.doc.slice(range.start, range.end);
|
const slice = state.doc.slice(range.start, range.end);
|
||||||
|
|
||||||
if(slice.content.firstChild.type.name === "detailsSummary") return false;
|
if (slice.content.firstChild.type.name === "detailsSummary")
|
||||||
|
return false;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!state.schema.nodes.detailsContent.contentMatch.matchFragment(
|
!state.schema.nodes.detailsContent.contentMatch.matchFragment(
|
||||||
slice.content,
|
slice.content,
|
||||||
|
|||||||
Reference in New Issue
Block a user