Adds co-located unit tests for ten targets (client → vitest *.test.ts(x), server → jest *.spec.ts), plus minimal behavior-preserving extractions/exports where the issue required a pure function to test: - encode-wav: WAV header + PCM16 clamping - editor-ext embed-provider / utils (sanitizeUrl, isInternalFileUrl) / indent (export clampIndent) - label.dto @Matches regex - move-page.dto vs generateJitteredKeyBetween parity (bug locked via test.failing) - new-note-button canCreatePage (extracted to can-create-page.ts) - history-editor diff (extracted pure computeHistoryDiff into history-diff.ts) - notification getTypesForTab + repo contract (direct-tab divergence locked via test.failing) - search buildTsQuery (extracted + sanitizes operator inputs so adversarial queries no longer risk a to_tsquery 500) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
import { Button, Menu, Text } from "@mantine/core";
|
|
import { IconPlus } from "@tabler/icons-react";
|
|
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 { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
|
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
|
import { canCreatePage } from "./can-create-page.ts";
|
|
|
|
// 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"
|
|
variant="light"
|
|
color="gray"
|
|
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"
|
|
variant="light"
|
|
color="gray"
|
|
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>
|
|
);
|
|
}
|