portal-mojo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (369) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +55 -0
  3. package/docs/DetailView.md +173 -0
  4. package/docs/ModelTable.md +128 -0
  5. package/docs/README.md +98 -0
  6. package/docs/admin-assistant.md +72 -0
  7. package/docs/admin-bouncer.md +120 -0
  8. package/docs/admin-cloudwatch.md +38 -0
  9. package/docs/admin-credentials.md +116 -0
  10. package/docs/admin-dashboard.md +15 -0
  11. package/docs/admin-devices-geoip.md +284 -0
  12. package/docs/admin-dns-certificates.md +122 -0
  13. package/docs/admin-dns-records.md +65 -0
  14. package/docs/admin-dns-registrar.md +33 -0
  15. package/docs/admin-dns.md +80 -0
  16. package/docs/admin-identity-users.md +103 -0
  17. package/docs/admin-incidents.md +71 -0
  18. package/docs/admin-jobs.md +426 -0
  19. package/docs/admin-members.md +112 -0
  20. package/docs/admin-messaging.md +56 -0
  21. package/docs/admin-metrics-explorer.md +63 -0
  22. package/docs/admin-monitoring.md +99 -0
  23. package/docs/admin-network-security.md +393 -0
  24. package/docs/admin-phonehub.md +27 -0
  25. package/docs/admin-push.md +36 -0
  26. package/docs/admin-rules.md +87 -0
  27. package/docs/admin-security-tickets.md +40 -0
  28. package/docs/admin-settings.md +78 -0
  29. package/docs/admin-shortlinks.md +32 -0
  30. package/docs/admin-storage.md +141 -0
  31. package/docs/auth-pages.md +212 -0
  32. package/docs/auth.md +58 -0
  33. package/docs/calendar.md +122 -0
  34. package/docs/charts.md +303 -0
  35. package/docs/client.md +206 -0
  36. package/docs/collection-multiselect.md +117 -0
  37. package/docs/collection-select.md +132 -0
  38. package/docs/combobox.md +105 -0
  39. package/docs/dataview.md +226 -0
  40. package/docs/datepicker.md +151 -0
  41. package/docs/daterange.md +170 -0
  42. package/docs/datetimepicker.md +249 -0
  43. package/docs/defineModel.md +72 -0
  44. package/docs/detail-primitives.md +174 -0
  45. package/docs/feedback.md +85 -0
  46. package/docs/file-upload-ux.md +146 -0
  47. package/docs/forms.md +435 -0
  48. package/docs/grouping-and-fmt.md +98 -0
  49. package/docs/idioms.md +112 -0
  50. package/docs/image-editor.md +101 -0
  51. package/docs/loading.md +242 -0
  52. package/docs/location-address.md +76 -0
  53. package/docs/markdown.md +203 -0
  54. package/docs/menus-and-access.md +89 -0
  55. package/docs/multiselect-dropdown.md +153 -0
  56. package/docs/params.md +73 -0
  57. package/docs/popover.md +130 -0
  58. package/docs/realtime.md +61 -0
  59. package/docs/record-feed.md +238 -0
  60. package/docs/right-panel.md +42 -0
  61. package/docs/tabs-and-form-wizard.md +60 -0
  62. package/docs/taginput.md +120 -0
  63. package/docs/timepicker.md +173 -0
  64. package/docs/timezone-select.md +147 -0
  65. package/docs/worldmap.md +302 -0
  66. package/package.json +55 -0
  67. package/src/admin/assistant/AssistantFeed.tsx +29 -0
  68. package/src/admin/assistant/AssistantPanel.tsx +65 -0
  69. package/src/admin/assistant/api.ts +17 -0
  70. package/src/admin/assistant/data.ts +63 -0
  71. package/src/admin/assistant/index.ts +21 -0
  72. package/src/admin/assistant/launchers.tsx +33 -0
  73. package/src/admin/assistant/pages.tsx +78 -0
  74. package/src/admin/assistant/streaming.ts +238 -0
  75. package/src/admin/assistant/types.ts +30 -0
  76. package/src/admin/bouncer/devices.tsx +230 -0
  77. package/src/admin/bouncer/index.ts +34 -0
  78. package/src/admin/bouncer/models.ts +199 -0
  79. package/src/admin/bouncer/signals.tsx +182 -0
  80. package/src/admin/bouncer/signatures.tsx +220 -0
  81. package/src/admin/cloudwatch/CloudWatchChart.tsx +61 -0
  82. package/src/admin/cloudwatch/CloudWatchDashboardPage.tsx +52 -0
  83. package/src/admin/cloudwatch/CloudWatchResourceDetail.tsx +33 -0
  84. package/src/admin/cloudwatch/data.ts +163 -0
  85. package/src/admin/cloudwatch/index.ts +16 -0
  86. package/src/admin/credentials/group-api-keys.tsx +433 -0
  87. package/src/admin/credentials/index.ts +4 -0
  88. package/src/admin/credentials/models.ts +244 -0
  89. package/src/admin/credentials/secret-dialog.tsx +77 -0
  90. package/src/admin/credentials/webhook-subscriptions.tsx +423 -0
  91. package/src/admin/dashboard/AdminDashboardPage.tsx +101 -0
  92. package/src/admin/dashboard/data.ts +60 -0
  93. package/src/admin/dashboard/index.ts +2 -0
  94. package/src/admin/dns/CertificateDetail.tsx +115 -0
  95. package/src/admin/dns/CertificateLifecyclePoller.tsx +141 -0
  96. package/src/admin/dns/CertificateRequestDialog.tsx +103 -0
  97. package/src/admin/dns/CertificatesPage.tsx +85 -0
  98. package/src/admin/dns/DnsRecordEditor.tsx +110 -0
  99. package/src/admin/dns/DnsRecordsPage.tsx +36 -0
  100. package/src/admin/dns/DnsRecordsPanel.tsx +71 -0
  101. package/src/admin/dns/DomainCertificatesSection.tsx +40 -0
  102. package/src/admin/dns/DomainDetail.tsx +41 -0
  103. package/src/admin/dns/DomainOnboardingDialog.tsx +27 -0
  104. package/src/admin/dns/DomainPurchaseDetail.tsx +11 -0
  105. package/src/admin/dns/DomainPurchaseWizard.tsx +112 -0
  106. package/src/admin/dns/DomainPurchasesPage.tsx +16 -0
  107. package/src/admin/dns/DomainsPage.tsx +27 -0
  108. package/src/admin/dns/ProviderCredentialsPage.tsx +304 -0
  109. package/src/admin/dns/RegistrantContactPage.tsx +42 -0
  110. package/src/admin/dns/api.ts +515 -0
  111. package/src/admin/dns/certificate-data.ts +221 -0
  112. package/src/admin/dns/data.ts +32 -0
  113. package/src/admin/dns/dns-data.ts +266 -0
  114. package/src/admin/dns/dns-integration.ts +24 -0
  115. package/src/admin/dns/domain-links.ts +37 -0
  116. package/src/admin/dns/index.ts +86 -0
  117. package/src/admin/dns/models.ts +418 -0
  118. package/src/admin/dns/purchase-data.ts +67 -0
  119. package/src/admin/dns/registrant-data.ts +52 -0
  120. package/src/admin/identity/index.ts +5 -0
  121. package/src/admin/identity/members/GroupMembersPanel.tsx +146 -0
  122. package/src/admin/identity/members/MemberDetail.tsx +226 -0
  123. package/src/admin/identity/members/MembersPage.tsx +144 -0
  124. package/src/admin/identity/members/index.ts +24 -0
  125. package/src/admin/identity/members/member-flows.tsx +132 -0
  126. package/src/admin/identity/members/models.ts +204 -0
  127. package/src/admin/identity/users/UserDetail.tsx +257 -0
  128. package/src/admin/identity/users/UsersPage.tsx +163 -0
  129. package/src/admin/identity/users/index.ts +27 -0
  130. package/src/admin/identity/users/models.ts +232 -0
  131. package/src/admin/identity/users/sections/ApiKeysSection.tsx +119 -0
  132. package/src/admin/identity/users/sections/AuditSection.tsx +190 -0
  133. package/src/admin/identity/users/sections/DevicesSection.tsx +140 -0
  134. package/src/admin/identity/users/sections/GroupsSection.tsx +64 -0
  135. package/src/admin/identity/users/sections/LoginsSection.tsx +106 -0
  136. package/src/admin/identity/users/sections/MetadataPanel.tsx +36 -0
  137. package/src/admin/identity/users/sections/NotificationsSection.tsx +117 -0
  138. package/src/admin/identity/users/sections/OAuthSection.tsx +72 -0
  139. package/src/admin/identity/users/sections/OverviewSection.tsx +109 -0
  140. package/src/admin/identity/users/sections/PermissionsSection.tsx +35 -0
  141. package/src/admin/identity/users/sections/PersonalSection.tsx +73 -0
  142. package/src/admin/identity/users/sections/ProfileSection.tsx +175 -0
  143. package/src/admin/identity/users/sections/SecuritySection.tsx +123 -0
  144. package/src/admin/identity/users/sections/actions.tsx +752 -0
  145. package/src/admin/identity/users/sections/permission-catalog.ts +138 -0
  146. package/src/admin/identity/users/sections/queries.ts +74 -0
  147. package/src/admin/identity/users/sections/shared.tsx +299 -0
  148. package/src/admin/incidents/EventDetail.tsx +43 -0
  149. package/src/admin/incidents/EventsPage.tsx +52 -0
  150. package/src/admin/incidents/IncidentDetail.tsx +68 -0
  151. package/src/admin/incidents/IncidentsPage.tsx +102 -0
  152. package/src/admin/incidents/forensics.tsx +73 -0
  153. package/src/admin/incidents/index.ts +16 -0
  154. package/src/admin/incidents/models.ts +113 -0
  155. package/src/admin/incidents/sanitize.ts +71 -0
  156. package/src/admin/index.ts +383 -0
  157. package/src/admin/jobs/JobDashboardPage.tsx +111 -0
  158. package/src/admin/jobs/JobDetail.tsx +721 -0
  159. package/src/admin/jobs/JobRunnersPage.tsx +216 -0
  160. package/src/admin/jobs/JobsTablePage.tsx +122 -0
  161. package/src/admin/jobs/RunnerDetail.tsx +576 -0
  162. package/src/admin/jobs/ScheduledTaskDetail.tsx +421 -0
  163. package/src/admin/jobs/ScheduledTasksPage.tsx +167 -0
  164. package/src/admin/jobs/columns.tsx +297 -0
  165. package/src/admin/jobs/control.ts +488 -0
  166. package/src/admin/jobs/index.ts +85 -0
  167. package/src/admin/jobs/models.ts +599 -0
  168. package/src/admin/jobs/queries.ts +272 -0
  169. package/src/admin/jobs/scheduled-task-form.tsx +493 -0
  170. package/src/admin/jobs/sections/JobChannelsPanel.tsx +142 -0
  171. package/src/admin/jobs/sections/JobOperationsSection.tsx +423 -0
  172. package/src/admin/jobs/sections/JobRunnersStrip.tsx +82 -0
  173. package/src/admin/jobs/sections/JobThroughputSection.tsx +118 -0
  174. package/src/admin/messaging/EmailDomainWizard.tsx +24 -0
  175. package/src/admin/messaging/EmailDomainsPage.tsx +17 -0
  176. package/src/admin/messaging/EmailTemplatesPage.tsx +13 -0
  177. package/src/admin/messaging/MailboxesPage.tsx +15 -0
  178. package/src/admin/messaging/PublicMessagesPage.tsx +13 -0
  179. package/src/admin/messaging/SandboxedEmailPreview.tsx +23 -0
  180. package/src/admin/messaging/SentMessagesPage.tsx +11 -0
  181. package/src/admin/messaging/api.ts +17 -0
  182. package/src/admin/messaging/data.ts +10 -0
  183. package/src/admin/messaging/index.ts +23 -0
  184. package/src/admin/messaging/models.ts +76 -0
  185. package/src/admin/messaging/push/PushPage.tsx +44 -0
  186. package/src/admin/messaging/push/api.ts +17 -0
  187. package/src/admin/messaging/push/index.ts +9 -0
  188. package/src/admin/messaging/push/models.ts +43 -0
  189. package/src/admin/messaging/sanitize.ts +44 -0
  190. package/src/admin/monitoring/LogInspector.tsx +160 -0
  191. package/src/admin/monitoring/LogsPage.tsx +135 -0
  192. package/src/admin/monitoring/MetricsExplorerPage.tsx +360 -0
  193. package/src/admin/monitoring/MetricsPermissionsPage.tsx +166 -0
  194. package/src/admin/monitoring/MetricsSourcePicker.tsx +136 -0
  195. package/src/admin/monitoring/index.ts +55 -0
  196. package/src/admin/monitoring/metrics-explorer-client.ts +229 -0
  197. package/src/admin/monitoring/metrics-explorer-data.ts +158 -0
  198. package/src/admin/monitoring/models.ts +155 -0
  199. package/src/admin/network/BlockedIPsPage.tsx +194 -0
  200. package/src/admin/network/BlockedIpDetail.tsx +262 -0
  201. package/src/admin/network/FirewallLogPage.tsx +115 -0
  202. package/src/admin/network/IPSetDetail.tsx +280 -0
  203. package/src/admin/network/IPSetEditor.tsx +191 -0
  204. package/src/admin/network/IPSetsPage.tsx +177 -0
  205. package/src/admin/network/geofence/BlocksTab.tsx +289 -0
  206. package/src/admin/network/geofence/ExemptionsTab.tsx +349 -0
  207. package/src/admin/network/geofence/GeofencingPage.tsx +119 -0
  208. package/src/admin/network/geofence/PostureHeader.tsx +184 -0
  209. package/src/admin/network/geofence/RuleEditor.tsx +176 -0
  210. package/src/admin/network/geofence/RulesTab.tsx +254 -0
  211. package/src/admin/network/geofence/SimulatorTab.tsx +281 -0
  212. package/src/admin/network/geofence/geofence-data.ts +674 -0
  213. package/src/admin/network/index.ts +68 -0
  214. package/src/admin/network/models.ts +607 -0
  215. package/src/admin/phonehub/PhoneHubPage.tsx +57 -0
  216. package/src/admin/phonehub/api.ts +47 -0
  217. package/src/admin/phonehub/data.ts +6 -0
  218. package/src/admin/phonehub/index.ts +10 -0
  219. package/src/admin/phonehub/models.ts +68 -0
  220. package/src/admin/rules/HandlerChainBuilder.tsx +51 -0
  221. package/src/admin/rules/RuleSetDetailPage.tsx +33 -0
  222. package/src/admin/rules/RuleSetsPage.tsx +33 -0
  223. package/src/admin/rules/editors.tsx +54 -0
  224. package/src/admin/rules/handler-dsl.ts +213 -0
  225. package/src/admin/rules/index.ts +13 -0
  226. package/src/admin/rules/models.ts +112 -0
  227. package/src/admin/security/devices/LoginEventDetail.tsx +123 -0
  228. package/src/admin/security/devices/LoginLocationMap.tsx +321 -0
  229. package/src/admin/security/devices/LoginLocationsPage.tsx +162 -0
  230. package/src/admin/security/devices/UserDeviceDetail.tsx +546 -0
  231. package/src/admin/security/devices/UserDeviceLocationDetail.tsx +272 -0
  232. package/src/admin/security/devices/UserDevicesPage.tsx +84 -0
  233. package/src/admin/security/devices/index.ts +40 -0
  234. package/src/admin/security/devices/models.ts +470 -0
  235. package/src/admin/security/geoip/GeoIpCachePage.tsx +135 -0
  236. package/src/admin/security/geoip/GeoIpDossier.tsx +804 -0
  237. package/src/admin/security/geoip/geoip-forms.ts +102 -0
  238. package/src/admin/security/geoip/index.ts +27 -0
  239. package/src/admin/security/geoip/models.ts +397 -0
  240. package/src/admin/security/index.ts +31 -0
  241. package/src/admin/security/models.ts +137 -0
  242. package/src/admin/security/tickets.tsx +562 -0
  243. package/src/admin/security-permissions.ts +5 -0
  244. package/src/admin/settings/SettingDetail.tsx +57 -0
  245. package/src/admin/settings/SettingEditor.tsx +157 -0
  246. package/src/admin/settings/SettingsPage.tsx +72 -0
  247. package/src/admin/settings/index.ts +19 -0
  248. package/src/admin/settings/model.ts +143 -0
  249. package/src/admin/shortlinks/ShortlinkDetail.tsx +55 -0
  250. package/src/admin/shortlinks/ShortlinkHistoryPage.tsx +18 -0
  251. package/src/admin/shortlinks/ShortlinksPage.tsx +59 -0
  252. package/src/admin/shortlinks/index.ts +18 -0
  253. package/src/admin/shortlinks/models.ts +246 -0
  254. package/src/admin/storage/BackendsPage.tsx +43 -0
  255. package/src/admin/storage/BucketDetail.tsx +77 -0
  256. package/src/admin/storage/BucketsPage.tsx +64 -0
  257. package/src/admin/storage/FileManagerDetail.tsx +36 -0
  258. package/src/admin/storage/FilePreview.tsx +54 -0
  259. package/src/admin/storage/FileUploadSurface.tsx +250 -0
  260. package/src/admin/storage/FileView.tsx +109 -0
  261. package/src/admin/storage/FilesPage.tsx +53 -0
  262. package/src/admin/storage/file-renditions.ts +99 -0
  263. package/src/admin/storage/index.ts +31 -0
  264. package/src/admin/storage/models.ts +585 -0
  265. package/src/admin/storage/storage-dialogs.tsx +80 -0
  266. package/src/charts/CircularProgress.tsx +281 -0
  267. package/src/charts/KPIStrip.tsx +162 -0
  268. package/src/charts/KPITile.tsx +125 -0
  269. package/src/charts/MetricsChart.tsx +459 -0
  270. package/src/charts/MetricsMiniWidget.tsx +588 -0
  271. package/src/charts/MiniChart.tsx +283 -0
  272. package/src/charts/PieChart.tsx +354 -0
  273. package/src/charts/SeriesChart.tsx +378 -0
  274. package/src/charts/chart-dialogs.tsx +149 -0
  275. package/src/charts/exportChart.ts +144 -0
  276. package/src/charts/index.ts +28 -0
  277. package/src/charts/pie-math.ts +127 -0
  278. package/src/charts/stats.ts +265 -0
  279. package/src/charts/worldmap/WorldMap.tsx +649 -0
  280. package/src/charts/worldmap/countryCentroids.ts +304 -0
  281. package/src/charts/worldmap/geo.ts +293 -0
  282. package/src/charts/worldmap/use-world-land.ts +47 -0
  283. package/src/charts/worldmap/world-land.ts +18 -0
  284. package/src/charts/worldmap/worldmap-data.ts +285 -0
  285. package/src/client/auth.ts +737 -0
  286. package/src/client/client.ts +268 -0
  287. package/src/client/duid.ts +31 -0
  288. package/src/client/errors.ts +33 -0
  289. package/src/client/group-context.ts +28 -0
  290. package/src/client/group.tsx +126 -0
  291. package/src/client/hooks.ts +50 -0
  292. package/src/client/index.ts +37 -0
  293. package/src/client/jwt.ts +75 -0
  294. package/src/client/location.ts +285 -0
  295. package/src/client/lookups.ts +41 -0
  296. package/src/client/markdown-fallback.tsx +115 -0
  297. package/src/client/markdown-parse.ts +386 -0
  298. package/src/client/markdown-sanitize.ts +217 -0
  299. package/src/client/markdown.ts +122 -0
  300. package/src/client/me.ts +188 -0
  301. package/src/client/mock.ts +9984 -0
  302. package/src/client/model.ts +227 -0
  303. package/src/client/params.ts +292 -0
  304. package/src/client/realtime-mock.ts +221 -0
  305. package/src/client/realtime.ts +561 -0
  306. package/src/client/record-feed.ts +329 -0
  307. package/src/client/safe-export.ts +71 -0
  308. package/src/client/types.ts +278 -0
  309. package/src/client/upload.ts +421 -0
  310. package/src/ui/AddressField.tsx +273 -0
  311. package/src/ui/AttachmentQueue.tsx +92 -0
  312. package/src/ui/CollectionMultiSelect.tsx +622 -0
  313. package/src/ui/CollectionSelect.tsx +517 -0
  314. package/src/ui/ComboBox.tsx +336 -0
  315. package/src/ui/DetailView.tsx +438 -0
  316. package/src/ui/FileDrop.tsx +194 -0
  317. package/src/ui/FileField.tsx +369 -0
  318. package/src/ui/FilterBar.tsx +269 -0
  319. package/src/ui/FormFields.tsx +77 -0
  320. package/src/ui/FormView.tsx +360 -0
  321. package/src/ui/FormWizard.tsx +336 -0
  322. package/src/ui/GroupSwitcher.tsx +202 -0
  323. package/src/ui/Guarded.tsx +16 -0
  324. package/src/ui/MarkdownView.tsx +89 -0
  325. package/src/ui/ModelTable.tsx +848 -0
  326. package/src/ui/MultiSelectDropdown.tsx +390 -0
  327. package/src/ui/Popover.tsx +204 -0
  328. package/src/ui/RecordFeed.tsx +627 -0
  329. package/src/ui/RequiresGroup.tsx +32 -0
  330. package/src/ui/RightPanel.tsx +107 -0
  331. package/src/ui/SidebarNav.tsx +330 -0
  332. package/src/ui/Tabs.tsx +161 -0
  333. package/src/ui/TagInput.tsx +381 -0
  334. package/src/ui/ThemeProvider.tsx +52 -0
  335. package/src/ui/UploadQueue.tsx +603 -0
  336. package/src/ui/armed-button.tsx +91 -0
  337. package/src/ui/date/Calendar.tsx +498 -0
  338. package/src/ui/date/DatePicker.tsx +289 -0
  339. package/src/ui/date/DateRangePicker.tsx +280 -0
  340. package/src/ui/date/DateTimePicker.tsx +587 -0
  341. package/src/ui/date/PresetRail.tsx +165 -0
  342. package/src/ui/date/TimePicker.tsx +624 -0
  343. package/src/ui/date/TimezoneSelect.tsx +267 -0
  344. package/src/ui/date/fns.ts +679 -0
  345. package/src/ui/detail/DataView.tsx +692 -0
  346. package/src/ui/detail/FlowStrip.tsx +80 -0
  347. package/src/ui/detail/KnownFieldsCard.tsx +121 -0
  348. package/src/ui/detail/MetadataSection.tsx +219 -0
  349. package/src/ui/detail/StackTraceView.tsx +150 -0
  350. package/src/ui/detail/StatusPanel.tsx +73 -0
  351. package/src/ui/detail/Timeline.tsx +52 -0
  352. package/src/ui/detail/index.ts +12 -0
  353. package/src/ui/field-registry.tsx +428 -0
  354. package/src/ui/field-wire.ts +322 -0
  355. package/src/ui/form-autosave.ts +583 -0
  356. package/src/ui/format.ts +444 -0
  357. package/src/ui/grouping.ts +150 -0
  358. package/src/ui/image-editor/ImageEditor.tsx +484 -0
  359. package/src/ui/image-editor/canvas.ts +254 -0
  360. package/src/ui/image-editor/index.ts +3 -0
  361. package/src/ui/image-editor/math.ts +416 -0
  362. package/src/ui/index.ts +58 -0
  363. package/src/ui/loading.tsx +604 -0
  364. package/src/ui/menu-registry.ts +213 -0
  365. package/src/ui/modal.tsx +297 -0
  366. package/src/ui/password.tsx +302 -0
  367. package/src/ui/schema-form-core.tsx +315 -0
  368. package/src/ui/toast.tsx +219 -0
  369. package/src/ui/ui.tsx +45 -0
