Compare commits
6 Commits
3d04995f97
...
global-ass
| Author | SHA1 | Date | |
|---|---|---|---|
| c13afa7554 | |||
| 8033450321 | |||
| 98de8e948b | |||
| 3697119656 | |||
| e268b55392 | |||
| da5b1009e3 |
@@ -68,13 +68,13 @@ export function useExtensionRequestColumns(): ColumnDef<TicketExtensionRequest>[
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'ticket_name',
|
||||
header: 'Ticket / Lot',
|
||||
header: 'Ticket / Assignment',
|
||||
size: 170,
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
<p className="font-medium">{row.original.ticket_name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Lot #{row.original.assignment_id}
|
||||
Assignment #{row.original.assignment_id}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -90,8 +90,8 @@ export function ExtensionRequestDecisionDialog({
|
||||
: 'Reject extension request'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Review {request.ticket_name}, Lot #{request.assignment_id} before
|
||||
confirming this decision.
|
||||
Review {request.ticket_name}, Assignment #{request.assignment_id}{' '}
|
||||
before confirming this decision.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -205,8 +205,8 @@ export function ExtensionRequestDecisionDialog({
|
||||
<AlertDialogTitle>Reject extension request?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to reject the extension request for{' '}
|
||||
{request.ticket_name}, Lot #{request.assignment_id}? This decision
|
||||
cannot be undone.
|
||||
{request.ticket_name}, Assignment #{request.assignment_id}? This
|
||||
decision cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
||||
@@ -95,7 +95,7 @@ export default function ExtensionRequestsPage() {
|
||||
<main className="relative z-10 space-y-5">
|
||||
<PageHeader
|
||||
title="Extension Requests"
|
||||
description="Track deadline extension requests across all ticket lots."
|
||||
description="Track deadline extension requests across all ticket assignments."
|
||||
icon={CalendarClock}
|
||||
/>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const ModulesLayout = ({
|
||||
}>) => {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<SidebarProvider>
|
||||
<SidebarProvider defaultOpen={false}>
|
||||
<AppSidebar />
|
||||
<main className="flex h-screen min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="scroll-stable min-w-0 flex-1 overflow-auto">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Edit, RotateCcw } from 'lucide-react';
|
||||
import { Edit, ShieldCheck, ShieldX } from 'lucide-react';
|
||||
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { PERMISSIONS } from '@/constants/permissions';
|
||||
@@ -58,7 +58,12 @@ export function RoleTable({
|
||||
},
|
||||
{
|
||||
label: (role) => (role.effective_status ? 'Deactivate' : 'Activate'),
|
||||
icon: <RotateCcw className="size-4" />,
|
||||
icon: (role) =>
|
||||
role.effective_status ? (
|
||||
<ShieldX className="size-4 text-destructive" />
|
||||
) : (
|
||||
<ShieldCheck className="size-4 text-emerald-600" />
|
||||
),
|
||||
permission: PERMISSIONS.ROLE.DELETE,
|
||||
disabled: () => isStatusPending,
|
||||
onClick: onToggleStatus,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { AlertTriangle, ArrowLeft, LockKeyhole } from 'lucide-react';
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
@@ -78,7 +79,7 @@ export default function TicketAssignmentPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const ticketLabel = overview.ticket_name || overview.id;
|
||||
const ticketName = overview.ticket_name || 'Unnamed ticket';
|
||||
|
||||
return (
|
||||
<main className="relative z-10 space-y-5 xl:flex xl:h-[calc(100vh-6.5rem)] xl:min-h-0 xl:flex-col xl:gap-5 xl:space-y-0 xl:overflow-hidden">
|
||||
@@ -93,13 +94,17 @@ export default function TicketAssignmentPage() {
|
||||
>
|
||||
<ArrowLeft />
|
||||
</Button>
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Assign ticket
|
||||
Assign Ticket
|
||||
</h1>
|
||||
<p className="truncate text-sm text-muted-foreground">
|
||||
Ticket: {ticketLabel}
|
||||
</p>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="max-w-full font-mono text-xs"
|
||||
title={ticketName}
|
||||
>
|
||||
<span className="truncate">{ticketName}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { BriefcaseBusiness, CalendarClock, ListChecks } from 'lucide-react';
|
||||
import {
|
||||
BriefcaseBusiness,
|
||||
CalendarClock,
|
||||
ChevronDown,
|
||||
Images,
|
||||
ListChecks,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { PersonInfo } from '@/components/person-avatar';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -260,7 +271,25 @@ export function TicketAssignmentsCard({
|
||||
>
|
||||
<AssignmentOverview assignment={selectedAssignment} />
|
||||
{videoId ? (
|
||||
<div className={'mt-4 border-t pt-4'}>
|
||||
<Collapsible
|
||||
key={selectedAssignment.id}
|
||||
className="group mt-4 overflow-hidden rounded-lg border"
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-auto w-full justify-between rounded-none px-4 py-3"
|
||||
>
|
||||
<span className="flex items-center gap-2 font-medium">
|
||||
<Images className="size-4 text-primary" />
|
||||
Assigned issue images
|
||||
</span>
|
||||
<ChevronDown className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="border-t p-4">
|
||||
<TicketDetectionPreview
|
||||
key={`${videoId}:${selectedAssignment.id}`}
|
||||
ticketId={ticketId}
|
||||
@@ -268,6 +297,8 @@ export function TicketAssignmentsCard({
|
||||
assignmentId={selectedAssignment.id}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,8 +14,10 @@ import { OpenRepairReviewAction } from './actions/OpenRepairReviewAction';
|
||||
|
||||
export function TicketDetailHeader({
|
||||
ticket,
|
||||
backHref,
|
||||
}: {
|
||||
ticket: TicketOverviewDetail;
|
||||
backHref: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const ticketName = ticket.ticket_name || 'Unnamed ticket';
|
||||
@@ -27,7 +29,7 @@ export function TicketDetailHeader({
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onClick={() => router.push('/ticket')}
|
||||
onClick={() => router.push(backHref)}
|
||||
aria-label="Back to ticket list"
|
||||
className="shrink-0"
|
||||
>
|
||||
|
||||
@@ -147,7 +147,7 @@ export function ExtensionRequestHistoryDialog({
|
||||
<DialogTitle>Extension Request History</DialogTitle>
|
||||
<DialogDescription>
|
||||
Review every deadline extension request and manager decision for
|
||||
this ticket lot.
|
||||
this ticket assignment.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogBody className="space-y-3">
|
||||
|
||||
@@ -18,7 +18,7 @@ export function RequestExtensionAction({
|
||||
<TicketActionCard
|
||||
icon={Clock3}
|
||||
title={canRequest ? 'Request Extension' : 'Extension Request History'}
|
||||
contextLabel={`Lot ${assignmentId}`}
|
||||
contextLabel={`Assignment ${assignmentId}`}
|
||||
>
|
||||
<TicketExtensionRequestPanel
|
||||
ticketId={ticketId}
|
||||
|
||||
@@ -115,7 +115,7 @@ function ExtensionReviewForm({
|
||||
<TicketActionCard
|
||||
icon={CalendarClock}
|
||||
title="Extension Request Details"
|
||||
contextLabel={`Lot ${assignmentId}`}
|
||||
contextLabel={`Assignment ${assignmentId}`}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<Requester request={request} />
|
||||
@@ -271,7 +271,7 @@ export function ReviewExtensionRequestAction({
|
||||
<TicketActionCard
|
||||
icon={Clock3}
|
||||
title="Extension Request"
|
||||
contextLabel={`Lot ${assignmentId}`}
|
||||
contextLabel={`Assignment ${assignmentId}`}
|
||||
>
|
||||
<div className="flex items-center justify-center py-8 text-muted-foreground">
|
||||
<Loader2 className="mr-2 size-4 animate-spin" />
|
||||
@@ -298,7 +298,7 @@ export function ReviewExtensionRequestAction({
|
||||
<TicketActionCard
|
||||
icon={CalendarClock}
|
||||
title="Extension Request"
|
||||
contextLabel={`Lot ${assignmentId}`}
|
||||
contextLabel={`Assignment ${assignmentId}`}
|
||||
>
|
||||
<ExtensionRequestDetails request={latestRequest} requests={requests} />
|
||||
</TicketActionCard>
|
||||
|
||||
@@ -258,7 +258,7 @@ export function TicketExtensionRequestPanel({
|
||||
if (!canRequest) {
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No extension request history is available for this ticket lot.
|
||||
No extension request history is available for this ticket assignment.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { PERMISSIONS } from '@/constants/permissions';
|
||||
import { PermissionGuard } from '@/guards';
|
||||
import { ROUTES } from '@/utils/routes';
|
||||
|
||||
import { TicketDetailHeader } from './components/TicketDetailHeader';
|
||||
import {
|
||||
@@ -26,8 +27,20 @@ import {
|
||||
|
||||
export default function TicketDetailPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { ticketId } = useParams() as { ticketId: string };
|
||||
const [selectedAssignmentId, setSelectedAssignmentId] = useState<number>();
|
||||
const listParams = new URLSearchParams();
|
||||
|
||||
for (const key of ['page', 'limit', 'segment']) {
|
||||
const value = searchParams.get(key);
|
||||
if (value) listParams.set(key, value);
|
||||
}
|
||||
|
||||
const listSearch = listParams.toString();
|
||||
const ticketListHref = listSearch
|
||||
? `${ROUTES.TICKET}?${listSearch}`
|
||||
: ROUTES.TICKET;
|
||||
|
||||
const overviewQuery = useTicketOverviewQuery(ticketId);
|
||||
const overview = overviewQuery.data;
|
||||
@@ -52,7 +65,7 @@ export default function TicketDetailPage() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push('/ticket')}
|
||||
onClick={() => router.push(ticketListHref)}
|
||||
>
|
||||
<ArrowLeft />
|
||||
Back to Tickets
|
||||
@@ -65,7 +78,7 @@ export default function TicketDetailPage() {
|
||||
<main className="relative z-10 min-w-0 max-w-full space-y-5 xl:flex xl:h-[calc(100vh-6.5rem)] xl:min-h-0 xl:flex-col xl:gap-5 xl:space-y-0 xl:overflow-hidden">
|
||||
<div className="xl:shrink-0">
|
||||
{overview ? (
|
||||
<TicketDetailHeader ticket={overview} />
|
||||
<TicketDetailHeader ticket={overview} backHref={ticketListHref} />
|
||||
) : (
|
||||
<TicketOverviewHeaderSkeleton />
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import { Ticket } from 'lucide-react';
|
||||
import type { TicketListItem } from '@/types';
|
||||
@@ -17,11 +17,48 @@ import {
|
||||
useTicketSummaryQuery,
|
||||
} from './hooks/useTicketQueries';
|
||||
|
||||
const DEFAULT_PAGE = 1;
|
||||
const DEFAULT_LIMIT = 10;
|
||||
const PAGE_SIZE_OPTIONS = new Set([10, 20, 40, 50, 100]);
|
||||
|
||||
function parsePage(value: string | null) {
|
||||
const page = Number(value);
|
||||
return Number.isInteger(page) && page > 0 ? page : DEFAULT_PAGE;
|
||||
}
|
||||
|
||||
function parseLimit(value: string | null) {
|
||||
const limit = Number(value);
|
||||
return PAGE_SIZE_OPTIONS.has(limit) ? limit : DEFAULT_LIMIT;
|
||||
}
|
||||
|
||||
export default function TicketPage() {
|
||||
const router = useRouter();
|
||||
const [skip, setSkip] = useState(0);
|
||||
const [limit, setLimit] = useState(10);
|
||||
const [segmentId, setSegmentId] = useState('');
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const searchParamsString = searchParams.toString();
|
||||
const latestSearchParamsRef = useRef(searchParamsString);
|
||||
|
||||
useEffect(() => {
|
||||
latestSearchParamsRef.current = searchParamsString;
|
||||
}, [searchParamsString]);
|
||||
|
||||
const page = parsePage(searchParams.get('page'));
|
||||
const limit = parseLimit(searchParams.get('limit'));
|
||||
const segmentId = searchParams.get('segment') ?? '';
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const replaceListParams = useCallback(
|
||||
(update: (params: URLSearchParams) => void) => {
|
||||
const nextParams = new URLSearchParams(latestSearchParamsRef.current);
|
||||
update(nextParams);
|
||||
const nextSearch = nextParams.toString();
|
||||
latestSearchParamsRef.current = nextSearch;
|
||||
router.replace(nextSearch ? `${pathname}?${nextSearch}` : pathname, {
|
||||
scroll: false,
|
||||
});
|
||||
},
|
||||
[pathname, router],
|
||||
);
|
||||
|
||||
useTenantTicketTableEvents();
|
||||
|
||||
@@ -36,16 +73,56 @@ export default function TicketPage() {
|
||||
const tickets = ticketsQuery.data?.items ?? [];
|
||||
const total = ticketsQuery.data?.total ?? 0;
|
||||
|
||||
const handleSegmentChange = useCallback((nextSegmentId: string) => {
|
||||
setSegmentId(nextSegmentId);
|
||||
setSkip(0);
|
||||
}, []);
|
||||
const handleSegmentChange = useCallback(
|
||||
(nextSegmentId: string) => {
|
||||
replaceListParams((params) => {
|
||||
params.delete('page');
|
||||
if (nextSegmentId) {
|
||||
params.set('segment', nextSegmentId);
|
||||
} else {
|
||||
params.delete('segment');
|
||||
}
|
||||
});
|
||||
},
|
||||
[replaceListParams],
|
||||
);
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(nextSkip: number) => {
|
||||
const nextPage = Math.floor(nextSkip / limit) + 1;
|
||||
replaceListParams((params) => {
|
||||
if (nextPage === DEFAULT_PAGE) {
|
||||
params.delete('page');
|
||||
} else {
|
||||
params.set('page', String(nextPage));
|
||||
}
|
||||
});
|
||||
},
|
||||
[limit, replaceListParams],
|
||||
);
|
||||
|
||||
const handleLimitChange = useCallback(
|
||||
(nextLimit: number) => {
|
||||
replaceListParams((params) => {
|
||||
params.delete('page');
|
||||
if (nextLimit === DEFAULT_LIMIT) {
|
||||
params.delete('limit');
|
||||
} else {
|
||||
params.set('limit', String(nextLimit));
|
||||
}
|
||||
});
|
||||
},
|
||||
[replaceListParams],
|
||||
);
|
||||
|
||||
const handleView = useCallback(
|
||||
(ticket: TicketListItem) => {
|
||||
router.push(ROUTES.TICKET_DETAIL(ticket.id));
|
||||
const detailPath = ROUTES.TICKET_DETAIL(ticket.id);
|
||||
router.push(
|
||||
searchParamsString ? `${detailPath}?${searchParamsString}` : detailPath,
|
||||
);
|
||||
},
|
||||
[router],
|
||||
[router, searchParamsString],
|
||||
);
|
||||
|
||||
const toolbar = useMemo(
|
||||
@@ -81,8 +158,8 @@ export default function TicketPage() {
|
||||
skip={skip}
|
||||
limit={limit}
|
||||
total={total}
|
||||
onPageChange={setSkip}
|
||||
onLimitChange={setLimit}
|
||||
onPageChange={handlePageChange}
|
||||
onLimitChange={handleLimitChange}
|
||||
onView={handleView}
|
||||
/>
|
||||
</main>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import { Edit, RotateCcw } from 'lucide-react';
|
||||
import { Edit, UserCheck, UserX } from 'lucide-react';
|
||||
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { PERMISSIONS } from '@/constants/permissions';
|
||||
@@ -59,7 +59,12 @@ export function UserTable({
|
||||
{
|
||||
label: (user) =>
|
||||
user.effective_status === 'active' ? 'Deactivate' : 'Activate',
|
||||
icon: <RotateCcw className="size-4" />,
|
||||
icon: (user) =>
|
||||
user.effective_status === 'active' ? (
|
||||
<UserX className="size-4 text-destructive" />
|
||||
) : (
|
||||
<UserCheck className="size-4 text-emerald-600" />
|
||||
),
|
||||
permission: PERMISSIONS.USER.DELETE,
|
||||
disabled: (user) =>
|
||||
pendingUserId === user.id || user.effective_status === 'pending',
|
||||
|
||||
@@ -140,6 +140,40 @@
|
||||
|
||||
* {
|
||||
@apply outline-none border-border outline-ring/50;
|
||||
scrollbar-color: color-mix(
|
||||
in oklab,
|
||||
var(--muted-foreground) 45%,
|
||||
transparent
|
||||
)
|
||||
transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track,
|
||||
*::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
border-radius: 9999px;
|
||||
background-color: color-mix(
|
||||
in oklab,
|
||||
var(--muted-foreground) 45%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background-color: color-mix(
|
||||
in oklab,
|
||||
var(--muted-foreground) 70%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -14,6 +14,14 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -124,6 +132,7 @@ function SidebarNavItem({
|
||||
item: MenuItem;
|
||||
pathname: string;
|
||||
}) {
|
||||
const { isMobile, state } = useSidebar();
|
||||
const isActive = isRouteActive(pathname, item.path);
|
||||
const isChildActive =
|
||||
item.children?.some((child) => isRouteActive(pathname, child.path)) ??
|
||||
@@ -136,6 +145,52 @@ function SidebarNavItem({
|
||||
}, [isChildActive]);
|
||||
|
||||
if (hasChildren) {
|
||||
if (state === 'collapsed' && !isMobile) {
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
isActive={isChildActive}
|
||||
aria-label={item.title}
|
||||
>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
<ChevronRight className="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="right"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
className="min-w-48"
|
||||
>
|
||||
<DropdownMenuLabel>{item.title}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{item.children?.map((child) =>
|
||||
child.path ? (
|
||||
<DropdownMenuItem
|
||||
key={child.title}
|
||||
asChild
|
||||
className={
|
||||
isRouteActive(pathname, child.path)
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Link href={child.path}>
|
||||
<child.icon />
|
||||
<span>{child.title}</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
) : null,
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
asChild
|
||||
|
||||
@@ -41,7 +41,7 @@ export type ColumnAlign = 'left' | 'right';
|
||||
|
||||
export interface DataTableAction<TData> {
|
||||
label: string | ((item: TData) => string);
|
||||
icon: React.ReactNode;
|
||||
icon: React.ReactNode | ((item: TData) => React.ReactNode);
|
||||
onClick: (item: TData) => void;
|
||||
permission?: PermissionInput;
|
||||
disabled?: (item: TData) => boolean;
|
||||
@@ -163,6 +163,10 @@ function DataTableContent<TData, TValue>({
|
||||
typeof action.label === 'function'
|
||||
? action.label(item)
|
||||
: action.label;
|
||||
const icon =
|
||||
typeof action.icon === 'function'
|
||||
? action.icon(item)
|
||||
: action.icon;
|
||||
|
||||
return (
|
||||
<TableActionButton
|
||||
@@ -176,7 +180,7 @@ function DataTableContent<TData, TValue>({
|
||||
action.onClick(item);
|
||||
}}
|
||||
>
|
||||
{action.icon}
|
||||
{icon}
|
||||
</TableActionButton>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user