mirror of
https://github.com/docmost/docmost.git
synced 2026-08-21 23:01:37 +10:00
Merge branch 'main' into confluence-importer
This commit is contained in:
@@ -94,21 +94,21 @@ export const GridCell = memo(function GridCell({
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!property || e.button !== 0) return;
|
||||
if (!property || e.button !== 0 || isEditing) return;
|
||||
setFocusedCell({ rowId, propertyId: property.id });
|
||||
},
|
||||
[property, rowId, setFocusedCell],
|
||||
[property, rowId, setFocusedCell, isEditing],
|
||||
);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!property) return;
|
||||
if (!property || isEditing) return;
|
||||
setFocusedCell({ rowId, propertyId: property.id });
|
||||
(e.currentTarget.closest('[role="grid"]') as HTMLElement | null)?.focus({
|
||||
preventScroll: true,
|
||||
});
|
||||
},
|
||||
[property, rowId, setFocusedCell],
|
||||
[property, rowId, setFocusedCell, isEditing],
|
||||
);
|
||||
|
||||
const cellReadOnly = property
|
||||
|
||||
@@ -47,7 +47,6 @@ interface MfaSetupModalProps {
|
||||
}
|
||||
|
||||
interface SetupData {
|
||||
secret: string;
|
||||
qrCode: string;
|
||||
manualKey: string;
|
||||
}
|
||||
@@ -101,7 +100,6 @@ export function MfaSetupModal({
|
||||
const enableMutation = useMutation({
|
||||
mutationFn: (verificationCode: string) =>
|
||||
enableMfa({
|
||||
secret: setupData!.secret,
|
||||
verificationCode,
|
||||
}),
|
||||
onSuccess: (data) => {
|
||||
|
||||
@@ -31,12 +31,10 @@ export interface MfaSetupRequest {
|
||||
export interface MfaSetupResponse {
|
||||
method: string;
|
||||
qrCode: string;
|
||||
secret: string;
|
||||
manualKey: string;
|
||||
}
|
||||
|
||||
export interface MfaEnableRequest {
|
||||
secret: string;
|
||||
verificationCode: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ import { FixedToolbar } from "@/features/editor/components/fixed-toolbar/fixed-t
|
||||
import { EditorLinkMenu } from "@/features/editor/components/link/link-menu";
|
||||
import { EditorBubbleMenu } from "@/features/editor/components/bubble-menu/bubble-menu";
|
||||
import { EditorAiMenu } from "@/ee/ai/components/editor/ai-menu/ai-menu";
|
||||
import TableMenu from "@/features/editor/components/table/table-menu.tsx";
|
||||
import { TableHandlesLayer } from "@/features/editor/components/table/handle/table-handles-layer";
|
||||
import CalloutMenu from "@/features/editor/components/callout/callout-menu.tsx";
|
||||
import ColumnsMenu from "@/features/editor/components/columns/columns-menu.tsx";
|
||||
|
||||
import classes from "./template-editor.module.css";
|
||||
|
||||
@@ -85,6 +89,8 @@ export default function TemplateEditor() {
|
||||
extensions: templateExtensions,
|
||||
content: "",
|
||||
editorProps: {
|
||||
scrollThreshold: 80,
|
||||
scrollMargin: 80,
|
||||
handleDOMEvents: {
|
||||
keydown: (_view, event) => {
|
||||
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
|
||||
@@ -398,9 +404,18 @@ export default function TemplateEditor() {
|
||||
<EditorAiMenu editor={editor} />
|
||||
<EditorBubbleMenu editor={editor} templateMode />
|
||||
<EditorLinkMenu editor={editor} />
|
||||
<TableMenu editor={editor} />
|
||||
<TableHandlesLayer editor={editor} />
|
||||
<CalloutMenu editor={editor} />
|
||||
<ColumnsMenu editor={editor} />
|
||||
</>
|
||||
)}
|
||||
<div style={{ paddingBottom: "20vh" }} />
|
||||
<div
|
||||
onClick={() => {
|
||||
if (editor && !editor.isDestroyed) editor.commands.focus("end");
|
||||
}}
|
||||
style={{ paddingBottom: "20vh" }}
|
||||
/>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -29,11 +29,17 @@ import UseTemplateModal from "@/ee/template/components/use-template-modal";
|
||||
import TemplatePreviewModal from "@/ee/template/components/template-preview-modal";
|
||||
import useUserRole from "@/hooks/use-user-role";
|
||||
import CreateTemplateModal from "@/ee/template/components/create-template-modal";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom";
|
||||
|
||||
export default function TemplateList() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { isAdmin: isWorkspaceAdmin } = useUserRole();
|
||||
const workspace = useAtomValue(workspaceAtom);
|
||||
const canCreateTemplate =
|
||||
isWorkspaceAdmin ||
|
||||
workspace?.settings?.templates?.allowMemberTemplates === true;
|
||||
const [spaceFilter, setSpaceFilter] = useState<string | null>(null);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<ITemplate | null>(
|
||||
null,
|
||||
@@ -105,7 +111,7 @@ export default function TemplateList() {
|
||||
<Container size="900" pt="xl">
|
||||
<Group justify="space-between" mb="xl">
|
||||
<Title order={3}>{t("Templates")}</Title>
|
||||
{isWorkspaceAdmin && (
|
||||
{canCreateTemplate && (
|
||||
<Button
|
||||
leftSection={<IconPlus size={16} />}
|
||||
onClick={openCreateModal}
|
||||
|
||||
@@ -76,7 +76,7 @@ export function useCreateTemplateMutation() {
|
||||
onError: (error) => {
|
||||
const errorMessage = error["response"]?.data?.message;
|
||||
notifications.show({
|
||||
message: errorMessage || t("Failed to create template"),
|
||||
message: errorMessage ? t(errorMessage) : t("Failed to create template"),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -117,7 +117,7 @@ export function useUpdateTemplateMutation() {
|
||||
onError: (error) => {
|
||||
const errorMessage = error["response"]?.data?.message;
|
||||
notifications.show({
|
||||
message: errorMessage || t("Failed to update template"),
|
||||
message: errorMessage ? t(errorMessage) : t("Failed to update template"),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
|
||||
@@ -333,6 +333,22 @@ export function useDeletedPagesQuery(
|
||||
});
|
||||
}
|
||||
|
||||
function getChildrenCacheKeys(
|
||||
parentPageId: string | null,
|
||||
spaceId: string,
|
||||
): QueryKey[] {
|
||||
if (parentPageId === null) {
|
||||
return [["root-sidebar-pages", spaceId]];
|
||||
}
|
||||
return queryClient
|
||||
.getQueriesData({
|
||||
predicate: (query) =>
|
||||
query.queryKey[0] === "sidebar-pages" &&
|
||||
(query.queryKey[1] as { pageId?: string })?.pageId === parentPageId,
|
||||
})
|
||||
.map(([key]) => key);
|
||||
}
|
||||
|
||||
export function invalidateOnCreatePage(data: Partial<IPage>) {
|
||||
const newPage: Partial<IPage> = {
|
||||
creatorId: data.creatorId,
|
||||
@@ -346,35 +362,27 @@ export function invalidateOnCreatePage(data: Partial<IPage>) {
|
||||
title: data.title,
|
||||
};
|
||||
|
||||
let queryKey: QueryKey = null;
|
||||
if (data.parentPageId === null) {
|
||||
queryKey = ["root-sidebar-pages", data.spaceId];
|
||||
} else {
|
||||
queryKey = [
|
||||
"sidebar-pages",
|
||||
{ pageId: data.parentPageId, spaceId: data.spaceId },
|
||||
];
|
||||
}
|
||||
|
||||
//update all sidebar pages
|
||||
queryClient.setQueryData<InfiniteData<IPagination<Partial<IPage>>>>(
|
||||
queryKey,
|
||||
(old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
pages: old.pages.map((page, index) => {
|
||||
if (index === old.pages.length - 1) {
|
||||
return {
|
||||
...page,
|
||||
items: [...page.items, newPage],
|
||||
};
|
||||
}
|
||||
return page;
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
getChildrenCacheKeys(data.parentPageId, data.spaceId).forEach((queryKey) => {
|
||||
queryClient.setQueryData<InfiniteData<IPagination<Partial<IPage>>>>(
|
||||
queryKey,
|
||||
(old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
pages: old.pages.map((page, index) => {
|
||||
if (index === old.pages.length - 1) {
|
||||
return {
|
||||
...page,
|
||||
items: [...page.items, newPage],
|
||||
};
|
||||
}
|
||||
return page;
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
//update sidebar haschildren
|
||||
if (data.parentPageId !== null) {
|
||||
@@ -438,34 +446,30 @@ export function invalidateOnUpdatePage(
|
||||
title: string,
|
||||
icon: string,
|
||||
) {
|
||||
let queryKey: QueryKey = null;
|
||||
if (parentPageId === null) {
|
||||
queryKey = ["root-sidebar-pages", spaceId];
|
||||
} else {
|
||||
queryKey = ["sidebar-pages", { pageId: parentPageId, spaceId: spaceId }];
|
||||
}
|
||||
//update all sidebar pages
|
||||
queryClient.setQueryData<InfiniteData<IPagination<IPage>>>(
|
||||
queryKey,
|
||||
(old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
pages: old.pages.map((page) => ({
|
||||
...page,
|
||||
items: page.items.map((sidebarPage: IPage) =>
|
||||
sidebarPage.id === id
|
||||
? {
|
||||
...sidebarPage,
|
||||
...(title !== undefined ? { title } : {}),
|
||||
...(icon !== undefined ? { icon } : {}),
|
||||
}
|
||||
: sidebarPage,
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
);
|
||||
getChildrenCacheKeys(parentPageId, spaceId).forEach((queryKey) => {
|
||||
queryClient.setQueryData<InfiniteData<IPagination<IPage>>>(
|
||||
queryKey,
|
||||
(old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
pages: old.pages.map((page) => ({
|
||||
...page,
|
||||
items: page.items.map((sidebarPage: IPage) =>
|
||||
sidebarPage.id === id
|
||||
? {
|
||||
...sidebarPage,
|
||||
...(title !== undefined ? { title } : {}),
|
||||
...(icon !== undefined ? { icon } : {}),
|
||||
}
|
||||
: sidebarPage,
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
//update recent changes
|
||||
queryClient.invalidateQueries({
|
||||
@@ -481,24 +485,21 @@ export function updateCacheOnMovePage(
|
||||
pageData: Partial<IPage>,
|
||||
) {
|
||||
// Remove page from old parent's cache
|
||||
const oldQueryKey =
|
||||
oldParentId === null
|
||||
? ["root-sidebar-pages", spaceId]
|
||||
: ["sidebar-pages", { pageId: oldParentId, spaceId }];
|
||||
|
||||
queryClient.setQueryData<InfiniteData<IPagination<IPage>>>(
|
||||
oldQueryKey,
|
||||
(old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
pages: old.pages.map((page) => ({
|
||||
...page,
|
||||
items: page.items.filter((item) => item.id !== pageId),
|
||||
})),
|
||||
};
|
||||
},
|
||||
);
|
||||
getChildrenCacheKeys(oldParentId, spaceId).forEach((oldQueryKey) => {
|
||||
queryClient.setQueryData<InfiniteData<IPagination<IPage>>>(
|
||||
oldQueryKey,
|
||||
(old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
pages: old.pages.map((page) => ({
|
||||
...page,
|
||||
items: page.items.filter((item) => item.id !== pageId),
|
||||
})),
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Update old parent's hasChildren flag if it has no more children
|
||||
if (oldParentId !== null) {
|
||||
@@ -540,36 +541,33 @@ export function updateCacheOnMovePage(
|
||||
}
|
||||
|
||||
// Add page to new parent's cache
|
||||
const newQueryKey =
|
||||
newParentId === null
|
||||
? ["root-sidebar-pages", spaceId]
|
||||
: ["sidebar-pages", { pageId: newParentId, spaceId }];
|
||||
getChildrenCacheKeys(newParentId, spaceId).forEach((newQueryKey) => {
|
||||
queryClient.setQueryData<InfiniteData<IPagination<Partial<IPage>>>>(
|
||||
newQueryKey,
|
||||
(old) => {
|
||||
if (!old) return old;
|
||||
|
||||
queryClient.setQueryData<InfiniteData<IPagination<Partial<IPage>>>>(
|
||||
newQueryKey,
|
||||
(old) => {
|
||||
if (!old) return old;
|
||||
// Check if page already exists in new location
|
||||
const exists = old.pages.some((page) =>
|
||||
page.items.some((item) => item.id === pageId),
|
||||
);
|
||||
if (exists) return old;
|
||||
|
||||
// Check if page already exists in new location
|
||||
const exists = old.pages.some((page) =>
|
||||
page.items.some((item) => item.id === pageId),
|
||||
);
|
||||
if (exists) return old;
|
||||
|
||||
return {
|
||||
...old,
|
||||
pages: old.pages.map((page, index) => {
|
||||
if (index === old.pages.length - 1) {
|
||||
return {
|
||||
...page,
|
||||
items: [...page.items, pageData],
|
||||
};
|
||||
}
|
||||
return page;
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
return {
|
||||
...old,
|
||||
pages: old.pages.map((page, index) => {
|
||||
if (index === old.pages.length - 1) {
|
||||
return {
|
||||
...page,
|
||||
items: [...page.items, pageData],
|
||||
};
|
||||
}
|
||||
return page;
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Update new parent's hasChildren flag
|
||||
if (newParentId !== null) {
|
||||
|
||||
@@ -203,7 +203,14 @@ export function mergeRootTrees(
|
||||
prevRoots: SpaceTreeNode[],
|
||||
incomingRoots: SpaceTreeNode[],
|
||||
): SpaceTreeNode[] {
|
||||
const seen = new Set(prevRoots.map((r) => r.id));
|
||||
const seen = new Set<string>();
|
||||
const collect = (nodes: SpaceTreeNode[]) => {
|
||||
for (const node of nodes) {
|
||||
seen.add(node.id);
|
||||
if (node.children?.length) collect(node.children);
|
||||
}
|
||||
};
|
||||
collect(prevRoots);
|
||||
|
||||
// add new roots that were not present before
|
||||
const merged = [...prevRoots];
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { Group, Text, SegmentedControl } from "@mantine/core";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { getApiErrorMessage } from "@/lib/api-error.ts";
|
||||
import { PageEditMode } from "@/features/user/types/user.types.ts";
|
||||
|
||||
export default function WorkspaceDefaultPageEditMode() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">{t("Default page edit mode")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t(
|
||||
"Choose the page edit mode new members start with. Existing members are not affected.",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<DefaultPageEditModeControl />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
function DefaultPageEditModeControl() {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const defaultPageEditMode =
|
||||
workspace?.settings?.defaultPageEditMode ?? PageEditMode.Edit;
|
||||
const [value, setValue] = useState<string>(defaultPageEditMode);
|
||||
|
||||
const handleChange = async (newValue: string) => {
|
||||
const prevValue = value;
|
||||
setValue(newValue);
|
||||
try {
|
||||
const updatedWorkspace = await updateWorkspace({
|
||||
defaultPageEditMode: newValue,
|
||||
});
|
||||
setWorkspace(updatedWorkspace);
|
||||
} catch (err) {
|
||||
setValue(prevValue);
|
||||
notifications.show({
|
||||
message: getApiErrorMessage(err, t("Failed to update setting")),
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultPageEditMode !== value) {
|
||||
setValue(defaultPageEditMode);
|
||||
}
|
||||
}, [defaultPageEditMode, value]);
|
||||
|
||||
return (
|
||||
<SegmentedControl
|
||||
aria-label={t("Default page edit mode")}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
data={[
|
||||
{ label: t("Edit"), value: PageEditMode.Edit },
|
||||
{ label: t("Read"), value: PageEditMode.Read },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -29,6 +29,7 @@ export interface IWorkspace {
|
||||
restrictApiToAdmins?: boolean;
|
||||
allowMemberTemplates?: boolean;
|
||||
allowPersonalSpaces?: boolean;
|
||||
defaultPageEditMode?: string;
|
||||
isScimEnabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -38,6 +39,7 @@ export interface IWorkspaceSettings {
|
||||
api?: IWorkspaceApiSettings;
|
||||
templates?: IWorkspaceTemplateSettings;
|
||||
spaces?: IWorkspaceSpaceSettings;
|
||||
defaultPageEditMode?: string;
|
||||
}
|
||||
|
||||
export interface IWorkspaceApiSettings {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Helmet } from "react-helmet-async";
|
||||
import ManageHostname from "@/ee/components/manage-hostname.tsx";
|
||||
import { Divider } from "@mantine/core";
|
||||
import AllowMemberTemplates from "@/ee/security/components/allow-member-templates.tsx";
|
||||
import WorkspaceDefaultPageEditMode from "@/features/workspace/components/settings/components/workspace-default-page-edit-mode.tsx";
|
||||
import PersonalSpacesSetting from "@/ee/personal-space/components/personal-spaces-setting.tsx";
|
||||
|
||||
export default function WorkspaceSettings() {
|
||||
@@ -32,6 +33,9 @@ export default function WorkspaceSettings() {
|
||||
<ManageHostname />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider my="md" />
|
||||
<WorkspaceDefaultPageEditMode />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
CSSVariablesResolver,
|
||||
MantineColorsTuple,
|
||||
Tabs,
|
||||
Tooltip,
|
||||
v8CssVariablesResolver,
|
||||
} from "@mantine/core";
|
||||
|
||||
@@ -38,7 +39,13 @@ export const theme = createTheme({
|
||||
blue,
|
||||
red,
|
||||
},
|
||||
defaultRadius: 'sm',
|
||||
components: {
|
||||
Tooltip: Tooltip.extend({
|
||||
defaultProps: {
|
||||
events: { hover: true, focus: true, touch: false },
|
||||
},
|
||||
}),
|
||||
// Size badges to their content; fit-content collapses inside table cells.
|
||||
Badge: Badge.extend({
|
||||
styles: (_theme, props) => ({
|
||||
|
||||
Reference in New Issue
Block a user