mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2025-11-30 03:18:02 +08:00
chore: refactor database view tags
This commit is contained in:
166
src/components/database/components/tabs/DatabaseTabItem.tsx
Normal file
166
src/components/database/components/tabs/DatabaseTabItem.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import {
|
||||||
|
DatabaseViewLayout,
|
||||||
|
View,
|
||||||
|
ViewLayout,
|
||||||
|
YDatabaseView,
|
||||||
|
YjsDatabaseKey,
|
||||||
|
} from '@/application/types';
|
||||||
|
import PageIcon from '@/components/_shared/view-icon/PageIcon';
|
||||||
|
import { DatabaseViewActions } from '@/components/database/components/tabs/ViewActions';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { TabLabel, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export interface DatabaseTabItemProps {
|
||||||
|
viewId: string;
|
||||||
|
view: YDatabaseView;
|
||||||
|
meta: View | null;
|
||||||
|
iidIndex: string;
|
||||||
|
menuViewId: string | null;
|
||||||
|
readOnly: boolean;
|
||||||
|
visibleViewIds: string[];
|
||||||
|
onSetMenuViewId: (id: string | null) => void;
|
||||||
|
onOpenDeleteModal: (id: string) => void;
|
||||||
|
onOpenRenameModal: (id: string) => void;
|
||||||
|
onReloadView: () => void;
|
||||||
|
setTabRef: (id: string, el: HTMLElement | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DatabaseTabItem = memo(
|
||||||
|
({
|
||||||
|
viewId,
|
||||||
|
view,
|
||||||
|
meta,
|
||||||
|
iidIndex,
|
||||||
|
menuViewId,
|
||||||
|
readOnly,
|
||||||
|
visibleViewIds,
|
||||||
|
onSetMenuViewId,
|
||||||
|
onOpenDeleteModal,
|
||||||
|
onOpenRenameModal,
|
||||||
|
onReloadView,
|
||||||
|
setTabRef,
|
||||||
|
}: DatabaseTabItemProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const databaseLayout = Number(view.get(YjsDatabaseKey.layout)) as DatabaseViewLayout;
|
||||||
|
const folderView =
|
||||||
|
viewId === iidIndex ? meta : meta?.children?.find((v) => v.view_id === viewId);
|
||||||
|
|
||||||
|
const name = folderView?.name || view.get(YjsDatabaseKey.name) || t('untitled');
|
||||||
|
|
||||||
|
const menuView = useMemo(() => {
|
||||||
|
if (menuViewId === iidIndex) return meta;
|
||||||
|
return meta?.children.find((v) => v.view_id === menuViewId);
|
||||||
|
}, [iidIndex, menuViewId, meta]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabsTrigger
|
||||||
|
key={viewId}
|
||||||
|
value={viewId}
|
||||||
|
id={`view-tab-${viewId}`}
|
||||||
|
data-testid={`view-tab-${viewId}`}
|
||||||
|
className={'min-w-[80px] max-w-[200px]'}
|
||||||
|
ref={(el) => {
|
||||||
|
setTabRef(viewId, el);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabLabel
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
// For left-click, let Radix UI tabs handle it via onValueChange
|
||||||
|
if (e.button === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For right-click and other buttons, prevent default and handle menu
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (readOnly) return;
|
||||||
|
|
||||||
|
if (viewId !== menuViewId) {
|
||||||
|
onSetMenuViewId(viewId);
|
||||||
|
} else {
|
||||||
|
onSetMenuViewId(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={'flex items-center gap-1.5 overflow-hidden'}
|
||||||
|
>
|
||||||
|
<PageIcon
|
||||||
|
iconSize={16}
|
||||||
|
view={
|
||||||
|
folderView || {
|
||||||
|
layout:
|
||||||
|
databaseLayout === DatabaseViewLayout.Board
|
||||||
|
? ViewLayout.Board
|
||||||
|
: databaseLayout === DatabaseViewLayout.Calendar
|
||||||
|
? ViewLayout.Calendar
|
||||||
|
: ViewLayout.Grid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
className={'!h-5 !w-5 text-base leading-[1.3rem]'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tooltip delayDuration={500}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
className={'flex-1 truncate'}
|
||||||
|
>
|
||||||
|
{name || t('grid.title.placeholder')}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent sideOffset={10} side={'right'}>
|
||||||
|
{name}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TabLabel>
|
||||||
|
<DropdownMenu
|
||||||
|
modal
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
onSetMenuViewId(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
open={menuViewId === viewId}
|
||||||
|
>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<div className={'pointer-events-none absolute bottom-0 left-0 opacity-0'} />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
side={'bottom'}
|
||||||
|
align={'start'}
|
||||||
|
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{menuView && (
|
||||||
|
<DatabaseViewActions
|
||||||
|
onClose={() => {
|
||||||
|
onSetMenuViewId(null);
|
||||||
|
}}
|
||||||
|
onOpenDeleteModal={(viewId: string) => {
|
||||||
|
onOpenDeleteModal(viewId);
|
||||||
|
}}
|
||||||
|
onOpenRenameModal={(viewId: string) => {
|
||||||
|
onOpenRenameModal(viewId);
|
||||||
|
}}
|
||||||
|
deleteDisabled={viewId === iidIndex && visibleViewIds.length > 1}
|
||||||
|
view={menuView}
|
||||||
|
onUpdatedIcon={onReloadView}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TabsTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
DatabaseTabItem.displayName = 'DatabaseTabItem';
|
||||||
|
|
||||||
@@ -12,7 +12,6 @@ import { ReactComponent as PlusIcon } from '@/assets/icons/plus.svg';
|
|||||||
import { findView } from '@/components/_shared/outline/utils';
|
import { findView } from '@/components/_shared/outline/utils';
|
||||||
import { AFScroller } from '@/components/_shared/scroller';
|
import { AFScroller } from '@/components/_shared/scroller';
|
||||||
import { ViewIcon } from '@/components/_shared/view-icon';
|
import { ViewIcon } from '@/components/_shared/view-icon';
|
||||||
import PageIcon from '@/components/_shared/view-icon/PageIcon';
|
|
||||||
import {
|
import {
|
||||||
SCROLL_DELAY,
|
SCROLL_DELAY,
|
||||||
SCROLL_FALLBACK_DELAY,
|
SCROLL_FALLBACK_DELAY,
|
||||||
@@ -20,13 +19,13 @@ import {
|
|||||||
import { useDatabaseViewSync } from '@/components/app/hooks/useViewSync';
|
import { useDatabaseViewSync } from '@/components/app/hooks/useViewSync';
|
||||||
import RenameModal from '@/components/app/view-actions/RenameModal';
|
import RenameModal from '@/components/app/view-actions/RenameModal';
|
||||||
import { DatabaseActions } from '@/components/database/components/conditions';
|
import { DatabaseActions } from '@/components/database/components/conditions';
|
||||||
|
import { DatabaseTabItem } from '@/components/database/components/tabs/DatabaseTabItem';
|
||||||
import DeleteViewConfirm from '@/components/database/components/tabs/DeleteViewConfirm';
|
import DeleteViewConfirm from '@/components/database/components/tabs/DeleteViewConfirm';
|
||||||
import { DatabaseViewActions } from '@/components/database/components/tabs/ViewActions';
|
import { useTabScroller } from '@/components/database/components/tabs/useTabScroller';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { TabLabel, Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsList } from '@/components/ui/tabs';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
||||||
|
|
||||||
export interface DatabaseTabBarProps {
|
export interface DatabaseTabBarProps {
|
||||||
viewIds: string[];
|
viewIds: string[];
|
||||||
@@ -46,7 +45,7 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
const { loadViewMeta, readOnly, showActions = true, eventEmitter } = context;
|
const { loadViewMeta, readOnly, showActions = true, eventEmitter } = context;
|
||||||
const updatePage = useUpdateDatabaseView();
|
const updatePage = useUpdateDatabaseView();
|
||||||
const [meta, setMeta] = useState<View | null>(null);
|
const [meta, setMeta] = useState<View | null>(null);
|
||||||
const scrollLeft = context.paddingStart;
|
const scrollLeftPadding = context.paddingStart;
|
||||||
const [addLoading, setAddLoading] = useState(false);
|
const [addLoading, setAddLoading] = useState(false);
|
||||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState<string | null>();
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState<string | null>();
|
||||||
const [renameViewId, setRenameViewId] = useState<string | null>();
|
const [renameViewId, setRenameViewId] = useState<string | null>();
|
||||||
@@ -54,15 +53,20 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
|
|
||||||
const [tabsWidth, setTabsWidth] = useState<number | null>(null);
|
const [tabsWidth, setTabsWidth] = useState<number | null>(null);
|
||||||
const [tabsContainer, setTabsContainer] = useState<HTMLDivElement | null>(null);
|
const [tabsContainer, setTabsContainer] = useState<HTMLDivElement | null>(null);
|
||||||
const [showScrollRightButton, setShowScrollRightButton] = useState(false);
|
|
||||||
const [showScrollLeftButton, setShowScrollLeftButton] = useState(false);
|
|
||||||
const [scrollerContainer, setScrollerContainer] = useState<HTMLDivElement | null>(null);
|
|
||||||
const tabRefs = useRef<Map<string, HTMLElement>>(new Map());
|
const tabRefs = useRef<Map<string, HTMLElement>>(new Map());
|
||||||
|
|
||||||
const { waitForViewData } = useDatabaseViewSync(views as any);
|
const {
|
||||||
|
setScrollerContainer,
|
||||||
|
showScrollLeftButton,
|
||||||
|
showScrollRightButton,
|
||||||
|
scrollLeft,
|
||||||
|
scrollRight,
|
||||||
|
handleObserverScroller,
|
||||||
|
} = useTabScroller();
|
||||||
|
|
||||||
const scrollToView = useCallback(
|
const { waitForViewData } = useDatabaseViewSync(views);
|
||||||
(viewId: string) => {
|
|
||||||
|
const scrollToView = useCallback((viewId: string) => {
|
||||||
const element = tabRefs.current.get(viewId);
|
const element = tabRefs.current.get(viewId);
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
@@ -74,9 +78,7 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
}, []);
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const navigateToView = useCallback(
|
const navigateToView = useCallback(
|
||||||
async (viewId: string) => {
|
async (viewId: string) => {
|
||||||
@@ -97,18 +99,6 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
[setSelectedViewId, scrollToView]
|
[setSelectedViewId, scrollToView]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleObserverScroller = useCallback(() => {
|
|
||||||
if (scrollerContainer) {
|
|
||||||
const scrollWidth = scrollerContainer.scrollWidth;
|
|
||||||
const clientWidth = scrollerContainer.clientWidth;
|
|
||||||
|
|
||||||
setShowScrollRightButton(
|
|
||||||
scrollWidth > clientWidth && scrollerContainer.scrollLeft + 1 < scrollWidth - clientWidth
|
|
||||||
);
|
|
||||||
setShowScrollLeftButton(scrollerContainer.scrollLeft > 5);
|
|
||||||
}
|
|
||||||
}, [scrollerContainer]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onResize = () => {
|
const onResize = () => {
|
||||||
if (tabsContainer) {
|
if (tabsContainer) {
|
||||||
@@ -134,30 +124,13 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
};
|
};
|
||||||
}, [tabsContainer]);
|
}, [tabsContainer]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!scrollerContainer) return;
|
|
||||||
const onResize = () => {
|
|
||||||
handleObserverScroller();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initial call to set the width
|
|
||||||
onResize();
|
|
||||||
|
|
||||||
const observer = new ResizeObserver(onResize);
|
|
||||||
|
|
||||||
observer.observe(scrollerContainer);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
observer.disconnect();
|
|
||||||
};
|
|
||||||
}, [handleObserverScroller, scrollerContainer]);
|
|
||||||
|
|
||||||
const reloadView = useCallback(async () => {
|
const reloadView = useCallback(async () => {
|
||||||
if (loadViewMeta) {
|
if (loadViewMeta) {
|
||||||
try {
|
try {
|
||||||
const meta = await loadViewMeta(iidIndex);
|
const meta = await loadViewMeta(iidIndex);
|
||||||
|
|
||||||
setMeta(meta);
|
setMeta(meta);
|
||||||
|
return meta;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
@@ -189,11 +162,6 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
return meta?.children.find((v) => v.view_id === renameViewId);
|
return meta?.children.find((v) => v.view_id === renameViewId);
|
||||||
}, [iidIndex, meta, renameViewId]);
|
}, [iidIndex, meta, renameViewId]);
|
||||||
|
|
||||||
const menuView = useMemo(() => {
|
|
||||||
if (menuViewId === iidIndex) return meta;
|
|
||||||
return meta?.children.find((v) => v.view_id === menuViewId);
|
|
||||||
}, [iidIndex, menuViewId, meta]);
|
|
||||||
|
|
||||||
const visibleViewIds = useMemo(() => {
|
const visibleViewIds = useMemo(() => {
|
||||||
return viewIds.filter((viewId) => {
|
return viewIds.filter((viewId) => {
|
||||||
const databaseView = views?.get(viewId) as YDatabaseView | null;
|
const databaseView = views?.get(viewId) as YDatabaseView | null;
|
||||||
@@ -212,7 +180,14 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
const synced = await waitForViewData(viewId);
|
const synced = await waitForViewData(viewId);
|
||||||
|
|
||||||
// Reload view metadata to ensure folder structure is updated
|
// Reload view metadata to ensure folder structure is updated
|
||||||
|
const meta = await reloadView();
|
||||||
|
|
||||||
|
// Sometimes the view metadata is not immediately available after creation due to race conditions on the backend/sync
|
||||||
|
// If the new view is not found in the children list, wait a bit and try reloading again
|
||||||
|
if (meta && !meta.children.some((v) => v.view_id === viewId)) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
await reloadView();
|
await reloadView();
|
||||||
|
}
|
||||||
|
|
||||||
if (synced) {
|
if (synced) {
|
||||||
await navigateToView(viewId);
|
await navigateToView(viewId);
|
||||||
@@ -258,17 +233,28 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
document.removeEventListener('contextmenu', preventDefault);
|
document.removeEventListener('contextmenu', preventDefault);
|
||||||
};
|
};
|
||||||
}, [menuViewId]);
|
}, [menuViewId]);
|
||||||
|
|
||||||
|
const setTabRef = useCallback((viewId: string, el: HTMLElement | null) => {
|
||||||
|
if (el) {
|
||||||
|
tabRefs.current.set(viewId, el);
|
||||||
|
} else {
|
||||||
|
tabRefs.current.delete(viewId);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (viewIds.length === 0) return null;
|
if (viewIds.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={className}
|
className={className}
|
||||||
style={{
|
style={{
|
||||||
paddingLeft: scrollLeft === undefined ? 96 : scrollLeft,
|
paddingLeft: scrollLeftPadding === undefined ? 96 : scrollLeftPadding,
|
||||||
paddingRight: scrollLeft === undefined ? 96 : scrollLeft,
|
paddingRight: scrollLeftPadding === undefined ? 96 : scrollLeftPadding,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={`database-tabs flex w-full items-center gap-1.5 overflow-hidden border-b border-border-primary`}>
|
<div
|
||||||
|
className={`database-tabs flex w-full items-center gap-1.5 overflow-hidden border-b border-border-primary`}
|
||||||
|
>
|
||||||
<div className='relative flex h-[34px] flex-1 items-end justify-start overflow-hidden'>
|
<div className='relative flex h-[34px] flex-1 items-end justify-start overflow-hidden'>
|
||||||
{showScrollLeftButton && (
|
{showScrollLeftButton && (
|
||||||
<Button
|
<Button
|
||||||
@@ -280,15 +266,7 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
'absolute left-0 top-0 z-10 bg-surface-primary text-icon-secondary hover:bg-surface-primary-hover '
|
'absolute left-0 top-0 z-10 bg-surface-primary text-icon-secondary hover:bg-surface-primary-hover '
|
||||||
}
|
}
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
tabIndex={-1}
|
onClick={scrollLeft}
|
||||||
onClick={() => {
|
|
||||||
if (scrollerContainer) {
|
|
||||||
scrollerContainer.scrollTo({
|
|
||||||
left: scrollerContainer.scrollLeft - 200,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ChevronLeft className={'h-5 w-5'} />
|
<ChevronLeft className={'h-5 w-5'} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -304,15 +282,7 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
'absolute right-9 top-0 z-10 bg-surface-primary text-icon-secondary hover:bg-surface-primary-hover'
|
'absolute right-9 top-0 z-10 bg-surface-primary text-icon-secondary hover:bg-surface-primary-hover'
|
||||||
}
|
}
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
tabIndex={-1}
|
onClick={scrollRight}
|
||||||
onClick={() => {
|
|
||||||
if (scrollerContainer) {
|
|
||||||
scrollerContainer.scrollTo({
|
|
||||||
left: scrollerContainer.scrollLeft + 200,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ChevronRight className={'h-5 w-5'} />
|
<ChevronRight className={'h-5 w-5'} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -325,13 +295,8 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
}}
|
}}
|
||||||
className={'relative flex h-full flex-1'}
|
className={'relative flex h-full flex-1'}
|
||||||
overflowYHidden
|
overflowYHidden
|
||||||
ref={(el: HTMLDivElement | null) => {
|
ref={setScrollerContainer}
|
||||||
setScrollerContainer(el);
|
onScroll={handleObserverScroller}
|
||||||
handleObserverScroller();
|
|
||||||
}}
|
|
||||||
onScroll={() => {
|
|
||||||
handleObserverScroller();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={setTabsContainer}
|
ref={setTabsContainer}
|
||||||
@@ -354,114 +319,23 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
const view = views?.get(viewId) as YDatabaseView | null;
|
const view = views?.get(viewId) as YDatabaseView | null;
|
||||||
|
|
||||||
if (!view) return null;
|
if (!view) return null;
|
||||||
const databaseLayout = Number(view.get(YjsDatabaseKey.layout)) as DatabaseViewLayout;
|
|
||||||
const folderView = viewId === iidIndex ? meta : meta?.children?.find((v) => v.view_id === viewId);
|
|
||||||
|
|
||||||
const name = folderView?.name || view.get(YjsDatabaseKey.name) || t('untitled');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsTrigger
|
<DatabaseTabItem
|
||||||
key={viewId}
|
key={viewId}
|
||||||
value={viewId}
|
viewId={viewId}
|
||||||
id={`view-tab-${viewId}`}
|
view={view}
|
||||||
data-testid={`view-tab-${viewId}`}
|
meta={meta}
|
||||||
className={'min-w-[80px] max-w-[200px]'}
|
iidIndex={iidIndex}
|
||||||
ref={(el) => {
|
menuViewId={menuViewId}
|
||||||
if (el) {
|
readOnly={!!readOnly}
|
||||||
tabRefs.current.set(viewId, el);
|
visibleViewIds={visibleViewIds}
|
||||||
} else {
|
onSetMenuViewId={setMenuViewId}
|
||||||
tabRefs.current.delete(viewId);
|
onOpenDeleteModal={setDeleteConfirmOpen}
|
||||||
}
|
onOpenRenameModal={setRenameViewId}
|
||||||
}}
|
onReloadView={reloadView}
|
||||||
>
|
setTabRef={setTabRef}
|
||||||
<TabLabel
|
|
||||||
onPointerDown={(e) => {
|
|
||||||
// For left-click, let Radix UI tabs handle it via onValueChange
|
|
||||||
if (e.button === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For right-click and other buttons, prevent default and handle menu
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (readOnly) return;
|
|
||||||
|
|
||||||
if (viewId !== menuViewId) {
|
|
||||||
setMenuViewId(viewId);
|
|
||||||
} else {
|
|
||||||
setMenuViewId(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={'flex items-center gap-1.5 overflow-hidden'}
|
|
||||||
>
|
|
||||||
<PageIcon
|
|
||||||
iconSize={16}
|
|
||||||
view={
|
|
||||||
folderView || {
|
|
||||||
layout:
|
|
||||||
databaseLayout === DatabaseViewLayout.Board
|
|
||||||
? ViewLayout.Board
|
|
||||||
: databaseLayout === DatabaseViewLayout.Calendar
|
|
||||||
? ViewLayout.Calendar
|
|
||||||
: ViewLayout.Grid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
className={'!h-5 !w-5 text-base leading-[1.3rem]'}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tooltip delayDuration={500}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<span
|
|
||||||
onContextMenu={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
className={'flex-1 truncate'}
|
|
||||||
>
|
|
||||||
{name || t('grid.title.placeholder')}
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent sideOffset={10} side={'right'}>
|
|
||||||
{name}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TabLabel>
|
|
||||||
<DropdownMenu
|
|
||||||
modal
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setMenuViewId(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
open={menuViewId === viewId}
|
|
||||||
>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<div className={'pointer-events-none absolute bottom-0 left-0 opacity-0'} />
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
side={'bottom'}
|
|
||||||
align={'start'}
|
|
||||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
{menuView && (
|
|
||||||
<DatabaseViewActions
|
|
||||||
onClose={() => {
|
|
||||||
setMenuViewId(null);
|
|
||||||
}}
|
|
||||||
onOpenDeleteModal={(viewId: string) => {
|
|
||||||
setDeleteConfirmOpen(viewId);
|
|
||||||
}}
|
|
||||||
onOpenRenameModal={(viewId: string) => {
|
|
||||||
setRenameViewId(viewId);
|
|
||||||
}}
|
|
||||||
deleteDisabled={viewId === iidIndex && visibleViewIds.length > 1}
|
|
||||||
view={menuView}
|
|
||||||
onUpdatedIcon={reloadView}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</TabsTrigger>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -473,13 +347,17 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
data-testid="add-view-button"
|
data-testid='add-view-button'
|
||||||
size={'icon'}
|
size={'icon'}
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
loading={addLoading}
|
loading={addLoading}
|
||||||
className={'mx-1.5 mb-1.5 text-icon-secondary'}
|
className={'mx-1.5 mb-1.5 text-icon-secondary'}
|
||||||
>
|
>
|
||||||
{addLoading ? <Progress variant={'inherit'} /> : <PlusIcon className={'h-5 w-5'} />}
|
{addLoading ? (
|
||||||
|
<Progress variant={'inherit'} />
|
||||||
|
) : (
|
||||||
|
<PlusIcon className={'h-5 w-5'} />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
@@ -559,3 +437,5 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DatabaseTabs.displayName = 'DatabaseTabs';
|
||||||
|
|||||||
65
src/components/database/components/tabs/useTabScroller.ts
Normal file
65
src/components/database/components/tabs/useTabScroller.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const useTabScroller = () => {
|
||||||
|
const [scrollerContainer, setScrollerContainer] = useState<HTMLDivElement | null>(null);
|
||||||
|
const [showScrollRightButton, setShowScrollRightButton] = useState(false);
|
||||||
|
const [showScrollLeftButton, setShowScrollLeftButton] = useState(false);
|
||||||
|
|
||||||
|
const handleObserverScroller = useCallback(() => {
|
||||||
|
if (scrollerContainer) {
|
||||||
|
const scrollWidth = scrollerContainer.scrollWidth;
|
||||||
|
const clientWidth = scrollerContainer.clientWidth;
|
||||||
|
|
||||||
|
setShowScrollRightButton(
|
||||||
|
scrollWidth > clientWidth && scrollerContainer.scrollLeft + 1 < scrollWidth - clientWidth
|
||||||
|
);
|
||||||
|
setShowScrollLeftButton(scrollerContainer.scrollLeft > 5);
|
||||||
|
}
|
||||||
|
}, [scrollerContainer]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!scrollerContainer) return;
|
||||||
|
const onResize = () => {
|
||||||
|
handleObserverScroller();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial call
|
||||||
|
onResize();
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(onResize);
|
||||||
|
|
||||||
|
observer.observe(scrollerContainer);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [handleObserverScroller, scrollerContainer]);
|
||||||
|
|
||||||
|
const scrollLeft = useCallback(() => {
|
||||||
|
if (scrollerContainer) {
|
||||||
|
scrollerContainer.scrollTo({
|
||||||
|
left: scrollerContainer.scrollLeft - 200,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [scrollerContainer]);
|
||||||
|
|
||||||
|
const scrollRight = useCallback(() => {
|
||||||
|
if (scrollerContainer) {
|
||||||
|
scrollerContainer.scrollTo({
|
||||||
|
left: scrollerContainer.scrollLeft + 200,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [scrollerContainer]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
setScrollerContainer,
|
||||||
|
showScrollLeftButton,
|
||||||
|
showScrollRightButton,
|
||||||
|
scrollLeft,
|
||||||
|
scrollRight,
|
||||||
|
handleObserverScroller,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
Reference in New Issue
Block a user