Merge branch 'main' into confluence-importer

This commit is contained in:
Philipinho
2026-07-01 23:05:52 +01:00
42 changed files with 1237 additions and 199 deletions
@@ -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];
@@ -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 {