mirror of
https://github.com/docmost/docmost.git
synced 2025-11-13 09:12:36 +10:00
feat: home space list (#1400)
This commit is contained in:
@ -403,6 +403,7 @@
|
|||||||
"Replace (Enter)": "Replace (Enter)",
|
"Replace (Enter)": "Replace (Enter)",
|
||||||
"Replace all (Ctrl+Alt+Enter)": "Replace all (Ctrl+Alt+Enter)",
|
"Replace all (Ctrl+Alt+Enter)": "Replace all (Ctrl+Alt+Enter)",
|
||||||
"Replace all": "Replace all",
|
"Replace all": "Replace all",
|
||||||
|
"View all spaces": "View all spaces"
|
||||||
"Error": "Error",
|
"Error": "Error",
|
||||||
"Failed to disable MFA": "Failed to disable MFA",
|
"Failed to disable MFA": "Failed to disable MFA",
|
||||||
"Disable two-factor authentication": "Disable two-factor authentication",
|
"Disable two-factor authentication": "Disable two-factor authentication",
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import Shares from "@/pages/settings/shares/shares.tsx";
|
|||||||
import ShareLayout from "@/features/share/components/share-layout.tsx";
|
import ShareLayout from "@/features/share/components/share-layout.tsx";
|
||||||
import ShareRedirect from "@/pages/share/share-redirect.tsx";
|
import ShareRedirect from "@/pages/share/share-redirect.tsx";
|
||||||
import { useTrackOrigin } from "@/hooks/use-track-origin";
|
import { useTrackOrigin } from "@/hooks/use-track-origin";
|
||||||
|
import SpacesPage from "@/pages/spaces/spaces.tsx";
|
||||||
import { MfaChallengePage } from "@/ee/mfa/pages/mfa-challenge-page";
|
import { MfaChallengePage } from "@/ee/mfa/pages/mfa-challenge-page";
|
||||||
import { MfaSetupRequiredPage } from "@/ee/mfa/pages/mfa-setup-required-page";
|
import { MfaSetupRequiredPage } from "@/ee/mfa/pages/mfa-setup-required-page";
|
||||||
|
|
||||||
@ -77,6 +78,7 @@ export default function App() {
|
|||||||
|
|
||||||
<Route element={<Layout />}>
|
<Route element={<Layout />}>
|
||||||
<Route path={"/home"} element={<Home />} />
|
<Route path={"/home"} element={<Home />} />
|
||||||
|
<Route path={"/spaces"} element={<SpacesPage />} />
|
||||||
<Route path={"/s/:spaceSlug"} element={<SpaceHome />} />
|
<Route path={"/s/:spaceSlug"} element={<SpaceHome />} />
|
||||||
<Route
|
<Route
|
||||||
path={"/s/:spaceSlug/p/:pageSlug"}
|
path={"/s/:spaceSlug/p/:pageSlug"}
|
||||||
|
|||||||
@ -27,6 +27,8 @@ export function AppHeader() {
|
|||||||
const { isTrial, trialDaysLeft } = useTrial();
|
const { isTrial, trialDaysLeft } = useTrial();
|
||||||
|
|
||||||
const isHomeRoute = location.pathname.startsWith("/home");
|
const isHomeRoute = location.pathname.startsWith("/home");
|
||||||
|
const isSpacesRoute = location.pathname === "/spaces";
|
||||||
|
const hideSidebar = isHomeRoute || isSpacesRoute;
|
||||||
|
|
||||||
const items = links.map((link) => (
|
const items = links.map((link) => (
|
||||||
<Link key={link.label} to={link.link} className={classes.link}>
|
<Link key={link.label} to={link.link} className={classes.link}>
|
||||||
@ -38,7 +40,7 @@ export function AppHeader() {
|
|||||||
<>
|
<>
|
||||||
<Group h="100%" px="md" justify="space-between" wrap={"nowrap"}>
|
<Group h="100%" px="md" justify="space-between" wrap={"nowrap"}>
|
||||||
<Group wrap="nowrap">
|
<Group wrap="nowrap">
|
||||||
{!isHomeRoute && (
|
{!hideSidebar && (
|
||||||
<>
|
<>
|
||||||
<Tooltip label={t("Sidebar toggle")}>
|
<Tooltip label={t("Sidebar toggle")}>
|
||||||
<SidebarToggle
|
<SidebarToggle
|
||||||
|
|||||||
@ -73,13 +73,15 @@ export default function GlobalAppShell({
|
|||||||
const isSettingsRoute = location.pathname.startsWith("/settings");
|
const isSettingsRoute = location.pathname.startsWith("/settings");
|
||||||
const isSpaceRoute = location.pathname.startsWith("/s/");
|
const isSpaceRoute = location.pathname.startsWith("/s/");
|
||||||
const isHomeRoute = location.pathname.startsWith("/home");
|
const isHomeRoute = location.pathname.startsWith("/home");
|
||||||
|
const isSpacesRoute = location.pathname === "/spaces";
|
||||||
const isPageRoute = location.pathname.includes("/p/");
|
const isPageRoute = location.pathname.includes("/p/");
|
||||||
|
const hideSidebar = isHomeRoute || isSpacesRoute;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
header={{ height: 45 }}
|
header={{ height: 45 }}
|
||||||
navbar={
|
navbar={
|
||||||
!isHomeRoute && {
|
!hideSidebar && {
|
||||||
width: isSpaceRoute ? sidebarWidth : 300,
|
width: isSpaceRoute ? sidebarWidth : 300,
|
||||||
breakpoint: "sm",
|
breakpoint: "sm",
|
||||||
collapsed: {
|
collapsed: {
|
||||||
@ -100,7 +102,7 @@ export default function GlobalAppShell({
|
|||||||
<AppShell.Header px="md" className={classes.header}>
|
<AppShell.Header px="md" className={classes.header}>
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
</AppShell.Header>
|
</AppShell.Header>
|
||||||
{!isHomeRoute && (
|
{!hideSidebar && (
|
||||||
<AppShell.Navbar
|
<AppShell.Navbar
|
||||||
className={classes.navbar}
|
className={classes.navbar}
|
||||||
withBorder={false}
|
withBorder={false}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Text, Avatar, SimpleGrid, Card, rem } from "@mantine/core";
|
import { Text, Avatar, SimpleGrid, Card, rem, Group, Button } from "@mantine/core";
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
prefetchSpace,
|
prefetchSpace,
|
||||||
@ -9,10 +9,11 @@ import { Link } from "react-router-dom";
|
|||||||
import classes from "./space-grid.module.css";
|
import classes from "./space-grid.module.css";
|
||||||
import { formatMemberCount } from "@/lib";
|
import { formatMemberCount } from "@/lib";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { IconArrowRight } from "@tabler/icons-react";
|
||||||
|
|
||||||
export default function SpaceGrid() {
|
export default function SpaceGrid() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data, isLoading } = useGetSpacesQuery({ page: 1 });
|
const { data, isLoading } = useGetSpacesQuery({ page: 1, limit: 9 });
|
||||||
|
|
||||||
const cards = data?.items.map((space, index) => (
|
const cards = data?.items.map((space, index) => (
|
||||||
<Card
|
<Card
|
||||||
@ -46,11 +47,25 @@ export default function SpaceGrid() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Text fz="sm" fw={500} mb={"md"}>
|
<Group justify="space-between" align="center" mb="md">
|
||||||
{t("Spaces you belong to")}
|
<Text fz="sm" fw={500}>
|
||||||
</Text>
|
{t("Spaces you belong to")}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 1, xs: 2, sm: 3 }}>{cards}</SimpleGrid>
|
<SimpleGrid cols={{ base: 1, xs: 2, sm: 3 }}>{cards}</SimpleGrid>
|
||||||
|
|
||||||
|
<Group justify="flex-end" mt="lg">
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to="/spaces"
|
||||||
|
variant="subtle"
|
||||||
|
rightSection={<IconArrowRight size={16} />}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{t("View all spaces")}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
.spaceLink {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,160 @@
|
|||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Group,
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Space,
|
||||||
|
Menu,
|
||||||
|
Avatar,
|
||||||
|
Anchor,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { IconDots, IconSettings } from "@tabler/icons-react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import { formatMemberCount } from "@/lib";
|
||||||
|
import { getSpaceUrl } from "@/lib/config";
|
||||||
|
import { prefetchSpace } from "@/features/space/queries/space-query";
|
||||||
|
import { SearchInput } from "@/components/common/search-input";
|
||||||
|
import Paginate from "@/components/common/paginate";
|
||||||
|
import NoTableResults from "@/components/common/no-table-results";
|
||||||
|
import SpaceSettingsModal from "@/features/space/components/settings-modal";
|
||||||
|
import classes from "./all-spaces-list.module.css";
|
||||||
|
|
||||||
|
interface AllSpacesListProps {
|
||||||
|
spaces: any[];
|
||||||
|
onSearch: (query: string) => void;
|
||||||
|
page: number;
|
||||||
|
hasPrevPage?: boolean;
|
||||||
|
hasNextPage?: boolean;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AllSpacesList({
|
||||||
|
spaces,
|
||||||
|
onSearch,
|
||||||
|
page,
|
||||||
|
hasPrevPage,
|
||||||
|
hasNextPage,
|
||||||
|
onPageChange,
|
||||||
|
}: AllSpacesListProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [settingsOpened, { open: openSettings, close: closeSettings }] =
|
||||||
|
useDisclosure(false);
|
||||||
|
const [selectedSpaceId, setSelectedSpaceId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleOpenSettings = (spaceId: string) => {
|
||||||
|
setSelectedSpaceId(spaceId);
|
||||||
|
openSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<SearchInput onSearch={onSearch} />
|
||||||
|
|
||||||
|
<Space h="md" />
|
||||||
|
|
||||||
|
<Table.ScrollContainer minWidth={500}>
|
||||||
|
<Table highlightOnHover verticalSpacing="sm">
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>{t("Space")}</Table.Th>
|
||||||
|
<Table.Th>{t("Members")}</Table.Th>
|
||||||
|
<Table.Th w={100}></Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
|
||||||
|
<Table.Tbody>
|
||||||
|
{spaces.length > 0 ? (
|
||||||
|
spaces.map((space) => (
|
||||||
|
<Table.Tr key={space.id}>
|
||||||
|
<Table.Td>
|
||||||
|
<Anchor
|
||||||
|
size="sm"
|
||||||
|
underline="never"
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
color: "var(--mantine-color-text)",
|
||||||
|
}}
|
||||||
|
component={Link}
|
||||||
|
to={getSpaceUrl(space.slug)}
|
||||||
|
>
|
||||||
|
<Group
|
||||||
|
gap="sm"
|
||||||
|
wrap="nowrap"
|
||||||
|
className={classes.spaceLink}
|
||||||
|
onMouseEnter={() => prefetchSpace(space.slug, space.id)}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
color="initials"
|
||||||
|
variant="filled"
|
||||||
|
name={space.name}
|
||||||
|
size={40}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Text fz="sm" fw={500} lineClamp={1}>
|
||||||
|
{space.name}
|
||||||
|
</Text>
|
||||||
|
{space.description && (
|
||||||
|
<Text fz="xs" c="dimmed" lineClamp={2}>
|
||||||
|
{space.description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Anchor>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
|
||||||
|
{formatMemberCount(space.memberCount, t)}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Group gap="xs" justify="flex-end">
|
||||||
|
<Menu position="bottom-end">
|
||||||
|
<Menu.Target>
|
||||||
|
<ActionIcon variant="subtle" color="gray">
|
||||||
|
<IconDots size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<IconSettings size={16} />}
|
||||||
|
onClick={() => handleOpenSettings(space.id)}
|
||||||
|
>
|
||||||
|
{t("Space settings")}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<NoTableResults colSpan={3} />
|
||||||
|
)}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Table.ScrollContainer>
|
||||||
|
|
||||||
|
{spaces.length > 0 && (
|
||||||
|
<Paginate
|
||||||
|
currentPage={page}
|
||||||
|
hasPrevPage={hasPrevPage}
|
||||||
|
hasNextPage={hasNextPage}
|
||||||
|
onPageChange={onPageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedSpaceId && (
|
||||||
|
<SpaceSettingsModal
|
||||||
|
spaceId={selectedSpaceId}
|
||||||
|
opened={settingsOpened}
|
||||||
|
onClose={closeSettings}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default as AllSpacesList } from "./all-spaces-list";
|
||||||
@ -1,5 +1,6 @@
|
|||||||
const APP_ROUTE = {
|
const APP_ROUTE = {
|
||||||
HOME: "/home",
|
HOME: "/home",
|
||||||
|
SPACES: "/spaces",
|
||||||
AUTH: {
|
AUTH: {
|
||||||
LOGIN: "/login",
|
LOGIN: "/login",
|
||||||
SIGNUP: "/signup",
|
SIGNUP: "/signup",
|
||||||
|
|||||||
53
apps/client/src/pages/spaces/spaces.tsx
Normal file
53
apps/client/src/pages/spaces/spaces.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Container, Title, Text, Group, Box } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Helmet } from "react-helmet-async";
|
||||||
|
import { getAppName } from "@/lib/config";
|
||||||
|
import { useGetSpacesQuery } from "@/features/space/queries/space-query";
|
||||||
|
import CreateSpaceModal from "@/features/space/components/create-space-modal";
|
||||||
|
import { AllSpacesList } from "@/features/space/components/spaces-page";
|
||||||
|
import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search";
|
||||||
|
import useUserRole from "@/hooks/use-user-role";
|
||||||
|
|
||||||
|
export default function Spaces() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { isAdmin } = useUserRole();
|
||||||
|
const { search, page, setPage, handleSearch } = usePaginateAndSearch();
|
||||||
|
|
||||||
|
const { data, isLoading } = useGetSpacesQuery({
|
||||||
|
page,
|
||||||
|
limit: 30,
|
||||||
|
query: search,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>
|
||||||
|
{t("Spaces")} - {getAppName()}
|
||||||
|
</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<Container size={"800"} pt="xl">
|
||||||
|
<Group justify="space-between" mb="xl">
|
||||||
|
<Title order={3}>{t("Spaces")}</Title>
|
||||||
|
{isAdmin && <CreateSpaceModal />}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text size="sm" c="dimmed" mb="md">
|
||||||
|
{t("Spaces you belong to")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<AllSpacesList
|
||||||
|
spaces={data?.items || []}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
page={page}
|
||||||
|
hasPrevPage={data?.meta?.hasPrevPage}
|
||||||
|
hasNextPage={data?.meta?.hasNextPage}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user