Compare commits

..

1 Commits

Author SHA1 Message Date
f9bd5e7b57 feat: ukrainian language support 2025-06-11 15:29:53 -07:00
63 changed files with 471 additions and 2013 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "client", "name": "client",
"private": true, "private": true,
"version": "0.21.0", "version": "0.20.4",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
@ -24,8 +24,7 @@
"@mantine/spotlight": "^7.17.0", "@mantine/spotlight": "^7.17.0",
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.34.0",
"@tanstack/react-query": "^5.80.6", "@tanstack/react-query": "^5.80.6",
"@tiptap/extension-character-count": "^2.10.3", "@tiptap/extension-character-count": "^2.14.0",
"alfaaz": "^1.1.0",
"axios": "^1.9.0", "axios": "^1.9.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"emoji-mart": "^5.6.0", "emoji-mart": "^5.6.0",
@ -41,7 +40,6 @@
"lowlight": "^3.3.0", "lowlight": "^3.3.0",
"mermaid": "^11.6.0", "mermaid": "^11.6.0",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"posthog-js": "^1.255.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-arborist": "3.4.0", "react-arborist": "3.4.0",
"react-clear-modal": "^2.0.15", "react-clear-modal": "^2.0.15",

View File

@ -354,9 +354,6 @@
"Character count: {{characterCount}}": "Character count: {{characterCount}}", "Character count: {{characterCount}}": "Character count: {{characterCount}}",
"New update": "New update", "New update": "New update",
"{{latestVersion}} is available": "{{latestVersion}} is available", "{{latestVersion}} is available": "{{latestVersion}} is available",
"Default page edit mode": "Default page edit mode",
"Choose your preferred page edit mode. Avoid accidental edits.": "Choose your preferred page edit mode. Avoid accidental edits.",
"Reading": "Reading"
"Delete member": "Delete member", "Delete member": "Delete member",
"Member deleted successfully": "Member deleted successfully", "Member deleted successfully": "Member deleted successfully",
"Are you sure you want to delete this workspace member? This action is irreversible.": "Are you sure you want to delete this workspace member? This action is irreversible.", "Are you sure you want to delete this workspace member? This action is irreversible.": "Are you sure you want to delete this workspace member? This action is irreversible.",
@ -389,15 +386,5 @@
"Failed to share page": "Failed to share page", "Failed to share page": "Failed to share page",
"Copy page": "Copy page", "Copy page": "Copy page",
"Copy page to a different space.": "Copy page to a different space.", "Copy page to a different space.": "Copy page to a different space.",
"Page copied successfully": "Page copied successfully", "Page copied successfully": "Page copied successfully"
"Find": "Find",
"Not found": "Not found",
"Previous Match (Shift+Enter)": "Previous Match (Shift+Enter)",
"Next match (Enter)": "Next match (Enter)",
"Match case (Alt+C)": "Match case (Alt+C)",
"Replace": "Replace",
"Close (Escape)": "Close (Escape)",
"Replace (Enter)": "Replace (Enter)",
"Replace all (Ctrl+Alt+Enter)": "Replace all (Ctrl+Alt+Enter)",
"Replace all": "Replace all"
} }

View File

