mirror of
https://github.com/docmost/docmost.git
synced 2026-07-11 06:54:46 +10:00
fix: license-gate base and kanban inserts
This commit is contained in:
@@ -2,6 +2,8 @@ import type { Editor, Range } from "@tiptap/core";
|
||||
import { v7 as uuid7 } from "uuid";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import api from "@/lib/api-client";
|
||||
import i18n from "@/i18n.ts";
|
||||
import { getApiErrorMessage } from "@/lib/api-error";
|
||||
|
||||
function findBaseEmbedPlaceholderPos(
|
||||
editor: Editor,
|
||||
@@ -50,7 +52,7 @@ export async function insertBaseEmbedBlock(
|
||||
return true;
|
||||
})
|
||||
.run();
|
||||
} catch {
|
||||
} catch (err) {
|
||||
const pos = findBaseEmbedPlaceholderPos(editor, pendingKey);
|
||||
if (pos !== null) {
|
||||
editor
|
||||
@@ -62,6 +64,9 @@ export async function insertBaseEmbedBlock(
|
||||
})
|
||||
.run();
|
||||
}
|
||||
notifications.show({ message: "Failed to create base", color: "red" });
|
||||
notifications.show({
|
||||
message: getApiErrorMessage(err, i18n.t("Failed to create base")),
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+46
-13
@@ -1,6 +1,6 @@
|
||||
import { FC } from "react";
|
||||
import type { Editor } from "@tiptap/react";
|
||||
import { ActionIcon, Menu, Tooltip } from "@mantine/core";
|
||||
import { ActionIcon, Badge, Menu, Tooltip } from "@mantine/core";
|
||||
import {
|
||||
IconAppWindow,
|
||||
IconCalendar,
|
||||
@@ -32,6 +32,9 @@ import {
|
||||
} from "@/components/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { insertBaseEmbedBlock } from "@/features/editor/components/base-embed/insert-base-embed";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
|
||||
|
||||
interface Props {
|
||||
editor: Editor;
|
||||
@@ -40,6 +43,8 @@ interface Props {
|
||||
|
||||
export const MoreInsertsGroup: FC<Props> = ({ editor, templateMode }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const hasBases = useHasFeature(Feature.BASES);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
|
||||
const setEmbed = (provider: string) =>
|
||||
editor.chain().focus().setEmbed({ provider }).run();
|
||||
@@ -106,20 +111,48 @@ export const MoreInsertsGroup: FC<Props> = ({ editor, templateMode }) => {
|
||||
</Menu.Item>
|
||||
)}
|
||||
{!templateMode && (
|
||||
<Menu.Item
|
||||
leftSection={<IconTable size={16} />}
|
||||
onClick={() => insertBaseEmbedBlock(editor)}
|
||||
>
|
||||
{t("Base (Inline)")}
|
||||
</Menu.Item>
|
||||
<Tooltip label={upgradeLabel} disabled={hasBases} position="right">
|
||||
<Menu.Item
|
||||
leftSection={<IconTable size={16} />}
|
||||
aria-disabled={!hasBases}
|
||||
closeMenuOnClick={hasBases}
|
||||
style={{ opacity: hasBases ? undefined : 0.7 }}
|
||||
rightSection={
|
||||
!hasBases && (
|
||||
<Badge size="xs" variant="light" color="gray">
|
||||
{t("Upgrade")}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
if (hasBases) insertBaseEmbedBlock(editor);
|
||||
}}
|
||||
>
|
||||
{t("Base (Inline)")}
|
||||
</Menu.Item>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!templateMode && (
|
||||
<Menu.Item
|
||||
leftSection={<IconLayoutKanban size={16} />}
|
||||
onClick={() => insertBaseEmbedBlock(editor, { template: "kanban" })}
|
||||
>
|
||||
{t("Kanban")}
|
||||
</Menu.Item>
|
||||
<Tooltip label={upgradeLabel} disabled={hasBases} position="right">
|
||||
<Menu.Item
|
||||
leftSection={<IconLayoutKanban size={16} />}
|
||||
aria-disabled={!hasBases}
|
||||
closeMenuOnClick={hasBases}
|
||||
style={{ opacity: hasBases ? undefined : 0.7 }}
|
||||
rightSection={
|
||||
!hasBases && (
|
||||
<Badge size="xs" variant="light" color="gray">
|
||||
{t("Upgrade")}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
if (hasBases) insertBaseEmbedBlock(editor, { template: "kanban" });
|
||||
}}
|
||||
>
|
||||
{t("Kanban")}
|
||||
</Menu.Item>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Paper,
|
||||
ScrollArea,
|
||||
Text,
|
||||
Tooltip,
|
||||
UnstyledButton,
|
||||
VisuallyHidden,
|
||||
} from "@mantine/core";
|
||||
@@ -18,6 +19,7 @@ import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
|
||||
|
||||
const CommandList = ({
|
||||
items,
|
||||
@@ -37,11 +39,12 @@ const CommandList = ({
|
||||
const [selectionAnnouncement, setSelectionAnnouncement] = useState("");
|
||||
|
||||
const hasBases = useHasFeature(Feature.BASES);
|
||||
// Title must match the "Base (Inline)" item in menu-items.ts. Without the
|
||||
// bases entitlement the item stays visible but disabled; an expired license
|
||||
// the client can't detect falls through to a handled create failure.
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
// Without the bases entitlement the item stays visible but inert; an
|
||||
// expired license the client can't detect falls through to a handled
|
||||
// create failure.
|
||||
const isItemDisabled = (item: SlashMenuItemType) =>
|
||||
!hasBases && item.title === "Base (Inline)";
|
||||
!hasBases && item.requiresBases === true;
|
||||
|
||||
const flatItems = useMemo(() => {
|
||||
return Object.values(items).flat();
|
||||
@@ -152,18 +155,22 @@ const CommandList = ({
|
||||
const itemIndex = flatIndex;
|
||||
const disabled = isItemDisabled(item);
|
||||
return (
|
||||
<Tooltip
|
||||
key={itemIndex}
|
||||
label={upgradeLabel}
|
||||
disabled={!disabled}
|
||||
position="right"
|
||||
>
|
||||
<UnstyledButton
|
||||
data-item-index={itemIndex}
|
||||
key={itemIndex}
|
||||
id={`slash-command-option-${itemIndex}`}
|
||||
role="option"
|
||||
aria-selected={itemIndex === selectedIndex}
|
||||
aria-disabled={disabled}
|
||||
disabled={disabled}
|
||||
onClick={() => selectItem(itemIndex)}
|
||||
className={clsx(classes.menuBtn, {
|
||||
[classes.selectedItem]: itemIndex === selectedIndex,
|
||||
[classes.disabledItem]: disabled,
|
||||
[classes.gatedItem]: disabled,
|
||||
})}
|
||||
>
|
||||
<Group wrap="nowrap">
|
||||
@@ -188,6 +195,7 @@ const CommandList = ({
|
||||
)}
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -366,6 +366,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
||||
description: "Insert an inline base on this page",
|
||||
searchTerms: ["base", "database", "table", "grid", "spreadsheet"],
|
||||
icon: IconTable,
|
||||
requiresBases: true,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
insertBaseEmbedBlock(editor, { range });
|
||||
},
|
||||
@@ -375,6 +376,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
||||
description: "Insert a kanban board on this page",
|
||||
searchTerms: ["kanban", "board", "cards", "status", "task", "database"],
|
||||
icon: IconLayoutKanban,
|
||||
requiresBases: true,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
insertBaseEmbedBlock(editor, { range, template: "kanban" });
|
||||
},
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.disabledItem {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
.gatedItem {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export type SlashMenuItemType = {
|
||||
searchTerms: string[];
|
||||
command: (props: CommandProps) => void;
|
||||
disable?: (editor: ReturnType<typeof useEditor>) => boolean;
|
||||
requiresBases?: true;
|
||||
};
|
||||
|
||||
export type SlashMenuGroupedItemsType = {
|
||||
|
||||
Reference in New Issue
Block a user