chore(base): drop pre-virtualizer-fix scroll-jump workarounds

Two pieces of code added under the wrong height-mismatch theory of
the inline-embed scroll jump are no longer load-bearing now that the
real cause (virtual-core's _willUpdate calling _scrollToOffset(NaN))
is fixed by the initialOffset seed in grid-container.tsx:

  - BaseTableSkeleton's `rows` prop, whose only consumer was the
    "creating database" placeholder passing rows=0 to "match" the
    eventual empty-base height. Reverted to a fixed 10-row skeleton.
  - The Database slash command's React Query cache prefill of
    ["bases", id] and ["base-rows", id, …] (~30 lines), which
    existed to skip BaseTable's own loading skeleton on swap. The
    create endpoint return type is back to { id }.

The placeholder approach (pendingKey + setNodeMarkup patch) stays —
that's what gives the user visible state during the create request,
unrelated to the scroll jump.
This commit is contained in:
Philipinho
2026-04-28 11:29:28 +01:00
parent e9926d5cef
commit dac65914ed
3 changed files with 10 additions and 63 deletions
@@ -5,24 +5,14 @@ import classes from "@/features/base/styles/base-table-skeleton.module.css";
const ROW_NUMBER_WIDTH = 64;
const COLUMN_WIDTH = 180;
const COLUMN_COUNT = 6;
const DEFAULT_ROW_COUNT = 10;
const ROW_COUNT = 10;
// Deterministic per-cell widths so the skeleton doesn't flicker between
// renders. Values are rough normal distribution around 55-85 % of cell.
const CELL_WIDTH_RATIOS = [0.78, 0.62, 0.84, 0.55, 0.71, 0.66];
const HEADER_WIDTH_RATIOS = [0.42, 0.58, 0.5, 0.64, 0.46, 0.54];
type BaseTableSkeletonProps = {
// Override the body row count. Pass 0 when rendering as the
// "creating database" placeholder for a freshly-inserted inline embed
// — the eventual empty base has no rows, so a 10-row skeleton would
// shrink ~330px on swap and trip the browser's scrollY clamp.
rows?: number;
};
export function BaseTableSkeleton({
rows = DEFAULT_ROW_COUNT,
}: BaseTableSkeletonProps = {}) {
export function BaseTableSkeleton() {
const gridTemplateColumns = [
`${ROW_NUMBER_WIDTH}px`,
...Array.from({ length: COLUMN_COUNT }, () => `${COLUMN_WIDTH}px`),
@@ -64,7 +54,7 @@ export function BaseTableSkeleton({
</div>
))}
{Array.from({ length: rows }).map((_, rowIndex) => (
{Array.from({ length: ROW_COUNT }).map((_, rowIndex) => (
<div key={`row-${rowIndex}`} style={{ display: "contents" }}>
<div className={gridClasses.cell}>
<div className={classes.cellInner}>
@@ -74,14 +74,9 @@ export function BaseEmbedView({ node }: NodeViewProps) {
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} />;
// assigned pageId — render the same skeleton BaseTable shows
// during its own initial load so the swap is visually a no-op.
content = <BaseTableSkeleton />;
} else if (!pageId) {
content = (
<Box p="md">
@@ -55,13 +55,6 @@ import {
import api from "@/lib/api-client";
import { notifications } from "@mantine/notifications";
import type { Editor } from "@tiptap/core";
import type { InfiniteData } from "@tanstack/react-query";
import { queryClient } from "@/main";
import type {
IBase,
IBaseRow,
} from "@/features/base/types/base.types";
import type { IPagination } from "@/lib/types";
// Resolve the position of a baseEmbed placeholder by its pendingKey.
// Used by the Database slash command to patch in the real pageId once
@@ -520,10 +513,9 @@ const CommandGroups: SlashMenuGroupedItemsType = {
if (!parentPageId) return;
// Insert a placeholder embed at the slash position synchronously
// so (a) the position is established before any focus/selection
// drift during the await, and (b) the user sees a skeleton in
// the document instead of an empty gap. The API call then patches
// the real pageId into this exact node, identified by pendingKey.
// so the user sees a skeleton immediately while we wait on the
// create-base API. Once the response lands we look the
// placeholder up by its pendingKey and patch in the real pageId.
const pendingKey =
typeof crypto !== "undefined" && "randomUUID" in crypto
? crypto.randomUUID()
@@ -537,40 +529,10 @@ const CommandGroups: SlashMenuGroupedItemsType = {
.run();
try {
// The create endpoint returns the full base (properties +
// views), not just an id — see base.service.ts `create`. Type
// it as IBase so we can seed the React Query cache below.
const res = await api.post<IBase>("/bases/inline-embed", {
const res = await api.post<{ id: string }>("/bases/inline-embed", {
parentPageId,
});
// Seed the caches BaseTable will read on mount so it doesn't
// render its own (10-row) skeleton between our placeholder
// and the actual content. Without this seeding the wrapper
// would grow to ~436px during BaseTable's load and shrink to
// ~112px once the rows resolve — on a short doc the shrink
// pushes scrollY past the new doc bottom and the browser
// clamps to 0, which is the "jump to top" the user reported.
queryClient.setQueryData<IBase>(["bases", res.data.id], res.data);
queryClient.setQueryData<InfiniteData<IPagination<IBaseRow>>>(
["base-rows", res.data.id, undefined, undefined, undefined],
{
pages: [
{
items: [],
meta: {
limit: 100,
hasNextPage: false,
hasPrevPage: false,
nextCursor: null,
prevCursor: null,
},
},
],
pageParams: [undefined],
},
);
const pos = findBaseEmbedPlaceholderPos(editor, pendingKey);
if (pos === null) return;
editor