Merge branch 'feature/home-new-note' into develop
feat(home): prominent "New note" button on the dashboard
This commit is contained in:
@@ -119,6 +119,8 @@
|
||||
"Name": "Name",
|
||||
"New email": "New email",
|
||||
"New page": "New page",
|
||||
"New note": "New note",
|
||||
"Create in space": "Create in space",
|
||||
"New password": "New password",
|
||||
"No group found": "No group found",
|
||||
"No page history saved yet.": "No page history saved yet.",
|
||||
|
||||
@@ -119,6 +119,8 @@
|
||||
"Name": "Имя",
|
||||
"New email": "Новый электронный адрес",
|
||||
"New page": "Новая страница",
|
||||
"New note": "Новая заметка",
|
||||
"Create in space": "Создать в пространстве",
|
||||
"New password": "Новый пароль",
|
||||
"No group found": "Группа не найдена",
|
||||
"No page history saved yet.": "История страниц ещё не сохранена.",
|
||||
|
||||
111
apps/client/src/features/home/components/new-note-button.tsx
Normal file
111
apps/client/src/features/home/components/new-note-button.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Button, Menu, Text } from "@mantine/core";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { createMongoAbility } from "@casl/ability";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts";
|
||||
import { useCreatePageMutation } from "@/features/page/queries/page-query.ts";
|
||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||
import { ISpace } from "@/features/space/types/space.types.ts";
|
||||
import {
|
||||
SpaceAbility,
|
||||
SpaceCaslAction,
|
||||
SpaceCaslSubject,
|
||||
} from "@/features/space/permissions/permissions.type.ts";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||
|
||||
// A space is writable when the member's CASL rules allow managing pages —
|
||||
// mirrors the create-page gate used in the space sidebar.
|
||||
function canCreatePage(space: ISpace): boolean {
|
||||
const ability = createMongoAbility<SpaceAbility>(
|
||||
(space.membership?.permissions ?? []) as any,
|
||||
);
|
||||
return ability.can(SpaceCaslAction.Manage, SpaceCaslSubject.Page);
|
||||
}
|
||||
|
||||
// Prominent home-screen action to create a new note (page). Because the home
|
||||
// screen has no active space, the target space is resolved from the user's
|
||||
// writable spaces: created directly when there is one, picked from a dropdown
|
||||
// when there are several.
|
||||
export default function NewNoteButton() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const createPageMutation = useCreatePageMutation();
|
||||
const { data } = useGetSpacesQuery({ limit: 100 });
|
||||
|
||||
const writableSpaces = (data?.items ?? []).filter(canCreatePage);
|
||||
|
||||
const createNote = async (space: ISpace) => {
|
||||
try {
|
||||
// `spaceId` is accepted by the create-page endpoint but is not part of
|
||||
// the shared `IPageInput` type; cast to satisfy the mutation signature.
|
||||
const createdPage = await createPageMutation.mutateAsync({
|
||||
spaceId: space.id,
|
||||
} as any);
|
||||
navigate(buildPageUrl(space.slug, createdPage.slugId, createdPage.title));
|
||||
} catch {
|
||||
// useCreatePageMutation already surfaces a red notification on error.
|
||||
}
|
||||
};
|
||||
|
||||
// No writable space → nothing to create in; render nothing.
|
||||
if (writableSpaces.length === 0) return null;
|
||||
|
||||
const isPending = createPageMutation.isPending;
|
||||
|
||||
// Exactly one writable space → create directly, no picker needed.
|
||||
if (writableSpaces.length === 1) {
|
||||
return (
|
||||
<Button
|
||||
fullWidth
|
||||
size="md"
|
||||
leftSection={<IconPlus size={18} />}
|
||||
loading={isPending}
|
||||
onClick={() => createNote(writableSpaces[0])}
|
||||
>
|
||||
{t("New note")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Multiple writable spaces → pick the target space from a dropdown.
|
||||
return (
|
||||
<Menu shadow="md" width="target" position="bottom-start">
|
||||
<Menu.Target>
|
||||
<Button
|
||||
fullWidth
|
||||
size="md"
|
||||
leftSection={<IconPlus size={18} />}
|
||||
loading={isPending}
|
||||
>
|
||||
{t("New note")}
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>{t("Create in space")}</Menu.Label>
|
||||
{writableSpaces.map((space) => (
|
||||
<Menu.Item
|
||||
key={space.id}
|
||||
disabled={isPending}
|
||||
leftSection={
|
||||
<CustomAvatar
|
||||
name={space.name}
|
||||
avatarUrl={space.logo}
|
||||
type={AvatarIconType.SPACE_ICON}
|
||||
color="initials"
|
||||
variant="filled"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
onClick={() => createNote(space)}
|
||||
>
|
||||
<Text size="sm" lineClamp={1}>
|
||||
{space.name}
|
||||
</Text>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Container, Space } from "@mantine/core";
|
||||
import HomeTabs from "@/features/home/components/home-tabs";
|
||||
import NewNoteButton from "@/features/home/components/new-note-button";
|
||||
import SpaceCarousel from "@/features/space/components/space-carousel.tsx";
|
||||
import { getAppName } from "@/lib/config.ts";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
@@ -16,6 +17,10 @@ export default function Home() {
|
||||
</title>
|
||||
</Helmet>
|
||||
<Container size={"900"} pt="xl">
|
||||
<NewNoteButton />
|
||||
|
||||
<Space h="xl" />
|
||||
|
||||
<SpaceCarousel />
|
||||
|
||||
<Space h="xl" />
|
||||
|
||||
Reference in New Issue
Block a user