mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2026-03-13 10:00:26 +08:00
282 lines
8.4 KiB
TypeScript
282 lines
8.4 KiB
TypeScript
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import { ErrorBoundary } from 'react-error-boundary';
|
|
|
|
import { useDatabaseViewsSelector } from '@/application/database-yjs';
|
|
import { DatabaseViewLayout, YjsDatabaseKey } from '@/application/types';
|
|
import { Board } from '@/components/database/board';
|
|
import { DatabaseConditionsContext } from '@/components/database/components/conditions/context';
|
|
import { DatabaseTabs } from '@/components/database/components/tabs';
|
|
import { Calendar } from '@/components/database/fullcalendar';
|
|
import { Grid } from '@/components/database/grid';
|
|
import { ElementFallbackRender } from '@/components/error/ElementFallbackRender';
|
|
import { Progress } from '@/components/ui/progress';
|
|
|
|
import DatabaseConditions from 'src/components/database/components/conditions/DatabaseConditions';
|
|
|
|
const logDebug = (...args: Parameters<typeof console.debug>) => {
|
|
if (import.meta.env.DEV) {
|
|
console.debug(...args);
|
|
}
|
|
};
|
|
|
|
const getScrollElement = () =>
|
|
document.querySelector('.appflowy-scroll-container') as HTMLElement | null;
|
|
|
|
function DatabaseViews({
|
|
onChangeView,
|
|
viewId,
|
|
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 [viewVisible, setViewVisible] = useState(true);
|
|
const viewContainerRef = useRef<HTMLDivElement | null>(null);
|
|
const [lockedHeight, setLockedHeight] = useState<number | null>(fixedHeight ?? null);
|
|
const lastScrollRef = useRef<number | null>(null);
|
|
const value = useMemo(() => {
|
|
return Math.max(
|
|
0,
|
|
viewIds.findIndex((id) => id === viewId)
|
|
);
|
|
}, [viewId, viewIds]);
|
|
|
|
const [conditionsExpanded, setConditionsExpanded] = useState<boolean>(false);
|
|
const toggleExpanded = useCallback(() => {
|
|
setConditionsExpanded((prev) => !prev);
|
|
}, []);
|
|
const [openFilterId, setOpenFilterId] = useState<string>();
|
|
|
|
const activeView = useMemo(() => {
|
|
return childViews[value];
|
|
}, [childViews, value]);
|
|
|
|
useEffect(() => {
|
|
if (!activeView) return;
|
|
|
|
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();
|
|
|
|
activeView.observe(observerEvent);
|
|
|
|
return () => {
|
|
activeView.unobserve(observerEvent);
|
|
};
|
|
}, [activeView]);
|
|
|
|
const handleViewChange = useCallback(
|
|
(newViewId: string) => {
|
|
const scrollElement = getScrollElement();
|
|
lastScrollRef.current = scrollElement?.scrollTop ?? null;
|
|
logDebug('[DatabaseViews] captured scroll before view change', {
|
|
scrollTop: lastScrollRef.current,
|
|
});
|
|
|
|
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);
|
|
},
|
|
[fixedHeight, onChangeView]
|
|
);
|
|
|
|
|
|
const view = useMemo(() => {
|
|
if (isLoading) return null;
|
|
switch (layout) {
|
|
case DatabaseViewLayout.Grid:
|
|
return <Grid />;
|
|
case DatabaseViewLayout.Board:
|
|
return <Board />;
|
|
case DatabaseViewLayout.Calendar:
|
|
return <Calendar />;
|
|
}
|
|
}, [layout, isLoading]);
|
|
|
|
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
|
|
// Board's autoScrollForElements interferes with scroll, so we enforce for multiple frames
|
|
useEffect(() => {
|
|
if (isLoading) return;
|
|
if (lastScrollRef.current == null) return;
|
|
|
|
const scrollElement = getScrollElement();
|
|
if (!scrollElement) {
|
|
lastScrollRef.current = null;
|
|
return;
|
|
}
|
|
|
|
const targetScroll = lastScrollRef.current;
|
|
let rafCount = 0;
|
|
let rafId: number;
|
|
|
|
// Temporarily prevent scroll events during restoration
|
|
const preventScroll = (e: Event) => {
|
|
if (scrollElement.scrollTop !== targetScroll) {
|
|
e.preventDefault();
|
|
scrollElement.scrollTop = targetScroll;
|
|
}
|
|
};
|
|
|
|
scrollElement.addEventListener('scroll', preventScroll, { passive: false });
|
|
|
|
// Use RAF loop to enforce scroll position
|
|
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,
|
|
});
|
|
}
|
|
|
|
rafCount++;
|
|
// Run for 5 frames (~80ms) to catch delayed scroll changes from Board mount
|
|
if (rafCount < 5) {
|
|
rafId = requestAnimationFrame(enforceScroll);
|
|
} else {
|
|
logDebug('[DatabaseViews] scroll restoration completed', {
|
|
final: scrollElement.scrollTop,
|
|
target: targetScroll,
|
|
});
|
|
// Remove scroll listener and clean up
|
|
scrollElement.removeEventListener('scroll', preventScroll);
|
|
lastScrollRef.current = null;
|
|
setViewVisible(true);
|
|
// Release height lock to allow view to resize to its natural height
|
|
if (!fixedHeight) {
|
|
setLockedHeight(null);
|
|
}
|
|
}
|
|
};
|
|
|
|
rafId = requestAnimationFrame(enforceScroll);
|
|
|
|
return () => {
|
|
if (rafId) {
|
|
cancelAnimationFrame(rafId);
|
|
}
|
|
scrollElement.removeEventListener('scroll', preventScroll);
|
|
};
|
|
}, [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 (
|
|
<>
|
|
<DatabaseConditionsContext.Provider
|
|
value={{
|
|
expanded: conditionsExpanded,
|
|
toggleExpanded,
|
|
openFilterId,
|
|
setOpenFilterId,
|
|
}}
|
|
>
|
|
<DatabaseTabs
|
|
viewName={viewName}
|
|
iidIndex={iidIndex}
|
|
selectedViewId={viewId}
|
|
setSelectedViewId={handleViewChange}
|
|
viewIds={viewIds}
|
|
/>
|
|
<DatabaseConditions />
|
|
|
|
<div
|
|
ref={viewContainerRef}
|
|
className={'relative flex h-full w-full flex-1 flex-col overflow-hidden'}
|
|
style={
|
|
effectiveHeight !== null
|
|
? { height: `${effectiveHeight}px`, maxHeight: `${effectiveHeight}px` }
|
|
: undefined
|
|
}
|
|
>
|
|
<div
|
|
className='h-full w-full'
|
|
style={
|
|
effectiveHeight !== null
|
|
? { height: `${effectiveHeight}px`, maxHeight: `${effectiveHeight}px` }
|
|
: {}
|
|
}
|
|
>
|
|
<Suspense fallback={null}>
|
|
<ErrorBoundary fallbackRender={ElementFallbackRender}>{view}</ErrorBoundary>
|
|
</Suspense>
|
|
</div>
|
|
{isLoading && (
|
|
<div className='absolute inset-0 z-50 flex items-center justify-center bg-background-primary/70 backdrop-blur-sm'>
|
|
<Progress />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</DatabaseConditionsContext.Provider>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default DatabaseViews;
|