From b1ec6d45accbb893e2fcd75d80993326ca536774 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 18 Nov 2025 13:13:13 +0800 Subject: [PATCH] fix: scroll to top when swithc embeded database view --- src/components/app/hooks/useViewOperations.ts | 4 + src/components/database/Database.tsx | 13 +- src/components/database/DatabaseViews.tsx | 143 +++++++++++++----- .../blocks/database/DatabaseBlock.tsx | 35 ++++- .../database/components/DatabaseContent.tsx | 5 +- .../database/hooks/useDatabaseLoading.ts | 17 ++- .../panels/slash-panel/SlashPanel.tsx | 36 ++++- 7 files changed, 206 insertions(+), 47 deletions(-) diff --git a/src/components/app/hooks/useViewOperations.ts b/src/components/app/hooks/useViewOperations.ts index 5d10e870..453131d7 100644 --- a/src/components/app/hooks/useViewOperations.ts +++ b/src/components/app/hooks/useViewOperations.ts @@ -56,8 +56,11 @@ export function useViewOperations() { )?.database_id; if (databaseId) { + console.debug('[useViewOperations] mapped view to database', { viewId: id, databaseId }); resolve(databaseId); + return; } + console.warn('[useViewOperations] databaseId not found for view', { viewId: id }); }; observeEvent(); @@ -165,6 +168,7 @@ export function useViewOperations() { return Promise.reject(new Error('Invalid view layout')); } + console.debug('[useViewOperations] loadView resolved layout', { viewId: id, layout: view?.layout, collabType }); if (collabType === Types.Document) { let awareness: Awareness | undefined; diff --git a/src/components/database/Database.tsx b/src/components/database/Database.tsx index 07dc87dc..32828cf0 100644 --- a/src/components/database/Database.tsx +++ b/src/components/database/Database.tsx @@ -47,6 +47,7 @@ export interface Database2Props { showActions?: boolean; createFolderView?: (payload: CreateFolderViewPayload) => Promise; getViewIdFromDatabaseId?: (databaseId: string) => Promise; + embeddedHeight?: number; } function Database(props: Database2Props) { @@ -66,6 +67,7 @@ function Database(props: Database2Props) { navigateToView, modalRowId, isDocumentBlock, + embeddedHeight, } = props; const database = doc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase | null; @@ -155,8 +157,14 @@ function Database(props: Database2Props) { const rowOrdersData = rowOrders?.toJSON() || []; const ids = rowOrdersData.map(({ id }: { id: string }) => id); + console.debug('[Database] row orders updated', { + viewId, + iidIndex, + ids, + raw: rowOrdersData, + }); setRowIds(ids); - }, [rowOrders]); + }, [iidIndex, rowOrders, viewId]); useEffect(() => { void handleUpdateRowDocMap(); @@ -252,13 +260,14 @@ function Database(props: Database2Props) { {rowId ? ( ) : ( -
+
)} diff --git a/src/components/database/DatabaseViews.tsx b/src/components/database/DatabaseViews.tsx index 40a0e163..7bbf5ebf 100644 --- a/src/components/database/DatabaseViews.tsx +++ b/src/components/database/DatabaseViews.tsx @@ -31,22 +31,23 @@ function DatabaseViews({ iidIndex, viewName, visibleViewIds, + fixedHeight, }: { onChangeView: (viewId: string) => void; viewId: string; iidIndex: string; viewName?: string; visibleViewIds?: string[]; + fixedHeight?: number; }) { const { childViews, viewIds } = useDatabaseViewsSelector(iidIndex, visibleViewIds); const [isLoading, setIsLoading] = useState(false); const [layout, setLayout] = useState(null); - const pendingScrollTopRef = useRef(null); - const restoreRafRef = useRef(); const [viewVisible, setViewVisible] = useState(true); const viewContainerRef = useRef(null); - const [lockedHeight, setLockedHeight] = useState(null); + const [lockedHeight, setLockedHeight] = useState(fixedHeight ?? null); + const lastScrollRef = useRef(null); const value = useMemo(() => { return Math.max( 0, @@ -70,6 +71,13 @@ function DatabaseViews({ const observerEvent = () => { setLayout(Number(activeView.get(YjsDatabaseKey.layout)) as DatabaseViewLayout); setIsLoading(false); + const currentHeight = viewContainerRef.current?.offsetHeight ?? null; + logDebug('[DatabaseViews] layout set', { + layout: Number(activeView.get(YjsDatabaseKey.layout)), + viewId, + iidIndex, + currentHeight, + }); }; observerEvent(); @@ -84,17 +92,24 @@ function DatabaseViews({ const handleViewChange = useCallback( (newViewId: string) => { const scrollElement = getScrollElement(); - pendingScrollTopRef.current = scrollElement?.scrollTop ?? null; + lastScrollRef.current = scrollElement?.scrollTop ?? null; logDebug('[DatabaseViews] captured scroll before view change', { - scrollTop: pendingScrollTopRef.current, + scrollTop: lastScrollRef.current, }); - setLockedHeight(viewContainerRef.current?.offsetHeight ?? null); - setViewVisible(false); + const currentHeight = viewContainerRef.current?.offsetHeight; + const heightToLock = fixedHeight ?? currentHeight ?? null; + setLockedHeight(heightToLock ?? null); + logDebug('[DatabaseViews] handleViewChange height lock', { + currentHeight, + fixedHeight, + heightToLock, + }); setIsLoading(true); + setViewVisible(false); // Hide view during transition to prevent flash onChangeView(newViewId); }, - [onChangeView] + [fixedHeight, onChangeView] ); const skeleton = useMemo(() => { @@ -110,6 +125,7 @@ function DatabaseViews({ } }, [layout]); + // Simple conditional rendering - Board's autoScrollForElements doesn't support keep-alive const view = useMemo(() => { if (isLoading) return skeleton; switch (layout) { @@ -122,53 +138,94 @@ function DatabaseViews({ } }, [layout, isLoading, skeleton]); + useEffect(() => { + if (!isLoading && viewContainerRef.current) { + const h = viewContainerRef.current.offsetHeight; + if (h > 0) { + logDebug('[DatabaseViews] measured container height', { + height: h, + viewId, + iidIndex, + layout, + }); + } + } + }, [isLoading, viewVisible, layout, viewId]); + + // Scroll restoration with RAF enforcement + // Even with keep-mounted, Board's autoScrollForElements can still interfere on first switch useEffect(() => { if (isLoading) return; - if (pendingScrollTopRef.current == null) return; + if (lastScrollRef.current == null) return; const scrollElement = getScrollElement(); - if (!scrollElement) return; - - const target = pendingScrollTopRef.current; - if (restoreRafRef.current !== undefined) { - cancelAnimationFrame(restoreRafRef.current); + if (!scrollElement) { + lastScrollRef.current = null; + return; } - let start = performance.now(); - setViewVisible(false); - const enforce = () => { - const delta = scrollElement.scrollTop - target; - if (Math.abs(delta) > 0.5) { - scrollElement.scrollTop = target; - logDebug('[DatabaseViews] enforcing scroll position', { - target, - applied: scrollElement.scrollTop, + const targetScroll = lastScrollRef.current; + let rafCount = 0; + let rafId: number; + + // Use RAF loop to enforce scroll position + // This handles Board's autoScrollForElements which may still interfere on first display + const enforceScroll = () => { + const currentScroll = scrollElement.scrollTop; + const delta = Math.abs(currentScroll - targetScroll); + + if (delta > 0.5) { + scrollElement.scrollTop = targetScroll; + logDebug('[DatabaseViews] RAF restore scroll', { + frame: rafCount, + target: targetScroll, + current: currentScroll, delta, }); } - if (performance.now() - start < 350) { - restoreRafRef.current = requestAnimationFrame(enforce); + rafCount++; + // Run for 3 frames (~48ms) - shorter than before since views stay mounted + if (rafCount < 3) { + rafId = requestAnimationFrame(enforceScroll); } else { - pendingScrollTopRef.current = null; - restoreRafRef.current = undefined; - setViewVisible(true); - setLockedHeight(null); - logDebug('[DatabaseViews] scroll enforcement completed', { + logDebug('[DatabaseViews] scroll restoration completed', { final: scrollElement.scrollTop, + target: targetScroll, }); + lastScrollRef.current = null; + setViewVisible(true); } }; - restoreRafRef.current = requestAnimationFrame(enforce); + rafId = requestAnimationFrame(enforceScroll); return () => { - if (restoreRafRef.current !== undefined) { - cancelAnimationFrame(restoreRafRef.current); - restoreRafRef.current = undefined; + if (rafId) { + cancelAnimationFrame(rafId); } }; - }, [isLoading, layout, viewId]); + }, [isLoading, viewId]); + + useEffect(() => { + setLockedHeight(fixedHeight ?? null); + }, [fixedHeight]); + + useEffect(() => { + if (!viewContainerRef.current) return; + const rect = viewContainerRef.current.getBoundingClientRect(); + logDebug('[DatabaseViews] container render', { + height: rect.height, + lockedHeight, + fixedHeight, + isLoading, + viewVisible, + viewId, + layout, + }); + }, [lockedHeight, fixedHeight, isLoading, viewVisible, viewId, layout]); + + const effectiveHeight = lockedHeight ?? fixedHeight ?? null; return ( <> @@ -192,11 +249,21 @@ function DatabaseViews({
diff --git a/src/components/editor/components/blocks/database/DatabaseBlock.tsx b/src/components/editor/components/blocks/database/DatabaseBlock.tsx index 4321797d..56414a2d 100644 --- a/src/components/editor/components/blocks/database/DatabaseBlock.tsx +++ b/src/components/editor/components/blocks/database/DatabaseBlock.tsx @@ -50,7 +50,13 @@ export const DatabaseBlock = memo( if (!sharedRoot) return; const setStatus = () => { - setHasDatabase(!!sharedRoot.get(YjsEditorKey.database)); + const hasDb = !!sharedRoot.get(YjsEditorKey.database); + setHasDatabase(hasDb); + if (hasDb) { + console.debug('[DatabaseBlock] database found in doc', { viewId }); + } else { + console.warn('[DatabaseBlock] database missing in doc', { viewId }); + } }; setStatus(); @@ -59,7 +65,26 @@ export const DatabaseBlock = memo( return () => { sharedRoot.unobserve(setStatus); }; - }, [doc]); + }, [doc, viewId]); + + const EMBEDDED_DB_HEIGHT = 560; + + const logContainerSize = () => { + if (!containerRef.current) return; + const rect = containerRef.current.getBoundingClientRect(); + console.debug('[DatabaseBlock] container size', { + viewId, + selectedViewId, + width, + height: rect.height, + paddingStart, + paddingEnd, + }); + }; + + useEffect(() => { + logContainerSize(); + }, [selectedViewId, width, paddingStart, paddingEnd]); return (
@@ -70,6 +95,11 @@ export const DatabaseBlock = memo( contentEditable={false} ref={containerRef} className='container-bg relative my-1 flex w-full select-none flex-col' + style={{ + minHeight: EMBEDDED_DB_HEIGHT, + height: EMBEDDED_DB_HEIGHT, + width: '100%', + }} >
diff --git a/src/components/editor/components/blocks/database/components/DatabaseContent.tsx b/src/components/editor/components/blocks/database/components/DatabaseContent.tsx index 324f218d..8b497f33 100644 --- a/src/components/editor/components/blocks/database/components/DatabaseContent.tsx +++ b/src/components/editor/components/blocks/database/components/DatabaseContent.tsx @@ -24,6 +24,7 @@ interface DatabaseContentProps { visibleViewIds: string[]; onChangeView: (viewId: string) => void; context: DatabaseContextState; + fixedHeight?: number; } export const DatabaseContent = ({ @@ -45,6 +46,7 @@ export const DatabaseContent = ({ visibleViewIds, onChangeView, context, + fixedHeight, }: DatabaseContentProps) => { const { t } = useTranslation(); const isPublishVarient = context?.variant === UIVariant.Publish; @@ -76,6 +78,7 @@ export const DatabaseContent = ({ paddingStart={paddingStart} paddingEnd={paddingEnd} isDocumentBlock={true} + embeddedHeight={fixedHeight} />
); @@ -92,4 +95,4 @@ export const DatabaseContent = ({ )}
); -}; \ No newline at end of file +}; diff --git a/src/components/editor/components/blocks/database/hooks/useDatabaseLoading.ts b/src/components/editor/components/blocks/database/hooks/useDatabaseLoading.ts index ff068d91..5431655d 100644 --- a/src/components/editor/components/blocks/database/hooks/useDatabaseLoading.ts +++ b/src/components/editor/components/blocks/database/hooks/useDatabaseLoading.ts @@ -75,10 +75,12 @@ export const useDatabaseLoading = ({ viewId, loadView, loadViewMeta }: UseDataba const loadViewData = async () => { try { const view = await retryLoadView(viewId); + console.debug('[DatabaseBlock] loaded view doc', { viewId }); setDoc(view); setNotFound(false); } catch (error) { + console.error('[DatabaseBlock] failed to load view doc', { viewId, error }); setNotFound(true); } }; @@ -91,15 +93,26 @@ export const useDatabaseLoading = ({ viewId, loadView, loadViewMeta }: UseDataba }, [visibleViewIds]); useLayoutEffect(() => { - void loadViewMetaWithCallback(viewId).then(() => { + void loadViewMetaWithCallback(viewId).then((meta) => { if (!viewIdsRef.current.includes(viewId) && viewIdsRef.current.length > 0) { setSelectedViewId(viewIdsRef.current[0]); + console.debug('[DatabaseBlock] selected first child view', { viewId, selected: viewIdsRef.current[0] }); } else { setSelectedViewId(viewId); + console.debug('[DatabaseBlock] selected requested view', { viewId }); + } + + if (meta) { + console.debug('[DatabaseBlock] loaded view meta', { + viewId, + children: meta.children?.map((c) => c.view_id), + name: meta.name, + }); } setNotFound(false); - }).catch(() => { + }).catch((error) => { + console.error('[DatabaseBlock] failed to load view meta', { viewId, error }); setNotFound(true); }); }, [loadViewMetaWithCallback, viewId]); diff --git a/src/components/editor/components/panels/slash-panel/SlashPanel.tsx b/src/components/editor/components/panels/slash-panel/SlashPanel.tsx index 4449b0a6..b6118696 100644 --- a/src/components/editor/components/panels/slash-panel/SlashPanel.tsx +++ b/src/components/editor/components/panels/slash-panel/SlashPanel.tsx @@ -439,13 +439,45 @@ export function SlashPanel({ try { const baseViewId = (await getViewIdFromDatabaseId?.(option.databaseId)) || option.view.view_id; + const baseName = + option.view.name || + t('document.view.placeholder', { defaultValue: 'Untitled' }); + + const prefix = (() => { + switch (linkedPicker.layout) { + case ViewLayout.Grid: + return t('document.grid.referencedGridPrefix', { + defaultValue: 'View of', + }); + case ViewLayout.Board: + return t('document.board.referencedBoardPrefix', { + defaultValue: 'View of', + }); + case ViewLayout.Calendar: + return t('document.calendar.referencedCalendarPrefix', { + defaultValue: 'View of', + }); + default: + return ''; + } + })(); + const referencedName = prefix ? `${prefix} ${baseName}` : baseName; + const newViewId = await createFolderView({ - parentViewId: viewId, + parentViewId: baseViewId, layout: linkedPicker.layout, - name: option.view.name, + name: referencedName, databaseId: option.databaseId, }); + console.debug('[SlashPanel] created linked database', { + optionKey: linkedPicker.layout, + baseViewId, + databaseId: option.databaseId, + newViewId, + referencedName, + }); + turnInto(blockType, { view_id: newViewId, parent_id: baseViewId,