@exxatdesignux/ui 0.0.6 → 0.0.8
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/bin/init.mjs +29 -0
- package/package.json +7 -2
- package/template/.nvmrc +1 -0
- package/template/.prettierignore +7 -0
- package/template/.prettierrc +11 -0
- package/template/AGENTS.md +485 -0
- package/template/Logo/Exxat_Prism.svg +39 -0
- package/template/Logo/Exxat_one.svg +36 -0
- package/template/README.md +58 -0
- package/template/app/(app)/compliance/page.tsx +10 -0
- package/template/app/(app)/dashboard/loading.tsx +18 -0
- package/template/app/(app)/dashboard/page.tsx +36 -0
- package/template/app/(app)/data-list/[id]/page.tsx +28 -0
- package/template/app/(app)/data-list/new/page.tsx +31 -0
- package/template/app/(app)/data-list/page.tsx +10 -0
- package/template/app/(app)/error.tsx +43 -0
- package/template/app/(app)/help/page.tsx +34 -0
- package/template/app/(app)/layout.tsx +54 -0
- package/template/app/(app)/loading.tsx +18 -0
- package/template/app/(app)/question-bank/page.tsx +10 -0
- package/template/app/(app)/rotations/page.tsx +15 -0
- package/template/app/(app)/settings/page.tsx +17 -0
- package/template/app/(app)/sites/all/page.tsx +13 -0
- package/template/app/(app)/team/page.tsx +10 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/globals.css +1811 -0
- package/template/app/layout.tsx +95 -0
- package/template/app/page.tsx +9 -0
- package/template/components/.gitkeep +0 -0
- package/template/components/app-sidebar-dynamic.tsx +15 -0
- package/template/components/app-sidebar.tsx +901 -0
- package/template/components/ask-leo-composer.tsx +216 -0
- package/template/components/ask-leo-sidebar.tsx +509 -0
- package/template/components/chart-area-interactive.tsx +293 -0
- package/template/components/charts-overview.tsx +2321 -0
- package/template/components/command-menu-01.tsx +133 -0
- package/template/components/command-menu-02.tsx +386 -0
- package/template/components/command-menu.tsx +182 -0
- package/template/components/compliance-board-view.tsx +134 -0
- package/template/components/compliance-client.tsx +92 -0
- package/template/components/compliance-list-view.tsx +59 -0
- package/template/components/compliance-page-header.tsx +89 -0
- package/template/components/compliance-table.tsx +525 -0
- package/template/components/dashboard-onboarding-gallery.tsx +13 -0
- package/template/components/dashboard-onboarding.tsx +21 -0
- package/template/components/dashboard-promo-banner.tsx +67 -0
- package/template/components/dashboard-quota-progress-card.tsx +369 -0
- package/template/components/dashboard-report-charts.tsx +69 -0
- package/template/components/dashboard-section-heading.tsx +68 -0
- package/template/components/dashboard-tabs.tsx +598 -0
- package/template/components/data-list-client.tsx +239 -0
- package/template/components/data-list-table-cells.test.tsx +22 -0
- package/template/components/data-list-table-cells.tsx +173 -0
- package/template/components/data-list-table.tsx +879 -0
- package/template/components/data-table/filter-date-calendar.tsx +38 -0
- package/template/components/data-table/filter-text-value-input.tsx +77 -0
- package/template/components/data-table/index.tsx +1612 -0
- package/template/components/data-table/pagination.tsx +256 -0
- package/template/components/data-table/types.ts +91 -0
- package/template/components/data-table/use-table-state.ts +566 -0
- package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
- package/template/components/data-view-dashboard-charts-team.tsx +968 -0
- package/template/components/data-view-dashboard-charts.tsx +1668 -0
- package/template/components/data-views/board-card-primitives.tsx +93 -0
- package/template/components/data-views/index.ts +41 -0
- package/template/components/data-views/list-page-board-card.tsx +192 -0
- package/template/components/data-views/list-page-board-template.tsx +122 -0
- package/template/components/data-views/placement-board-card.tsx +262 -0
- package/template/components/export-drawer.tsx +375 -0
- package/template/components/exxat-product-logo.tsx +453 -0
- package/template/components/form-layout-01.tsx +131 -0
- package/template/components/getting-started.tsx +625 -0
- package/template/components/key-metrics.tsx +920 -0
- package/template/components/leo-insight-indicator.tsx +364 -0
- package/template/components/leo-typing-dots.tsx +121 -0
- package/template/components/list-hub-status-badge.tsx +51 -0
- package/template/components/list-page-dashboard-charts.tsx +18 -0
- package/template/components/nav-documents.tsx +89 -0
- package/template/components/nav-main.tsx +58 -0
- package/template/components/nav-secondary.tsx +64 -0
- package/template/components/nav-user.tsx +190 -0
- package/template/components/new-placement-back-btn.tsx +28 -0
- package/template/components/new-placement-form.tsx +1066 -0
- package/template/components/onboarding/index.ts +4 -0
- package/template/components/onboarding/onboarding-01.tsx +7 -0
- package/template/components/onboarding/onboarding-02.tsx +7 -0
- package/template/components/onboarding/onboarding-03.tsx +7 -0
- package/template/components/onboarding/onboarding-04.tsx +7 -0
- package/template/components/page-header.tsx +57 -0
- package/template/components/placement-detail.tsx +438 -0
- package/template/components/placements-board-view.tsx +404 -0
- package/template/components/placements-list-view.tsx +285 -0
- package/template/components/placements-page-header.tsx +160 -0
- package/template/components/placements-table-columns.tsx +639 -0
- package/template/components/product-switcher.tsx +116 -0
- package/template/components/question-bank-board-view.tsx +205 -0
- package/template/components/question-bank-client.tsx +77 -0
- package/template/components/question-bank-list-view.tsx +59 -0
- package/template/components/question-bank-page-header.tsx +89 -0
- package/template/components/question-bank-table.tsx +586 -0
- package/template/components/rotations-empty-state.tsx +47 -0
- package/template/components/rotations-panel-activator.tsx +8 -0
- package/template/components/secondary-nav.tsx +394 -0
- package/template/components/secondary-panel.tsx +239 -0
- package/template/components/section-cards.tsx +106 -0
- package/template/components/settings-appearance-card.tsx +424 -0
- package/template/components/settings-client.tsx +537 -0
- package/template/components/settings-form-row.tsx +42 -0
- package/template/components/sidebar-auto-collapse.tsx +23 -0
- package/template/components/sidebar-auto-open.tsx +18 -0
- package/template/components/sidebar-shell.tsx +37 -0
- package/template/components/site-header.tsx +93 -0
- package/template/components/sites-all-client.tsx +154 -0
- package/template/components/sites-board-view.tsx +67 -0
- package/template/components/sites-list-view.tsx +47 -0
- package/template/components/sites-table.tsx +312 -0
- package/template/components/system-banner-slot.tsx +66 -0
- package/template/components/table-properties/column-row.tsx +90 -0
- package/template/components/table-properties/draggable-list.ts +49 -0
- package/template/components/table-properties/drawer-button.tsx +231 -0
- package/template/components/table-properties/drawer.tsx +1102 -0
- package/template/components/table-properties/filter-card.tsx +251 -0
- package/template/components/table-properties/index.ts +22 -0
- package/template/components/table-properties/sort-card.tsx +59 -0
- package/template/components/table-properties/types.ts +124 -0
- package/template/components/task-list-panel.tsx +98 -0
- package/template/components/task-priority-badge.tsx +28 -0
- package/template/components/team-board-view.tsx +114 -0
- package/template/components/team-client.tsx +93 -0
- package/template/components/team-list-view.tsx +62 -0
- package/template/components/team-page-header.tsx +92 -0
- package/template/components/team-table.tsx +525 -0
- package/template/components/templates/list-page.tsx +576 -0
- package/template/components/templates/primary-page-template.tsx +56 -0
- package/template/components/theme-color-sync.tsx +32 -0
- package/template/components/theme-provider.tsx +71 -0
- package/template/components/tinted-icon-disc.tsx +53 -0
- package/template/components/ui/ai-thinking-surface.tsx +121 -0
- package/template/components/ui/avatar.tsx +1 -0
- package/template/components/ui/badge.tsx +1 -0
- package/template/components/ui/banner.tsx +1 -0
- package/template/components/ui/breadcrumb.tsx +1 -0
- package/template/components/ui/button.tsx +1 -0
- package/template/components/ui/calendar.tsx +1 -0
- package/template/components/ui/card.tsx +1 -0
- package/template/components/ui/chart.tsx +1 -0
- package/template/components/ui/checkbox.tsx +1 -0
- package/template/components/ui/coach-mark.tsx +1 -0
- package/template/components/ui/collapsible.tsx +1 -0
- package/template/components/ui/command.tsx +1 -0
- package/template/components/ui/date-picker-field.tsx +1 -0
- package/template/components/ui/dialog.tsx +1 -0
- package/template/components/ui/dot-pattern.tsx +159 -0
- package/template/components/ui/drag-handle-grip.tsx +1 -0
- package/template/components/ui/drawer.tsx +1 -0
- package/template/components/ui/dropdown-menu.tsx +1 -0
- package/template/components/ui/field.tsx +1 -0
- package/template/components/ui/form.tsx +1 -0
- package/template/components/ui/input-group.tsx +1 -0
- package/template/components/ui/input-mask.tsx +1 -0
- package/template/components/ui/input.tsx +1 -0
- package/template/components/ui/kbd.tsx +1 -0
- package/template/components/ui/label.tsx +1 -0
- package/template/components/ui/leo-icon.tsx +726 -0
- package/template/components/ui/payment-card-fields.tsx +1 -0
- package/template/components/ui/popover.tsx +1 -0
- package/template/components/ui/radio-group.tsx +1 -0
- package/template/components/ui/select.tsx +1 -0
- package/template/components/ui/selection-tile-grid.tsx +1 -0
- package/template/components/ui/separator.tsx +1 -0
- package/template/components/ui/sheet.tsx +1 -0
- package/template/components/ui/sidebar.tsx +1 -0
- package/template/components/ui/skeleton.tsx +1 -0
- package/template/components/ui/sonner.tsx +1 -0
- package/template/components/ui/status-badge.tsx +1 -0
- package/template/components/ui/table.tsx +1 -0
- package/template/components/ui/tabs.tsx +1 -0
- package/template/components/ui/textarea.tsx +1 -0
- package/template/components/ui/tip.tsx +1 -0
- package/template/components/ui/toggle-group.tsx +1 -0
- package/template/components/ui/toggle-switch.tsx +1 -0
- package/template/components/ui/toggle.tsx +1 -0
- package/template/components/ui/tooltip.tsx +1 -0
- package/template/components/ui/view-segmented-control.tsx +1 -0
- package/template/components.json +27 -0
- package/template/contexts/chart-variant-context.tsx +35 -0
- package/template/contexts/command-menu-context.tsx +28 -0
- package/template/contexts/dashboard-view-context.tsx +35 -0
- package/template/contexts/product-context.tsx +38 -0
- package/template/contexts/system-banner-context.tsx +127 -0
- package/template/docs/command-menu-pattern.md +45 -0
- package/template/docs/data-views-pattern.md +160 -0
- package/template/ecosystem.config.cjs +20 -0
- package/template/eslint.config.mjs +18 -0
- package/template/fontawesome-subset.manifest.json +190 -0
- package/template/hooks/.gitkeep +0 -0
- package/template/hooks/use-app-theme.ts +1 -0
- package/template/hooks/use-coach-mark.ts +1 -0
- package/template/hooks/use-mobile.ts +1 -0
- package/template/hooks/use-mod-key-label.ts +1 -0
- package/template/lib/.gitkeep +0 -0
- package/template/lib/ask-leo-route-context.ts +133 -0
- package/template/lib/chart-keyboard-selection.test.ts +20 -0
- package/template/lib/chart-keyboard-selection.ts +17 -0
- package/template/lib/chart-line-dash.ts +16 -0
- package/template/lib/coach-mark-registry.ts +68 -0
- package/template/lib/command-menu-config.ts +127 -0
- package/template/lib/command-menu-search-data.ts +44 -0
- package/template/lib/conditional-rule-match.ts +32 -0
- package/template/lib/dashboard-customize-coach-mark.ts +18 -0
- package/template/lib/dashboard-layout-merge.ts +63 -0
- package/template/lib/data-list-display-options.ts +35 -0
- package/template/lib/data-list-persistence.ts +280 -0
- package/template/lib/data-list-view-surface.ts +58 -0
- package/template/lib/data-list-view.ts +29 -0
- package/template/lib/data-view-dashboard-storage.ts +101 -0
- package/template/lib/date-filter.ts +8 -0
- package/template/lib/dev-log.test.ts +28 -0
- package/template/lib/dev-log.ts +8 -0
- package/template/lib/editable-target.ts +10 -0
- package/template/lib/floating-sheet-panel.ts +72 -0
- package/template/lib/initials-from-name.ts +7 -0
- package/template/lib/list-page-table-properties.ts +52 -0
- package/template/lib/list-status-badges.ts +168 -0
- package/template/lib/logo-dev.ts +12 -0
- package/template/lib/mock/compliance-kpi.ts +61 -0
- package/template/lib/mock/compliance.ts +146 -0
- package/template/lib/mock/dashboard.ts +105 -0
- package/template/lib/mock/navigation.tsx +231 -0
- package/template/lib/mock/placements-kpi.ts +134 -0
- package/template/lib/mock/placements.ts +183 -0
- package/template/lib/mock/question-bank-kpi.ts +61 -0
- package/template/lib/mock/question-bank.ts +142 -0
- package/template/lib/mock/sites-directory.ts +16 -0
- package/template/lib/mock/sites-kpi.ts +25 -0
- package/template/lib/mock/team-kpi.ts +60 -0
- package/template/lib/mock/team.ts +118 -0
- package/template/lib/motion-ui.ts +17 -0
- package/template/lib/placement-board-card-layout.ts +79 -0
- package/template/lib/placement-lifecycle.ts +5 -0
- package/template/lib/row-height.ts +10 -0
- package/template/lib/stock-portrait.ts +11 -0
- package/template/lib/utils.test.ts +13 -0
- package/template/lib/utils.ts +1 -0
- package/template/next.config.mjs +15 -0
- package/template/package.json +83 -0
- package/template/postcss.config.mjs +8 -0
- package/template/public/.gitkeep +0 -0
- package/template/public/Illustration/Rotation.svg +74 -0
- package/template/public/avatars/user.svg +11 -0
- package/template/public/favicon/favicon.ico +0 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/logos/exxat-one.svg +36 -0
- package/template/public/logos/exxat-prism.svg +39 -0
- package/template/public/mock-schools/emory.svg +4 -0
- package/template/public/mock-schools/rush.svg +4 -0
- package/template/scripts/fontawesome-subset-audit.mjs +190 -0
- package/template/scripts/pm2-startup-macos.sh +13 -0
- package/template/skills-lock.json +10 -0
- package/template/stores/app-store.ts +33 -0
- package/template/tests/setup.ts +1 -0
- package/template/tsconfig.json +35 -0
- package/template/types/react-payment-inputs.d.ts +19 -0
- package/template/vitest.config.ts +18 -0
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
import type { DataListViewType } from "@/lib/data-list-view"
|
|
5
|
+
import { DATA_LIST_VIEW_TILES, dataListViewLabel } from "@/lib/data-list-view"
|
|
6
|
+
import type { RowHeight } from "@/lib/row-height"
|
|
7
|
+
import { ROW_HEIGHT_TILES } from "@/lib/row-height"
|
|
8
|
+
import { SelectionTileGrid } from "@/components/ui/selection-tile-grid"
|
|
9
|
+
import {
|
|
10
|
+
DropdownMenu,
|
|
11
|
+
DropdownMenuContent,
|
|
12
|
+
DropdownMenuItem,
|
|
13
|
+
DropdownMenuLabel,
|
|
14
|
+
DropdownMenuSeparator,
|
|
15
|
+
DropdownMenuTrigger,
|
|
16
|
+
} from "@/components/ui/dropdown-menu"
|
|
17
|
+
import {
|
|
18
|
+
Sheet,
|
|
19
|
+
SheetContent,
|
|
20
|
+
SheetTitle,
|
|
21
|
+
} from "@/components/ui/sheet"
|
|
22
|
+
import {
|
|
23
|
+
Select,
|
|
24
|
+
SelectContent,
|
|
25
|
+
SelectItem,
|
|
26
|
+
SelectTrigger,
|
|
27
|
+
SelectValue,
|
|
28
|
+
} from "@/components/ui/select"
|
|
29
|
+
import type { DataListDisplayOptions } from "@/lib/data-list-display-options"
|
|
30
|
+
import { Tip } from "@/components/ui/tip"
|
|
31
|
+
import { ToggleSwitch } from "@/components/ui/toggle-switch"
|
|
32
|
+
import { Button } from "@/components/ui/button"
|
|
33
|
+
import { DrawerFilterCard } from "./filter-card"
|
|
34
|
+
import { DrawerSortCard } from "./sort-card"
|
|
35
|
+
import { ColumnRow } from "./column-row"
|
|
36
|
+
import { useDraggableList } from "./draggable-list"
|
|
37
|
+
import {
|
|
38
|
+
type ActiveFilter,
|
|
39
|
+
type SortRule,
|
|
40
|
+
type ConditionalRule,
|
|
41
|
+
type FilterFieldDef,
|
|
42
|
+
COLUMNS,
|
|
43
|
+
FILTER_FIELDS,
|
|
44
|
+
RULE_COLORS,
|
|
45
|
+
} from "./types"
|
|
46
|
+
|
|
47
|
+
export interface TablePropertiesDrawerProps {
|
|
48
|
+
open: boolean
|
|
49
|
+
onOpenChange: (open: boolean) => void
|
|
50
|
+
// Display
|
|
51
|
+
showGridlines: boolean
|
|
52
|
+
onShowGridlinesChange: (v: boolean) => void
|
|
53
|
+
rowHeight: RowHeight
|
|
54
|
+
onRowHeightChange: (v: RowHeight) => void
|
|
55
|
+
pagination: boolean
|
|
56
|
+
onPaginationChange: (v: boolean) => void
|
|
57
|
+
// Filters
|
|
58
|
+
activeFilters: ActiveFilter[]
|
|
59
|
+
onAddFilter: (fieldKey: string) => void
|
|
60
|
+
onUpdateFilter: (id: string, patch: Partial<ActiveFilter>) => void
|
|
61
|
+
onRemoveFilter: (id: string) => void
|
|
62
|
+
/** How the filter after `leftFilterId` combines with the one above (default "and"). */
|
|
63
|
+
getFilterConnector: (leftFilterId: string) => "and" | "or"
|
|
64
|
+
onToggleFilterConnector: (leftFilterId: string) => void
|
|
65
|
+
filterBarVisible: boolean
|
|
66
|
+
onFilterBarVisibleChange: (v: boolean) => void
|
|
67
|
+
drawerExpandedFilters: Set<string>
|
|
68
|
+
onDrawerExpandedFiltersChange: React.Dispatch<React.SetStateAction<Set<string>>>
|
|
69
|
+
totalRows: number
|
|
70
|
+
filteredRows: number
|
|
71
|
+
// Sort
|
|
72
|
+
sortRules: SortRule[]
|
|
73
|
+
onSortRulesChange: (rules: SortRule[]) => void
|
|
74
|
+
onAddSortRule: (fieldKey: string) => void
|
|
75
|
+
onRemoveSortRule: (id: string) => void
|
|
76
|
+
onToggleSortDir: (id: string) => void
|
|
77
|
+
// Columns
|
|
78
|
+
colOrder: string[]
|
|
79
|
+
onColOrderChange: (order: string[]) => void
|
|
80
|
+
hiddenCols: Set<string>
|
|
81
|
+
onToggleColVisibility: (key: string) => void
|
|
82
|
+
onMoveCol: (key: string, dir: "up" | "down") => void
|
|
83
|
+
// Group
|
|
84
|
+
groupBy: string | null
|
|
85
|
+
onGroupByChange: (key: string | null) => void
|
|
86
|
+
// Sort key for display in main panel
|
|
87
|
+
primarySortKey?: string
|
|
88
|
+
// Conditional formatting
|
|
89
|
+
conditionalRules: ConditionalRule[]
|
|
90
|
+
onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
|
|
91
|
+
onRemoveConditionalRule: (id: string) => void
|
|
92
|
+
onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
|
|
93
|
+
/** Filter field defs for drawer + conditional rules — defaults to FILTER_FIELDS; pass column-derived defs to match the table */
|
|
94
|
+
filterFields?: FilterFieldDef[]
|
|
95
|
+
// View type
|
|
96
|
+
currentView?: DataListViewType
|
|
97
|
+
onViewChange?: (view: DataListViewType) => void
|
|
98
|
+
/** Lifecycle context (e.g. tab filter) — shown in the drawer header */
|
|
99
|
+
lifecycleTabLabel?: string
|
|
100
|
+
/**
|
|
101
|
+
* Column labels for the active table definition (placements use dynamic columns per tab).
|
|
102
|
+
* When set, overrides static `COLUMNS` from types for Columns / Sort / Group labels.
|
|
103
|
+
*/
|
|
104
|
+
fieldDefinitions?: { key: string; label: string; sortable?: boolean }[]
|
|
105
|
+
resolveColumnLabel?: (key: string) => string
|
|
106
|
+
/** Shared display options (table + board); persisted at page level. */
|
|
107
|
+
displayOptions: DataListDisplayOptions
|
|
108
|
+
onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
|
|
109
|
+
/**
|
|
110
|
+
* When the active view is Board and more than one entry is provided, shows a control to pick
|
|
111
|
+
* which field defines swimlane columns (`displayOptions.boardGroupByColumnKey`).
|
|
112
|
+
*/
|
|
113
|
+
boardGroupByColumnOptions?: { key: string; label: string }[]
|
|
114
|
+
/** Optional custom option renderer for filter values (e.g. status chips). */
|
|
115
|
+
renderFilterOptionValue?: (fieldKey: string, value: string) => React.ReactNode
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type SheetPanel = "main" | "table-display" | "filter" | "sort" | "group" | "columns" | "conditional-rules"
|
|
119
|
+
|
|
120
|
+
export function TablePropertiesDrawer({
|
|
121
|
+
open,
|
|
122
|
+
onOpenChange,
|
|
123
|
+
showGridlines,
|
|
124
|
+
onShowGridlinesChange,
|
|
125
|
+
rowHeight,
|
|
126
|
+
onRowHeightChange,
|
|
127
|
+
pagination,
|
|
128
|
+
onPaginationChange,
|
|
129
|
+
activeFilters,
|
|
130
|
+
onAddFilter,
|
|
131
|
+
onUpdateFilter,
|
|
132
|
+
onRemoveFilter,
|
|
133
|
+
getFilterConnector,
|
|
134
|
+
onToggleFilterConnector,
|
|
135
|
+
filterBarVisible,
|
|
136
|
+
onFilterBarVisibleChange,
|
|
137
|
+
drawerExpandedFilters,
|
|
138
|
+
onDrawerExpandedFiltersChange,
|
|
139
|
+
totalRows,
|
|
140
|
+
filteredRows,
|
|
141
|
+
sortRules,
|
|
142
|
+
onSortRulesChange,
|
|
143
|
+
onAddSortRule,
|
|
144
|
+
onRemoveSortRule,
|
|
145
|
+
onToggleSortDir,
|
|
146
|
+
colOrder,
|
|
147
|
+
onColOrderChange,
|
|
148
|
+
hiddenCols,
|
|
149
|
+
onToggleColVisibility,
|
|
150
|
+
onMoveCol,
|
|
151
|
+
groupBy,
|
|
152
|
+
onGroupByChange,
|
|
153
|
+
primarySortKey,
|
|
154
|
+
conditionalRules,
|
|
155
|
+
onAddConditionalRule,
|
|
156
|
+
onRemoveConditionalRule,
|
|
157
|
+
onUpdateConditionalRule,
|
|
158
|
+
filterFields = FILTER_FIELDS,
|
|
159
|
+
currentView,
|
|
160
|
+
onViewChange,
|
|
161
|
+
lifecycleTabLabel,
|
|
162
|
+
fieldDefinitions,
|
|
163
|
+
resolveColumnLabel: resolveColumnLabelProp,
|
|
164
|
+
displayOptions,
|
|
165
|
+
onDisplayOptionsChange,
|
|
166
|
+
boardGroupByColumnOptions,
|
|
167
|
+
renderFilterOptionValue,
|
|
168
|
+
}: TablePropertiesDrawerProps) {
|
|
169
|
+
const [sheetPanel, setSheetPanel] = React.useState<SheetPanel>("main")
|
|
170
|
+
|
|
171
|
+
// Reset to main panel when drawer is closed
|
|
172
|
+
React.useEffect(() => {
|
|
173
|
+
if (!open) setSheetPanel("main")
|
|
174
|
+
}, [open])
|
|
175
|
+
|
|
176
|
+
const resolveColumnLabel = React.useCallback(
|
|
177
|
+
(key: string) =>
|
|
178
|
+
resolveColumnLabelProp?.(key)
|
|
179
|
+
?? COLUMNS.find(c => c.key === key)?.label
|
|
180
|
+
?? key,
|
|
181
|
+
[resolveColumnLabelProp],
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
const sortFieldList = React.useMemo(() => {
|
|
185
|
+
if (fieldDefinitions?.length) {
|
|
186
|
+
return fieldDefinitions.filter(f => f.sortable !== false && f.key !== "select" && f.key !== "actions")
|
|
187
|
+
}
|
|
188
|
+
return COLUMNS.filter(c => c.sortable && c.sortKey).map(c => ({ key: c.key, label: c.label, sortable: true }))
|
|
189
|
+
}, [fieldDefinitions])
|
|
190
|
+
|
|
191
|
+
const groupFieldList = React.useMemo(() => {
|
|
192
|
+
if (fieldDefinitions?.length) {
|
|
193
|
+
return fieldDefinitions.filter(f => f.key !== "select" && f.key !== "actions")
|
|
194
|
+
}
|
|
195
|
+
return COLUMNS.filter(c => c.key !== "select" && c.key !== "actions")
|
|
196
|
+
}, [fieldDefinitions])
|
|
197
|
+
|
|
198
|
+
const viewSurface = currentView ?? "table"
|
|
199
|
+
const isBoardView = viewSurface === "board"
|
|
200
|
+
const boardGroupByLabel =
|
|
201
|
+
boardGroupByColumnOptions?.find(o => o.key === displayOptions.boardGroupByColumnKey)?.label
|
|
202
|
+
const viewDisplayLabel = dataListViewLabel(viewSurface)
|
|
203
|
+
const viewDisplayDesc = (() => {
|
|
204
|
+
if (viewSurface === "board") {
|
|
205
|
+
return [
|
|
206
|
+
boardGroupByLabel ? `By ${boardGroupByLabel}` : null,
|
|
207
|
+
`${displayOptions.boardLineCount}-line`,
|
|
208
|
+
displayOptions.showColumnLabels ? "Column labels" : "No labels",
|
|
209
|
+
]
|
|
210
|
+
.filter(Boolean)
|
|
211
|
+
.join(" · ")
|
|
212
|
+
}
|
|
213
|
+
if (viewSurface === "list") {
|
|
214
|
+
return [
|
|
215
|
+
displayOptions.showColumnLabels ? "Column labels" : "No labels",
|
|
216
|
+
displayOptions.showToolbarSearch ? "Toolbar search" : "No search",
|
|
217
|
+
].join(" · ")
|
|
218
|
+
}
|
|
219
|
+
if (viewSurface === "dashboard") {
|
|
220
|
+
return "Charts · KPI metrics"
|
|
221
|
+
}
|
|
222
|
+
return [showGridlines ? "Gridlines" : null, pagination ? "Paginated" : null].filter(Boolean).join(" · ") || "Default"
|
|
223
|
+
})()
|
|
224
|
+
const viewDisplayIcon =
|
|
225
|
+
DATA_LIST_VIEW_TILES.find(t => t.value === viewSurface)?.icon ?? "fa-table"
|
|
226
|
+
|
|
227
|
+
// ── Sort drag-and-drop ────────────────────────────────────────────────────
|
|
228
|
+
const sortDrag = useDraggableList(sortRules, r => r.id, onSortRulesChange)
|
|
229
|
+
|
|
230
|
+
// ── Columns drag-and-drop ─────────────────────────────────────────────────
|
|
231
|
+
const orderable = colOrder.filter(k => k !== "select" && k !== "actions")
|
|
232
|
+
const colDrag = useDraggableList(
|
|
233
|
+
orderable,
|
|
234
|
+
k => k,
|
|
235
|
+
newOrder => onColOrderChange(["select", ...newOrder, "actions"]),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
// Current primary sort label for display in main panel
|
|
239
|
+
const primarySortLabel = primarySortKey
|
|
240
|
+
? resolveColumnLabel(primarySortKey)
|
|
241
|
+
: sortRules[0]?.fieldKey
|
|
242
|
+
? resolveColumnLabel(sortRules[0].fieldKey)
|
|
243
|
+
: "—"
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
247
|
+
<SheetContent
|
|
248
|
+
side="right"
|
|
249
|
+
showCloseButton={false}
|
|
250
|
+
showOverlay={false}
|
|
251
|
+
// w-[min(20rem,calc(100vw-1rem))]: cap to viewport width - 1rem at narrow/zoomed viewports
|
|
252
|
+
// so the drawer never overflows horizontally. Use 100svh so height is correct on mobile.
|
|
253
|
+
className="w-[min(20rem,calc(100vw-1rem))] p-0 gap-0 flex flex-col border border-border shadow-xl rounded-xl overflow-hidden"
|
|
254
|
+
style={{ top: "0.5rem", bottom: "0.5rem", right: "0.5rem", height: "calc(100svh - 1rem)" }}
|
|
255
|
+
>
|
|
256
|
+
|
|
257
|
+
{sheetPanel === "main" ? (
|
|
258
|
+
<>
|
|
259
|
+
{/* Header */}
|
|
260
|
+
<div className="flex items-center justify-between gap-3 px-4 pt-5 pb-3">
|
|
261
|
+
<div className="min-w-0">
|
|
262
|
+
<SheetTitle className="text-base font-semibold leading-tight">Properties</SheetTitle>
|
|
263
|
+
{lifecycleTabLabel ? (
|
|
264
|
+
<p className="text-xs text-muted-foreground mt-0.5 truncate" title={lifecycleTabLabel}>
|
|
265
|
+
{lifecycleTabLabel}
|
|
266
|
+
</p>
|
|
267
|
+
) : null}
|
|
268
|
+
</div>
|
|
269
|
+
<Tip label="Close" side="bottom">
|
|
270
|
+
<Button
|
|
271
|
+
type="button"
|
|
272
|
+
variant="ghost"
|
|
273
|
+
size="icon-sm"
|
|
274
|
+
aria-label="Close"
|
|
275
|
+
onClick={() => onOpenChange(false)}
|
|
276
|
+
>
|
|
277
|
+
<i className="fa-light fa-xmark text-[13px]" aria-hidden="true" />
|
|
278
|
+
</Button>
|
|
279
|
+
</Tip>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
{/* View type switcher — card tiles like export file format */}
|
|
283
|
+
{onViewChange && currentView && (
|
|
284
|
+
<div className="px-4 pb-3">
|
|
285
|
+
<SelectionTileGrid<DataListViewType>
|
|
286
|
+
sectionLabel="View type"
|
|
287
|
+
options={DATA_LIST_VIEW_TILES}
|
|
288
|
+
columns={4}
|
|
289
|
+
value={currentView}
|
|
290
|
+
onValueChange={onViewChange}
|
|
291
|
+
interaction="button"
|
|
292
|
+
idPrefix="props-view"
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
|
|
297
|
+
{/* Option list — inset rows + rounded hover (not edge-to-edge) */}
|
|
298
|
+
<div className="flex-1 overflow-y-auto py-2 px-3 space-y-1">
|
|
299
|
+
{([
|
|
300
|
+
{
|
|
301
|
+
id: "table-display" as SheetPanel,
|
|
302
|
+
icon: viewDisplayIcon,
|
|
303
|
+
label: viewDisplayLabel,
|
|
304
|
+
desc: viewDisplayDesc,
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
id: "filter" as SheetPanel,
|
|
308
|
+
icon: "fa-filter",
|
|
309
|
+
label: "Filter",
|
|
310
|
+
desc: activeFilters.length === 0
|
|
311
|
+
? `Showing all ${filteredRows} rows.`
|
|
312
|
+
: `${activeFilters.length} filter${activeFilters.length !== 1 ? "s" : ""} active · ${filteredRows} rows.`,
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
id: "sort" as SheetPanel,
|
|
316
|
+
icon: "fa-arrow-up-arrow-down",
|
|
317
|
+
label: "Sort",
|
|
318
|
+
desc: `Sorted by ${primarySortLabel}.`,
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: "group" as SheetPanel,
|
|
322
|
+
icon: "fa-layer-group",
|
|
323
|
+
label: "Group",
|
|
324
|
+
desc: groupBy
|
|
325
|
+
? `Grouped by ${resolveColumnLabel(groupBy)}.`
|
|
326
|
+
: "No grouping.",
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: "columns" as SheetPanel,
|
|
330
|
+
icon: "fa-table-columns",
|
|
331
|
+
label: "Columns",
|
|
332
|
+
desc: hiddenCols.size === 0
|
|
333
|
+
? "All columns visible."
|
|
334
|
+
: `${hiddenCols.size} column${hiddenCols.size !== 1 ? "s" : ""} hidden.`,
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
id: "conditional-rules" as SheetPanel,
|
|
338
|
+
icon: "fa-palette",
|
|
339
|
+
label: "Conditional rules",
|
|
340
|
+
desc: conditionalRules.length === 0
|
|
341
|
+
? "No rules applied."
|
|
342
|
+
: `${conditionalRules.length} rule${conditionalRules.length !== 1 ? "s" : ""} active.`,
|
|
343
|
+
},
|
|
344
|
+
] as { id: SheetPanel; icon: string; label: string; desc: string }[]).map(item => (
|
|
345
|
+
<Button
|
|
346
|
+
key={item.id}
|
|
347
|
+
type="button"
|
|
348
|
+
variant="ghost"
|
|
349
|
+
onClick={() => setSheetPanel(item.id)}
|
|
350
|
+
className={cn(
|
|
351
|
+
"w-full h-auto justify-start gap-3 px-3 py-3 rounded-2xl font-normal border border-transparent",
|
|
352
|
+
"hover:bg-muted/60 hover:text-foreground",
|
|
353
|
+
"focus-visible:bg-muted/60 focus-visible:text-foreground",
|
|
354
|
+
)}
|
|
355
|
+
>
|
|
356
|
+
<span className="inline-flex items-center justify-center size-9 rounded-lg bg-secondary border border-border shrink-0">
|
|
357
|
+
<i className={`fa-light ${item.icon} text-[15px] text-secondary-foreground`} aria-hidden="true" />
|
|
358
|
+
</span>
|
|
359
|
+
<span className="flex-1 min-w-0 text-left">
|
|
360
|
+
<span className="block text-sm font-medium text-foreground">{item.label}</span>
|
|
361
|
+
<span className="block text-xs text-muted-foreground mt-0.5">{item.desc}</span>
|
|
362
|
+
</span>
|
|
363
|
+
<i className="fa-light fa-chevron-right text-xs text-muted-foreground shrink-0" aria-hidden="true" />
|
|
364
|
+
</Button>
|
|
365
|
+
))}
|
|
366
|
+
</div>
|
|
367
|
+
</>
|
|
368
|
+
) : (
|
|
369
|
+
<>
|
|
370
|
+
{/* Sub-panel header — back + title stack as one cluster; close aligns to row center */}
|
|
371
|
+
<div className="flex items-center justify-between gap-3 px-4 pt-4 pb-3">
|
|
372
|
+
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
373
|
+
<Tip label="Back to Properties" side="bottom">
|
|
374
|
+
<Button
|
|
375
|
+
type="button"
|
|
376
|
+
variant="ghost"
|
|
377
|
+
size="icon-sm"
|
|
378
|
+
className="shrink-0"
|
|
379
|
+
aria-label="Back to Properties"
|
|
380
|
+
onClick={() => setSheetPanel("main")}
|
|
381
|
+
>
|
|
382
|
+
<i className="fa-light fa-chevron-left text-[13px]" aria-hidden="true" />
|
|
383
|
+
</Button>
|
|
384
|
+
</Tip>
|
|
385
|
+
<div className="min-w-0">
|
|
386
|
+
<SheetTitle className="text-base font-semibold text-foreground leading-tight flex items-center gap-1.5">
|
|
387
|
+
{{
|
|
388
|
+
"table-display": viewDisplayLabel,
|
|
389
|
+
filter: "Filter",
|
|
390
|
+
sort: "Sort",
|
|
391
|
+
group: "Group",
|
|
392
|
+
columns: "Columns",
|
|
393
|
+
"conditional-rules": "Conditional rules",
|
|
394
|
+
main: "",
|
|
395
|
+
}[sheetPanel]}
|
|
396
|
+
{sheetPanel === "filter" && (
|
|
397
|
+
<i className="fa-light fa-circle-question text-xs text-muted-foreground" aria-hidden="true" />
|
|
398
|
+
)}
|
|
399
|
+
</SheetTitle>
|
|
400
|
+
{sheetPanel === "filter" && (
|
|
401
|
+
<p
|
|
402
|
+
className="text-xs text-muted-foreground mt-0.5"
|
|
403
|
+
aria-live="polite"
|
|
404
|
+
aria-atomic="true"
|
|
405
|
+
>
|
|
406
|
+
{activeFilters.length === 0
|
|
407
|
+
? `Showing all ${filteredRows} rows`
|
|
408
|
+
: `${filteredRows} of ${totalRows} rows match · ${activeFilters.length} filter${activeFilters.length !== 1 ? "s" : ""} active`}
|
|
409
|
+
</p>
|
|
410
|
+
)}
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
<Tip label="Close" side="bottom">
|
|
414
|
+
<Button
|
|
415
|
+
type="button"
|
|
416
|
+
variant="ghost"
|
|
417
|
+
size="icon-sm"
|
|
418
|
+
className="shrink-0"
|
|
419
|
+
aria-label="Close panel"
|
|
420
|
+
onClick={() => onOpenChange(false)}
|
|
421
|
+
>
|
|
422
|
+
<i className="fa-light fa-xmark text-[13px]" aria-hidden="true" />
|
|
423
|
+
</Button>
|
|
424
|
+
</Tip>
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
<div className="flex-1 overflow-y-auto">
|
|
428
|
+
|
|
429
|
+
{/* ── Table / Board display ── */}
|
|
430
|
+
{sheetPanel === "table-display" && (
|
|
431
|
+
<div className="p-4 space-y-5">
|
|
432
|
+
{isBoardView ? (
|
|
433
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
434
|
+
{dataListViewLabel("board")} groups rows into columns. Sort, filter, and column settings apply to the same dataset as other views (e.g. Table view).
|
|
435
|
+
</p>
|
|
436
|
+
) : null}
|
|
437
|
+
|
|
438
|
+
{isBoardView && boardGroupByColumnOptions && boardGroupByColumnOptions.length > 1 ? (
|
|
439
|
+
<div className="flex items-center justify-between gap-3 py-2">
|
|
440
|
+
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
|
441
|
+
<span className="inline-flex items-center justify-center size-9 rounded-lg bg-secondary border border-border shrink-0">
|
|
442
|
+
<i className="fa-light fa-table-columns text-[15px] text-secondary-foreground" aria-hidden="true" />
|
|
443
|
+
</span>
|
|
444
|
+
<div className="min-w-0">
|
|
445
|
+
<p className="text-sm font-medium text-foreground leading-tight">Board columns</p>
|
|
446
|
+
<p className="text-xs text-muted-foreground mt-0.5">Choose which field splits the board into swimlanes.</p>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
<Select
|
|
450
|
+
value={
|
|
451
|
+
boardGroupByColumnOptions.some(o => o.key === displayOptions.boardGroupByColumnKey)
|
|
452
|
+
? displayOptions.boardGroupByColumnKey
|
|
453
|
+
: boardGroupByColumnOptions[0]!.key
|
|
454
|
+
}
|
|
455
|
+
onValueChange={v => onDisplayOptionsChange({ boardGroupByColumnKey: v })}
|
|
456
|
+
>
|
|
457
|
+
<SelectTrigger
|
|
458
|
+
size="sm"
|
|
459
|
+
className="w-[9.5rem] shrink-0"
|
|
460
|
+
id="board-group-by-field"
|
|
461
|
+
aria-label="Field for board columns"
|
|
462
|
+
>
|
|
463
|
+
<SelectValue />
|
|
464
|
+
</SelectTrigger>
|
|
465
|
+
<SelectContent align="end">
|
|
466
|
+
{boardGroupByColumnOptions.map(o => (
|
|
467
|
+
<SelectItem key={o.key} value={o.key}>
|
|
468
|
+
{o.label}
|
|
469
|
+
</SelectItem>
|
|
470
|
+
))}
|
|
471
|
+
</SelectContent>
|
|
472
|
+
</Select>
|
|
473
|
+
</div>
|
|
474
|
+
) : null}
|
|
475
|
+
|
|
476
|
+
{viewSurface === "table" ? (
|
|
477
|
+
<>
|
|
478
|
+
<div>
|
|
479
|
+
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Appearance</p>
|
|
480
|
+
<div className="space-y-1">
|
|
481
|
+
{([
|
|
482
|
+
{ id: "gridlines", icon: "fa-border-all", label: "Gridlines", checked: showGridlines, onChange: onShowGridlinesChange },
|
|
483
|
+
{ id: "pagination", icon: "fa-table-list", label: "Pagination", checked: pagination, onChange: onPaginationChange },
|
|
484
|
+
] as { id: string; icon: string; label: string; checked: boolean; onChange: (v: boolean) => void }[]).map(row => (
|
|
485
|
+
<div key={row.id} className="flex items-center justify-between py-2">
|
|
486
|
+
<div className="flex items-center gap-2.5 text-sm">
|
|
487
|
+
<i className={`fa-light ${row.icon} text-muted-foreground w-4 text-center`} aria-hidden="true" />
|
|
488
|
+
<label htmlFor={`toggle-${row.id}`} className="cursor-pointer select-none">{row.label}</label>
|
|
489
|
+
</div>
|
|
490
|
+
<ToggleSwitch id={`toggle-${row.id}`} checked={row.checked} onChange={row.onChange} />
|
|
491
|
+
</div>
|
|
492
|
+
))}
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<div className="border-t border-border pt-4">
|
|
497
|
+
<SelectionTileGrid<RowHeight>
|
|
498
|
+
sectionLabel="Row height"
|
|
499
|
+
options={ROW_HEIGHT_TILES}
|
|
500
|
+
columns={3}
|
|
501
|
+
value={rowHeight}
|
|
502
|
+
onValueChange={onRowHeightChange}
|
|
503
|
+
interaction="button"
|
|
504
|
+
idPrefix="row-height"
|
|
505
|
+
/>
|
|
506
|
+
</div>
|
|
507
|
+
</>
|
|
508
|
+
) : null}
|
|
509
|
+
|
|
510
|
+
<div
|
|
511
|
+
className={cn(
|
|
512
|
+
"space-y-3",
|
|
513
|
+
(viewSurface === "board" || viewSurface === "table") && "border-t border-border pt-4",
|
|
514
|
+
)}
|
|
515
|
+
>
|
|
516
|
+
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Display options</p>
|
|
517
|
+
<div className="space-y-1">
|
|
518
|
+
{isBoardView && (
|
|
519
|
+
<div className="flex items-center justify-between gap-2 py-2">
|
|
520
|
+
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
|
521
|
+
<span className="inline-flex items-center justify-center size-9 rounded-lg bg-secondary border border-border shrink-0">
|
|
522
|
+
<i className="fa-light fa-file-lines text-[15px] text-secondary-foreground" aria-hidden="true" />
|
|
523
|
+
</span>
|
|
524
|
+
<div className="min-w-0">
|
|
525
|
+
<p className="text-sm font-medium text-foreground leading-tight">Line count</p>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
<Select
|
|
529
|
+
value={String(displayOptions.boardLineCount)}
|
|
530
|
+
onValueChange={v =>
|
|
531
|
+
onDisplayOptionsChange({ boardLineCount: Number(v) as 1 | 2 | 3 })}
|
|
532
|
+
>
|
|
533
|
+
<SelectTrigger size="sm" className="w-[6.5rem] shrink-0" id="board-line-count" aria-label="Line count">
|
|
534
|
+
<SelectValue />
|
|
535
|
+
</SelectTrigger>
|
|
536
|
+
<SelectContent align="end">
|
|
537
|
+
<SelectItem value="1">1 line</SelectItem>
|
|
538
|
+
<SelectItem value="2">2 lines</SelectItem>
|
|
539
|
+
<SelectItem value="3">3 lines</SelectItem>
|
|
540
|
+
</SelectContent>
|
|
541
|
+
</Select>
|
|
542
|
+
</div>
|
|
543
|
+
)}
|
|
544
|
+
|
|
545
|
+
{viewSurface === "table" && (
|
|
546
|
+
<div className="flex items-center justify-between gap-2 py-2">
|
|
547
|
+
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
|
548
|
+
<span className="inline-flex items-center justify-center size-9 rounded-lg bg-secondary border border-border shrink-0">
|
|
549
|
+
<i className="fa-light fa-font text-[15px] text-secondary-foreground" aria-hidden="true" />
|
|
550
|
+
</span>
|
|
551
|
+
<div className="min-w-0">
|
|
552
|
+
<p className="text-sm font-medium text-foreground leading-tight">Table title</p>
|
|
553
|
+
<p className="text-xs text-muted-foreground mt-0.5">Show the page heading and subtitle.</p>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
<ToggleSwitch
|
|
557
|
+
id="toggle-view-title"
|
|
558
|
+
checked={displayOptions.showViewTitle}
|
|
559
|
+
onChange={v => onDisplayOptionsChange({ showViewTitle: v })}
|
|
560
|
+
/>
|
|
561
|
+
</div>
|
|
562
|
+
)}
|
|
563
|
+
|
|
564
|
+
<div className="flex items-center justify-between gap-2 py-2">
|
|
565
|
+
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
|
566
|
+
<span className="inline-flex items-center justify-center size-9 rounded-lg bg-secondary border border-border shrink-0">
|
|
567
|
+
<i className="fa-light fa-table-columns text-[15px] text-secondary-foreground" aria-hidden="true" />
|
|
568
|
+
</span>
|
|
569
|
+
<div className="min-w-0">
|
|
570
|
+
<p className="text-sm font-medium text-foreground leading-tight">Column labels</p>
|
|
571
|
+
{viewSurface === "table" ? (
|
|
572
|
+
<p className="text-xs text-muted-foreground mt-0.5">Column headers in the table.</p>
|
|
573
|
+
) : viewSurface === "list" ? (
|
|
574
|
+
<p className="text-xs text-muted-foreground mt-0.5">Column headers in the list.</p>
|
|
575
|
+
) : null}
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
<ToggleSwitch
|
|
579
|
+
id="toggle-column-labels"
|
|
580
|
+
checked={displayOptions.showColumnLabels}
|
|
581
|
+
onChange={v => onDisplayOptionsChange({ showColumnLabels: v })}
|
|
582
|
+
/>
|
|
583
|
+
</div>
|
|
584
|
+
|
|
585
|
+
{isBoardView && (
|
|
586
|
+
<>
|
|
587
|
+
<div className="flex items-center justify-between gap-2 py-2">
|
|
588
|
+
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
|
589
|
+
<span className="inline-flex items-center justify-center size-9 rounded-lg bg-secondary border border-border shrink-0">
|
|
590
|
+
<i className="fa-light fa-hashtag text-[15px] text-secondary-foreground" aria-hidden="true" />
|
|
591
|
+
</span>
|
|
592
|
+
<div className="min-w-0">
|
|
593
|
+
<p className="text-sm font-medium text-foreground leading-tight">Column counts</p>
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
<ToggleSwitch
|
|
597
|
+
id="toggle-board-counts"
|
|
598
|
+
checked={displayOptions.showBoardColumnCounts}
|
|
599
|
+
onChange={v => onDisplayOptionsChange({ showBoardColumnCounts: v })}
|
|
600
|
+
/>
|
|
601
|
+
</div>
|
|
602
|
+
|
|
603
|
+
<div className="flex items-center justify-between gap-2 py-2">
|
|
604
|
+
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
|
605
|
+
<span className="inline-flex items-center justify-center size-9 rounded-lg bg-secondary border border-border shrink-0">
|
|
606
|
+
<i className="fa-light fa-square-plus text-[15px] text-secondary-foreground" aria-hidden="true" />
|
|
607
|
+
</span>
|
|
608
|
+
<div className="min-w-0">
|
|
609
|
+
<p className="text-sm font-medium text-foreground leading-tight">Above new card button</p>
|
|
610
|
+
</div>
|
|
611
|
+
</div>
|
|
612
|
+
<ToggleSwitch
|
|
613
|
+
id="toggle-new-card-above"
|
|
614
|
+
checked={displayOptions.boardNewCardAbove}
|
|
615
|
+
onChange={v => onDisplayOptionsChange({ boardNewCardAbove: v })}
|
|
616
|
+
/>
|
|
617
|
+
</div>
|
|
618
|
+
</>
|
|
619
|
+
)}
|
|
620
|
+
|
|
621
|
+
{(viewSurface === "table" || viewSurface === "list") && (
|
|
622
|
+
<div className="flex items-center justify-between gap-2 py-2">
|
|
623
|
+
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
|
624
|
+
<span className="inline-flex items-center justify-center size-9 rounded-lg bg-secondary border border-border shrink-0">
|
|
625
|
+
<i className="fa-light fa-magnifying-glass text-[15px] text-secondary-foreground" aria-hidden="true" />
|
|
626
|
+
</span>
|
|
627
|
+
<div className="min-w-0">
|
|
628
|
+
<p className="text-sm font-medium text-foreground leading-tight">Search</p>
|
|
629
|
+
<p className="text-xs text-muted-foreground mt-0.5">Toolbar search for this view.</p>
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
<ToggleSwitch
|
|
633
|
+
id="toggle-toolbar-search"
|
|
634
|
+
checked={displayOptions.showToolbarSearch}
|
|
635
|
+
onChange={v => onDisplayOptionsChange({ showToolbarSearch: v })}
|
|
636
|
+
/>
|
|
637
|
+
</div>
|
|
638
|
+
)}
|
|
639
|
+
</div>
|
|
640
|
+
</div>
|
|
641
|
+
</div>
|
|
642
|
+
)}
|
|
643
|
+
|
|
644
|
+
{/* ── Filter ── */}
|
|
645
|
+
{sheetPanel === "filter" && (
|
|
646
|
+
<div className="px-4 py-4 space-y-2">
|
|
647
|
+
{activeFilters.length === 0 ? (
|
|
648
|
+
<div className="rounded-xl border border-border bg-muted/40 p-4 space-y-3">
|
|
649
|
+
<div className="flex items-center gap-2">
|
|
650
|
+
<span className="inline-flex items-center justify-center size-7 rounded-lg bg-background border border-border shrink-0">
|
|
651
|
+
<i className="fa-light fa-filter text-muted-foreground text-xs" aria-hidden="true" />
|
|
652
|
+
</span>
|
|
653
|
+
<p className="text-sm font-medium text-foreground">No filters yet</p>
|
|
654
|
+
</div>
|
|
655
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
656
|
+
Use filters to show only the rows you need. With multiple filters, use <span className="font-medium text-foreground/80">and</span> or <span className="font-medium text-foreground/80">or</span> between them to control how they combine.
|
|
657
|
+
</p>
|
|
658
|
+
<div className="space-y-1.5">
|
|
659
|
+
{[
|
|
660
|
+
{ icon: "fa-circle-1", text: "Click \"Add filter\" below" },
|
|
661
|
+
{ icon: "fa-circle-2", text: "Choose a field to filter by" },
|
|
662
|
+
{ icon: "fa-circle-3", text: "Pick a condition and value" },
|
|
663
|
+
].map(step => (
|
|
664
|
+
<div key={step.icon} className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
665
|
+
<i className={`fa-light ${step.icon} text-muted-foreground text-xs shrink-0`} aria-hidden="true" />
|
|
666
|
+
{step.text}
|
|
667
|
+
</div>
|
|
668
|
+
))}
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
) : (
|
|
672
|
+
<>
|
|
673
|
+
{activeFilters.map((f, idx) => {
|
|
674
|
+
const fieldDef = filterFields.find(fd => fd.key === f.fieldKey)
|
|
675
|
+
if (!fieldDef) return null
|
|
676
|
+
const leftId = idx > 0 ? activeFilters[idx - 1]!.id : null
|
|
677
|
+
const connector = leftId ? getFilterConnector(leftId) : "and"
|
|
678
|
+
return (
|
|
679
|
+
<React.Fragment key={f.id}>
|
|
680
|
+
{idx > 0 && leftId && (
|
|
681
|
+
<div className="flex items-center gap-2 py-1">
|
|
682
|
+
<div className="flex-1 h-px bg-border" aria-hidden="true" />
|
|
683
|
+
<Tip label="Click to switch: AND — every filter must match; OR — any matching filter is enough." side="top">
|
|
684
|
+
<button
|
|
685
|
+
type="button"
|
|
686
|
+
onClick={() => onToggleFilterConnector(leftId)}
|
|
687
|
+
className={cn(
|
|
688
|
+
"shrink-0 rounded-md border px-2.5 py-0.5 text-xs font-semibold uppercase tracking-wide transition-colors",
|
|
689
|
+
"border-border bg-muted/40 text-muted-foreground hover:bg-interactive-hover hover:text-interactive-hover-foreground",
|
|
690
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
691
|
+
)}
|
|
692
|
+
aria-label={
|
|
693
|
+
connector === "and"
|
|
694
|
+
? "Filters are combined with AND. Click to use OR instead."
|
|
695
|
+
: "Filters are combined with OR. Click to use AND instead."
|
|
696
|
+
}
|
|
697
|
+
>
|
|
698
|
+
{connector}
|
|
699
|
+
</button>
|
|
700
|
+
</Tip>
|
|
701
|
+
<div className="flex-1 h-px bg-border" aria-hidden="true" />
|
|
702
|
+
</div>
|
|
703
|
+
)}
|
|
704
|
+
<DrawerFilterCard
|
|
705
|
+
filter={f}
|
|
706
|
+
fieldDef={fieldDef}
|
|
707
|
+
expanded={drawerExpandedFilters.has(f.id)}
|
|
708
|
+
onToggleExpand={() => onDrawerExpandedFiltersChange(prev => {
|
|
709
|
+
const next = new Set(prev)
|
|
710
|
+
if (next.has(f.id)) next.delete(f.id)
|
|
711
|
+
else next.add(f.id)
|
|
712
|
+
return next
|
|
713
|
+
})}
|
|
714
|
+
onUpdate={onUpdateFilter}
|
|
715
|
+
onRemove={id => {
|
|
716
|
+
onRemoveFilter(id)
|
|
717
|
+
onDrawerExpandedFiltersChange(prev => { const next = new Set(prev); next.delete(id); return next })
|
|
718
|
+
}}
|
|
719
|
+
renderOptionLabel={value => renderFilterOptionValue?.(f.fieldKey, value)}
|
|
720
|
+
/>
|
|
721
|
+
</React.Fragment>
|
|
722
|
+
)
|
|
723
|
+
})}
|
|
724
|
+
</>
|
|
725
|
+
)}
|
|
726
|
+
|
|
727
|
+
{/* Add filter + Remove all */}
|
|
728
|
+
<div className="flex items-center gap-2 pt-2">
|
|
729
|
+
<DropdownMenu>
|
|
730
|
+
<DropdownMenuTrigger asChild>
|
|
731
|
+
<Button
|
|
732
|
+
type="button"
|
|
733
|
+
variant="outline"
|
|
734
|
+
className="flex-1 gap-1.5 h-8 border-dashed text-muted-foreground"
|
|
735
|
+
>
|
|
736
|
+
<i className="fa-light fa-plus text-xs" aria-hidden="true" />
|
|
737
|
+
Add filter
|
|
738
|
+
</Button>
|
|
739
|
+
</DropdownMenuTrigger>
|
|
740
|
+
<DropdownMenuContent align="start" className="w-48">
|
|
741
|
+
<DropdownMenuLabel className="text-xs">Filter by field</DropdownMenuLabel>
|
|
742
|
+
<DropdownMenuSeparator />
|
|
743
|
+
{filterFields.map(f => (
|
|
744
|
+
<DropdownMenuItem key={f.key} onClick={() => onAddFilter(f.key)}>
|
|
745
|
+
<i className={`fa-light ${f.icon}`} aria-hidden="true" />
|
|
746
|
+
{f.label}
|
|
747
|
+
</DropdownMenuItem>
|
|
748
|
+
))}
|
|
749
|
+
</DropdownMenuContent>
|
|
750
|
+
</DropdownMenu>
|
|
751
|
+
{activeFilters.length > 0 && (
|
|
752
|
+
<Button
|
|
753
|
+
type="button"
|
|
754
|
+
variant="ghost"
|
|
755
|
+
size="sm"
|
|
756
|
+
className="shrink-0 text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
757
|
+
onClick={() => { activeFilters.forEach(f => onRemoveFilter(f.id)); onDrawerExpandedFiltersChange(new Set()) }}
|
|
758
|
+
>
|
|
759
|
+
Remove all
|
|
760
|
+
</Button>
|
|
761
|
+
)}
|
|
762
|
+
</div>
|
|
763
|
+
|
|
764
|
+
{/* Enable filter bar toggle */}
|
|
765
|
+
<div className="flex items-start justify-between gap-3 pt-3 mt-1 border-t border-border">
|
|
766
|
+
<div>
|
|
767
|
+
<label htmlFor="toggle-filter-bar" className="text-sm font-medium text-foreground cursor-pointer">Enable filter bar</label>
|
|
768
|
+
<p className="text-xs text-muted-foreground mt-0.5">Show filters above the table.</p>
|
|
769
|
+
</div>
|
|
770
|
+
<ToggleSwitch id="toggle-filter-bar" checked={filterBarVisible} onChange={onFilterBarVisibleChange} />
|
|
771
|
+
</div>
|
|
772
|
+
</div>
|
|
773
|
+
)}
|
|
774
|
+
|
|
775
|
+
{/* ── Sort ── */}
|
|
776
|
+
{sheetPanel === "sort" && (
|
|
777
|
+
<div className="px-4 py-4 space-y-2">
|
|
778
|
+
{sortRules.length === 0 ? (
|
|
779
|
+
/* Empty state */
|
|
780
|
+
<div className="rounded-xl border border-dashed border-border bg-muted/30 px-4 py-6 text-center space-y-2">
|
|
781
|
+
<div className="inline-flex items-center justify-center size-9 rounded-lg bg-muted mb-1">
|
|
782
|
+
<i className="fa-light fa-arrow-up-arrow-down text-muted-foreground text-[16px]" aria-hidden="true" />
|
|
783
|
+
</div>
|
|
784
|
+
<p className="text-sm font-medium text-foreground">No sorts applied</p>
|
|
785
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
786
|
+
Add a sort rule to order rows by any field. Multiple rules are applied in priority order.
|
|
787
|
+
</p>
|
|
788
|
+
<div className="space-y-1.5 text-left pt-1">
|
|
789
|
+
{[
|
|
790
|
+
{ icon: "fa-circle-1", text: "Click \"Add sort\" below" },
|
|
791
|
+
{ icon: "fa-circle-2", text: "Choose a field to sort by" },
|
|
792
|
+
{ icon: "fa-circle-3", text: "Toggle ascending or descending" },
|
|
793
|
+
].map(step => (
|
|
794
|
+
<div key={step.icon} className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
795
|
+
<i className={`fa-light ${step.icon} text-muted-foreground text-xs shrink-0`} aria-hidden="true" />
|
|
796
|
+
{step.text}
|
|
797
|
+
</div>
|
|
798
|
+
))}
|
|
799
|
+
</div>
|
|
800
|
+
</div>
|
|
801
|
+
) : (
|
|
802
|
+
sortRules.map((rule, idx) => {
|
|
803
|
+
const dragProps = sortDrag.getItemProps(rule.id)
|
|
804
|
+
return (
|
|
805
|
+
<React.Fragment key={rule.id}>
|
|
806
|
+
{idx > 0 && (
|
|
807
|
+
<div className="flex items-center gap-2 py-0.5">
|
|
808
|
+
<div className="flex-1 h-px bg-border" />
|
|
809
|
+
<span className="text-xs font-medium text-muted-foreground px-1">then by</span>
|
|
810
|
+
<div className="flex-1 h-px bg-border" />
|
|
811
|
+
</div>
|
|
812
|
+
)}
|
|
813
|
+
<div
|
|
814
|
+
{...dragProps}
|
|
815
|
+
className={cn(
|
|
816
|
+
"transition-all",
|
|
817
|
+
dragProps["data-dragging"] && "opacity-40",
|
|
818
|
+
dragProps["data-over"] && "ring-2 ring-ring bg-accent/30 rounded-lg",
|
|
819
|
+
)}
|
|
820
|
+
>
|
|
821
|
+
<DrawerSortCard
|
|
822
|
+
rule={rule}
|
|
823
|
+
fieldLabel={resolveColumnLabel(rule.fieldKey)}
|
|
824
|
+
isPrimary={idx === 0}
|
|
825
|
+
onRemove={() => onRemoveSortRule(rule.id)}
|
|
826
|
+
onToggleDir={() => onToggleSortDir(rule.id)}
|
|
827
|
+
/>
|
|
828
|
+
</div>
|
|
829
|
+
</React.Fragment>
|
|
830
|
+
)
|
|
831
|
+
})
|
|
832
|
+
)}
|
|
833
|
+
|
|
834
|
+
{/* Add sort + Remove all */}
|
|
835
|
+
<div className="flex items-center gap-2 pt-2">
|
|
836
|
+
<DropdownMenu>
|
|
837
|
+
<DropdownMenuTrigger asChild>
|
|
838
|
+
<Button
|
|
839
|
+
type="button"
|
|
840
|
+
variant="outline"
|
|
841
|
+
className="flex-1 gap-1.5 h-8 border-dashed text-muted-foreground"
|
|
842
|
+
>
|
|
843
|
+
<i className="fa-light fa-plus text-xs" aria-hidden="true" />
|
|
844
|
+
Add sort
|
|
845
|
+
</Button>
|
|
846
|
+
</DropdownMenuTrigger>
|
|
847
|
+
<DropdownMenuContent align="start" className="w-48">
|
|
848
|
+
<DropdownMenuLabel className="text-xs">Sort by field</DropdownMenuLabel>
|
|
849
|
+
<DropdownMenuSeparator />
|
|
850
|
+
{sortFieldList.filter(f => !sortRules.some(r => r.fieldKey === f.key)).map(col => (
|
|
851
|
+
<DropdownMenuItem key={col.key} onClick={() => onAddSortRule(col.key)}>
|
|
852
|
+
<i className="fa-light fa-arrow-up-arrow-down text-xs" aria-hidden="true" />
|
|
853
|
+
{col.label}
|
|
854
|
+
</DropdownMenuItem>
|
|
855
|
+
))}
|
|
856
|
+
{sortFieldList.filter(f => !sortRules.some(r => r.fieldKey === f.key)).length === 0 && (
|
|
857
|
+
<p className="px-2 py-1.5 text-xs text-muted-foreground">All fields added</p>
|
|
858
|
+
)}
|
|
859
|
+
</DropdownMenuContent>
|
|
860
|
+
</DropdownMenu>
|
|
861
|
+
{sortRules.length > 0 && (
|
|
862
|
+
<Button
|
|
863
|
+
type="button"
|
|
864
|
+
variant="ghost"
|
|
865
|
+
size="sm"
|
|
866
|
+
className="shrink-0 text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
867
|
+
onClick={() => onSortRulesChange([])}
|
|
868
|
+
>
|
|
869
|
+
Remove all
|
|
870
|
+
</Button>
|
|
871
|
+
)}
|
|
872
|
+
</div>
|
|
873
|
+
</div>
|
|
874
|
+
)}
|
|
875
|
+
|
|
876
|
+
{/* ── Group ── */}
|
|
877
|
+
{sheetPanel === "group" && (
|
|
878
|
+
<div className="p-4 space-y-2">
|
|
879
|
+
<p className="text-xs text-muted-foreground mb-3">
|
|
880
|
+
{groupBy ? `Grouped by ${resolveColumnLabel(groupBy)}.` : "No grouping applied."}
|
|
881
|
+
</p>
|
|
882
|
+
<Button
|
|
883
|
+
type="button"
|
|
884
|
+
variant="ghost"
|
|
885
|
+
onClick={() => onGroupByChange(null)}
|
|
886
|
+
className={cn("w-full justify-start gap-2 px-3 py-2 h-auto text-sm font-normal",
|
|
887
|
+
!groupBy ? "bg-accent text-accent-foreground font-medium" : "text-muted-foreground",
|
|
888
|
+
)}
|
|
889
|
+
>
|
|
890
|
+
<i className="fa-light fa-ban text-xs" aria-hidden="true" />
|
|
891
|
+
None
|
|
892
|
+
</Button>
|
|
893
|
+
{groupFieldList.map(col => (
|
|
894
|
+
<Button
|
|
895
|
+
key={col.key}
|
|
896
|
+
type="button"
|
|
897
|
+
variant="ghost"
|
|
898
|
+
onClick={() => onGroupByChange(groupBy === col.key ? null : col.key)}
|
|
899
|
+
className={cn("w-full justify-start gap-2 px-3 py-2 h-auto text-sm font-normal",
|
|
900
|
+
groupBy === col.key ? "bg-accent text-accent-foreground font-medium" : "",
|
|
901
|
+
)}
|
|
902
|
+
>
|
|
903
|
+
<i className="fa-light fa-layer-group text-xs text-muted-foreground" aria-hidden="true" />
|
|
904
|
+
{col.label}
|
|
905
|
+
{groupBy === col.key && <i className="fa-solid fa-check text-accent-foreground text-xs ml-auto" aria-hidden="true" />}
|
|
906
|
+
</Button>
|
|
907
|
+
))}
|
|
908
|
+
</div>
|
|
909
|
+
)}
|
|
910
|
+
|
|
911
|
+
{/* ── Columns ── */}
|
|
912
|
+
{sheetPanel === "columns" && (
|
|
913
|
+
<div className="px-4 py-4">
|
|
914
|
+
{isBoardView ? (
|
|
915
|
+
<p className="text-xs text-muted-foreground mb-3">
|
|
916
|
+
Column visibility and order apply when you use Table view. They are saved with this tab.
|
|
917
|
+
</p>
|
|
918
|
+
) : null}
|
|
919
|
+
<p className="text-xs text-muted-foreground mb-3">
|
|
920
|
+
{hiddenCols.size === 0
|
|
921
|
+
? "All columns visible. Drag to reorder."
|
|
922
|
+
: `${hiddenCols.size} column${hiddenCols.size !== 1 ? "s" : ""} hidden. Drag handle to reorder.`}
|
|
923
|
+
</p>
|
|
924
|
+
<div className="space-y-0.5" role="list" aria-label="Column order and visibility">
|
|
925
|
+
{orderable.map((key, idx, arr) => {
|
|
926
|
+
const dragProps = colDrag.getItemProps(key)
|
|
927
|
+
return (
|
|
928
|
+
<ColumnRow
|
|
929
|
+
key={key}
|
|
930
|
+
label={resolveColumnLabel(key)}
|
|
931
|
+
isFirst={idx === 0}
|
|
932
|
+
isLast={idx === arr.length - 1}
|
|
933
|
+
visible={!hiddenCols.has(key)}
|
|
934
|
+
onToggleVisible={() => onToggleColVisibility(key)}
|
|
935
|
+
onMoveUp={() => onMoveCol(key, "up")}
|
|
936
|
+
onMoveDown={() => onMoveCol(key, "down")}
|
|
937
|
+
draggable={dragProps.draggable}
|
|
938
|
+
onDragStart={dragProps.onDragStart}
|
|
939
|
+
onDragOver={dragProps.onDragOver}
|
|
940
|
+
onDrop={dragProps.onDrop}
|
|
941
|
+
onDragEnd={dragProps.onDragEnd}
|
|
942
|
+
isDragging={dragProps["data-dragging"]}
|
|
943
|
+
isOver={dragProps["data-over"]}
|
|
944
|
+
/>
|
|
945
|
+
)
|
|
946
|
+
})}
|
|
947
|
+
</div>
|
|
948
|
+
</div>
|
|
949
|
+
)}
|
|
950
|
+
|
|
951
|
+
{/* ── Conditional rules ── */}
|
|
952
|
+
{sheetPanel === "conditional-rules" && (
|
|
953
|
+
<ConditionalRulesPanel
|
|
954
|
+
filterFields={filterFields}
|
|
955
|
+
rules={conditionalRules}
|
|
956
|
+
onAdd={onAddConditionalRule}
|
|
957
|
+
onRemove={onRemoveConditionalRule}
|
|
958
|
+
onUpdate={onUpdateConditionalRule}
|
|
959
|
+
renderFilterOptionValue={renderFilterOptionValue}
|
|
960
|
+
/>
|
|
961
|
+
)}
|
|
962
|
+
|
|
963
|
+
</div>
|
|
964
|
+
</>
|
|
965
|
+
)}
|
|
966
|
+
|
|
967
|
+
</SheetContent>
|
|
968
|
+
</Sheet>
|
|
969
|
+
)
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
973
|
+
// ConditionalRulesPanel — same DrawerFilterCard as filters (incl. operator cycle);
|
|
974
|
+
// highlight color lives inside the card. Adding a rule expands only that card (like
|
|
975
|
+
// add filter from drawer). No And/Or connectors.
|
|
976
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
977
|
+
|
|
978
|
+
function ConditionalRulesPanel({
|
|
979
|
+
filterFields,
|
|
980
|
+
rules,
|
|
981
|
+
onAdd,
|
|
982
|
+
onRemove,
|
|
983
|
+
onUpdate,
|
|
984
|
+
renderFilterOptionValue,
|
|
985
|
+
}: {
|
|
986
|
+
filterFields: FilterFieldDef[]
|
|
987
|
+
rules: ConditionalRule[]
|
|
988
|
+
onAdd: (rule: Omit<ConditionalRule, "id">) => void
|
|
989
|
+
onRemove: (id: string) => void
|
|
990
|
+
onUpdate: (id: string, patch: Partial<ConditionalRule>) => void
|
|
991
|
+
renderFilterOptionValue?: (fieldKey: string, value: string) => React.ReactNode
|
|
992
|
+
}) {
|
|
993
|
+
const [expandedIds, setExpandedIds] = React.useState<Set<string>>(() => new Set())
|
|
994
|
+
|
|
995
|
+
const prevLenRef = React.useRef(rules.length)
|
|
996
|
+
React.useEffect(() => {
|
|
997
|
+
if (rules.length > prevLenRef.current && rules.length > 0) {
|
|
998
|
+
const last = rules[rules.length - 1]
|
|
999
|
+
setExpandedIds(new Set([last.id]))
|
|
1000
|
+
}
|
|
1001
|
+
prevLenRef.current = rules.length
|
|
1002
|
+
}, [rules])
|
|
1003
|
+
|
|
1004
|
+
function toggleExpanded(id: string) {
|
|
1005
|
+
setExpandedIds(prev => {
|
|
1006
|
+
const next = new Set(prev)
|
|
1007
|
+
if (next.has(id)) next.delete(id)
|
|
1008
|
+
else next.add(id)
|
|
1009
|
+
return next
|
|
1010
|
+
})
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return (
|
|
1014
|
+
<div className="px-4 py-4 space-y-2">
|
|
1015
|
+
{rules.length === 0 ? (
|
|
1016
|
+
<div className="rounded-xl border border-dashed border-border bg-muted/30 px-4 py-6 text-center space-y-2">
|
|
1017
|
+
<div className="inline-flex items-center justify-center size-9 rounded-lg bg-muted mb-1">
|
|
1018
|
+
<i className="fa-light fa-palette text-muted-foreground text-[16px]" aria-hidden="true" />
|
|
1019
|
+
</div>
|
|
1020
|
+
<p className="text-sm font-medium text-foreground">No rules yet</p>
|
|
1021
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
1022
|
+
Highlight cells with a background color based on their value.
|
|
1023
|
+
</p>
|
|
1024
|
+
</div>
|
|
1025
|
+
) : (
|
|
1026
|
+
<div className="space-y-2">
|
|
1027
|
+
{rules.map(rule => {
|
|
1028
|
+
const fd = filterFields.find(f => f.key === rule.fieldKey)
|
|
1029
|
+
if (!fd) return null
|
|
1030
|
+
return (
|
|
1031
|
+
<DrawerFilterCard
|
|
1032
|
+
key={rule.id}
|
|
1033
|
+
variant="conditional"
|
|
1034
|
+
filter={rule}
|
|
1035
|
+
fieldDef={fd}
|
|
1036
|
+
expanded={expandedIds.has(rule.id)}
|
|
1037
|
+
onToggleExpand={() => toggleExpanded(rule.id)}
|
|
1038
|
+
onUpdate={onUpdate}
|
|
1039
|
+
onRemove={id => {
|
|
1040
|
+
onRemove(id)
|
|
1041
|
+
setExpandedIds(prev => {
|
|
1042
|
+
const next = new Set(prev)
|
|
1043
|
+
next.delete(id)
|
|
1044
|
+
return next
|
|
1045
|
+
})
|
|
1046
|
+
}}
|
|
1047
|
+
renderOptionLabel={value => renderFilterOptionValue?.(rule.fieldKey, value)}
|
|
1048
|
+
/>
|
|
1049
|
+
)
|
|
1050
|
+
})}
|
|
1051
|
+
</div>
|
|
1052
|
+
)}
|
|
1053
|
+
|
|
1054
|
+
<div className="flex items-center gap-2 pt-2">
|
|
1055
|
+
<DropdownMenu>
|
|
1056
|
+
<DropdownMenuTrigger asChild>
|
|
1057
|
+
<Button
|
|
1058
|
+
type="button"
|
|
1059
|
+
variant="outline"
|
|
1060
|
+
className="flex-1 gap-1.5 h-8 border-dashed text-muted-foreground"
|
|
1061
|
+
>
|
|
1062
|
+
<i className="fa-light fa-plus text-xs" aria-hidden="true" />
|
|
1063
|
+
Add rule
|
|
1064
|
+
</Button>
|
|
1065
|
+
</DropdownMenuTrigger>
|
|
1066
|
+
<DropdownMenuContent align="start" className="w-48">
|
|
1067
|
+
<DropdownMenuLabel className="text-xs">Rule for column</DropdownMenuLabel>
|
|
1068
|
+
<DropdownMenuSeparator />
|
|
1069
|
+
{filterFields.map(f => (
|
|
1070
|
+
<DropdownMenuItem
|
|
1071
|
+
key={f.key}
|
|
1072
|
+
onClick={() => onAdd({
|
|
1073
|
+
fieldKey: f.key,
|
|
1074
|
+
operator: f.operators[0],
|
|
1075
|
+
values: [],
|
|
1076
|
+
bgColor: RULE_COLORS[0].bg,
|
|
1077
|
+
})}
|
|
1078
|
+
>
|
|
1079
|
+
<i className={`fa-light ${f.icon}`} aria-hidden="true" />
|
|
1080
|
+
{f.label}
|
|
1081
|
+
</DropdownMenuItem>
|
|
1082
|
+
))}
|
|
1083
|
+
</DropdownMenuContent>
|
|
1084
|
+
</DropdownMenu>
|
|
1085
|
+
{rules.length > 0 && (
|
|
1086
|
+
<Button
|
|
1087
|
+
type="button"
|
|
1088
|
+
variant="ghost"
|
|
1089
|
+
size="sm"
|
|
1090
|
+
className="shrink-0 text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
1091
|
+
onClick={() => {
|
|
1092
|
+
rules.forEach(r => onRemove(r.id))
|
|
1093
|
+
setExpandedIds(new Set())
|
|
1094
|
+
}}
|
|
1095
|
+
>
|
|
1096
|
+
Remove all
|
|
1097
|
+
</Button>
|
|
1098
|
+
)}
|
|
1099
|
+
</div>
|
|
1100
|
+
</div>
|
|
1101
|
+
)
|
|
1102
|
+
}
|