@open-mercato/ui 0.4.2-canary-c02407ff85

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.
Files changed (319) hide show
  1. package/build.mjs +62 -0
  2. package/dist/backend/AppShell.js +902 -0
  3. package/dist/backend/AppShell.js.map +7 -0
  4. package/dist/backend/ConfirmDialog.js +17 -0
  5. package/dist/backend/ConfirmDialog.js.map +7 -0
  6. package/dist/backend/ContextHelp.js +31 -0
  7. package/dist/backend/ContextHelp.js.map +7 -0
  8. package/dist/backend/CrudForm.js +2028 -0
  9. package/dist/backend/CrudForm.js.map +7 -0
  10. package/dist/backend/DataTable.js +1363 -0
  11. package/dist/backend/DataTable.js.map +7 -0
  12. package/dist/backend/EmptyState.js +52 -0
  13. package/dist/backend/EmptyState.js.map +7 -0
  14. package/dist/backend/FilterBar.js +140 -0
  15. package/dist/backend/FilterBar.js.map +7 -0
  16. package/dist/backend/FilterOverlay.js +279 -0
  17. package/dist/backend/FilterOverlay.js.map +7 -0
  18. package/dist/backend/FlashMessages.js +66 -0
  19. package/dist/backend/FlashMessages.js.map +7 -0
  20. package/dist/backend/JsonBuilder.js +322 -0
  21. package/dist/backend/JsonBuilder.js.map +7 -0
  22. package/dist/backend/JsonDisplay.js +203 -0
  23. package/dist/backend/JsonDisplay.js.map +7 -0
  24. package/dist/backend/Page.js +27 -0
  25. package/dist/backend/Page.js.map +7 -0
  26. package/dist/backend/PerspectiveSidebar.js +282 -0
  27. package/dist/backend/PerspectiveSidebar.js.map +7 -0
  28. package/dist/backend/RowActions.js +148 -0
  29. package/dist/backend/RowActions.js.map +7 -0
  30. package/dist/backend/TruncatedCell.js +92 -0
  31. package/dist/backend/TruncatedCell.js.map +7 -0
  32. package/dist/backend/UserMenu.js +107 -0
  33. package/dist/backend/UserMenu.js.map +7 -0
  34. package/dist/backend/ValueIcons.js +34 -0
  35. package/dist/backend/ValueIcons.js.map +7 -0
  36. package/dist/backend/custom-fields/FieldDefinitionsEditor.js +1264 -0
  37. package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +7 -0
  38. package/dist/backend/custom-fields/FieldDefinitionsManager.js +332 -0
  39. package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +7 -0
  40. package/dist/backend/dashboard/DashboardScreen.js +578 -0
  41. package/dist/backend/dashboard/DashboardScreen.js.map +7 -0
  42. package/dist/backend/dashboard/index.js +5 -0
  43. package/dist/backend/dashboard/index.js.map +7 -0
  44. package/dist/backend/dashboard/widgetRegistry.js +55 -0
  45. package/dist/backend/dashboard/widgetRegistry.js.map +7 -0
  46. package/dist/backend/detail/ActivitiesSection.js +962 -0
  47. package/dist/backend/detail/ActivitiesSection.js.map +7 -0
  48. package/dist/backend/detail/AddressEditor.js +413 -0
  49. package/dist/backend/detail/AddressEditor.js.map +7 -0
  50. package/dist/backend/detail/AddressTiles.js +437 -0
  51. package/dist/backend/detail/AddressTiles.js.map +7 -0
  52. package/dist/backend/detail/AddressesSection.js +264 -0
  53. package/dist/backend/detail/AddressesSection.js.map +7 -0
  54. package/dist/backend/detail/AttachmentDeleteDialog.js +41 -0
  55. package/dist/backend/detail/AttachmentDeleteDialog.js.map +7 -0
  56. package/dist/backend/detail/AttachmentMetadataDialog.js +517 -0
  57. package/dist/backend/detail/AttachmentMetadataDialog.js.map +7 -0
  58. package/dist/backend/detail/AttachmentsSection.js +367 -0
  59. package/dist/backend/detail/AttachmentsSection.js.map +7 -0
  60. package/dist/backend/detail/CustomDataSection.js +433 -0
  61. package/dist/backend/detail/CustomDataSection.js.map +7 -0
  62. package/dist/backend/detail/DetailFieldsSection.js +75 -0
  63. package/dist/backend/detail/DetailFieldsSection.js.map +7 -0
  64. package/dist/backend/detail/ErrorMessage.js +28 -0
  65. package/dist/backend/detail/ErrorMessage.js.map +7 -0
  66. package/dist/backend/detail/InlineEditors.js +681 -0
  67. package/dist/backend/detail/InlineEditors.js.map +7 -0
  68. package/dist/backend/detail/LoadingMessage.js +14 -0
  69. package/dist/backend/detail/LoadingMessage.js.map +7 -0
  70. package/dist/backend/detail/NotesSection.js +1032 -0
  71. package/dist/backend/detail/NotesSection.js.map +7 -0
  72. package/dist/backend/detail/TabEmptyState.js +25 -0
  73. package/dist/backend/detail/TabEmptyState.js.map +7 -0
  74. package/dist/backend/detail/TagsSection.js +254 -0
  75. package/dist/backend/detail/TagsSection.js.map +7 -0
  76. package/dist/backend/detail/addressFormat.js +77 -0
  77. package/dist/backend/detail/addressFormat.js.map +7 -0
  78. package/dist/backend/detail/index.js +34 -0
  79. package/dist/backend/detail/index.js.map +7 -0
  80. package/dist/backend/fields/registry.generated.js +8 -0
  81. package/dist/backend/fields/registry.generated.js.map +7 -0
  82. package/dist/backend/fields/registry.js +29 -0
  83. package/dist/backend/fields/registry.js.map +7 -0
  84. package/dist/backend/indexes/PartialIndexBanner.js +58 -0
  85. package/dist/backend/indexes/PartialIndexBanner.js.map +7 -0
  86. package/dist/backend/indexes/store.js +62 -0
  87. package/dist/backend/indexes/store.js.map +7 -0
  88. package/dist/backend/injection/InjectionSpot.js +179 -0
  89. package/dist/backend/injection/InjectionSpot.js.map +7 -0
  90. package/dist/backend/injection/PageInjectionBoundary.js +26 -0
  91. package/dist/backend/injection/PageInjectionBoundary.js.map +7 -0
  92. package/dist/backend/injection/helpers.js +26 -0
  93. package/dist/backend/injection/helpers.js.map +7 -0
  94. package/dist/backend/injection/widgetRegistry.js +55 -0
  95. package/dist/backend/injection/widgetRegistry.js.map +7 -0
  96. package/dist/backend/inputs/ComboboxInput.js +225 -0
  97. package/dist/backend/inputs/ComboboxInput.js.map +7 -0
  98. package/dist/backend/inputs/LookupSelect.js +191 -0
  99. package/dist/backend/inputs/LookupSelect.js.map +7 -0
  100. package/dist/backend/inputs/PhoneNumberField.js +100 -0
  101. package/dist/backend/inputs/PhoneNumberField.js.map +7 -0
  102. package/dist/backend/inputs/SwitchableMarkdownInput.js +92 -0
  103. package/dist/backend/inputs/SwitchableMarkdownInput.js.map +7 -0
  104. package/dist/backend/inputs/TagsInput.js +222 -0
  105. package/dist/backend/inputs/TagsInput.js.map +7 -0
  106. package/dist/backend/inputs/index.js +6 -0
  107. package/dist/backend/inputs/index.js.map +7 -0
  108. package/dist/backend/operations/LastOperationBanner.js +80 -0
  109. package/dist/backend/operations/LastOperationBanner.js.map +7 -0
  110. package/dist/backend/operations/store.js +183 -0
  111. package/dist/backend/operations/store.js.map +7 -0
  112. package/dist/backend/schedule/ScheduleAgenda.js +107 -0
  113. package/dist/backend/schedule/ScheduleAgenda.js.map +7 -0
  114. package/dist/backend/schedule/ScheduleGrid.js +107 -0
  115. package/dist/backend/schedule/ScheduleGrid.js.map +7 -0
  116. package/dist/backend/schedule/ScheduleToolbar.js +166 -0
  117. package/dist/backend/schedule/ScheduleToolbar.js.map +7 -0
  118. package/dist/backend/schedule/ScheduleView.js +165 -0
  119. package/dist/backend/schedule/ScheduleView.js.map +7 -0
  120. package/dist/backend/schedule/index.js +6 -0
  121. package/dist/backend/schedule/index.js.map +7 -0
  122. package/dist/backend/schedule/recurrence.js +83 -0
  123. package/dist/backend/schedule/recurrence.js.map +7 -0
  124. package/dist/backend/schedule/types.js +1 -0
  125. package/dist/backend/schedule/types.js.map +7 -0
  126. package/dist/backend/upgrades/UpgradeActionBanner.js +91 -0
  127. package/dist/backend/upgrades/UpgradeActionBanner.js.map +7 -0
  128. package/dist/backend/utils/api.js +127 -0
  129. package/dist/backend/utils/api.js.map +7 -0
  130. package/dist/backend/utils/apiCall.js +48 -0
  131. package/dist/backend/utils/apiCall.js.map +7 -0
  132. package/dist/backend/utils/crud.js +126 -0
  133. package/dist/backend/utils/crud.js.map +7 -0
  134. package/dist/backend/utils/customFieldColumns.js +56 -0
  135. package/dist/backend/utils/customFieldColumns.js.map +7 -0
  136. package/dist/backend/utils/customFieldDefs.js +143 -0
  137. package/dist/backend/utils/customFieldDefs.js.map +7 -0
  138. package/dist/backend/utils/customFieldFilters.js +126 -0
  139. package/dist/backend/utils/customFieldFilters.js.map +7 -0
  140. package/dist/backend/utils/customFieldForms.js +162 -0
  141. package/dist/backend/utils/customFieldForms.js.map +7 -0
  142. package/dist/backend/utils/customFieldValues.js +26 -0
  143. package/dist/backend/utils/customFieldValues.js.map +7 -0
  144. package/dist/backend/utils/flash.js +16 -0
  145. package/dist/backend/utils/flash.js.map +7 -0
  146. package/dist/backend/utils/nav.js +185 -0
  147. package/dist/backend/utils/nav.js.map +7 -0
  148. package/dist/backend/utils/serverErrors.js +230 -0
  149. package/dist/backend/utils/serverErrors.js.map +7 -0
  150. package/dist/frontend/AuthFooter.js +23 -0
  151. package/dist/frontend/AuthFooter.js.map +7 -0
  152. package/dist/frontend/LanguageSwitcher.js +57 -0
  153. package/dist/frontend/LanguageSwitcher.js.map +7 -0
  154. package/dist/frontend/Layout.js +14 -0
  155. package/dist/frontend/Layout.js.map +7 -0
  156. package/dist/index.js +32 -0
  157. package/dist/index.js.map +7 -0
  158. package/dist/primitives/DataLoader.js +67 -0
  159. package/dist/primitives/DataLoader.js.map +7 -0
  160. package/dist/primitives/ErrorNotice.js +20 -0
  161. package/dist/primitives/ErrorNotice.js.map +7 -0
  162. package/dist/primitives/alert.js +38 -0
  163. package/dist/primitives/alert.js.map +7 -0
  164. package/dist/primitives/badge.js +28 -0
  165. package/dist/primitives/badge.js.map +7 -0
  166. package/dist/primitives/button.js +44 -0
  167. package/dist/primitives/button.js.map +7 -0
  168. package/dist/primitives/card.js +91 -0
  169. package/dist/primitives/card.js.map +7 -0
  170. package/dist/primitives/checkbox.js +28 -0
  171. package/dist/primitives/checkbox.js.map +7 -0
  172. package/dist/primitives/dialog.js +90 -0
  173. package/dist/primitives/dialog.js.map +7 -0
  174. package/dist/primitives/input.js +22 -0
  175. package/dist/primitives/input.js.map +7 -0
  176. package/dist/primitives/label.js +21 -0
  177. package/dist/primitives/label.js.map +7 -0
  178. package/dist/primitives/separator.js +9 -0
  179. package/dist/primitives/separator.js.map +7 -0
  180. package/dist/primitives/spinner.js +24 -0
  181. package/dist/primitives/spinner.js.map +7 -0
  182. package/dist/primitives/switch.js +80 -0
  183. package/dist/primitives/switch.js.map +7 -0
  184. package/dist/primitives/table.js +29 -0
  185. package/dist/primitives/table.js.map +7 -0
  186. package/dist/primitives/tabs.js +87 -0
  187. package/dist/primitives/tabs.js.map +7 -0
  188. package/dist/primitives/textarea.js +21 -0
  189. package/dist/primitives/textarea.js.map +7 -0
  190. package/dist/primitives/tooltip.js +60 -0
  191. package/dist/primitives/tooltip.js.map +7 -0
  192. package/dist/theme/QueryProvider.js +44 -0
  193. package/dist/theme/QueryProvider.js.map +7 -0
  194. package/dist/theme/ThemeProvider.js +95 -0
  195. package/dist/theme/ThemeProvider.js.map +7 -0
  196. package/dist/theme/ThemeToggle.js +88 -0
  197. package/dist/theme/ThemeToggle.js.map +7 -0
  198. package/dist/theme/index.js +10 -0
  199. package/dist/theme/index.js.map +7 -0
  200. package/dist/types/react-big-calendar.d.js +1 -0
  201. package/dist/types/react-big-calendar.d.js.map +7 -0
  202. package/jest.config.cjs +23 -0
  203. package/jest.setup.ts +55 -0
  204. package/package.json +105 -0
  205. package/src/backend/AppShell.tsx +1096 -0
  206. package/src/backend/ConfirmDialog.tsx +19 -0
  207. package/src/backend/ContextHelp.tsx +38 -0
  208. package/src/backend/CrudForm.tsx +2503 -0
  209. package/src/backend/DataTable.tsx +1730 -0
  210. package/src/backend/EmptyState.tsx +65 -0
  211. package/src/backend/FilterBar.tsx +161 -0
  212. package/src/backend/FilterOverlay.tsx +328 -0
  213. package/src/backend/FlashMessages.tsx +82 -0
  214. package/src/backend/JsonBuilder.tsx +362 -0
  215. package/src/backend/JsonDisplay.tsx +254 -0
  216. package/src/backend/Page.tsx +30 -0
  217. package/src/backend/PerspectiveSidebar.tsx +337 -0
  218. package/src/backend/RowActions.tsx +151 -0
  219. package/src/backend/TruncatedCell.tsx +133 -0
  220. package/src/backend/UserMenu.tsx +118 -0
  221. package/src/backend/ValueIcons.tsx +48 -0
  222. package/src/backend/__tests__/AppShell.test.tsx +115 -0
  223. package/src/backend/__tests__/CrudForm.render.test.tsx +30 -0
  224. package/src/backend/__tests__/DataTable.render.test.tsx +48 -0
  225. package/src/backend/__tests__/custom-field-filters.test.ts +72 -0
  226. package/src/backend/__tests__/custom-field-forms.test.ts +54 -0
  227. package/src/backend/__tests__/serverErrors.test.ts +83 -0
  228. package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +1292 -0
  229. package/src/backend/custom-fields/FieldDefinitionsManager.tsx +381 -0
  230. package/src/backend/dashboard/DashboardScreen.tsx +684 -0
  231. package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +112 -0
  232. package/src/backend/dashboard/index.ts +1 -0
  233. package/src/backend/dashboard/widgetRegistry.ts +68 -0
  234. package/src/backend/detail/ActivitiesSection.tsx +1284 -0
  235. package/src/backend/detail/AddressEditor.tsx +472 -0
  236. package/src/backend/detail/AddressTiles.tsx +587 -0
  237. package/src/backend/detail/AddressesSection.tsx +346 -0
  238. package/src/backend/detail/AttachmentDeleteDialog.tsx +56 -0
  239. package/src/backend/detail/AttachmentMetadataDialog.tsx +672 -0
  240. package/src/backend/detail/AttachmentsSection.tsx +414 -0
  241. package/src/backend/detail/CustomDataSection.tsx +530 -0
  242. package/src/backend/detail/DetailFieldsSection.tsx +147 -0
  243. package/src/backend/detail/ErrorMessage.tsx +32 -0
  244. package/src/backend/detail/InlineEditors.tsx +877 -0
  245. package/src/backend/detail/LoadingMessage.tsx +14 -0
  246. package/src/backend/detail/NotesSection.tsx +1275 -0
  247. package/src/backend/detail/TabEmptyState.tsx +48 -0
  248. package/src/backend/detail/TagsSection.tsx +314 -0
  249. package/src/backend/detail/addressFormat.tsx +121 -0
  250. package/src/backend/detail/index.ts +44 -0
  251. package/src/backend/fields/registry.generated.ts +8 -0
  252. package/src/backend/fields/registry.ts +38 -0
  253. package/src/backend/indexes/PartialIndexBanner.tsx +68 -0
  254. package/src/backend/indexes/store.ts +88 -0
  255. package/src/backend/injection/InjectionSpot.tsx +236 -0
  256. package/src/backend/injection/PageInjectionBoundary.tsx +31 -0
  257. package/src/backend/injection/helpers.ts +35 -0
  258. package/src/backend/injection/widgetRegistry.ts +68 -0
  259. package/src/backend/inputs/ComboboxInput.tsx +269 -0
  260. package/src/backend/inputs/LookupSelect.tsx +247 -0
  261. package/src/backend/inputs/PhoneNumberField.tsx +129 -0
  262. package/src/backend/inputs/SwitchableMarkdownInput.tsx +128 -0
  263. package/src/backend/inputs/TagsInput.tsx +259 -0
  264. package/src/backend/inputs/index.ts +5 -0
  265. package/src/backend/operations/LastOperationBanner.tsx +85 -0
  266. package/src/backend/operations/__tests__/LastOperationBanner.test.tsx +99 -0
  267. package/src/backend/operations/store.ts +230 -0
  268. package/src/backend/schedule/ScheduleAgenda.tsx +136 -0
  269. package/src/backend/schedule/ScheduleGrid.tsx +136 -0
  270. package/src/backend/schedule/ScheduleToolbar.tsx +178 -0
  271. package/src/backend/schedule/ScheduleView.tsx +198 -0
  272. package/src/backend/schedule/index.ts +5 -0
  273. package/src/backend/schedule/recurrence.ts +99 -0
  274. package/src/backend/schedule/types.ts +26 -0
  275. package/src/backend/upgrades/UpgradeActionBanner.tsx +128 -0
  276. package/src/backend/utils/__tests__/apiCall.test.ts +109 -0
  277. package/src/backend/utils/__tests__/crud.test.ts +87 -0
  278. package/src/backend/utils/__tests__/customFieldDefs.test.ts +25 -0
  279. package/src/backend/utils/__tests__/customFieldValues.test.ts +35 -0
  280. package/src/backend/utils/api.ts +149 -0
  281. package/src/backend/utils/apiCall.ts +96 -0
  282. package/src/backend/utils/crud.ts +174 -0
  283. package/src/backend/utils/customFieldColumns.ts +71 -0
  284. package/src/backend/utils/customFieldDefs.ts +245 -0
  285. package/src/backend/utils/customFieldFilters.ts +145 -0
  286. package/src/backend/utils/customFieldForms.ts +196 -0
  287. package/src/backend/utils/customFieldValues.ts +41 -0
  288. package/src/backend/utils/flash.ts +17 -0
  289. package/src/backend/utils/nav.ts +238 -0
  290. package/src/backend/utils/serverErrors.ts +302 -0
  291. package/src/frontend/AuthFooter.tsx +29 -0
  292. package/src/frontend/LanguageSwitcher.tsx +66 -0
  293. package/src/frontend/Layout.tsx +13 -0
  294. package/src/index.ts +32 -0
  295. package/src/primitives/DataLoader.tsx +92 -0
  296. package/src/primitives/ErrorNotice.tsx +26 -0
  297. package/src/primitives/alert.tsx +52 -0
  298. package/src/primitives/badge.tsx +31 -0
  299. package/src/primitives/button.tsx +47 -0
  300. package/src/primitives/card.tsx +92 -0
  301. package/src/primitives/checkbox.tsx +28 -0
  302. package/src/primitives/dialog.tsx +110 -0
  303. package/src/primitives/input.tsx +20 -0
  304. package/src/primitives/label.tsx +18 -0
  305. package/src/primitives/separator.tsx +7 -0
  306. package/src/primitives/spinner.tsx +27 -0
  307. package/src/primitives/switch.tsx +86 -0
  308. package/src/primitives/table.tsx +27 -0
  309. package/src/primitives/tabs.tsx +128 -0
  310. package/src/primitives/textarea.tsx +20 -0
  311. package/src/primitives/tooltip.tsx +85 -0
  312. package/src/theme/QueryProvider.tsx +46 -0
  313. package/src/theme/ThemeProvider.tsx +120 -0
  314. package/src/theme/ThemeToggle.tsx +88 -0
  315. package/src/theme/index.ts +3 -0
  316. package/src/types/react-big-calendar.d.ts +16 -0
  317. package/tsconfig.build.json +11 -0
  318. package/tsconfig.json +9 -0
  319. package/watch.mjs +6 -0
