mirror of
https://github.com/docmost/docmost.git
synced 2026-07-16 06:08:32 +10:00
b411f52c18
The placeholder rendered the full 10-row BaseTableSkeleton (~440px)
while waiting for the create response, then BaseTable mounted, ran its
own queries, and rendered the same 10-row skeleton again until rows
loaded. The actual content for a freshly-created empty base is ~112px
— so the swap shrank the doc by ~330px and on a short page the
browser clamped scrollY past the new doc bottom, manifesting as a
"jump to top of editor."
Two changes to keep the height constant end-to-end:
1. BaseTableSkeleton now accepts a `rows` prop (default 10). The
placeholder in BaseEmbedView passes `rows={0}` so the skeleton
matches the height of the eventual empty base shell — header row +
AddRow button, no fake body rows.
2. The Database slash command now seeds `["bases", id]` and the
`["base-rows", id, undefined, undefined, undefined]` infinite-query
cache from the create response (the endpoint already returns the
full base with properties + views; the typed return was just too
narrow). BaseTable mounts with baseLoading/rowsLoading already
false and skips its own skeleton — no transient grow-then-shrink
between placeholder and final content.
End state: placeholder height ≈ rendered-empty-base height, and no
intermediate skeleton appears while BaseTable is "loading." The
scrollY clamp can't fire because the doc never shrinks.
115 lines
4.3 KiB
TypeScript
115 lines
4.3 KiB
TypeScript
import { NodeViewWrapper, NodeViewProps } from "@tiptap/react";
|
|
import { Box, Text } from "@mantine/core";
|
|
import { useEffect, useRef } from "react";
|
|
import { BaseTable } from "@/features/base/components/base-table";
|
|
import { BaseTableSkeleton } from "@/features/base/components/base-table-skeleton";
|
|
import { useBaseQuery } from "@/features/base/queries/base-query";
|
|
|
|
const SIDE_GUTTER = 8;
|
|
|
|
// Extend the scroll viewport on both sides (toward AppShell.Main's
|
|
// edges), but offset the grid content with padding-left = extendLeft
|
|
// so the first cell still lines up with page-content on load. The
|
|
// extra leftward area becomes scrollable empty space the user can
|
|
// pan into — same behavior as Notion's inline databases.
|
|
function applyExtension(wrapper: HTMLDivElement) {
|
|
const rect = wrapper.getBoundingClientRect();
|
|
if (rect.width === 0) return;
|
|
|
|
const main = wrapper.closest("main") as HTMLElement | null;
|
|
const mainRect = main?.getBoundingClientRect();
|
|
const targetLeft = (mainRect?.left ?? 0) + SIDE_GUTTER;
|
|
const targetRight = mainRect
|
|
? mainRect.right - SIDE_GUTTER
|
|
: window.innerWidth - SIDE_GUTTER;
|
|
|
|
const extendLeft = Math.max(0, rect.left - targetLeft);
|
|
const extendRight = Math.max(0, targetRight - rect.right);
|
|
|
|
wrapper.style.setProperty("--embed-extend-l", `${extendLeft}px`);
|
|
wrapper.style.setProperty("--embed-extend-r", `${extendRight}px`);
|
|
wrapper.style.setProperty("--embed-grid-pad-left", `${extendLeft}px`);
|
|
// Symmetric right-side padding so the user can pan past the last
|
|
// column into empty space — same behaviour as Notion, gives the
|
|
// table breathing room on the right when scrolled fully right.
|
|
wrapper.style.setProperty("--embed-grid-pad-right", `${extendRight}px`);
|
|
// Inline sticky band clears the fixed PageHeader. Standalone leaves
|
|
// the var unset (resolves to the rule default of 0).
|
|
wrapper.style.setProperty(
|
|
"--sticky-band-top",
|
|
"var(--page-header-height)",
|
|
);
|
|
}
|
|
|
|
export function BaseEmbedView({ node }: NodeViewProps) {
|
|
const pageId = node.attrs.pageId as string | null;
|
|
const pendingKey = node.attrs.pendingKey as string | null;
|
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
|
// Suppress the query while the slash command is still waiting for the
|
|
// server to assign a pageId — useBaseQuery would otherwise fire with
|
|
// an empty key and surface a transient error.
|
|
const { isLoading, isError } = useBaseQuery(pendingKey ? "" : pageId ?? "");
|
|
|
|
useEffect(() => {
|
|
const wrapper = wrapperRef.current;
|
|
if (!wrapper) return;
|
|
|
|
const update = () => applyExtension(wrapper);
|
|
update();
|
|
|
|
const ro = new ResizeObserver(update);
|
|
ro.observe(wrapper);
|
|
// Sidebar collapse changes <main>'s left/width without resizing
|
|
// the wrapper itself, so observe <main> too.
|
|
const main = wrapper.closest("main");
|
|
if (main) ro.observe(main);
|
|
|
|
window.addEventListener("resize", update);
|
|
return () => {
|
|
ro.disconnect();
|
|
window.removeEventListener("resize", update);
|
|
};
|
|
}, [isLoading, isError, pageId]);
|
|
|
|
let content: React.ReactNode;
|
|
if (pendingKey) {
|
|
// Slash command inserted the embed and is awaiting the server's
|
|
// assigned pageId. Render with `rows={0}` so the placeholder
|
|
// matches the height of the eventual empty base shell — anything
|
|
// taller would shrink hundreds of px on swap, and on a short doc
|
|
// the browser would clamp scrollY (looks like "page jumps to top
|
|
// of editor" when the create response lands). The slash command
|
|
// also prefills the React Query cache so BaseTable mounts with
|
|
// baseLoading/rowsLoading already false and skips its own skeleton.
|
|
content = <BaseTableSkeleton rows={0} />;
|
|
} else if (!pageId) {
|
|
content = (
|
|
<Box p="md">
|
|
<Text c="red">Invalid base embed (missing page id)</Text>
|
|
</Box>
|
|
);
|
|
} else if (isLoading) {
|
|
content = (
|
|
<Box p="md">
|
|
<Text c="dimmed">Loading...</Text>
|
|
</Box>
|
|
);
|
|
} else if (isError) {
|
|
content = (
|
|
<Box p="md" bg="gray.0" style={{ borderRadius: 8 }}>
|
|
<Text c="dimmed">You don't have access to this database.</Text>
|
|
</Box>
|
|
);
|
|
} else {
|
|
content = <BaseTable pageId={pageId} embedded />;
|
|
}
|
|
|
|
return (
|
|
<NodeViewWrapper>
|
|
<div ref={wrapperRef} style={{ minHeight: 200 }}>
|
|
{content}
|
|
</div>
|
|
</NodeViewWrapper>
|
|
);
|
|
}
|