mirror of
https://github.com/docmost/docmost.git
synced 2026-05-16 14:14:06 +08:00
feat: favorites (#2103)
* feat: favorites and templates(ee) * rename migrations * fix sidebar * cleanup tabs * fix * turn off templates * cleanup * uuid validation
This commit is contained in:
@@ -56,6 +56,7 @@ export function SpaceSidebar() {
|
||||
const [mobileSidebarOpened] = useAtom(mobileSidebarAtom);
|
||||
const toggleMobileSidebar = useToggleSidebar(mobileSidebarAtom);
|
||||
|
||||
|
||||
const { spaceSlug } = useParams();
|
||||
const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
|
||||
|
||||
@@ -81,11 +82,13 @@ export function SpaceSidebar() {
|
||||
marginBottom: 3,
|
||||
}}
|
||||
>
|
||||
<SwitchSpace
|
||||
spaceName={space?.name}
|
||||
spaceSlug={space?.slug}
|
||||
spaceIcon={space?.logo}
|
||||
/>
|
||||
<Group gap={4} wrap="nowrap" justify="space-between" style={{ width: "100%" }}>
|
||||
<SwitchSpace
|
||||
spaceName={space?.name}
|
||||
spaceSlug={space?.slug}
|
||||
spaceIcon={space?.logo}
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<div className={classes.section}>
|
||||
|
||||
@@ -7,9 +7,26 @@
|
||||
}
|
||||
|
||||
.cardSection {
|
||||
position: relative;
|
||||
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
|
||||
}
|
||||
|
||||
.starButton {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
|
||||
.starButton[data-favorited="true"] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card:hover .starButton {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family:
|
||||
Greycliff CF,
|
||||
|
||||
@@ -12,12 +12,15 @@ import { useTranslation } from "react-i18next";
|
||||
import { IconArrowRight } from "@tabler/icons-react";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||
import StarButton from "@/features/favorite/components/star-button";
|
||||
import { useFavoriteIds } from "@/features/favorite/queries/favorite-query";
|
||||
|
||||
export default function SpaceGrid() {
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading } = useGetSpacesQuery({ limit: 10 });
|
||||
const spaceFavoriteIds = useFavoriteIds("space");
|
||||
|
||||
const cards = data?.items.slice(0, 9).map((space, index) => (
|
||||
const cards = data?.items.slice(0, 6).map((space, index) => (
|
||||
<Card
|
||||
key={space.id}
|
||||
p="xs"
|
||||
@@ -28,7 +31,11 @@ export default function SpaceGrid() {
|
||||
className={classes.card}
|
||||
withBorder
|
||||
>
|
||||
<Card.Section className={classes.cardSection} h={40}></Card.Section>
|
||||
<Card.Section className={classes.cardSection} h={40}>
|
||||
<div className={classes.starButton} data-favorited={spaceFavoriteIds.has(space.id)}>
|
||||
<StarButton type="space" spaceId={space.id} size={16} />
|
||||
</div>
|
||||
</Card.Section>
|
||||
<CustomAvatar
|
||||
name={space.name}
|
||||
avatarUrl={space.logo}
|
||||
@@ -59,7 +66,7 @@ export default function SpaceGrid() {
|
||||
|
||||
<SimpleGrid cols={{ base: 1, xs: 2, sm: 3 }}>{cards}</SimpleGrid>
|
||||
|
||||
{data?.items && data.items.length > 9 && (
|
||||
{data?.items && data.items.length > 6 && (
|
||||
<Group justify="flex-end" mt="lg">
|
||||
<Button
|
||||
component={Link}
|
||||
|
||||
@@ -1,23 +1,44 @@
|
||||
import { Text, Tabs, Space } from "@mantine/core";
|
||||
import { IconClockHour3 } from "@tabler/icons-react";
|
||||
import RecentChanges from "@/components/common/recent-changes.tsx";
|
||||
import { IconClockHour3, IconStar, IconUser } from "@tabler/icons-react";
|
||||
import RecentChanges from "@/components/common/recent-changes";
|
||||
import FavoritesPages from "@/features/home/components/favorites-pages";
|
||||
import CreatedByMe from "@/features/home/components/created-by-me";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
|
||||
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAtom } from "jotai";
|
||||
import { homeTabAtom } from "@/features/home/atoms/home-tab-atom";
|
||||
|
||||
export default function SpaceHomeTabs() {
|
||||
const { t } = useTranslation();
|
||||
const { spaceSlug } = useParams();
|
||||
const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
|
||||
const [activeTab, setActiveTab] = useAtom(homeTabAtom);
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="recent">
|
||||
<Tabs.List>
|
||||
<Tabs
|
||||
color="dark"
|
||||
value={activeTab}
|
||||
onChange={(value) => {
|
||||
if (value) setActiveTab(value);
|
||||
}}
|
||||
>
|
||||
<Tabs.List style={{ flexWrap: "nowrap", overflowX: "auto" }}>
|
||||
<Tabs.Tab value="recent" leftSection={<IconClockHour3 size={18} />}>
|
||||
<Text size="sm" fw={500}>
|
||||
{t("Recently updated")}
|
||||
</Text>
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="favorites" leftSection={<IconStar size={18} />}>
|
||||
<Text size="sm" fw={500}>
|
||||
{t("Favorites")}
|
||||
</Text>
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="created" leftSection={<IconUser size={18} />}>
|
||||
<Text size="sm" fw={500}>
|
||||
{t("Created by me")}
|
||||
</Text>
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Space my="md" />
|
||||
@@ -25,6 +46,12 @@ export default function SpaceHomeTabs() {
|
||||
<Tabs.Panel value="recent">
|
||||
{space?.id && <RecentChanges spaceId={space.id} />}
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="favorites">
|
||||
<FavoritesPages />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="created">
|
||||
{space?.id && <CreatedByMe spaceId={space.id} />}
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Anchor,
|
||||
} from "@mantine/core";
|
||||
import { IconDots, IconSettings } from "@tabler/icons-react";
|
||||
import StarButton from "@/features/favorite/components/star-button";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React, { useState } from "react";
|
||||
@@ -117,6 +118,7 @@ export default function AllSpacesList({
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs" justify="flex-end">
|
||||
<StarButton type="space" spaceId={space.id} size={16} />
|
||||
<Menu position="bottom-end">
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" color="gray">
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Text, SimpleGrid, Card, rem, Group, Box, Button } from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFavoritesQuery } from "@/features/favorite/queries/favorite-query";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar";
|
||||
import { AvatarIconType } from "@/features/attachments/types/attachment.types";
|
||||
import { getSpaceUrl } from "@/lib/config";
|
||||
import { prefetchSpace } from "@/features/space/queries/space-query";
|
||||
import StarButton from "@/features/favorite/components/star-button";
|
||||
import { IconChevronDown } from "@tabler/icons-react";
|
||||
import spaceClasses from "../space-grid.module.css";
|
||||
|
||||
const INITIAL_COUNT = 8;
|
||||
|
||||
export default function FavoriteSpacesGrid() {
|
||||
const { t } = useTranslation();
|
||||
const { data } = useFavoritesQuery("space");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const allSpaces = (data?.pages.flatMap((p) => p.items) ?? [])
|
||||
.filter((fav) => fav.space)
|
||||
.sort((a, b) => a.space!.name.localeCompare(b.space!.name));
|
||||
|
||||
if (allSpaces.length === 0) return null;
|
||||
|
||||
const visibleSpaces = expanded
|
||||
? allSpaces
|
||||
: allSpaces.slice(0, INITIAL_COUNT);
|
||||
|
||||
return (
|
||||
<Box mb="xl">
|
||||
<Text size="sm" fw={500} mb="md">
|
||||
{t("Favorite spaces")}
|
||||
</Text>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, xs: 2, sm: 4 }}>
|
||||
{visibleSpaces.map((fav) => (
|
||||
<Card
|
||||
key={fav.id}
|
||||
p="xs"
|
||||
radius="md"
|
||||
component={Link}
|
||||
to={getSpaceUrl(fav.space!.slug)}
|
||||
onMouseEnter={() =>
|
||||
prefetchSpace(fav.space!.slug, fav.space!.id)
|
||||
}
|
||||
className={spaceClasses.card}
|
||||
withBorder
|
||||
>
|
||||
<Card.Section className={spaceClasses.cardSection} h={40}>
|
||||
<div className={spaceClasses.starButton} data-favorited="true">
|
||||
<StarButton
|
||||
type="space"
|
||||
spaceId={fav.space!.id}
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
</Card.Section>
|
||||
<CustomAvatar
|
||||
name={fav.space!.name}
|
||||
avatarUrl={fav.space!.logo}
|
||||
type={AvatarIconType.SPACE_ICON}
|
||||
color="initials"
|
||||
variant="filled"
|
||||
size="md"
|
||||
mt={rem(-20)}
|
||||
/>
|
||||
<Text fz="md" fw={500} mt="xs" className={spaceClasses.title}>
|
||||
{fav.space!.name}
|
||||
</Text>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
{!expanded && allSpaces.length > INITIAL_COUNT && (
|
||||
<Group justify="center" mt="sm">
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
rightSection={<IconChevronDown size={14} />}
|
||||
onClick={() => setExpanded(true)}
|
||||
>
|
||||
{t("Show more")}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -69,9 +69,10 @@ export const prefetchSpace = (spaceSlug: string, spaceId?: string) => {
|
||||
|
||||
if (spaceId) {
|
||||
// this endpoint only accepts uuid for now
|
||||
queryClient.prefetchQuery({
|
||||
queryClient.prefetchInfiniteQuery({
|
||||
queryKey: ["recent-changes", spaceId],
|
||||
queryFn: () => getRecentChanges(spaceId),
|
||||
queryFn: () => getRecentChanges({ spaceId }),
|
||||
initialPageParam: undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user