@ -1,8 +1,6 @@
import { UserProvider } from "@/features/user/user-provider.tsx"; import { UserProvider } from "@/features/user/user-provider.tsx";
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import GlobalAppShell from "@/components/layouts/global/global-app-shell.tsx"; import GlobalAppShell from "@/components/layouts/global/global-app-shell.tsx";
import { PosthogUser } from "@/ee/components/posthog-user.tsx";
import { isCloud } from "@/lib/config.ts";
export default function Layout() { export default function Layout() {
return ( return (
@ -10,7 +8,6 @@ export default function Layout() {
<GlobalAppShell> <GlobalAppShell>
<Outlet /> <Outlet />
</GlobalAppShell> </GlobalAppShell>
{isCloud() && <PosthogUser />}
</UserProvider> </UserProvider>
); );
} }

View File

@ -30,12 +30,12 @@ export default function BillingDetails() {
> >
Plan Plan
</Text> </Text>
<Text fw={700} fz="lg" tt="capitalize"> <Text fw={700} fz="lg">
{plans.find( {
plans.find(
(plan) => plan.productId === billing.stripeProductId, (plan) => plan.productId === billing.stripeProductId,
)?.name || )?.name
billing.planName || }
"Standard"}
</Text> </Text>
</div> </div>
</Group> </Group>
@ -112,21 +112,8 @@ export default function BillingDetails() {
fz="xs" fz="xs"
className={classes.label} className={classes.label}
> >
Cost Total
</Text> </Text>
{billing.billingScheme === "tiered" && (
<>
<Text fw={700} fz="lg">
${billing.amount / 100} {billing.currency.toUpperCase()}
</Text>
<Text c="dimmed" fz="sm">
per {billing.interval}
</Text>
</>
)}
{billing.billingScheme !== "tiered" && (
<>
<Text fw={700} fz="lg"> <Text fw={700} fz="lg">
{(billing.amount / 100) * billing.quantity}{" "} {(billing.amount / 100) * billing.quantity}{" "}
{billing.currency.toUpperCase()} {billing.currency.toUpperCase()}
@ -134,36 +121,9 @@ export default function BillingDetails() {
<Text c="dimmed" fz="sm"> <Text c="dimmed" fz="sm">
${billing.amount / 100} /user/{billing.interval} ${billing.amount / 100} /user/{billing.interval}
</Text> </Text>
</>
)}
</div> </div>
</Group> </Group>
</Paper> </Paper>
{billing.billingScheme === "tiered" && billing.tieredUpTo && (
<Paper p="md" radius="md">
<Group justify="apart">
<div>
<Text
c="dimmed"
tt="uppercase"
fw={700}
fz="xs"
className={classes.label}
>
Current Tier
</Text>
<Text fw={700} fz="lg">
For {billing.tieredUpTo} users
</Text>
{/*billing.tieredFlatAmount && (
<Text c="dimmed" fz="sm">
</Text>
)*/}
</div>
</Group>
</Paper>
)}
</SimpleGrid> </SimpleGrid>
</div> </div>
); );

View File

@ -2,28 +2,24 @@ import {
Button, Button,
Card, Card,
List, List,
SegmentedControl,
ThemeIcon, ThemeIcon,
Title, Title,
Text, Text,
Group, Group,
Select,
Container,
Stack,
Badge,
Flex,
Switch,
} from "@mantine/core"; } from "@mantine/core";
import { useState } from "react"; import { useState } from "react";
import { IconCheck } from "@tabler/icons-react"; import { IconCheck } from "@tabler/icons-react";
import { getCheckoutLink } from "@/ee/billing/services/billing-service.ts";
import { useBillingPlans } from "@/ee/billing/queries/billing-query.ts"; import { useBillingPlans } from "@/ee/billing/queries/billing-query.ts";
import { getCheckoutLink } from "@/ee/billing/services/billing-service.ts";
export default function BillingPlans() { export default function BillingPlans() {
const { data: plans } = useBillingPlans(); const { data: plans } = useBillingPlans();
const [isAnnual, setIsAnnual] = useState(true); const [interval, setInterval] = useState("yearly");
const [selectedTierValue, setSelectedTierValue] = useState<string | null>(
null, if (!plans) {
); return null;
}
const handleCheckout = async (priceId: string) => { const handleCheckout = async (priceId: string) => {
try { try {
@ -36,153 +32,84 @@ export default function BillingPlans() {
} }
}; };
if (!plans || plans.length === 0) {
return null;
}
const firstPlan = plans[0];
// Set initial tier value if not set
if (!selectedTierValue && firstPlan.pricingTiers.length > 0) {
setSelectedTierValue(firstPlan.pricingTiers[0].upTo.toString());
return null;
}
if (!selectedTierValue) {
return null;
}
const selectData = firstPlan.pricingTiers
.filter((tier) => !tier.custom)
.map((tier, index) => {
const prevMaxUsers =
index > 0 ? firstPlan.pricingTiers[index - 1].upTo : 0;
return {
value: tier.upTo.toString(),
label: `${prevMaxUsers + 1}-${tier.upTo} users`,
};
});
return ( return (
<Container size="xl" py="xl"> <Group justify="center" p="xl">
{/* Controls Section */} {plans.map((plan) => {
<Stack gap="xl" mb="md"> const price =
{/* Team Size and Billing Controls */} interval === "monthly" ? plan.price.monthly : plan.price.yearly;
<Group justify="center" align="center" gap="sm"> const priceId = interval === "monthly" ? plan.monthlyId : plan.yearlyId;
<Select const yearlyMonthPrice = parseInt(plan.price.yearly) / 12;
label="Team size"
description="Select the number of users"
value={selectedTierValue}
onChange={setSelectedTierValue}
data={selectData}
w={250}
size="md"
allowDeselect={false}
/>
<Group justify="center" align="start">
<Flex justify="center" gap="md" align="center">
<Text size="md">Monthly</Text>
<Switch
defaultChecked={isAnnual}
onChange={(event) => setIsAnnual(event.target.checked)}
size="sm"
/>
<Text size="md">
Annually
<Badge component="span" variant="light" color="blue">
15% OFF
</Badge>
</Text>
</Flex>
</Group>
</Group>
</Stack>
{/* Plans Grid */}
<Group justify="center" gap="lg" align="stretch">
{plans.map((plan, index) => {
const tieredPlan = plan;
const planSelectedTier =
tieredPlan.pricingTiers.find(
(tier) => tier.upTo.toString() === selectedTierValue,
) || tieredPlan.pricingTiers[0];
const price = isAnnual
? planSelectedTier.yearly
: planSelectedTier.monthly;
const priceId = isAnnual ? plan.yearlyId : plan.monthlyId;
return ( return (
<Card <Card
key={plan.name} key={plan.name}
withBorder withBorder
radius="lg" radius="md"
shadow="sm" shadow="sm"
p="xl" p="xl"
w={350} w={300}
miw={300}
style={{
position: "relative",
}}
> >
<Stack gap="lg"> <SegmentedControl
{/* Plan Header */} value={interval}
<Stack gap="xs"> onChange={setInterval}
<Title order={3} size="h4"> fullWidth
data={[
{ label: "Monthly", value: "monthly" },
{ label: "Yearly (25% OFF)", value: "yearly" },
]}
/>
<Title order={3} ta="center" mt="sm" mb="xs">
{plan.name} {plan.name}
</Title> </Title>
{plan.description && ( <Text ta="center" size="lg" fw={700}>
<Text size="sm" c="dimmed"> {interval === "monthly" && (
{plan.description} <>
${price}{" "}
<Text span size="sm" fw={500} c="dimmed">
/user/month
</Text> </Text>
</>
)} )}
</Stack> {interval === "yearly" && (
<>
{/* Pricing */} ${yearlyMonthPrice}{" "}
<Stack gap="xs"> <Text span size="sm" fw={500} c="dimmed">
<Group align="baseline" gap="xs"> /user/month
<Title order={1} size="h1">
${isAnnual ? (price / 12).toFixed(0) : price}
</Title>
<Text size="lg" c="dimmed">
per {isAnnual ? "month" : "month"}
</Text>
</Group>
{isAnnual && (
<Text size="sm" c="dimmed">
Billed annually
</Text> </Text>
</>
)} )}
<Text size="md" fw={500}> <br/>
For {planSelectedTier.upTo} users <Text span ta="center" size="md" fw={500} c="dimmed">
billed {interval}
</Text>
</Text> </Text>
</Stack>
{/* CTA Button */} <Card.Section mt="lg">
<Button onClick={() => handleCheckout(priceId)} fullWidth> <Button onClick={() => handleCheckout(priceId)} fullWidth>
Upgrade Subscribe
</Button> </Button>
</Card.Section>
{/* Features */} <Card.Section mt="md">
<List <List
spacing="xs" spacing="xs"
size="sm" size="sm"
center
icon={ icon={
<ThemeIcon size={20} radius="xl"> <ThemeIcon variant="light" size={24} radius="xl">
<IconCheck size={14} /> <IconCheck size={16} />
</ThemeIcon> </ThemeIcon>
} }
> >
{plan.features.map((feature, featureIndex) => ( {plan.features.map((feature, index) => (
<List.Item key={featureIndex}>{feature}</List.Item> <List.Item key={index}>{feature}</List.Item>
))} ))}
</List> </List>
</Stack> </Card.Section>
</Card> </Card>
); );
})} })}
</Group> </Group>
</Container>
); );
} }

View File

@ -25,11 +25,6 @@ export interface IBilling {
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
deletedAt: Date; deletedAt: Date;
billingScheme: string | null;
tieredUpTo: string | null;
tieredFlatAmount: number | null;
tieredUnitAmount: number | null;
planName: string | null;
} }
export interface ICheckoutLink { export interface ICheckoutLink {
@ -47,18 +42,9 @@ export interface IBillingPlan {
monthlyId: string; monthlyId: string;
yearlyId: string; yearlyId: string;
currency: string; currency: string;
price?: { price: {
monthly: string; monthly: string;
yearly: string; yearly: string;
}; };
features: string[]; features: string[];
billingScheme: string | null;
pricingTiers: PricingTier[];
}
interface PricingTier {
upTo: number;
monthly?: number;
yearly?: number;
custom?: boolean;
} }

View File

@ -1,41 +0,0 @@
import { usePostHog } from "posthog-js/react";
import { useEffect } from "react";
import { useAtom } from "jotai";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
export function PosthogUser() {
const posthog = usePostHog();
const [currentUser] = useAtom(currentUserAtom);
useEffect(() => {
if (currentUser) {
const user = currentUser?.user;
const workspace = currentUser?.workspace;
if (!user || !workspace) return;
posthog?.identify(user.id, {
name: user.name,
email: user.email,
workspaceId: user.workspaceId,
workspaceHostname: workspace.hostname,
lastActiveAt: new Date().toISOString(),
createdAt: user.createdAt,
source: "docmost-app",
});
posthog?.group("workspace", workspace.id, {
name: workspace.name,
hostname: workspace.hostname,
plan: workspace?.plan,
status: workspace.status,
isOnTrial: !!workspace.trialEndAt,
hasStripeCustomerId: !!workspace.stripeCustomerId,
memberCount: workspace.memberCount,
lastActiveAt: new Date().toISOString(),
createdAt: workspace.createdAt,
source: "docmost-app",
});
}
}, [posthog, currentUser]);
return null;
}

View File

@ -18,7 +18,6 @@ import classes from "@/features/auth/components/auth.module.css";
import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-query.ts"; import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-query.ts";
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts"; import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import SsoLogin from "@/ee/components/sso-login.tsx";
const formSchema = z.object({ const formSchema = z.object({
name: z.string().trim().min(1), name: z.string().trim().min(1),
@ -72,9 +71,6 @@ export function InviteSignUpForm() {
{t("Join the workspace")} {t("Join the workspace")}
</Title> </Title>
<SsoLogin />
{!invitation.enforceSso && (
<Stack align="stretch" justify="center" gap="xl"> <Stack align="stretch" justify="center" gap="xl">
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
<TextInput <TextInput
@ -108,7 +104,6 @@ export function InviteSignUpForm() {
</Button> </Button>
</form> </form>
</Stack> </Stack>
)}
</Box> </Box>
</Container> </Container>
); );

View File

@ -21,7 +21,7 @@ import { Link } from "react-router-dom";
import APP_ROUTE from "@/lib/app-route.ts"; import APP_ROUTE from "@/lib/app-route.ts";
const formSchema = z.object({ const formSchema = z.object({
workspaceName: z.string().trim().max(50).optional(), workspaceName: z.string().trim().min(3).max(50),
name: z.string().min(1).max(50), name: z.string().min(1).max(50),
email: z email: z
.string() .string()
@ -60,7 +60,6 @@ export function SetupWorkspaceForm() {
{isCloud() && <SsoCloudSignup />} {isCloud() && <SsoCloudSignup />}
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
{!isCloud() && (
<TextInput <TextInput
id="workspaceName" id="workspaceName"
type="text" type="text"
@ -70,7 +69,6 @@ export function SetupWorkspaceForm() {
mt="md" mt="md"
{...form.getInputProps("workspaceName")} {...form.getInputProps("workspaceName")}
/> />
)}
<TextInput <TextInput
id="name" id="name"

View File

@ -10,7 +10,7 @@ export interface IRegister {
} }
export interface ISetupWorkspace { export interface ISetupWorkspace {
workspaceName?: string; workspaceName: string;
name: string; name: string;
email: string; email: string;
password: string; password: string;

View File

@ -12,12 +12,6 @@
padding: 8px; padding: 8px;
background: var(--mantine-color-gray-light); background: var(--mantine-color-gray-light);
cursor: pointer; cursor: pointer;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
-ms-word-break: break-word;
max-width: 100%;
box-sizing: border-box;
} }
.commentEditor { .commentEditor {

View File

@ -156,11 +156,13 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
) )
} }
onClick={() => { onClick={() => {
if (name === "Default") {
editor.commands.unsetColor(); editor.commands.unsetColor();
} else { name !== "Default" &&
editor.chain().focus().setColor(color || "").run(); editor
} .chain()
.focus()
.setColor(color || "")
.run();
setIsOpen(false); setIsOpen(false);
}} }}
style={{ border: "none" }} style={{ border: "none" }}

View File

@ -32,7 +32,7 @@ const schema = z.object({
export default function EmbedView(props: NodeViewProps) { export default function EmbedView(props: NodeViewProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { node, selected, updateAttributes, editor } = props; const { node, selected, updateAttributes } = props;
const { src, provider } = node.attrs; const { src, provider } = node.attrs;
const embedUrl = useMemo(() => { const embedUrl = useMemo(() => {
@ -50,10 +50,6 @@ export default function EmbedView(props: NodeViewProps) {
}); });
async function onSubmit(data: { url: string }) { async function onSubmit(data: { url: string }) {
if (!editor.isEditable) {
return;
}
if (provider) { if (provider) {
const embedProvider = getEmbedProviderById(provider); const embedProvider = getEmbedProviderById(provider);
if (embedProvider.id === "iframe") { if (embedProvider.id === "iframe") {
@ -89,13 +85,7 @@ export default function EmbedView(props: NodeViewProps) {
</AspectRatio> </AspectRatio>
</> </>
) : ( ) : (
<Popover <Popover width={300} position="bottom" withArrow shadow="md">
width={300}
position="bottom"
withArrow
shadow="md"
disabled={!editor.isEditable}
>
<Popover.Target> <Popover.Target>
<Card <Card
radius="md" radius="md"

View File

@ -1,9 +0,0 @@
import { atom } from "jotai";
type SearchAndReplaceAtomType = {
isOpen: boolean;
};
export const searchAndReplaceStateAtom = atom<SearchAndReplaceAtomType>({
isOpen: false,
});

View File

@ -1,312 +0,0 @@
import {
ActionIcon,
Button,
Dialog,
Flex,
Input,
Stack,
Text,
Tooltip,
} from "@mantine/core";
import {
IconArrowNarrowDown,
IconArrowNarrowUp,
IconLetterCase,
IconReplace,
IconSearch,
IconX,
} from "@tabler/icons-react";
import { useEditor } from "@tiptap/react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { searchAndReplaceStateAtom } from "@/features/editor/components/search-and-replace/atoms/search-and-replace-state-atom.ts";
import { useAtom } from "jotai";
import { useTranslation } from "react-i18next";
import { getHotkeyHandler, useToggle } from "@mantine/hooks";
import { useLocation } from "react-router-dom";
import classes from "./search-replace.module.css";
interface PageFindDialogDialogProps {
editor: ReturnType<typeof useEditor>;
editable?: boolean;
}
function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialogProps) {
const { t } = useTranslation();
const [searchText, setSearchText] = useState("");
const [replaceText, setReplaceText] = useState("");
const [pageFindState, setPageFindState] = useAtom(searchAndReplaceStateAtom);
const inputRef = useRef(null);
const [replaceButton, replaceButtonToggle] = useToggle([
{ isReplaceShow: false, color: "gray" },
{ isReplaceShow: true, color: "blue" },
]);
const [caseSensitive, caseSensitiveToggle] = useToggle([
{ isCaseSensitive: false, color: "gray" },
{ isCaseSensitive: true, color: "blue" },
]);
const searchInputEvent = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchText(event.target.value);
};
const replaceInputEvent = (event: React.ChangeEvent<HTMLInputElement>) => {
setReplaceText(event.target.value);
};
const closeDialog = () => {
setSearchText("");
setReplaceText("");
setPageFindState({ isOpen: false });
// Reset replace button state when closing
if (replaceButton.isReplaceShow) {
replaceButtonToggle();
}
// Clear search term in editor
if (editor) {
editor.commands.setSearchTerm("");
}
};
const goToSelection = () => {
if (!editor) return;
const { results, resultIndex } = editor.storage.searchAndReplace;
const position: Range = results[resultIndex];
if (!position) return;
// @ts-ignore
editor.commands.setTextSelection(position);
const element = document.querySelector(".search-result-current");
if (element)
element.scrollIntoView({ behavior: "smooth", block: "center" });
editor.commands.setTextSelection(0);
};
const next = () => {
editor.commands.nextSearchResult();
goToSelection();
};
const previous = () => {
editor.commands.previousSearchResult();
goToSelection();
};
const replace = () => {
editor.commands.setReplaceTerm(replaceText);
editor.commands.replace();
goToSelection();
};
const replaceAll = () => {
editor.commands.setReplaceTerm(replaceText);
editor.commands.replaceAll();
};
useEffect(() => {
editor.commands.setSearchTerm(searchText);
editor.commands.resetIndex();
editor.commands.selectCurrentItem();
}, [searchText]);
const handleOpenEvent = (e) => {
setPageFindState({ isOpen: true });
const selectedText = editor.state.doc.textBetween(
editor.state.selection.from,
editor.state.selection.to,
);
if (selectedText !== "") {
setSearchText(selectedText);
}
inputRef.current?.focus();
inputRef.current?.select();
};
const handleCloseEvent = (e) => {
closeDialog();
};
useEffect(() => {
!pageFindState.isOpen && closeDialog();
document.addEventListener("openFindDialogFromEditor", handleOpenEvent);
document.addEventListener("closeFindDialogFromEditor", handleCloseEvent);
return () => {
document.removeEventListener("openFindDialogFromEditor", handleOpenEvent);
document.removeEventListener(
"closeFindDialogFromEditor",
handleCloseEvent,
);
};
}, [pageFindState.isOpen]);
useEffect(() => {
editor.commands.setCaseSensitive(caseSensitive.isCaseSensitive);
editor.commands.resetIndex();
goToSelection();
}, [caseSensitive]);
const resultsCount = useMemo(
() =>
searchText.trim() === ""
? ""
: editor?.storage?.searchAndReplace?.results.length > 0
? editor?.storage?.searchAndReplace?.resultIndex +
1 +
"/" +
editor?.storage?.searchAndReplace?.results.length
: t("Not found"),
[
searchText,
editor?.storage?.searchAndReplace?.resultIndex,
editor?.storage?.searchAndReplace?.results.length,
],
);
const location = useLocation();
useEffect(() => {
closeDialog();
}, [location]);
return (
<Dialog
className={classes.findDialog}
opened={pageFindState.isOpen}
size="lg"
radius="md"
w={"auto"}
position={{ top: 90, right: 50 }}
withBorder
transitionProps={{ transition: "slide-down" }}
>
<Stack gap="xs">
<Flex align="center" gap="xs">
<Input
ref={inputRef}
placeholder={t("Find")}
leftSection={<IconSearch size={16} />}
rightSection={
<Text size="xs" ta="right">
{resultsCount}
</Text>
}
rightSectionWidth="70"
rightSectionPointerEvents="all"
size="xs"
w={220}
onChange={searchInputEvent}
value={searchText}
autoFocus
onKeyDown={getHotkeyHandler([
["Enter", next],
["shift+Enter", previous],
["alt+C", caseSensitiveToggle],
//@ts-ignore
...(editable ? [["alt+R", replaceButtonToggle]] : []),
])}
/>
<ActionIcon.Group>
<Tooltip label={t("Previous match (Shift+Enter)")}>
<ActionIcon variant="subtle" color="gray" onClick={previous}>
<IconArrowNarrowUp
style={{ width: "70%", height: "70%" }}
stroke={1.5}
/>
</ActionIcon>
</Tooltip>
<Tooltip label={t("Next match (Enter)")}>
<ActionIcon variant="subtle" color="gray" onClick={next}>
<IconArrowNarrowDown
style={{ width: "70%", height: "70%" }}
stroke={1.5}
/>
</ActionIcon>
</Tooltip>
<Tooltip label={t("Match case (Alt+C)")}>
<ActionIcon
variant="subtle"
color={caseSensitive.color}
onClick={() => caseSensitiveToggle()}
>
<IconLetterCase
style={{ width: "70%", height: "70%" }}
stroke={1.5}
/>
</ActionIcon>
</Tooltip>
{editable && (
<Tooltip label={t("Replace")}>
<ActionIcon
variant="subtle"
color={replaceButton.color}
onClick={() => replaceButtonToggle()}
>
<IconReplace
style={{ width: "70%", height: "70%" }}
stroke={1.5}
/>
</ActionIcon>
</Tooltip>
)}
<Tooltip label={t("Close (Escape)")}>
<ActionIcon variant="subtle" color="gray" onClick={closeDialog}>
<IconX style={{ width: "70%", height: "70%" }} stroke={1.5} />
</ActionIcon>
</Tooltip>
</ActionIcon.Group>
</Flex>
{replaceButton.isReplaceShow && editable && (
<Flex align="center" gap="xs">
<Input
placeholder={t("Replace")}
leftSection={<IconReplace size={16} />}
rightSection={<div></div>}
rightSectionPointerEvents="all"
size="xs"
w={180}
autoFocus
onChange={replaceInputEvent}
value={replaceText}
onKeyDown={getHotkeyHandler([
["Enter", replace],
["ctrl+alt+Enter", replaceAll],
])}
/>
<ActionIcon.Group>
<Tooltip label={t("Replace (Enter)")}>
<Button
size="xs"
variant="subtle"
color="gray"
onClick={replace}
>
{t("Replace")}
</Button>
</Tooltip>
<Tooltip label={t("Replace all (Ctrl+Alt+Enter)")}>
<Button
size="xs"
variant="subtle"
color="gray"
onClick={replaceAll}
>
{t("Replace all")}
</Button>
</Tooltip>
</ActionIcon.Group>
</Flex>
)}
</Stack>
</Dialog>
);
}
export default SearchAndReplaceDialog;

View File

@ -1,10 +0,0 @@
.findDialog{
@media print {
display: none;
}
}
.findDialog div[data-position="right"].mantine-Input-section {
justify-content: right;
padding-right: 8px;
}

View File

@ -36,7 +36,6 @@ import {
Drawio, Drawio,
Excalidraw, Excalidraw,
Embed, Embed,
SearchAndReplace,
Mention, Mention,
} from "@docmost/editor-ext"; } from "@docmost/editor-ext";
import { import {
@ -74,7 +73,6 @@ import i18n from "@/i18n.ts";
import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboard.ts"; import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboard.ts";
import EmojiCommand from "./emoji-command"; import EmojiCommand from "./emoji-command";
import { CharacterCount } from "@tiptap/extension-character-count"; import { CharacterCount } from "@tiptap/extension-character-count";
import { countWords } from "alfaaz";
const lowlight = createLowlight(common); const lowlight = createLowlight(common);
lowlight.register("mermaid", plaintext); lowlight.register("mermaid", plaintext);
@ -215,25 +213,7 @@ export const mainExtensions = [
MarkdownClipboard.configure({ MarkdownClipboard.configure({
transformPastedText: true, transformPastedText: true,
}), }),
CharacterCount.configure({ CharacterCount
wordCounter: (text) => countWords(text),
}),
SearchAndReplace.extend({
addKeyboardShortcuts() {
return {
'Mod-f': () => {
const event = new CustomEvent("openFindDialogFromEditor", {});
document.dispatchEvent(event);
return true;
},
'Escape': () => {
const event = new CustomEvent("closeFindDialogFromEditor", {});
document.dispatchEvent(event);
return true;
},
}
},
}).configure(),
] as any; ] as any;
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[]; type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];

View File

@ -42,11 +42,7 @@ export function FullEditor({
spaceSlug={spaceSlug} spaceSlug={spaceSlug}
editable={editable} editable={editable}
/> />
<MemoizedPageEditor <MemoizedPageEditor pageId={pageId} editable={editable} content={content} />
pageId={pageId}
editable={editable}
content={content}
/>
</Container> </Container>
); );
} }

View File

@ -1,5 +1,11 @@
import "@/features/editor/styles/index.css"; import "@/features/editor/styles/index.css";
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { IndexeddbPersistence } from "y-indexeddb"; import { IndexeddbPersistence } from "y-indexeddb";
import * as Y from "yjs"; import * as Y from "yjs";
import { import {
@ -39,7 +45,6 @@ import LinkMenu from "@/features/editor/components/link/link-menu.tsx";
import ExcalidrawMenu from "./components/excalidraw/excalidraw-menu"; import ExcalidrawMenu from "./components/excalidraw/excalidraw-menu";
import DrawioMenu from "./components/drawio/drawio-menu"; import DrawioMenu from "./components/drawio/drawio-menu";
import { useCollabToken } from "@/features/auth/queries/auth-query.tsx"; import { useCollabToken } from "@/features/auth/queries/auth-query.tsx";
import SearchAndReplaceDialog from "@/features/editor/components/search-and-replace/search-and-replace-dialog.tsx";
import { useDebouncedCallback, useDocumentVisibility } from "@mantine/hooks"; import { useDebouncedCallback, useDocumentVisibility } from "@mantine/hooks";
import { useIdle } from "@/hooks/use-idle.ts"; import { useIdle } from "@/hooks/use-idle.ts";
import { queryClient } from "@/main.tsx"; import { queryClient } from "@/main.tsx";
@ -47,7 +52,6 @@ import { IPage } from "@/features/page/types/page.types.ts";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { extractPageSlugId } from "@/lib"; import { extractPageSlugId } from "@/lib";
import { FIVE_MINUTES } from "@/lib/constants.ts"; import { FIVE_MINUTES } from "@/lib/constants.ts";
import { PageEditMode } from "@/features/user/types/user.types.ts";
import { jwtDecode } from "jwt-decode"; import { jwtDecode } from "jwt-decode";
interface PageEditorProps { interface PageEditorProps {
@ -67,11 +71,7 @@ export default function PageEditor({
const [, setAsideState] = useAtom(asideStateAtom); const [, setAsideState] = useAtom(asideStateAtom);
const [, setActiveCommentId] = useAtom(activeCommentIdAtom); const [, setActiveCommentId] = useAtom(activeCommentIdAtom);
const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom); const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom);
const ydocRef = useRef<Y.Doc | null>(null); const ydoc = useMemo(() => new Y.Doc(), [pageId]);
if (!ydocRef.current) {
ydocRef.current = new Y.Doc();
}
const ydoc = ydocRef.current;
const [isLocalSynced, setLocalSynced] = useState(false); const [isLocalSynced, setLocalSynced] = useState(false);
const [isRemoteSynced, setRemoteSynced] = useState(false); const [isRemoteSynced, setRemoteSynced] = useState(false);
const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom( const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom(
@ -85,56 +85,31 @@ export default function PageEditor({
const [isCollabReady, setIsCollabReady] = useState(false); const [isCollabReady, setIsCollabReady] = useState(false);
const { pageSlug } = useParams(); const { pageSlug } = useParams();
const slugId = extractPageSlugId(pageSlug); const slugId = extractPageSlugId(pageSlug);
const userPageEditMode =
currentUser?.user?.settings?.preferences?.pageEditMode ?? PageEditMode.Edit;
// Providers only created once per pageId const localProvider = useMemo(() => {
const providersRef = useRef<{ const provider = new IndexeddbPersistence(documentName, ydoc);
local: IndexeddbPersistence;
remote: HocuspocusProvider;
} | null>(null);
const [providersReady, setProvidersReady] = useState(false);
const localProvider = providersRef.current?.local; provider.on("synced", () => {
const remoteProvider = providersRef.current?.remote; setLocalSynced(true);
});
// Track when collaborative provider is ready and synced return provider;
const [collabReady, setCollabReady] = useState(false); }, [pageId, ydoc]);
useEffect(() => {
if (
remoteProvider?.status === WebSocketStatus.Connected &&
isLocalSynced &&
isRemoteSynced
) {
setCollabReady(true);
}
}, [remoteProvider?.status, isLocalSynced, isRemoteSynced]);
useEffect(() => { const remoteProvider = useMemo(() => {
if (!providersRef.current) { const provider = new HocuspocusProvider({
const local = new IndexeddbPersistence(documentName, ydoc);
local.on("synced", () => setLocalSynced(true));
const remote = new HocuspocusProvider({
name: documentName, name: documentName,
url: collaborationURL, url: collaborationURL,
document: ydoc, document: ydoc,
token: collabQuery?.token, token: collabQuery?.token,
connect: true, connect: false,
preserveConnection: false, preserveConnection: false,
onAuthenticationFailed: (auth: onAuthenticationFailedParameters) => { onAuthenticationFailed: (auth: onAuthenticationFailedParameters) => {
const payload = jwtDecode(collabQuery?.token); const payload = jwtDecode(collabQuery?.token);
const now = Date.now().valueOf() / 1000; const now = Date.now().valueOf() / 1000;
const isTokenExpired = now >= payload.exp; const isTokenExpired = now >= payload.exp;
if (isTokenExpired) { if (isTokenExpired) {
refetchCollabToken().then((result) => { refetchCollabToken();
if (result.data?.token) {
remote.disconnect();
setTimeout(() => {
remote.configuration.token = result.data.token;
remote.connect();
}, 100);
}
});
} }
}, },
onStatus: (status) => { onStatus: (status) => {
@ -143,68 +118,34 @@ export default function PageEditor({
} }
}, },
}); });
remote.on("synced", () => setRemoteSynced(true));
remote.on("disconnect", () => { provider.on("synced", () => {
setRemoteSynced(true);
});
provider.on("disconnect", () => {
setYjsConnectionStatus(WebSocketStatus.Disconnected); setYjsConnectionStatus(WebSocketStatus.Disconnected);
}); });
providersRef.current = { local, remote };
setProvidersReady(true);
} else {
setProvidersReady(true);
}
// Only destroy on final unmount
return () => {
providersRef.current?.remote.destroy();
providersRef.current?.local.destroy();
providersRef.current = null;
};
}, [pageId]);
/* return provider;
useEffect(() => { }, [ydoc, pageId, collabQuery?.token]);
// Handle token updates by reconnecting with new token
if (providersRef.current?.remote && collabQuery?.token) {
const currentToken = providersRef.current.remote.configuration.token;
if (currentToken !== collabQuery.token) {
// Token has changed, need to reconnect with new token
providersRef.current.remote.disconnect();
providersRef.current.remote.configuration.token = collabQuery.token;
providersRef.current.remote.connect();
}
}
}, [collabQuery?.token]);
*/
// Only connect/disconnect on tab/idle, not destroy useLayoutEffect(() => {
useEffect(() => {
if (!providersReady || !providersRef.current) return;
const remoteProvider = providersRef.current.remote;
if (
isIdle &&
documentState === "hidden" &&
remoteProvider.status === WebSocketStatus.Connected
) {
remoteProvider.disconnect();
setIsCollabReady(false);
return;
}
if (
documentState === "visible" &&
remoteProvider.status === WebSocketStatus.Disconnected
) {
resetIdle();
remoteProvider.connect(); remoteProvider.connect();
setTimeout(() => setIsCollabReady(true), 500); return () => {
} setRemoteSynced(false);
}, [isIdle, documentState, providersReady, resetIdle]); setLocalSynced(false);
remoteProvider.destroy();
localProvider.destroy();
};
}, [remoteProvider, localProvider]);
const extensions = useMemo(() => { const extensions = useMemo(() => {
if (!remoteProvider || !currentUser?.user) return mainExtensions;
return [ return [
...mainExtensions, ...mainExtensions,
...collabExtensions(remoteProvider, currentUser?.user), ...collabExtensions(remoteProvider, currentUser?.user),
]; ];
}, [remoteProvider, currentUser?.user]); }, [ydoc, pageId, remoteProvider, currentUser?.user]);
const editor = useEditor( const editor = useEditor(
{ {
@ -258,7 +199,7 @@ export default function PageEditor({
debouncedUpdateContent(editorJson); debouncedUpdateContent(editorJson);
}, },
}, },
[pageId, editable, remoteProvider], [pageId, editable, remoteProvider?.status],
); );
const debouncedUpdateContent = useDebouncedCallback((newContent: any) => { const debouncedUpdateContent = useDebouncedCallback((newContent: any) => {
@ -311,6 +252,29 @@ export default function PageEditor({
} }
}, [remoteProvider?.status]); }, [remoteProvider?.status]);
useEffect(() => {
if (
isIdle &&
documentState === "hidden" &&
remoteProvider?.status === WebSocketStatus.Connected
) {
remoteProvider.disconnect();
setIsCollabReady(false);
return;
}
if (
documentState === "visible" &&
remoteProvider?.status === WebSocketStatus.Disconnected
) {
resetIdle();
remoteProvider.connect();
setTimeout(() => {
setIsCollabReady(true);
}, 600);
}
}, [isIdle, documentState, remoteProvider]);
const isSynced = isLocalSynced && isRemoteSynced; const isSynced = isLocalSynced && isRemoteSynced;
useEffect(() => { useEffect(() => {
@ -326,54 +290,11 @@ export default function PageEditor({
return () => clearTimeout(collabReadyTimeout); return () => clearTimeout(collabReadyTimeout);
}, [isRemoteSynced, isLocalSynced, remoteProvider?.status]); }, [isRemoteSynced, isLocalSynced, remoteProvider?.status]);
useEffect(() => { return isCollabReady ? (
// Only honor user default page edit mode preference and permissions <div>
if (editor) {
if (userPageEditMode && editable) {
if (userPageEditMode === PageEditMode.Edit) {
editor.setEditable(true);
} else if (userPageEditMode === PageEditMode.Read) {
editor.setEditable(false);
}
} else {
editor.setEditable(false);
}
}
}, [userPageEditMode, editor, editable]);
const hasConnectedOnceRef = useRef(false);
const [showStatic, setShowStatic] = useState(true);
useEffect(() => {
if (
!hasConnectedOnceRef.current &&
remoteProvider?.status === WebSocketStatus.Connected
) {
hasConnectedOnceRef.current = true;
setShowStatic(false);
}
}, [remoteProvider?.status]);
if (showStatic) {
return (
<EditorProvider
editable={false}
immediatelyRender={true}
extensions={mainExtensions}
content={content}
/>
);
}
return (
<div style={{ position: "relative" }}>
<div ref={menuContainerRef}> <div ref={menuContainerRef}>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
{editor && (
<SearchAndReplaceDialog editor={editor} editable={editable} />
)}
{editor && editor.isEditable && ( {editor && editor.isEditable && (
<div> <div>
<EditorBubbleMenu editor={editor} /> <EditorBubbleMenu editor={editor} />
@ -387,12 +308,21 @@ export default function PageEditor({
<LinkMenu editor={editor} appendTo={menuContainerRef} /> <LinkMenu editor={editor} appendTo={menuContainerRef} />
</div> </div>
)} )}
{showCommentPopup && <CommentDialog editor={editor} pageId={pageId} />} {showCommentPopup && <CommentDialog editor={editor} pageId={pageId} />}
</div> </div>
<div <div
onClick={() => editor.commands.focus("end")} onClick={() => editor.commands.focus("end")}
style={{ paddingBottom: "20vh" }} style={{ paddingBottom: "20vh" }}
></div> ></div>
</div> </div>
) : (
<EditorProvider
editable={false}
immediatelyRender={true}
extensions={mainExtensions}
content={content}
></EditorProvider>
); );
} }

View File

@ -71,12 +71,4 @@
[data-type="details"][open] > [data-type="detailsButton"] .ProseMirror-icon{ [data-type="details"][open] > [data-type="detailsButton"] .ProseMirror-icon{
transform: rotateZ(90deg); transform: rotateZ(90deg);
} }
[data-type="details"]:has(.search-result) > [data-type="detailsContainer"] > [data-type="detailsContent"]{
display: block;
}
[data-type="details"]:has(.search-result) > [data-type="detailsButton"] .ProseMirror-icon{
transform: rotateZ(90deg);
}
} }

View File

@ -1,9 +0,0 @@
.search-result{
background: #ffff65;
color: #212529;
}
.search-result-current{
background: #ffc266 !important;
color: #212529;
}

View File

@ -9,5 +9,5 @@
@import "./media.css"; @import "./media.css";
@import "./code.css"; @import "./code.css";
@import "./print.css"; @import "./print.css";
@import "./find.css";
@import "./mention.css"; @import "./mention.css";

View File

@ -10,11 +10,8 @@ import {
pageEditorAtom, pageEditorAtom,
titleEditorAtom, titleEditorAtom,
} from "@/features/editor/atoms/editor-atoms"; } from "@/features/editor/atoms/editor-atoms";
import { import { updatePageData, useUpdateTitlePageMutation } from "@/features/page/queries/page-query";
updatePageData, import { useDebouncedCallback } from "@mantine/hooks";
useUpdateTitlePageMutation,
} from "@/features/page/queries/page-query";
import { useDebouncedCallback, getHotkeyHandler } from "@mantine/hooks";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useQueryEmit } from "@/features/websocket/use-query-emit.ts"; import { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
import { History } from "@tiptap/extension-history"; import { History } from "@tiptap/extension-history";
@ -24,8 +21,6 @@ import { useTranslation } from "react-i18next";
import EmojiCommand from "@/features/editor/extensions/emoji-command.ts"; import EmojiCommand from "@/features/editor/extensions/emoji-command.ts";
import { UpdateEvent } from "@/features/websocket/types"; import { UpdateEvent } from "@/features/websocket/types";
import localEmitter from "@/lib/local-emitter.ts"; import localEmitter from "@/lib/local-emitter.ts";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
import { PageEditMode } from "@/features/user/types/user.types.ts";
export interface TitleEditorProps { export interface TitleEditorProps {
pageId: string; pageId: string;
@ -43,16 +38,12 @@ export function TitleEditor({
editable, editable,
}: TitleEditorProps) { }: TitleEditorProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { mutateAsync: updateTitlePageMutationAsync } = const { mutateAsync: updateTitlePageMutationAsync } = useUpdateTitlePageMutation();
useUpdateTitlePageMutation();
const pageEditor = useAtomValue(pageEditorAtom); const pageEditor = useAtomValue(pageEditorAtom);
const [, setTitleEditor] = useAtom(titleEditorAtom); const [, setTitleEditor] = useAtom(titleEditorAtom);
const emit = useQueryEmit(); const emit = useQueryEmit();
const navigate = useNavigate(); const navigate = useNavigate();
const [activePageId, setActivePageId] = useState(pageId); const [activePageId, setActivePageId] = useState(pageId);
const [currentUser] = useAtom(currentUserAtom);
const userPageEditMode =
currentUser?.user?.settings?.preferences?.pageEditMode ?? PageEditMode.Edit;
const titleEditor = useEditor({ const titleEditor = useEditor({
extensions: [ extensions: [
@ -112,12 +103,7 @@ export function TitleEditor({
spaceId: page.spaceId, spaceId: page.spaceId,
entity: ["pages"], entity: ["pages"],
id: page.id, id: page.id,
payload: { payload: { title: page.title, slugId: page.slugId, parentPageId: page.parentPageId, icon: page.icon },
title: page.title,
slugId: page.slugId,
parentPageId: page.parentPageId,
icon: page.icon,
},
}; };
if (page.title !== titleEditor.getText()) return; if (page.title !== titleEditor.getText()) return;
@ -150,30 +136,9 @@ export function TitleEditor({
}; };
}, [pageId]); }, [pageId]);
useEffect(() => { function handleTitleKeyDown(event) {
// honor user default page edit mode preference
if (userPageEditMode && titleEditor && editable) {
if (userPageEditMode === PageEditMode.Edit) {
titleEditor.setEditable(true);
} else if (userPageEditMode === PageEditMode.Read) {
titleEditor.setEditable(false);
}
}
}, [userPageEditMode, titleEditor, editable]);
const openSearchDialog = () => {
const event = new CustomEvent("openFindDialogFromEditor", {});
document.dispatchEvent(event);
};
function handleTitleKeyDown(event: any) {
if (!titleEditor || !pageEditor || event.shiftKey) return; if (!titleEditor || !pageEditor || event.shiftKey) return;
// Prevent focus shift when IME composition is active
// `keyCode === 229` is added to support Safari where `isComposing` may not be reliable
if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229)
return;
const { key } = event; const { key } = event;
const { $head } = titleEditor.state.selection; const { $head } = titleEditor.state.selection;
@ -187,16 +152,5 @@ export function TitleEditor({
} }
} }
return ( return <EditorContent editor={titleEditor} onKeyDown={handleTitleKeyDown} />;
<EditorContent
editor={titleEditor}
onKeyDown={(event) => {
// First handle the search hotkey
getHotkeyHandler([["mod+F", openSearchDialog]])(event);
// Then handle other key events
handleTitleKeyDown(event);
}}
/>
);
} }

View File

@ -22,9 +22,3 @@
max-width: 200px; max-width: 200px;
} }
.breadcrumbDiv {
overflow: hidden;
@media (max-width: $mantine-breakpoint-sm) {
overflow: visible;
}
}

View File

@ -161,7 +161,7 @@ export default function Breadcrumb() {
}; };
return ( return (
<div className={classes.breadcrumbDiv}> <div style={{ overflow: "hidden" }}>
{breadcrumbNodes && ( {breadcrumbNodes && (
<Breadcrumbs className={classes.breadcrumbs}> <Breadcrumbs className={classes.breadcrumbs}>
{isMobile ? getMobileBreadcrumbItems() : getBreadcrumbItems()} {isMobile ? getMobileBreadcrumbItems() : getBreadcrumbItems()}

View File

@ -9,7 +9,6 @@ import {
IconList, IconList,
IconMessage, IconMessage,
IconPrinter, IconPrinter,
IconSearch,
IconTrash, IconTrash,
IconWifiOff, IconWifiOff,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
@ -17,12 +16,7 @@ import React from "react";
import useToggleAside from "@/hooks/use-toggle-aside.tsx"; import useToggleAside from "@/hooks/use-toggle-aside.tsx";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts"; import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts";
import { import { useClipboard, useDisclosure } from "@mantine/hooks";
getHotkeyHandler,
useClipboard,
useDisclosure,
useHotkeys,
} from "@mantine/hooks";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { usePageQuery } from "@/features/page/queries/page-query.ts"; import { usePageQuery } from "@/features/page/queries/page-query.ts";
import { buildPageUrl } from "@/features/page/page.utils.ts"; import { buildPageUrl } from "@/features/page/page.utils.ts";
@ -38,9 +32,7 @@ import {
pageEditorAtom, pageEditorAtom,
yjsConnectionStatusAtom, yjsConnectionStatusAtom,
} from "@/features/editor/atoms/editor-atoms.ts"; } from "@/features/editor/atoms/editor-atoms.ts";
import { searchAndReplaceStateAtom } from "@/features/editor/components/search-and-replace/atoms/search-and-replace-state-atom.ts";
import { formattedDate, timeAgo } from "@/lib/time.ts"; import { formattedDate, timeAgo } from "@/lib/time.ts";
import { PageStateSegmentedControl } from "@/features/user/components/page-state-pref.tsx";
import MovePageModal from "@/features/page/components/move-page-modal.tsx"; import MovePageModal from "@/features/page/components/move-page-modal.tsx";
import { useTimeAgo } from "@/hooks/use-time-ago.tsx"; import { useTimeAgo } from "@/hooks/use-time-ago.tsx";
import ShareModal from "@/features/share/components/share-modal.tsx"; import ShareModal from "@/features/share/components/share-modal.tsx";
@ -53,26 +45,6 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
const toggleAside = useToggleAside(); const toggleAside = useToggleAside();
const [yjsConnectionStatus] = useAtom(yjsConnectionStatusAtom); const [yjsConnectionStatus] = useAtom(yjsConnectionStatusAtom);
useHotkeys(
[
[
"mod+F",
() => {
const event = new CustomEvent("openFindDialogFromEditor", {});
document.dispatchEvent(event);
},
],
[
"Escape",
() => {
const event = new CustomEvent("closeFindDialogFromEditor", {});
document.dispatchEvent(event);
},
],
],
[],
);
return ( return (
<> <>
{yjsConnectionStatus === "disconnected" && ( {yjsConnectionStatus === "disconnected" && (
@ -87,8 +59,6 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
</Tooltip> </Tooltip>
)} )}
{!readOnly && <PageStateSegmentedControl size="xs" />}
<ShareModal readOnly={readOnly} /> <ShareModal readOnly={readOnly} />
<Tooltip label={t("Comments")} openDelay={250} withArrow> <Tooltip label={t("Comments")} openDelay={250} withArrow>

View File

@ -9,19 +9,7 @@
inset-inline-start: var(--app-shell-navbar-offset, 0rem); inset-inline-start: var(--app-shell-navbar-offset, 0rem);
inset-inline-end: var(--app-shell-aside-offset, 0rem); inset-inline-end: var(--app-shell-aside-offset, 0rem);
@media (max-width: $mantine-breakpoint-sm) {
padding-left: var(--mantine-spacing-xs);
padding-right: var(--mantine-spacing-xs);
}
@media print { @media print {
display: none; display: none;
} }
} }
.group {
@media (max-width: $mantine-breakpoint-sm) {
gap: var(--mantine-spacing-sm);
padding-inline: 0 !important;
}
}

View File

@ -9,10 +9,10 @@ interface Props {
export default function PageHeader({ readOnly }: Props) { export default function PageHeader({ readOnly }: Props) {
return ( return (
<div className={classes.header}> <div className={classes.header}>
<Group justify="space-between" h="100%" px="md" wrap="nowrap" className={classes.group}> <Group justify="space-between" h="100%" px="md" wrap="nowrap">
<Breadcrumb /> <Breadcrumb />
<Group justify="flex-end" h="100%" px="md" wrap="nowrap" gap="var(--mantine-spacing-xs)"> <Group justify="flex-end" h="100%" px="md" wrap="nowrap">
<PageHeaderMenu readOnly={readOnly} /> <PageHeaderMenu readOnly={readOnly} />
</Group> </Group>
</Group> </Group>

View File

@ -65,7 +65,6 @@ export interface IPageInput {
icon: string; icon: string;
coverPhoto: string; coverPhoto: string;
position: string; position: string;
isLocked: boolean;
} }
export interface IExportPageParams { export interface IExportPageParams {

View File

@ -11,7 +11,7 @@ import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const formSchema = z.object({ const formSchema = z.object({
name: z.string().min(1).max(40), name: z.string().min(2).max(40),
}); });
type FormValues = z.infer<typeof formSchema>; type FormValues = z.infer<typeof formSchema>;

View File

@ -1,65 +0,0 @@
import { Group, Text, MantineSize, SegmentedControl } from "@mantine/core";
import { useAtom } from "jotai";
import { userAtom } from "@/features/user/atoms/current-user-atom.ts";
import { updateUser } from "@/features/user/services/user-service.ts";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { PageEditMode } from "@/features/user/types/user.types.ts";
export default function PageStatePref() {
const { t } = useTranslation();
return (
<Group justify="space-between" wrap="nowrap" gap="xl">
<div>
<Text size="md">{t("Default page edit mode")}</Text>
<Text size="sm" c="dimmed">
{t("Choose your preferred page edit mode. Avoid accidental edits.")}
</Text>
</div>
<PageStateSegmentedControl />
</Group>
);
}
interface PageStateSegmentedControlProps {
size?: MantineSize;
}
export function PageStateSegmentedControl({
size,
}: PageStateSegmentedControlProps) {
const { t } = useTranslation();
const [user, setUser] = useAtom(userAtom);
const pageEditMode =
user?.settings?.preferences?.pageEditMode ?? PageEditMode.Edit;
const [value, setValue] = useState(pageEditMode);
const handleChange = useCallback(
async (value: string) => {
const updatedUser = await updateUser({ pageEditMode: value });
setValue(value);
setUser(updatedUser);
},
[user, setUser],
);
useEffect(() => {
if (pageEditMode !== value) {
setValue(pageEditMode);
}
}, [pageEditMode, value]);
return (
<SegmentedControl
size={size}
value={value}
onChange={handleChange}
data={[
{ label: t("Edit"), value: PageEditMode.Edit },
{ label: t("Read"), value: PageEditMode.Read },
]}
/>
);
}

View File

@ -19,7 +19,6 @@ export interface IUser {
deactivatedAt: Date; deactivatedAt: Date;
deletedAt: Date; deletedAt: Date;
fullPageWidth: boolean; // used for update fullPageWidth: boolean; // used for update
pageEditMode: string; // used for update
} }
export interface ICurrentUser { export interface ICurrentUser {
@ -30,11 +29,5 @@ export interface ICurrentUser {
export interface IUserSettings { export interface IUserSettings {
preferences: { preferences: {
fullPageWidth: boolean; fullPageWidth: boolean;
pageEditMode: string;
}; };
} }
export enum PageEditMode {
Read = "read",
Edit = "edit",
}

View File

@ -11,7 +11,7 @@ import useUserRole from "@/hooks/use-user-role.tsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const formSchema = z.object({ const formSchema = z.object({
name: z.string().min(1), name: z.string().min(4),
}); });
type FormValues = z.infer<typeof formSchema>; type FormValues = z.infer<typeof formSchema>;

View File

@ -173,7 +173,7 @@ export function useRevokeInvitationMutation() {
export function useGetInvitationQuery( export function useGetInvitationQuery(
invitationId: string, invitationId: string,
): UseQueryResult<IInvitation, Error> { ): UseQueryResult<any, Error> {
return useQuery({ return useQuery({
queryKey: ["invitations", invitationId], queryKey: ["invitations", invitationId],
queryFn: () => getInvitationById({ invitationId }), queryFn: () => getInvitationById({ invitationId }),

View File

@ -12,7 +12,6 @@ export interface IWorkspace {
settings: any; settings: any;
status: string; status: string;
enforceSso: boolean; enforceSso: boolean;
stripeCustomerId: string;
billingEmail: string; billingEmail: string;
trialEndAt: Date; trialEndAt: Date;
createdAt: Date; createdAt: Date;
@ -36,7 +35,6 @@ export interface IInvitation {
workspaceId: string; workspaceId: string;
invitedById: string; invitedById: string;
createdAt: Date; createdAt: Date;
enforceSso: boolean;
} }
export interface IInvitationLink { export interface IInvitationLink {

View File

@ -83,18 +83,6 @@ export function getBillingTrialDays() {
return getConfigValue("BILLING_TRIAL_DAYS"); return getConfigValue("BILLING_TRIAL_DAYS");
} }
export function getPostHogHost() {
return getConfigValue("POSTHOG_HOST");
}
export function isPostHogEnabled(): boolean {
return Boolean(getPostHogHost() && getPostHogKey());
}
export function getPostHogKey() {
return getConfigValue("POSTHOG_KEY");
}
function getConfigValue(key: string, defaultValue: string = undefined): string { function getConfigValue(key: string, defaultValue: string = undefined): string {
const rawValue = import.meta.env.DEV const rawValue = import.meta.env.DEV
? process?.env?.[key] ? process?.env?.[key]

View File

@ -3,7 +3,7 @@ import "@mantine/spotlight/styles.css";
import "@mantine/notifications/styles.css"; import "@mantine/notifications/styles.css";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./App.tsx"; import App from "./App.tsx";
import { mantineCssResolver, theme } from "@/theme"; import { mantineCssResolver, theme } from '@/theme';
import { MantineProvider } from "@mantine/core"; import { MantineProvider } from "@mantine/core";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { ModalsProvider } from "@mantine/modals"; import { ModalsProvider } from "@mantine/modals";
@ -11,14 +11,6 @@ import { Notifications } from "@mantine/notifications";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { HelmetProvider } from "react-helmet-async"; import { HelmetProvider } from "react-helmet-async";
import "./i18n"; import "./i18n";
import { PostHogProvider } from "posthog-js/react";
import {
getPostHogHost,
getPostHogKey,
isCloud,
isPostHogEnabled,
} from "@/lib/config.ts";
import posthog from "posthog-js";
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@ -31,17 +23,9 @@ export const queryClient = new QueryClient({
}, },
}); });
if (isCloud() && isPostHogEnabled) {
posthog.init(getPostHogKey(), {
api_host: getPostHogHost(),
defaults: "2025-05-24",
disable_session_recording: true,
capture_pageleave: false,
});
}
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement, document.getElementById("root") as HTMLElement
); );
root.render( root.render(
@ -51,12 +35,10 @@ root.render(
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Notifications position="bottom-center" limit={3} /> <Notifications position="bottom-center" limit={3} />
<HelmetProvider> <HelmetProvider>
<PostHogProvider client={posthog}>
<App /> <App />
</PostHogProvider>
</HelmetProvider> </HelmetProvider>
</QueryClientProvider> </QueryClientProvider>
</ModalsProvider> </ModalsProvider>
</MantineProvider> </MantineProvider>
</BrowserRouter>, </BrowserRouter>
); );

View File

@ -12,11 +12,6 @@ import {
SpaceCaslSubject, SpaceCaslSubject,
} from "@/features/space/permissions/permissions.type.ts"; } from "@/features/space/permissions/permissions.type.ts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import React from "react";
const MemoizedFullEditor = React.memo(FullEditor);
const MemoizedPageHeader = React.memo(PageHeader);
const MemoizedHistoryModal = React.memo(HistoryModal);
export default function Page() { export default function Page() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -54,14 +49,14 @@ export default function Page() {
<title>{`${page?.icon || ""} ${page?.title || t("untitled")}`}</title> <title>{`${page?.icon || ""} ${page?.title || t("untitled")}`}</title>
</Helmet> </Helmet>
<MemoizedPageHeader <PageHeader
readOnly={spaceAbility.cannot( readOnly={spaceAbility.cannot(
SpaceCaslAction.Manage, SpaceCaslAction.Manage,
SpaceCaslSubject.Page, SpaceCaslSubject.Page,
)} )}
/> />
<MemoizedFullEditor <FullEditor
key={page.id} key={page.id}
pageId={page.id} pageId={page.id}
title={page.title} title={page.title}
@ -73,7 +68,7 @@ export default function Page() {
SpaceCaslSubject.Page, SpaceCaslSubject.Page,
)} )}
/> />
<MemoizedHistoryModal pageId={page.id} /> <HistoryModal pageId={page.id} />
</div> </div>
) )
); );

View File

@ -2,7 +2,6 @@ import SettingsTitle from "@/components/settings/settings-title.tsx";
import AccountLanguage from "@/features/user/components/account-language.tsx"; import AccountLanguage from "@/features/user/components/account-language.tsx";
import AccountTheme from "@/features/user/components/account-theme.tsx"; import AccountTheme from "@/features/user/components/account-theme.tsx";
import PageWidthPref from "@/features/user/components/page-width-pref.tsx"; import PageWidthPref from "@/features/user/components/page-width-pref.tsx";
import PageEditPref from "@/features/user/components/page-state-pref";
import { getAppName } from "@/lib/config.ts"; import { getAppName } from "@/lib/config.ts";
import { Divider } from "@mantine/core"; import { Divider } from "@mantine/core";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
@ -29,10 +28,6 @@ export default function AccountPreferences() {
<Divider my={"md"} /> <Divider my={"md"} />
<PageWidthPref /> <PageWidthPref />
<Divider my={"md"} />
<PageEditPref />
</> </>
); );
} }

View File

@ -14,8 +14,6 @@ export default defineConfig(({ mode }) => {
SUBDOMAIN_HOST, SUBDOMAIN_HOST,
COLLAB_URL, COLLAB_URL,
BILLING_TRIAL_DAYS, BILLING_TRIAL_DAYS,
POSTHOG_HOST,
POSTHOG_KEY,
} = loadEnv(mode, envPath, ""); } = loadEnv(mode, envPath, "");
return { return {
@ -29,8 +27,6 @@ export default defineConfig(({ mode }) => {
SUBDOMAIN_HOST, SUBDOMAIN_HOST,
COLLAB_URL, COLLAB_URL,
BILLING_TRIAL_DAYS, BILLING_TRIAL_DAYS,
POSTHOG_HOST,
POSTHOG_KEY,
}, },
APP_VERSION: JSON.stringify(process.env.npm_package_version), APP_VERSION: JSON.stringify(process.env.npm_package_version),
}, },

View File

@ -1,6 +1,6 @@
{ {
"name": "server", "name": "server",
"version": "0.21.0", "version": "0.20.4",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,

View File

@ -16,12 +16,6 @@ export async function comparePasswordHash(
return bcrypt.compare(plainPassword, passwordHash); return bcrypt.compare(plainPassword, passwordHash);
} }
export function generateRandomSuffixNumbers(length: number) {
return Math.random()
.toFixed(length)
.substring(2, 2 + length);
}
export type RedisConfig = { export type RedisConfig = {
host: string; host: string;
port: number; port: number;

View File

@ -1,9 +1,11 @@
import { import {
BadRequestException,
Body, Body,
Controller, Controller,
HttpCode, HttpCode,
HttpStatus, HttpStatus,
Post, Post,
Req,
Res, Res,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
@ -21,6 +23,7 @@ import { ForgotPasswordDto } from './dto/forgot-password.dto';
import { PasswordResetDto } from './dto/password-reset.dto'; import { PasswordResetDto } from './dto/password-reset.dto';
import { VerifyUserTokenDto } from './dto/verify-user-token.dto'; import { VerifyUserTokenDto } from './dto/verify-user-token.dto';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import { addDays } from 'date-fns';
import { validateSsoEnforcement } from './auth.util'; import { validateSsoEnforcement } from './auth.util';
@Controller('auth') @Controller('auth')
@ -122,7 +125,7 @@ export class AuthController {
res.setCookie('authToken', token, { res.setCookie('authToken', token, {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
expires: this.environmentService.getCookieExpiresIn(), expires: addDays(new Date(), 30),
secure: this.environmentService.isHttps(), secure: this.environmentService.isHttps(),
}); });
} }

View File

@ -6,16 +6,3 @@ export function validateSsoEnforcement(workspace: Workspace) {
throw new BadRequestException('This workspace has enforced SSO login.'); throw new BadRequestException('This workspace has enforced SSO login.');
} }
} }
export function validateAllowedEmail(userEmail: string, workspace: Workspace) {
const emailParts = userEmail.split('@');
const emailDomain = emailParts[1].toLowerCase();
if (
workspace.emailDomains?.length > 0 &&
!workspace.emailDomains.includes(emailDomain)
) {
throw new BadRequestException(
`The email domain "${emailDomain}" is not approved for this workspace.`,
);
}
}

View File

@ -1,12 +1,6 @@
import { import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
IsNotEmpty,
IsOptional,
IsString,
MaxLength,
MinLength,
} from 'class-validator';
import { CreateUserDto } from './create-user.dto'; import { CreateUserDto } from './create-user.dto';
import { Transform, TransformFnParams } from 'class-transformer'; import {Transform, TransformFnParams} from "class-transformer";
export class CreateAdminUserDto extends CreateUserDto { export class CreateAdminUserDto extends CreateUserDto {
@IsNotEmpty() @IsNotEmpty()
@ -15,17 +9,10 @@ export class CreateAdminUserDto extends CreateUserDto {
@Transform(({ value }: TransformFnParams) => value?.trim()) @Transform(({ value }: TransformFnParams) => value?.trim())
name: string; name: string;
@IsOptional() @IsNotEmpty()
@MinLength(1) @MinLength(3)
@MaxLength(50) @MaxLength(50)
@IsString() @IsString()
@Transform(({ value }: TransformFnParams) => value?.trim()) @Transform(({ value }: TransformFnParams) => value?.trim())
workspaceName: string; workspaceName: string;
@IsOptional()
@MinLength(4)
@MaxLength(50)
@IsString()
@Transform(({ value }: TransformFnParams) => value?.trim())
hostname?: string;
} }

View File

@ -92,8 +92,7 @@ export class SignupService {
// create workspace with full setup // create workspace with full setup
const workspaceData: CreateWorkspaceDto = { const workspaceData: CreateWorkspaceDto = {
name: createAdminUserDto.workspaceName || 'My workspace', name: createAdminUserDto.workspaceName,
hostname: createAdminUserDto.hostname,
}; };
workspace = await this.workspaceService.create( workspace = await this.workspaceService.create(

View File

@ -1,13 +1,5 @@
import { OmitType, PartialType } from '@nestjs/mapped-types'; import { OmitType, PartialType } from '@nestjs/mapped-types';
import { import { IsBoolean, IsOptional, IsString } from 'class-validator';
IsBoolean,
IsIn,
IsNotEmpty,
IsOptional,
IsString,
MaxLength,
MinLength,
} from 'class-validator';
import { CreateUserDto } from '../../auth/dto/create-user.dto'; import { CreateUserDto } from '../../auth/dto/create-user.dto';
export class UpdateUserDto extends PartialType( export class UpdateUserDto extends PartialType(
@ -21,18 +13,7 @@ export class UpdateUserDto extends PartialType(
@IsBoolean() @IsBoolean()
fullPageWidth: boolean; fullPageWidth: boolean;
@IsOptional()
@IsString()
@IsIn(['read', 'edit'])
pageEditMode: string;
@IsOptional() @IsOptional()
@IsString() @IsString()
locale: string; locale: string;
@IsOptional()
@MinLength(8)
@MaxLength(70)
@IsString()
confirmPassword: string;
} }

View File

@ -50,6 +50,6 @@ export class UserController {
@AuthUser() user: User, @AuthUser() user: User,
@AuthWorkspace() workspace: Workspace, @AuthWorkspace() workspace: Workspace,
) { ) {
return this.userService.update(updateUserDto, user.id, workspace); return this.userService.update(updateUserDto, user.id, workspace.id);
} }
} }

View File

@ -3,12 +3,8 @@ import {
BadRequestException, BadRequestException,
Injectable, Injectable,
NotFoundException, NotFoundException,
UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import { UpdateUserDto } from './dto/update-user.dto'; import { UpdateUserDto } from './dto/update-user.dto';
import { comparePasswordHash } from 'src/common/helpers/utils';
import { Workspace } from '@docmost/db/types/entity.types';
import { validateSsoEnforcement } from '../auth/auth.util';
@Injectable() @Injectable()
export class UserService { export class UserService {
@ -21,14 +17,9 @@ export class UserService {
async update( async update(
updateUserDto: UpdateUserDto, updateUserDto: UpdateUserDto,
userId: string, userId: string,
workspace: Workspace, workspaceId: string,
) { ) {
const includePassword = const user = await this.userRepo.findById(userId, workspaceId);
updateUserDto.email != null && updateUserDto.confirmPassword != null;
const user = await this.userRepo.findById(userId, workspace.id, {
includePassword,
});
if (!user) { if (!user) {
throw new NotFoundException('User not found'); throw new NotFoundException('User not found');
@ -43,40 +34,14 @@ export class UserService {
); );
} }
if (typeof updateUserDto.pageEditMode !== 'undefined') {
return this.userRepo.updatePreference(
userId,
'pageEditMode',
updateUserDto.pageEditMode.toLowerCase(),
);
}
if (updateUserDto.name) { if (updateUserDto.name) {
user.name = updateUserDto.name; user.name = updateUserDto.name;
} }
if (updateUserDto.email && user.email != updateUserDto.email) { if (updateUserDto.email && user.email != updateUserDto.email) {
validateSsoEnforcement(workspace); if (await this.userRepo.findByEmail(updateUserDto.email, workspaceId)) {
if (!updateUserDto.confirmPassword) {
throw new BadRequestException(
'You must provide a password to change your email',
);
}
const isPasswordMatch = await comparePasswordHash(
updateUserDto.confirmPassword,
user.password,
);
if (!isPasswordMatch) {
throw new BadRequestException('You must provide the correct password to change your email');
}
if (await this.userRepo.findByEmail(updateUserDto.email, workspace.id)) {
throw new BadRequestException('A user with this email already exists'); throw new BadRequestException('A user with this email already exists');
} }
user.email = updateUserDto.email; user.email = updateUserDto.email;
} }
@ -88,9 +53,7 @@ export class UserService {
user.locale = updateUserDto.locale; user.locale = updateUserDto.locale;
} }
delete updateUserDto.confirmPassword; await this.userRepo.updateUser(updateUserDto, userId, workspaceId);
await this.userRepo.updateUser(updateUserDto, userId, workspace.id);
return user; return user;
} }
} }

View File

@ -29,7 +29,9 @@ import WorkspaceAbilityFactory from '../../casl/abilities/workspace-ability.fact
import { import {
WorkspaceCaslAction, WorkspaceCaslAction,
WorkspaceCaslSubject, WorkspaceCaslSubject,
} from '../../casl/interfaces/workspace-ability.type';import { FastifyReply } from 'fastify'; } from '../../casl/interfaces/workspace-ability.type';
import { addDays } from 'date-fns';
import { FastifyReply } from 'fastify';
import { EnvironmentService } from '../../../integrations/environment/environment.service'; import { EnvironmentService } from '../../../integrations/environment/environment.service';
import { CheckHostnameDto } from '../dto/check-hostname.dto'; import { CheckHostnameDto } from '../dto/check-hostname.dto';
import { RemoveWorkspaceUserDto } from '../dto/remove-workspace-user.dto'; import { RemoveWorkspaceUserDto } from '../dto/remove-workspace-user.dto';
@ -178,13 +180,10 @@ export class WorkspaceController {
@Public() @Public()
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post('invites/info') @Post('invites/info')
async getInvitationById( async getInvitationById(@Body() dto: InvitationIdDto, @Req() req: any) {
@Body() dto: InvitationIdDto,
@AuthWorkspace() workspace: Workspace,
) {
return this.workspaceInvitationService.getInvitationById( return this.workspaceInvitationService.getInvitationById(
dto.invitationId, dto.invitationId,
workspace, req.raw.workspaceId,
); );
} }
@ -254,18 +253,18 @@ export class WorkspaceController {
@Post('invites/accept') @Post('invites/accept')
async acceptInvite( async acceptInvite(
@Body() acceptInviteDto: AcceptInviteDto, @Body() acceptInviteDto: AcceptInviteDto,
@AuthWorkspace() workspace: Workspace, @Req() req: any,
@Res({ passthrough: true }) res: FastifyReply, @Res({ passthrough: true }) res: FastifyReply,
) { ) {
const authToken = await this.workspaceInvitationService.acceptInvitation( const authToken = await this.workspaceInvitationService.acceptInvitation(
acceptInviteDto, acceptInviteDto,
workspace, req.raw.workspaceId,
); );
res.setCookie('authToken', authToken, { res.setCookie('authToken', authToken, {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
expires: this.environmentService.getCookieExpiresIn(), expires: addDays(new Date(), 30),
secure: this.environmentService.isHttps(), secure: this.environmentService.isHttps(),
}); });
} }

View File

@ -28,10 +28,6 @@ import { InjectQueue } from '@nestjs/bullmq';
import { QueueJob, QueueName } from '../../../integrations/queue/constants'; import { QueueJob, QueueName } from '../../../integrations/queue/constants';
import { Queue } from 'bullmq'; import { Queue } from 'bullmq';
import { EnvironmentService } from '../../../integrations/environment/environment.service'; import { EnvironmentService } from '../../../integrations/environment/environment.service';
import {
validateAllowedEmail,
validateSsoEnforcement,
} from '../../auth/auth.util';
@Injectable() @Injectable()
export class WorkspaceInvitationService { export class WorkspaceInvitationService {
@ -67,19 +63,19 @@ export class WorkspaceInvitationService {
return result; return result;
} }
async getInvitationById(invitationId: string, workspace: Workspace) { async getInvitationById(invitationId: string, workspaceId: string) {
const invitation = await this.db const invitation = await this.db
.selectFrom('workspaceInvitations') .selectFrom('workspaceInvitations')
.select(['id', 'email', 'createdAt']) .select(['id', 'email', 'createdAt'])
.where('id', '=', invitationId) .where('id', '=', invitationId)
.where('workspaceId', '=', workspace.id) .where('workspaceId', '=', workspaceId)
.executeTakeFirst(); .executeTakeFirst();
if (!invitation) { if (!invitation) {
throw new NotFoundException('Invitation not found'); throw new NotFoundException('Invitation not found');
} }
return { ...invitation, enforceSso: workspace.enforceSso }; return invitation;
} }
async getInvitationTokenById(invitationId: string, workspaceId: string) { async getInvitationTokenById(invitationId: string, workspaceId: string) {
@ -145,10 +141,6 @@ export class WorkspaceInvitationService {
groupIds: validGroups?.map((group: Partial<Group>) => group.id), groupIds: validGroups?.map((group: Partial<Group>) => group.id),
})); }));
if (invitesToInsert.length < 1) {
return;
}
invites = await trx invites = await trx
.insertInto('workspaceInvitations') .insertInto('workspaceInvitations')
.values(invitesToInsert) .values(invitesToInsert)
@ -177,12 +169,12 @@ export class WorkspaceInvitationService {
} }
} }
async acceptInvitation(dto: AcceptInviteDto, workspace: Workspace) { async acceptInvitation(dto: AcceptInviteDto, workspaceId: string) {
const invitation = await this.db const invitation = await this.db
.selectFrom('workspaceInvitations') .selectFrom('workspaceInvitations')
.selectAll() .selectAll()
.where('id', '=', dto.invitationId) .where('id', '=', dto.invitationId)
.where('workspaceId', '=', workspace.id) .where('workspaceId', '=', workspaceId)
.executeTakeFirst(); .executeTakeFirst();
if (!invitation) { if (!invitation) {
@ -193,9 +185,6 @@ export class WorkspaceInvitationService {
throw new BadRequestException('Invalid invitation token'); throw new BadRequestException('Invalid invitation token');
} }
validateSsoEnforcement(workspace);
validateAllowedEmail(invitation.email, workspace);
let newUser: User; let newUser: User;
try { try {
@ -208,7 +197,7 @@ export class WorkspaceInvitationService {
password: dto.password, password: dto.password,
role: invitation.role, role: invitation.role,
invitedById: invitation.invitedById, invitedById: invitation.invitedById,
workspaceId: workspace.id, workspaceId: workspaceId,
}, },
trx, trx,
); );
@ -216,7 +205,7 @@ export class WorkspaceInvitationService {
// add user to default group // add user to default group
await this.groupUserRepo.addUserToDefaultGroup( await this.groupUserRepo.addUserToDefaultGroup(
newUser.id, newUser.id,
workspace.id, workspaceId,
trx, trx,
); );
@ -226,7 +215,7 @@ export class WorkspaceInvitationService {
.selectFrom('groups') .selectFrom('groups')
.select(['id', 'name']) .select(['id', 'name'])
.where('groups.id', 'in', invitation.groupIds) .where('groups.id', 'in', invitation.groupIds)
.where('groups.workspaceId', '=', workspace.id) .where('groups.workspaceId', '=', workspaceId)
.execute(); .execute();
if (validGroups && validGroups.length > 0) { if (validGroups && validGroups.length > 0) {
@ -267,7 +256,7 @@ export class WorkspaceInvitationService {
// notify the inviter // notify the inviter
const invitedByUser = await this.userRepo.findById( const invitedByUser = await this.userRepo.findById(
invitation.invitedById, invitation.invitedById,
workspace.id, workspaceId,
); );
if (invitedByUser) { if (invitedByUser) {
@ -284,9 +273,7 @@ export class WorkspaceInvitationService {
} }
if (this.environmentService.isCloud()) { if (this.environmentService.isCloud()) {
await this.billingQueue.add(QueueJob.STRIPE_SEATS_SYNC, { await this.billingQueue.add(QueueJob.STRIPE_SEATS_SYNC, { workspaceId });
workspaceId: workspace.id,
});
} }
return this.tokenService.generateAccessToken(newUser); return this.tokenService.generateAccessToken(newUser);

View File

@ -32,7 +32,6 @@ import { AttachmentType } from 'src/core/attachment/attachment.constants';
import { InjectQueue } from '@nestjs/bullmq'; import { InjectQueue } from '@nestjs/bullmq';
import { QueueJob, QueueName } from '../../../integrations/queue/constants'; import { QueueJob, QueueName } from '../../../integrations/queue/constants';
import { Queue } from 'bullmq'; import { Queue } from 'bullmq';
import { generateRandomSuffixNumbers } from '../../../common/helpers';
@Injectable() @Injectable()
export class WorkspaceService { export class WorkspaceService {
@ -378,20 +377,24 @@ export class WorkspaceService {
name: string, name: string,
trx?: KyselyTransaction, trx?: KyselyTransaction,
): Promise<string> { ): Promise<string> {
const generateRandomSuffix = (length: number) =>
Math.random()
.toFixed(length)
.substring(2, 2 + length);
let subdomain = name let subdomain = name
.toLowerCase() .toLowerCase()
.replace(/[^a-z0-9-]/g, '') .replace(/[^a-z0-9]/g, '')
.substring(0, 20) .substring(0, 20);
.replace(/^-+|-+$/g, ''); //remove any hyphen at the start or end
// Ensure we leave room for a random suffix. // Ensure we leave room for a random suffix.
const maxSuffixLength = 6; const maxSuffixLength = 6;
if (subdomain.length < 4) { if (subdomain.length < 4) {
subdomain = `${subdomain}-${generateRandomSuffixNumbers(maxSuffixLength)}`; subdomain = `${subdomain}-${generateRandomSuffix(maxSuffixLength)}`;
} }
if (DISALLOWED_HOSTNAMES.includes(subdomain)) { if (DISALLOWED_HOSTNAMES.includes(subdomain)) {
subdomain = `workspace-${generateRandomSuffixNumbers(maxSuffixLength)}`; subdomain = `workspace-${generateRandomSuffix(maxSuffixLength)}`;
} }
let uniqueHostname = subdomain; let uniqueHostname = subdomain;
@ -405,7 +408,7 @@ export class WorkspaceService {
break; break;
} }
// Append a random suffix and retry. // Append a random suffix and retry.
const randomSuffix = generateRandomSuffixNumbers(maxSuffixLength); const randomSuffix = generateRandomSuffix(maxSuffixLength);
uniqueHostname = `${subdomain}-${randomSuffix}`.substring(0, 25); uniqueHostname = `${subdomain}-${randomSuffix}`.substring(0, 25);
} }

View File

@ -1,23 +0,0 @@
import { type Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable('billing')
.addColumn('billing_scheme', 'varchar', (col) => col)
.addColumn('tiered_up_to', 'varchar', (col) => col)
.addColumn('tiered_flat_amount', 'int8', (col) => col)
.addColumn('tiered_unit_amount', 'int8', (col) => col)
.addColumn('plan_name', 'varchar', (col) => col)
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable('billing')
.dropColumn('billing_scheme')
.dropColumn('tiered_up_to')
.dropColumn('tiered_flat_amount')
.dropColumn('tiered_unit_amount')
.dropColumn('plan_name')
.execute();
}

View File

@ -84,7 +84,6 @@ export interface Backlinks {
export interface Billing { export interface Billing {
amount: Int8 | null; amount: Int8 | null;
billingScheme: string | null;
cancelAt: Timestamp | null; cancelAt: Timestamp | null;
cancelAtPeriodEnd: boolean | null; cancelAtPeriodEnd: boolean | null;
canceledAt: Timestamp | null; canceledAt: Timestamp | null;
@ -97,7 +96,6 @@ export interface Billing {
metadata: Json | null; metadata: Json | null;
periodEndAt: Timestamp | null; periodEndAt: Timestamp | null;
periodStartAt: Timestamp; periodStartAt: Timestamp;
planName: string | null;
quantity: Int8 | null; quantity: Int8 | null;
status: string; status: string;
stripeCustomerId: string | null; stripeCustomerId: string | null;
@ -105,9 +103,6 @@ export interface Billing {
stripePriceId: string | null; stripePriceId: string | null;
stripeProductId: string | null; stripeProductId: string | null;
stripeSubscriptionId: string; stripeSubscriptionId: string;
tieredFlatAmount: Int8 | null;
tieredUnitAmount: Int8 | null;
tieredUpTo: string | null;
updatedAt: Generated<Timestamp>; updatedAt: Generated<Timestamp>;
workspaceId: string; workspaceId: string;
} }

View File

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import ms, { StringValue } from 'ms';
@Injectable() @Injectable()
export class EnvironmentService { export class EnvironmentService {
@ -57,18 +56,7 @@ export class EnvironmentService {
} }
getJwtTokenExpiresIn(): string { getJwtTokenExpiresIn(): string {
return this.configService.get<string>('JWT_TOKEN_EXPIRES_IN', '90d'); return this.configService.get<string>('JWT_TOKEN_EXPIRES_IN', '30d');
}
getCookieExpiresIn(): Date {
const expiresInStr = this.getJwtTokenExpiresIn();
let msUntilExpiry: number;
try {
msUntilExpiry = ms(expiresInStr as StringValue);
} catch (err) {
msUntilExpiry = ms('90d');
}
return new Date(Date.now() + msUntilExpiry);
} }
getStorageDriver(): string { getStorageDriver(): string {
@ -205,12 +193,4 @@ export class EnvironmentService {
.toLowerCase(); .toLowerCase();
return disable === 'true'; return disable === 'true';
} }
getPostHogHost(): string {
return this.configService.get<string>('POSTHOG_HOST');
}
getPostHogKey(): string {
return this.configService.get<string>('POSTHOG_KEY');
}
} }

View File

@ -37,8 +37,6 @@ export class StaticModule implements OnModuleInit {
CLOUD: this.environmentService.isCloud(), CLOUD: this.environmentService.isCloud(),
FILE_UPLOAD_SIZE_LIMIT: FILE_UPLOAD_SIZE_LIMIT:
this.environmentService.getFileUploadSizeLimit(), this.environmentService.getFileUploadSizeLimit(),
FILE_IMPORT_SIZE_LIMIT:
this.environmentService.getFileImportSizeLimit(),
DRAWIO_URL: this.environmentService.getDrawioUrl(), DRAWIO_URL: this.environmentService.getDrawioUrl(),
SUBDOMAIN_HOST: this.environmentService.isCloud() SUBDOMAIN_HOST: this.environmentService.isCloud()
? this.environmentService.getSubdomainHost() ? this.environmentService.getSubdomainHost()
@ -47,8 +45,6 @@ export class StaticModule implements OnModuleInit {
BILLING_TRIAL_DAYS: this.environmentService.isCloud() BILLING_TRIAL_DAYS: this.environmentService.isCloud()
? this.environmentService.getBillingTrialDays() ? this.environmentService.getBillingTrialDays()
: undefined, : undefined,
POSTHOG_HOST: this.environmentService.getPostHogHost(),
POSTHOG_KEY: this.environmentService.getPostHogKey(),
}; };
const windowScriptContent = `<script>window.CONFIG=${JSON.stringify(configString)};</script>`; const windowScriptContent = `<script>window.CONFIG=${JSON.stringify(configString)};</script>`;

View File

@ -1,7 +1,7 @@
{ {
"name": "docmost", "name": "docmost",
"homepage": "https://docmost.com", "homepage": "https://docmost.com",
"version": "0.21.0", "version": "0.20.4",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "nx run-many -t build", "build": "nx run-many -t build",
@ -26,40 +26,40 @@
"@joplin/turndown": "^4.0.74", "@joplin/turndown": "^4.0.74",
"@joplin/turndown-plugin-gfm": "^1.0.56", "@joplin/turndown-plugin-gfm": "^1.0.56",
"@sindresorhus/slugify": "1.1.0", "@sindresorhus/slugify": "1.1.0",
"@tiptap/core": "^2.10.3", "@tiptap/core": "^2.14.0",
"@tiptap/extension-code-block": "^2.10.3", "@tiptap/extension-code-block": "^2.14.0",
"@tiptap/extension-code-block-lowlight": "^2.10.3", "@tiptap/extension-code-block-lowlight": "^2.14.0",
"@tiptap/extension-collaboration": "^2.10.3", "@tiptap/extension-collaboration": "^2.14.0",
"@tiptap/extension-collaboration-cursor": "^2.10.3", "@tiptap/extension-collaboration-cursor": "^2.14.0",
"@tiptap/extension-color": "^2.10.3", "@tiptap/extension-color": "^2.14.0",
"@tiptap/extension-document": "^2.10.3", "@tiptap/extension-document": "^2.14.0",
"@tiptap/extension-heading": "^2.10.3", "@tiptap/extension-heading": "^2.14.0",
"@tiptap/extension-highlight": "^2.10.3", "@tiptap/extension-highlight": "^2.14.0",
"@tiptap/extension-history": "^2.10.3", "@tiptap/extension-history": "^2.14.0",
"@tiptap/extension-image": "^2.10.3", "@tiptap/extension-image": "^2.14.0",
"@tiptap/extension-link": "^2.10.3", "@tiptap/extension-link": "^2.14.0",
"@tiptap/extension-list-item": "^2.10.3", "@tiptap/extension-list-item": "^2.14.0",
"@tiptap/extension-list-keymap": "^2.10.3", "@tiptap/extension-list-keymap": "^2.14.0",
"@tiptap/extension-placeholder": "^2.10.3", "@tiptap/extension-placeholder": "^2.14.0",
"@tiptap/extension-subscript": "^2.10.3", "@tiptap/extension-subscript": "^2.14.0",
"@tiptap/extension-superscript": "^2.10.3", "@tiptap/extension-superscript": "^2.14.0",
"@tiptap/extension-table": "^2.10.3", "@tiptap/extension-table": "^2.14.0",
"@tiptap/extension-table-cell": "^2.10.3", "@tiptap/extension-table-cell": "^2.14.0",
"@tiptap/extension-table-header": "^2.10.3", "@tiptap/extension-table-header": "^2.14.0",
"@tiptap/extension-table-row": "^2.10.3", "@tiptap/extension-table-row": "^2.14.0",
"@tiptap/extension-task-item": "^2.10.3", "@tiptap/extension-task-item": "^2.14.0",
"@tiptap/extension-task-list": "^2.10.3", "@tiptap/extension-task-list": "^2.14.0",
"@tiptap/extension-text": "^2.10.3", "@tiptap/extension-text": "^2.14.0",
"@tiptap/extension-text-align": "^2.10.3", "@tiptap/extension-text-align": "^2.14.0",
"@tiptap/extension-text-style": "^2.10.3", "@tiptap/extension-text-style": "^2.14.0",
"@tiptap/extension-typography": "^2.10.3", "@tiptap/extension-typography": "^2.14.0",
"@tiptap/extension-underline": "^2.10.3", "@tiptap/extension-underline": "^2.14.0",
"@tiptap/extension-youtube": "^2.10.3", "@tiptap/extension-youtube": "^2.14.0",
"@tiptap/html": "^2.10.3", "@tiptap/html": "^2.14.0",
"@tiptap/pm": "^2.10.3", "@tiptap/pm": "^2.14.0",
"@tiptap/react": "^2.10.3", "@tiptap/react": "^2.14.0",
"@tiptap/starter-kit": "^2.10.3", "@tiptap/starter-kit": "^2.14.0",
"@tiptap/suggestion": "^2.10.3", "@tiptap/suggestion": "^2.14.0",
"bytes": "^3.1.2", "bytes": "^3.1.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
@ -69,7 +69,6 @@
"jszip": "^3.10.1", "jszip": "^3.10.1",
"linkifyjs": "^4.2.0", "linkifyjs": "^4.2.0",
"marked": "13.0.3", "marked": "13.0.3",
"ms": "3.0.0-canary.1",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"y-indexeddb": "^9.0.12", "y-indexeddb": "^9.0.12",
"yjs": "^13.6.27" "yjs": "^13.6.27"

View File

@ -17,5 +17,4 @@ export * from "./lib/excalidraw";
export * from "./lib/embed"; export * from "./lib/embed";
export * from "./lib/mention"; export * from "./lib/mention";
export * from "./lib/markdown"; export * from "./lib/markdown";
export * from "./lib/search-and-replace";
export * from "./lib/embed-provider"; export * from "./lib/embed-provider";

View File

@ -35,42 +35,6 @@ export const CustomCodeBlock = CodeBlockLowlight.extend<CustomCodeBlockOptions>(
return true; return true;
} }
}, },
"Mod-a": () => {
if (this.editor.isActive("codeBlock")) {
const { state } = this.editor;
const { $from } = state.selection;
let codeBlockNode = null;
let codeBlockPos = null;
let depth = 0;
for (depth = $from.depth; depth > 0; depth--) {
const node = $from.node(depth);
if (node.type.name === "codeBlock") {
codeBlockNode = node;
codeBlockPos = $from.start(depth) - 1;
break;
}
}
if (codeBlockNode && codeBlockPos !== null) {
const codeBlockStart = codeBlockPos;
const codeBlockEnd = codeBlockPos + codeBlockNode.nodeSize;
const contentStart = codeBlockStart + 1;
const contentEnd = codeBlockEnd - 1;
this.editor.commands.setTextSelection({
from: contentStart,
to: contentEnd,
});
return true;
}
}
return false;
},
}; };
}, },

View File

@ -1,3 +0,0 @@
import { SearchAndReplace } from './search-and-replace'
export * from './search-and-replace'
export default SearchAndReplace

View File

@ -1,455 +0,0 @@
/***
MIT License
Copyright (c) 2023 - 2024 Jeet Mandaliya (Github Username: sereneinserenade)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
***/
import { Extension, Range, type Dispatch } from "@tiptap/core";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import {
Plugin,
PluginKey,
type EditorState,
type Transaction,
} from "@tiptap/pm/state";
import { Node as PMNode, Mark } from "@tiptap/pm/model";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
search: {
/**
* @description Set search term in extension.
*/
setSearchTerm: (searchTerm: string) => ReturnType;
/**
* @description Set replace term in extension.
*/
setReplaceTerm: (replaceTerm: string) => ReturnType;
/**
* @description Set case sensitivity in extension.
*/
setCaseSensitive: (caseSensitive: boolean) => ReturnType;
/**
* @description Reset current search result to first instance.
*/
resetIndex: () => ReturnType;
/**
* @description Find next instance of search result.
*/
nextSearchResult: () => ReturnType;
/**
* @description Find previous instance of search result.
*/
previousSearchResult: () => ReturnType;
/**
* @description Replace first instance of search result with given replace term.
*/
replace: () => ReturnType;
/**
* @description Replace all instances of search result with given replace term.
*/
replaceAll: () => ReturnType;
/**
* @description Find selected instance of search result.
*/
selectCurrentItem: () => ReturnType;
};
}
}
interface TextNodesWithPosition {
text: string;
pos: number;
}
const getRegex = (
s: string,
disableRegex: boolean,
caseSensitive: boolean,
): RegExp => {
return RegExp(
disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : s,
caseSensitive ? "gu" : "gui",
);
};
interface ProcessedSearches {
decorationsToReturn: DecorationSet;
results: Range[];
}
function processSearches(
doc: PMNode,
searchTerm: RegExp,
searchResultClass: string,
resultIndex: number,
): ProcessedSearches {
const decorations: Decoration[] = [];
const results: Range[] = [];
let textNodesWithPosition: TextNodesWithPosition[] = [];
let index = 0;
if (!searchTerm) {
return {
decorationsToReturn: DecorationSet.empty,
results: [],
};
}
doc?.descendants((node, pos) => {
if (node.isText) {
if (textNodesWithPosition[index]) {
textNodesWithPosition[index] = {
text: textNodesWithPosition[index].text + node.text,
pos: textNodesWithPosition[index].pos,
};
} else {
textNodesWithPosition[index] = {
text: `${node.text}`,
pos,
};
}
} else {
index += 1;
}
});
textNodesWithPosition = textNodesWithPosition.filter(Boolean);
for (const element of textNodesWithPosition) {
const { text, pos } = element;
const matches = Array.from(text.matchAll(searchTerm)).filter(
([matchText]) => matchText.trim(),
);
for (const m of matches) {
if (m[0] === "") break;
if (m.index !== undefined) {
results.push({
from: pos + m.index,
to: pos + m.index + m[0].length,
});
}
}
}
for (let i = 0; i < results.length; i += 1) {
const r = results[i];
const className =
i === resultIndex
? `${searchResultClass} ${searchResultClass}-current`
: searchResultClass;
const decoration: Decoration = Decoration.inline(r.from, r.to, {
class: className,
});
decorations.push(decoration);
}
return {
decorationsToReturn: DecorationSet.create(doc, decorations),
results,
};
}
const replace = (
replaceTerm: string,
results: Range[],
resultIndex: number,
{ state, dispatch }: { state: EditorState; dispatch: Dispatch },
) => {
const firstResult = results[resultIndex];
if (!firstResult) return;
const { from, to } = results[resultIndex];
if (dispatch) {
const tr = state.tr;
// Get all marks that span the text being replaced
const marksSet = new Set<Mark>();
state.doc.nodesBetween(from, to, (node) => {
if (node.isText && node.marks) {
node.marks.forEach(mark => marksSet.add(mark));
}
});
const marks = Array.from(marksSet);
// Delete the old text and insert new text with preserved marks
tr.delete(from, to);
tr.insert(from, state.schema.text(replaceTerm, marks));
dispatch(tr);
}
};
const replaceAll = (
replaceTerm: string,
results: Range[],
{ tr, dispatch }: { tr: Transaction; dispatch: Dispatch },
) => {
const resultsCopy = results.slice();
if (!resultsCopy.length) return;
// Process replacements in reverse order to avoid position shifting issues
for (let i = resultsCopy.length - 1; i >= 0; i -= 1) {
const { from, to } = resultsCopy[i];
// Get all marks that span the text being replaced
const marksSet = new Set<Mark>();
tr.doc.nodesBetween(from, to, (node) => {
if (node.isText && node.marks) {
node.marks.forEach(mark => marksSet.add(mark));
}
});
const marks = Array.from(marksSet);
// Delete and insert with preserved marks
tr.delete(from, to);
tr.insert(from, tr.doc.type.schema.text(replaceTerm, marks));
}
dispatch(tr);
};
export const searchAndReplacePluginKey = new PluginKey(
"searchAndReplacePlugin",
);
export interface SearchAndReplaceOptions {
searchResultClass: string;
disableRegex: boolean;
}
export interface SearchAndReplaceStorage {
searchTerm: string;
replaceTerm: string;
results: Range[];
lastSearchTerm: string;
caseSensitive: boolean;
lastCaseSensitive: boolean;
resultIndex: number;
lastResultIndex: number;
}
export const SearchAndReplace = Extension.create<
SearchAndReplaceOptions,
SearchAndReplaceStorage
>({
name: "searchAndReplace",
addOptions() {
return {
searchResultClass: "search-result",
disableRegex: true,
};
},
addStorage() {
return {
searchTerm: "",
replaceTerm: "",
results: [],
lastSearchTerm: "",
caseSensitive: false,
lastCaseSensitive: false,
resultIndex: 0,
lastResultIndex: 0,
};
},
addCommands() {
return {
setSearchTerm:
(searchTerm: string) =>
({ editor }) => {
editor.storage.searchAndReplace.searchTerm = searchTerm;
return false;
},
setReplaceTerm:
(replaceTerm: string) =>
({ editor }) => {
editor.storage.searchAndReplace.replaceTerm = replaceTerm;
return false;
},
setCaseSensitive:
(caseSensitive: boolean) =>
({ editor }) => {
editor.storage.searchAndReplace.caseSensitive = caseSensitive;
return false;
},
resetIndex:
() =>
({ editor }) => {
editor.storage.searchAndReplace.resultIndex = 0;
return false;
},
nextSearchResult:
() =>
({ editor }) => {
const { results, resultIndex } = editor.storage.searchAndReplace;
const nextIndex = resultIndex + 1;
if (results[nextIndex]) {
editor.storage.searchAndReplace.resultIndex = nextIndex;
} else {
editor.storage.searchAndReplace.resultIndex = 0;
}
return false;
},
previousSearchResult:
() =>
({ editor }) => {
const { results, resultIndex } = editor.storage.searchAndReplace;
const prevIndex = resultIndex - 1;
if (results[prevIndex]) {
editor.storage.searchAndReplace.resultIndex = prevIndex;
} else {
editor.storage.searchAndReplace.resultIndex = results.length - 1;
}
return false;
},
replace:
() =>
({ editor, state, dispatch }) => {
const { replaceTerm, results, resultIndex } =
editor.storage.searchAndReplace;
replace(replaceTerm, results, resultIndex, { state, dispatch });
// After replace, adjust index if needed
// The results will be recalculated by the plugin, but we need to ensure
// the index doesn't exceed the new bounds
setTimeout(() => {
const newResultsLength = editor.storage.searchAndReplace.results.length;
if (newResultsLength > 0 && editor.storage.searchAndReplace.resultIndex >= newResultsLength) {
// Keep the same position if possible, otherwise go to the last result
editor.storage.searchAndReplace.resultIndex = Math.min(resultIndex, newResultsLength - 1);
}
}, 0);
return false;
},
replaceAll:
() =>
({ editor, tr, dispatch }) => {
const { replaceTerm, results } = editor.storage.searchAndReplace;
replaceAll(replaceTerm, results, { tr, dispatch });
return false;
},
selectCurrentItem:
() =>
({ editor }) => {
const { results } = editor.storage.searchAndReplace;
for (let i = 0; i < results.length; i++) {
if (
results[i].from == editor.state.selection.from &&
results[i].to == editor.state.selection.to
) {
editor.storage.searchAndReplace.resultIndex = i;
}
}
return false;
},
};
},
addProseMirrorPlugins() {
const editor = this.editor;
const { searchResultClass, disableRegex } = this.options;
const setLastSearchTerm = (t: string) =>
(editor.storage.searchAndReplace.lastSearchTerm = t);
const setLastCaseSensitive = (t: boolean) =>
(editor.storage.searchAndReplace.lastCaseSensitive = t);
const setLastResultIndex = (t: number) =>
(editor.storage.searchAndReplace.lastResultIndex = t);
return [
new Plugin({
key: searchAndReplacePluginKey,
state: {
init: () => DecorationSet.empty,
apply({ doc, docChanged }, oldState) {
const {
searchTerm,
lastSearchTerm,
caseSensitive,
lastCaseSensitive,
resultIndex,
lastResultIndex,
} = editor.storage.searchAndReplace;
if (
!docChanged &&
lastSearchTerm === searchTerm &&
lastCaseSensitive === caseSensitive &&
lastResultIndex === resultIndex
)
return oldState;
setLastSearchTerm(searchTerm);
setLastCaseSensitive(caseSensitive);
setLastResultIndex(resultIndex);
if (!searchTerm) {
editor.storage.searchAndReplace.results = [];
return DecorationSet.empty;
}
const { decorationsToReturn, results } = processSearches(
doc,
getRegex(searchTerm, disableRegex, caseSensitive),
searchResultClass,
resultIndex,
);
editor.storage.searchAndReplace.results = results;
return decorationsToReturn;
},
},
props: {
decorations(state) {
return this.getState(state);
},
},
}),
];
},
});
export default SearchAndReplace;

223
pnpm-lock.yaml generated
View File

@ -30,7 +30,7 @@ importers:
version: 2.15.2(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) version: 2.15.2(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
'@hocuspocus/transformer': '@hocuspocus/transformer':
specifier: ^2.15.2 specifier: ^2.15.2
version: 2.15.2(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)(y-prosemirror@1.2.3(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27) version: 2.15.2(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)(y-prosemirror@1.2.3(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.37.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27)
'@joplin/turndown': '@joplin/turndown':
specifier: ^4.0.74 specifier: ^4.0.74
version: 4.0.74 version: 4.0.74
@ -41,106 +41,106 @@ importers:
specifier: 1.1.0 specifier: 1.1.0
version: 1.1.0 version: 1.1.0
'@tiptap/core': '@tiptap/core':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/pm@2.14.0) version: 2.14.0(@tiptap/pm@2.14.0)
'@tiptap/extension-code-block': '@tiptap/extension-code-block':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)
'@tiptap/extension-code-block-lowlight': '@tiptap/extension-code-block-lowlight':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/extension-code-block@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)(highlight.js@11.11.1)(lowlight@3.3.0) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/extension-code-block@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)(highlight.js@11.11.1)(lowlight@3.3.0)
'@tiptap/extension-collaboration': '@tiptap/extension-collaboration':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)(y-prosemirror@1.2.3(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)(y-prosemirror@1.2.3(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.37.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))
'@tiptap/extension-collaboration-cursor': '@tiptap/extension-collaboration-cursor':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(y-prosemirror@1.2.3(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(y-prosemirror@1.2.3(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.37.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))
'@tiptap/extension-color': '@tiptap/extension-color':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/extension-text-style@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/extension-text-style@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)))
'@tiptap/extension-document': '@tiptap/extension-document':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-heading': '@tiptap/extension-heading':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-highlight': '@tiptap/extension-highlight':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-history': '@tiptap/extension-history':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)
'@tiptap/extension-image': '@tiptap/extension-image':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-link': '@tiptap/extension-link':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)
'@tiptap/extension-list-item': '@tiptap/extension-list-item':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-list-keymap': '@tiptap/extension-list-keymap':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-placeholder': '@tiptap/extension-placeholder':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)
'@tiptap/extension-subscript': '@tiptap/extension-subscript':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-superscript': '@tiptap/extension-superscript':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-table': '@tiptap/extension-table':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)
'@tiptap/extension-table-cell': '@tiptap/extension-table-cell':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-table-header': '@tiptap/extension-table-header':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-table-row': '@tiptap/extension-table-row':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-task-item': '@tiptap/extension-task-item':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)
'@tiptap/extension-task-list': '@tiptap/extension-task-list':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-text': '@tiptap/extension-text':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-text-align': '@tiptap/extension-text-align':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-text-style': '@tiptap/extension-text-style':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-typography': '@tiptap/extension-typography':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-underline': '@tiptap/extension-underline':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-youtube': '@tiptap/extension-youtube':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/html': '@tiptap/html':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)
'@tiptap/pm': '@tiptap/pm':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0 version: 2.14.0
'@tiptap/react': '@tiptap/react':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tiptap/starter-kit': '@tiptap/starter-kit':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0 version: 2.14.0
'@tiptap/suggestion': '@tiptap/suggestion':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)
bytes: bytes:
specifier: ^3.1.2 specifier: ^3.1.2
@ -169,9 +169,6 @@ importers:
marked: marked:
specifier: 13.0.3 specifier: 13.0.3
version: 13.0.3 version: 13.0.3
ms:
specifier: 3.0.0-canary.1
version: 3.0.0-canary.1
uuid: uuid:
specifier: ^11.1.0 specifier: ^11.1.0
version: 11.1.0 version: 11.1.0
@ -246,11 +243,8 @@ importers:
specifier: ^5.80.6 specifier: ^5.80.6
version: 5.80.6(react@18.3.1) version: 5.80.6(react@18.3.1)
'@tiptap/extension-character-count': '@tiptap/extension-character-count':
specifier: ^2.10.3 specifier: ^2.14.0
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0) version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)
alfaaz:
specifier: ^1.1.0
version: 1.1.0
axios: axios:
specifier: ^1.9.0 specifier: ^1.9.0
version: 1.9.0 version: 1.9.0
@ -296,9 +290,6 @@ importers:
mitt: mitt:
specifier: ^3.0.1 specifier: ^3.0.1
version: 3.0.1 version: 3.0.1
posthog-js:
specifier: ^1.255.1
version: 1.255.1
react: react:
specifier: ^18.3.1 specifier: ^18.3.1
version: 18.3.1 version: 18.3.1
@ -4686,9 +4677,6 @@ packages:
ajv@8.17.1: ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
alfaaz@1.1.0:
resolution: {integrity: sha512-J/P07R41APslK7NmD5303bwStN8jpRA4DdvtLeAr1Jhfj6XWGrASUWI0G6jbWjJAZyw3Lu1Pb4J8rsM/cb+xDQ==}
ansi-align@3.0.1: ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
@ -5216,9 +5204,6 @@ packages:
core-js-compat@3.35.0: core-js-compat@3.35.0:
resolution: {integrity: sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==} resolution: {integrity: sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==}
core-js@3.43.0:
resolution: {integrity: sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==}
core-util-is@1.0.3: core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@ -5994,9 +5979,6 @@ packages:
picomatch: picomatch:
optional: true optional: true
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
fflate@0.8.2: fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
@ -7376,10 +7358,6 @@ packages:
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
ms@3.0.0-canary.1:
resolution: {integrity: sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==}
engines: {node: '>=12.13'}
msgpackr-extract@3.0.2: msgpackr-extract@3.0.2:
resolution: {integrity: sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==} resolution: {integrity: sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==}
hasBin: true hasBin: true
@ -7964,23 +7942,9 @@ packages:
postgres-range@1.1.4: postgres-range@1.1.4:
resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==}
posthog-js@1.255.1:
resolution: {integrity: sha512-KMh0o9MhORhEZVjXpktXB5rJ8PfDk+poqBoTSoLzWgNjhJf6D8jcyB9jUMA6vVPfn4YeepVX5NuclDRqOwr5Mw==}
peerDependencies:
'@rrweb/types': 2.0.0-alpha.17
rrweb-snapshot: 2.0.0-alpha.17
peerDependenciesMeta:
'@rrweb/types':
optional: true
rrweb-snapshot:
optional: true
postmark@4.0.5: postmark@4.0.5:
resolution: {integrity: sha512-nerZdd3TwOH4CgGboZnlUM/q7oZk0EqpZgJL+Y3Nup8kHeaukxouQ6JcFF3EJEijc4QbuNv1TefGhboAKtf/SQ==} resolution: {integrity: sha512-nerZdd3TwOH4CgGboZnlUM/q7oZk0EqpZgJL+Y3Nup8kHeaukxouQ6JcFF3EJEijc4QbuNv1TefGhboAKtf/SQ==}
preact@10.26.9:
resolution: {integrity: sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==}
prelude-ls@1.2.1: prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -8060,6 +8024,9 @@ packages:
prosemirror-menu@1.2.4: prosemirror-menu@1.2.4:
resolution: {integrity: sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==} resolution: {integrity: sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==}
prosemirror-model@1.23.0:
resolution: {integrity: sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ==}
prosemirror-model@1.25.1: prosemirror-model@1.25.1:
resolution: {integrity: sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==} resolution: {integrity: sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==}
@ -8082,9 +8049,15 @@ packages:
prosemirror-state: ^1.4.2 prosemirror-state: ^1.4.2
prosemirror-view: ^1.33.8 prosemirror-view: ^1.33.8
prosemirror-transform@1.10.2:
resolution: {integrity: sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==}
prosemirror-transform@1.10.4: prosemirror-transform@1.10.4:
resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==} resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==}
prosemirror-view@1.37.0:
resolution: {integrity: sha512-z2nkKI1sJzyi7T47Ji/ewBPuIma1RNvQCCYVdV+MqWBV7o4Sa1n94UJCJJ1aQRF/xRkFfyqLGlGFWitIcCOtbg==}
prosemirror-view@1.40.0: prosemirror-view@1.40.0:
resolution: {integrity: sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==} resolution: {integrity: sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==}
@ -9320,9 +9293,6 @@ packages:
wcwidth@1.0.1: wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
web-vitals@4.2.4:
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
web-worker@1.5.0: web-worker@1.5.0:
resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==}
@ -11826,12 +11796,12 @@ snapshots:
- bufferutil - bufferutil
- utf-8-validate - utf-8-validate
'@hocuspocus/transformer@2.15.2(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)(y-prosemirror@1.2.3(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27)': '@hocuspocus/transformer@2.15.2(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)(y-prosemirror@1.2.3(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.37.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27)':
dependencies: dependencies:
'@tiptap/core': 2.14.0(@tiptap/pm@2.14.0) '@tiptap/core': 2.14.0(@tiptap/pm@2.14.0)
'@tiptap/pm': 2.14.0 '@tiptap/pm': 2.14.0
'@tiptap/starter-kit': 2.14.0 '@tiptap/starter-kit': 2.14.0
y-prosemirror: 1.2.3(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) y-prosemirror: 1.2.3(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.37.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
yjs: 13.6.27 yjs: 13.6.27
'@humanfs/core@0.19.1': {} '@humanfs/core@0.19.1': {}
@ -13587,16 +13557,16 @@ snapshots:
dependencies: dependencies:
'@tiptap/core': 2.14.0(@tiptap/pm@2.14.0) '@tiptap/core': 2.14.0(@tiptap/pm@2.14.0)
'@tiptap/extension-collaboration-cursor@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(y-prosemirror@1.2.3(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))': '@tiptap/extension-collaboration-cursor@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(y-prosemirror@1.2.3(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.37.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))':
dependencies: dependencies:
'@tiptap/core': 2.14.0(@tiptap/pm@2.14.0) '@tiptap/core': 2.14.0(@tiptap/pm@2.14.0)
y-prosemirror: 1.2.3(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) y-prosemirror: 1.2.3(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.37.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
'@tiptap/extension-collaboration@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)(y-prosemirror@1.2.3(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))': '@tiptap/extension-collaboration@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)(y-prosemirror@1.2.3(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.37.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))':
dependencies: dependencies:
'@tiptap/core': 2.14.0(@tiptap/pm@2.14.0) '@tiptap/core': 2.14.0(@tiptap/pm@2.14.0)
'@tiptap/pm': 2.14.0 '@tiptap/pm': 2.14.0
y-prosemirror: 1.2.3(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) y-prosemirror: 1.2.3(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.37.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)
'@tiptap/extension-color@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/extension-text-style@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)))': '@tiptap/extension-color@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/extension-text-style@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0)))':
dependencies: dependencies:
@ -13760,14 +13730,14 @@ snapshots:
prosemirror-keymap: 1.2.2 prosemirror-keymap: 1.2.2
prosemirror-markdown: 1.13.1 prosemirror-markdown: 1.13.1
prosemirror-menu: 1.2.4 prosemirror-menu: 1.2.4
prosemirror-model: 1.25.1 prosemirror-model: 1.23.0
prosemirror-schema-basic: 1.2.3 prosemirror-schema-basic: 1.2.3
prosemirror-schema-list: 1.4.1 prosemirror-schema-list: 1.4.1
prosemirror-state: 1.4.3 prosemirror-state: 1.4.3
prosemirror-tables: 1.7.1 prosemirror-tables: 1.7.1
prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0) prosemirror-trailing-node: 3.0.0(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.37.0)
prosemirror-transform: 1.10.4 prosemirror-transform: 1.10.2
prosemirror-view: 1.40.0 prosemirror-view: 1.37.0
'@tiptap/react@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@tiptap/react@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
@ -14588,8 +14558,6 @@ snapshots:
json-schema-traverse: 1.0.0 json-schema-traverse: 1.0.0
require-from-string: 2.0.2 require-from-string: 2.0.2
alfaaz@1.1.0: {}
ansi-align@3.0.1: ansi-align@3.0.1:
dependencies: dependencies:
string-width: 4.2.3 string-width: 4.2.3
@ -15220,8 +15188,6 @@ snapshots:
dependencies: dependencies:
browserslist: 4.24.2 browserslist: 4.24.2
core-js@3.43.0: {}
core-util-is@1.0.3: {} core-util-is@1.0.3: {}
cors@2.8.5: cors@2.8.5:
@ -16209,8 +16175,6 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.2 picomatch: 4.0.2
fflate@0.4.8: {}
fflate@0.8.2: {} fflate@0.8.2: {}
figures@3.2.0: figures@3.2.0:
@ -17889,8 +17853,6 @@ snapshots:
ms@2.1.3: {} ms@2.1.3: {}
ms@3.0.0-canary.1: {}
msgpackr-extract@3.0.2: msgpackr-extract@3.0.2:
dependencies: dependencies:
node-gyp-build-optional-packages: 5.0.7 node-gyp-build-optional-packages: 5.0.7
@ -18512,21 +18474,12 @@ snapshots:
postgres-range@1.1.4: {} postgres-range@1.1.4: {}
posthog-js@1.255.1:
dependencies:
core-js: 3.43.0
fflate: 0.4.8
preact: 10.26.9
web-vitals: 4.2.4
postmark@4.0.5: postmark@4.0.5:
dependencies: dependencies:
axios: 1.9.0 axios: 1.9.0
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
preact@10.26.9: {}
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prettier@3.4.1: {} prettier@3.4.1: {}
@ -18566,7 +18519,7 @@ snapshots:
prosemirror-changeset@2.3.1: prosemirror-changeset@2.3.1:
dependencies: dependencies:
prosemirror-transform: 1.10.4 prosemirror-transform: 1.10.2
prosemirror-collab@1.3.1: prosemirror-collab@1.3.1:
dependencies: dependencies:
@ -18574,34 +18527,34 @@ snapshots:
prosemirror-commands@1.6.2: prosemirror-commands@1.6.2:
dependencies: dependencies:
prosemirror-model: 1.25.1 prosemirror-model: 1.23.0
prosemirror-state: 1.4.3 prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4 prosemirror-transform: 1.10.2
prosemirror-dropcursor@1.8.1: prosemirror-dropcursor@1.8.1:
dependencies: dependencies:
prosemirror-state: 1.4.3 prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4 prosemirror-transform: 1.10.2
prosemirror-view: 1.40.0 prosemirror-view: 1.37.0
prosemirror-gapcursor@1.3.2: prosemirror-gapcursor@1.3.2:
dependencies: dependencies:
prosemirror-keymap: 1.2.2 prosemirror-keymap: 1.2.2
prosemirror-model: 1.25.1 prosemirror-model: 1.23.0
prosemirror-state: 1.4.3 prosemirror-state: 1.4.3
prosemirror-view: 1.40.0 prosemirror-view: 1.37.0
prosemirror-history@1.4.1: prosemirror-history@1.4.1:
dependencies: dependencies:
prosemirror-state: 1.4.3 prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4 prosemirror-transform: 1.10.2
prosemirror-view: 1.40.0 prosemirror-view: 1.37.0
rope-sequence: 1.3.4 rope-sequence: 1.3.4
prosemirror-inputrules@1.4.0: prosemirror-inputrules@1.4.0:
dependencies: dependencies:
prosemirror-state: 1.4.3 prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4 prosemirror-transform: 1.10.2
prosemirror-keymap@1.2.2: prosemirror-keymap@1.2.2:
dependencies: dependencies:
@ -18612,7 +18565,7 @@ snapshots:
dependencies: dependencies:
'@types/markdown-it': 14.1.2 '@types/markdown-it': 14.1.2
markdown-it: 14.1.0 markdown-it: 14.1.0
prosemirror-model: 1.25.1 prosemirror-model: 1.23.0
prosemirror-menu@1.2.4: prosemirror-menu@1.2.4:
dependencies: dependencies:
@ -18621,25 +18574,29 @@ snapshots:
prosemirror-history: 1.4.1 prosemirror-history: 1.4.1
prosemirror-state: 1.4.3 prosemirror-state: 1.4.3
prosemirror-model@1.23.0:
dependencies:
orderedmap: 2.1.1
prosemirror-model@1.25.1: prosemirror-model@1.25.1:
dependencies: dependencies:
orderedmap: 2.1.1 orderedmap: 2.1.1
prosemirror-schema-basic@1.2.3: prosemirror-schema-basic@1.2.3:
dependencies: dependencies:
prosemirror-model: 1.25.1 prosemirror-model: 1.23.0
prosemirror-schema-list@1.4.1: prosemirror-schema-list@1.4.1:
dependencies: dependencies:
prosemirror-model: 1.25.1 prosemirror-model: 1.23.0
prosemirror-state: 1.4.3 prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4 prosemirror-transform: 1.10.2
prosemirror-state@1.4.3: prosemirror-state@1.4.3:
dependencies: dependencies:
prosemirror-model: 1.25.1 prosemirror-model: 1.23.0
prosemirror-transform: 1.10.4 prosemirror-transform: 1.10.2
prosemirror-view: 1.40.0 prosemirror-view: 1.37.0
prosemirror-tables@1.7.1: prosemirror-tables@1.7.1:
dependencies: dependencies:
@ -18649,18 +18606,28 @@ snapshots:
prosemirror-transform: 1.10.4 prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0 prosemirror-view: 1.40.0
prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0): prosemirror-trailing-node@3.0.0(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.37.0):
dependencies: dependencies:
'@remirror/core-constants': 3.0.0 '@remirror/core-constants': 3.0.0
escape-string-regexp: 4.0.0 escape-string-regexp: 4.0.0
prosemirror-model: 1.25.1 prosemirror-model: 1.23.0
prosemirror-state: 1.4.3 prosemirror-state: 1.4.3
prosemirror-view: 1.40.0 prosemirror-view: 1.37.0
prosemirror-transform@1.10.2:
dependencies:
prosemirror-model: 1.23.0
prosemirror-transform@1.10.4: prosemirror-transform@1.10.4:
dependencies: dependencies:
prosemirror-model: 1.25.1 prosemirror-model: 1.25.1
prosemirror-view@1.37.0:
dependencies:
prosemirror-model: 1.23.0
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.2
prosemirror-view@1.40.0: prosemirror-view@1.40.0:
dependencies: dependencies:
prosemirror-model: 1.25.1 prosemirror-model: 1.25.1
@ -19950,8 +19917,6 @@ snapshots:
dependencies: dependencies:
defaults: 1.0.4 defaults: 1.0.4
web-vitals@4.2.4: {}
web-worker@1.5.0: {} web-worker@1.5.0: {}
webidl-conversions@3.0.1: {} webidl-conversions@3.0.1: {}
@ -20134,12 +20099,12 @@ snapshots:
lib0: 0.2.88 lib0: 0.2.88
yjs: 13.6.27 yjs: 13.6.27
y-prosemirror@1.2.3(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27): y-prosemirror@1.2.3(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.37.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27):
dependencies: dependencies:
lib0: 0.2.108 lib0: 0.2.108
prosemirror-model: 1.25.1 prosemirror-model: 1.23.0
prosemirror-state: 1.4.3 prosemirror-state: 1.4.3
prosemirror-view: 1.40.0 prosemirror-view: 1.37.0
y-protocols: 1.0.6(yjs@13.6.27) y-protocols: 1.0.6(yjs@13.6.27)
yjs: 13.6.27 yjs: 13.6.27