From 6126fe378b64d9ea2c2ad2688479ad8e25fc620b Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Fri, 3 Jul 2026 01:47:51 +0100 Subject: [PATCH] fix: license-gate base and kanban inserts --- .../base-embed/insert-base-embed.ts | 9 ++- .../groups/more-inserts-group.tsx | 59 +++++++++++++++---- .../components/slash-menu/command-list.tsx | 22 ++++--- .../components/slash-menu/menu-items.ts | 2 + .../slash-menu/slash-menu.module.css | 5 +- .../editor/components/slash-menu/types.ts | 1 + 6 files changed, 73 insertions(+), 25 deletions(-) diff --git a/apps/client/src/features/editor/components/base-embed/insert-base-embed.ts b/apps/client/src/features/editor/components/base-embed/insert-base-embed.ts index 8ef1fd500..107ed9576 100644 --- a/apps/client/src/features/editor/components/base-embed/insert-base-embed.ts +++ b/apps/client/src/features/editor/components/base-embed/insert-base-embed.ts @@ -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", + }); } } diff --git a/apps/client/src/features/editor/components/fixed-toolbar/groups/more-inserts-group.tsx b/apps/client/src/features/editor/components/fixed-toolbar/groups/more-inserts-group.tsx index 2e414b217..b12f609cf 100644 --- a/apps/client/src/features/editor/components/fixed-toolbar/groups/more-inserts-group.tsx +++ b/apps/client/src/features/editor/components/fixed-toolbar/groups/more-inserts-group.tsx @@ -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 = ({ 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 = ({ editor, templateMode }) => { )} {!templateMode && ( - } - onClick={() => insertBaseEmbedBlock(editor)} - > - {t("Base (Inline)")} - + + } + aria-disabled={!hasBases} + closeMenuOnClick={hasBases} + style={{ opacity: hasBases ? undefined : 0.7 }} + rightSection={ + !hasBases && ( + + {t("Upgrade")} + + ) + } + onClick={() => { + if (hasBases) insertBaseEmbedBlock(editor); + }} + > + {t("Base (Inline)")} + + )} {!templateMode && ( - } - onClick={() => insertBaseEmbedBlock(editor, { template: "kanban" })} - > - {t("Kanban")} - + + } + aria-disabled={!hasBases} + closeMenuOnClick={hasBases} + style={{ opacity: hasBases ? undefined : 0.7 }} + rightSection={ + !hasBases && ( + + {t("Upgrade")} + + ) + } + onClick={() => { + if (hasBases) insertBaseEmbedBlock(editor, { template: "kanban" }); + }} + > + {t("Kanban")} + + )} diff --git a/apps/client/src/features/editor/components/slash-menu/command-list.tsx b/apps/client/src/features/editor/components/slash-menu/command-list.tsx index d645e404f..885c056e2 100644 --- a/apps/client/src/features/editor/components/slash-menu/command-list.tsx +++ b/apps/client/src/features/editor/components/slash-menu/command-list.tsx @@ -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 ( + selectItem(itemIndex)} className={clsx(classes.menuBtn, { [classes.selectedItem]: itemIndex === selectedIndex, - [classes.disabledItem]: disabled, + [classes.gatedItem]: disabled, })} > @@ -188,6 +195,7 @@ const CommandList = ({ )} + ); })} diff --git a/apps/client/src/features/editor/components/slash-menu/menu-items.ts b/apps/client/src/features/editor/components/slash-menu/menu-items.ts index 352bdb84c..0327acdde 100644 --- a/apps/client/src/features/editor/components/slash-menu/menu-items.ts +++ b/apps/client/src/features/editor/components/slash-menu/menu-items.ts @@ -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" }); }, diff --git a/apps/client/src/features/editor/components/slash-menu/slash-menu.module.css b/apps/client/src/features/editor/components/slash-menu/slash-menu.module.css index 24ad3ecc0..5ffe1e591 100644 --- a/apps/client/src/features/editor/components/slash-menu/slash-menu.module.css +++ b/apps/client/src/features/editor/components/slash-menu/slash-menu.module.css @@ -26,7 +26,6 @@ } } -.disabledItem { - opacity: 0.45; - cursor: not-allowed; +.gatedItem { + opacity: 0.7; } diff --git a/apps/client/src/features/editor/components/slash-menu/types.ts b/apps/client/src/features/editor/components/slash-menu/types.ts index cf5bd3e42..07e1f6a4f 100644 --- a/apps/client/src/features/editor/components/slash-menu/types.ts +++ b/apps/client/src/features/editor/components/slash-menu/types.ts @@ -21,6 +21,7 @@ export type SlashMenuItemType = { searchTerms: string[]; command: (props: CommandProps) => void; disable?: (editor: ReturnType) => boolean; + requiresBases?: true; }; export type SlashMenuGroupedItemsType = {