feat: base nodeview menu

This commit is contained in:
Philipinho
2026-06-14 15:32:36 +01:00
parent 7d98c7c069
commit f1af3e78a1
2 changed files with 72 additions and 5 deletions
@@ -1,13 +1,14 @@
import { NodeViewWrapper, NodeViewProps } from "@tiptap/react"; import { NodeViewWrapper, NodeViewProps } from "@tiptap/react";
import { Box, Text } from "@mantine/core"; import { ActionIcon, Box, Menu, Text } from "@mantine/core";
import { useEffect, useRef } from "react"; import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { BaseView } from "@/ee/base/components/base-view"; import { BaseView } from "@/ee/base/components/base-view";
import { BaseTableSkeleton } from "@/ee/base/components/base-table-skeleton"; import { BaseTableSkeleton } from "@/ee/base/components/base-table-skeleton";
import { useBaseQuery } from "@/ee/base/queries/base-query"; import { useBaseQuery } from "@/ee/base/queries/base-query";
import { pinOffsetWatcher } from "@docmost/editor-ext"; import { pinOffsetWatcher } from "@docmost/editor-ext";
import { useHasFeature } from "@/ee/hooks/use-feature"; import { useHasFeature } from "@/ee/hooks/use-feature";
import { Feature } from "@/ee/features"; import { Feature } from "@/ee/features";
import { IconTable } from "@tabler/icons-react"; import { IconDots, IconTable, IconX } from "@tabler/icons-react";
import { usePageQuery } from "@/features/page/queries/page-query"; import { usePageQuery } from "@/features/page/queries/page-query";
import classes from "./base-embed.module.css"; import classes from "./base-embed.module.css";
@@ -49,11 +50,13 @@ function applyExtension(wrapper: HTMLDivElement) {
); );
} }
export function BaseEmbedView({ node, editor }: NodeViewProps) { export function BaseEmbedView({ node, editor, deleteNode }: NodeViewProps) {
const { t } = useTranslation();
const pageId = node.attrs.pageId as string | null; const pageId = node.attrs.pageId as string | null;
const pendingKey = node.attrs.pendingKey as string | null; const pendingKey = node.attrs.pendingKey as string | null;
const wrapperRef = useRef<HTMLDivElement | null>(null); const wrapperRef = useRef<HTMLDivElement | null>(null);
const hasBases = useHasFeature(Feature.BASES); const hasBases = useHasFeature(Feature.BASES);
const [menuOpen, setMenuOpen] = useState(false);
// Suppress the query while the slash command awaits the server-assigned // Suppress the query while the slash command awaits the server-assigned
// pageId; useBaseQuery would otherwise fire with an empty key. // pageId; useBaseQuery would otherwise fire with an empty key.
const { data: base, isLoading, isError } = useBaseQuery( const { data: base, isLoading, isError } = useBaseQuery(
@@ -95,6 +98,8 @@ export function BaseEmbedView({ node, editor }: NodeViewProps) {
// mounts) is reserved only for the skeleton/loading/table states. // mounts) is reserved only for the skeleton/loading/table states.
const isCompact = !pendingKey && (!pageId || isError); const isCompact = !pendingKey && (!pageId || isError);
const showControls = editor.isEditable && !pendingKey;
let content: React.ReactNode; let content: React.ReactNode;
if (pendingKey) { if (pendingKey) {
// Slash command inserted the embed and is awaiting the server's // Slash command inserted the embed and is awaiting the server's
@@ -133,7 +138,37 @@ export function BaseEmbedView({ node, editor }: NodeViewProps) {
} }
return ( return (
<NodeViewWrapper className={classes.handleGutter}> <NodeViewWrapper
className={classes.handleGutter}
data-menu-open={menuOpen ? "true" : "false"}
>
{showControls && (
<div
className={classes.controls}
contentEditable={false}
onMouseDown={(e) => e.preventDefault()}
>
<Menu position="bottom-end" withinPortal onChange={setMenuOpen}>
<Menu.Target>
<ActionIcon
variant="default"
size="sm"
aria-label={t("Base options")}
>
<IconDots size={16} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<IconX size={14} />}
onClick={() => deleteNode()}
>
{t("Remove from page")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
</div>
)}
<div data-drag-preview hidden className={classes.dragPreview}> <div data-drag-preview hidden className={classes.dragPreview}>
<IconTable size={16} /> <IconTable size={16} />
<span>{page?.title?.trim() || "Untitled base"}</span> <span>{page?.title?.trim() || "Untitled base"}</span>
@@ -1,8 +1,40 @@
.handleGutter { .handleGutter {
position: relative;
margin-left: -1.5rem; margin-left: -1.5rem;
padding-left: 1.5rem; padding-left: 1.5rem;
} }
.controls {
position: absolute;
bottom: calc(100% + 4px);
right: 0;
z-index: 20;
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease;
}
.controls::after {
content: "";
position: absolute;
top: 100%;
left: 0;
right: 0;
height: 8px;
}
.handleGutter:hover .controls,
.handleGutter[data-menu-open="true"] .controls {
opacity: 1;
pointer-events: auto;
}
@media print {
.controls {
display: none !important;
}
}
@media (max-width: 48em) { @media (max-width: 48em) {
.handleGutter { .handleGutter {
margin-left: -1rem; margin-left: -1rem;