Enhanced Sidebar to support sub collapsible sections (#80)
* Enhanced Sidebar to support sub collapsible sections * Data-Table: support getSortedRowModel * Add missing renderAction; * Fix Sidebar on mobile * Do not initialize Analytics Provider server side * Do not bind i18Next until it's initialized * Avoid infinite redirects in the auth path when Supabase emits a SIGNED_OUT event * Force admin layout to be dynamic
This commit is contained in:
committed by
GitHub
parent
27ef8f7510
commit
465655fdd4
@@ -7,6 +7,7 @@ import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import type {
|
||||
@@ -45,6 +46,8 @@ interface ReactTableProps<T extends object> {
|
||||
pageSize?: number;
|
||||
pageCount?: number;
|
||||
onPaginationChange?: (pagination: PaginationState) => void;
|
||||
manualPagination?: boolean;
|
||||
manualSorting?: boolean;
|
||||
tableProps?: React.ComponentProps<typeof Table> &
|
||||
Record<`data-${string}`, string>;
|
||||
}
|
||||
@@ -57,6 +60,8 @@ export function DataTable<T extends object>({
|
||||
pageCount,
|
||||
onPaginationChange,
|
||||
tableProps,
|
||||
manualPagination = true,
|
||||
manualSorting = false,
|
||||
}: ReactTableProps<T>) {
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: pageIndex ?? 0,
|
||||
@@ -74,7 +79,9 @@ export function DataTable<T extends object>({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
manualPagination,
|
||||
manualSorting,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
|
||||
@@ -9,25 +9,32 @@ const Divider = z.object({
|
||||
divider: z.literal(true),
|
||||
});
|
||||
|
||||
const RouteChildren = z.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
path: z.string(),
|
||||
Icon: z.custom<React.ReactNode>(),
|
||||
end: RouteMatchingEnd,
|
||||
children: z
|
||||
.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
path: z.string(),
|
||||
Icon: z.custom<React.ReactNode>(),
|
||||
end: RouteMatchingEnd,
|
||||
}),
|
||||
)
|
||||
.default([])
|
||||
.optional(),
|
||||
}),
|
||||
);
|
||||
const RouteSubChild = z.object({
|
||||
label: z.string(),
|
||||
path: z.string(),
|
||||
Icon: z.custom<React.ReactNode>().optional(),
|
||||
end: RouteMatchingEnd,
|
||||
renderAction: z.custom<React.ReactNode>().optional(),
|
||||
});
|
||||
|
||||
const RouteChild = z.object({
|
||||
label: z.string(),
|
||||
path: z.string(),
|
||||
Icon: z.custom<React.ReactNode>().optional(),
|
||||
end: RouteMatchingEnd,
|
||||
children: z.array(RouteSubChild).default([]).optional(),
|
||||
collapsible: z.boolean().default(false).optional(),
|
||||
collapsed: z.boolean().default(false).optional(),
|
||||
renderAction: z.custom<React.ReactNode>().optional(),
|
||||
});
|
||||
|
||||
const RouteGroup = z.object({
|
||||
label: z.string(),
|
||||
collapsible: z.boolean().optional(),
|
||||
collapsed: z.boolean().optional(),
|
||||
children: z.array(RouteChild),
|
||||
renderAction: z.custom<React.ReactNode>().optional(),
|
||||
});
|
||||
|
||||
export const NavigationConfigSchema = z.object({
|
||||
style: z.enum(['custom', 'sidebar', 'header']).default('sidebar'),
|
||||
@@ -36,16 +43,5 @@ export const NavigationConfigSchema = z.object({
|
||||
.default('false')
|
||||
.optional()
|
||||
.transform((value) => value === `true`),
|
||||
routes: z.array(
|
||||
z.union([
|
||||
z.object({
|
||||
label: z.string(),
|
||||
collapsible: z.boolean().optional(),
|
||||
collapsed: z.boolean().optional(),
|
||||
children: RouteChildren,
|
||||
renderAction: z.custom<React.ReactNode>().optional(),
|
||||
}),
|
||||
Divider,
|
||||
]),
|
||||
),
|
||||
routes: z.array(z.union([RouteGroup, Divider])),
|
||||
});
|
||||
|
||||
@@ -329,16 +329,24 @@ export function SidebarNavigation({
|
||||
collapsed={item.collapsed}
|
||||
>
|
||||
{item.children.map((child) => {
|
||||
return (
|
||||
<SidebarItem
|
||||
key={child.path}
|
||||
end={child.end}
|
||||
path={child.path}
|
||||
Icon={child.Icon}
|
||||
>
|
||||
<Trans i18nKey={child.label} defaults={child.label} />
|
||||
</SidebarItem>
|
||||
);
|
||||
if ('collapsible' in child && child.collapsible) {
|
||||
throw new Error(
|
||||
'Collapsible groups are not supported in the old Sidebar. Please migrate to the new Sidebar.',
|
||||
);
|
||||
}
|
||||
|
||||
if ('path' in child) {
|
||||
return (
|
||||
<SidebarItem
|
||||
key={child.path}
|
||||
end={child.end}
|
||||
path={child.path}
|
||||
Icon={child.Icon}
|
||||
>
|
||||
<Trans i18nKey={child.label} defaults={child.label} />
|
||||
</SidebarItem>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</SidebarGroup>
|
||||
);
|
||||
|
||||
@@ -910,18 +910,96 @@ export function SidebarNavigation({
|
||||
</SidebarGroupAction>
|
||||
</If>
|
||||
|
||||
<ContentContainer>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{item.children.map((child) => {
|
||||
const isActive = isRouteActive(
|
||||
child.path,
|
||||
currentPath,
|
||||
child.end,
|
||||
);
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<ContentContainer>
|
||||
{item.children.map((child, childIndex) => {
|
||||
if (child.renderAction) {
|
||||
return (
|
||||
<SidebarMenuSubItem key={child.path}>
|
||||
{child.renderAction}
|
||||
</SidebarMenuSubItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenuItem key={child.path}>
|
||||
const Container = (props: React.PropsWithChildren) => {
|
||||
if ('collapsible' in child && child.collapsible) {
|
||||
return (
|
||||
<Collapsible
|
||||
defaultOpen={!child.collapsed}
|
||||
className={'group/collapsible'}
|
||||
>
|
||||
{props.children}
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
return props.children;
|
||||
};
|
||||
|
||||
const ContentContainer = (
|
||||
props: React.PropsWithChildren,
|
||||
) => {
|
||||
if ('collapsible' in child && child.collapsible) {
|
||||
return (
|
||||
<CollapsibleContent>
|
||||
{props.children}
|
||||
</CollapsibleContent>
|
||||
);
|
||||
}
|
||||
|
||||
return props.children;
|
||||
};
|
||||
|
||||
const TriggerItem = () => {
|
||||
if ('collapsible' in child && child.collapsible) {
|
||||
return (
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={child.label}>
|
||||
<div
|
||||
className={cn('flex items-center gap-2', {
|
||||
'mx-auto w-full gap-0 [&>svg]:flex-1 [&>svg]:shrink-0':
|
||||
minimized,
|
||||
})}
|
||||
>
|
||||
{child.Icon}
|
||||
<span
|
||||
className={cn(
|
||||
'w-auto transition-opacity duration-300',
|
||||
{
|
||||
'w-0 opacity-0': minimized,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Trans
|
||||
i18nKey={child.label}
|
||||
defaults={child.label}
|
||||
/>
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'ml-auto size-4 transition-transform group-data-[state=open]/collapsible:rotate-180',
|
||||
{
|
||||
'hidden size-0': minimized,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
const path = 'path' in child ? child.path : '';
|
||||
const end = 'end' in child ? child.end : false;
|
||||
|
||||
const isActive = isRouteActive(
|
||||
path,
|
||||
currentPath,
|
||||
end,
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={isActive}
|
||||
@@ -932,10 +1010,9 @@ export function SidebarNavigation({
|
||||
'mx-auto w-full !gap-0 [&>svg]:flex-1':
|
||||
minimized,
|
||||
})}
|
||||
href={child.path}
|
||||
href={path}
|
||||
>
|
||||
{child.Icon}
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
'w-auto transition-opacity duration-300',
|
||||
@@ -951,61 +1028,86 @@ export function SidebarNavigation({
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
);
|
||||
};
|
||||
|
||||
<If condition={child.children}>
|
||||
{(children) => (
|
||||
<SidebarMenuSub
|
||||
className={cn({ 'mx-0 px-1.5': minimized })}
|
||||
>
|
||||
{children.map((child) => {
|
||||
const isActive = isRouteActive(
|
||||
child.path,
|
||||
currentPath,
|
||||
child.end,
|
||||
);
|
||||
return (
|
||||
<Container key={`group-${index}-${childIndex}`}>
|
||||
<SidebarMenuItem>
|
||||
<TriggerItem />
|
||||
|
||||
return (
|
||||
<SidebarMenuSubItem key={child.path}>
|
||||
<SidebarMenuSubButton
|
||||
isActive={isActive}
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
className={cn('flex items-center', {
|
||||
'mx-auto w-full !gap-0 [&>svg]:flex-1':
|
||||
minimized,
|
||||
})}
|
||||
href={child.path}
|
||||
>
|
||||
{child.Icon}
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
'w-auto transition-opacity duration-300',
|
||||
{
|
||||
'w-0 opacity-0': minimized,
|
||||
},
|
||||
)}
|
||||
<ContentContainer>
|
||||
<If condition={child.children}>
|
||||
{(children) => (
|
||||
<SidebarMenuSub
|
||||
className={cn({
|
||||
'mx-0 px-1.5': minimized,
|
||||
})}
|
||||
>
|
||||
{children.map((child) => {
|
||||
if (child.renderAction) {
|
||||
return (
|
||||
<SidebarMenuSubItem
|
||||
key={child.path}
|
||||
>
|
||||
<Trans
|
||||
i18nKey={child.label}
|
||||
defaults={child.label}
|
||||
/>
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenuSub>
|
||||
)}
|
||||
</If>
|
||||
</SidebarMenuItem>
|
||||
{child.renderAction}
|
||||
</SidebarMenuSubItem>
|
||||
);
|
||||
}
|
||||
|
||||
const isActive = isRouteActive(
|
||||
child.path,
|
||||
currentPath,
|
||||
child.end,
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarMenuSubItem key={child.path}>
|
||||
<SidebarMenuSubButton
|
||||
isActive={isActive}
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
className={cn(
|
||||
'flex items-center',
|
||||
{
|
||||
'mx-auto w-full !gap-0 [&>svg]:flex-1':
|
||||
minimized,
|
||||
},
|
||||
)}
|
||||
href={child.path}
|
||||
>
|
||||
{child.Icon}
|
||||
<span
|
||||
className={cn(
|
||||
'w-auto transition-opacity duration-300',
|
||||
{
|
||||
'w-0 opacity-0':
|
||||
minimized,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Trans
|
||||
i18nKey={child.label}
|
||||
defaults={child.label}
|
||||
/>
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenuSub>
|
||||
)}
|
||||
</If>
|
||||
</ContentContainer>
|
||||
</SidebarMenuItem>
|
||||
</Container>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</ContentContainer>
|
||||
</ContentContainer>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
<If condition={minimized && !isLast}>
|
||||
|
||||
Reference in New Issue
Block a user