Merge branch 'main' into feat/sharing

This commit is contained in:
Philipinho
2025-04-09 20:22:44 +01:00
10 changed files with 76 additions and 32 deletions

View File

@ -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).

View File

@ -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",

View File

@ -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);

View File

@ -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>
); );
} }

View File

@ -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"
> >

View 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;
}

View File

@ -1,6 +1,6 @@
{ {
"name": "server", "name": "server",
"version": "0.9.0", "version": "0.10.0",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,

View File

@ -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();

View File

@ -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",

View File

@ -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,