mirror of https://github.com/buster-so/buster.git
Create a better handler for clicking favorites
This commit is contained in:
parent
ba93fa9c39
commit
7afa3cf399
|
@ -28,8 +28,10 @@ import {
|
|||
} from '@/api/buster_rest';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useInviteModalStore } from '@/context/BusterAppLayout';
|
||||
import { useFavoriteSidebarPanel } from './useFavoritesSidebarPanel';
|
||||
import { ShareAssetType } from '@/api/asset_interfaces/share';
|
||||
|
||||
const topItems: ISidebarList = {
|
||||
const topItems = (currentParentRoute: BusterRoutes): ISidebarList => ({
|
||||
id: 'top-items',
|
||||
items: [
|
||||
{
|
||||
|
@ -44,35 +46,52 @@ const topItems: ISidebarList = {
|
|||
route: BusterRoutes.APP_CHAT,
|
||||
id: BusterRoutes.APP_CHAT
|
||||
}
|
||||
]
|
||||
].map((x) => ({
|
||||
...x,
|
||||
active: x.route === currentParentRoute
|
||||
}))
|
||||
});
|
||||
|
||||
const yourStuff = (
|
||||
currentParentRoute: BusterRoutes,
|
||||
favoritedPageType: ShareAssetType | null
|
||||
): ISidebarGroup => {
|
||||
const isActiveCheck = (type: ShareAssetType, route: BusterRoutes) => {
|
||||
if (favoritedPageType === type) return false;
|
||||
if (favoritedPageType === null) return currentParentRoute === route;
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
label: 'Your stuff',
|
||||
id: 'your-stuff',
|
||||
items: [
|
||||
{
|
||||
label: 'Metrics',
|
||||
icon: <ASSET_ICONS.metrics />,
|
||||
route: BusterRoutes.APP_METRIC,
|
||||
id: BusterRoutes.APP_METRIC,
|
||||
active: isActiveCheck(ShareAssetType.METRIC, BusterRoutes.APP_METRIC)
|
||||
},
|
||||
{
|
||||
label: 'Dashboards',
|
||||
icon: <ASSET_ICONS.dashboards />,
|
||||
route: BusterRoutes.APP_DASHBOARDS,
|
||||
id: BusterRoutes.APP_DASHBOARDS,
|
||||
active: isActiveCheck(ShareAssetType.DASHBOARD, BusterRoutes.APP_DASHBOARDS)
|
||||
},
|
||||
{
|
||||
label: 'Collections',
|
||||
icon: <ASSET_ICONS.collections />,
|
||||
route: BusterRoutes.APP_COLLECTIONS,
|
||||
id: BusterRoutes.APP_COLLECTIONS,
|
||||
active: isActiveCheck(ShareAssetType.COLLECTION, BusterRoutes.APP_COLLECTIONS)
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
const yourStuff: ISidebarGroup = {
|
||||
label: 'Your stuff',
|
||||
id: 'your-stuff',
|
||||
items: [
|
||||
{
|
||||
label: 'Metrics',
|
||||
icon: <ASSET_ICONS.metrics />,
|
||||
route: BusterRoutes.APP_METRIC,
|
||||
id: BusterRoutes.APP_METRIC
|
||||
},
|
||||
{
|
||||
label: 'Dashboards',
|
||||
icon: <ASSET_ICONS.dashboards />,
|
||||
route: BusterRoutes.APP_DASHBOARDS,
|
||||
id: BusterRoutes.APP_DASHBOARDS
|
||||
},
|
||||
{
|
||||
label: 'Collections',
|
||||
icon: <ASSET_ICONS.collections />,
|
||||
route: BusterRoutes.APP_COLLECTIONS,
|
||||
id: BusterRoutes.APP_COLLECTIONS
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const adminTools: ISidebarGroup = {
|
||||
const adminTools = (currentParentRoute: BusterRoutes): ISidebarGroup => ({
|
||||
label: 'Admin tools',
|
||||
id: 'admin-tools',
|
||||
items: [
|
||||
|
@ -94,8 +113,11 @@ const adminTools: ISidebarGroup = {
|
|||
route: BusterRoutes.APP_DATASETS,
|
||||
id: BusterRoutes.APP_DATASETS
|
||||
}
|
||||
]
|
||||
};
|
||||
].map((x) => ({
|
||||
...x,
|
||||
active: x.route === currentParentRoute
|
||||
}))
|
||||
});
|
||||
|
||||
const tryGroup = (
|
||||
onClickInvitePeople: () => void,
|
||||
|
@ -126,36 +148,43 @@ const tryGroup = (
|
|||
export const SidebarPrimary = React.memo(() => {
|
||||
const isAdmin = useUserConfigContextSelector((x) => x.isAdmin);
|
||||
const isUserRegistered = useUserConfigContextSelector((x) => x.isUserRegistered);
|
||||
const { data: favorites } = useGetUserFavorites();
|
||||
const currentParentRoute = useAppLayoutContextSelector((x) => x.currentParentRoute);
|
||||
const onToggleInviteModal = useInviteModalStore((s) => s.onToggleInviteModal);
|
||||
const onOpenContactSupportModal = useContactSupportModalStore((s) => s.onOpenContactSupportModal);
|
||||
const { mutateAsync: updateUserFavorites } = useUpdateUserFavorites();
|
||||
const { mutateAsync: deleteUserFavorite } = useDeleteUserFavorite();
|
||||
|
||||
const onFavoritesReorder = useMemoizedFn((itemIds: string[]) => {
|
||||
updateUserFavorites(itemIds);
|
||||
});
|
||||
const { favoritesDropdownItems, favoritedPageType } = useFavoriteSidebarPanel();
|
||||
|
||||
const topItemsItems = useMemo(() => topItems(currentParentRoute), [currentParentRoute]);
|
||||
|
||||
const adminToolsItems = useMemo(() => {
|
||||
if (!isAdmin) return null;
|
||||
return adminTools(currentParentRoute);
|
||||
}, [isAdmin, currentParentRoute]);
|
||||
|
||||
const yourStuffItems = useMemo(
|
||||
() => yourStuff(currentParentRoute, favoritedPageType),
|
||||
[currentParentRoute, favoritedPageType]
|
||||
);
|
||||
|
||||
const sidebarItems: SidebarProps['content'] = useMemo(() => {
|
||||
if (!isUserRegistered) return [];
|
||||
|
||||
const items = [topItems];
|
||||
const items = [topItemsItems];
|
||||
|
||||
if (isAdmin) {
|
||||
items.push(adminTools);
|
||||
if (adminToolsItems) {
|
||||
items.push(adminToolsItems);
|
||||
}
|
||||
|
||||
items.push(yourStuff);
|
||||
items.push(yourStuffItems);
|
||||
|
||||
if (favorites && favorites.length > 0) {
|
||||
items.push(favoritesDropdown(favorites, { deleteUserFavorite, onFavoritesReorder }));
|
||||
if (favoritesDropdownItems) {
|
||||
items.push(favoritesDropdownItems);
|
||||
}
|
||||
|
||||
items.push(tryGroup(onToggleInviteModal, () => onOpenContactSupportModal('feedback'), isAdmin));
|
||||
|
||||
return items;
|
||||
}, [isAdmin, isUserRegistered, favorites, currentParentRoute, onFavoritesReorder]);
|
||||
}, [isUserRegistered, adminToolsItems, yourStuffItems, favoritesDropdownItems]);
|
||||
|
||||
const onCloseSupportModal = useMemoizedFn(() => onOpenContactSupportModal(false));
|
||||
|
||||
|
@ -167,12 +196,7 @@ export const SidebarPrimary = React.memo(() => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Sidebar
|
||||
content={sidebarItems}
|
||||
header={HeaderMemoized}
|
||||
activeItem={currentParentRoute}
|
||||
footer={FooterMemoized}
|
||||
/>
|
||||
<Sidebar content={sidebarItems} header={HeaderMemoized} footer={FooterMemoized} />
|
||||
|
||||
<GlobalModals onCloseSupportModal={onCloseSupportModal} />
|
||||
</>
|
||||
|
@ -233,32 +257,3 @@ const GlobalModals = ({ onCloseSupportModal }: { onCloseSupportModal: () => void
|
|||
);
|
||||
};
|
||||
GlobalModals.displayName = 'GlobalModals';
|
||||
|
||||
const favoritesDropdown = (
|
||||
favorites: BusterUserFavorite[],
|
||||
{
|
||||
onFavoritesReorder,
|
||||
deleteUserFavorite
|
||||
}: {
|
||||
onFavoritesReorder: (itemIds: string[]) => void;
|
||||
deleteUserFavorite: (itemIds: string[]) => void;
|
||||
}
|
||||
): ISidebarGroup => {
|
||||
return {
|
||||
label: 'Favorites',
|
||||
id: 'favorites',
|
||||
isSortable: true,
|
||||
onItemsReorder: onFavoritesReorder,
|
||||
items: favorites.map((favorite) => {
|
||||
const Icon = assetTypeToIcon(favorite.asset_type);
|
||||
const route = assetTypeToRoute(favorite.asset_type, favorite.id);
|
||||
return {
|
||||
label: favorite.name,
|
||||
icon: <Icon />,
|
||||
route,
|
||||
id: favorite.id,
|
||||
onRemove: () => deleteUserFavorite([favorite.id])
|
||||
};
|
||||
})
|
||||
};
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ import { useUserConfigContextSelector } from '@/context/Users';
|
|||
import { useAppLayoutContextSelector } from '@/context/BusterAppLayout';
|
||||
import { SidebarUserFooter } from './SidebarUserFooter/SidebarUserFooter';
|
||||
|
||||
const accountItems: ISidebarGroup = {
|
||||
const accountItems = (currentParentRoute: BusterRoutes): ISidebarGroup => ({
|
||||
label: 'Account',
|
||||
variant: 'icon',
|
||||
id: 'account',
|
||||
|
@ -18,12 +18,13 @@ const accountItems: ISidebarGroup = {
|
|||
{
|
||||
label: 'Profile',
|
||||
route: createBusterRoute({ route: BusterRoutes.SETTINGS_PROFILE }),
|
||||
id: createBusterRoute({ route: BusterRoutes.SETTINGS_PROFILE })
|
||||
id: BusterRoutes.SETTINGS_PROFILE,
|
||||
active: currentParentRoute === BusterRoutes.SETTINGS_PROFILE
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
const workspaceItems: ISidebarGroup = {
|
||||
const workspaceItems = (currentParentRoute: BusterRoutes): ISidebarGroup => ({
|
||||
label: 'Workspace',
|
||||
variant: 'icon',
|
||||
id: 'workspace',
|
||||
|
@ -32,17 +33,20 @@ const workspaceItems: ISidebarGroup = {
|
|||
{
|
||||
label: 'API Keys',
|
||||
route: createBusterRoute({ route: BusterRoutes.SETTINGS_API_KEYS }),
|
||||
id: createBusterRoute({ route: BusterRoutes.SETTINGS_API_KEYS })
|
||||
id: BusterRoutes.SETTINGS_API_KEYS
|
||||
},
|
||||
{
|
||||
label: 'Data Sources',
|
||||
route: createBusterRoute({ route: BusterRoutes.SETTINGS_DATASOURCES }),
|
||||
id: createBusterRoute({ route: BusterRoutes.SETTINGS_DATASOURCES })
|
||||
id: BusterRoutes.SETTINGS_DATASOURCES
|
||||
}
|
||||
]
|
||||
};
|
||||
].map((item) => ({
|
||||
...item,
|
||||
active: currentParentRoute === item.id
|
||||
}))
|
||||
});
|
||||
|
||||
const permissionAndSecurityItems: ISidebarGroup = {
|
||||
const permissionAndSecurityItems = (currentParentRoute: BusterRoutes): ISidebarGroup => ({
|
||||
label: 'Permission & Security',
|
||||
variant: 'icon',
|
||||
id: 'permission-and-security',
|
||||
|
@ -63,21 +67,24 @@ const permissionAndSecurityItems: ISidebarGroup = {
|
|||
route: createBusterRoute({ route: BusterRoutes.SETTINGS_PERMISSION_GROUPS }),
|
||||
id: createBusterRoute({ route: BusterRoutes.SETTINGS_PERMISSION_GROUPS })
|
||||
}
|
||||
]
|
||||
};
|
||||
].map((item) => ({
|
||||
...item,
|
||||
active: currentParentRoute === item.id
|
||||
}))
|
||||
});
|
||||
|
||||
export const SidebarSettings: React.FC<{}> = React.memo(({}) => {
|
||||
const isAdmin = useUserConfigContextSelector((x) => x.isAdmin);
|
||||
const currentParentRoute = useAppLayoutContextSelector((x) => x.currentParentRoute);
|
||||
|
||||
const content = useMemo(() => {
|
||||
const items = [accountItems];
|
||||
const items = [accountItems(currentParentRoute)];
|
||||
if (isAdmin) {
|
||||
items.push(workspaceItems);
|
||||
items.push(permissionAndSecurityItems);
|
||||
items.push(workspaceItems(currentParentRoute));
|
||||
items.push(permissionAndSecurityItems(currentParentRoute));
|
||||
}
|
||||
return items;
|
||||
}, [isAdmin]);
|
||||
}, [isAdmin, currentParentRoute]);
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
|
@ -88,7 +95,6 @@ export const SidebarSettings: React.FC<{}> = React.memo(({}) => {
|
|||
),
|
||||
[]
|
||||
)}
|
||||
activeItem={currentParentRoute}
|
||||
footer={useMemo(
|
||||
() => (
|
||||
<SidebarUserFooter />
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useFavoriteSidebarPanel } from './useFavoritesSidebarPanel';
|
||||
import {
|
||||
useGetUserFavorites,
|
||||
useUpdateUserFavorites,
|
||||
useDeleteUserFavorite
|
||||
} from '@/api/buster_rest/users';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { ShareAssetType } from '@/api/asset_interfaces/share';
|
||||
|
||||
// Mock the hooks
|
||||
jest.mock('@/api/buster_rest/users', () => ({
|
||||
useGetUserFavorites: jest.fn(),
|
||||
useUpdateUserFavorites: jest.fn(),
|
||||
useDeleteUserFavorite: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
useParams: jest.fn()
|
||||
}));
|
||||
|
||||
// Do not mock useMemoizedFn to use the real implementation
|
||||
|
||||
describe('useFavoriteSidebarPanel', () => {
|
||||
const mockFavorites = [
|
||||
{ id: 'metric1', name: 'Metric 1', asset_type: ShareAssetType.METRIC },
|
||||
{ id: 'dashboard1', name: 'Dashboard 1', asset_type: ShareAssetType.DASHBOARD },
|
||||
{ id: 'chat1', name: 'Chat 1', asset_type: ShareAssetType.CHAT },
|
||||
{ id: 'collection1', name: 'Collection 1', asset_type: ShareAssetType.COLLECTION }
|
||||
];
|
||||
|
||||
const mockUpdateFavorites = jest.fn();
|
||||
const mockDeleteFavorite = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
(useGetUserFavorites as jest.Mock).mockReturnValue({
|
||||
data: mockFavorites
|
||||
});
|
||||
|
||||
(useUpdateUserFavorites as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockUpdateFavorites
|
||||
});
|
||||
|
||||
(useDeleteUserFavorite as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockDeleteFavorite
|
||||
});
|
||||
|
||||
(useParams as jest.Mock).mockReturnValue({
|
||||
chatId: undefined,
|
||||
metricId: undefined,
|
||||
dashboardId: undefined,
|
||||
collectionId: undefined
|
||||
});
|
||||
});
|
||||
|
||||
test('should return correct initial structure', () => {
|
||||
const { result } = renderHook(() => useFavoriteSidebarPanel());
|
||||
|
||||
expect(result.current).toHaveProperty('favoritesDropdownItems');
|
||||
expect(result.current).toHaveProperty('favoritedPageType');
|
||||
});
|
||||
|
||||
test('should call updateUserFavorites when onFavoritesReorder is called', () => {
|
||||
const { result } = renderHook(() => useFavoriteSidebarPanel());
|
||||
const itemIds = ['metric1', 'dashboard1'];
|
||||
|
||||
act(() => {
|
||||
const onItemsReorder = result.current.favoritesDropdownItems?.onItemsReorder;
|
||||
if (onItemsReorder) {
|
||||
onItemsReorder(itemIds);
|
||||
}
|
||||
});
|
||||
|
||||
expect(mockUpdateFavorites).toHaveBeenCalledWith(itemIds);
|
||||
});
|
||||
|
||||
test('should correctly identify active chat asset', () => {
|
||||
(useParams as jest.Mock).mockReturnValue({
|
||||
chatId: 'chat1'
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useFavoriteSidebarPanel());
|
||||
|
||||
const chatItem = result.current.favoritesDropdownItems?.items.find(
|
||||
(item) => item.id === 'chat1'
|
||||
);
|
||||
|
||||
expect(chatItem?.active).toBe(true);
|
||||
});
|
||||
|
||||
test('should correctly identify active metric asset', () => {
|
||||
(useParams as jest.Mock).mockReturnValue({
|
||||
metricId: 'metric1'
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useFavoriteSidebarPanel());
|
||||
|
||||
const metricItem = result.current.favoritesDropdownItems?.items.find(
|
||||
(item) => item.id === 'metric1'
|
||||
);
|
||||
|
||||
expect(metricItem?.active).toBe(true);
|
||||
});
|
||||
|
||||
test('should set favoritedPageType to METRIC when metricId is in favorites', () => {
|
||||
(useParams as jest.Mock).mockReturnValue({
|
||||
metricId: 'metric1'
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useFavoriteSidebarPanel());
|
||||
|
||||
expect(result.current.favoritedPageType).toBe(ShareAssetType.METRIC);
|
||||
});
|
||||
|
||||
test('should set favoritedPageType to null when page is not favorited', () => {
|
||||
(useParams as jest.Mock).mockReturnValue({
|
||||
metricId: 'nonexistent'
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useFavoriteSidebarPanel());
|
||||
|
||||
expect(result.current.favoritedPageType).toBe(null);
|
||||
});
|
||||
|
||||
test('should return null for favoritesDropdownItems when no favorites exist', () => {
|
||||
(useGetUserFavorites as jest.Mock).mockReturnValue({
|
||||
data: []
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useFavoriteSidebarPanel());
|
||||
|
||||
expect(result.current.favoritesDropdownItems).toBe(null);
|
||||
});
|
||||
|
||||
test('should call deleteUserFavorite when item removal is triggered', () => {
|
||||
const { result } = renderHook(() => useFavoriteSidebarPanel());
|
||||
|
||||
act(() => {
|
||||
const firstItem = result.current.favoritesDropdownItems?.items[0];
|
||||
if (firstItem?.onRemove) {
|
||||
firstItem.onRemove();
|
||||
}
|
||||
});
|
||||
|
||||
expect(mockDeleteFavorite).toHaveBeenCalledWith(['metric1']);
|
||||
});
|
||||
|
||||
test('should set favoritedPageType to null when chatId and another param exist', () => {
|
||||
(useParams as jest.Mock).mockReturnValue({
|
||||
chatId: 'chat1',
|
||||
metricId: 'metric1'
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useFavoriteSidebarPanel());
|
||||
|
||||
expect(result.current.favoritedPageType).toBe(null);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,102 @@
|
|||
import type { BusterUserFavorite } from '@/api/asset_interfaces/users';
|
||||
import { ISidebarGroup } from '@/components/ui/sidebar';
|
||||
import { assetTypeToIcon, assetTypeToRoute } from '../config/assetIcons';
|
||||
import { useMemoizedFn } from '@/hooks';
|
||||
import {
|
||||
updateUserFavorites,
|
||||
useDeleteUserFavorite,
|
||||
useGetUserFavorites,
|
||||
useUpdateUserFavorites
|
||||
} from '@/api/buster_rest/users';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { ShareAssetType } from '@/api/asset_interfaces/share';
|
||||
|
||||
export const useFavoriteSidebarPanel = () => {
|
||||
const { data: favorites } = useGetUserFavorites();
|
||||
const { mutateAsync: updateUserFavorites } = useUpdateUserFavorites();
|
||||
const { mutateAsync: deleteUserFavorite } = useDeleteUserFavorite();
|
||||
|
||||
const { chatId, metricId, dashboardId, collectionId } = useParams() as {
|
||||
chatId: string | undefined;
|
||||
metricId: string | undefined;
|
||||
dashboardId: string | undefined;
|
||||
collectionId: string | undefined;
|
||||
};
|
||||
|
||||
const onFavoritesReorder = useMemoizedFn((itemIds: string[]) => {
|
||||
updateUserFavorites(itemIds);
|
||||
});
|
||||
|
||||
const isAssetActive = useMemoizedFn((favorite: BusterUserFavorite) => {
|
||||
const assetType = favorite.asset_type;
|
||||
const id = favorite.id;
|
||||
|
||||
switch (assetType) {
|
||||
case ShareAssetType.CHAT:
|
||||
return id === chatId;
|
||||
case ShareAssetType.METRIC:
|
||||
return id === metricId;
|
||||
case ShareAssetType.DASHBOARD:
|
||||
return id === dashboardId;
|
||||
case ShareAssetType.COLLECTION:
|
||||
return id === collectionId;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const favoritedPageType: ShareAssetType | null = useMemo(() => {
|
||||
if (chatId && (metricId || dashboardId || collectionId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (metricId && favorites.some((f) => f.id === metricId)) {
|
||||
return ShareAssetType.METRIC;
|
||||
}
|
||||
|
||||
if (dashboardId && favorites.some((f) => f.id === dashboardId)) {
|
||||
return ShareAssetType.DASHBOARD;
|
||||
}
|
||||
|
||||
if (collectionId && favorites.some((f) => f.id === collectionId)) {
|
||||
return ShareAssetType.COLLECTION;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [favorites, chatId, metricId, dashboardId, collectionId]);
|
||||
|
||||
const favoritesDropdownItems: ISidebarGroup | null = useMemo(() => {
|
||||
if (!favorites || favorites.length === 0) return null;
|
||||
|
||||
return {
|
||||
label: 'Favorites',
|
||||
id: 'favorites',
|
||||
isSortable: true,
|
||||
onItemsReorder: onFavoritesReorder,
|
||||
items: favorites.map((favorite) => {
|
||||
const Icon = assetTypeToIcon(favorite.asset_type);
|
||||
const route = assetTypeToRoute(favorite.asset_type, favorite.id);
|
||||
return {
|
||||
label: favorite.name,
|
||||
icon: <Icon />,
|
||||
route,
|
||||
active: isAssetActive(favorite),
|
||||
id: favorite.id,
|
||||
onRemove: () => deleteUserFavorite([favorite.id])
|
||||
};
|
||||
})
|
||||
} satisfies ISidebarGroup;
|
||||
}, [
|
||||
favorites,
|
||||
deleteUserFavorite,
|
||||
onFavoritesReorder,
|
||||
isAssetActive,
|
||||
chatId,
|
||||
metricId,
|
||||
dashboardId,
|
||||
collectionId
|
||||
]);
|
||||
|
||||
return { favoritesDropdownItems, favoritedPageType };
|
||||
};
|
|
@ -72,8 +72,7 @@ export const Default: Story = {
|
|||
content: mockGroupedContent,
|
||||
footer: (
|
||||
<div className="flex h-full items-center justify-center text-sm text-gray-500">Footer</div>
|
||||
),
|
||||
activeItem: '1'
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -88,27 +87,25 @@ export const WithLongContent: Story = {
|
|||
id: `item-${i}`,
|
||||
label: `Menu Item ${i + 1}`,
|
||||
icon: <Window width="1.25em" height="1.25em" />,
|
||||
route: BusterRoutes.APP_HOME
|
||||
route: BusterRoutes.APP_HOME,
|
||||
active: i === 0
|
||||
}))
|
||||
}
|
||||
],
|
||||
footer: <div className="text-sm text-gray-500">Sticky Footer</div>,
|
||||
activeItem: 'item-1'
|
||||
footer: <div className="text-sm text-gray-500">Sticky Footer</div>
|
||||
}
|
||||
};
|
||||
|
||||
export const NoFooter: Story = {
|
||||
args: {
|
||||
header: <div className="text-xl font-semibold">My App</div>,
|
||||
content: mockGroupedContent,
|
||||
activeItem: '1'
|
||||
content: mockGroupedContent
|
||||
}
|
||||
};
|
||||
|
||||
export const ScrollAndTruncationTest: Story = {
|
||||
args: {
|
||||
header: <div className="text-xl font-semibold">Scroll & Truncation Test</div>,
|
||||
activeItem: 'long-4',
|
||||
content: [
|
||||
{
|
||||
id: 'default-items',
|
||||
|
@ -121,7 +118,8 @@ export const ScrollAndTruncationTest: Story = {
|
|||
id: `short-${i}`,
|
||||
label: `Item ${i + 1}`,
|
||||
icon: <Window width="1.25em" height="1.25em" />,
|
||||
route: BusterRoutes.APP_HOME
|
||||
route: BusterRoutes.APP_HOME,
|
||||
active: i === 4
|
||||
}))
|
||||
},
|
||||
{
|
||||
|
@ -175,7 +173,6 @@ export const WithRemovableItems: Story = {
|
|||
items: mockItems
|
||||
}
|
||||
],
|
||||
activeItem: '1',
|
||||
footer: <div className="text-sm text-gray-500">Footer</div>
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,46 +3,42 @@ import { ISidebarGroup, ISidebarList, SidebarProps } from './interfaces';
|
|||
import { SidebarCollapsible } from './SidebarCollapsible';
|
||||
import { SidebarItem } from './SidebarItem';
|
||||
|
||||
export const Sidebar: React.FC<SidebarProps> = React.memo(
|
||||
({ header, content, footer, activeItem }) => {
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden px-3.5 pt-4.5">
|
||||
<div className="flex flex-col space-y-4.5 overflow-hidden">
|
||||
<div className="mb-5"> {header}</div>
|
||||
<div className="flex flex-grow flex-col space-y-4.5 overflow-y-auto pb-3">
|
||||
{content.map((item) => (
|
||||
<ContentSelector key={item.id} content={item} activeItem={activeItem} />
|
||||
))}
|
||||
</div>
|
||||
export const Sidebar: React.FC<SidebarProps> = React.memo(({ header, content, footer }) => {
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden px-3.5 pt-4.5">
|
||||
<div className="flex flex-col space-y-4.5 overflow-hidden">
|
||||
<div className="mb-5"> {header}</div>
|
||||
<div className="flex flex-grow flex-col space-y-4.5 overflow-y-auto pb-3">
|
||||
{content.map((item) => (
|
||||
<ContentSelector key={item.id} content={item} />
|
||||
))}
|
||||
</div>
|
||||
{footer && <div className="mt-auto mb-2 overflow-hidden pt-5">{footer}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
{footer && <div className="mt-auto mb-2 overflow-hidden pt-5">{footer}</div>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Sidebar.displayName = 'Sidebar';
|
||||
|
||||
const ContentSelector: React.FC<{
|
||||
content: SidebarProps['content'][number];
|
||||
activeItem: SidebarProps['activeItem'];
|
||||
}> = React.memo(({ content, activeItem }) => {
|
||||
}> = React.memo(({ content }) => {
|
||||
if (isSidebarGroup(content)) {
|
||||
return <SidebarCollapsible {...content} activeItem={activeItem} />;
|
||||
return <SidebarCollapsible {...content} />;
|
||||
}
|
||||
|
||||
return <SidebarList items={content.items} activeItem={activeItem} />;
|
||||
return <SidebarList items={content.items} />;
|
||||
});
|
||||
ContentSelector.displayName = 'ContentSelector';
|
||||
|
||||
const SidebarList: React.FC<{
|
||||
items: ISidebarList['items'];
|
||||
activeItem: SidebarProps['activeItem'];
|
||||
}> = ({ items, activeItem }) => {
|
||||
}> = ({ items }) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-0.5">
|
||||
{items.map((item) => (
|
||||
<SidebarItem key={item.id} {...item} active={activeItem === item.id || item.active} />
|
||||
<SidebarItem key={item.id} {...item} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -33,6 +33,5 @@ export interface SidebarProps {
|
|||
header: React.ReactNode;
|
||||
content: SidebarContent[];
|
||||
footer?: React.ReactNode;
|
||||
activeItem: string;
|
||||
isSortable?: boolean;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue