@wakastellar/ui 2.0.0 → 2.1.1
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/README.md +71 -8
- package/dist/cli/commands/add.d.ts +7 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/search.d.ts +1 -0
- package/dist/cli/index.cjs +6014 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/utils/config.d.ts +29 -0
- package/dist/cli/utils/logger.d.ts +20 -0
- package/dist/cli/utils/registry.d.ts +23 -0
- package/package.json +14 -3
- package/src/blocks/activity-timeline/index.tsx +586 -0
- package/src/blocks/calendar-view/index.tsx +756 -0
- package/src/blocks/chat/index.tsx +1018 -0
- package/src/blocks/chat/widget.tsx +504 -0
- package/src/blocks/dashboard/index.tsx +522 -0
- package/src/blocks/empty-states/index.tsx +452 -0
- package/src/blocks/error-pages/index.tsx +426 -0
- package/src/blocks/faq/index.tsx +479 -0
- package/src/blocks/file-manager/index.tsx +890 -0
- package/src/blocks/footer/index.tsx +133 -0
- package/src/blocks/header/index.tsx +357 -0
- package/src/blocks/headtab/index.tsx +139 -0
- package/src/blocks/i18n-editor/index.tsx +1016 -0
- package/src/blocks/index.ts +80 -0
- package/src/blocks/kanban-board/index.tsx +779 -0
- package/src/blocks/landing/index.tsx +677 -0
- package/src/blocks/language-selector/index.tsx +88 -0
- package/src/blocks/layout/index.tsx +159 -0
- package/src/blocks/login/index.tsx +339 -0
- package/src/blocks/login/types.ts +131 -0
- package/src/blocks/pricing/index.tsx +564 -0
- package/src/blocks/profile/index.tsx +746 -0
- package/src/blocks/settings/index.tsx +558 -0
- package/src/blocks/sidebar/index.tsx +713 -0
- package/src/blocks/theme-creator-block/index.tsx +835 -0
- package/src/blocks/user-management/index.tsx +1037 -0
- package/src/blocks/wizard/index.tsx +719 -0
- package/src/components/DataTable/DataTable.tsx +406 -0
- package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
- package/src/components/DataTable/DataTableBody.tsx +216 -0
- package/src/components/DataTable/DataTableCell.tsx +172 -0
- package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
- package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
- package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
- package/src/components/DataTable/DataTableEditCell.tsx +279 -0
- package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
- package/src/components/DataTable/DataTableFilters.tsx +535 -0
- package/src/components/DataTable/DataTableGrouping.tsx +147 -0
- package/src/components/DataTable/DataTableHeader.tsx +172 -0
- package/src/components/DataTable/DataTablePagination.tsx +125 -0
- package/src/components/DataTable/DataTableSelection.tsx +269 -0
- package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
- package/src/components/DataTable/DataTableToolbar.tsx +262 -0
- package/src/components/DataTable/README.md +446 -0
- package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
- package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
- package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
- package/src/components/DataTable/examples/EditExample.tsx +166 -0
- package/src/components/DataTable/formatters/index.ts +335 -0
- package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
- package/src/components/DataTable/hooks/useDataTable.ts +145 -0
- package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
- package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
- package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
- package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
- package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
- package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
- package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
- package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
- package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
- package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
- package/src/components/DataTable/index.ts +81 -0
- package/src/components/DataTable/services/IndexedDBService.ts +504 -0
- package/src/components/DataTable/templates/index.tsx +803 -0
- package/src/components/DataTable/types.ts +504 -0
- package/src/components/DataTable/utils.ts +164 -0
- package/src/components/DataTable/workers/exportWorker.ts +213 -0
- package/src/components/accordion/index.tsx +61 -0
- package/src/components/alert/index.tsx +61 -0
- package/src/components/alert-dialog/index.tsx +146 -0
- package/src/components/aspect-ratio/index.tsx +12 -0
- package/src/components/avatar/index.tsx +54 -0
- package/src/components/badge/Badge.stories.tsx +64 -0
- package/src/components/badge/index.tsx +38 -0
- package/src/components/button/Button.stories.tsx +173 -0
- package/src/components/button/index.tsx +56 -0
- package/src/components/calendar/index.tsx +73 -0
- package/src/components/card/index.tsx +78 -0
- package/src/components/checkbox/index.tsx +34 -0
- package/src/components/code/index.tsx +229 -0
- package/src/components/collapsible/index.tsx +16 -0
- package/src/components/command/index.tsx +162 -0
- package/src/components/context-menu/index.tsx +204 -0
- package/src/components/dialog/index.tsx +126 -0
- package/src/components/dropdown-menu/index.tsx +204 -0
- package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
- package/src/components/error-boundary/index.ts +7 -0
- package/src/components/form/index.tsx +183 -0
- package/src/components/hover-card/index.tsx +33 -0
- package/src/components/index.ts +368 -0
- package/src/components/input/Input.stories.tsx +100 -0
- package/src/components/input/index.tsx +27 -0
- package/src/components/input-otp/index.tsx +277 -0
- package/src/components/label/index.tsx +30 -0
- package/src/components/language-selector/index.tsx +341 -0
- package/src/components/menubar/index.tsx +240 -0
- package/src/components/navigation-menu/index.tsx +134 -0
- package/src/components/popover/index.tsx +35 -0
- package/src/components/progress/index.tsx +32 -0
- package/src/components/radio-group/index.tsx +48 -0
- package/src/components/scroll-area/index.tsx +52 -0
- package/src/components/select/index.tsx +164 -0
- package/src/components/separator/index.tsx +35 -0
- package/src/components/sheet/index.tsx +147 -0
- package/src/components/skeleton/index.tsx +22 -0
- package/src/components/slider/index.tsx +32 -0
- package/src/components/switch/index.tsx +33 -0
- package/src/components/table/index.tsx +117 -0
- package/src/components/tabs/index.tsx +59 -0
- package/src/components/textarea/index.tsx +30 -0
- package/src/components/theme-selector/index.tsx +327 -0
- package/src/components/toast/index.tsx +133 -0
- package/src/components/toaster/index.tsx +34 -0
- package/src/components/toggle/index.tsx +49 -0
- package/src/components/tooltip/index.tsx +34 -0
- package/src/components/typography/index.tsx +276 -0
- package/src/components/waka-3d-pie-chart/index.tsx +486 -0
- package/src/components/waka-achievement-unlock/index.tsx +716 -0
- package/src/components/waka-activity-feed/index.tsx +686 -0
- package/src/components/waka-address-autocomplete/index.tsx +1202 -0
- package/src/components/waka-admincrumb/index.tsx +349 -0
- package/src/components/waka-alert-stack/index.tsx +827 -0
- package/src/components/waka-allocation-matrix/index.tsx +1278 -0
- package/src/components/waka-approval-chain/index.tsx +766 -0
- package/src/components/waka-audit-log/index.tsx +1475 -0
- package/src/components/waka-autocomplete/index.tsx +358 -0
- package/src/components/waka-badge-showcase/index.tsx +704 -0
- package/src/components/waka-barcode/index.tsx +260 -0
- package/src/components/waka-biometric-prompt/index.tsx +765 -0
- package/src/components/waka-bottom-sheet/index.tsx +495 -0
- package/src/components/waka-breadcrumb/index.tsx +376 -0
- package/src/components/waka-breadcrumb-path/index.tsx +513 -0
- package/src/components/waka-budget-burn/index.tsx +1234 -0
- package/src/components/waka-capacity-planner/index.tsx +1107 -0
- package/src/components/waka-carousel/index.tsx +893 -0
- package/src/components/waka-cart-summary/index.tsx +1055 -0
- package/src/components/waka-challenge-timer/index.tsx +1044 -0
- package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
- package/src/components/waka-charts/WakaBarChart.tsx +222 -0
- package/src/components/waka-charts/WakaChart.tsx +124 -0
- package/src/components/waka-charts/WakaLineChart.tsx +219 -0
- package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
- package/src/components/waka-charts/WakaPieChart.tsx +214 -0
- package/src/components/waka-charts/WakaSparkline.tsx +229 -0
- package/src/components/waka-charts/dataTableHelpers.ts +109 -0
- package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
- package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
- package/src/components/waka-charts/index.ts +90 -0
- package/src/components/waka-charts/types.ts +330 -0
- package/src/components/waka-chat-bubble/index.tsx +1060 -0
- package/src/components/waka-checklist/index.tsx +1067 -0
- package/src/components/waka-checkout-stepper/index.tsx +976 -0
- package/src/components/waka-cohort-table/index.tsx +1011 -0
- package/src/components/waka-color-picker/index.tsx +447 -0
- package/src/components/waka-combo-counter/index.tsx +864 -0
- package/src/components/waka-combobox/index.tsx +497 -0
- package/src/components/waka-command-bar/index.tsx +403 -0
- package/src/components/waka-compare-period/index.tsx +1230 -0
- package/src/components/waka-connection-matrix/index.tsx +1053 -0
- package/src/components/waka-contribution-graph/index.tsx +552 -0
- package/src/components/waka-cost-breakdown/index.tsx +1065 -0
- package/src/components/waka-coupon-input/index.tsx +592 -0
- package/src/components/waka-credit-card-input/index.tsx +982 -0
- package/src/components/waka-daily-reward/index.tsx +762 -0
- package/src/components/waka-date-range-picker/index.tsx +378 -0
- package/src/components/waka-datetime-picker/index.tsx +793 -0
- package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
- package/src/components/waka-deployment-lane/index.tsx +673 -0
- package/src/components/waka-device-trust/index.tsx +1259 -0
- package/src/components/waka-dock/index.tsx +285 -0
- package/src/components/waka-drawer/index.tsx +319 -0
- package/src/components/waka-empty-state/index.tsx +545 -0
- package/src/components/waka-error-shake/index.tsx +398 -0
- package/src/components/waka-feature-announcement/index.tsx +991 -0
- package/src/components/waka-file-upload/index.tsx +437 -0
- package/src/components/waka-floating-nav/index.tsx +413 -0
- package/src/components/waka-flow-diagram/index.tsx +508 -0
- package/src/components/waka-funnel-chart/index.tsx +823 -0
- package/src/components/waka-glow-card/index.tsx +246 -0
- package/src/components/waka-goal-progress/index.tsx +1025 -0
- package/src/components/waka-haptic-button/index.tsx +388 -0
- package/src/components/waka-health-pulse/index.tsx +451 -0
- package/src/components/waka-heatmap/index.tsx +1026 -0
- package/src/components/waka-hotspot/index.tsx +682 -0
- package/src/components/waka-image/index.tsx +373 -0
- package/src/components/waka-incident-timeline/index.tsx +686 -0
- package/src/components/waka-invoice-preview/index.tsx +829 -0
- package/src/components/waka-kanban/index.tsx +646 -0
- package/src/components/waka-kpi-dashboard/index.tsx +755 -0
- package/src/components/waka-leaderboard/index.tsx +746 -0
- package/src/components/waka-level-progress/index.tsx +665 -0
- package/src/components/waka-liquid-button/index.tsx +520 -0
- package/src/components/waka-loading-orbit/index.tsx +478 -0
- package/src/components/waka-loot-box/index.tsx +1091 -0
- package/src/components/waka-magic-link/index.tsx +321 -0
- package/src/components/waka-magnetic-button/index.tsx +567 -0
- package/src/components/waka-mention-input/index.tsx +953 -0
- package/src/components/waka-metric-sparkline/index.tsx +627 -0
- package/src/components/waka-milestone-road/index.tsx +1064 -0
- package/src/components/waka-modal/index.tsx +374 -0
- package/src/components/waka-morph-button/index.tsx +495 -0
- package/src/components/waka-network-topology/index.tsx +801 -0
- package/src/components/waka-notifications/index.tsx +414 -0
- package/src/components/waka-number-input/index.tsx +373 -0
- package/src/components/waka-orbital-menu/index.tsx +445 -0
- package/src/components/waka-order-tracker/index.tsx +1041 -0
- package/src/components/waka-pagination/index.tsx +393 -0
- package/src/components/waka-password-strength/index.tsx +824 -0
- package/src/components/waka-payment-method-picker/index.tsx +715 -0
- package/src/components/waka-permission-matrix/index.tsx +1302 -0
- package/src/components/waka-phone-input/index.tsx +801 -0
- package/src/components/waka-pipeline-view/index.tsx +604 -0
- package/src/components/waka-player-card/index.tsx +691 -0
- package/src/components/waka-points-popup/index.tsx +366 -0
- package/src/components/waka-power-up/index.tsx +1155 -0
- package/src/components/waka-presence-indicator/index.tsx +1181 -0
- package/src/components/waka-pricing-table/index.tsx +755 -0
- package/src/components/waka-product-card/index.tsx +786 -0
- package/src/components/waka-progress-onboarding/index.tsx +878 -0
- package/src/components/waka-pull-to-refresh/index.tsx +451 -0
- package/src/components/waka-qrcode/index.tsx +232 -0
- package/src/components/waka-quest-card/index.tsx +1275 -0
- package/src/components/waka-quota-bar/index.tsx +693 -0
- package/src/components/waka-radar-score/index.tsx +512 -0
- package/src/components/waka-rank-badge/index.tsx +813 -0
- package/src/components/waka-rating-input/index.tsx +560 -0
- package/src/components/waka-reaction-picker/index.tsx +1062 -0
- package/src/components/waka-region-map/index.tsx +730 -0
- package/src/components/waka-resource-gauge/index.tsx +654 -0
- package/src/components/waka-resource-pool/index.tsx +1035 -0
- package/src/components/waka-rich-text-editor/index.tsx +594 -0
- package/src/components/waka-rollback-slider/index.tsx +891 -0
- package/src/components/waka-sankey-diagram/index.tsx +1032 -0
- package/src/components/waka-schedule-picker/index.tsx +1060 -0
- package/src/components/waka-scratch-card/index.tsx +914 -0
- package/src/components/waka-season-pass/index.tsx +886 -0
- package/src/components/waka-security-score/index.tsx +1126 -0
- package/src/components/waka-segmented-control/index.tsx +238 -0
- package/src/components/waka-server-rack/index.tsx +764 -0
- package/src/components/waka-session-manager/index.tsx +815 -0
- package/src/components/waka-signature-pad/index.tsx +744 -0
- package/src/components/waka-skeleton-wave/index.tsx +454 -0
- package/src/components/waka-skill-tree/index.tsx +1031 -0
- package/src/components/waka-sla-tracker/index.tsx +798 -0
- package/src/components/waka-slider-range/index.tsx +765 -0
- package/src/components/waka-spin-wheel/index.tsx +671 -0
- package/src/components/waka-spinner/index.tsx +284 -0
- package/src/components/waka-spotlight/index.tsx +410 -0
- package/src/components/waka-stat/index.tsx +428 -0
- package/src/components/waka-stats-hexagon/index.tsx +824 -0
- package/src/components/waka-status-matrix/index.tsx +565 -0
- package/src/components/waka-stepper/index.tsx +489 -0
- package/src/components/waka-streak-counter/index.tsx +334 -0
- package/src/components/waka-success-explosion/index.tsx +453 -0
- package/src/components/waka-swipe-card/index.tsx +574 -0
- package/src/components/waka-tabs-morph/index.tsx +509 -0
- package/src/components/waka-tag-input/index.tsx +877 -0
- package/src/components/waka-team-banner/index.tsx +1183 -0
- package/src/components/waka-terminal-output/index.tsx +836 -0
- package/src/components/waka-theme-creator/index.tsx +762 -0
- package/src/components/waka-theme-manager/index.tsx +654 -0
- package/src/components/waka-thread-view/index.tsx +874 -0
- package/src/components/waka-tilt-card/index.tsx +250 -0
- package/src/components/waka-time-picker/index.tsx +479 -0
- package/src/components/waka-timeline/index.tsx +385 -0
- package/src/components/waka-tooltip-tour/index.tsx +855 -0
- package/src/components/waka-tour-guide/index.tsx +920 -0
- package/src/components/waka-tournament-bracket/index.tsx +1276 -0
- package/src/components/waka-tree/index.tsx +557 -0
- package/src/components/waka-treemap-chart/index.tsx +1031 -0
- package/src/components/waka-two-factor-setup/index.tsx +995 -0
- package/src/components/waka-typewriter/index.tsx +566 -0
- package/src/components/waka-typing-indicator/index.tsx +649 -0
- package/src/components/waka-versus-card/index.tsx +1026 -0
- package/src/components/waka-video/index.tsx +557 -0
- package/src/components/waka-video-call/index.tsx +1087 -0
- package/src/components/waka-virtual-list/index.tsx +327 -0
- package/src/components/waka-voice-message/index.tsx +1019 -0
- package/src/components/waka-welcome-modal/index.tsx +790 -0
- package/src/components/waka-xp-bar/index.tsx +799 -0
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Templates de colonnes prédéfinis pour le DataTable
|
|
5
|
+
* @module templates
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as React from "react"
|
|
9
|
+
import { Check, X, Copy, ExternalLink, Mail, Phone, MoreHorizontal, Star, StarHalf } from "lucide-react"
|
|
10
|
+
import { Badge } from "../../badge"
|
|
11
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../../avatar"
|
|
12
|
+
import { Progress } from "../../progress"
|
|
13
|
+
import { Button } from "../../button"
|
|
14
|
+
import {
|
|
15
|
+
DropdownMenu,
|
|
16
|
+
DropdownMenuContent,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuTrigger,
|
|
19
|
+
} from "../../dropdown-menu"
|
|
20
|
+
import {
|
|
21
|
+
AlertDialog,
|
|
22
|
+
AlertDialogAction,
|
|
23
|
+
AlertDialogCancel,
|
|
24
|
+
AlertDialogContent,
|
|
25
|
+
AlertDialogDescription,
|
|
26
|
+
AlertDialogFooter,
|
|
27
|
+
AlertDialogHeader,
|
|
28
|
+
AlertDialogTitle,
|
|
29
|
+
} from "../../alert-dialog"
|
|
30
|
+
import { cn } from "../../../utils/cn"
|
|
31
|
+
import { formatters } from "../formatters"
|
|
32
|
+
import type { ColumnTemplate, ColumnTemplateOptions, ColumnTemplateAction } from "../types"
|
|
33
|
+
|
|
34
|
+
// ============================================
|
|
35
|
+
// HELPER COMPONENTS
|
|
36
|
+
// ============================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Composant pour afficher une valeur vide
|
|
40
|
+
*/
|
|
41
|
+
const EmptyValue = ({ className }: { className?: string }) => (
|
|
42
|
+
<span className={cn("text-muted-foreground", className)}>—</span>
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Composant pour copier une valeur
|
|
47
|
+
*/
|
|
48
|
+
const CopyButton = ({ value }: { value: string }) => {
|
|
49
|
+
const [copied, setCopied] = React.useState(false)
|
|
50
|
+
|
|
51
|
+
const handleCopy = async (e: React.MouseEvent) => {
|
|
52
|
+
e.stopPropagation()
|
|
53
|
+
try {
|
|
54
|
+
await navigator.clipboard.writeText(value)
|
|
55
|
+
setCopied(true)
|
|
56
|
+
setTimeout(() => setCopied(false), 2000)
|
|
57
|
+
} catch {
|
|
58
|
+
// Fallback silencieux
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<button
|
|
64
|
+
onClick={handleCopy}
|
|
65
|
+
className="ml-1 p-1 rounded hover:bg-muted transition-colors"
|
|
66
|
+
title={copied ? "Copié !" : "Copier"}
|
|
67
|
+
>
|
|
68
|
+
{copied ? (
|
|
69
|
+
<Check className="h-3 w-3 text-green-500" />
|
|
70
|
+
) : (
|
|
71
|
+
<Copy className="h-3 w-3 text-muted-foreground" />
|
|
72
|
+
)}
|
|
73
|
+
</button>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Composant d'action avec confirmation
|
|
79
|
+
*/
|
|
80
|
+
function ActionWithConfirm<TData>({
|
|
81
|
+
action,
|
|
82
|
+
row,
|
|
83
|
+
}: {
|
|
84
|
+
action: ColumnTemplateAction<TData>
|
|
85
|
+
row: TData
|
|
86
|
+
}) {
|
|
87
|
+
const [open, setOpen] = React.useState(false)
|
|
88
|
+
|
|
89
|
+
const handleClick = () => {
|
|
90
|
+
if (action.requiresConfirm) {
|
|
91
|
+
setOpen(true)
|
|
92
|
+
} else {
|
|
93
|
+
action.onClick(row)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const handleConfirm = () => {
|
|
98
|
+
action.onClick(row)
|
|
99
|
+
setOpen(false)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isDisabled = typeof action.disabled === "function"
|
|
103
|
+
? action.disabled(row)
|
|
104
|
+
: action.disabled
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<>
|
|
108
|
+
<DropdownMenuItem
|
|
109
|
+
onClick={handleClick}
|
|
110
|
+
disabled={isDisabled}
|
|
111
|
+
className={action.variant === "destructive" ? "text-destructive focus:text-destructive" : ""}
|
|
112
|
+
>
|
|
113
|
+
{action.icon && <span className="mr-2">{action.icon}</span>}
|
|
114
|
+
{action.label}
|
|
115
|
+
</DropdownMenuItem>
|
|
116
|
+
|
|
117
|
+
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
118
|
+
<AlertDialogContent>
|
|
119
|
+
<AlertDialogHeader>
|
|
120
|
+
<AlertDialogTitle>
|
|
121
|
+
{action.confirmTitle || "Confirmer l'action"}
|
|
122
|
+
</AlertDialogTitle>
|
|
123
|
+
<AlertDialogDescription>
|
|
124
|
+
{action.confirmMessage || "Êtes-vous sûr de vouloir effectuer cette action ?"}
|
|
125
|
+
</AlertDialogDescription>
|
|
126
|
+
</AlertDialogHeader>
|
|
127
|
+
<AlertDialogFooter>
|
|
128
|
+
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
|
129
|
+
<AlertDialogAction
|
|
130
|
+
onClick={handleConfirm}
|
|
131
|
+
className={action.variant === "destructive" ? "bg-destructive hover:bg-destructive/90" : ""}
|
|
132
|
+
>
|
|
133
|
+
Confirmer
|
|
134
|
+
</AlertDialogAction>
|
|
135
|
+
</AlertDialogFooter>
|
|
136
|
+
</AlertDialogContent>
|
|
137
|
+
</AlertDialog>
|
|
138
|
+
</>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ============================================
|
|
143
|
+
// COLUMN TEMPLATES
|
|
144
|
+
// ============================================
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Template Date
|
|
148
|
+
*/
|
|
149
|
+
export const dateTemplate: ColumnTemplate<unknown, unknown> = {
|
|
150
|
+
cell: (value, _row, options) => {
|
|
151
|
+
if (!value) return <EmptyValue />
|
|
152
|
+
const formatted = formatters.date(value, options?.format || "PP", options?.locale)
|
|
153
|
+
return formatted || <EmptyValue />
|
|
154
|
+
},
|
|
155
|
+
sortingFn: "datetime",
|
|
156
|
+
size: 120,
|
|
157
|
+
enableSorting: true,
|
|
158
|
+
enableFiltering: true,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Template DateTime
|
|
163
|
+
*/
|
|
164
|
+
export const datetimeTemplate: ColumnTemplate<unknown, unknown> = {
|
|
165
|
+
cell: (value, _row, options) => {
|
|
166
|
+
if (!value) return <EmptyValue />
|
|
167
|
+
const formatted = formatters.datetime(value, options?.format || "Pp", options?.locale)
|
|
168
|
+
return formatted || <EmptyValue />
|
|
169
|
+
},
|
|
170
|
+
sortingFn: "datetime",
|
|
171
|
+
size: 160,
|
|
172
|
+
enableSorting: true,
|
|
173
|
+
enableFiltering: true,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Template Time
|
|
178
|
+
*/
|
|
179
|
+
export const timeTemplate: ColumnTemplate<unknown, unknown> = {
|
|
180
|
+
cell: (value, _row, options) => {
|
|
181
|
+
if (!value) return <EmptyValue />
|
|
182
|
+
const formatted = formatters.time(value, options?.format || "HH:mm", options?.locale)
|
|
183
|
+
return formatted || <EmptyValue />
|
|
184
|
+
},
|
|
185
|
+
sortingFn: "datetime",
|
|
186
|
+
size: 80,
|
|
187
|
+
enableSorting: true,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Template Currency
|
|
192
|
+
*/
|
|
193
|
+
export const currencyTemplate: ColumnTemplate<unknown, unknown> = {
|
|
194
|
+
cell: (value, _row, options) => {
|
|
195
|
+
if (value == null || value === "") return <EmptyValue />
|
|
196
|
+
const formatted = formatters.currency(
|
|
197
|
+
value,
|
|
198
|
+
options?.currency || "EUR",
|
|
199
|
+
options?.locale || "fr-FR",
|
|
200
|
+
{
|
|
201
|
+
minimumFractionDigits: options?.minimumFractionDigits,
|
|
202
|
+
maximumFractionDigits: options?.maximumFractionDigits,
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
return <span className="font-mono tabular-nums">{formatted}</span>
|
|
206
|
+
},
|
|
207
|
+
sortingFn: "basic",
|
|
208
|
+
size: 120,
|
|
209
|
+
enableSorting: true,
|
|
210
|
+
meta: { align: "right" },
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Template Percentage
|
|
215
|
+
*/
|
|
216
|
+
export const percentageTemplate: ColumnTemplate<unknown, unknown> = {
|
|
217
|
+
cell: (value, _row, options) => {
|
|
218
|
+
if (value == null) return <EmptyValue />
|
|
219
|
+
const numValue = typeof value === "number" ? value : parseFloat(String(value))
|
|
220
|
+
if (isNaN(numValue)) return <EmptyValue />
|
|
221
|
+
|
|
222
|
+
const displayValue = options?.showValue !== false ? formatters.percentage(numValue) : null
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<div className="flex items-center gap-2 min-w-[80px]">
|
|
226
|
+
<Progress
|
|
227
|
+
value={Math.min(100, Math.max(0, numValue))}
|
|
228
|
+
className={cn("h-2 flex-1", options?.trackColor)}
|
|
229
|
+
style={options?.progressColor ? { ['--progress-color' as string]: options.progressColor } : undefined}
|
|
230
|
+
/>
|
|
231
|
+
{displayValue && <span className="text-sm tabular-nums w-10 text-right">{displayValue}</span>}
|
|
232
|
+
</div>
|
|
233
|
+
)
|
|
234
|
+
},
|
|
235
|
+
sortingFn: "basic",
|
|
236
|
+
size: 140,
|
|
237
|
+
enableSorting: true,
|
|
238
|
+
meta: { align: "right" },
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Template Number
|
|
243
|
+
*/
|
|
244
|
+
export const numberTemplate: ColumnTemplate<unknown, unknown> = {
|
|
245
|
+
cell: (value, _row, options) => {
|
|
246
|
+
if (value == null || value === "") return <EmptyValue />
|
|
247
|
+
const formatted = formatters.number(value, options?.locale || "fr-FR", {
|
|
248
|
+
notation: options?.notation,
|
|
249
|
+
minimumFractionDigits: options?.minimumFractionDigits,
|
|
250
|
+
maximumFractionDigits: options?.maximumFractionDigits,
|
|
251
|
+
})
|
|
252
|
+
return <span className="font-mono tabular-nums">{formatted}</span>
|
|
253
|
+
},
|
|
254
|
+
sortingFn: "basic",
|
|
255
|
+
size: 100,
|
|
256
|
+
enableSorting: true,
|
|
257
|
+
meta: { align: "right" },
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Template Status/Badge
|
|
262
|
+
*/
|
|
263
|
+
export const statusTemplate: ColumnTemplate<unknown, unknown> = {
|
|
264
|
+
cell: (value, _row, options) => {
|
|
265
|
+
if (!value) return <EmptyValue />
|
|
266
|
+
const strValue = String(value)
|
|
267
|
+
|
|
268
|
+
const colors = options?.statusColors || {}
|
|
269
|
+
const labels = options?.statusLabels || {}
|
|
270
|
+
const icons = options?.statusIcons || {}
|
|
271
|
+
|
|
272
|
+
const colorClass = colors[strValue] || "bg-gray-100 text-gray-800"
|
|
273
|
+
const label = labels[strValue] || strValue
|
|
274
|
+
const icon = icons[strValue]
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<Badge variant="outline" className={cn("capitalize gap-1", colorClass)}>
|
|
278
|
+
{icon && <span className="mr-1">{icon}</span>}
|
|
279
|
+
{label}
|
|
280
|
+
</Badge>
|
|
281
|
+
)
|
|
282
|
+
},
|
|
283
|
+
size: 120,
|
|
284
|
+
enableSorting: true,
|
|
285
|
+
enableFiltering: true,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Template Badge (alias de status)
|
|
290
|
+
*/
|
|
291
|
+
export const badgeTemplate = statusTemplate
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Template Email
|
|
295
|
+
*/
|
|
296
|
+
export const emailTemplate: ColumnTemplate<unknown, unknown> = {
|
|
297
|
+
cell: (value, _row, options) => {
|
|
298
|
+
if (!value) return <EmptyValue />
|
|
299
|
+
const strValue = String(value)
|
|
300
|
+
|
|
301
|
+
const displayValue = options?.truncate && options?.maxLength
|
|
302
|
+
? formatters.truncate(strValue, options.maxLength)
|
|
303
|
+
: strValue
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<div className="flex items-center gap-1 group">
|
|
307
|
+
<a
|
|
308
|
+
href={`mailto:${strValue}`}
|
|
309
|
+
className="text-primary hover:underline flex items-center gap-1"
|
|
310
|
+
onClick={(e) => e.stopPropagation()}
|
|
311
|
+
>
|
|
312
|
+
<Mail className="h-3 w-3 flex-shrink-0" />
|
|
313
|
+
<span className="truncate max-w-[200px]">{displayValue}</span>
|
|
314
|
+
</a>
|
|
315
|
+
{options?.copyable && (
|
|
316
|
+
<span className="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
317
|
+
<CopyButton value={strValue} />
|
|
318
|
+
</span>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
)
|
|
322
|
+
},
|
|
323
|
+
size: 200,
|
|
324
|
+
enableSorting: true,
|
|
325
|
+
enableFiltering: true,
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Template Phone
|
|
330
|
+
*/
|
|
331
|
+
export const phoneTemplate: ColumnTemplate<unknown, unknown> = {
|
|
332
|
+
cell: (value, _row, options) => {
|
|
333
|
+
if (!value) return <EmptyValue />
|
|
334
|
+
const strValue = String(value)
|
|
335
|
+
|
|
336
|
+
const formatted = formatters.phone(strValue, options?.countryCode || "FR")
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<div className="flex items-center gap-1 group">
|
|
340
|
+
<a
|
|
341
|
+
href={`tel:${strValue}`}
|
|
342
|
+
className="text-primary hover:underline flex items-center gap-1"
|
|
343
|
+
onClick={(e) => e.stopPropagation()}
|
|
344
|
+
>
|
|
345
|
+
<Phone className="h-3 w-3 flex-shrink-0" />
|
|
346
|
+
<span className="font-mono">{formatted}</span>
|
|
347
|
+
</a>
|
|
348
|
+
{options?.copyable !== false && (
|
|
349
|
+
<span className="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
350
|
+
<CopyButton value={strValue} />
|
|
351
|
+
</span>
|
|
352
|
+
)}
|
|
353
|
+
</div>
|
|
354
|
+
)
|
|
355
|
+
},
|
|
356
|
+
size: 150,
|
|
357
|
+
enableSorting: false,
|
|
358
|
+
enableFiltering: true,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Template URL
|
|
363
|
+
*/
|
|
364
|
+
export const urlTemplate: ColumnTemplate<unknown, unknown> = {
|
|
365
|
+
cell: (value, _row, options) => {
|
|
366
|
+
if (!value) return <EmptyValue />
|
|
367
|
+
const strValue = String(value)
|
|
368
|
+
|
|
369
|
+
const displayValue = options?.truncate && options?.maxLength
|
|
370
|
+
? formatters.truncate(strValue, options.maxLength)
|
|
371
|
+
: strValue
|
|
372
|
+
|
|
373
|
+
return (
|
|
374
|
+
<a
|
|
375
|
+
href={strValue}
|
|
376
|
+
target={options?.target || "_blank"}
|
|
377
|
+
rel="noopener noreferrer"
|
|
378
|
+
className="text-primary hover:underline flex items-center gap-1"
|
|
379
|
+
onClick={(e) => e.stopPropagation()}
|
|
380
|
+
>
|
|
381
|
+
<span className="truncate max-w-[200px]">{displayValue}</span>
|
|
382
|
+
{options?.showIcon !== false && <ExternalLink className="h-3 w-3 flex-shrink-0" />}
|
|
383
|
+
</a>
|
|
384
|
+
)
|
|
385
|
+
},
|
|
386
|
+
size: 200,
|
|
387
|
+
enableSorting: false,
|
|
388
|
+
enableFiltering: true,
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Template Avatar
|
|
393
|
+
*/
|
|
394
|
+
export const avatarTemplate: ColumnTemplate<unknown, unknown> = {
|
|
395
|
+
cell: (value, _row, options) => {
|
|
396
|
+
if (!value) return <EmptyValue />
|
|
397
|
+
|
|
398
|
+
let name: string | undefined
|
|
399
|
+
let image: string | undefined
|
|
400
|
+
|
|
401
|
+
if (typeof value === "string") {
|
|
402
|
+
name = value
|
|
403
|
+
} else if (typeof value === "object" && value !== null) {
|
|
404
|
+
const objValue = value as Record<string, unknown>
|
|
405
|
+
const nameKey = options?.nameKey || "name"
|
|
406
|
+
const imageKey = options?.imageKey || "image"
|
|
407
|
+
name = objValue[nameKey] as string
|
|
408
|
+
image = objValue[imageKey] as string
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const sizeClasses = {
|
|
412
|
+
sm: "h-6 w-6 text-xs",
|
|
413
|
+
md: "h-8 w-8 text-sm",
|
|
414
|
+
lg: "h-10 w-10 text-base",
|
|
415
|
+
}
|
|
416
|
+
const size = options?.avatarSize || "md"
|
|
417
|
+
|
|
418
|
+
return (
|
|
419
|
+
<div className="flex items-center gap-2">
|
|
420
|
+
<Avatar className={sizeClasses[size]}>
|
|
421
|
+
<AvatarImage src={image} alt={name} />
|
|
422
|
+
<AvatarFallback>
|
|
423
|
+
{options?.fallbackInitials !== false && name ? formatters.initials(name) : "?"}
|
|
424
|
+
</AvatarFallback>
|
|
425
|
+
</Avatar>
|
|
426
|
+
{options?.showName && name && (
|
|
427
|
+
<span className="truncate max-w-[150px]">{name}</span>
|
|
428
|
+
)}
|
|
429
|
+
</div>
|
|
430
|
+
)
|
|
431
|
+
},
|
|
432
|
+
size: 60,
|
|
433
|
+
enableSorting: false,
|
|
434
|
+
enableFiltering: false,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Template Boolean
|
|
439
|
+
*/
|
|
440
|
+
export const booleanTemplate: ColumnTemplate<unknown, unknown> = {
|
|
441
|
+
cell: (value, _row, options) => {
|
|
442
|
+
if (value == null) return <EmptyValue />
|
|
443
|
+
const boolValue = Boolean(value)
|
|
444
|
+
|
|
445
|
+
const TrueIcon = options?.trueIcon || Check
|
|
446
|
+
const FalseIcon = options?.falseIcon || X
|
|
447
|
+
const trueLabel = options?.trueLabel
|
|
448
|
+
const falseLabel = options?.falseLabel
|
|
449
|
+
const trueColor = options?.trueColor || "text-green-600"
|
|
450
|
+
const falseColor = options?.falseColor || "text-gray-400"
|
|
451
|
+
|
|
452
|
+
const Icon = boolValue ? TrueIcon : FalseIcon
|
|
453
|
+
const label = boolValue ? trueLabel : falseLabel
|
|
454
|
+
const colorClass = boolValue ? trueColor : falseColor
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<div className={cn("flex items-center gap-1", colorClass)}>
|
|
458
|
+
{typeof Icon === "function" ? <Icon className="h-4 w-4" /> : Icon}
|
|
459
|
+
{label && <span className="text-sm">{label}</span>}
|
|
460
|
+
</div>
|
|
461
|
+
)
|
|
462
|
+
},
|
|
463
|
+
size: 80,
|
|
464
|
+
enableSorting: true,
|
|
465
|
+
enableFiltering: true,
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Template Actions
|
|
470
|
+
*/
|
|
471
|
+
export function createActionsTemplate<TData>(): ColumnTemplate<TData, unknown> {
|
|
472
|
+
return {
|
|
473
|
+
cell: (_value, row, options) => {
|
|
474
|
+
const actions = (options?.actions || []) as ColumnTemplateAction<TData>[]
|
|
475
|
+
|
|
476
|
+
if (!actions.length) return null
|
|
477
|
+
|
|
478
|
+
return (
|
|
479
|
+
<DropdownMenu>
|
|
480
|
+
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
|
481
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
482
|
+
<span className="sr-only">Actions</span>
|
|
483
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
484
|
+
</Button>
|
|
485
|
+
</DropdownMenuTrigger>
|
|
486
|
+
<DropdownMenuContent align={options?.dropdownAlign || "end"}>
|
|
487
|
+
{actions.map((action) => (
|
|
488
|
+
<ActionWithConfirm key={action.id} action={action} row={row} />
|
|
489
|
+
))}
|
|
490
|
+
</DropdownMenuContent>
|
|
491
|
+
</DropdownMenu>
|
|
492
|
+
)
|
|
493
|
+
},
|
|
494
|
+
size: 50,
|
|
495
|
+
enableSorting: false,
|
|
496
|
+
enableFiltering: false,
|
|
497
|
+
enableHiding: false,
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Template Actions par défaut
|
|
503
|
+
*/
|
|
504
|
+
export const actionsTemplate: ColumnTemplate<unknown, unknown> = createActionsTemplate<unknown>()
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Template Index (numéro de ligne)
|
|
508
|
+
*/
|
|
509
|
+
export const indexTemplate: ColumnTemplate<unknown, unknown> = {
|
|
510
|
+
cell: (_value, _row, _options) => {
|
|
511
|
+
// Note: l'index sera passé via meta ou calculé côté hook
|
|
512
|
+
return <span className="text-muted-foreground tabular-nums">#</span>
|
|
513
|
+
},
|
|
514
|
+
size: 50,
|
|
515
|
+
enableSorting: false,
|
|
516
|
+
enableFiltering: false,
|
|
517
|
+
enableHiding: false,
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Template ID
|
|
522
|
+
*/
|
|
523
|
+
export const idTemplate: ColumnTemplate<unknown, unknown> = {
|
|
524
|
+
cell: (value, _row, options) => {
|
|
525
|
+
if (!value) return <EmptyValue />
|
|
526
|
+
const strValue = String(value)
|
|
527
|
+
|
|
528
|
+
const displayValue = options?.truncate && options?.maxLength
|
|
529
|
+
? formatters.truncate(strValue, options.maxLength)
|
|
530
|
+
: strValue
|
|
531
|
+
|
|
532
|
+
return (
|
|
533
|
+
<div className="flex items-center gap-1 group">
|
|
534
|
+
<code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono">
|
|
535
|
+
{displayValue}
|
|
536
|
+
</code>
|
|
537
|
+
{options?.copyable && (
|
|
538
|
+
<span className="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
539
|
+
<CopyButton value={strValue} />
|
|
540
|
+
</span>
|
|
541
|
+
)}
|
|
542
|
+
</div>
|
|
543
|
+
)
|
|
544
|
+
},
|
|
545
|
+
size: 100,
|
|
546
|
+
enableSorting: true,
|
|
547
|
+
enableFiltering: true,
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Template Rating (étoiles)
|
|
552
|
+
*/
|
|
553
|
+
export const ratingTemplate: ColumnTemplate<unknown, unknown> = {
|
|
554
|
+
cell: (value, _row, options) => {
|
|
555
|
+
if (value == null) return <EmptyValue />
|
|
556
|
+
const numValue = typeof value === "number" ? value : parseFloat(String(value))
|
|
557
|
+
if (isNaN(numValue)) return <EmptyValue />
|
|
558
|
+
|
|
559
|
+
const maxRating = options?.maxRating || 5
|
|
560
|
+
const allowHalf = options?.allowHalf || false
|
|
561
|
+
const color = options?.color || "text-yellow-400"
|
|
562
|
+
|
|
563
|
+
const fullStars = Math.floor(numValue)
|
|
564
|
+
const hasHalf = allowHalf && numValue % 1 >= 0.5
|
|
565
|
+
const emptyStars = maxRating - fullStars - (hasHalf ? 1 : 0)
|
|
566
|
+
|
|
567
|
+
return (
|
|
568
|
+
<div className="flex items-center gap-0.5">
|
|
569
|
+
{Array.from({ length: fullStars }).map((_, i) => (
|
|
570
|
+
<Star key={`full-${i}`} className={cn("h-4 w-4 fill-current", color)} />
|
|
571
|
+
))}
|
|
572
|
+
{hasHalf && <StarHalf className={cn("h-4 w-4 fill-current", color)} />}
|
|
573
|
+
{Array.from({ length: emptyStars }).map((_, i) => (
|
|
574
|
+
<Star key={`empty-${i}`} className="h-4 w-4 text-muted-foreground" />
|
|
575
|
+
))}
|
|
576
|
+
<span className="ml-1 text-sm text-muted-foreground">{numValue.toFixed(1)}</span>
|
|
577
|
+
</div>
|
|
578
|
+
)
|
|
579
|
+
},
|
|
580
|
+
sortingFn: "basic",
|
|
581
|
+
size: 140,
|
|
582
|
+
enableSorting: true,
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Template Progress
|
|
587
|
+
*/
|
|
588
|
+
export const progressTemplate: ColumnTemplate<unknown, unknown> = {
|
|
589
|
+
cell: (value, _row, options) => {
|
|
590
|
+
if (value == null) return <EmptyValue />
|
|
591
|
+
|
|
592
|
+
const numValue = typeof value === "number" ? value : parseFloat(String(value))
|
|
593
|
+
if (isNaN(numValue)) return <EmptyValue />
|
|
594
|
+
|
|
595
|
+
const clampedValue = Math.min(100, Math.max(0, numValue))
|
|
596
|
+
const showLabel = options?.showLabel !== false
|
|
597
|
+
|
|
598
|
+
return (
|
|
599
|
+
<div className="flex items-center gap-2 min-w-[100px]">
|
|
600
|
+
<Progress
|
|
601
|
+
value={clampedValue}
|
|
602
|
+
className="h-2 flex-1"
|
|
603
|
+
/>
|
|
604
|
+
{showLabel && (
|
|
605
|
+
<span className="text-sm tabular-nums w-10 text-right">{Math.round(clampedValue)}%</span>
|
|
606
|
+
)}
|
|
607
|
+
</div>
|
|
608
|
+
)
|
|
609
|
+
},
|
|
610
|
+
sortingFn: "basic",
|
|
611
|
+
size: 140,
|
|
612
|
+
enableSorting: true,
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Template Tags
|
|
617
|
+
*/
|
|
618
|
+
export const tagsTemplate: ColumnTemplate<unknown, unknown> = {
|
|
619
|
+
cell: (value, _row, options) => {
|
|
620
|
+
if (!value) return <EmptyValue />
|
|
621
|
+
|
|
622
|
+
let tags: string[]
|
|
623
|
+
if (typeof value === "string") {
|
|
624
|
+
const separator = options?.separator || ","
|
|
625
|
+
tags = value.split(separator).map((t) => t.trim()).filter(Boolean)
|
|
626
|
+
} else if (Array.isArray(value)) {
|
|
627
|
+
tags = value.map(String)
|
|
628
|
+
} else {
|
|
629
|
+
return <EmptyValue />
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (!tags.length) return <EmptyValue />
|
|
633
|
+
|
|
634
|
+
const maxTags = options?.maxTags || 3
|
|
635
|
+
const displayTags = tags.slice(0, maxTags)
|
|
636
|
+
const remainingCount = tags.length - maxTags
|
|
637
|
+
const tagColors = options?.tagColors || {}
|
|
638
|
+
|
|
639
|
+
return (
|
|
640
|
+
<div className="flex flex-wrap gap-1">
|
|
641
|
+
{displayTags.map((tag, i) => (
|
|
642
|
+
<Badge
|
|
643
|
+
key={i}
|
|
644
|
+
variant="secondary"
|
|
645
|
+
className={cn("text-xs", tagColors[tag])}
|
|
646
|
+
>
|
|
647
|
+
{tag}
|
|
648
|
+
</Badge>
|
|
649
|
+
))}
|
|
650
|
+
{remainingCount > 0 && (
|
|
651
|
+
<Badge variant="outline" className="text-xs">
|
|
652
|
+
+{remainingCount}
|
|
653
|
+
</Badge>
|
|
654
|
+
)}
|
|
655
|
+
</div>
|
|
656
|
+
)
|
|
657
|
+
},
|
|
658
|
+
size: 200,
|
|
659
|
+
enableSorting: false,
|
|
660
|
+
enableFiltering: true,
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Template JSON
|
|
665
|
+
*/
|
|
666
|
+
export const jsonTemplate: ColumnTemplate<unknown, unknown> = {
|
|
667
|
+
cell: (value, _row, options) => {
|
|
668
|
+
if (value == null) return <EmptyValue />
|
|
669
|
+
|
|
670
|
+
const [expanded, setExpanded] = React.useState(false)
|
|
671
|
+
const formatted = formatters.json(value, 2)
|
|
672
|
+
const isExpandable = options?.expandable !== false && formatted.length > 50
|
|
673
|
+
|
|
674
|
+
if (!isExpandable) {
|
|
675
|
+
return (
|
|
676
|
+
<code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono whitespace-pre-wrap">
|
|
677
|
+
{formatted}
|
|
678
|
+
</code>
|
|
679
|
+
)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return (
|
|
683
|
+
<div className="relative">
|
|
684
|
+
<button
|
|
685
|
+
onClick={(e) => {
|
|
686
|
+
e.stopPropagation()
|
|
687
|
+
setExpanded(!expanded)
|
|
688
|
+
}}
|
|
689
|
+
className="text-left w-full"
|
|
690
|
+
>
|
|
691
|
+
<code className={cn(
|
|
692
|
+
"text-xs bg-muted px-1.5 py-0.5 rounded font-mono block",
|
|
693
|
+
expanded ? "whitespace-pre-wrap" : "truncate max-w-[200px]"
|
|
694
|
+
)}>
|
|
695
|
+
{formatted}
|
|
696
|
+
</code>
|
|
697
|
+
</button>
|
|
698
|
+
</div>
|
|
699
|
+
)
|
|
700
|
+
},
|
|
701
|
+
size: 200,
|
|
702
|
+
enableSorting: false,
|
|
703
|
+
enableFiltering: false,
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Template Text (par défaut)
|
|
708
|
+
*/
|
|
709
|
+
export const textTemplate: ColumnTemplate<unknown, unknown> = {
|
|
710
|
+
cell: (value, _row, options) => {
|
|
711
|
+
if (value == null || value === "") return <EmptyValue />
|
|
712
|
+
|
|
713
|
+
const strValue = String(value)
|
|
714
|
+
const displayValue = options?.truncate && options?.maxLength
|
|
715
|
+
? formatters.truncate(strValue, options.maxLength)
|
|
716
|
+
: strValue
|
|
717
|
+
|
|
718
|
+
// Highlight si spécifié
|
|
719
|
+
if (options?.highlight) {
|
|
720
|
+
const regex = new RegExp(`(${options.highlight})`, "gi")
|
|
721
|
+
const parts = displayValue.split(regex)
|
|
722
|
+
|
|
723
|
+
return (
|
|
724
|
+
<div className="flex items-center gap-1 group">
|
|
725
|
+
<span>
|
|
726
|
+
{parts.map((part, i) =>
|
|
727
|
+
regex.test(part) ? (
|
|
728
|
+
<mark key={i} className="bg-yellow-200 dark:bg-yellow-800">{part}</mark>
|
|
729
|
+
) : (
|
|
730
|
+
<React.Fragment key={i}>{part}</React.Fragment>
|
|
731
|
+
)
|
|
732
|
+
)}
|
|
733
|
+
</span>
|
|
734
|
+
{options?.copyable && (
|
|
735
|
+
<span className="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
736
|
+
<CopyButton value={strValue} />
|
|
737
|
+
</span>
|
|
738
|
+
)}
|
|
739
|
+
</div>
|
|
740
|
+
)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return (
|
|
744
|
+
<div className="flex items-center gap-1 group">
|
|
745
|
+
<span className={options?.truncate ? "truncate" : ""}>{displayValue}</span>
|
|
746
|
+
{options?.copyable && (
|
|
747
|
+
<span className="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
748
|
+
<CopyButton value={strValue} />
|
|
749
|
+
</span>
|
|
750
|
+
)}
|
|
751
|
+
</div>
|
|
752
|
+
)
|
|
753
|
+
},
|
|
754
|
+
enableSorting: true,
|
|
755
|
+
enableFiltering: true,
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ============================================
|
|
759
|
+
// TEMPLATES MAP
|
|
760
|
+
// ============================================
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Map des templates par nom
|
|
764
|
+
* Note: Les templates sont typés avec `unknown` pour permettre l'accès dynamique par nom.
|
|
765
|
+
* Le typage précis est assuré lors de l'utilisation via useDataTableColumnTemplates.
|
|
766
|
+
*/
|
|
767
|
+
export const templatesMap: Record<string, ColumnTemplate<unknown, unknown>> = {
|
|
768
|
+
date: dateTemplate,
|
|
769
|
+
datetime: datetimeTemplate,
|
|
770
|
+
time: timeTemplate,
|
|
771
|
+
currency: currencyTemplate,
|
|
772
|
+
percentage: percentageTemplate,
|
|
773
|
+
number: numberTemplate,
|
|
774
|
+
status: statusTemplate,
|
|
775
|
+
badge: badgeTemplate,
|
|
776
|
+
email: emailTemplate,
|
|
777
|
+
phone: phoneTemplate,
|
|
778
|
+
url: urlTemplate,
|
|
779
|
+
avatar: avatarTemplate,
|
|
780
|
+
boolean: booleanTemplate,
|
|
781
|
+
actions: actionsTemplate,
|
|
782
|
+
index: indexTemplate,
|
|
783
|
+
id: idTemplate,
|
|
784
|
+
rating: ratingTemplate,
|
|
785
|
+
progress: progressTemplate,
|
|
786
|
+
tags: tagsTemplate,
|
|
787
|
+
json: jsonTemplate,
|
|
788
|
+
text: textTemplate,
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Récupère un template par son nom
|
|
793
|
+
*/
|
|
794
|
+
export function getTemplate(name: string): ColumnTemplate<unknown, unknown> | undefined {
|
|
795
|
+
return templatesMap[name]
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Liste des noms de templates disponibles
|
|
800
|
+
*/
|
|
801
|
+
export const availableTemplateNames = Object.keys(templatesMap)
|
|
802
|
+
|
|
803
|
+
export default templatesMap
|