mirror of
https://github.com/docmost/docmost.git
synced 2025-11-16 06:21:13 +10:00
Compare commits
9 Commits
1c200dbd0f
...
toggle-tab
| Author | SHA1 | Date | |
|---|---|---|---|
| e7651e4b8c | |||
| b58dc6dee9 | |||
| 69447fc375 | |||
| 858ff9da06 | |||
| 343b2976c2 | |||
| 7491224d0f | |||
| 4a0b4040ed | |||
| e3ba817723 | |||
| b0491d5da4 |
@ -29,6 +29,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"emoji-mart": "^5.6.0",
|
"emoji-mart": "^5.6.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"highlightjs-sap-abap": "^0.3.0",
|
||||||
"i18next": "^23.14.0",
|
"i18next": "^23.14.0",
|
||||||
"i18next-http-backend": "^2.6.1",
|
"i18next-http-backend": "^2.6.1",
|
||||||
"jotai": "^2.12.1",
|
"jotai": "^2.12.1",
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
export enum BillingPlan {
|
export enum BillingPlan {
|
||||||
STANDARD = "standard",
|
STANDARD = "standard",
|
||||||
|
BUSINESS = "business",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBilling {
|
export interface IBilling {
|
||||||
|
|||||||
@ -2,14 +2,18 @@ import { useAtom } from "jotai";
|
|||||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||||
import { BillingPlan } from "@/ee/billing/types/billing.types.ts";
|
import { BillingPlan } from "@/ee/billing/types/billing.types.ts";
|
||||||
|
|
||||||
export const usePlan = () => {
|
const usePlan = () => {
|
||||||
const [workspace] = useAtom(workspaceAtom);
|
const [workspace] = useAtom(workspaceAtom);
|
||||||
|
|
||||||
const isStandard =
|
const isStandard =
|
||||||
typeof workspace?.plan === "string" &&
|
typeof workspace?.plan === "string" &&
|
||||||
workspace?.plan.toLowerCase() === BillingPlan.STANDARD.toLowerCase();
|
workspace?.plan.toLowerCase() === BillingPlan.STANDARD.toLowerCase();
|
||||||
|
|
||||||
return { isStandard };
|
const isBusiness =
|
||||||
|
typeof workspace?.plan === "string" &&
|
||||||
|
workspace?.plan.toLowerCase() === BillingPlan.BUSINESS.toLowerCase();
|
||||||
|
|
||||||
|
return { isStandard, isBusiness };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default usePlan;
|
export default usePlan;
|
||||||
|
|||||||
@ -10,11 +10,13 @@ import EnforceSso from "@/ee/security/components/enforce-sso.tsx";
|
|||||||
import AllowedDomains from "@/ee/security/components/allowed-domains.tsx";
|
import AllowedDomains from "@/ee/security/components/allowed-domains.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import useLicense from "@/ee/hooks/use-license.tsx";
|
import useLicense from "@/ee/hooks/use-license.tsx";
|
||||||
|
import usePlan from "@/ee/hooks/use-plan.tsx";
|
||||||
|
|
||||||
export default function Security() {
|
export default function Security() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isAdmin } = useUserRole();
|
const { isAdmin } = useUserRole();
|
||||||
const { hasLicenseKey } = useLicense();
|
const { hasLicenseKey } = useLicense();
|
||||||
|
const { isBusiness } = usePlan();
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
return null;
|
return null;
|
||||||
@ -35,8 +37,7 @@ export default function Security() {
|
|||||||
Single sign-on (SSO)
|
Single sign-on (SSO)
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
{/*TODO: revisit when we add a second plan */}
|
{(isCloud() && isBusiness) || (!isCloud() && hasLicenseKey) ? (
|
||||||
{!isCloud() && hasLicenseKey ? (
|
|
||||||
<>
|
<>
|
||||||
<EnforceSso />
|
<EnforceSso />
|
||||||
<Divider my="lg" />
|
<Divider my="lg" />
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -17,9 +17,9 @@ import {
|
|||||||
IconColumnRemove,
|
IconColumnRemove,
|
||||||
IconRowInsertBottom,
|
IconRowInsertBottom,
|
||||||
IconRowInsertTop,
|
IconRowInsertTop,
|
||||||
IconRowRemove,
|
IconRowRemove, IconTableColumn, IconTableRow,
|
||||||
IconTrashX,
|
IconTrashX,
|
||||||
} from "@tabler/icons-react";
|
} from '@tabler/icons-react';
|
||||||
import { isCellSelection } from "@docmost/editor-ext";
|
import { isCellSelection } from "@docmost/editor-ext";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@ -50,6 +50,14 @@ export const TableMenu = React.memo(
|
|||||||
return posToDOMRect(editor.view, selection.from, selection.to);
|
return posToDOMRect(editor.view, selection.from, selection.to);
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
|
const toggleHeaderColumn = useCallback(() => {
|
||||||
|
editor.chain().focus().toggleHeaderColumn().run();
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
const toggleHeaderRow = useCallback(() => {
|
||||||
|
editor.chain().focus().toggleHeaderRow().run();
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
const addColumnLeft = useCallback(() => {
|
const addColumnLeft = useCallback(() => {
|
||||||
editor.chain().focus().addColumnBefore().run();
|
editor.chain().focus().addColumnBefore().run();
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
@ -180,6 +188,30 @@ export const TableMenu = React.memo(
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip position="top" label={t("Toggle header row")}
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
onClick={toggleHeaderRow}
|
||||||
|
variant="default"
|
||||||
|
size="lg"
|
||||||
|
aria-label={t("Toggle header row")}
|
||||||
|
>
|
||||||
|
<IconTableRow size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip position="top" label={t("Toggle header column")}
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
onClick={toggleHeaderColumn}
|
||||||
|
variant="default"
|
||||||
|
size="lg"
|
||||||
|
aria-label={t("Toggle header column")}
|
||||||
|
>
|
||||||
|
<IconTableColumn size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Delete table")}>
|
<Tooltip position="top" label={t("Delete table")}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={deleteTable}
|
onClick={deleteTable}
|
||||||
|
|||||||
@ -58,6 +58,7 @@ import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-v
|
|||||||
import EmbedView from "@/features/editor/components/embed/embed-view.tsx";
|
import EmbedView from "@/features/editor/components/embed/embed-view.tsx";
|
||||||
import plaintext from "highlight.js/lib/languages/plaintext";
|
import plaintext from "highlight.js/lib/languages/plaintext";
|
||||||
import powershell from "highlight.js/lib/languages/powershell";
|
import powershell from "highlight.js/lib/languages/powershell";
|
||||||
|
import abap from "highlightjs-sap-abap";
|
||||||
import elixir from "highlight.js/lib/languages/elixir";
|
import elixir from "highlight.js/lib/languages/elixir";
|
||||||
import erlang from "highlight.js/lib/languages/erlang";
|
import erlang from "highlight.js/lib/languages/erlang";
|
||||||
import dockerfile from "highlight.js/lib/languages/dockerfile";
|
import dockerfile from "highlight.js/lib/languages/dockerfile";
|
||||||
@ -76,7 +77,7 @@ import { CharacterCount } from "@tiptap/extension-character-count";
|
|||||||
const lowlight = createLowlight(common);
|
const lowlight = createLowlight(common);
|
||||||
lowlight.register("mermaid", plaintext);
|
lowlight.register("mermaid", plaintext);
|
||||||
lowlight.register("powershell", powershell);
|
lowlight.register("powershell", powershell);
|
||||||
lowlight.register("powershell", powershell);
|
lowlight.register("abap", abap);
|
||||||
lowlight.register("erlang", erlang);
|
lowlight.register("erlang", erlang);
|
||||||
lowlight.register("elixir", elixir);
|
lowlight.register("elixir", elixir);
|
||||||
lowlight.register("dockerfile", dockerfile);
|
lowlight.register("dockerfile", dockerfile);
|
||||||
|
|||||||
16
apps/client/src/features/share/components/share-branding.tsx
Normal file
16
apps/client/src/features/share/components/share-branding.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Affix, Button } from "@mantine/core";
|
||||||
|
|
||||||
|
export default function ShareBranding() {
|
||||||
|
return (
|
||||||
|
<Affix position={{ bottom: 20, right: 20 }}>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
component="a"
|
||||||
|
target="_blank"
|
||||||
|
href="https://docmost.com?ref=public-share"
|
||||||
|
>
|
||||||
|
Powered by Docmost
|
||||||
|
</Button>
|
||||||
|
</Affix>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -36,6 +36,7 @@ import {
|
|||||||
} from "@/features/search/components/search-control.tsx";
|
} from "@/features/search/components/search-control.tsx";
|
||||||
import { ShareSearchSpotlight } from "@/features/search/share-search-spotlight";
|
import { ShareSearchSpotlight } from "@/features/search/share-search-spotlight";
|
||||||
import { shareSearchSpotlight } from "@/features/search/constants";
|
import { shareSearchSpotlight } from "@/features/search/constants";
|
||||||
|
import ShareBranding from '@/features/share/components/share-branding.tsx';
|
||||||
|
|
||||||
const MemoizedSharedTree = React.memo(SharedTree);
|
const MemoizedSharedTree = React.memo(SharedTree);
|
||||||
|
|
||||||
@ -163,16 +164,7 @@ export default function ShareShell({
|
|||||||
<AppShell.Main>
|
<AppShell.Main>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
<Affix position={{ bottom: 20, right: 20 }}>
|
{data && shareId && !data.hasLicenseKey && <ShareBranding />}
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
component="a"
|
|
||||||
target="_blank"
|
|
||||||
href="https://docmost.com?ref=public-share"
|
|
||||||
>
|
|
||||||
Powered by Docmost
|
|
||||||
</Button>
|
|
||||||
</Affix>
|
|
||||||
</AppShell.Main>
|
</AppShell.Main>
|
||||||
|
|
||||||
<AppShell.Aside
|
<AppShell.Aside
|
||||||
|
|||||||
@ -41,6 +41,7 @@ export interface ISharedPage extends IShare {
|
|||||||
level: number;
|
level: number;
|
||||||
sharedPage: { id: string; slugId: string; title: string; icon: string };
|
sharedPage: { id: string; slugId: string; title: string; icon: string };
|
||||||
};
|
};
|
||||||
|
hasLicenseKey: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IShareForPage extends IShare {
|
export interface IShareForPage extends IShare {
|
||||||
@ -70,4 +71,5 @@ export interface IShareInfoInput {
|
|||||||
export interface ISharedPageTree {
|
export interface ISharedPageTree {
|
||||||
share: IShare;
|
share: IShare;
|
||||||
pageTree: Partial<IPage[]>;
|
pageTree: Partial<IPage[]>;
|
||||||
|
hasLicenseKey: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,9 @@ import React, { useEffect } from "react";
|
|||||||
import ReadonlyPageEditor from "@/features/editor/readonly-page-editor.tsx";
|
import ReadonlyPageEditor from "@/features/editor/readonly-page-editor.tsx";
|
||||||
import { extractPageSlugId } from "@/lib";
|
import { extractPageSlugId } from "@/lib";
|
||||||
import { Error404 } from "@/components/ui/error-404.tsx";
|
import { Error404 } from "@/components/ui/error-404.tsx";
|
||||||
|
import ShareBranding from "@/features/share/components/share-branding.tsx";
|
||||||
|
|
||||||
export default function SingleSharedPage() {
|
export default function SharedPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { pageSlug } = useParams();
|
const { pageSlug } = useParams();
|
||||||
const { shareId } = useParams();
|
const { shareId } = useParams();
|
||||||
@ -53,6 +54,8 @@ export default function SingleSharedPage() {
|
|||||||
content={data.page.content}
|
content={data.page.content}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
{data && !shareId && !data.hasLicenseKey && <ShareBranding />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
|||||||
import { Public } from '../../common/decorators/public.decorator';
|
import { Public } from '../../common/decorators/public.decorator';
|
||||||
import { ShareRepo } from '@docmost/db/repos/share/share.repo';
|
import { ShareRepo } from '@docmost/db/repos/share/share.repo';
|
||||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||||
|
import { EnvironmentService } from '../../integrations/environment/environment.service';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('shares')
|
@Controller('shares')
|
||||||
@ -39,6 +40,7 @@ export class ShareController {
|
|||||||
private readonly spaceAbility: SpaceAbilityFactory,
|
private readonly spaceAbility: SpaceAbilityFactory,
|
||||||
private readonly shareRepo: ShareRepo,
|
private readonly shareRepo: ShareRepo,
|
||||||
private readonly pageRepo: PageRepo,
|
private readonly pageRepo: PageRepo,
|
||||||
|
private readonly environmentService: EnvironmentService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ -61,7 +63,12 @@ export class ShareController {
|
|||||||
throw new BadRequestException();
|
throw new BadRequestException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.shareService.getSharedPage(dto, workspace.id);
|
return {
|
||||||
|
...(await this.shareService.getSharedPage(dto, workspace.id)),
|
||||||
|
hasLicenseKey:
|
||||||
|
Boolean(workspace.licenseKey) ||
|
||||||
|
(this.environmentService.isCloud() && workspace.plan === 'business'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@ -166,6 +173,11 @@ export class ShareController {
|
|||||||
@Body() dto: ShareIdDto,
|
@Body() dto: ShareIdDto,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.shareService.getShareTree(dto.shareId, workspace.id);
|
return {
|
||||||
|
...(await this.shareService.getShareTree(dto.shareId, workspace.id)),
|
||||||
|
hasLicenseKey:
|
||||||
|
Boolean(workspace.licenseKey) ||
|
||||||
|
(this.environmentService.isCloud() && workspace.plan === 'business'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule apps/server/src/ee updated: 96404fc121...70eb45eaec
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -257,6 +257,9 @@ importers:
|
|||||||
file-saver:
|
file-saver:
|
||||||
specifier: ^2.0.5
|
specifier: ^2.0.5
|
||||||
version: 2.0.5
|
version: 2.0.5
|
||||||
|
highlightjs-sap-abap:
|
||||||
|
specifier: ^0.3.0
|
||||||
|
version: 0.3.0
|
||||||
i18next:
|
i18next:
|
||||||
specifier: ^23.14.0
|
specifier: ^23.14.0
|
||||||
version: 23.14.0
|
version: 23.14.0
|
||||||
@ -5872,6 +5875,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==}
|
resolution: {integrity: sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
highlightjs-sap-abap@0.3.0:
|
||||||
|
resolution: {integrity: sha512-nSiUvEOCycjtFA3pHaTowrbAAk5+lciBHyoVkDsd6FTRBtW9sT2dt42o2jAKbXjZVUidtacdk+j0Y2xnd233Mw==}
|
||||||
|
|
||||||
hoist-non-react-statics@3.3.2:
|
hoist-non-react-statics@3.3.2:
|
||||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||||
|
|
||||||
@ -15436,6 +15442,8 @@ snapshots:
|
|||||||
|
|
||||||
highlight.js@11.10.0: {}
|
highlight.js@11.10.0: {}
|
||||||
|
|
||||||
|
highlightjs-sap-abap@0.3.0: {}
|
||||||
|
|
||||||
hoist-non-react-statics@3.3.2:
|
hoist-non-react-statics@3.3.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
react-is: 16.13.1
|
react-is: 16.13.1
|
||||||
|
|||||||
Reference in New Issue
Block a user