mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2026-03-13 10:00:26 +08:00
fix: scroll to top when swithc embeded database view
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface Database2Props {
|
||||
showActions?: boolean;
|
||||
createFolderView?: (payload: CreateFolderViewPayload) => Promise<string>;
|
||||
getViewIdFromDatabaseId?: (databaseId: string) => Promise<string | null>;
|
||||
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 ? (
|
||||
<DatabaseRow appendBreadcrumb={appendBreadcrumb} rowId={rowId} />
|
||||
) : (
|
||||
<div className='appflowy-database relative flex w-full flex-1 select-text flex-col overflow-y-hidden'>
|
||||
<div className='appflowy-database relative flex w-full flex-1 select-text flex-col overflow-hidden'>
|
||||
<DatabaseViews
|
||||
visibleViewIds={visibleViewIds}
|
||||
iidIndex={iidIndex}
|
||||
viewName={iidName}
|
||||
onChangeView={onChangeView}
|
||||
viewId={viewId}
|
||||
fixedHeight={embeddedHeight}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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<DatabaseViewLayout | null>(null);
|
||||
const pendingScrollTopRef = useRef<number | null>(null);
|
||||
const restoreRafRef = useRef<number>();
|
||||
const [viewVisible, setViewVisible] = useState(true);
|
||||
const viewContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [lockedHeight, setLockedHeight] = useState<number | null>(null);
|
||||
const [lockedHeight, setLockedHeight] = useState<number | null>(fixedHeight ?? null);
|
||||
const lastScrollRef = useRef<number | null>(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({
|
||||
<div
|
||||
ref={viewContainerRef}
|
||||
className={'relative flex h-full w-full flex-1 flex-col overflow-hidden'}
|
||||
style={lockedHeight !== null ? { height: `${lockedHeight}px` } : undefined}
|
||||
style={
|
||||
effectiveHeight !== null
|
||||
? { height: `${effectiveHeight}px` }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div
|
||||
className='h-full w-full transition-opacity duration-75'
|
||||
style={{ opacity: viewVisible ? 1 : 0, pointerEvents: viewVisible ? undefined : 'none' }}
|
||||
className='h-full w-full transition-opacity duration-100'
|
||||
style={{
|
||||
...(effectiveHeight !== null
|
||||
? { minHeight: `${effectiveHeight}px`, height: `${effectiveHeight}px` }
|
||||
: {}),
|
||||
opacity: viewVisible ? 1 : 0,
|
||||
pointerEvents: viewVisible ? undefined : 'none',
|
||||
}}
|
||||
aria-hidden={!viewVisible}
|
||||
>
|
||||
<Suspense fallback={skeleton}>
|
||||
|
||||
@@ -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 (
|
||||
<div {...attributes} contentEditable={readOnly ? false : undefined} className='relative w-full cursor-pointer'>
|
||||
@@ -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%',
|
||||
}}
|
||||
>
|
||||
<DatabaseContent
|
||||
selectedViewId={selectedViewId}
|
||||
@@ -91,6 +121,7 @@ export const DatabaseBlock = memo(
|
||||
onChangeView={onChangeView}
|
||||
// eslint-disable-next-line
|
||||
context={context as DatabaseContextState}
|
||||
fixedHeight={EMBEDDED_DB_HEIGHT}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -92,4 +95,4 @@ export const DatabaseContent = ({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user