- {breadcrumbNodes ? (
-
{getBreadcrumbItems()}
- ) : (
- <>>
+
+ {breadcrumbNodes && (
+
+ {getBreadcrumbItems()}
+
)}
);
diff --git a/apps/client/src/components/layouts/dashboard/header.tsx b/apps/client/src/features/page/components/header/page-header-menu.tsx
similarity index 68%
rename from apps/client/src/components/layouts/dashboard/header.tsx
rename to apps/client/src/features/page/components/header/page-header-menu.tsx
index 2c7721b..65cacf8 100644
--- a/apps/client/src/components/layouts/dashboard/header.tsx
+++ b/apps/client/src/features/page/components/header/page-header-menu.tsx
@@ -4,6 +4,7 @@ import {
IconHistory,
IconLink,
IconMessage,
+ IconTrash,
} from "@tabler/icons-react";
import React from "react";
import useToggleAside from "@/hooks/use-toggle-aside.tsx";
@@ -12,20 +13,18 @@ import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts";
import { useClipboard } from "@mantine/hooks";
import { useParams } from "react-router-dom";
import { usePageQuery } from "@/features/page/queries/page-query.ts";
-import { buildPageSlug } from "@/features/page/page.utils.ts";
+import { buildPageUrl } from "@/features/page/page.utils.ts";
import { notifications } from "@mantine/notifications";
+import { getAppUrl } from "@/lib/config.ts";
+import { extractPageSlugId } from "@/lib";
+import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
+import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
-export default function Header() {
+export default function PageHeaderMenu() {
const toggleAside = useToggleAside();
return (
<>
- {/*
-
- */}
-
{
- const pageLink =
- window.location.host + buildPageSlug(page.slugId, page.title);
- clipboard.copy(pageLink);
+ const pageUrl =
+ getAppUrl() + buildPageUrl(spaceSlug, page.slugId, page.title);
+
+ clipboard.copy(pageUrl);
notifications.show({ message: "Link copied" });
};
@@ -58,6 +62,10 @@ function PageActionMenu() {
setHistoryModalOpen(true);
};
+ const handleDeletePage = () => {
+ openDeleteModal({ onConfirm: () => tree?.delete(page.id) });
+ };
+
return (
);
diff --git a/apps/client/src/features/page/components/header/page-header.module.css b/apps/client/src/features/page/components/header/page-header.module.css
new file mode 100644
index 0000000..4b3b46d
--- /dev/null
+++ b/apps/client/src/features/page/components/header/page-header.module.css
@@ -0,0 +1,11 @@
+.header {
+ height: 45px;
+ background-color: var(--mantine-color-body);
+ padding-left: var(--mantine-spacing-md);
+ padding-right: var(--mantine-spacing-md);
+ position: fixed;
+ z-index: 99;
+ top: var(--app-shell-header-offset, 0rem);
+ inset-inline-start: var(--app-shell-navbar-offset, 0rem);
+ inset-inline-end: var(--app-shell-aside-offset, 0rem);
+}
diff --git a/apps/client/src/features/page/components/header/page-header.tsx b/apps/client/src/features/page/components/header/page-header.tsx
new file mode 100644
index 0000000..d802e8e
--- /dev/null
+++ b/apps/client/src/features/page/components/header/page-header.tsx
@@ -0,0 +1,18 @@
+import classes from "./page-header.module.css";
+import PageHeaderMenu from "@/features/page/components/header/page-header-menu.tsx";
+import { Group } from "@mantine/core";
+import Breadcrumb from "@/features/page/components/breadcrumbs/breadcrumb.tsx";
+
+export default function PageHeader() {
+ return (
+
+ );
+}
diff --git a/apps/client/src/features/page/hooks/use-delete-page-modal.tsx b/apps/client/src/features/page/hooks/use-delete-page-modal.tsx
new file mode 100644
index 0000000..e10e3ba
--- /dev/null
+++ b/apps/client/src/features/page/hooks/use-delete-page-modal.tsx
@@ -0,0 +1,26 @@
+import { modals } from "@mantine/modals";
+import { Text } from "@mantine/core";
+
+type UseDeleteModalProps = {
+ onConfirm: () => void;
+};
+
+export function useDeletePageModal() {
+ const openDeleteModal = ({ onConfirm }: UseDeleteModalProps) => {
+ modals.openConfirmModal({
+ title: "Are you sure you want to delete this page?",
+ children: (
+
+ Are you sure you want to delete this page? This will delete its
+ children and page history. This action is irreversible.
+
+ ),
+ centered: true,
+ labels: { confirm: "Delete", cancel: "Cancel" },
+ confirmProps: { color: "red" },
+ onConfirm,
+ });
+ };
+
+ return { openDeleteModal } as const;
+}
diff --git a/apps/client/src/features/page/page.utils.ts b/apps/client/src/features/page/page.utils.ts
index 382bf4d..fc8de53 100644
--- a/apps/client/src/features/page/page.utils.ts
+++ b/apps/client/src/features/page/page.utils.ts
@@ -1,15 +1,23 @@
import slugify from "@sindresorhus/slugify";
-export const buildPageSlug = (
- pageShortId: string,
- pageTitle?: string,
-): string => {
- const titleSlug = slugify(pageTitle?.substring(0, 99) || "untitled", {
+const buildPageSlug = (pageSlugId: string, pageTitle?: string): string => {
+ const titleSlug = slugify(pageTitle?.substring(0, 70) || "untitled", {
customReplacements: [
["♥", ""],
["🦄", ""],
],
});
- return `/p/${pageShortId}/${titleSlug}`;
+ return `p/${titleSlug}-${pageSlugId}`;
+};
+
+export const buildPageUrl = (
+ spaceName: string,
+ pageSlugId: string,
+ pageTitle?: string,
+): string => {
+ if (spaceName === undefined) {
+ return `/${buildPageSlug(pageSlugId, pageTitle)}`;
+ }
+ return `/s/${spaceName}/${buildPageSlug(pageSlugId, pageTitle)}`;
};
diff --git a/apps/client/src/features/page/queries/page-query.ts b/apps/client/src/features/page/queries/page-query.ts
index fec5d9f..3d8f36e 100644
--- a/apps/client/src/features/page/queries/page-query.ts
+++ b/apps/client/src/features/page/queries/page-query.ts
@@ -10,14 +10,15 @@ import {
deletePage,
getPageById,
getSidebarPages,
- getRecentChanges,
updatePage,
movePage,
getPageBreadcrumbs,
+ getRecentChanges,
} from "@/features/page/services/page-service";
import {
IMovePage,
IPage,
+ IPageInput,
SidebarPagesParams,
} from "@/features/page/types/page.types";
import { notifications } from "@mantine/notifications";
@@ -25,32 +26,19 @@ import { IPagination } from "@/lib/types.ts";
import { queryClient } from "@/main.tsx";
import { buildTree } from "@/features/page/tree/utils";
-const RECENT_CHANGES_KEY = ["recentChanges"];
-
export function usePageQuery(
- pageIdOrSlugId: string,
+ pageInput: Partial,
): UseQueryResult {
return useQuery({
- queryKey: ["pages", pageIdOrSlugId],
- queryFn: () => getPageById(pageIdOrSlugId),
- enabled: !!pageIdOrSlugId,
+ queryKey: ["pages", pageInput.pageId],
+ queryFn: () => getPageById(pageInput),
+ enabled: !!pageInput.pageId,
staleTime: 5 * 60 * 1000,
});
}
-export function useRecentChangesQuery(): UseQueryResult<
- IPagination,
- Error
-> {
- return useQuery({
- queryKey: RECENT_CHANGES_KEY,
- queryFn: () => getRecentChanges(),
- refetchOnMount: true,
- });
-}
-
export function useCreatePageMutation() {
- return useMutation>({
+ return useMutation>({
mutationFn: (data) => createPage(data),
onSuccess: (data) => {},
onError: (error) => {
@@ -61,7 +49,7 @@ export function useCreatePageMutation() {
export function useUpdatePageMutation() {
const queryClient = useQueryClient();
- return useMutation>({
+ return useMutation>({
mutationFn: (data) => updatePage(data),
onSuccess: (data) => {
// update page in cache
@@ -130,3 +118,13 @@ export async function fetchAncestorChildren(params: SidebarPagesParams) {
});
return buildTree(response.items);
}
+
+export function useRecentChangesQuery(
+ spaceId?: string,
+): UseQueryResult, Error> {
+ return useQuery({
+ queryKey: ["recent-changes", spaceId],
+ queryFn: () => getRecentChanges(spaceId),
+ refetchOnMount: true,
+ });
+}
diff --git a/apps/client/src/features/page/services/page-service.ts b/apps/client/src/features/page/services/page-service.ts
index 121163c..0a3a3d3 100644
--- a/apps/client/src/features/page/services/page-service.ts
+++ b/apps/client/src/features/page/services/page-service.ts
@@ -2,6 +2,7 @@ import api from "@/lib/api-client";
import {
IMovePage,
IPage,
+ IPageInput,
SidebarPagesParams,
} from "@/features/page/types/page.types";
import { IPagination } from "@/lib/types.ts";
@@ -11,12 +12,14 @@ export async function createPage(data: Partial): Promise {
return req.data;
}
-export async function getPageById(pageId: string): Promise {
- const req = await api.post("/pages/info", { pageId });
+export async function getPageById(
+ pageInput: Partial,
+): Promise {
+ const req = await api.post("/pages/info", pageInput);
return req.data;
}
-export async function updatePage(data: Partial): Promise {
+export async function updatePage(data: Partial): Promise {
const req = await api.post("/pages/update", data);
return req.data;
}
@@ -29,11 +32,6 @@ export async function movePage(data: IMovePage): Promise {
await api.post("/pages/move", data);
}
-export async function getRecentChanges(): Promise> {
- const req = await api.post("/pages/recent");
- return req.data;
-}
-
export async function getSidebarPages(
params: SidebarPagesParams,
): Promise> {
@@ -47,3 +45,10 @@ export async function getPageBreadcrumbs(
const req = await api.post("/pages/breadcrumbs", { pageId });
return req.data;
}
+
+export async function getRecentChanges(
+ spaceId?: string,
+): Promise> {
+ const req = await api.post("/pages/recent", { spaceId });
+ return req.data;
+}
diff --git a/apps/client/src/features/page/tree/components/space-content.tsx b/apps/client/src/features/page/tree/components/space-content.tsx
deleted file mode 100644
index 7ae2cd6..0000000
--- a/apps/client/src/features/page/tree/components/space-content.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useSpaceQuery } from "@/features/space/queries/space-query.ts";
-import { useAtom } from "jotai/index";
-import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
-import { Box } from "@mantine/core";
-import { IconNotes } from "@tabler/icons-react";
-import React from "react";
-import SpaceTree from "@/features/page/tree/components/space-tree.tsx";
-import { TreeCollapse } from "@/features/page/tree/components/tree-collapse.tsx";
-
-export default function SpaceContent() {
- const [currentUser] = useAtom(currentUserAtom);
- const { data: space } = useSpaceQuery(currentUser?.workspace.defaultSpaceId);
-
- if (!space) {
- return Loading...
;
- }
-
- return (
- <>
-
-
-
-
-
- >
- );
-}
diff --git a/apps/client/src/features/page/tree/components/space-tree.tsx b/apps/client/src/features/page/tree/components/space-tree.tsx
index 05a4398..b0ee15d 100644
--- a/apps/client/src/features/page/tree/components/space-tree.tsx
+++ b/apps/client/src/features/page/tree/components/space-tree.tsx
@@ -8,9 +8,9 @@ import {
useUpdatePageMutation,
} from "@/features/page/queries/page-query.ts";
import React, { useEffect, useRef } from "react";
-import { useLocation, useNavigate, useParams } from "react-router-dom";
+import { useNavigate, useParams } from "react-router-dom";
import classes from "@/features/page/tree/styles/tree.module.css";
-import { ActionIcon, Menu, rem, Text } from "@mantine/core";
+import { ActionIcon, Menu, rem } from "@mantine/core";
import {
IconChevronDown,
IconChevronRight,
@@ -42,10 +42,11 @@ import { OpenMap } from "react-arborist/dist/main/state/open-slice";
import { useClipboard, useElementSize, useMergedRef } from "@mantine/hooks";
import { dfs } from "react-arborist/dist/module/utils";
import { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
-import { buildPageSlug } from "@/features/page/page.utils.ts";
+import { buildPageUrl } from "@/features/page/page.utils.ts";
import { notifications } from "@mantine/notifications";
-import { modals } from "@mantine/modals";
-import APP_ROUTE from "@/lib/app-route.ts";
+import { getAppUrl } from "@/lib/config.ts";
+import { extractPageSlugId } from "@/lib";
+import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
interface SpaceTreeProps {
spaceId: string;
@@ -54,7 +55,7 @@ interface SpaceTreeProps {
const openTreeNodesAtom = atom({});
export default function SpaceTree({ spaceId }: SpaceTreeProps) {
- const { slugId } = useParams();
+ const { pageSlug } = useParams();
const { data, setData, controllers } =
useTreeMutation>(spaceId);
const {
@@ -72,20 +73,21 @@ export default function SpaceTree({ spaceId }: SpaceTreeProps) {
const { ref: sizeRef, width, height } = useElementSize();
const mergedRef = useMergedRef(rootElement, sizeRef);
const isDataLoaded = useRef(false);
- const { data: currentPage } = usePageQuery(slugId);
- const location = useLocation();
+ const { data: currentPage } = usePageQuery({
+ pageId: extractPageSlugId(pageSlug),
+ });
useEffect(() => {
if (hasNextPage && !isFetching) {
fetchNextPage();
}
- }, [hasNextPage, fetchNextPage, isFetching]);
+ }, [hasNextPage, fetchNextPage, isFetching, spaceId]);
useEffect(() => {
if (pagesData?.pages && !hasNextPage) {
const allItems = pagesData.pages.flatMap((page) => page.items);
const treeData = buildTree(allItems);
- if (data.length < 1) {
+ if (data.length < 1 || data?.[0].spaceId !== spaceId) {
//Thoughts
// don't reset if there is data in state
// we only expect to call this once on initial load
@@ -94,6 +96,7 @@ export default function SpaceTree({ spaceId }: SpaceTreeProps) {
// which looses async loaded children too
setData(treeData);
isDataLoaded.current = true;
+ setOpenTreeNodes({});
}
}
}, [pagesData, hasNextPage]);
@@ -166,7 +169,10 @@ export default function SpaceTree({ spaceId }: SpaceTreeProps) {
useEffect(() => {
if (currentPage?.id) {
- treeApiRef.current?.select(currentPage.id, { align: "auto" });
+ setTimeout(() => {
+ // focus on node and open all parents
+ treeApiRef.current?.select(currentPage.id, { align: "auto" });
+ }, 200);
} else {
treeApiRef.current?.deselectAll();
}
@@ -212,6 +218,7 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps) {
const updatePageMutation = useUpdatePageMutation();
const [treeData, setTreeData] = useAtom(treeDataAtom);
const emit = useQueryEmit();
+ const { spaceSlug } = useParams();
async function handleLoadChildren(node: NodeApi) {
if (!node.data.hasChildren) return;
@@ -228,7 +235,7 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps) {
const newChildren = await queryClient.fetchQuery({
queryKey: ["sidebar-pages", params],
queryFn: () => getSidebarPages(params),
- staleTime: 30 * 60 * 1000,
+ staleTime: 10 * 60 * 1000,
});
const childrenTree = buildTree(newChildren.items);
@@ -246,7 +253,8 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps) {
}
const handleClick = () => {
- navigate(buildPageSlug(node.data.slugId, node.data.name));
+ const pageUrl = buildPageUrl(spaceSlug, node.data.slugId, node.data.name);
+ navigate(pageUrl);
};
const handleUpdateNodeIcon = (nodeId: string, newIcon: string) => {
@@ -381,29 +389,16 @@ interface NodeMenuProps {
function NodeMenu({ node, treeApi }: NodeMenuProps) {
const clipboard = useClipboard({ timeout: 500 });
+ const { spaceSlug } = useParams();
+ const { openDeleteModal } = useDeletePageModal();
const handleCopyLink = () => {
- const pageLink =
- window.location.host + buildPageSlug(node.data.id, node.data.name);
- clipboard.copy(pageLink);
+ const pageUrl =
+ getAppUrl() + buildPageUrl(spaceSlug, node.data.slugId, node.data.name);
+ clipboard.copy(pageUrl);
notifications.show({ message: "Link copied" });
};
- const openDeleteModal = () =>
- modals.openConfirmModal({
- title: "Are you sure you want to delete this page?",
- children: (
-
- Are you sure you want to delete this page? This action is
- irreversible.
-
- ),
- centered: true,
- labels: { confirm: "Delete", cancel: "Cancel" },
- confirmProps: { color: "red" },
- onConfirm: () => treeApi?.delete(node),
- });
-
return (
)
diff --git a/apps/client/src/pages/settings/account/account-preferences.tsx b/apps/client/src/pages/settings/account/account-preferences.tsx
index a3cf22a..7e4df93 100644
--- a/apps/client/src/pages/settings/account/account-preferences.tsx
+++ b/apps/client/src/pages/settings/account/account-preferences.tsx
@@ -1,4 +1,4 @@
-import SettingsTitle from "@/components/layouts/settings/settings-title.tsx";
+import SettingsTitle from "@/components/settings/settings-title.tsx";
import AccountTheme from "@/features/user/components/account-theme.tsx";
export default function AccountPreferences() {
diff --git a/apps/client/src/pages/settings/account/account-settings.tsx b/apps/client/src/pages/settings/account/account-settings.tsx
index 6fc29fd..c115d25 100644
--- a/apps/client/src/pages/settings/account/account-settings.tsx
+++ b/apps/client/src/pages/settings/account/account-settings.tsx
@@ -3,7 +3,7 @@ import ChangeEmail from "@/features/user/components/change-email";
import ChangePassword from "@/features/user/components/change-password";
import { Divider } from "@mantine/core";
import AccountAvatar from "@/features/user/components/account-avatar";
-import SettingsTitle from "@/components/layouts/settings/settings-title.tsx";
+import SettingsTitle from "@/components/settings/settings-title.tsx";
export default function AccountSettings() {
return (
diff --git a/apps/client/src/pages/settings/group/group-info.tsx b/apps/client/src/pages/settings/group/group-info.tsx
index 62af7b6..7a22e7d 100644
--- a/apps/client/src/pages/settings/group/group-info.tsx
+++ b/apps/client/src/pages/settings/group/group-info.tsx
@@ -1,4 +1,4 @@
-import SettingsTitle from "@/components/layouts/settings/settings-title.tsx";
+import SettingsTitle from "@/components/settings/settings-title.tsx";
import GroupMembersList from "@/features/group/components/group-members";
import GroupDetails from "@/features/group/components/group-details";
diff --git a/apps/client/src/pages/settings/group/groups.tsx b/apps/client/src/pages/settings/group/groups.tsx
index 5ca2930..8adafa7 100644
--- a/apps/client/src/pages/settings/group/groups.tsx
+++ b/apps/client/src/pages/settings/group/groups.tsx
@@ -1,5 +1,5 @@
import GroupList from "@/features/group/components/group-list";
-import SettingsTitle from "@/components/layouts/settings/settings-title.tsx";
+import SettingsTitle from "@/components/settings/settings-title.tsx";
import { Group, Text } from "@mantine/core";
import CreateGroupModal from "@/features/group/components/create-group-modal";
diff --git a/apps/client/src/pages/settings/space/spaces.tsx b/apps/client/src/pages/settings/space/spaces.tsx
index 4021298..e54027c 100644
--- a/apps/client/src/pages/settings/space/spaces.tsx
+++ b/apps/client/src/pages/settings/space/spaces.tsx
@@ -1,4 +1,4 @@
-import SettingsTitle from "@/components/layouts/settings/settings-title.tsx";
+import SettingsTitle from "@/components/settings/settings-title.tsx";
import SpaceList from "@/features/space/components/space-list.tsx";
export default function Spaces() {
diff --git a/apps/client/src/pages/settings/workspace/workspace-members.tsx b/apps/client/src/pages/settings/workspace/workspace-members.tsx
index a037d7d..ffbce23 100644
--- a/apps/client/src/pages/settings/workspace/workspace-members.tsx
+++ b/apps/client/src/pages/settings/workspace/workspace-members.tsx
@@ -2,7 +2,7 @@ import WorkspaceInviteSection from "@/features/workspace/components/members/comp
import WorkspaceInviteModal from "@/features/workspace/components/members/components/workspace-invite-modal";
import { Divider, Group, SegmentedControl, Space, Text } from "@mantine/core";
import WorkspaceMembersTable from "@/features/workspace/components/members/components/workspace-members-table";
-import SettingsTitle from "@/components/layouts/settings/settings-title.tsx";
+import SettingsTitle from "@/components/settings/settings-title.tsx";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import WorkspaceInvitesTable from "@/features/workspace/components/members/components/workspace-invites-table.tsx";
diff --git a/apps/client/src/pages/settings/workspace/workspace-settings.tsx b/apps/client/src/pages/settings/workspace/workspace-settings.tsx
index b6e7918..66e93eb 100644
--- a/apps/client/src/pages/settings/workspace/workspace-settings.tsx
+++ b/apps/client/src/pages/settings/workspace/workspace-settings.tsx
@@ -1,4 +1,4 @@
-import SettingsTitle from "@/components/layouts/settings/settings-title.tsx";
+import SettingsTitle from "@/components/settings/settings-title.tsx";
import WorkspaceNameForm from "@/features/workspace/components/settings/components/workspace-name-form";
export default function WorkspaceSettings() {
diff --git a/apps/client/src/pages/space/space-home.tsx b/apps/client/src/pages/space/space-home.tsx
new file mode 100644
index 0000000..86ffe5f
--- /dev/null
+++ b/apps/client/src/pages/space/space-home.tsx
@@ -0,0 +1,15 @@
+import { Container } from "@mantine/core";
+import SpaceHomeTabs from "@/features/space/components/space-home-tabs.tsx";
+import { useParams } from "react-router-dom";
+import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
+
+export default function SpaceHome() {
+ const { spaceSlug } = useParams();
+ const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
+
+ return (
+