@mostrom/app-shell 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/ralph-loop.local.md +9 -0
- package/README.md +172 -0
- package/bin/init.js +269 -0
- package/bun.lock +401 -0
- package/components.json +28 -0
- package/package.json +74 -0
- package/scripts/publish-npm.sh +202 -0
- package/src/AppShell.tsx +847 -0
- package/src/components/PageHeader.tsx +160 -0
- package/src/components/data-table/README.md +447 -0
- package/src/components/data-table/data-table-preferences.tsx +184 -0
- package/src/components/data-table/data-table-toolbar.tsx +118 -0
- package/src/components/data-table/data-table.tsx +37 -0
- package/src/components/data-table/index.ts +32 -0
- package/src/components/global-header/AllServicesButton.tsx +127 -0
- package/src/components/global-header/CategoriesButton.tsx +120 -0
- package/src/components/global-header/GlobalHeader.tsx +59 -0
- package/src/components/global-header/GlobalHeaderSearch.tsx +57 -0
- package/src/components/global-header/HeaderUtilities.tsx +243 -0
- package/src/components/global-header/ServicesMenu.tsx +246 -0
- package/src/components/layout/AppBreadcrumb.tsx +70 -0
- package/src/components/layout/AppFlashbar.tsx +95 -0
- package/src/components/layout/AppLayout.tsx +271 -0
- package/src/components/layout/AppNavigation.tsx +313 -0
- package/src/components/layout/AppSidebar.tsx +229 -0
- package/src/components/patterns/index.ts +14 -0
- package/src/components/patterns/p-alert-5.tsx +19 -0
- package/src/components/patterns/p-autocomplete-5.tsx +89 -0
- package/src/components/patterns/p-breadcrumb-1.tsx +28 -0
- package/src/components/patterns/p-button-42.tsx +37 -0
- package/src/components/patterns/p-button-51.tsx +14 -0
- package/src/components/patterns/p-button-6.tsx +5 -0
- package/src/components/patterns/p-calendar-1.tsx +18 -0
- package/src/components/patterns/p-card-1.tsx +33 -0
- package/src/components/patterns/p-card-2.tsx +26 -0
- package/src/components/patterns/p-card-5.tsx +31 -0
- package/src/components/patterns/p-collapsible-7.tsx +121 -0
- package/src/components/patterns/p-command-6.tsx +113 -0
- package/src/components/patterns/p-dialog-1.tsx +56 -0
- package/src/components/patterns/p-dropdown-menu-1.tsx +38 -0
- package/src/components/patterns/p-dropdown-menu-11.tsx +122 -0
- package/src/components/patterns/p-dropdown-menu-14.tsx +165 -0
- package/src/components/patterns/p-dropdown-menu-9.tsx +108 -0
- package/src/components/patterns/p-empty-2.tsx +34 -0
- package/src/components/patterns/p-file-upload-1.tsx +72 -0
- package/src/components/patterns/p-filters-1.tsx +666 -0
- package/src/components/patterns/p-frame-2.tsx +26 -0
- package/src/components/patterns/p-tabs-2.tsx +129 -0
- package/src/components/reui/alert.tsx +92 -0
- package/src/components/reui/autocomplete.tsx +343 -0
- package/src/components/reui/badge.tsx +87 -0
- package/src/components/reui/data-grid/data-grid-column-filter.tsx +165 -0
- package/src/components/reui/data-grid/data-grid-column-header.tsx +339 -0
- package/src/components/reui/data-grid/data-grid-column-visibility.tsx +55 -0
- package/src/components/reui/data-grid/data-grid-pagination.tsx +224 -0
- package/src/components/reui/data-grid/data-grid-table-dnd-rows.tsx +260 -0
- package/src/components/reui/data-grid/data-grid-table-dnd.tsx +253 -0
- package/src/components/reui/data-grid/data-grid-table.tsx +639 -0
- package/src/components/reui/data-grid/data-grid.tsx +209 -0
- package/src/components/reui/date-selector.tsx +1330 -0
- package/src/components/reui/filters.tsx +1869 -0
- package/src/components/reui/frame.tsx +134 -0
- package/src/components/reui/index.ts +17 -0
- package/src/components/reui/timeline.tsx +219 -0
- package/src/components/search/Autocomplete.tsx +183 -0
- package/src/components/search/AutocompleteClient.tsx +293 -0
- package/src/components/search/GlobalSearch.tsx +187 -0
- package/src/components/section-drawer/deal-drawer-content.tsx +891 -0
- package/src/components/section-drawer/index.ts +19 -0
- package/src/components/section-drawer/section-drawer.css +665 -0
- package/src/components/section-drawer/section-drawer.tsx +467 -0
- package/src/components/sectioned-list-board/README.md +78 -0
- package/src/components/sectioned-list-board/board-card-content.tsx +340 -0
- package/src/components/sectioned-list-board/date-range-filter.tsx +249 -0
- package/src/components/sectioned-list-board/index.ts +19 -0
- package/src/components/sectioned-list-board/sectioned-list-board.css +564 -0
- package/src/components/sectioned-list-board/sectioned-list-board.tsx +731 -0
- package/src/components/sectioned-list-board/sortable-card.tsx +314 -0
- package/src/components/sectioned-list-board/sortable-section.tsx +319 -0
- package/src/components/sectioned-list-board/types.ts +216 -0
- package/src/components/sectioned-list-table/README.md +80 -0
- package/src/components/sectioned-list-table/index.ts +14 -0
- package/src/components/sectioned-list-table/sectioned-list-table.css +534 -0
- package/src/components/sectioned-list-table/sectioned-list-table.tsx +740 -0
- package/src/components/sectioned-list-table/sortable-column-header.tsx +120 -0
- package/src/components/sectioned-list-table/sortable-row.tsx +420 -0
- package/src/components/sectioned-list-table/sortable-section.tsx +251 -0
- package/src/components/sectioned-list-table/table-cell-content.tsx +129 -0
- package/src/components/sectioned-list-table/types.ts +120 -0
- package/src/components/sectioned-list-table/use-column-preferences.ts +103 -0
- package/src/components/ui/actions-dropdown.tsx +109 -0
- package/src/components/ui/assignee-selector.tsx +209 -0
- package/src/components/ui/avatar.tsx +107 -0
- package/src/components/ui/breadcrumb.tsx +109 -0
- package/src/components/ui/button-group.tsx +83 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/calendar.tsx +220 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/chart.tsx +376 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/command.tsx +182 -0
- package/src/components/ui/context-menu.tsx +250 -0
- package/src/components/ui/create-button-group.tsx +128 -0
- package/src/components/ui/dialog.tsx +156 -0
- package/src/components/ui/drawer.tsx +133 -0
- package/src/components/ui/dropdown-menu.tsx +255 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +248 -0
- package/src/components/ui/form.tsx +165 -0
- package/src/components/ui/index.ts +37 -0
- package/src/components/ui/input-group.tsx +168 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/kbd.tsx +28 -0
- package/src/components/ui/label.tsx +22 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/page-header.tsx +80 -0
- package/src/components/ui/popover.tsx +87 -0
- package/src/components/ui/scroll-area.tsx +56 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/sheet.tsx +141 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +38 -0
- package/src/components/ui/switch.tsx +33 -0
- package/src/components/ui/tabs.tsx +91 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +83 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/tooltip.tsx +57 -0
- package/src/hooks/use-copy-to-clipboard.ts +37 -0
- package/src/hooks/use-file-upload.ts +415 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.ts +95 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles.css +1859 -0
- package/src/urls.ts +83 -0
- package/src/vite.d.ts +22 -0
- package/src/vite.js +241 -0
- package/tsconfig.base.json +18 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from "react"
|
|
4
|
+
import { Column } from "@tanstack/react-table"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import { Badge } from "@/components/reui/badge"
|
|
8
|
+
import { Button } from "@/components/ui/button"
|
|
9
|
+
import { Input } from "@/components/ui/input"
|
|
10
|
+
import {
|
|
11
|
+
Popover,
|
|
12
|
+
PopoverContent,
|
|
13
|
+
PopoverTrigger,
|
|
14
|
+
} from "@/components/ui/popover"
|
|
15
|
+
import { Separator } from "@/components/ui/separator"
|
|
16
|
+
import { CirclePlusIcon, CheckIcon } from "lucide-react"
|
|
17
|
+
|
|
18
|
+
interface DataGridColumnFilterProps<TData, TValue> {
|
|
19
|
+
column?: Column<TData, TValue>
|
|
20
|
+
title?: string
|
|
21
|
+
options: {
|
|
22
|
+
label: string
|
|
23
|
+
value: string
|
|
24
|
+
icon?: React.ComponentType<{ className?: string }>
|
|
25
|
+
}[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function DataGridColumnFilter<TData, TValue>({
|
|
29
|
+
column,
|
|
30
|
+
title,
|
|
31
|
+
options,
|
|
32
|
+
}: DataGridColumnFilterProps<TData, TValue>) {
|
|
33
|
+
const facets = column?.getFacetedUniqueValues()
|
|
34
|
+
const selectedValues = new Set(column?.getFilterValue() as string[])
|
|
35
|
+
const [searchQuery, setSearchQuery] = useState("")
|
|
36
|
+
|
|
37
|
+
const filteredOptions = useMemo(() => {
|
|
38
|
+
if (!searchQuery) return options
|
|
39
|
+
return options.filter((option) =>
|
|
40
|
+
option.label.toLowerCase().includes(searchQuery.toLowerCase())
|
|
41
|
+
)
|
|
42
|
+
}, [options, searchQuery])
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Popover>
|
|
46
|
+
<PopoverTrigger asChild>
|
|
47
|
+
<Button variant="outline" size="sm">
|
|
48
|
+
<CirclePlusIcon className="size-4" />
|
|
49
|
+
{title}
|
|
50
|
+
{selectedValues?.size > 0 && (
|
|
51
|
+
<>
|
|
52
|
+
<Separator orientation="vertical" className="mx-2 h-4" />
|
|
53
|
+
<Badge
|
|
54
|
+
variant="secondary"
|
|
55
|
+
className="rounded-sm px-1 font-normal lg:hidden"
|
|
56
|
+
>
|
|
57
|
+
{selectedValues.size}
|
|
58
|
+
</Badge>
|
|
59
|
+
<div className="hidden space-x-1 lg:flex">
|
|
60
|
+
{selectedValues.size > 2 ? (
|
|
61
|
+
<Badge
|
|
62
|
+
variant="secondary"
|
|
63
|
+
className="rounded-sm px-1 font-normal"
|
|
64
|
+
>
|
|
65
|
+
{selectedValues.size} selected
|
|
66
|
+
</Badge>
|
|
67
|
+
) : (
|
|
68
|
+
options
|
|
69
|
+
.filter((option) => selectedValues.has(option.value))
|
|
70
|
+
.map((option) => (
|
|
71
|
+
<Badge
|
|
72
|
+
variant="secondary"
|
|
73
|
+
key={option.value}
|
|
74
|
+
className="rounded-sm px-1 font-normal"
|
|
75
|
+
>
|
|
76
|
+
{option.label}
|
|
77
|
+
</Badge>
|
|
78
|
+
))
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
</>
|
|
82
|
+
)}
|
|
83
|
+
</Button>
|
|
84
|
+
</PopoverTrigger>
|
|
85
|
+
<PopoverContent className="w-[200px] p-0" align="start">
|
|
86
|
+
<div className="p-2">
|
|
87
|
+
<Input
|
|
88
|
+
placeholder={title}
|
|
89
|
+
value={searchQuery}
|
|
90
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
91
|
+
className="h-8"
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
<div className="max-h-[300px] overflow-y-auto">
|
|
95
|
+
{filteredOptions.length === 0 ? (
|
|
96
|
+
<div className="text-muted-foreground py-6 text-center text-sm">
|
|
97
|
+
No results found.
|
|
98
|
+
</div>
|
|
99
|
+
) : (
|
|
100
|
+
<div className="p-1">
|
|
101
|
+
{filteredOptions.map((option) => {
|
|
102
|
+
const isSelected = selectedValues.has(option.value)
|
|
103
|
+
return (
|
|
104
|
+
<div
|
|
105
|
+
key={option.value}
|
|
106
|
+
onClick={() => {
|
|
107
|
+
if (isSelected) {
|
|
108
|
+
selectedValues.delete(option.value)
|
|
109
|
+
} else {
|
|
110
|
+
selectedValues.add(option.value)
|
|
111
|
+
}
|
|
112
|
+
const filterValues = Array.from(selectedValues)
|
|
113
|
+
column?.setFilterValue(
|
|
114
|
+
filterValues.length ? filterValues : undefined
|
|
115
|
+
)
|
|
116
|
+
}}
|
|
117
|
+
className={cn(
|
|
118
|
+
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none",
|
|
119
|
+
"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
<div
|
|
123
|
+
className={cn(
|
|
124
|
+
"border-primary me-2 flex h-4 w-4 items-center justify-center rounded-sm border",
|
|
125
|
+
isSelected
|
|
126
|
+
? "bg-primary text-primary-foreground"
|
|
127
|
+
: "opacity-50 [&_svg]:invisible"
|
|
128
|
+
)}
|
|
129
|
+
>
|
|
130
|
+
<CheckIcon className="size-4" />
|
|
131
|
+
</div>
|
|
132
|
+
{option.icon && (
|
|
133
|
+
<option.icon className="text-muted-foreground mr-2 h-4 w-4" />
|
|
134
|
+
)}
|
|
135
|
+
<span>{option.label}</span>
|
|
136
|
+
{facets?.get(option.value) && (
|
|
137
|
+
<span className="ms-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
|
|
138
|
+
{facets.get(option.value)}
|
|
139
|
+
</span>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
})}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
{selectedValues.size > 0 && (
|
|
147
|
+
<>
|
|
148
|
+
<div className="bg-border -mx-1 my-1 h-px" />
|
|
149
|
+
<div className="p-1">
|
|
150
|
+
<div
|
|
151
|
+
onClick={() => column?.setFilterValue(undefined)}
|
|
152
|
+
className="hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center justify-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none"
|
|
153
|
+
>
|
|
154
|
+
Clear filters
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
</PopoverContent>
|
|
161
|
+
</Popover>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export { DataGridColumnFilter, type DataGridColumnFilterProps }
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { HTMLAttributes, memo, ReactNode, useMemo } from "react"
|
|
2
|
+
import { useDataGrid } from "@/components/reui/data-grid/data-grid"
|
|
3
|
+
import { Column } from "@tanstack/react-table"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
import { Button } from "@/components/ui/button"
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuCheckboxItem,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuGroup,
|
|
12
|
+
DropdownMenuItem,
|
|
13
|
+
DropdownMenuLabel,
|
|
14
|
+
DropdownMenuSeparator,
|
|
15
|
+
DropdownMenuSub,
|
|
16
|
+
DropdownMenuSubContent,
|
|
17
|
+
DropdownMenuSubTrigger,
|
|
18
|
+
DropdownMenuTrigger,
|
|
19
|
+
} from "@/components/ui/dropdown-menu"
|
|
20
|
+
import { ArrowDownIcon, ArrowUpIcon, ChevronsUpDownIcon, CheckIcon, ArrowLeftToLineIcon, ArrowRightToLineIcon, ArrowLeftIcon, ArrowRightIcon, Settings2Icon, PinOffIcon } from "lucide-react"
|
|
21
|
+
|
|
22
|
+
interface DataGridColumnHeaderProps<
|
|
23
|
+
TData,
|
|
24
|
+
TValue,
|
|
25
|
+
> extends HTMLAttributes<HTMLDivElement> {
|
|
26
|
+
column: Column<TData, TValue>
|
|
27
|
+
title?: string
|
|
28
|
+
icon?: ReactNode
|
|
29
|
+
pinnable?: boolean
|
|
30
|
+
filter?: ReactNode
|
|
31
|
+
visibility?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function DataGridColumnHeaderInner<TData, TValue>({
|
|
35
|
+
column,
|
|
36
|
+
title = "",
|
|
37
|
+
icon,
|
|
38
|
+
className,
|
|
39
|
+
filter,
|
|
40
|
+
visibility = false,
|
|
41
|
+
}: DataGridColumnHeaderProps<TData, TValue>) {
|
|
42
|
+
const { isLoading, table, props, recordCount } = useDataGrid()
|
|
43
|
+
|
|
44
|
+
const columnOrder = table.getState().columnOrder
|
|
45
|
+
const columnVisibilityKey = JSON.stringify(table.getState().columnVisibility)
|
|
46
|
+
const isSorted = column.getIsSorted()
|
|
47
|
+
const isPinned = column.getIsPinned()
|
|
48
|
+
const canSort = column.getCanSort()
|
|
49
|
+
const canPin = column.getCanPin()
|
|
50
|
+
const canResize = column.getCanResize()
|
|
51
|
+
|
|
52
|
+
const columnIndex = columnOrder.indexOf(column.id)
|
|
53
|
+
const canMoveLeft = columnIndex > 0
|
|
54
|
+
const canMoveRight = columnIndex < columnOrder.length - 1
|
|
55
|
+
|
|
56
|
+
const handleSort = () => {
|
|
57
|
+
if (isSorted === "asc") {
|
|
58
|
+
column.toggleSorting(true)
|
|
59
|
+
} else if (isSorted === "desc") {
|
|
60
|
+
column.clearSorting()
|
|
61
|
+
} else {
|
|
62
|
+
column.toggleSorting(false)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const headerLabelClassName = cn(
|
|
67
|
+
"text-secondary-foreground/80 inline-flex h-full items-center gap-1.5 font-normal [&_svg]:opacity-60 text-[0.8125rem] leading-[calc(1.125/0.8125)] [&_svg]:size-3.5",
|
|
68
|
+
className
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const headerButtonClassName = cn(
|
|
72
|
+
"text-secondary-foreground/80 hover:bg-secondary data-[state=open]:bg-secondary hover:text-foreground data-[state=open]:text-foreground px-0 has-[>svg]:px-0 justify-start font-normal h-6 rounded-lg",
|
|
73
|
+
className
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const sortIcon =
|
|
77
|
+
canSort &&
|
|
78
|
+
(isSorted === "desc" ? (
|
|
79
|
+
<ArrowDownIcon className="size-3.25" />
|
|
80
|
+
) : isSorted === "asc" ? (
|
|
81
|
+
<ArrowUpIcon className="size-3.25" />
|
|
82
|
+
) : (
|
|
83
|
+
<ChevronsUpDownIcon className="mt-px size-3.25" />
|
|
84
|
+
))
|
|
85
|
+
|
|
86
|
+
const hasControls =
|
|
87
|
+
props.tableLayout?.columnsMovable ||
|
|
88
|
+
(props.tableLayout?.columnsVisibility && visibility) ||
|
|
89
|
+
(props.tableLayout?.columnsPinnable && canPin) ||
|
|
90
|
+
filter
|
|
91
|
+
|
|
92
|
+
const menuItems = useMemo(() => {
|
|
93
|
+
const items: ReactNode[] = []
|
|
94
|
+
let hasPreviousSection = false
|
|
95
|
+
|
|
96
|
+
// Filter section
|
|
97
|
+
if (filter) {
|
|
98
|
+
items.push(
|
|
99
|
+
<DropdownMenuGroup key="group-filter">
|
|
100
|
+
<DropdownMenuLabel key="filter">{filter}</DropdownMenuLabel>
|
|
101
|
+
</DropdownMenuGroup>
|
|
102
|
+
)
|
|
103
|
+
hasPreviousSection = true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Sort section
|
|
107
|
+
if (canSort) {
|
|
108
|
+
if (hasPreviousSection) {
|
|
109
|
+
items.push(<DropdownMenuSeparator key="sep-sort" />)
|
|
110
|
+
}
|
|
111
|
+
items.push(
|
|
112
|
+
<DropdownMenuItem
|
|
113
|
+
key="sort-asc"
|
|
114
|
+
onClick={() => {
|
|
115
|
+
if (isSorted === "asc") {
|
|
116
|
+
column.clearSorting()
|
|
117
|
+
} else {
|
|
118
|
+
column.toggleSorting(false)
|
|
119
|
+
}
|
|
120
|
+
}}
|
|
121
|
+
disabled={!canSort}
|
|
122
|
+
>
|
|
123
|
+
<ArrowUpIcon className="size-3.5!" />
|
|
124
|
+
<span className="grow">Asc</span>
|
|
125
|
+
{isSorted === "asc" && (
|
|
126
|
+
<CheckIcon className="text-primary size-4 opacity-100!" />
|
|
127
|
+
)}
|
|
128
|
+
</DropdownMenuItem>,
|
|
129
|
+
<DropdownMenuItem
|
|
130
|
+
key="sort-desc"
|
|
131
|
+
onClick={() => {
|
|
132
|
+
if (isSorted === "desc") {
|
|
133
|
+
column.clearSorting()
|
|
134
|
+
} else {
|
|
135
|
+
column.toggleSorting(true)
|
|
136
|
+
}
|
|
137
|
+
}}
|
|
138
|
+
disabled={!canSort}
|
|
139
|
+
>
|
|
140
|
+
<ArrowDownIcon className="size-3.5!" />
|
|
141
|
+
<span className="grow">Desc</span>
|
|
142
|
+
{isSorted === "desc" && (
|
|
143
|
+
<CheckIcon className="text-primary size-4 opacity-100!" />
|
|
144
|
+
)}
|
|
145
|
+
</DropdownMenuItem>
|
|
146
|
+
)
|
|
147
|
+
hasPreviousSection = true
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Pin section
|
|
151
|
+
if (props.tableLayout?.columnsPinnable && canPin) {
|
|
152
|
+
if (hasPreviousSection) {
|
|
153
|
+
items.push(<DropdownMenuSeparator key="sep-pin" />)
|
|
154
|
+
}
|
|
155
|
+
items.push(
|
|
156
|
+
<DropdownMenuItem
|
|
157
|
+
key="pin-left"
|
|
158
|
+
onClick={() => column.pin(isPinned === "left" ? false : "left")}
|
|
159
|
+
>
|
|
160
|
+
<ArrowLeftToLineIcon className="size-3.5!" aria-hidden="true" />
|
|
161
|
+
<span className="grow">Pin to left</span>
|
|
162
|
+
{isPinned === "left" && (
|
|
163
|
+
<CheckIcon className="text-primary size-4 opacity-100!" />
|
|
164
|
+
)}
|
|
165
|
+
</DropdownMenuItem>,
|
|
166
|
+
<DropdownMenuItem
|
|
167
|
+
key="pin-right"
|
|
168
|
+
onClick={() => column.pin(isPinned === "right" ? false : "right")}
|
|
169
|
+
>
|
|
170
|
+
<ArrowRightToLineIcon className="size-3.5!" aria-hidden="true" />
|
|
171
|
+
<span className="grow">Pin to right</span>
|
|
172
|
+
{isPinned === "right" && (
|
|
173
|
+
<CheckIcon className="text-primary size-4 opacity-100!" />
|
|
174
|
+
)}
|
|
175
|
+
</DropdownMenuItem>
|
|
176
|
+
)
|
|
177
|
+
hasPreviousSection = true
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Move section
|
|
181
|
+
if (props.tableLayout?.columnsMovable) {
|
|
182
|
+
if (hasPreviousSection) {
|
|
183
|
+
items.push(<DropdownMenuSeparator key="sep-move" />)
|
|
184
|
+
}
|
|
185
|
+
items.push(
|
|
186
|
+
<DropdownMenuItem
|
|
187
|
+
key="move-left"
|
|
188
|
+
onClick={() => {
|
|
189
|
+
if (columnIndex > 0) {
|
|
190
|
+
const newOrder = [...columnOrder]
|
|
191
|
+
const [movedColumn] = newOrder.splice(columnIndex, 1)
|
|
192
|
+
newOrder.splice(columnIndex - 1, 0, movedColumn)
|
|
193
|
+
table.setColumnOrder(newOrder)
|
|
194
|
+
}
|
|
195
|
+
}}
|
|
196
|
+
disabled={!canMoveLeft || isPinned !== false}
|
|
197
|
+
>
|
|
198
|
+
<ArrowLeftIcon className="size-3.5!" aria-hidden="true" />
|
|
199
|
+
<span>Move to Left</span>
|
|
200
|
+
</DropdownMenuItem>,
|
|
201
|
+
<DropdownMenuItem
|
|
202
|
+
key="move-right"
|
|
203
|
+
onClick={() => {
|
|
204
|
+
if (columnIndex < columnOrder.length - 1) {
|
|
205
|
+
const newOrder = [...columnOrder]
|
|
206
|
+
const [movedColumn] = newOrder.splice(columnIndex, 1)
|
|
207
|
+
newOrder.splice(columnIndex + 1, 0, movedColumn)
|
|
208
|
+
table.setColumnOrder(newOrder)
|
|
209
|
+
}
|
|
210
|
+
}}
|
|
211
|
+
disabled={!canMoveRight || isPinned !== false}
|
|
212
|
+
>
|
|
213
|
+
<ArrowRightIcon className="size-3.5!" aria-hidden="true" />
|
|
214
|
+
<span>Move to Right</span>
|
|
215
|
+
</DropdownMenuItem>
|
|
216
|
+
)
|
|
217
|
+
hasPreviousSection = true
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Visibility section
|
|
221
|
+
if (props.tableLayout?.columnsVisibility && visibility) {
|
|
222
|
+
if (hasPreviousSection) {
|
|
223
|
+
items.push(<DropdownMenuSeparator key="sep-visibility" />)
|
|
224
|
+
}
|
|
225
|
+
items.push(
|
|
226
|
+
<DropdownMenuSub key="visibility">
|
|
227
|
+
<DropdownMenuSubTrigger>
|
|
228
|
+
<Settings2Icon className="size-3.5!" />
|
|
229
|
+
<span>Columns</span>
|
|
230
|
+
</DropdownMenuSubTrigger>
|
|
231
|
+
<DropdownMenuSubContent>
|
|
232
|
+
{table
|
|
233
|
+
.getAllColumns()
|
|
234
|
+
.filter(
|
|
235
|
+
(col) =>
|
|
236
|
+
typeof col.accessorFn !== "undefined" && col.getCanHide()
|
|
237
|
+
)
|
|
238
|
+
.map((col) => (
|
|
239
|
+
<DropdownMenuCheckboxItem
|
|
240
|
+
key={col.id}
|
|
241
|
+
checked={col.getIsVisible()}
|
|
242
|
+
onSelect={(event) => event.preventDefault()}
|
|
243
|
+
onCheckedChange={(value) => col.toggleVisibility(!!value)}
|
|
244
|
+
className="capitalize"
|
|
245
|
+
>
|
|
246
|
+
{col.columnDef.meta?.headerTitle || col.id}
|
|
247
|
+
</DropdownMenuCheckboxItem>
|
|
248
|
+
))}
|
|
249
|
+
</DropdownMenuSubContent>
|
|
250
|
+
</DropdownMenuSub>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return items
|
|
255
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
256
|
+
}, [
|
|
257
|
+
filter,
|
|
258
|
+
canSort,
|
|
259
|
+
isSorted,
|
|
260
|
+
column,
|
|
261
|
+
props.tableLayout?.columnsPinnable,
|
|
262
|
+
props.tableLayout?.columnsMovable,
|
|
263
|
+
props.tableLayout?.columnsVisibility,
|
|
264
|
+
canPin,
|
|
265
|
+
isPinned,
|
|
266
|
+
canMoveLeft,
|
|
267
|
+
canMoveRight,
|
|
268
|
+
visibility,
|
|
269
|
+
table,
|
|
270
|
+
columnIndex,
|
|
271
|
+
columnOrder,
|
|
272
|
+
columnVisibilityKey, // Needed to update checkbox states when visibility changes
|
|
273
|
+
])
|
|
274
|
+
|
|
275
|
+
if (hasControls) {
|
|
276
|
+
return (
|
|
277
|
+
<div className="flex h-full items-center justify-between gap-1.5">
|
|
278
|
+
<DropdownMenu>
|
|
279
|
+
<DropdownMenuTrigger asChild>
|
|
280
|
+
<Button
|
|
281
|
+
variant="ghost"
|
|
282
|
+
className={headerButtonClassName}
|
|
283
|
+
disabled={isLoading || recordCount === 0}
|
|
284
|
+
>
|
|
285
|
+
{icon && icon}
|
|
286
|
+
{title}
|
|
287
|
+
{sortIcon}
|
|
288
|
+
</Button>
|
|
289
|
+
</DropdownMenuTrigger>
|
|
290
|
+
<DropdownMenuContent className="w-40" align="start">
|
|
291
|
+
{menuItems}
|
|
292
|
+
</DropdownMenuContent>
|
|
293
|
+
</DropdownMenu>
|
|
294
|
+
{props.tableLayout?.columnsPinnable && canPin && isPinned && (
|
|
295
|
+
<Button
|
|
296
|
+
size="icon-sm"
|
|
297
|
+
variant="ghost"
|
|
298
|
+
className="-me-1 size-7 rounded-md"
|
|
299
|
+
onClick={() => column.pin(false)}
|
|
300
|
+
aria-label={`Unpin ${title} column`}
|
|
301
|
+
title={`Unpin ${title} column`}
|
|
302
|
+
>
|
|
303
|
+
<PinOffIcon className="size-3.5! opacity-50!" aria-hidden="true" />
|
|
304
|
+
</Button>
|
|
305
|
+
)}
|
|
306
|
+
</div>
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (canSort || (props.tableLayout?.columnsResizable && canResize)) {
|
|
311
|
+
return (
|
|
312
|
+
<div className="flex h-full items-center">
|
|
313
|
+
<Button
|
|
314
|
+
variant="ghost"
|
|
315
|
+
className={headerButtonClassName}
|
|
316
|
+
disabled={isLoading || recordCount === 0}
|
|
317
|
+
onClick={handleSort}
|
|
318
|
+
>
|
|
319
|
+
{icon && icon}
|
|
320
|
+
{title}
|
|
321
|
+
{sortIcon}
|
|
322
|
+
</Button>
|
|
323
|
+
</div>
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return (
|
|
328
|
+
<div className={headerLabelClassName}>
|
|
329
|
+
{icon && icon}
|
|
330
|
+
{title}
|
|
331
|
+
</div>
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const DataGridColumnHeader = memo(
|
|
336
|
+
DataGridColumnHeaderInner
|
|
337
|
+
) as typeof DataGridColumnHeaderInner
|
|
338
|
+
|
|
339
|
+
export { DataGridColumnHeader, type DataGridColumnHeaderProps }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { ReactElement } from "react"
|
|
4
|
+
import { Table } from "@tanstack/react-table"
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownMenuCheckboxItem,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuGroup,
|
|
11
|
+
DropdownMenuLabel,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from "@/components/ui/dropdown-menu"
|
|
14
|
+
|
|
15
|
+
function DataGridColumnVisibility<TData>({
|
|
16
|
+
table,
|
|
17
|
+
trigger,
|
|
18
|
+
}: {
|
|
19
|
+
table: Table<TData>
|
|
20
|
+
trigger: ReactElement<Record<string, unknown>>
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<DropdownMenu>
|
|
24
|
+
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
|
25
|
+
<DropdownMenuContent align="end" className="min-w-[150px]">
|
|
26
|
+
<DropdownMenuGroup>
|
|
27
|
+
<DropdownMenuLabel className="font-medium">
|
|
28
|
+
Toggle Columns
|
|
29
|
+
</DropdownMenuLabel>
|
|
30
|
+
{table
|
|
31
|
+
.getAllColumns()
|
|
32
|
+
.filter(
|
|
33
|
+
(column) =>
|
|
34
|
+
typeof column.accessorFn !== "undefined" && column.getCanHide()
|
|
35
|
+
)
|
|
36
|
+
.map((column) => {
|
|
37
|
+
return (
|
|
38
|
+
<DropdownMenuCheckboxItem
|
|
39
|
+
key={column.id}
|
|
40
|
+
className="capitalize"
|
|
41
|
+
checked={column.getIsVisible()}
|
|
42
|
+
onSelect={(event) => event.preventDefault()}
|
|
43
|
+
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
|
44
|
+
>
|
|
45
|
+
{column.columnDef.meta?.headerTitle || column.id}
|
|
46
|
+
</DropdownMenuCheckboxItem>
|
|
47
|
+
)
|
|
48
|
+
})}
|
|
49
|
+
</DropdownMenuGroup>
|
|
50
|
+
</DropdownMenuContent>
|
|
51
|
+
</DropdownMenu>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { DataGridColumnVisibility }
|