@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,165 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Calendar, dateFnsLocalizer } from "react-big-calendar";
5
+ import { addDays, differenceInCalendarDays, endOfDay, endOfMonth, endOfWeek, format, getDay, parse, startOfDay, startOfMonth, startOfWeek } from "date-fns";
6
+ import { enUS } from "date-fns/locale/en-US";
7
+ import { ScheduleToolbar } from "./ScheduleToolbar.js";
8
+ import { expandRecurringItems } from "./recurrence.js";
9
+ const localizer = dateFnsLocalizer({
10
+ format,
11
+ parse,
12
+ startOfWeek,
13
+ getDay,
14
+ locales: { "en-US": enUS }
15
+ });
16
+ const VIEW_MAP = {
17
+ day: "day",
18
+ week: "week",
19
+ month: "month",
20
+ agenda: "agenda"
21
+ };
22
+ function deriveRange(date, view, agendaLength) {
23
+ if (view === "day") {
24
+ return { start: startOfDay(date), end: endOfDay(date) };
25
+ }
26
+ if (view === "week") {
27
+ return { start: startOfWeek(date, { locale: enUS }), end: endOfWeek(date, { locale: enUS }) };
28
+ }
29
+ if (view === "month") {
30
+ return { start: startOfMonth(date), end: endOfMonth(date) };
31
+ }
32
+ const length = Math.max(1, agendaLength);
33
+ return { start: startOfDay(date), end: endOfDay(addDays(date, length - 1)) };
34
+ }
35
+ function normalizeRange(nextRange, view, agendaLength) {
36
+ if (!nextRange) return null;
37
+ if (Array.isArray(nextRange)) {
38
+ if (nextRange.length === 0) return null;
39
+ if (view === "agenda") {
40
+ return { start: nextRange[0], end: nextRange[nextRange.length - 1] };
41
+ }
42
+ return deriveRange(nextRange[0], view, agendaLength);
43
+ }
44
+ if (nextRange.start && nextRange.end) return { start: nextRange.start, end: nextRange.end };
45
+ return deriveRange(/* @__PURE__ */ new Date(), view, agendaLength);
46
+ }
47
+ function getEventStyles(item) {
48
+ if (item.kind === "event") {
49
+ return { backgroundColor: "rgba(59, 130, 246, 0.15)", border: "1px solid rgba(59, 130, 246, 0.5)", color: "#1e3a8a" };
50
+ }
51
+ if (item.kind === "exception") {
52
+ return { backgroundColor: "rgba(148, 163, 184, 0.2)", border: "1px solid rgba(100, 116, 139, 0.6)", color: "#334155" };
53
+ }
54
+ return { backgroundColor: "rgba(16, 185, 129, 0.15)", border: "1px solid rgba(16, 185, 129, 0.5)", color: "#064e3b" };
55
+ }
56
+ function ScheduleView({
57
+ items,
58
+ view,
59
+ range,
60
+ timezone,
61
+ onRangeChange,
62
+ onViewChange,
63
+ onItemClick,
64
+ onSlotClick,
65
+ onTimezoneChange,
66
+ className
67
+ }) {
68
+ const agendaLength = React.useMemo(
69
+ () => Math.max(1, differenceInCalendarDays(range.end, range.start) + 1),
70
+ [range.end, range.start]
71
+ );
72
+ const currentView = VIEW_MAP[view];
73
+ const expandedItems = React.useMemo(() => expandRecurringItems(items, range), [items, range]);
74
+ const events = React.useMemo(
75
+ () => expandedItems.map((item) => ({
76
+ id: item.id,
77
+ title: item.title,
78
+ start: item.startsAt,
79
+ end: item.endsAt,
80
+ resource: item
81
+ })),
82
+ [expandedItems]
83
+ );
84
+ const handleNavigate = React.useCallback((date, nextView) => {
85
+ const resolvedView = nextView ?? currentView;
86
+ onRangeChange(deriveRange(date, resolvedView, agendaLength));
87
+ }, [agendaLength, currentView, onRangeChange]);
88
+ const handleRangeChange = React.useCallback((nextRange, nextView) => {
89
+ const resolvedView = nextView ?? currentView;
90
+ const normalized = normalizeRange(nextRange, resolvedView, agendaLength);
91
+ if (normalized) onRangeChange(normalized);
92
+ }, [agendaLength, currentView, onRangeChange]);
93
+ const handleViewChange = React.useCallback((nextView) => {
94
+ const resolved = nextView;
95
+ if (resolved !== view) {
96
+ onViewChange(resolved);
97
+ onRangeChange(deriveRange(/* @__PURE__ */ new Date(), resolved, agendaLength));
98
+ }
99
+ }, [agendaLength, onRangeChange, onViewChange, view]);
100
+ const rootClassName = ["schedule-view", className].filter(Boolean).join(" ");
101
+ return /* @__PURE__ */ jsxs("div", { className: rootClassName, children: [
102
+ /* @__PURE__ */ jsx(
103
+ ScheduleToolbar,
104
+ {
105
+ view,
106
+ range,
107
+ timezone,
108
+ onRangeChange,
109
+ onViewChange,
110
+ onTimezoneChange
111
+ }
112
+ ),
113
+ /* @__PURE__ */ jsx("div", { className: "schedule-calendar mt-4 rounded-xl border bg-card p-3", children: /* @__PURE__ */ jsx(
114
+ Calendar,
115
+ {
116
+ localizer,
117
+ culture: "en-US",
118
+ events,
119
+ view: currentView,
120
+ date: range.start,
121
+ toolbar: false,
122
+ selectable: Boolean(onSlotClick),
123
+ popup: true,
124
+ length: agendaLength,
125
+ onView: handleViewChange,
126
+ onNavigate: handleNavigate,
127
+ onRangeChange: handleRangeChange,
128
+ onSelectEvent: (event) => onItemClick?.(event.resource),
129
+ onSelectSlot: (slot) => {
130
+ if (!onSlotClick) return;
131
+ onSlotClick({ start: slot.start, end: slot.end });
132
+ },
133
+ eventPropGetter: (event) => ({
134
+ style: getEventStyles(event.resource)
135
+ }),
136
+ components: {
137
+ event: ({ event }) => {
138
+ const resource = event.resource;
139
+ const hasLink = Boolean(resource.linkLabel) && typeof onItemClick === "function";
140
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
141
+ /* @__PURE__ */ jsx("span", { className: "truncate text-xs font-medium", children: resource.title }),
142
+ hasLink ? /* @__PURE__ */ jsx(
143
+ "button",
144
+ {
145
+ type: "button",
146
+ className: "text-[11px] font-medium underline-offset-2 hover:underline",
147
+ onClick: (clickEvent) => {
148
+ clickEvent.stopPropagation();
149
+ onItemClick?.(resource);
150
+ },
151
+ children: resource.linkLabel
152
+ }
153
+ ) : null
154
+ ] });
155
+ }
156
+ },
157
+ style: { height: 640 }
158
+ }
159
+ ) })
160
+ ] });
161
+ }
162
+ export {
163
+ ScheduleView
164
+ };
165
+ //# sourceMappingURL=ScheduleView.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/schedule/ScheduleView.tsx"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Calendar, dateFnsLocalizer, type View, type SlotInfo } from 'react-big-calendar'\nimport { addDays, differenceInCalendarDays, endOfDay, endOfMonth, endOfWeek, format, getDay, parse, startOfDay, startOfMonth, startOfWeek } from 'date-fns'\nimport { enUS } from 'date-fns/locale/en-US'\nimport type { ScheduleItem, ScheduleRange, ScheduleSlot, ScheduleViewMode } from './types'\nimport { ScheduleToolbar } from './ScheduleToolbar'\nimport { expandRecurringItems } from './recurrence'\n\ntype CalendarEvent = {\n id: string\n title: string\n start: Date\n end: Date\n resource: ScheduleItem\n}\n\nconst localizer = dateFnsLocalizer({\n format,\n parse,\n startOfWeek,\n getDay,\n locales: { 'en-US': enUS },\n})\n\nconst VIEW_MAP: Record<ScheduleViewMode, View> = {\n day: 'day',\n week: 'week',\n month: 'month',\n agenda: 'agenda',\n}\n\nfunction deriveRange(date: Date, view: ScheduleViewMode, agendaLength: number): ScheduleRange {\n if (view === 'day') {\n return { start: startOfDay(date), end: endOfDay(date) }\n }\n if (view === 'week') {\n return { start: startOfWeek(date, { locale: enUS }), end: endOfWeek(date, { locale: enUS }) }\n }\n if (view === 'month') {\n return { start: startOfMonth(date), end: endOfMonth(date) }\n }\n const length = Math.max(1, agendaLength)\n return { start: startOfDay(date), end: endOfDay(addDays(date, length - 1)) }\n}\n\nfunction normalizeRange(\n nextRange: Date[] | { start: Date; end: Date } | null | undefined,\n view: ScheduleViewMode,\n agendaLength: number,\n): ScheduleRange | null {\n if (!nextRange) return null\n if (Array.isArray(nextRange)) {\n if (nextRange.length === 0) return null\n if (view === 'agenda') {\n return { start: nextRange[0], end: nextRange[nextRange.length - 1] }\n }\n return deriveRange(nextRange[0], view, agendaLength)\n }\n if (nextRange.start && nextRange.end) return { start: nextRange.start, end: nextRange.end }\n return deriveRange(new Date(), view, agendaLength)\n}\n\nfunction getEventStyles(item: ScheduleItem): React.CSSProperties {\n if (item.kind === 'event') {\n return { backgroundColor: 'rgba(59, 130, 246, 0.15)', border: '1px solid rgba(59, 130, 246, 0.5)', color: '#1e3a8a' }\n }\n if (item.kind === 'exception') {\n return { backgroundColor: 'rgba(148, 163, 184, 0.2)', border: '1px solid rgba(100, 116, 139, 0.6)', color: '#334155' }\n }\n return { backgroundColor: 'rgba(16, 185, 129, 0.15)', border: '1px solid rgba(16, 185, 129, 0.5)', color: '#064e3b' }\n}\n\nexport type ScheduleViewProps = {\n items: ScheduleItem[]\n view: ScheduleViewMode\n range: ScheduleRange\n timezone?: string\n onRangeChange: (range: ScheduleRange) => void\n onViewChange: (view: ScheduleViewMode) => void\n onItemClick?: (item: ScheduleItem) => void\n onSlotClick?: (slot: ScheduleSlot) => void\n onTimezoneChange?: (timezone: string) => void\n className?: string\n}\n\nexport function ScheduleView({\n items,\n view,\n range,\n timezone,\n onRangeChange,\n onViewChange,\n onItemClick,\n onSlotClick,\n onTimezoneChange,\n className,\n}: ScheduleViewProps) {\n const agendaLength = React.useMemo(\n () => Math.max(1, differenceInCalendarDays(range.end, range.start) + 1),\n [range.end, range.start],\n )\n const currentView = VIEW_MAP[view]\n const expandedItems = React.useMemo(() => expandRecurringItems(items, range), [items, range])\n const events = React.useMemo<CalendarEvent[]>(\n () => expandedItems.map((item) => ({\n id: item.id,\n title: item.title,\n start: item.startsAt,\n end: item.endsAt,\n resource: item,\n })),\n [expandedItems],\n )\n\n const handleNavigate = React.useCallback((date: Date, nextView?: View) => {\n const resolvedView = (nextView ?? currentView) as ScheduleViewMode\n onRangeChange(deriveRange(date, resolvedView, agendaLength))\n }, [agendaLength, currentView, onRangeChange])\n\n const handleRangeChange = React.useCallback((nextRange: Date[] | { start: Date; end: Date }, nextView?: View) => {\n const resolvedView = (nextView ?? currentView) as ScheduleViewMode\n const normalized = normalizeRange(nextRange, resolvedView, agendaLength)\n if (normalized) onRangeChange(normalized)\n }, [agendaLength, currentView, onRangeChange])\n\n const handleViewChange = React.useCallback((nextView: View) => {\n const resolved = nextView as ScheduleViewMode\n if (resolved !== view) {\n onViewChange(resolved)\n onRangeChange(deriveRange(new Date(), resolved, agendaLength))\n }\n }, [agendaLength, onRangeChange, onViewChange, view])\n\n const rootClassName = ['schedule-view', className].filter(Boolean).join(' ')\n\n return (\n <div className={rootClassName}>\n <ScheduleToolbar\n view={view}\n range={range}\n timezone={timezone}\n onRangeChange={onRangeChange}\n onViewChange={onViewChange}\n onTimezoneChange={onTimezoneChange}\n />\n <div className=\"schedule-calendar mt-4 rounded-xl border bg-card p-3\">\n <Calendar\n localizer={localizer}\n culture=\"en-US\"\n events={events}\n view={currentView}\n date={range.start}\n toolbar={false}\n selectable={Boolean(onSlotClick)}\n popup\n length={agendaLength}\n onView={handleViewChange}\n onNavigate={handleNavigate}\n onRangeChange={handleRangeChange}\n onSelectEvent={(event: CalendarEvent) => onItemClick?.(event.resource)}\n onSelectSlot={(slot: SlotInfo) => {\n if (!onSlotClick) return\n onSlotClick({ start: slot.start, end: slot.end })\n }}\n eventPropGetter={(event: CalendarEvent) => ({\n style: getEventStyles(event.resource),\n })}\n components={{\n event: ({ event }: { event: CalendarEvent }) => {\n const resource = event.resource\n const hasLink = Boolean(resource.linkLabel) && typeof onItemClick === 'function'\n return (\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate text-xs font-medium\">{resource.title}</span>\n {hasLink ? (\n <button\n type=\"button\"\n className=\"text-[11px] font-medium underline-offset-2 hover:underline\"\n onClick={(clickEvent) => {\n clickEvent.stopPropagation()\n onItemClick?.(resource)\n }}\n >\n {resource.linkLabel}\n </button>\n ) : null}\n </div>\n )\n },\n }}\n style={{ height: 640 }}\n />\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./ScheduleToolbar.js";
3
+ export * from "./ScheduleGrid.js";
4
+ export * from "./ScheduleAgenda.js";
5
+ export * from "./ScheduleView.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/schedule/index.ts"],
4
+ "sourcesContent": ["export * from './types'\nexport * from './ScheduleToolbar'\nexport * from './ScheduleGrid'\nexport * from './ScheduleAgenda'\nexport * from './ScheduleView'\n"],
5
+ "mappings": "AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;",
6
+ "names": []
7
+ }
@@ -0,0 +1,83 @@
1
+ const DAY_MS = 24 * 60 * 60 * 1e3;
2
+ function startOfDay(value) {
3
+ return new Date(value.getFullYear(), value.getMonth(), value.getDate());
4
+ }
5
+ function toDateKey(value) {
6
+ const year = value.getFullYear();
7
+ const month = String(value.getMonth() + 1).padStart(2, "0");
8
+ const day = String(value.getDate()).padStart(2, "0");
9
+ return `${year}-${month}-${day}`;
10
+ }
11
+ function parseRepeat(rrule) {
12
+ const freqMatch = rrule.match(/FREQ=([A-Z]+)/);
13
+ const countMatch = rrule.match(/COUNT=(\d+)/);
14
+ const freq = freqMatch?.[1];
15
+ const count = countMatch?.[1] ? Number(countMatch[1]) : null;
16
+ if (freq === "WEEKLY") return "weekly";
17
+ if (freq === "DAILY") {
18
+ if (count === 1) return "once";
19
+ return "daily";
20
+ }
21
+ return "once";
22
+ }
23
+ function parseRuleMetadata(item) {
24
+ if (!item.metadata || typeof item.metadata !== "object") return null;
25
+ const metadata = item.metadata;
26
+ if (!metadata.rule || typeof metadata.rule !== "object") return null;
27
+ const rule = metadata.rule;
28
+ if (typeof rule.rrule !== "string") return null;
29
+ return rule;
30
+ }
31
+ function normalizeExdates(exdates) {
32
+ if (!Array.isArray(exdates)) return /* @__PURE__ */ new Set();
33
+ const keys = exdates.map((value) => {
34
+ if (typeof value !== "string") return null;
35
+ const parsed = new Date(value);
36
+ if (Number.isNaN(parsed.getTime())) return null;
37
+ return toDateKey(parsed);
38
+ }).filter((value) => value !== null);
39
+ return new Set(keys);
40
+ }
41
+ function expandRecurringItems(items, range) {
42
+ const expanded = [];
43
+ const rangeStart = startOfDay(range.start);
44
+ const rangeEnd = startOfDay(range.end);
45
+ items.forEach((item) => {
46
+ const rule = parseRuleMetadata(item);
47
+ if (!rule) {
48
+ expanded.push(item);
49
+ return;
50
+ }
51
+ const repeat = parseRepeat(rule.rrule ?? "");
52
+ if (repeat === "once") {
53
+ expanded.push(item);
54
+ return;
55
+ }
56
+ const durationMs = Math.max(0, item.endsAt.getTime() - item.startsAt.getTime());
57
+ const startHours = item.startsAt.getHours();
58
+ const startMinutes = item.startsAt.getMinutes();
59
+ const startSeconds = item.startsAt.getSeconds();
60
+ const startMs = item.startsAt.getMilliseconds();
61
+ const itemStartDay = startOfDay(item.startsAt);
62
+ const exdates = normalizeExdates(rule.exdates);
63
+ for (let cursor = new Date(rangeStart); cursor <= rangeEnd; cursor = new Date(cursor.getTime() + DAY_MS)) {
64
+ if (cursor < itemStartDay) continue;
65
+ if (repeat === "weekly" && cursor.getDay() !== item.startsAt.getDay()) continue;
66
+ const dateKey = toDateKey(cursor);
67
+ if (exdates.has(dateKey)) continue;
68
+ const start = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate(), startHours, startMinutes, startSeconds, startMs);
69
+ const end = new Date(start.getTime() + durationMs);
70
+ expanded.push({
71
+ ...item,
72
+ id: `${item.id}:${dateKey}`,
73
+ startsAt: start,
74
+ endsAt: end
75
+ });
76
+ }
77
+ });
78
+ return expanded;
79
+ }
80
+ export {
81
+ expandRecurringItems
82
+ };
83
+ //# sourceMappingURL=recurrence.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/schedule/recurrence.ts"],
4
+ "sourcesContent": ["import type { ScheduleItem, ScheduleRange } from './types'\n\ntype RuleMetadata = {\n rrule?: string\n exdates?: unknown\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\nfunction startOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate())\n}\n\nfunction toDateKey(value: Date): string {\n const year = value.getFullYear()\n const month = String(value.getMonth() + 1).padStart(2, '0')\n const day = String(value.getDate()).padStart(2, '0')\n return `${year}-${month}-${day}`\n}\n\nfunction parseRepeat(rrule: string): 'once' | 'daily' | 'weekly' {\n const freqMatch = rrule.match(/FREQ=([A-Z]+)/)\n const countMatch = rrule.match(/COUNT=(\\d+)/)\n const freq = freqMatch?.[1]\n const count = countMatch?.[1] ? Number(countMatch[1]) : null\n if (freq === 'WEEKLY') return 'weekly'\n if (freq === 'DAILY') {\n if (count === 1) return 'once'\n return 'daily'\n }\n return 'once'\n}\n\nfunction parseRuleMetadata(item: ScheduleItem): RuleMetadata | null {\n if (!item.metadata || typeof item.metadata !== 'object') return null\n const metadata = item.metadata as { rule?: unknown }\n if (!metadata.rule || typeof metadata.rule !== 'object') return null\n const rule = metadata.rule as RuleMetadata\n if (typeof rule.rrule !== 'string') return null\n return rule\n}\n\nfunction normalizeExdates(exdates: unknown): Set<string> {\n if (!Array.isArray(exdates)) return new Set()\n const keys = exdates\n .map((value) => {\n if (typeof value !== 'string') return null\n const parsed = new Date(value)\n if (Number.isNaN(parsed.getTime())) return null\n return toDateKey(parsed)\n })\n .filter((value): value is string => value !== null)\n return new Set(keys)\n}\n\nexport function expandRecurringItems(items: ScheduleItem[], range: ScheduleRange): ScheduleItem[] {\n const expanded: ScheduleItem[] = []\n const rangeStart = startOfDay(range.start)\n const rangeEnd = startOfDay(range.end)\n\n items.forEach((item) => {\n const rule = parseRuleMetadata(item)\n if (!rule) {\n expanded.push(item)\n return\n }\n\n const repeat = parseRepeat(rule.rrule ?? '')\n if (repeat === 'once') {\n expanded.push(item)\n return\n }\n\n const durationMs = Math.max(0, item.endsAt.getTime() - item.startsAt.getTime())\n const startHours = item.startsAt.getHours()\n const startMinutes = item.startsAt.getMinutes()\n const startSeconds = item.startsAt.getSeconds()\n const startMs = item.startsAt.getMilliseconds()\n const itemStartDay = startOfDay(item.startsAt)\n const exdates = normalizeExdates(rule.exdates)\n\n for (let cursor = new Date(rangeStart); cursor <= rangeEnd; cursor = new Date(cursor.getTime() + DAY_MS)) {\n if (cursor < itemStartDay) continue\n if (repeat === 'weekly' && cursor.getDay() !== item.startsAt.getDay()) continue\n const dateKey = toDateKey(cursor)\n if (exdates.has(dateKey)) continue\n const start = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate(), startHours, startMinutes, startSeconds, startMs)\n const end = new Date(start.getTime() + durationMs)\n expanded.push({\n ...item,\n id: `${item.id}:${dateKey}`,\n startsAt: start,\n endsAt: end,\n })\n }\n })\n\n return expanded\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -0,0 +1,91 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Sparkles } from "lucide-react";
5
+ import { Button } from "../../primitives/button.js";
6
+ import { apiCall } from "../utils/apiCall.js";
7
+ import { flash } from "../FlashMessages.js";
8
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
9
+ const upgradeActionsEnabled = process.env.NEXT_PUBLIC_UPGRADE_ACTIONS_ENABLED === "true" || process.env.UPGRADE_ACTIONS_ENABLED === "true";
10
+ function UpgradeActionBanner() {
11
+ const t = useT();
12
+ const [action, setAction] = React.useState(null);
13
+ const [loading, setLoading] = React.useState(false);
14
+ const cancelledRef = React.useRef(false);
15
+ const loadNextAction = React.useCallback(async () => {
16
+ if (!upgradeActionsEnabled) return;
17
+ if (typeof window === "undefined" || typeof fetch === "undefined") return;
18
+ const call = await apiCall("/api/configs/upgrade-actions");
19
+ if (cancelledRef.current) return;
20
+ if (!call.ok || !call.result || !Array.isArray(call.result.actions) || !call.result.actions.length) {
21
+ setAction(null);
22
+ return;
23
+ }
24
+ setAction(call.result.actions[0]);
25
+ }, []);
26
+ React.useEffect(() => {
27
+ cancelledRef.current = false;
28
+ void loadNextAction();
29
+ return () => {
30
+ cancelledRef.current = true;
31
+ };
32
+ }, [loadNextAction]);
33
+ if (!upgradeActionsEnabled || !action) return null;
34
+ async function handleRun() {
35
+ if (!upgradeActionsEnabled || !action || loading) return;
36
+ setLoading(true);
37
+ try {
38
+ const response = await apiCall("/api/configs/upgrade-actions", {
39
+ method: "POST",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify({ actionId: action.id })
42
+ });
43
+ if (!response.ok) {
44
+ const baseError = response.result && typeof response.result.error === "string" && response.result.error || t("upgrades.runFailed", "We could not run this upgrade action.");
45
+ const detail = response.result && typeof response.result.details === "string" ? response.result.details : null;
46
+ const errorMessage = detail ? `${baseError} (${detail})` : baseError;
47
+ flash(errorMessage, "error");
48
+ return;
49
+ }
50
+ const message = response.result?.message || action.successMessage || t("upgrades.v034.success", "Example catalog products and categories installed.");
51
+ flash(message, "success");
52
+ setAction(null);
53
+ await loadNextAction();
54
+ } catch (error) {
55
+ const message = error instanceof Error ? error.message : t("upgrades.runFailed", "We could not run this upgrade action.");
56
+ flash(message, "error");
57
+ } finally {
58
+ setLoading(false);
59
+ }
60
+ }
61
+ const loadingLabel = action.loadingLabel || t("upgrades.v034.loading", "Installing\u2026");
62
+ const title = action.ctaLabel || action.message;
63
+ const description = action.message && action.message !== title ? action.message : null;
64
+ return /* @__PURE__ */ jsxs("div", { className: "mb-4 flex flex-col gap-3 rounded-md border border-amber-300 bg-amber-50 px-3 py-3 text-sm text-amber-900 md:flex-row md:items-center md:justify-between", children: [
65
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 text-sm", children: [
66
+ /* @__PURE__ */ jsx(Sparkles, { className: "mt-0.5 size-4 text-amber-700", "aria-hidden": "true" }),
67
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
68
+ /* @__PURE__ */ jsx("div", { className: "font-medium text-amber-950", children: title }),
69
+ description ? /* @__PURE__ */ jsx("div", { className: "text-xs text-amber-900/80", children: description }) : null,
70
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-amber-900/80", children: t("upgrades.versionLabel", { version: action.version }) })
71
+ ] })
72
+ ] }),
73
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2", children: /* @__PURE__ */ jsx(
74
+ Button,
75
+ {
76
+ variant: "outline",
77
+ size: "sm",
78
+ onClick: () => {
79
+ void handleRun();
80
+ },
81
+ disabled: loading,
82
+ className: "border-amber-300 text-amber-900 hover:bg-amber-100",
83
+ children: loading ? loadingLabel : action.ctaLabel
84
+ }
85
+ ) })
86
+ ] });
87
+ }
88
+ export {
89
+ UpgradeActionBanner
90
+ };
91
+ //# sourceMappingURL=UpgradeActionBanner.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/upgrades/UpgradeActionBanner.tsx"],
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Sparkles } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { apiCall } from '../utils/apiCall'\nimport { flash } from '../FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nconst upgradeActionsEnabled =\n process.env.NEXT_PUBLIC_UPGRADE_ACTIONS_ENABLED === 'true' ||\n process.env.UPGRADE_ACTIONS_ENABLED === 'true'\n\ntype UpgradeActionPayload = {\n id: string\n version: string\n message: string\n ctaLabel: string\n successMessage?: string\n loadingLabel?: string\n}\n\ntype UpgradeActionResponse = {\n version: string\n actions?: UpgradeActionPayload[]\n error?: string\n}\n\ntype RunActionResponse = {\n status?: 'completed' | 'already_completed'\n message?: string\n error?: string\n}\n\nexport function UpgradeActionBanner() {\n const t = useT()\n const [action, setAction] = React.useState<UpgradeActionPayload | null>(null)\n const [loading, setLoading] = React.useState(false)\n const cancelledRef = React.useRef(false)\n\n const loadNextAction = React.useCallback(async () => {\n if (!upgradeActionsEnabled) return\n if (typeof window === 'undefined' || typeof fetch === 'undefined') return\n const call = await apiCall<UpgradeActionResponse>('/api/configs/upgrade-actions')\n if (cancelledRef.current) return\n if (!call.ok || !call.result || !Array.isArray(call.result.actions) || !call.result.actions.length) {\n setAction(null)\n return\n }\n setAction(call.result.actions[0]!)\n }, [])\n\n React.useEffect(() => {\n cancelledRef.current = false\n void loadNextAction()\n return () => {\n cancelledRef.current = true\n }\n }, [loadNextAction])\n\n if (!upgradeActionsEnabled || !action) return null\n\n async function handleRun() {\n if (!upgradeActionsEnabled || !action || loading) return\n setLoading(true)\n try {\n const response = await apiCall<RunActionResponse>('/api/configs/upgrade-actions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ actionId: action.id }),\n })\n if (!response.ok) {\n const baseError =\n (response.result && typeof response.result.error === 'string' && response.result.error) ||\n t('upgrades.runFailed', 'We could not run this upgrade action.')\n const detail = response.result && typeof (response.result as any).details === 'string' ? (response.result as any).details : null\n const errorMessage = detail ? `${baseError} (${detail})` : baseError\n flash(errorMessage, 'error')\n return\n }\n const message =\n response.result?.message ||\n action.successMessage ||\n t('upgrades.v034.success', 'Example catalog products and categories installed.')\n flash(message, 'success')\n setAction(null)\n await loadNextAction()\n } catch (error) {\n const message = error instanceof Error ? error.message : t('upgrades.runFailed', 'We could not run this upgrade action.')\n flash(message, 'error')\n } finally {\n setLoading(false)\n }\n }\n\n const loadingLabel = action.loadingLabel || t('upgrades.v034.loading', 'Installing\u2026')\n const title = action.ctaLabel || action.message\n const description = action.message && action.message !== title ? action.message : null\n\n return (\n <div className=\"mb-4 flex flex-col gap-3 rounded-md border border-amber-300 bg-amber-50 px-3 py-3 text-sm text-amber-900 md:flex-row md:items-center md:justify-between\">\n <div className=\"flex items-start gap-2 text-sm\">\n <Sparkles className=\"mt-0.5 size-4 text-amber-700\" aria-hidden=\"true\" />\n <div className=\"flex flex-col gap-1\">\n <div className=\"font-medium text-amber-950\">\n {title}\n </div>\n {description ? (\n <div className=\"text-xs text-amber-900/80\">\n {description}\n </div>\n ) : null}\n <div className=\"text-xs text-amber-900/80\">{t('upgrades.versionLabel', { version: action.version })}</div>\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => { void handleRun() }}\n disabled={loading}\n className=\"border-amber-300 text-amber-900 hover:bg-amber-100\"\n >\n {loading ? loadingLabel : action.ctaLabel}\n </Button>\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,127 @@
1
+ "use client";
2
+ import { flash } from "../FlashMessages.js";
3
+ import { deserializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
4
+ import { pushOperation } from "../operations/store.js";
5
+ import { pushPartialIndexWarning } from "../indexes/store.js";
6
+ class UnauthorizedError extends Error {
7
+ constructor(message = "Unauthorized") {
8
+ super(message);
9
+ this.status = 401;
10
+ this.name = "UnauthorizedError";
11
+ }
12
+ }
13
+ function redirectToSessionRefresh() {
14
+ if (typeof window === "undefined") return;
15
+ const current = window.location.pathname + window.location.search;
16
+ if (window.location.pathname.startsWith("/api/auth")) return;
17
+ try {
18
+ flash("Session expired. Redirecting to sign in\u2026", "warning");
19
+ setTimeout(() => {
20
+ window.location.href = `/api/auth/session/refresh?redirect=${encodeURIComponent(current)}`;
21
+ }, 20);
22
+ } catch {
23
+ }
24
+ }
25
+ class ForbiddenError extends Error {
26
+ constructor(message = "Forbidden") {
27
+ super(message);
28
+ this.status = 403;
29
+ this.name = "ForbiddenError";
30
+ }
31
+ }
32
+ let DEFAULT_FORBIDDEN_ROLES = ["admin"];
33
+ function setAuthRedirectConfig(cfg) {
34
+ if (cfg?.defaultForbiddenRoles && cfg.defaultForbiddenRoles.length) {
35
+ DEFAULT_FORBIDDEN_ROLES = [...cfg.defaultForbiddenRoles].map(String);
36
+ }
37
+ }
38
+ function redirectToForbiddenLogin(options) {
39
+ if (typeof window === "undefined") return;
40
+ if (window.location.pathname.startsWith("/login")) return;
41
+ try {
42
+ const current = window.location.pathname + window.location.search;
43
+ const features = options?.requiredFeatures?.filter(Boolean) ?? [];
44
+ const roles = options?.requiredRoles?.filter(Boolean) ?? [];
45
+ const query = features.length ? `requireFeature=${encodeURIComponent(features.join(","))}` : `requireRole=${encodeURIComponent((roles.length ? roles : DEFAULT_FORBIDDEN_ROLES).map(String).join(","))}`;
46
+ const url = `/login?${query}&redirect=${encodeURIComponent(current)}`;
47
+ flash("Insufficient permissions. Redirecting to login\u2026", "warning");
48
+ setTimeout(() => {
49
+ window.location.href = url;
50
+ }, 60);
51
+ } catch {
52
+ }
53
+ }
54
+ async function apiFetch(input, init) {
55
+ const baseFetch = typeof window !== "undefined" && window.__omOriginalFetch ? window.__omOriginalFetch : fetch;
56
+ const res = await baseFetch(input, init);
57
+ const onLoginPage = typeof window !== "undefined" && window.location.pathname.startsWith("/login");
58
+ if (res.status === 401) {
59
+ if (!onLoginPage) {
60
+ redirectToSessionRefresh();
61
+ throw new UnauthorizedError(await res.text().catch(() => "Unauthorized"));
62
+ }
63
+ return res;
64
+ }
65
+ if (res.status === 403) {
66
+ let roles = null;
67
+ let features = null;
68
+ let payload = null;
69
+ try {
70
+ const clone = res.clone();
71
+ const data = await clone.json();
72
+ if (Array.isArray(data?.requiredRoles)) roles = data.requiredRoles.map((r) => String(r));
73
+ if (Array.isArray(data?.requiredFeatures)) features = data.requiredFeatures.map((f) => String(f));
74
+ if (data && typeof data === "object") payload = data;
75
+ } catch {
76
+ }
77
+ if (!onLoginPage) {
78
+ const target = typeof input === "string" ? input : input instanceof URL ? input.toString() : typeof Request !== "undefined" && input instanceof Request ? input.url : "unknown";
79
+ try {
80
+ console.warn("[apiFetch] Forbidden response", {
81
+ url: target,
82
+ status: res.status,
83
+ requiredRoles: roles,
84
+ requiredFeatures: features,
85
+ details: payload
86
+ });
87
+ } catch {
88
+ }
89
+ redirectToForbiddenLogin({ requiredRoles: roles, requiredFeatures: features });
90
+ const msg = await res.text().catch(() => "Forbidden");
91
+ throw new ForbiddenError(msg);
92
+ }
93
+ }
94
+ try {
95
+ const header = res.headers.get("x-om-operation");
96
+ const metadata = deserializeOperationMetadata(header);
97
+ if (metadata) pushOperation(metadata);
98
+ } catch {
99
+ }
100
+ try {
101
+ const warningRaw = res.headers.get("x-om-partial-index");
102
+ if (warningRaw) {
103
+ const parsed = JSON.parse(warningRaw);
104
+ if (parsed && typeof parsed === "object" && parsed.type === "partial_index") {
105
+ const entity = typeof parsed.entity === "string" ? parsed.entity : String(parsed.entity ?? "");
106
+ if (entity) {
107
+ const baseCount = typeof parsed.baseCount === "number" ? parsed.baseCount : null;
108
+ const indexedCount = typeof parsed.indexedCount === "number" ? parsed.indexedCount : null;
109
+ const scope = parsed.scope === "global" ? "global" : "scoped";
110
+ const entityLabel = typeof parsed.entityLabel === "string" && parsed.entityLabel.trim() ? parsed.entityLabel.trim() : entity;
111
+ pushPartialIndexWarning({ entity, entityLabel, baseCount, indexedCount, scope });
112
+ }
113
+ }
114
+ }
115
+ } catch {
116
+ }
117
+ return res;
118
+ }
119
+ export {
120
+ ForbiddenError,
121
+ UnauthorizedError,
122
+ apiFetch,
123
+ redirectToForbiddenLogin,
124
+ redirectToSessionRefresh,
125
+ setAuthRedirectConfig
126
+ };
127
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/utils/api.ts"],
4
+ "sourcesContent": ["\"use client\"\n// Simple fetch wrapper that redirects to session refresh on 401 (Unauthorized)\n// Used across UI data utilities to avoid duplication.\nimport { flash } from '../FlashMessages'\nimport { deserializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { pushOperation } from '../operations/store'\nimport { pushPartialIndexWarning } from '../indexes/store'\nexport class UnauthorizedError extends Error {\n readonly status = 401\n constructor(message = 'Unauthorized') {\n super(message)\n this.name = 'UnauthorizedError'\n }\n}\n\nexport function redirectToSessionRefresh() {\n if (typeof window === 'undefined') return\n const current = window.location.pathname + window.location.search\n // Avoid redirect loops if already on an auth/session route\n if (window.location.pathname.startsWith('/api/auth')) return\n try {\n flash('Session expired. Redirecting to sign in\u2026', 'warning')\n setTimeout(() => {\n window.location.href = `/api/auth/session/refresh?redirect=${encodeURIComponent(current)}`\n }, 20)\n } catch {\n // no-op\n }\n}\n\nexport class ForbiddenError extends Error {\n readonly status = 403\n constructor(message = 'Forbidden') {\n super(message)\n this.name = 'ForbiddenError'\n }\n}\n\nlet DEFAULT_FORBIDDEN_ROLES: string[] = ['admin']\n\nexport function setAuthRedirectConfig(cfg: { defaultForbiddenRoles?: readonly string[] }) {\n if (cfg?.defaultForbiddenRoles && cfg.defaultForbiddenRoles.length) {\n DEFAULT_FORBIDDEN_ROLES = [...cfg.defaultForbiddenRoles].map(String)\n }\n}\n\nexport function redirectToForbiddenLogin(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }) {\n if (typeof window === 'undefined') return\n // We don't know required roles from the API response; use a generic hint.\n if (window.location.pathname.startsWith('/login')) return\n try {\n const current = window.location.pathname + window.location.search\n const features = options?.requiredFeatures?.filter(Boolean) ?? []\n const roles = options?.requiredRoles?.filter(Boolean) ?? []\n const query = features.length\n ? `requireFeature=${encodeURIComponent(features.join(','))}`\n : `requireRole=${encodeURIComponent((roles.length ? roles : DEFAULT_FORBIDDEN_ROLES).map(String).join(','))}`\n const url = `/login?${query}&redirect=${encodeURIComponent(current)}`\n flash('Insufficient permissions. Redirecting to login\u2026', 'warning')\n setTimeout(() => { window.location.href = url }, 60)\n } catch {\n // no-op\n }\n}\n\nexport async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n type FetchType = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;\n const baseFetch: FetchType = (typeof window !== 'undefined' && (window as any).__omOriginalFetch)\n ? ((window as any).__omOriginalFetch as FetchType)\n : fetch;\n const res = await baseFetch(input, init);\n const onLoginPage = typeof window !== 'undefined' && window.location.pathname.startsWith('/login')\n if (res.status === 401) {\n // Trigger same redirect flow as protected pages\n if (!onLoginPage) {\n redirectToSessionRefresh()\n // Throw a typed error for callers that might still handle it\n throw new UnauthorizedError(await res.text().catch(() => 'Unauthorized'))\n }\n return res\n }\n if (res.status === 403) {\n // Try to read requiredRoles from JSON body; ignore if not JSON\n let roles: string[] | null = null\n let features: string[] | null = null\n let payload: unknown = null\n try {\n const clone = res.clone()\n const data = await clone.json()\n if (Array.isArray(data?.requiredRoles)) roles = data.requiredRoles.map((r: any) => String(r))\n if (Array.isArray(data?.requiredFeatures)) features = data.requiredFeatures.map((f: any) => String(f))\n if (data && typeof data === 'object') payload = data\n } catch {}\n // Only redirect if not already on login page\n if (!onLoginPage) {\n const target =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : (typeof Request !== 'undefined' && input instanceof Request)\n ? input.url\n : 'unknown'\n try {\n // eslint-disable-next-line no-console\n console.warn('[apiFetch] Forbidden response', {\n url: target,\n status: res.status,\n requiredRoles: roles,\n requiredFeatures: features,\n details: payload,\n })\n } catch {}\n redirectToForbiddenLogin({ requiredRoles: roles, requiredFeatures: features })\n const msg = await res.text().catch(() => 'Forbidden')\n throw new ForbiddenError(msg)\n }\n // If already on login, just return the response for the caller to handle\n }\n try {\n const header = res.headers.get('x-om-operation')\n const metadata = deserializeOperationMetadata(header)\n if (metadata) pushOperation(metadata)\n } catch {\n // ignore malformed headers\n }\n try {\n const warningRaw = res.headers.get('x-om-partial-index')\n if (warningRaw) {\n const parsed = JSON.parse(warningRaw) as Record<string, unknown>\n if (parsed && typeof parsed === 'object' && parsed.type === 'partial_index') {\n const entity = typeof parsed.entity === 'string' ? parsed.entity : String(parsed.entity ?? '')\n if (entity) {\n const baseCount = typeof parsed.baseCount === 'number' ? parsed.baseCount : null\n const indexedCount = typeof parsed.indexedCount === 'number' ? parsed.indexedCount : null\n const scope = parsed.scope === 'global' ? 'global' : 'scoped'\n const entityLabel =\n typeof parsed.entityLabel === 'string' && parsed.entityLabel.trim()\n ? parsed.entityLabel.trim()\n : entity\n pushPartialIndexWarning({ entity, entityLabel, baseCount, indexedCount, scope })\n }\n }\n }\n } catch {\n // ignore malformed headers\n }\n return res\n}\n"],
5
+ "mappings": ";AAGA,SAAS,aAAa;AACtB,SAAS,oCAAoC;AAC7C,SAAS,qBAAqB;AAC9B,SAAS,+BAA+B;AACjC,MAAM,0BAA0B,MAAM;AAAA,EAE3C,YAAY,UAAU,gBAAgB;AACpC,UAAM,OAAO;AAFf,SAAS,SAAS;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,2BAA2B;AACzC,MAAI,OAAO,WAAW,YAAa;AACnC,QAAM,UAAU,OAAO,SAAS,WAAW,OAAO,SAAS;AAE3D,MAAI,OAAO,SAAS,SAAS,WAAW,WAAW,EAAG;AACtD,MAAI;AACF,UAAM,iDAA4C,SAAS;AAC3D,eAAW,MAAM;AACf,aAAO,SAAS,OAAO,sCAAsC,mBAAmB,OAAO,CAAC;AAAA,IAC1F,GAAG,EAAE;AAAA,EACP,QAAQ;AAAA,EAER;AACF;AAEO,MAAM,uBAAuB,MAAM;AAAA,EAExC,YAAY,UAAU,aAAa;AACjC,UAAM,OAAO;AAFf,SAAS,SAAS;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAEA,IAAI,0BAAoC,CAAC,OAAO;AAEzC,SAAS,sBAAsB,KAAoD;AACxF,MAAI,KAAK,yBAAyB,IAAI,sBAAsB,QAAQ;AAClE,8BAA0B,CAAC,GAAG,IAAI,qBAAqB,EAAE,IAAI,MAAM;AAAA,EACrE;AACF;AAEO,SAAS,yBAAyB,SAAmF;AAC1H,MAAI,OAAO,WAAW,YAAa;AAEnC,MAAI,OAAO,SAAS,SAAS,WAAW,QAAQ,EAAG;AACnD,MAAI;AACF,UAAM,UAAU,OAAO,SAAS,WAAW,OAAO,SAAS;AAC3D,UAAM,WAAW,SAAS,kBAAkB,OAAO,OAAO,KAAK,CAAC;AAChE,UAAM,QAAQ,SAAS,eAAe,OAAO,OAAO,KAAK,CAAC;AAC1D,UAAM,QAAQ,SAAS,SACnB,kBAAkB,mBAAmB,SAAS,KAAK,GAAG,CAAC,CAAC,KACxD,eAAe,oBAAoB,MAAM,SAAS,QAAQ,yBAAyB,IAAI,MAAM,EAAE,KAAK,GAAG,CAAC,CAAC;AAC7G,UAAM,MAAM,UAAU,KAAK,aAAa,mBAAmB,OAAO,CAAC;AACnE,UAAM,wDAAmD,SAAS;AAClE,eAAW,MAAM;AAAE,aAAO,SAAS,OAAO;AAAA,IAAI,GAAG,EAAE;AAAA,EACrD,QAAQ;AAAA,EAER;AACF;AAEA,eAAsB,SAAS,OAA0B,MAAuC;AAE9F,QAAM,YAAwB,OAAO,WAAW,eAAgB,OAAe,oBACzE,OAAe,oBACjB;AACJ,QAAM,MAAM,MAAM,UAAU,OAAO,IAAI;AACvC,QAAM,cAAc,OAAO,WAAW,eAAe,OAAO,SAAS,SAAS,WAAW,QAAQ;AACjG,MAAI,IAAI,WAAW,KAAK;AAEtB,QAAI,CAAC,aAAa;AAChB,+BAAyB;AAEzB,YAAM,IAAI,kBAAkB,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,cAAc,CAAC;AAAA,IAC1E;AACA,WAAO;AAAA,EACT;AACA,MAAI,IAAI,WAAW,KAAK;AAEtB,QAAI,QAAyB;AAC7B,QAAI,WAA4B;AAChC,QAAI,UAAmB;AACvB,QAAI;AACF,YAAM,QAAQ,IAAI,MAAM;AACxB,YAAM,OAAO,MAAM,MAAM,KAAK;AAC9B,UAAI,MAAM,QAAQ,MAAM,aAAa,EAAG,SAAQ,KAAK,cAAc,IAAI,CAAC,MAAW,OAAO,CAAC,CAAC;AAC5F,UAAI,MAAM,QAAQ,MAAM,gBAAgB,EAAG,YAAW,KAAK,iBAAiB,IAAI,CAAC,MAAW,OAAO,CAAC,CAAC;AACrG,UAAI,QAAQ,OAAO,SAAS,SAAU,WAAU;AAAA,IAClD,QAAQ;AAAA,IAAC;AAET,QAAI,CAAC,aAAa;AAChB,YAAM,SACJ,OAAO,UAAU,WACb,QACA,iBAAiB,MACf,MAAM,SAAS,IACd,OAAO,YAAY,eAAe,iBAAiB,UAClD,MAAM,MACN;AACV,UAAI;AAEF,gBAAQ,KAAK,iCAAiC;AAAA,UAC5C,KAAK;AAAA,UACL,QAAQ,IAAI;AAAA,UACZ,eAAe;AAAA,UACf,kBAAkB;AAAA,UAClB,SAAS;AAAA,QACX,CAAC;AAAA,MACH,QAAQ;AAAA,MAAC;AACT,+BAAyB,EAAE,eAAe,OAAO,kBAAkB,SAAS,CAAC;AAC7E,YAAM,MAAM,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,WAAW;AACpD,YAAM,IAAI,eAAe,GAAG;AAAA,IAC9B;AAAA,EAEF;AACA,MAAI;AACF,UAAM,SAAS,IAAI,QAAQ,IAAI,gBAAgB;AAC/C,UAAM,WAAW,6BAA6B,MAAM;AACpD,QAAI,SAAU,eAAc,QAAQ;AAAA,EACtC,QAAQ;AAAA,EAER;AACA,MAAI;AACF,UAAM,aAAa,IAAI,QAAQ,IAAI,oBAAoB;AACvD,QAAI,YAAY;AACd,YAAM,SAAS,KAAK,MAAM,UAAU;AACpC,UAAI,UAAU,OAAO,WAAW,YAAY,OAAO,SAAS,iBAAiB;AAC3E,cAAM,SAAS,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS,OAAO,OAAO,UAAU,EAAE;AAC7F,YAAI,QAAQ;AACV,gBAAM,YAAY,OAAO,OAAO,cAAc,WAAW,OAAO,YAAY;AAC5E,gBAAM,eAAe,OAAO,OAAO,iBAAiB,WAAW,OAAO,eAAe;AACrF,gBAAM,QAAQ,OAAO,UAAU,WAAW,WAAW;AACrD,gBAAM,cACJ,OAAO,OAAO,gBAAgB,YAAY,OAAO,YAAY,KAAK,IAC9D,OAAO,YAAY,KAAK,IACxB;AACN,kCAAwB,EAAE,QAAQ,aAAa,WAAW,cAAc,MAAM,CAAC;AAAA,QACjF;AAAA,MACF;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,48 @@
1
+ "use client";
2
+ import { apiFetch } from "./api.js";
3
+ import { raiseCrudError, readJsonSafe } from "./serverErrors.js";
4
+ async function apiCall(input, init, options) {
5
+ const response = await apiFetch(input, init);
6
+ const parser = options?.parse;
7
+ const fallback = options?.fallback ?? null;
8
+ let result = null;
9
+ const rawCacheStatus = response.headers?.get?.("x-om-cache") ?? response.headers?.get?.("x-cache-status") ?? null;
10
+ const cacheStatus = rawCacheStatus === "hit" || rawCacheStatus === "miss" ? rawCacheStatus : null;
11
+ try {
12
+ const source = typeof response.clone === "function" ? response.clone() : response;
13
+ if (parser) result = await parser(source);
14
+ else result = await readJsonSafe(source, fallback);
15
+ } catch {
16
+ result = fallback;
17
+ }
18
+ return {
19
+ ok: response.ok,
20
+ status: response.status,
21
+ result,
22
+ response,
23
+ cacheStatus
24
+ };
25
+ }
26
+ async function apiCallOrThrow(input, init, options) {
27
+ const { errorMessage, ...callOptions } = options ?? {};
28
+ const call = await apiCall(input, init, callOptions);
29
+ if (!call.ok) {
30
+ await raiseCrudError(call.response, errorMessage);
31
+ }
32
+ return call;
33
+ }
34
+ async function readApiResultOrThrow(input, init, options) {
35
+ const { allowNullResult = false, emptyResultMessage, ...callOptions } = options ?? {};
36
+ const call = await apiCallOrThrow(input, init, callOptions);
37
+ if (call.result == null && !allowNullResult) {
38
+ const fallback = emptyResultMessage ?? callOptions.errorMessage ?? `Missing response payload (${call.status})`;
39
+ throw new Error(fallback);
40
+ }
41
+ return call.result;
42
+ }
43
+ export {
44
+ apiCall,
45
+ apiCallOrThrow,
46
+ readApiResultOrThrow
47
+ };
48
+ //# sourceMappingURL=apiCall.js.map