@@ -0,0 +1,1363 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { useReactTable, getCoreRowModel, getSortedRowModel, flexRender } from "@tanstack/react-table";
6
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
7
+ import { RefreshCw, Loader2, SlidersHorizontal, MoreHorizontal, Circle } from "lucide-react";
8
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../primitives/table.js";
9
+ import { Button } from "../primitives/button.js";
10
+ import { Spinner } from "../primitives/spinner.js";
11
+ import { TooltipProvider } from "../primitives/tooltip.js";
12
+ import { TruncatedCell } from "./TruncatedCell.js";
13
+ import { FilterBar } from "./FilterBar.js";
14
+ import { useCustomFieldFilterDefs } from "./utils/customFieldFilters.js";
15
+ import { fetchCustomFieldDefinitionsPayload } from "./utils/customFieldDefs.js";
16
+ import { subscribeOrganizationScopeChanged } from "@open-mercato/shared/lib/frontend/organizationEvents";
17
+ import { InjectionSpot } from "./injection/InjectionSpot.js";
18
+ import { serializeExport, defaultExportFilename } from "@open-mercato/shared/lib/crud/exporters";
19
+ import { apiCall } from "./utils/apiCall.js";
20
+ import { raiseCrudError } from "./utils/serverErrors.js";
21
+ import { PerspectiveSidebar } from "./PerspectiveSidebar.js";
22
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
23
+ let refreshScheduled = false;
24
+ function scheduleRouterRefresh(router) {
25
+ if (refreshScheduled) return;
26
+ refreshScheduled = true;
27
+ if (typeof window === "undefined") {
28
+ refreshScheduled = false;
29
+ return;
30
+ }
31
+ window.requestAnimationFrame(() => {
32
+ refreshScheduled = false;
33
+ try {
34
+ router.refresh();
35
+ } catch {
36
+ }
37
+ });
38
+ }
39
+ function extractEditAction(items) {
40
+ const byId = items.find((item) => item.id === "edit" && (item.href || item.onSelect));
41
+ if (byId) return byId;
42
+ return items.find(
43
+ (item) => item.label.toLowerCase() === "edit" && (item.href || item.onSelect)
44
+ ) || null;
45
+ }
46
+ const DEFAULT_EXPORT_FORMATS = ["csv", "json", "xml", "markdown"];
47
+ const EXPORT_LABELS = {
48
+ csv: "CSV",
49
+ json: "JSON",
50
+ xml: "XML",
51
+ markdown: "Markdown"
52
+ };
53
+ const EMPTY_FILTER_DEFS = [];
54
+ const EMPTY_FILTER_VALUES = Object.freeze({});
55
+ function resolveExportSections(config) {
56
+ if (!config) return [];
57
+ const sections = [];
58
+ const baseFormats = config.formats && config.formats.length > 0 ? config.formats : DEFAULT_EXPORT_FORMATS;
59
+ const addSection = (key, section, fallbackTitle) => {
60
+ if (!section || !section.getUrl && !section.prepare) return;
61
+ const title = section.title?.trim().length ? section.title.trim() : fallbackTitle;
62
+ const seen = /* @__PURE__ */ new Set();
63
+ const formatsSource = section.formats && section.formats.length > 0 ? section.formats : baseFormats;
64
+ const formats = formatsSource.filter((format) => {
65
+ if (seen.has(format)) return false;
66
+ seen.add(format);
67
+ return true;
68
+ });
69
+ if (formats.length === 0) return;
70
+ sections.push({
71
+ key,
72
+ title,
73
+ description: section.description,
74
+ formats,
75
+ getUrl: section.getUrl,
76
+ prepare: section.prepare ? async (format) => {
77
+ const result = await section.prepare(format);
78
+ if (!result) return null;
79
+ if ("prepared" in result) return result;
80
+ return { prepared: result };
81
+ } : void 0,
82
+ filename: section.filename,
83
+ disabled: Boolean(config.disabled || section.disabled)
84
+ });
85
+ };
86
+ const hasExplicitSections = Array.isArray(config.sections) && config.sections.length > 0;
87
+ if (!config.view && !config.full && !hasExplicitSections && config.getUrl) {
88
+ addSection("view", { getUrl: config.getUrl, formats: config.formats }, "Export what you view");
89
+ } else {
90
+ addSection("view", config.view, "Export what you view");
91
+ }
92
+ if (hasExplicitSections) {
93
+ config.sections.forEach((section, idx) => {
94
+ addSection(`section-${idx}`, section, section.title?.trim().length ? section.title : `Export ${idx + 1}`);
95
+ });
96
+ }
97
+ addSection("full", config.full, "Full data export");
98
+ return sections;
99
+ }
100
+ const PERSPECTIVE_COOKIE_PREFIX = "om_table_perspective";
101
+ const PERSPECTIVE_STORAGE_PREFIX = "om_table_perspective_snapshot";
102
+ function formatDurationLabel(durationMs) {
103
+ if (durationMs == null) return "";
104
+ if (!Number.isFinite(durationMs)) return "";
105
+ if (durationMs < 0) return "";
106
+ if (durationMs < 1e3) return `${Math.round(durationMs)}ms`;
107
+ if (durationMs < 1e4) return `${(durationMs / 1e3).toFixed(1)}s`;
108
+ if (durationMs < 6e4) return `${Math.round(durationMs / 1e3)}s`;
109
+ if (durationMs < 36e5) return `${(durationMs / 6e4).toFixed(durationMs < 6e5 ? 1 : 0)}m`;
110
+ return `${(durationMs / 36e5).toFixed(durationMs < 72e5 ? 1 : 0)}h`;
111
+ }
112
+ function readPerspectiveCookie(tableId) {
113
+ if (typeof document === "undefined") return null;
114
+ const key = `${PERSPECTIVE_COOKIE_PREFIX}:${tableId}`;
115
+ const pattern = new RegExp(`(?:^|;\\s*)${key}=([^;]+)`);
116
+ const match = document.cookie.match(pattern);
117
+ return match ? decodeURIComponent(match[1]) : null;
118
+ }
119
+ function writePerspectiveCookie(tableId, perspectiveId) {
120
+ if (typeof document === "undefined") return;
121
+ const key = `${PERSPECTIVE_COOKIE_PREFIX}:${tableId}`;
122
+ const expires = perspectiveId ? "Max-Age=31536000" : "Max-Age=0";
123
+ const value = perspectiveId ? encodeURIComponent(perspectiveId) : "";
124
+ document.cookie = `${key}=${value}; Path=/; ${expires}; SameSite=Lax`;
125
+ }
126
+ function readPerspectiveSnapshot(tableId) {
127
+ if (typeof window === "undefined") return null;
128
+ try {
129
+ const raw = window.localStorage.getItem(`${PERSPECTIVE_STORAGE_PREFIX}:${tableId}`);
130
+ if (!raw) return null;
131
+ const parsed = JSON.parse(raw);
132
+ if (!parsed || typeof parsed !== "object") return null;
133
+ const perspectiveId = typeof parsed.perspectiveId === "string" && parsed.perspectiveId.trim().length > 0 ? parsed.perspectiveId : null;
134
+ const settings = typeof parsed.settings === "object" && parsed.settings !== null ? parsed.settings : null;
135
+ const updatedAt = typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now();
136
+ if (!settings) return null;
137
+ return { perspectiveId, settings, updatedAt };
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+ function writePerspectiveSnapshot(tableId, snapshot) {
143
+ if (typeof window === "undefined") return;
144
+ const key = `${PERSPECTIVE_STORAGE_PREFIX}:${tableId}`;
145
+ try {
146
+ if (!snapshot) {
147
+ window.localStorage.removeItem(key);
148
+ return;
149
+ }
150
+ window.localStorage.setItem(key, JSON.stringify(snapshot));
151
+ } catch {
152
+ }
153
+ }
154
+ function sanitizePerspectiveSettings(source) {
155
+ if (!source || typeof source !== "object") return null;
156
+ const forbidden = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
157
+ const result = {};
158
+ if (Array.isArray(source.columnOrder)) {
159
+ const seen = /* @__PURE__ */ new Set();
160
+ const order = source.columnOrder.map((id) => typeof id === "string" ? id.trim() : "").filter((id) => id.length > 0 && !seen.has(id) && (seen.add(id), true));
161
+ if (order.length) result.columnOrder = order;
162
+ }
163
+ if (source.columnVisibility && typeof source.columnVisibility === "object") {
164
+ const entries = Object.entries(source.columnVisibility).filter(([key, value]) => typeof key === "string" && key.trim().length > 0 && !forbidden.has(key) && typeof value === "boolean");
165
+ if (entries.length) {
166
+ const visibility = {};
167
+ entries.forEach(([key, value]) => {
168
+ visibility[key] = value;
169
+ });
170
+ result.columnVisibility = visibility;
171
+ }
172
+ }
173
+ if (Array.isArray(source.sorting)) {
174
+ const sorting = source.sorting.map((item) => {
175
+ const id = typeof item?.id === "string" ? item.id.trim() : "";
176
+ if (!id || forbidden.has(id)) return null;
177
+ return { id, desc: Boolean(item?.desc) };
178
+ }).filter((item) => item !== null);
179
+ if (sorting.length) result.sorting = sorting;
180
+ }
181
+ if (typeof source.pageSize === "number" && Number.isFinite(source.pageSize)) {
182
+ const pageSize = Math.max(1, Math.min(500, Math.floor(source.pageSize)));
183
+ result.pageSize = pageSize;
184
+ }
185
+ if (typeof source.searchValue === "string" && source.searchValue.trim().length > 0) {
186
+ result.searchValue = source.searchValue.trim().slice(0, 200);
187
+ }
188
+ if (source.filters && typeof source.filters === "object") {
189
+ const filters = {};
190
+ for (const [key, value] of Object.entries(source.filters)) {
191
+ if (typeof key === "string") {
192
+ const trimmed = key.trim();
193
+ if (trimmed.length > 0 && !forbidden.has(trimmed)) filters[trimmed] = value;
194
+ }
195
+ }
196
+ if (Object.keys(filters).length) result.filters = filters;
197
+ }
198
+ return Object.keys(result).length ? result : null;
199
+ }
200
+ function normalizeLabel(input) {
201
+ if (!input) return "";
202
+ return input.replace(/^cf[_:]/, "").replace(/[_:\-]+/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase());
203
+ }
204
+ function getColumnTruncateConfig(columnId, accessorKey, columnMeta) {
205
+ const key = accessorKey || columnId;
206
+ const metaMaxWidth = typeof columnMeta?.maxWidth === "string" ? columnMeta.maxWidth.trim() : "";
207
+ if (key.startsWith("cf_") || key.startsWith("cf:")) {
208
+ return {
209
+ maxWidth: metaMaxWidth || "120px",
210
+ truncate: typeof columnMeta?.truncate === "boolean" ? columnMeta.truncate : true
211
+ };
212
+ }
213
+ const wideColumns = ["title", "name", "description", "source", "companies", "people"];
214
+ if (wideColumns.includes(key)) {
215
+ return {
216
+ maxWidth: metaMaxWidth || "250px",
217
+ truncate: typeof columnMeta?.truncate === "boolean" ? columnMeta.truncate : true
218
+ };
219
+ }
220
+ const mediumColumns = ["status", "pipelineStage", "pipeline_stage", "type", "category"];
221
+ if (mediumColumns.includes(key)) {
222
+ return {
223
+ maxWidth: metaMaxWidth || "180px",
224
+ truncate: typeof columnMeta?.truncate === "boolean" ? columnMeta.truncate : true
225
+ };
226
+ }
227
+ if (key.endsWith("_at") || key.endsWith("At") || key.includes("date") || key.includes("Date")) {
228
+ return {
229
+ maxWidth: metaMaxWidth || "120px",
230
+ truncate: typeof columnMeta?.truncate === "boolean" ? columnMeta.truncate : true
231
+ };
232
+ }
233
+ return {
234
+ maxWidth: metaMaxWidth || "150px",
235
+ truncate: typeof columnMeta?.truncate === "boolean" ? columnMeta.truncate : true
236
+ };
237
+ }
238
+ function shouldSkipTruncation(columnId) {
239
+ const skipColumns = ["actions", "select", "checkbox", "expand"];
240
+ return skipColumns.includes(columnId.toLowerCase());
241
+ }
242
+ function ExportMenu({ config, sections }) {
243
+ const t = useT();
244
+ const { label } = config;
245
+ const defaultLabel = label ?? t("ui.dataTable.export.label", "Export");
246
+ const disabled = Boolean(config.disabled);
247
+ const hasSections = sections.length > 0;
248
+ const [open, setOpen] = React.useState(false);
249
+ const buttonRef = React.useRef(null);
250
+ const menuRef = React.useRef(null);
251
+ React.useEffect(() => {
252
+ if (!open || !hasSections) return;
253
+ const onDocClick = (event) => {
254
+ const target = event.target;
255
+ if (menuRef.current && !menuRef.current.contains(target) && buttonRef.current && !buttonRef.current.contains(target)) {
256
+ setOpen(false);
257
+ }
258
+ };
259
+ const onKeyDown = (event) => {
260
+ if (event.key === "Escape") {
261
+ setOpen(false);
262
+ buttonRef.current?.focus();
263
+ }
264
+ };
265
+ document.addEventListener("mousedown", onDocClick);
266
+ document.addEventListener("keydown", onKeyDown);
267
+ return () => {
268
+ document.removeEventListener("mousedown", onDocClick);
269
+ document.removeEventListener("keydown", onKeyDown);
270
+ };
271
+ }, [hasSections, open]);
272
+ if (!hasSections) return null;
273
+ const handleSelect = async (section, format) => {
274
+ try {
275
+ if (section.prepare) {
276
+ const preparedResult = await section.prepare(format);
277
+ if (!preparedResult) return;
278
+ const prepared = preparedResult.prepared;
279
+ const serialized = serializeExport(prepared, format);
280
+ const filename = preparedResult.filename ?? section.filename?.(format) ?? config.filename?.(format) ?? defaultExportFilename(section.title, format);
281
+ if (typeof window !== "undefined") {
282
+ const blob = new Blob([serialized.body], { type: serialized.contentType });
283
+ const href = URL.createObjectURL(blob);
284
+ const a = document.createElement("a");
285
+ a.href = href;
286
+ a.download = filename;
287
+ document.body.appendChild(a);
288
+ a.click();
289
+ document.body.removeChild(a);
290
+ URL.revokeObjectURL(href);
291
+ }
292
+ } else if (section.getUrl) {
293
+ const url = section.getUrl(format);
294
+ if (url && typeof window !== "undefined") {
295
+ window.open(url, "_blank", "noopener,noreferrer");
296
+ }
297
+ }
298
+ } catch {
299
+ } finally {
300
+ setOpen(false);
301
+ }
302
+ };
303
+ return /* @__PURE__ */ jsxs("div", { className: "relative inline-block", children: [
304
+ /* @__PURE__ */ jsx(
305
+ Button,
306
+ {
307
+ ref: buttonRef,
308
+ variant: "outline",
309
+ size: "sm",
310
+ type: "button",
311
+ onClick: () => {
312
+ if (disabled) return;
313
+ setOpen((prev) => !prev);
314
+ },
315
+ "aria-haspopup": "menu",
316
+ "aria-expanded": open,
317
+ disabled,
318
+ children: defaultLabel
319
+ }
320
+ ),
321
+ open ? /* @__PURE__ */ jsx(
322
+ "div",
323
+ {
324
+ ref: menuRef,
325
+ role: "menu",
326
+ className: "absolute right-0 mt-2 w-60 rounded-md border bg-background py-2 shadow z-20",
327
+ children: sections.map((section, idx) => /* @__PURE__ */ jsxs("div", { className: idx > 0 ? "mt-2 border-t pt-3" : "", children: [
328
+ /* @__PURE__ */ jsxs("div", { className: "px-3", children: [
329
+ /* @__PURE__ */ jsx("div", { className: "text-xs font-semibold uppercase text-muted-foreground", children: section.title }),
330
+ section.description ? /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground leading-snug", children: section.description }) : null
331
+ ] }),
332
+ /* @__PURE__ */ jsx("div", { className: "mt-2 space-y-1 px-2 pb-1", children: section.formats.map((format) => /* @__PURE__ */ jsx(
333
+ "button",
334
+ {
335
+ type: "button",
336
+ className: "block w-full rounded px-2 py-1 text-left text-sm hover:bg-accent",
337
+ onClick: () => void handleSelect(section, format),
338
+ disabled: section.disabled,
339
+ children: EXPORT_LABELS[format]
340
+ },
341
+ `${section.key}-${format}`
342
+ )) })
343
+ ] }, section.key))
344
+ }
345
+ ) : null
346
+ ] });
347
+ }
348
+ function DataTable({
349
+ columns,
350
+ data,
351
+ toolbar,
352
+ title,
353
+ actions,
354
+ refreshButton,
355
+ sortable,
356
+ sorting: sortingProp,
357
+ onSortingChange,
358
+ pagination,
359
+ isLoading,
360
+ emptyState,
361
+ error,
362
+ rowActions,
363
+ onRowClick,
364
+ disableRowClick = false,
365
+ searchValue,
366
+ onSearchChange,
367
+ searchPlaceholder,
368
+ searchAlign = "right",
369
+ filters: baseFilters = EMPTY_FILTER_DEFS,
370
+ filterValues = EMPTY_FILTER_VALUES,
371
+ onFiltersApply,
372
+ onFiltersClear,
373
+ entityId,
374
+ entityIds,
375
+ exporter,
376
+ perspective,
377
+ embedded = false,
378
+ onCustomFieldFilterFieldsetChange,
379
+ customFieldFilterKeyExtras,
380
+ injectionSpotId,
381
+ injectionContext
382
+ }) {
383
+ const t = useT();
384
+ const router = useRouter();
385
+ const lastScopeRef = React.useRef(null);
386
+ const hasInitializedScopeRef = React.useRef(false);
387
+ React.useEffect(() => {
388
+ return subscribeOrganizationScopeChanged((detail) => {
389
+ const prev = lastScopeRef.current;
390
+ lastScopeRef.current = detail;
391
+ if (!hasInitializedScopeRef.current) {
392
+ hasInitializedScopeRef.current = true;
393
+ return;
394
+ }
395
+ if (prev && prev.organizationId === detail.organizationId && prev.tenantId === detail.tenantId) {
396
+ return;
397
+ }
398
+ scheduleRouterRefresh(router);
399
+ });
400
+ }, [router]);
401
+ const queryClient = useQueryClient();
402
+ const perspectiveConfig = perspective ?? null;
403
+ const perspectiveTableId = perspectiveConfig?.tableId ?? null;
404
+ const perspectiveEnabled = Boolean(perspectiveTableId);
405
+ const initialSnapshotRef = React.useRef(null);
406
+ const snapshotTableIdRef = React.useRef(null);
407
+ if (typeof window !== "undefined") {
408
+ if (perspectiveTableId !== snapshotTableIdRef.current) {
409
+ initialSnapshotRef.current = perspectiveTableId ? readPerspectiveSnapshot(perspectiveTableId) : null;
410
+ snapshotTableIdRef.current = perspectiveTableId ?? null;
411
+ }
412
+ } else if (snapshotTableIdRef.current !== perspectiveTableId) {
413
+ snapshotTableIdRef.current = perspectiveTableId ?? null;
414
+ initialSnapshotRef.current = null;
415
+ }
416
+ const initialSnapshot = initialSnapshotRef.current;
417
+ const initialSettingsFromConfig = sanitizePerspectiveSettings(perspectiveConfig?.initialState?.initialSettings ?? null);
418
+ const initialSettingsFromSnapshot = sanitizePerspectiveSettings(initialSnapshot?.settings ?? null);
419
+ const mergedInitialSettings = initialSettingsFromConfig ?? initialSettingsFromSnapshot ?? null;
420
+ const initialActiveId = perspectiveConfig?.initialState?.activePerspectiveId ?? initialSnapshot?.perspectiveId ?? null;
421
+ const [isPerspectiveOpen, setPerspectiveOpen] = React.useState(false);
422
+ const [activePerspectiveId, setActivePerspectiveId] = React.useState(initialActiveId);
423
+ const [columnVisibility, setColumnVisibility] = React.useState(() => mergedInitialSettings?.columnVisibility ?? {});
424
+ const [columnOrder, setColumnOrder] = React.useState(() => mergedInitialSettings?.columnOrder ?? []);
425
+ const [deletingIds, setDeletingIds] = React.useState([]);
426
+ const [roleClearingIds, setRoleClearingIds] = React.useState([]);
427
+ const [perspectiveApiMissing, setPerspectiveApiMissing] = React.useState(false);
428
+ const perspectiveFeatureQuery = useQuery({
429
+ queryKey: ["feature-check", "perspectives"],
430
+ enabled: perspectiveEnabled,
431
+ staleTime: 5 * 60 * 1e3,
432
+ queryFn: async () => {
433
+ try {
434
+ const call = await apiCall(
435
+ "/api/auth/feature-check",
436
+ {
437
+ method: "POST",
438
+ headers: { "Content-Type": "application/json" },
439
+ body: JSON.stringify({ features: ["perspectives.use", "perspectives.role_defaults"] })
440
+ }
441
+ );
442
+ if (!call.ok) throw new Error(`feature-check failed (${call.status})`);
443
+ const data2 = call.result ?? {};
444
+ const granted = Array.isArray(data2?.granted) ? data2.granted.map((f) => String(f)) : [];
445
+ const has = (feature) => granted.some((grantedFeature) => {
446
+ if (grantedFeature === "*") return true;
447
+ if (grantedFeature === feature) return true;
448
+ if (grantedFeature.endsWith(".*")) {
449
+ const prefix = grantedFeature.slice(0, -2);
450
+ return feature === prefix || feature.startsWith(`${prefix}.`);
451
+ }
452
+ return false;
453
+ });
454
+ return {
455
+ use: has("perspectives.use"),
456
+ roleDefaults: has("perspectives.role_defaults")
457
+ };
458
+ } catch {
459
+ return {
460
+ use: true,
461
+ roleDefaults: true
462
+ };
463
+ }
464
+ }
465
+ });
466
+ const perspectivePermissions = perspectiveFeatureQuery.data;
467
+ const canUsePerspectives = perspectiveEnabled && Boolean(perspectivePermissions?.use);
468
+ const canUseRoleDefaultsFeature = Boolean(perspectivePermissions?.roleDefaults);
469
+ React.useEffect(() => {
470
+ if (!canUsePerspectives && isPerspectiveOpen) {
471
+ setPerspectiveOpen(false);
472
+ }
473
+ }, [canUsePerspectives, isPerspectiveOpen]);
474
+ React.useEffect(() => {
475
+ if (!perspectiveTableId) return;
476
+ if (!mergedInitialSettings) return;
477
+ const snapshot = {
478
+ perspectiveId: initialActiveId,
479
+ settings: mergedInitialSettings,
480
+ updatedAt: Date.now()
481
+ };
482
+ writePerspectiveSnapshot(perspectiveTableId, snapshot);
483
+ initialSnapshotRef.current = snapshot;
484
+ }, [perspectiveTableId, mergedInitialSettings, initialActiveId]);
485
+ const perspectiveQuery = useQuery({
486
+ queryKey: ["table-perspectives", perspectiveTableId],
487
+ queryFn: async () => {
488
+ if (!perspectiveTableId) throw new Error("Missing table id");
489
+ const call = await apiCall(`/api/perspectives/${encodeURIComponent(perspectiveTableId)}`);
490
+ if (call.status === 404) {
491
+ setPerspectiveApiMissing(true);
492
+ return {
493
+ tableId: perspectiveTableId,
494
+ perspectives: [],
495
+ defaultPerspectiveId: null,
496
+ rolePerspectives: [],
497
+ roles: [],
498
+ canApplyToRoles: false
499
+ };
500
+ }
501
+ if (!call.ok) {
502
+ await raiseCrudError(call.response, t("ui.dataTable.perspectives.error.load", "Failed to load perspectives"));
503
+ }
504
+ setPerspectiveApiMissing(false);
505
+ const payload = call.result;
506
+ if (!payload) throw new Error(t("ui.dataTable.perspectives.error.load", "Failed to load perspectives"));
507
+ return payload;
508
+ },
509
+ enabled: canUsePerspectives,
510
+ initialData: perspectiveConfig?.initialState?.response,
511
+ staleTime: 5 * 60 * 1e3,
512
+ gcTime: 10 * 60 * 1e3
513
+ });
514
+ const perspectiveData = perspectiveQuery.data;
515
+ const initialPerspectiveAppliedRef = React.useRef(Boolean(mergedInitialSettings));
516
+ const DATE_FORMAT = process.env.NEXT_PUBLIC_DATE_FORMAT || "YYYY-MM-DD HH:mm";
517
+ const pad2 = (n) => n < 10 ? `0${n}` : String(n);
518
+ const simpleFormat = (d, fmt) => {
519
+ const YYYY = String(d.getFullYear());
520
+ const MM = pad2(d.getMonth() + 1);
521
+ const DD = pad2(d.getDate());
522
+ const HH = pad2(d.getHours());
523
+ const mm = pad2(d.getMinutes());
524
+ const ss = pad2(d.getSeconds());
525
+ return fmt.replace(/YYYY/g, YYYY).replace(/MM/g, MM).replace(/DD/g, DD).replace(/HH/g, HH).replace(/mm/g, mm).replace(/ss/g, ss);
526
+ };
527
+ const tryParseDate = (v) => {
528
+ if (v == null) return null;
529
+ if (v instanceof Date) return isNaN(v.getTime()) ? null : v;
530
+ if (typeof v === "number") {
531
+ const d = new Date(v);
532
+ return isNaN(d.getTime()) ? null : d;
533
+ }
534
+ if (typeof v === "string") {
535
+ const s = v.trim();
536
+ if (!s) return null;
537
+ if (/^\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$/.test(s)) {
538
+ const d2 = new Date(s);
539
+ return isNaN(d2.getTime()) ? null : d2;
540
+ }
541
+ const d = new Date(s);
542
+ return isNaN(d.getTime()) ? null : d;
543
+ }
544
+ return null;
545
+ };
546
+ const [dateColumnIds, setDateColumnIds] = React.useState(null);
547
+ React.useEffect(() => {
548
+ if (dateColumnIds) return;
549
+ if (!data || data.length === 0) return;
550
+ const accessors = columns.map((c) => {
551
+ const key = c.accessorKey;
552
+ const id = c.id;
553
+ return { id: id || key || "", key };
554
+ });
555
+ const guessed = /* @__PURE__ */ new Set();
556
+ accessors.forEach((a) => {
557
+ if (!a.id) return;
558
+ const name = a.id;
559
+ if (name.endsWith("_at")) {
560
+ guessed.add(name);
561
+ return;
562
+ }
563
+ });
564
+ setDateColumnIds(guessed);
565
+ }, [dateColumnIds, data, columns]);
566
+ const responsiveClass = (priority, hidden) => {
567
+ if (hidden) return "hidden";
568
+ switch (priority) {
569
+ case 2:
570
+ return "hidden sm:table-cell";
571
+ case 3:
572
+ return "hidden md:table-cell";
573
+ case 4:
574
+ return "hidden lg:table-cell";
575
+ case 5:
576
+ return "hidden xl:table-cell";
577
+ case 6:
578
+ return "hidden 2xl:table-cell";
579
+ default:
580
+ return "";
581
+ }
582
+ };
583
+ const resolvePriority = React.useCallback((column) => {
584
+ const meta = column.columnDef?.meta;
585
+ const rawPriority = typeof meta?.priority === "number" ? meta.priority : void 0;
586
+ if (rawPriority && rawPriority > 0) return rawPriority;
587
+ const index = column.getIndex();
588
+ return index <= 1 ? 1 : 2;
589
+ }, []);
590
+ const initialSorting = React.useMemo(() => {
591
+ if (mergedInitialSettings?.sorting) {
592
+ return mergedInitialSettings.sorting.map((item) => ({ id: item.id, desc: Boolean(item.desc) }));
593
+ }
594
+ return [];
595
+ }, [mergedInitialSettings]);
596
+ const [sorting, setSorting] = React.useState(() => {
597
+ if (sortingProp && sortingProp.length) return sortingProp;
598
+ if (initialSorting.length) return initialSorting;
599
+ return [];
600
+ });
601
+ const table = useReactTable({
602
+ data,
603
+ columns,
604
+ getCoreRowModel: getCoreRowModel(),
605
+ ...sortable ? { getSortedRowModel: getSortedRowModel() } : {},
606
+ state: { sorting, columnVisibility, columnOrder },
607
+ onSortingChange: (updater) => {
608
+ const next = typeof updater === "function" ? updater(sorting) : updater;
609
+ setSorting(next);
610
+ onSortingChange?.(next);
611
+ },
612
+ onColumnVisibilityChange: (updater) => {
613
+ const next = typeof updater === "function" ? updater(columnVisibility) : updater;
614
+ setColumnVisibility(next);
615
+ },
616
+ onColumnOrderChange: (updater) => {
617
+ const next = typeof updater === "function" ? updater(columnOrder) : updater;
618
+ setColumnOrder(next);
619
+ }
620
+ });
621
+ React.useEffect(() => {
622
+ if (sortingProp) setSorting(sortingProp);
623
+ }, [sortingProp]);
624
+ React.useEffect(() => {
625
+ const ids = table.getAllLeafColumns().map((column) => column.id);
626
+ if (!ids.length) return;
627
+ setColumnOrder((prev) => {
628
+ if (!prev.length) return ids;
629
+ const allowed = ids;
630
+ const filtered = prev.filter((id) => allowed.includes(id));
631
+ const seen = new Set(filtered);
632
+ for (const id of allowed) {
633
+ if (!seen.has(id)) {
634
+ filtered.push(id);
635
+ seen.add(id);
636
+ }
637
+ }
638
+ const changed = filtered.length !== prev.length || filtered.some((id, index) => id !== prev[index]);
639
+ return changed ? filtered : prev;
640
+ });
641
+ }, [table, columns]);
642
+ const initialVisibilityApplied = React.useRef(Boolean(mergedInitialSettings?.columnVisibility));
643
+ React.useEffect(() => {
644
+ if (initialVisibilityApplied.current) return;
645
+ const hidden = {};
646
+ table.getAllLeafColumns().forEach((column) => {
647
+ const hiddenMeta = column.columnDef?.meta?.hidden;
648
+ if (hiddenMeta) hidden[column.id] = false;
649
+ });
650
+ if (Object.keys(hidden).length) {
651
+ setColumnVisibility((prev) => ({ ...hidden, ...prev }));
652
+ }
653
+ initialVisibilityApplied.current = true;
654
+ }, [table, columns]);
655
+ const getCurrentSettings = React.useCallback(() => {
656
+ const visibility = {};
657
+ for (const [key, value] of Object.entries(columnVisibility)) {
658
+ if (typeof key === "string" && typeof value === "boolean") {
659
+ visibility[key] = value;
660
+ }
661
+ }
662
+ const filtersRecord = {};
663
+ for (const [key, value] of Object.entries(filterValues ?? {})) {
664
+ if (typeof key === "string") filtersRecord[key] = value;
665
+ }
666
+ const candidate = {
667
+ columnOrder,
668
+ columnVisibility: visibility,
669
+ sorting,
670
+ filters: filtersRecord,
671
+ searchValue
672
+ };
673
+ return sanitizePerspectiveSettings(candidate) ?? {};
674
+ }, [columnOrder, columnVisibility, sorting, filterValues, searchValue]);
675
+ const applyPerspectiveSettings = React.useCallback((settings, nextId) => {
676
+ const normalized = sanitizePerspectiveSettings(settings) ?? {};
677
+ if (normalized.columnOrder && normalized.columnOrder.length) {
678
+ setColumnOrder(normalized.columnOrder);
679
+ } else {
680
+ const ids = table.getAllLeafColumns().map((column) => column.id);
681
+ if (ids.length) setColumnOrder(ids);
682
+ }
683
+ if (normalized.columnVisibility) setColumnVisibility(normalized.columnVisibility);
684
+ else setColumnVisibility({});
685
+ if (normalized.sorting) {
686
+ const sortingState = normalized.sorting.map((item) => ({
687
+ id: item.id,
688
+ desc: item.desc === true
689
+ }));
690
+ setSorting(sortingState);
691
+ onSortingChange?.(sortingState);
692
+ } else {
693
+ setSorting([]);
694
+ onSortingChange?.([]);
695
+ }
696
+ if (onFiltersApply) {
697
+ onFiltersApply(normalized.filters ?? {});
698
+ }
699
+ if (onSearchChange) {
700
+ onSearchChange(normalized.searchValue ?? "");
701
+ }
702
+ setActivePerspectiveId(nextId);
703
+ if (perspectiveTableId) {
704
+ writePerspectiveCookie(perspectiveTableId, nextId);
705
+ if (nextId) {
706
+ const snapshot = { perspectiveId: nextId, settings: normalized, updatedAt: Date.now() };
707
+ writePerspectiveSnapshot(perspectiveTableId, snapshot);
708
+ initialSnapshotRef.current = snapshot;
709
+ } else {
710
+ writePerspectiveSnapshot(perspectiveTableId, null);
711
+ initialSnapshotRef.current = null;
712
+ }
713
+ }
714
+ }, [onFiltersApply, onSearchChange, onSortingChange, perspectiveTableId, table]);
715
+ const perspectiveQueryKey = ["table-perspectives", perspectiveTableId];
716
+ const savePerspectiveMutation = useMutation({
717
+ mutationFn: async (input) => {
718
+ if (!perspectiveTableId) throw new Error("Missing table id");
719
+ const payload = {
720
+ perspectiveId: input.perspectiveId ?? void 0,
721
+ name: input.name,
722
+ settings: getCurrentSettings(),
723
+ isDefault: input.isDefault,
724
+ applyToRoles: input.applyToRoles,
725
+ setRoleDefault: input.setRoleDefault
726
+ };
727
+ if (process.env.NODE_ENV !== "production") {
728
+ console.debug("[DataTable] perspective payload", payload);
729
+ }
730
+ const call = await apiCall(
731
+ `/api/perspectives/${encodeURIComponent(perspectiveTableId)}`,
732
+ {
733
+ method: "POST",
734
+ headers: { "Content-Type": "application/json" },
735
+ body: JSON.stringify(payload)
736
+ }
737
+ );
738
+ if (call.status === 404) {
739
+ throw new Error(t("ui.dataTable.perspectives.error.apiUnavailable", "Perspectives API is not available. Run `npm run modules:prepare` to regenerate module routes and restart the dev server."));
740
+ }
741
+ if (!call.ok) {
742
+ await raiseCrudError(call.response, t("ui.dataTable.perspectives.error.save", "Failed to save perspective"));
743
+ }
744
+ const result = call.result;
745
+ if (!result) throw new Error(t("ui.dataTable.perspectives.error.save", "Failed to save perspective"));
746
+ return result;
747
+ },
748
+ onSuccess: (data2) => {
749
+ if (perspectiveTableId) {
750
+ void queryClient.invalidateQueries({ queryKey: perspectiveQueryKey });
751
+ }
752
+ if (data2.perspective) {
753
+ applyPerspectiveSettings(data2.perspective.settings, data2.perspective.id);
754
+ }
755
+ }
756
+ });
757
+ const resolveColumnLabel = React.useCallback((column) => {
758
+ const meta = column.columnDef?.meta;
759
+ if (typeof meta?.label === "string" && meta.label.trim().length > 0) return meta.label.trim();
760
+ if (typeof meta?.title === "string" && meta.title.trim().length > 0) return meta.title.trim();
761
+ const header = column.columnDef.header;
762
+ if (typeof header === "string") return header;
763
+ if (typeof header === "function") return normalizeLabel(column.id);
764
+ return normalizeLabel(column.id);
765
+ }, []);
766
+ const columnOptions = React.useMemo(() => {
767
+ const leaves = table.getAllLeafColumns();
768
+ const baseOrder = columnOrder.length ? columnOrder : leaves.map((column) => column.id);
769
+ const seen = /* @__PURE__ */ new Set();
770
+ const ordered = baseOrder.map((id) => {
771
+ const col = leaves.find((column) => column.id === id);
772
+ if (!col) return null;
773
+ seen.add(id);
774
+ return col;
775
+ }).filter(Boolean);
776
+ leaves.forEach((column) => {
777
+ if (!seen.has(column.id)) ordered.push(column);
778
+ });
779
+ return ordered.map((column) => ({
780
+ id: column.id,
781
+ label: resolveColumnLabel(column),
782
+ visible: columnVisibility[column.id] ?? column.getIsVisible(),
783
+ canHide: column.getCanHide()
784
+ }));
785
+ }, [table, columnOrder, resolveColumnLabel, columnVisibility, columns]);
786
+ const activePersonalPerspectiveId = React.useMemo(() => {
787
+ if (!perspectiveData || !activePerspectiveId) return null;
788
+ const found = perspectiveData.perspectives.find((p) => p.id === activePerspectiveId);
789
+ return found ? found.id : null;
790
+ }, [perspectiveData, activePerspectiveId]);
791
+ const deletePerspectiveMutation = useMutation({
792
+ mutationFn: async ({ perspectiveId }) => {
793
+ if (!perspectiveTableId) throw new Error("Missing table id");
794
+ const call = await apiCall(
795
+ `/api/perspectives/${encodeURIComponent(perspectiveTableId)}/${encodeURIComponent(perspectiveId)}`,
796
+ { method: "DELETE" }
797
+ );
798
+ if (call.status === 404) throw new Error(t("ui.dataTable.perspectives.error.apiUnavailable", "Perspectives API is not available. Run `npm run modules:prepare` and restart the dev server."));
799
+ if (!call.ok) {
800
+ await raiseCrudError(call.response, t("ui.dataTable.perspectives.error.delete", "Failed to delete perspective"));
801
+ }
802
+ },
803
+ onMutate: ({ perspectiveId }) => {
804
+ setDeletingIds((prev) => prev.includes(perspectiveId) ? prev : [...prev, perspectiveId]);
805
+ },
806
+ onSettled: (_data, _error, variables) => {
807
+ setDeletingIds((prev) => prev.filter((id) => id !== variables.perspectiveId));
808
+ },
809
+ onSuccess: (_data, variables) => {
810
+ const removedActive = activePerspectiveId === variables.perspectiveId;
811
+ if (perspectiveTableId) {
812
+ void queryClient.invalidateQueries({ queryKey: perspectiveQueryKey });
813
+ if (removedActive) {
814
+ setActivePerspectiveId(null);
815
+ writePerspectiveCookie(perspectiveTableId, null);
816
+ writePerspectiveSnapshot(perspectiveTableId, null);
817
+ initialSnapshotRef.current = null;
818
+ initialPerspectiveAppliedRef.current = false;
819
+ }
820
+ } else if (removedActive) {
821
+ setActivePerspectiveId(null);
822
+ initialPerspectiveAppliedRef.current = false;
823
+ }
824
+ }
825
+ });
826
+ const clearRoleMutation = useMutation({
827
+ mutationFn: async ({ roleId }) => {
828
+ if (!perspectiveTableId) throw new Error("Missing table id");
829
+ const call = await apiCall(
830
+ `/api/perspectives/${encodeURIComponent(perspectiveTableId)}/roles/${encodeURIComponent(roleId)}`,
831
+ { method: "DELETE" }
832
+ );
833
+ if (call.status === 404) throw new Error(t("ui.dataTable.perspectives.error.apiUnavailable", "Perspectives API is not available. Run `npm run modules:prepare` and restart the dev server."));
834
+ if (!call.ok) {
835
+ await raiseCrudError(call.response, t("ui.dataTable.perspectives.error.clearRoles", "Failed to clear role perspectives"));
836
+ }
837
+ },
838
+ onMutate: ({ roleId }) => {
839
+ setRoleClearingIds((prev) => prev.includes(roleId) ? prev : [...prev, roleId]);
840
+ },
841
+ onSettled: (_data, _error, variables) => {
842
+ setRoleClearingIds((prev) => prev.filter((id) => id !== variables.roleId));
843
+ },
844
+ onSuccess: (_data, variables) => {
845
+ if (perspectiveTableId) {
846
+ void queryClient.invalidateQueries({ queryKey: perspectiveQueryKey });
847
+ }
848
+ if (activePerspectiveId) {
849
+ const current = queryClient.getQueryData(perspectiveQueryKey);
850
+ const match = current?.rolePerspectives.find((rp) => rp.id === activePerspectiveId);
851
+ if (match && match.roleId === variables.roleId) {
852
+ setActivePerspectiveId(null);
853
+ if (perspectiveTableId) writePerspectiveCookie(perspectiveTableId, null);
854
+ if (perspectiveTableId) writePerspectiveSnapshot(perspectiveTableId, null);
855
+ initialSnapshotRef.current = null;
856
+ initialPerspectiveAppliedRef.current = false;
857
+ }
858
+ }
859
+ }
860
+ });
861
+ const handlePerspectiveActivate = React.useCallback((item, _source) => {
862
+ applyPerspectiveSettings(item.settings, item.id);
863
+ setPerspectiveOpen(false);
864
+ }, [applyPerspectiveSettings]);
865
+ const handlePerspectiveSave = React.useCallback(async (input) => {
866
+ const normalizedRoles = Array.from(new Set(input.applyToRoles));
867
+ await savePerspectiveMutation.mutateAsync({
868
+ name: input.name.trim(),
869
+ isDefault: input.isDefault,
870
+ applyToRoles: normalizedRoles,
871
+ setRoleDefault: normalizedRoles.length > 0 ? input.setRoleDefault : false,
872
+ perspectiveId: activePersonalPerspectiveId
873
+ });
874
+ }, [savePerspectiveMutation, activePersonalPerspectiveId]);
875
+ const handlePerspectiveDelete = React.useCallback(async (perspectiveId) => {
876
+ await deletePerspectiveMutation.mutateAsync({ perspectiveId });
877
+ }, [deletePerspectiveMutation]);
878
+ const handleClearRole = React.useCallback(async (roleId) => {
879
+ await clearRoleMutation.mutateAsync({ roleId });
880
+ }, [clearRoleMutation]);
881
+ const handleToggleColumn = React.useCallback((columnId, visible) => {
882
+ const column = table.getColumn(columnId);
883
+ if (!column) return;
884
+ setColumnVisibility((prev) => {
885
+ const next = { ...prev };
886
+ if (visible) delete next[columnId];
887
+ else next[columnId] = false;
888
+ return next;
889
+ });
890
+ column.toggleVisibility(visible);
891
+ }, [table]);
892
+ const handleMoveColumn = React.useCallback((columnId, direction) => {
893
+ setColumnOrder((prev) => {
894
+ const idx = prev.indexOf(columnId);
895
+ if (idx === -1) return prev;
896
+ const swap = direction === "up" ? idx - 1 : idx + 1;
897
+ if (swap < 0 || swap >= prev.length) return prev;
898
+ const next = [...prev];
899
+ const tmp = next[swap];
900
+ next[swap] = next[idx];
901
+ next[idx] = tmp;
902
+ table.setColumnOrder(next);
903
+ return next;
904
+ });
905
+ }, [table]);
906
+ const perspectiveApiWarning = perspectiveApiMissing && canUsePerspectives ? t("ui.dataTable.perspectives.warning.apiUnavailable", "Perspectives API is not available yet. Run `npm run modules:prepare` to regenerate module routes, then restart the server.") : null;
907
+ const loadStartRef = React.useRef(null);
908
+ const [measuredDurationMs, setMeasuredDurationMs] = React.useState(null);
909
+ React.useEffect(() => {
910
+ if (typeof isLoading !== "boolean") return;
911
+ if (isLoading) {
912
+ if (loadStartRef.current === null) {
913
+ const now = typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
914
+ loadStartRef.current = now;
915
+ }
916
+ return;
917
+ }
918
+ if (loadStartRef.current !== null) {
919
+ const now = typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
920
+ setMeasuredDurationMs(now - loadStartRef.current);
921
+ loadStartRef.current = null;
922
+ }
923
+ }, [isLoading]);
924
+ React.useLayoutEffect(() => {
925
+ if (!canUsePerspectives) return;
926
+ if (!perspectiveTableId) return;
927
+ if (initialPerspectiveAppliedRef.current && activePerspectiveId != null) return;
928
+ const source = perspectiveData ?? perspectiveConfig?.initialState?.response;
929
+ if (!source) return;
930
+ const tryResolve = (id) => {
931
+ if (!id) return void 0;
932
+ return source.perspectives.find((p) => p.id === id) ?? source.rolePerspectives.find((p) => p.id === id);
933
+ };
934
+ let target;
935
+ if (activePerspectiveId) {
936
+ target = tryResolve(activePerspectiveId);
937
+ }
938
+ const cookieId = readPerspectiveCookie(perspectiveTableId);
939
+ if (!target && cookieId) target = tryResolve(cookieId);
940
+ if (!target && source.defaultPerspectiveId) {
941
+ target = tryResolve(source.defaultPerspectiveId);
942
+ }
943
+ if (!target) {
944
+ target = source.rolePerspectives.find((p) => p.isDefault);
945
+ }
946
+ if (!target) {
947
+ target = source.perspectives[0];
948
+ }
949
+ if (target) {
950
+ applyPerspectiveSettings(target.settings, target.id);
951
+ }
952
+ initialPerspectiveAppliedRef.current = true;
953
+ }, [canUsePerspectives, perspectiveData, perspectiveTableId, perspectiveConfig, applyPerspectiveSettings, activePerspectiveId]);
954
+ const renderPagination = () => {
955
+ if (!pagination) return null;
956
+ const { page, totalPages, onPageChange, durationMs, cacheStatus } = pagination;
957
+ const startItem = (page - 1) * pagination.pageSize + 1;
958
+ const endItem = Math.min(page * pagination.pageSize, pagination.total);
959
+ const effectiveDuration = typeof durationMs === "number" && Number.isFinite(durationMs) && durationMs >= 0 ? durationMs : measuredDurationMs ?? void 0;
960
+ const durationLabel = formatDurationLabel(effectiveDuration);
961
+ const normalizedCacheStatus = cacheStatus === "hit" || cacheStatus === "miss" ? cacheStatus : null;
962
+ const cacheBadge = normalizedCacheStatus ? /* @__PURE__ */ jsxs(
963
+ "span",
964
+ {
965
+ className: "inline-flex items-center justify-center",
966
+ "aria-label": t("ui.dataTable.pagination.cache.ariaLabel", "Cache {status}", { status: normalizedCacheStatus.toUpperCase() }),
967
+ title: t("ui.dataTable.pagination.cache.title", "Cache {status}", { status: normalizedCacheStatus.toUpperCase() }),
968
+ children: [
969
+ /* @__PURE__ */ jsx(
970
+ Circle,
971
+ {
972
+ className: `h-3.5 w-3.5 ${normalizedCacheStatus === "hit" ? "text-emerald-500" : "text-amber-500"}`,
973
+ strokeWidth: 3
974
+ }
975
+ ),
976
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("ui.dataTable.pagination.cache.srOnly", "Cache {status}", { status: normalizedCacheStatus.toUpperCase() }) })
977
+ ]
978
+ }
979
+ ) : null;
980
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-t", children: [
981
+ /* @__PURE__ */ jsxs("div", { className: "text-sm text-muted-foreground flex items-center gap-2", children: [
982
+ /* @__PURE__ */ jsx("span", { children: durationLabel ? t("ui.dataTable.pagination.resultsWithDuration", "Showing {start} to {end} of {total} results in {duration}", { start: startItem, end: endItem, total: pagination.total, duration: durationLabel }) : t("ui.dataTable.pagination.results", "Showing {start} to {end} of {total} results", { start: startItem, end: endItem, total: pagination.total }) }),
983
+ cacheBadge
984
+ ] }),
985
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
986
+ /* @__PURE__ */ jsx(
987
+ Button,
988
+ {
989
+ variant: "outline",
990
+ size: "sm",
991
+ onClick: () => onPageChange(page - 1),
992
+ disabled: page <= 1,
993
+ children: t("ui.dataTable.pagination.previous", "Previous")
994
+ }
995
+ ),
996
+ /* @__PURE__ */ jsx("span", { className: "text-sm", children: t("ui.dataTable.pagination.pageInfo", "Page {page} of {totalPages}", { page, totalPages }) }),
997
+ /* @__PURE__ */ jsx(
998
+ Button,
999
+ {
1000
+ variant: "outline",
1001
+ size: "sm",
1002
+ onClick: () => onPageChange(page + 1),
1003
+ disabled: page >= totalPages,
1004
+ children: t("ui.dataTable.pagination.next", "Next")
1005
+ }
1006
+ )
1007
+ ] })
1008
+ ] });
1009
+ };
1010
+ const resolvedEntityIds = React.useMemo(() => {
1011
+ if (Array.isArray(entityIds) && entityIds.length) {
1012
+ const dedup = /* @__PURE__ */ new Set();
1013
+ const list = [];
1014
+ entityIds.forEach((id) => {
1015
+ const trimmed = typeof id === "string" ? id.trim() : "";
1016
+ if (!trimmed || dedup.has(trimmed)) return;
1017
+ dedup.add(trimmed);
1018
+ list.push(trimmed);
1019
+ });
1020
+ return list;
1021
+ }
1022
+ if (typeof entityId === "string" && entityId.trim().length > 0) {
1023
+ return [entityId.trim()];
1024
+ }
1025
+ return [];
1026
+ }, [entityId, entityIds]);
1027
+ const entityKey = React.useMemo(() => resolvedEntityIds.length ? resolvedEntityIds.join("|") : null, [resolvedEntityIds]);
1028
+ const customFieldFilterExtrasSignature = React.useMemo(
1029
+ () => JSON.stringify(customFieldFilterKeyExtras ?? []),
1030
+ [customFieldFilterKeyExtras]
1031
+ );
1032
+ const [cfFilterFieldsetsByEntity, setCfFilterFieldsetsByEntity] = React.useState({});
1033
+ const [cfFilterFieldsetSelection, setCfFilterFieldsetSelection] = React.useState({});
1034
+ React.useEffect(() => {
1035
+ if (!entityKey) {
1036
+ setCfFilterFieldsetsByEntity({});
1037
+ setCfFilterFieldsetSelection({});
1038
+ return;
1039
+ }
1040
+ let cancelled = false;
1041
+ const loadFieldsets = async () => {
1042
+ try {
1043
+ const payload = await fetchCustomFieldDefinitionsPayload(resolvedEntityIds);
1044
+ if (cancelled) return;
1045
+ const fieldsets = payload.fieldsetsByEntity ?? {};
1046
+ setCfFilterFieldsetsByEntity(fieldsets);
1047
+ const selectionChanges = [];
1048
+ let shouldNotify = false;
1049
+ setCfFilterFieldsetSelection((prev) => {
1050
+ const next = {};
1051
+ let changed = false;
1052
+ resolvedEntityIds.forEach((entityId2) => {
1053
+ const list = fieldsets[entityId2] ?? [];
1054
+ if (!list.length) {
1055
+ if (prev[entityId2] !== void 0) changed = true;
1056
+ return;
1057
+ }
1058
+ const existing = prev[entityId2];
1059
+ const fallback = list[0]?.code ?? null;
1060
+ const isValidExisting = existing ? list.some((entry) => entry.code === existing) : false;
1061
+ const value = isValidExisting ? existing : fallback ?? null;
1062
+ next[entityId2] = value;
1063
+ if (value !== existing) {
1064
+ changed = true;
1065
+ selectionChanges.push([entityId2, value]);
1066
+ }
1067
+ });
1068
+ if (Object.keys(prev).length !== Object.keys(next).length) changed = true;
1069
+ if (changed) {
1070
+ shouldNotify = true;
1071
+ return next;
1072
+ }
1073
+ return prev;
1074
+ });
1075
+ if (shouldNotify && selectionChanges.length && onCustomFieldFilterFieldsetChange) {
1076
+ selectionChanges.forEach(([entityId2, value]) => onCustomFieldFilterFieldsetChange(value, entityId2));
1077
+ }
1078
+ } catch {
1079
+ if (!cancelled) {
1080
+ setCfFilterFieldsetsByEntity({});
1081
+ setCfFilterFieldsetSelection({});
1082
+ }
1083
+ }
1084
+ };
1085
+ loadFieldsets();
1086
+ return () => {
1087
+ cancelled = true;
1088
+ };
1089
+ }, [customFieldFilterExtrasSignature, entityKey, onCustomFieldFilterFieldsetChange, resolvedEntityIds]);
1090
+ const supportsCustomFieldFilterFieldsets = resolvedEntityIds.length === 1 && (cfFilterFieldsetsByEntity[resolvedEntityIds[0]]?.length ?? 0) > 0;
1091
+ const activeCustomFieldFilterFieldset = supportsCustomFieldFilterFieldsets ? cfFilterFieldsetSelection[resolvedEntityIds[0]] ?? cfFilterFieldsetsByEntity[resolvedEntityIds[0]]?.[0]?.code ?? null : null;
1092
+ const handleCustomFieldFilterFieldsetChange = React.useCallback(
1093
+ (value) => {
1094
+ if (!supportsCustomFieldFilterFieldsets) return;
1095
+ const entityId2 = resolvedEntityIds[0];
1096
+ const nextValue = value || null;
1097
+ setCfFilterFieldsetSelection((prev) => {
1098
+ if (prev[entityId2] === nextValue) return prev;
1099
+ return { ...prev, [entityId2]: nextValue };
1100
+ });
1101
+ if (onCustomFieldFilterFieldsetChange) {
1102
+ onCustomFieldFilterFieldsetChange(nextValue, entityId2);
1103
+ }
1104
+ },
1105
+ [onCustomFieldFilterFieldsetChange, resolvedEntityIds, supportsCustomFieldFilterFieldsets]
1106
+ );
1107
+ const { data: cfFilters = [] } = useCustomFieldFilterDefs(entityKey ? resolvedEntityIds : [], {
1108
+ enabled: !!entityKey,
1109
+ fieldset: supportsCustomFieldFilterFieldsets ? activeCustomFieldFilterFieldset ?? void 0 : void 0,
1110
+ keyExtras: customFieldFilterKeyExtras
1111
+ });
1112
+ const builtToolbar = React.useMemo(() => {
1113
+ if (toolbar) return toolbar;
1114
+ const anySearch = onSearchChange != null;
1115
+ const anyFilters = baseFilters && baseFilters.length > 0 || cfFilters && cfFilters.length > 0;
1116
+ if (!anySearch && !anyFilters) return null;
1117
+ const baseList = baseFilters || [];
1118
+ const existing = new Set(baseList.map((f) => f.id));
1119
+ const cfOnly = (cfFilters || []).filter((f) => !existing.has(f.id));
1120
+ const combined = [...baseList, ...cfOnly];
1121
+ const perspectiveButton = canUsePerspectives ? /* @__PURE__ */ jsxs(Button, { variant: "outline", className: "h-9", onClick: () => setPerspectiveOpen(true), children: [
1122
+ /* @__PURE__ */ jsx(SlidersHorizontal, { className: "mr-2 h-4 w-4" }),
1123
+ t("ui.dataTable.perspectives.button", "Perspectives")
1124
+ ] }) : null;
1125
+ const fieldsetSelector = supportsCustomFieldFilterFieldsets && resolvedEntityIds.length === 1 ? /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
1126
+ /* @__PURE__ */ jsx("div", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: "Fieldset" }),
1127
+ /* @__PURE__ */ jsx(
1128
+ "select",
1129
+ {
1130
+ className: "w-full rounded border bg-background px-2 py-2 text-sm",
1131
+ value: activeCustomFieldFilterFieldset ?? "",
1132
+ onChange: (event) => handleCustomFieldFilterFieldsetChange(event.target.value),
1133
+ children: (cfFilterFieldsetsByEntity[resolvedEntityIds[0]] ?? []).map((fieldset) => /* @__PURE__ */ jsx("option", { value: fieldset.code, children: fieldset.label }, fieldset.code))
1134
+ }
1135
+ )
1136
+ ] }) : null;
1137
+ const leadingItems = perspectiveButton ? /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: perspectiveButton }) : null;
1138
+ return /* @__PURE__ */ jsx(
1139
+ FilterBar,
1140
+ {
1141
+ searchValue,
1142
+ onSearchChange,
1143
+ searchPlaceholder,
1144
+ searchAlign,
1145
+ filters: combined,
1146
+ values: filterValues,
1147
+ onApply: onFiltersApply,
1148
+ onClear: onFiltersClear,
1149
+ leadingItems,
1150
+ filtersExtraContent: fieldsetSelector,
1151
+ layout: embedded ? "inline" : "stacked",
1152
+ className: embedded ? "min-h-[2.25rem]" : void 0
1153
+ }
1154
+ );
1155
+ }, [
1156
+ toolbar,
1157
+ searchValue,
1158
+ onSearchChange,
1159
+ searchPlaceholder,
1160
+ searchAlign,
1161
+ baseFilters,
1162
+ cfFilters,
1163
+ filterValues,
1164
+ onFiltersApply,
1165
+ onFiltersClear,
1166
+ canUsePerspectives,
1167
+ embedded,
1168
+ supportsCustomFieldFilterFieldsets,
1169
+ resolvedEntityIds,
1170
+ activeCustomFieldFilterFieldset,
1171
+ handleCustomFieldFilterFieldsetChange,
1172
+ cfFilterFieldsetsByEntity
1173
+ ]);
1174
+ const hasTitle = title != null;
1175
+ const hasActions = actions !== void 0 && actions !== null && actions !== false;
1176
+ const shouldReserveActionsSpace = actions === null || actions === false;
1177
+ const exportConfig = exporter === false ? null : exporter || null;
1178
+ const resolvedExportSections = React.useMemo(() => resolveExportSections(exportConfig), [exportConfig]);
1179
+ const hasExport = resolvedExportSections.length > 0;
1180
+ const refreshButtonConfig = refreshButton;
1181
+ const hasRefreshButton = Boolean(refreshButtonConfig);
1182
+ const hasToolbar = builtToolbar != null;
1183
+ const shouldRenderActionsWrapper = hasActions || hasRefreshButton || shouldReserveActionsSpace || hasExport;
1184
+ const renderToolbarInline = embedded && hasToolbar;
1185
+ const shouldRenderToolbarBelow = hasToolbar && !renderToolbarInline;
1186
+ const shouldRenderHeader = hasTitle || renderToolbarInline || shouldRenderActionsWrapper || shouldRenderToolbarBelow;
1187
+ const resolvedInjectionSpotId = injectionSpotId ?? (perspective?.tableId ? `data-table:${perspective.tableId}` : null);
1188
+ const resolvedInjectionContext = React.useMemo(
1189
+ () => injectionContext ?? { tableId: perspective?.tableId ?? null, title: typeof title === "string" ? title : void 0 },
1190
+ [injectionContext, perspective?.tableId, title]
1191
+ );
1192
+ const headerInjectionSpotId = React.useMemo(
1193
+ () => resolvedInjectionSpotId ? `${resolvedInjectionSpotId}:header` : null,
1194
+ [resolvedInjectionSpotId]
1195
+ );
1196
+ const footerInjectionSpotId = React.useMemo(
1197
+ () => resolvedInjectionSpotId ? `${resolvedInjectionSpotId}:footer` : null,
1198
+ [resolvedInjectionSpotId]
1199
+ );
1200
+ const containerClassName = embedded ? "" : "rounded-lg border bg-card";
1201
+ const headerWrapperClassName = embedded ? "pb-3" : "px-4 py-3 border-b";
1202
+ const headerContentClassName = "flex items-center justify-between gap-2";
1203
+ const toolbarWrapperClassName = embedded ? "mt-2" : "mt-3 pt-3 border-t";
1204
+ const tableScrollWrapperClassName = embedded ? "" : "overflow-auto";
1205
+ const titleContent = hasTitle ? /* @__PURE__ */ jsx("div", { className: "text-base font-semibold leading-tight min-h-[2.25rem] flex items-center", children: typeof title === "string" ? /* @__PURE__ */ jsx("h2", { className: "text-base font-semibold", children: title }) : title }) : /* @__PURE__ */ jsx("div", { className: "min-h-[2.25rem]" });
1206
+ return /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 300, children: /* @__PURE__ */ jsxs("div", { className: containerClassName, children: [
1207
+ shouldRenderHeader && /* @__PURE__ */ jsxs("div", { className: headerWrapperClassName, children: [
1208
+ (hasTitle || shouldRenderActionsWrapper || renderToolbarInline) && /* @__PURE__ */ jsxs("div", { className: headerContentClassName, children: [
1209
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-w-0", children: renderToolbarInline ? builtToolbar : titleContent }),
1210
+ shouldRenderActionsWrapper ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 min-h-[2.25rem]", children: [
1211
+ refreshButtonConfig ? /* @__PURE__ */ jsxs(
1212
+ Button,
1213
+ {
1214
+ type: "button",
1215
+ variant: "ghost",
1216
+ size: "icon",
1217
+ onClick: refreshButtonConfig.onRefresh,
1218
+ "aria-label": refreshButtonConfig.label,
1219
+ title: refreshButtonConfig.label,
1220
+ disabled: refreshButtonConfig.disabled || refreshButtonConfig.isRefreshing,
1221
+ children: [
1222
+ refreshButtonConfig.isRefreshing ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(RefreshCw, { className: "h-4 w-4" }),
1223
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: refreshButtonConfig.label })
1224
+ ]
1225
+ }
1226
+ ) : null,
1227
+ canUsePerspectives ? /* @__PURE__ */ jsxs(
1228
+ Button,
1229
+ {
1230
+ type: "button",
1231
+ variant: "ghost",
1232
+ size: "icon",
1233
+ onClick: () => setPerspectiveOpen(true),
1234
+ "aria-label": t("ui.dataTable.customizeColumns.ariaLabel", "Customize columns"),
1235
+ title: t("ui.dataTable.customizeColumns.title", "Customize columns"),
1236
+ children: [
1237
+ /* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4" }),
1238
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("ui.dataTable.customizeColumns.srOnly", "Customize columns") })
1239
+ ]
1240
+ }
1241
+ ) : null,
1242
+ exportConfig && hasExport ? /* @__PURE__ */ jsx(ExportMenu, { config: exportConfig, sections: resolvedExportSections }) : null,
1243
+ hasActions ? actions : null
1244
+ ] }) : null
1245
+ ] }),
1246
+ shouldRenderToolbarBelow ? /* @__PURE__ */ jsx("div", { className: toolbarWrapperClassName, children: builtToolbar }) : null,
1247
+ headerInjectionSpotId ? /* @__PURE__ */ jsx("div", { className: embedded ? "mt-2" : "mt-3", children: /* @__PURE__ */ jsx(InjectionSpot, { spotId: headerInjectionSpotId, context: resolvedInjectionContext }) }) : null
1248
+ ] }),
1249
+ /* @__PURE__ */ jsx("div", { className: tableScrollWrapperClassName, children: /* @__PURE__ */ jsxs(Table, { children: [
1250
+ /* @__PURE__ */ jsx(TableHeader, { children: table.getHeaderGroups().map((hg) => /* @__PURE__ */ jsxs(TableRow, { children: [
1251
+ hg.headers.map((header) => {
1252
+ const columnMeta = header.column.columnDef?.meta;
1253
+ const priority = resolvePriority(header.column);
1254
+ return /* @__PURE__ */ jsx(TableHead, { className: responsiveClass(priority, columnMeta?.hidden), children: header.isPlaceholder ? null : /* @__PURE__ */ jsxs(
1255
+ "button",
1256
+ {
1257
+ type: "button",
1258
+ className: `inline-flex items-center gap-1 ${sortable && header.column.getCanSort?.() ? "cursor-pointer select-none" : ""}`,
1259
+ onClick: () => sortable && header.column.toggleSorting?.(header.column.getIsSorted() === "asc"),
1260
+ children: [
1261
+ flexRender(header.column.columnDef.header, header.getContext()),
1262
+ sortable && header.column.getIsSorted?.() ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: header.column.getIsSorted() === "asc" ? "\u25B2" : "\u25BC" }) : null
1263
+ ]
1264
+ }
1265
+ ) }, header.id);
1266
+ }),
1267
+ rowActions ? /* @__PURE__ */ jsx(TableHead, { className: "w-0 text-right", children: t("ui.dataTable.actionsColumn", "Actions") }) : null
1268
+ ] }, hg.id)) }),
1269
+ /* @__PURE__ */ jsx(TableBody, { children: isLoading ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, { colSpan: columns.length + (rowActions ? 1 : 0), className: "h-24 text-center", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2", children: [
1270
+ /* @__PURE__ */ jsx(Spinner, { size: "md" }),
1271
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Loading data..." })
1272
+ ] }) }) }) : error ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, { colSpan: columns.length + (rowActions ? 1 : 0), className: "h-24 text-center text-destructive", children: typeof error === "string" ? error : error }) }) : table.getRowModel().rows.length ? table.getRowModel().rows.map((row) => {
1273
+ const isClickable = !disableRowClick && (onRowClick || rowActions && rowActions(row.original));
1274
+ return /* @__PURE__ */ jsxs(
1275
+ TableRow,
1276
+ {
1277
+ "data-state": row.getIsSelected() && "selected",
1278
+ className: isClickable ? "cursor-pointer hover:bg-muted/50 transition-colors" : "",
1279
+ onClick: isClickable ? (e) => {
1280
+ if (e.target.closest("[data-actions-cell]")) {
1281
+ return;
1282
+ }
1283
+ if (onRowClick) {
1284
+ onRowClick(row.original);
1285
+ } else if (rowActions) {
1286
+ const rowActionsElement = rowActions(row.original);
1287
+ if (React.isValidElement(rowActionsElement) && "items" in rowActionsElement.props && Array.isArray(rowActionsElement.props.items)) {
1288
+ const editAction = extractEditAction(rowActionsElement.props.items);
1289
+ if (editAction) {
1290
+ if (editAction.href) {
1291
+ router.push(editAction.href);
1292
+ } else if (editAction.onSelect) {
1293
+ editAction.onSelect();
1294
+ }
1295
+ }
1296
+ }
1297
+ }
1298
+ } : void 0,
1299
+ children: [
1300
+ row.getVisibleCells().map((cell) => {
1301
+ const columnMeta = cell.column.columnDef?.meta;
1302
+ const priority = resolvePriority(cell.column);
1303
+ const hasCustomCell = Boolean(cell.column.columnDef.cell);
1304
+ const columnId = String(cell.column.id || "");
1305
+ const accessorKey = String(cell.column.columnDef?.accessorKey || "");
1306
+ const isDateCol = dateColumnIds ? dateColumnIds.has(columnId) : false;
1307
+ let content;
1308
+ if (isDateCol) {
1309
+ const raw = cell.getValue();
1310
+ const d = tryParseDate(raw);
1311
+ content = d ? simpleFormat(d, DATE_FORMAT) : raw;
1312
+ } else {
1313
+ content = flexRender(cell.column.columnDef.cell, cell.getContext());
1314
+ }
1315
+ const skipTruncation = shouldSkipTruncation(columnId);
1316
+ const truncateConfig = getColumnTruncateConfig(columnId, accessorKey, columnMeta);
1317
+ const shouldTruncate = truncateConfig.truncate && !skipTruncation;
1318
+ const maxWidth = truncateConfig.maxWidth;
1319
+ const cellValue = cell.getValue();
1320
+ const metaTooltipContent = columnMeta?.tooltipContent;
1321
+ const tooltipText = metaTooltipContent ? metaTooltipContent(row.original) : cellValue != null ? String(cellValue) : void 0;
1322
+ const wrappedContent = shouldTruncate ? /* @__PURE__ */ jsx(TruncatedCell, { maxWidth, tooltipContent: tooltipText, children: content }) : content;
1323
+ return /* @__PURE__ */ jsx(TableCell, { className: responsiveClass(priority, columnMeta?.hidden), children: wrappedContent }, cell.id);
1324
+ }),
1325
+ rowActions ? /* @__PURE__ */ jsx(TableCell, { className: "text-right whitespace-nowrap", "data-actions-cell": true, children: rowActions(row.original) }) : null
1326
+ ]
1327
+ },
1328
+ row.id
1329
+ );
1330
+ }) : /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, { colSpan: columns.length + (rowActions ? 1 : 0), className: "h-24 text-center text-muted-foreground", children: emptyState ?? t("ui.dataTable.emptyState.default", "No results.") }) }) })
1331
+ ] }) }),
1332
+ footerInjectionSpotId ? /* @__PURE__ */ jsx("div", { className: embedded ? "mt-3" : "px-4 py-3 border-t", children: /* @__PURE__ */ jsx(InjectionSpot, { spotId: footerInjectionSpotId, context: resolvedInjectionContext }) }) : null,
1333
+ renderPagination(),
1334
+ canUsePerspectives ? /* @__PURE__ */ jsx(
1335
+ PerspectiveSidebar,
1336
+ {
1337
+ open: isPerspectiveOpen,
1338
+ onOpenChange: setPerspectiveOpen,
1339
+ loading: perspectiveQuery.isFetching && !perspectiveQuery.data,
1340
+ perspectives: perspectiveData?.perspectives ?? [],
1341
+ rolePerspectives: perspectiveData?.rolePerspectives ?? [],
1342
+ roles: perspectiveData?.roles ?? [],
1343
+ activePerspectiveId,
1344
+ onActivatePerspective: handlePerspectiveActivate,
1345
+ onDeletePerspective: handlePerspectiveDelete,
1346
+ onClearRole: handleClearRole,
1347
+ onSave: handlePerspectiveSave,
1348
+ canApplyToRoles: Boolean(perspectiveData?.canApplyToRoles && canUseRoleDefaultsFeature),
1349
+ columnOptions,
1350
+ onToggleColumn: handleToggleColumn,
1351
+ onMoveColumn: handleMoveColumn,
1352
+ saving: savePerspectiveMutation.isPending,
1353
+ deletingIds,
1354
+ roleClearingIds,
1355
+ apiWarning: perspectiveApiWarning
1356
+ }
1357
+ ) : null
1358
+ ] }) });
1359
+ }
1360
+ export {
1361
+ DataTable
1362
+ };
1363
+ //# sourceMappingURL=DataTable.js.map