mirror of
https://github.com/docmost/docmost.git
synced 2025-11-14 02:11:17 +10:00
Compare commits
1 Commits
fix/editor
...
feat/ukrai
| Author | SHA1 | Date | |
|---|---|---|---|
| f9bd5e7b57 |
@ -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",
|
||||||
|
|||||||
@ -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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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" }}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
import { atom } from "jotai";
|
|
||||||
|
|
||||||
type SearchAndReplaceAtomType = {
|
|
||||||
isOpen: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const searchAndReplaceStateAtom = atom<SearchAndReplaceAtomType>({
|
|
||||||
isOpen: false,
|
|
||||||
});
|
|
||||||
@ -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;
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
.findDialog{
|
|
||||||
@media print {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.findDialog div[data-position="right"].mantine-Input-section {
|
|
||||||
justify-content: right;
|
|
||||||
padding-right: 8px;
|
|
||||||
}
|
|
||||||
@ -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[];
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,9 +0,0 @@
|
|||||||
.search-result{
|
|
||||||
background: #ffff65;
|
|
||||||
color: #212529;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-result-current{
|
|
||||||
background: #ffc266 !important;
|
|
||||||
color: #212529;
|
|
||||||
}
|
|
||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,9 +22,3 @@
|
|||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumbDiv {
|
|
||||||
overflow: hidden;
|
|
||||||
@media (max-width: $mantine-breakpoint-sm) {
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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()}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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 },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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",
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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 }),
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.21.0",
|
"version": "0.20.4",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
5
apps/server/src/database/types/db.d.ts
vendored
5
apps/server/src/database/types/db.d.ts
vendored
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule apps/server/src/ee updated: 4c252d1ec3...70eb45eaec
@ -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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>`;
|
||||||
|
|||||||
71
package.json
71
package.json
@ -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"
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
import { SearchAndReplace } from './search-and-replace'
|
|
||||||
export * from './search-and-replace'
|
|
||||||
export default SearchAndReplace
|
|
||||||
@ -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
223
pnpm-lock.yaml
generated
@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user