@@ -0,0 +1,322 @@
1
+ // field-wire — the ONE place date/datetime form values cross between the
2
+ // control shape (what a picker holds) and the WIRE shape (what rides the
3
+ // django-mojo save body). Board #1278.
4
+ //
5
+ // The measured contract (mojo/serializers/core/serializer.py:380-389 +
6
+ // mojo/models/rest.py:1888-1892 · dates.parse_datetime):
7
+ // · DateTimeField serializes OUT as epoch SECONDS (int) and the save path
8
+ // parses epoch numbers straight back in — epoch is the datetime wire
9
+ // shape, both directions.
10
+ // · DateField serializes OUT as 'YYYY-MM-DD' (isoformat); the save path
11
+ // accepts epochs AND canonical strings (both run through
12
+ // dates.parse_datetime; naive values are treated as UTC).
13
+ // So the boundary emits EPOCH SECONDS by default (UTC midnight for date-only
14
+ // precisions — a UTC-parsed epoch midnight lands on the same DateField day),
15
+ // and `Field.outputFormat: 'date'` opts a field into canonical-string output
16
+ // for DateField columns that should round-trip exactly as the server emits.
17
+ // Reads (`wireToField`) accept BOTH shapes regardless, plus this package's
18
+ // canonical picker strings — a row can carry `last_login` epochs and `dob`
19
+ // strings through the same code path.
20
+ //
21
+ // NOT this module's problem: the `dr_field/dr_start/dr_end` daterange FILTER
22
+ // triple stays 'YYYY-MM-DD' strings — that path is FilterBar's/params',
23
+ // untouched (rest.py parses dr_* dates itself).
24
+ //
25
+ // State principle (how the two form surfaces stay out of the conversion
26
+ // business): form STATE HOLDS THE WIRE SHAPE. The registry renderer converts
27
+ // at its own boundary — render through `wireToField`, commit through
28
+ // `fieldToWire` — so SchemaForm's submit payload and FormView's autosave
29
+ // batches carry wire-ready values without either surface knowing dates exist.
30
+ import {
31
+ detectTemporal,
32
+ formatByPrecision,
33
+ formatDateTime,
34
+ parseByPrecision,
35
+ parseDateTime,
36
+ partsToMs,
37
+ type ParsedDate,
38
+ type Precision,
39
+ type TemporalKind,
40
+ } from './date/fns';
41
+ import type { Field, FieldValue } from '../client/types';
42
+
43
+ // ── Type families ─────────────────────────────────────────────────────
44
+
45
+ const DATE_TYPES = new Set(['datepicker', 'monthpicker', 'yearpicker']);
46
+ const RANGE_TYPES = new Set(['daterange', 'monthrange', 'yearrange']);
47
+ const LIST_TYPES = new Set(['multiselect', 'collectionmultiselect', 'collection-multiselect']);
48
+ const FILE_TYPES = new Set(['file', 'image']);
49
+
50
+ /** File relations are deliberately narrower than other collection values:
51
+ * only a positive JSON integer (or an expanded row carrying one) survives. */
52
+ export function fileRelationId(raw: unknown): number | null {
53
+ if (typeof raw === 'number' && Number.isSafeInteger(raw) && raw > 0) return raw;
54
+ if (raw != null && typeof raw === 'object' && !Array.isArray(raw)) {
55
+ const id = (raw as Record<string, unknown>).id;
56
+ return typeof id === 'number' && Number.isSafeInteger(id) && id > 0 ? id : null;
57
+ }
58
+ return null;
59
+ }
60
+
61
+ export function isFileRelationField(field: Pick<Field, 'type'>): boolean {
62
+ return FILE_TYPES.has(field.type);
63
+ }
64
+
65
+ /** The monthpicker/yearpicker/monthrange/yearrange alias → precision map
66
+ * (web-mojo inputs/index.js PRECISION_ALIASES + FormBuilder cases). */
67
+ const PRECISION_ALIASES: Record<string, Precision> = {
68
+ monthpicker: 'month',
69
+ yearpicker: 'year',
70
+ monthrange: 'month',
71
+ yearrange: 'year',
72
+ };
73
+
74
+ /** Effective precision: an EXPLICIT Field.precision wins over the alias
75
+ * (createInput parity: `options.precision || precision`). */
76
+ export function fieldPrecision(field: Field): Precision {
77
+ if (field.precision === 'day' || field.precision === 'month' || field.precision === 'year') {
78
+ return field.precision;
79
+ }
80
+ return PRECISION_ALIASES[field.type] ?? 'day';
81
+ }
82
+
83
+ /**
84
+ * The empty value each type holds in form state (and posts when cleared):
85
+ * '' for text-ish/CSV/time types, false for switch, [] for the list types,
86
+ * null for the null-clearing types (dates, datetime, daterange, collection —
87
+ * django-mojo nulls a nullable column for null/'').
88
+ */
89
+ export function emptyFieldValue(field: Field): FieldValue {
90
+ if (field.type === 'switch') return false;
91
+ if (LIST_TYPES.has(field.type)) return [];
92
+ if (DATE_TYPES.has(field.type) || RANGE_TYPES.has(field.type)) return null;
93
+ if (field.type === 'datetimepicker' || field.type === 'collection' || FILE_TYPES.has(field.type)) return null;
94
+ return '';
95
+ }
96
+
97
+ // ── Epoch math (UTC for date-only precisions) ─────────────────────────
98
+
99
+ // One warn per (field, value shape) — unparseable stored values are data/config
100
+ // bugs, not render events (house rule: fall back WITH a warn, never silently).
101
+ const warnedValues = new Set<string>();
102
+ function warnOnce(field: Field, value: unknown, note: string): void {
103
+ const key = `${field.name}:${String(value)}`;
104
+ if (warnedValues.has(key)) return;
105
+ warnedValues.add(key);
106
+ console.warn(`field-wire: ${note} (field "${field.name}", value ${JSON.stringify(value)})`);
107
+ }
108
+
109
+ /**
110
+ * Epoch seconds for an incoming raw, or null when the value is not an epoch
111
+ * (a canonical/ISO string takes the string path instead). Numbers AND
112
+ * numeric strings count; MILLISECOND epochs are normalized down to seconds
113
+ * — before this went through detectTemporal a 13-digit ms value was read as
114
+ * seconds and rendered as the year 57564.
115
+ */
116
+ function asEpoch(raw: unknown): number | null {
117
+ const t = detectTemporal(raw);
118
+ if (!t || (t.kind !== 'epoch-s' && t.kind !== 'epoch-ms')) return null;
119
+ return Math.floor(t.ms / 1000);
120
+ }
121
+
122
+ /** Canonical date string at precision → epoch seconds at UTC midnight
123
+ * (first-of-month / Jan 1 for coarser precisions). */
124
+ function canonicalToEpoch(canonical: string, precision: Precision): number | null {
125
+ const p = parseByPrecision(canonical, precision);
126
+ if (!p) return null;
127
+ return Math.floor(Date.UTC(p.y, (p.m ?? 1) - 1, p.d ?? 1) / 1000);
128
+ }
129
+
130
+ /** Epoch seconds → canonical string at precision, read in UTC (the inverse
131
+ * of canonicalToEpoch — a UTC-midnight epoch round-trips to the same day). */
132
+ function epochToCanonical(epoch: number, precision: Precision): string {
133
+ const d = new Date(epoch * 1000);
134
+ const parsed: ParsedDate = { y: d.getUTCFullYear(), m: d.getUTCMonth() + 1, d: d.getUTCDate() };
135
+ return formatByPrecision(parsed, precision);
136
+ }
137
+
138
+ const pad2 = (n: number): string => (n < 10 ? `0${n}` : String(n));
139
+
140
+ /**
141
+ * Datetime string → epoch seconds. Accepts every shape fns.parseDateTime
142
+ * does ('YYYY-MM-DD HH:MM', ISO 'T' forms, '±HH:MM' offsets, IANA tails,
143
+ * date-only). Zone semantics:
144
+ * · explicit offset → exact instant;
145
+ * · IANA zone → resolved via ianaOffset at that instant (two-pass so a
146
+ * DST boundary lands on the right side);
147
+ * · no zone → the BROWSER'S local wall time (what the user meant when
148
+ * they picked 14:30 in a zoneless picker).
149
+ */
150
+ export function datetimeStringToEpoch(s: string): number | null {
151
+ const parsed = parseDateTime(s);
152
+ if (!parsed) return null;
153
+ // The zone resolution itself lives in fns.partsToMs — ONE implementation,
154
+ // shared with the detector, so a picker commit and a rendered cell can
155
+ // never disagree about what instant a wall time means.
156
+ return Math.floor(partsToMs(parsed) / 1000);
157
+ }
158
+
159
+ /** Epoch seconds → 'YYYY-MM-DD HH:MM' in the BROWSER'S local time (the
160
+ * inverse of the zoneless parse above — round-trips to the same epoch). */
161
+ export function epochToDatetimeString(epoch: number): string {
162
+ const d = new Date(epoch * 1000);
163
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
164
+ }
165
+
166
+ // ── Shape memory (answer in the shape the server spoke) ───────────────
167
+ //
168
+ // django-mojo's datetime wire shape is per-COLUMN, not global: a
169
+ // DateTimeField serializes epoch seconds, a DateField 'YYYY-MM-DD', and a
170
+ // JSONField carries whatever the producer wrote (ISO strings are common).
171
+ // Emitting epochs for all of them — the old behavior — silently rewrote a
172
+ // DateField's shape on every save and turned an ISO metadata value into a
173
+ // number. So the boundary REMEMBERS what each field last read and answers
174
+ // in that shape; `Field.outputFormat` remains the explicit override.
175
+ //
176
+ // Keyed by the Field OBJECT (schemas are module-level consts, so the same
177
+ // object flows through wireToField and fieldToWire) — never by name, which
178
+ // would collide across forms. A field never read yet defaults to epoch
179
+ // seconds, the documented DateTimeField contract.
180
+ const inboundKind = new WeakMap<Field, TemporalKind>();
181
+
182
+ type WireShape = 'epoch' | 'epoch-ms' | 'date' | 'iso';
183
+
184
+ function rememberKind(field: Field, raw: unknown): void {
185
+ const t = detectTemporal(raw);
186
+ if (t) inboundKind.set(field, t.kind);
187
+ }
188
+
189
+ /** Explicit `Field.outputFormat` wins; else the remembered inbound shape;
190
+ * else epoch seconds. ('iana' is a TimePicker serialization, not a date
191
+ * wire shape — it falls through to epoch here.) */
192
+ function outputShape(field: Field): WireShape {
193
+ if (field.outputFormat === 'date') return 'date';
194
+ if (field.outputFormat === 'iso') return 'iso';
195
+ if (field.outputFormat === 'epoch') return 'epoch';
196
+ switch (inboundKind.get(field)) {
197
+ case 'date-string': return 'date';
198
+ case 'datetime-string': return 'iso';
199
+ case 'epoch-ms': return 'epoch-ms';
200
+ default: return 'epoch';
201
+ }
202
+ }
203
+
204
+ /** Epoch seconds → the wire value for a resolved shape. */
205
+ function epochAs(shape: WireShape, epoch: number): FieldValue {
206
+ if (shape === 'epoch-ms') return epoch * 1000;
207
+ if (shape === 'iso') return new Date(epoch * 1000).toISOString();
208
+ return epoch;
209
+ }
210
+
211
+ // ── The boundary ──────────────────────────────────────────────────────
212
+
213
+ /** Read a single date-ish raw into the canonical control string (or null). */
214
+ function rawToCanonical(field: Field, raw: unknown, precision: Precision): string | null {
215
+ if (raw == null || raw === '') return null;
216
+ const epoch = asEpoch(raw);
217
+ if (epoch != null) return epochToCanonical(epoch, precision);
218
+ if (typeof raw === 'string') {
219
+ const parsed = parseByPrecision(raw, precision);
220
+ if (parsed) return formatByPrecision(parsed, precision);
221
+ }
222
+ warnOnce(field, raw, 'value is neither an epoch nor a canonical date string — treating as empty');
223
+ return null;
224
+ }
225
+
226
+ /**
227
+ * CONTROL → WIRE. Date/range types take the picker's canonical string(s);
228
+ * datetimepicker takes the datetime string. Default output is epoch seconds;
229
+ * `outputFormat: 'date'` keeps canonical strings. Every other type passes
230
+ * through unchanged (its control shape IS its wire shape — see the value
231
+ * table in docs/forms.md).
232
+ */
233
+ export function fieldToWire(field: Field, value: FieldValue): FieldValue {
234
+ const shape = outputShape(field);
235
+
236
+ if (DATE_TYPES.has(field.type)) {
237
+ if (value == null || value === '') return null;
238
+ const canonical = String(value);
239
+ if (shape === 'date') return canonical;
240
+ const epoch = canonicalToEpoch(canonical, fieldPrecision(field));
241
+ if (epoch == null) {
242
+ warnOnce(field, value, 'commit is not a canonical date string — posting null');
243
+ return null;
244
+ }
245
+ return epochAs(shape, epoch);
246
+ }
247
+
248
+ if (RANGE_TYPES.has(field.type)) {
249
+ if (value == null || value === '' || (Array.isArray(value) && value.length === 0)) return null;
250
+ const pair = Array.isArray(value) ? value : [value];
251
+ const [s, e] = [String(pair[0] ?? ''), String(pair[1] ?? '')];
252
+ if (!s || !e) return null; // the picker only ever commits complete-or-cleared
253
+ if (shape === 'date') return [s, e];
254
+ const precision = fieldPrecision(field);
255
+ const es = canonicalToEpoch(s, precision);
256
+ const ee = canonicalToEpoch(e, precision);
257
+ if (es == null || ee == null) {
258
+ warnOnce(field, value, 'range commit is not a canonical pair — posting null');
259
+ return null;
260
+ }
261
+ return [epochAs(shape, es), epochAs(shape, ee)] as FieldValue;
262
+ }
263
+
264
+ if (field.type === 'datetimepicker') {
265
+ if (value == null || value === '') return null;
266
+ const str = String(value);
267
+ // 'date' on a datetime field means "keep the picker's own string"
268
+ // (the pre-detection behavior of outputFormat: 'date').
269
+ if (shape === 'date') return str;
270
+ const epoch = datetimeStringToEpoch(str);
271
+ if (epoch == null) {
272
+ warnOnce(field, value, 'commit is not a parseable datetime — posting null');
273
+ return null;
274
+ }
275
+ return epochAs(shape, epoch);
276
+ }
277
+
278
+ return value;
279
+ }
280
+
281
+ /**
282
+ * WIRE → CONTROL. Row/state raws (epoch numbers, epoch-strings, canonical
283
+ * strings — whatever the server or a previous commit left) become what the
284
+ * picker renders: canonical strings for date/range types, a local
285
+ * 'YYYY-MM-DD HH:MM' for datetimepicker. Other types pass through.
286
+ */
287
+ export function wireToField(field: Field, raw: FieldValue): FieldValue {
288
+ if (FILE_TYPES.has(field.type)) return fileRelationId(raw);
289
+ if (DATE_TYPES.has(field.type)) {
290
+ rememberKind(field, raw);
291
+ return rawToCanonical(field, raw, fieldPrecision(field));
292
+ }
293
+
294
+ if (RANGE_TYPES.has(field.type)) {
295
+ if (raw == null || raw === '') return null;
296
+ const precision = fieldPrecision(field);
297
+ // An autosave snapshot round-trip flattens arrays to 'a,b' (toDisplay
298
+ // String()s scalars) — accept the flattened form back.
299
+ const parts = Array.isArray(raw) ? raw : String(raw).split(',');
300
+ if (parts.length !== 2) {
301
+ warnOnce(field, raw, 'range value is not a two-element pair — treating as empty');
302
+ return null;
303
+ }
304
+ rememberKind(field, parts[0]); // both ends always share a shape
305
+ const s = rawToCanonical(field, parts[0], precision);
306
+ const e = rawToCanonical(field, parts[1], precision);
307
+ return s && e ? [s, e] : null;
308
+ }
309
+
310
+ if (field.type === 'datetimepicker') {
311
+ if (raw == null || raw === '') return null;
312
+ rememberKind(field, raw);
313
+ const epoch = asEpoch(raw);
314
+ if (epoch != null) return epochToDatetimeString(epoch);
315
+ const parsed = parseDateTime(String(raw));
316
+ if (parsed) return formatDateTime(parsed);
317
+ warnOnce(field, raw, 'value is neither an epoch nor a parseable datetime — treating as empty');
318
+ return null;
319
+ }
320
+
321
+ return raw;
322
+ }