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.
- package/LICENSE +201 -0
- package/README.md +55 -0
- package/docs/DetailView.md +173 -0
- package/docs/ModelTable.md +128 -0
- package/docs/README.md +98 -0
- package/docs/admin-assistant.md +72 -0
- package/docs/admin-bouncer.md +120 -0
- package/docs/admin-cloudwatch.md +38 -0
- package/docs/admin-credentials.md +116 -0
- package/docs/admin-dashboard.md +15 -0
- package/docs/admin-devices-geoip.md +284 -0
- package/docs/admin-dns-certificates.md +122 -0
- package/docs/admin-dns-records.md +65 -0
- package/docs/admin-dns-registrar.md +33 -0
- package/docs/admin-dns.md +80 -0
- package/docs/admin-identity-users.md +103 -0
- package/docs/admin-incidents.md +71 -0
- package/docs/admin-jobs.md +426 -0
- package/docs/admin-members.md +112 -0
- package/docs/admin-messaging.md +56 -0
- package/docs/admin-metrics-explorer.md +63 -0
- package/docs/admin-monitoring.md +99 -0
- package/docs/admin-network-security.md +393 -0
- package/docs/admin-phonehub.md +27 -0
- package/docs/admin-push.md +36 -0
- package/docs/admin-rules.md +87 -0
- package/docs/admin-security-tickets.md +40 -0
- package/docs/admin-settings.md +78 -0
- package/docs/admin-shortlinks.md +32 -0
- package/docs/admin-storage.md +141 -0
- package/docs/auth-pages.md +212 -0
- package/docs/auth.md +58 -0
- package/docs/calendar.md +122 -0
- package/docs/charts.md +303 -0
- package/docs/client.md +206 -0
- package/docs/collection-multiselect.md +117 -0
- package/docs/collection-select.md +132 -0
- package/docs/combobox.md +105 -0
- package/docs/dataview.md +226 -0
- package/docs/datepicker.md +151 -0
- package/docs/daterange.md +170 -0
- package/docs/datetimepicker.md +249 -0
- package/docs/defineModel.md +72 -0
- package/docs/detail-primitives.md +174 -0
- package/docs/feedback.md +85 -0
- package/docs/file-upload-ux.md +146 -0
- package/docs/forms.md +435 -0
- package/docs/grouping-and-fmt.md +98 -0
- package/docs/idioms.md +112 -0
- package/docs/image-editor.md +101 -0
- package/docs/loading.md +242 -0
- package/docs/location-address.md +76 -0
- package/docs/markdown.md +203 -0
- package/docs/menus-and-access.md +89 -0
- package/docs/multiselect-dropdown.md +153 -0
- package/docs/params.md +73 -0
- package/docs/popover.md +130 -0
- package/docs/realtime.md +61 -0
- package/docs/record-feed.md +238 -0
- package/docs/right-panel.md +42 -0
- package/docs/tabs-and-form-wizard.md +60 -0
- package/docs/taginput.md +120 -0
- package/docs/timepicker.md +173 -0
- package/docs/timezone-select.md +147 -0
- package/docs/worldmap.md +302 -0
- package/package.json +55 -0
- package/src/admin/assistant/AssistantFeed.tsx +29 -0
- package/src/admin/assistant/AssistantPanel.tsx +65 -0
- package/src/admin/assistant/api.ts +17 -0
- package/src/admin/assistant/data.ts +63 -0
- package/src/admin/assistant/index.ts +21 -0
- package/src/admin/assistant/launchers.tsx +33 -0
- package/src/admin/assistant/pages.tsx +78 -0
- package/src/admin/assistant/streaming.ts +238 -0
- package/src/admin/assistant/types.ts +30 -0
- package/src/admin/bouncer/devices.tsx +230 -0
- package/src/admin/bouncer/index.ts +34 -0
- package/src/admin/bouncer/models.ts +199 -0
- package/src/admin/bouncer/signals.tsx +182 -0
- package/src/admin/bouncer/signatures.tsx +220 -0
- package/src/admin/cloudwatch/CloudWatchChart.tsx +61 -0
- package/src/admin/cloudwatch/CloudWatchDashboardPage.tsx +52 -0
- package/src/admin/cloudwatch/CloudWatchResourceDetail.tsx +33 -0
- package/src/admin/cloudwatch/data.ts +163 -0
- package/src/admin/cloudwatch/index.ts +16 -0
- package/src/admin/credentials/group-api-keys.tsx +433 -0
- package/src/admin/credentials/index.ts +4 -0
- package/src/admin/credentials/models.ts +244 -0
- package/src/admin/credentials/secret-dialog.tsx +77 -0
- package/src/admin/credentials/webhook-subscriptions.tsx +423 -0
- package/src/admin/dashboard/AdminDashboardPage.tsx +101 -0
- package/src/admin/dashboard/data.ts +60 -0
- package/src/admin/dashboard/index.ts +2 -0
- package/src/admin/dns/CertificateDetail.tsx +115 -0
- package/src/admin/dns/CertificateLifecyclePoller.tsx +141 -0
- package/src/admin/dns/CertificateRequestDialog.tsx +103 -0
- package/src/admin/dns/CertificatesPage.tsx +85 -0
- package/src/admin/dns/DnsRecordEditor.tsx +110 -0
- package/src/admin/dns/DnsRecordsPage.tsx +36 -0
- package/src/admin/dns/DnsRecordsPanel.tsx +71 -0
- package/src/admin/dns/DomainCertificatesSection.tsx +40 -0
- package/src/admin/dns/DomainDetail.tsx +41 -0
- package/src/admin/dns/DomainOnboardingDialog.tsx +27 -0
- package/src/admin/dns/DomainPurchaseDetail.tsx +11 -0
- package/src/admin/dns/DomainPurchaseWizard.tsx +112 -0
- package/src/admin/dns/DomainPurchasesPage.tsx +16 -0
- package/src/admin/dns/DomainsPage.tsx +27 -0
- package/src/admin/dns/ProviderCredentialsPage.tsx +304 -0
- package/src/admin/dns/RegistrantContactPage.tsx +42 -0
- package/src/admin/dns/api.ts +515 -0
- package/src/admin/dns/certificate-data.ts +221 -0
- package/src/admin/dns/data.ts +32 -0
- package/src/admin/dns/dns-data.ts +266 -0
- package/src/admin/dns/dns-integration.ts +24 -0
- package/src/admin/dns/domain-links.ts +37 -0
- package/src/admin/dns/index.ts +86 -0
- package/src/admin/dns/models.ts +418 -0
- package/src/admin/dns/purchase-data.ts +67 -0
- package/src/admin/dns/registrant-data.ts +52 -0
- package/src/admin/identity/index.ts +5 -0
- package/src/admin/identity/members/GroupMembersPanel.tsx +146 -0
- package/src/admin/identity/members/MemberDetail.tsx +226 -0
- package/src/admin/identity/members/MembersPage.tsx +144 -0
- package/src/admin/identity/members/index.ts +24 -0
- package/src/admin/identity/members/member-flows.tsx +132 -0
- package/src/admin/identity/members/models.ts +204 -0
- package/src/admin/identity/users/UserDetail.tsx +257 -0
- package/src/admin/identity/users/UsersPage.tsx +163 -0
- package/src/admin/identity/users/index.ts +27 -0
- package/src/admin/identity/users/models.ts +232 -0
- package/src/admin/identity/users/sections/ApiKeysSection.tsx +119 -0
- package/src/admin/identity/users/sections/AuditSection.tsx +190 -0
- package/src/admin/identity/users/sections/DevicesSection.tsx +140 -0
- package/src/admin/identity/users/sections/GroupsSection.tsx +64 -0
- package/src/admin/identity/users/sections/LoginsSection.tsx +106 -0
- package/src/admin/identity/users/sections/MetadataPanel.tsx +36 -0
- package/src/admin/identity/users/sections/NotificationsSection.tsx +117 -0
- package/src/admin/identity/users/sections/OAuthSection.tsx +72 -0
- package/src/admin/identity/users/sections/OverviewSection.tsx +109 -0
- package/src/admin/identity/users/sections/PermissionsSection.tsx +35 -0
- package/src/admin/identity/users/sections/PersonalSection.tsx +73 -0
- package/src/admin/identity/users/sections/ProfileSection.tsx +175 -0
- package/src/admin/identity/users/sections/SecuritySection.tsx +123 -0
- package/src/admin/identity/users/sections/actions.tsx +752 -0
- package/src/admin/identity/users/sections/permission-catalog.ts +138 -0
- package/src/admin/identity/users/sections/queries.ts +74 -0
- package/src/admin/identity/users/sections/shared.tsx +299 -0
- package/src/admin/incidents/EventDetail.tsx +43 -0
- package/src/admin/incidents/EventsPage.tsx +52 -0
- package/src/admin/incidents/IncidentDetail.tsx +68 -0
- package/src/admin/incidents/IncidentsPage.tsx +102 -0
- package/src/admin/incidents/forensics.tsx +73 -0
- package/src/admin/incidents/index.ts +16 -0
- package/src/admin/incidents/models.ts +113 -0
- package/src/admin/incidents/sanitize.ts +71 -0
- package/src/admin/index.ts +383 -0
- package/src/admin/jobs/JobDashboardPage.tsx +111 -0
- package/src/admin/jobs/JobDetail.tsx +721 -0
- package/src/admin/jobs/JobRunnersPage.tsx +216 -0
- package/src/admin/jobs/JobsTablePage.tsx +122 -0
- package/src/admin/jobs/RunnerDetail.tsx +576 -0
- package/src/admin/jobs/ScheduledTaskDetail.tsx +421 -0
- package/src/admin/jobs/ScheduledTasksPage.tsx +167 -0
- package/src/admin/jobs/columns.tsx +297 -0
- package/src/admin/jobs/control.ts +488 -0
- package/src/admin/jobs/index.ts +85 -0
- package/src/admin/jobs/models.ts +599 -0
- package/src/admin/jobs/queries.ts +272 -0
- package/src/admin/jobs/scheduled-task-form.tsx +493 -0
- package/src/admin/jobs/sections/JobChannelsPanel.tsx +142 -0
- package/src/admin/jobs/sections/JobOperationsSection.tsx +423 -0
- package/src/admin/jobs/sections/JobRunnersStrip.tsx +82 -0
- package/src/admin/jobs/sections/JobThroughputSection.tsx +118 -0
- package/src/admin/messaging/EmailDomainWizard.tsx +24 -0
- package/src/admin/messaging/EmailDomainsPage.tsx +17 -0
- package/src/admin/messaging/EmailTemplatesPage.tsx +13 -0
- package/src/admin/messaging/MailboxesPage.tsx +15 -0
- package/src/admin/messaging/PublicMessagesPage.tsx +13 -0
- package/src/admin/messaging/SandboxedEmailPreview.tsx +23 -0
- package/src/admin/messaging/SentMessagesPage.tsx +11 -0
- package/src/admin/messaging/api.ts +17 -0
- package/src/admin/messaging/data.ts +10 -0
- package/src/admin/messaging/index.ts +23 -0
- package/src/admin/messaging/models.ts +76 -0
- package/src/admin/messaging/push/PushPage.tsx +44 -0
- package/src/admin/messaging/push/api.ts +17 -0
- package/src/admin/messaging/push/index.ts +9 -0
- package/src/admin/messaging/push/models.ts +43 -0
- package/src/admin/messaging/sanitize.ts +44 -0
- package/src/admin/monitoring/LogInspector.tsx +160 -0
- package/src/admin/monitoring/LogsPage.tsx +135 -0
- package/src/admin/monitoring/MetricsExplorerPage.tsx +360 -0
- package/src/admin/monitoring/MetricsPermissionsPage.tsx +166 -0
- package/src/admin/monitoring/MetricsSourcePicker.tsx +136 -0
- package/src/admin/monitoring/index.ts +55 -0
- package/src/admin/monitoring/metrics-explorer-client.ts +229 -0
- package/src/admin/monitoring/metrics-explorer-data.ts +158 -0
- package/src/admin/monitoring/models.ts +155 -0
- package/src/admin/network/BlockedIPsPage.tsx +194 -0
- package/src/admin/network/BlockedIpDetail.tsx +262 -0
- package/src/admin/network/FirewallLogPage.tsx +115 -0
- package/src/admin/network/IPSetDetail.tsx +280 -0
- package/src/admin/network/IPSetEditor.tsx +191 -0
- package/src/admin/network/IPSetsPage.tsx +177 -0
- package/src/admin/network/geofence/BlocksTab.tsx +289 -0
- package/src/admin/network/geofence/ExemptionsTab.tsx +349 -0
- package/src/admin/network/geofence/GeofencingPage.tsx +119 -0
- package/src/admin/network/geofence/PostureHeader.tsx +184 -0
- package/src/admin/network/geofence/RuleEditor.tsx +176 -0
- package/src/admin/network/geofence/RulesTab.tsx +254 -0
- package/src/admin/network/geofence/SimulatorTab.tsx +281 -0
- package/src/admin/network/geofence/geofence-data.ts +674 -0
- package/src/admin/network/index.ts +68 -0
- package/src/admin/network/models.ts +607 -0
- package/src/admin/phonehub/PhoneHubPage.tsx +57 -0
- package/src/admin/phonehub/api.ts +47 -0
- package/src/admin/phonehub/data.ts +6 -0
- package/src/admin/phonehub/index.ts +10 -0
- package/src/admin/phonehub/models.ts +68 -0
- package/src/admin/rules/HandlerChainBuilder.tsx +51 -0
- package/src/admin/rules/RuleSetDetailPage.tsx +33 -0
- package/src/admin/rules/RuleSetsPage.tsx +33 -0
- package/src/admin/rules/editors.tsx +54 -0
- package/src/admin/rules/handler-dsl.ts +213 -0
- package/src/admin/rules/index.ts +13 -0
- package/src/admin/rules/models.ts +112 -0
- package/src/admin/security/devices/LoginEventDetail.tsx +123 -0
- package/src/admin/security/devices/LoginLocationMap.tsx +321 -0
- package/src/admin/security/devices/LoginLocationsPage.tsx +162 -0
- package/src/admin/security/devices/UserDeviceDetail.tsx +546 -0
- package/src/admin/security/devices/UserDeviceLocationDetail.tsx +272 -0
- package/src/admin/security/devices/UserDevicesPage.tsx +84 -0
- package/src/admin/security/devices/index.ts +40 -0
- package/src/admin/security/devices/models.ts +470 -0
- package/src/admin/security/geoip/GeoIpCachePage.tsx +135 -0
- package/src/admin/security/geoip/GeoIpDossier.tsx +804 -0
- package/src/admin/security/geoip/geoip-forms.ts +102 -0
- package/src/admin/security/geoip/index.ts +27 -0
- package/src/admin/security/geoip/models.ts +397 -0
- package/src/admin/security/index.ts +31 -0
- package/src/admin/security/models.ts +137 -0
- package/src/admin/security/tickets.tsx +562 -0
- package/src/admin/security-permissions.ts +5 -0
- package/src/admin/settings/SettingDetail.tsx +57 -0
- package/src/admin/settings/SettingEditor.tsx +157 -0
- package/src/admin/settings/SettingsPage.tsx +72 -0
- package/src/admin/settings/index.ts +19 -0
- package/src/admin/settings/model.ts +143 -0
- package/src/admin/shortlinks/ShortlinkDetail.tsx +55 -0
- package/src/admin/shortlinks/ShortlinkHistoryPage.tsx +18 -0
- package/src/admin/shortlinks/ShortlinksPage.tsx +59 -0
- package/src/admin/shortlinks/index.ts +18 -0
- package/src/admin/shortlinks/models.ts +246 -0
- package/src/admin/storage/BackendsPage.tsx +43 -0
- package/src/admin/storage/BucketDetail.tsx +77 -0
- package/src/admin/storage/BucketsPage.tsx +64 -0
- package/src/admin/storage/FileManagerDetail.tsx +36 -0
- package/src/admin/storage/FilePreview.tsx +54 -0
- package/src/admin/storage/FileUploadSurface.tsx +250 -0
- package/src/admin/storage/FileView.tsx +109 -0
- package/src/admin/storage/FilesPage.tsx +53 -0
- package/src/admin/storage/file-renditions.ts +99 -0
- package/src/admin/storage/index.ts +31 -0
- package/src/admin/storage/models.ts +585 -0
- package/src/admin/storage/storage-dialogs.tsx +80 -0
- package/src/charts/CircularProgress.tsx +281 -0
- package/src/charts/KPIStrip.tsx +162 -0
- package/src/charts/KPITile.tsx +125 -0
- package/src/charts/MetricsChart.tsx +459 -0
- package/src/charts/MetricsMiniWidget.tsx +588 -0
- package/src/charts/MiniChart.tsx +283 -0
- package/src/charts/PieChart.tsx +354 -0
- package/src/charts/SeriesChart.tsx +378 -0
- package/src/charts/chart-dialogs.tsx +149 -0
- package/src/charts/exportChart.ts +144 -0
- package/src/charts/index.ts +28 -0
- package/src/charts/pie-math.ts +127 -0
- package/src/charts/stats.ts +265 -0
- package/src/charts/worldmap/WorldMap.tsx +649 -0
- package/src/charts/worldmap/countryCentroids.ts +304 -0
- package/src/charts/worldmap/geo.ts +293 -0
- package/src/charts/worldmap/use-world-land.ts +47 -0
- package/src/charts/worldmap/world-land.ts +18 -0
- package/src/charts/worldmap/worldmap-data.ts +285 -0
- package/src/client/auth.ts +737 -0
- package/src/client/client.ts +268 -0
- package/src/client/duid.ts +31 -0
- package/src/client/errors.ts +33 -0
- package/src/client/group-context.ts +28 -0
- package/src/client/group.tsx +126 -0
- package/src/client/hooks.ts +50 -0
- package/src/client/index.ts +37 -0
- package/src/client/jwt.ts +75 -0
- package/src/client/location.ts +285 -0
- package/src/client/lookups.ts +41 -0
- package/src/client/markdown-fallback.tsx +115 -0
- package/src/client/markdown-parse.ts +386 -0
- package/src/client/markdown-sanitize.ts +217 -0
- package/src/client/markdown.ts +122 -0
- package/src/client/me.ts +188 -0
- package/src/client/mock.ts +9984 -0
- package/src/client/model.ts +227 -0
- package/src/client/params.ts +292 -0
- package/src/client/realtime-mock.ts +221 -0
- package/src/client/realtime.ts +561 -0
- package/src/client/record-feed.ts +329 -0
- package/src/client/safe-export.ts +71 -0
- package/src/client/types.ts +278 -0
- package/src/client/upload.ts +421 -0
- package/src/ui/AddressField.tsx +273 -0
- package/src/ui/AttachmentQueue.tsx +92 -0
- package/src/ui/CollectionMultiSelect.tsx +622 -0
- package/src/ui/CollectionSelect.tsx +517 -0
- package/src/ui/ComboBox.tsx +336 -0
- package/src/ui/DetailView.tsx +438 -0
- package/src/ui/FileDrop.tsx +194 -0
- package/src/ui/FileField.tsx +369 -0
- package/src/ui/FilterBar.tsx +269 -0
- package/src/ui/FormFields.tsx +77 -0
- package/src/ui/FormView.tsx +360 -0
- package/src/ui/FormWizard.tsx +336 -0
- package/src/ui/GroupSwitcher.tsx +202 -0
- package/src/ui/Guarded.tsx +16 -0
- package/src/ui/MarkdownView.tsx +89 -0
- package/src/ui/ModelTable.tsx +848 -0
- package/src/ui/MultiSelectDropdown.tsx +390 -0
- package/src/ui/Popover.tsx +204 -0
- package/src/ui/RecordFeed.tsx +627 -0
- package/src/ui/RequiresGroup.tsx +32 -0
- package/src/ui/RightPanel.tsx +107 -0
- package/src/ui/SidebarNav.tsx +330 -0
- package/src/ui/Tabs.tsx +161 -0
- package/src/ui/TagInput.tsx +381 -0
- package/src/ui/ThemeProvider.tsx +52 -0
- package/src/ui/UploadQueue.tsx +603 -0
- package/src/ui/armed-button.tsx +91 -0
- package/src/ui/date/Calendar.tsx +498 -0
- package/src/ui/date/DatePicker.tsx +289 -0
- package/src/ui/date/DateRangePicker.tsx +280 -0
- package/src/ui/date/DateTimePicker.tsx +587 -0
- package/src/ui/date/PresetRail.tsx +165 -0
- package/src/ui/date/TimePicker.tsx +624 -0
- package/src/ui/date/TimezoneSelect.tsx +267 -0
- package/src/ui/date/fns.ts +679 -0
- package/src/ui/detail/DataView.tsx +692 -0
- package/src/ui/detail/FlowStrip.tsx +80 -0
- package/src/ui/detail/KnownFieldsCard.tsx +121 -0
- package/src/ui/detail/MetadataSection.tsx +219 -0
- package/src/ui/detail/StackTraceView.tsx +150 -0
- package/src/ui/detail/StatusPanel.tsx +73 -0
- package/src/ui/detail/Timeline.tsx +52 -0
- package/src/ui/detail/index.ts +12 -0
- package/src/ui/field-registry.tsx +428 -0
- package/src/ui/field-wire.ts +322 -0
- package/src/ui/form-autosave.ts +583 -0
- package/src/ui/format.ts +444 -0
- package/src/ui/grouping.ts +150 -0
- package/src/ui/image-editor/ImageEditor.tsx +484 -0
- package/src/ui/image-editor/canvas.ts +254 -0
- package/src/ui/image-editor/index.ts +3 -0
- package/src/ui/image-editor/math.ts +416 -0
- package/src/ui/index.ts +58 -0
- package/src/ui/loading.tsx +604 -0
- package/src/ui/menu-registry.ts +213 -0
- package/src/ui/modal.tsx +297 -0
- package/src/ui/password.tsx +302 -0
- package/src/ui/schema-form-core.tsx +315 -0
- package/src/ui/toast.tsx +219 -0
- package/src/ui/ui.tsx +45 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Timeline — the vertical event feed: a hairline connector with tone-colored
|
|
2
|
+
// dots. Incident history, job lifecycle, recent activity, audit trails —
|
|
3
|
+
// anywhere the record's story is "this happened, then this, then this."
|
|
4
|
+
// Port of web-mojo src/core/views/data/Timeline.js.
|
|
5
|
+
//
|
|
6
|
+
// ● Title line 4m ago
|
|
7
|
+
// │ Optional supporting body
|
|
8
|
+
// ● Title line 1h ago
|
|
9
|
+
// │ body
|
|
10
|
+
//
|
|
11
|
+
// Deviations from source: `detail` was a TRUSTED-HTML string — it is the
|
|
12
|
+
// `body` ReactNode slot here (architecture rule 6); source field names
|
|
13
|
+
// headline/detail/when read as title/body/meta.
|
|
14
|
+
import type { ReactNode } from 'react';
|
|
15
|
+
import type { Tone } from '../format';
|
|
16
|
+
import { normalizeTone } from './StatusPanel';
|
|
17
|
+
|
|
18
|
+
export interface TimelineItem {
|
|
19
|
+
/** Colors the dot — the feed's only signal of "which of these went wrong". */
|
|
20
|
+
tone?: Tone;
|
|
21
|
+
title: string;
|
|
22
|
+
/** Right-aligned timestamp column ("4m ago", "2026-04-21 11:42"). */
|
|
23
|
+
meta?: string;
|
|
24
|
+
/** Supporting line under the title. A slot; compose `<code>`/`<a>` freely. */
|
|
25
|
+
body?: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function Timeline({ items, emptyText = 'No events yet.', limit }: {
|
|
29
|
+
items: TimelineItem[];
|
|
30
|
+
emptyText?: string;
|
|
31
|
+
/** Render at most N items (the feed's "recent activity" mode). */
|
|
32
|
+
limit?: number;
|
|
33
|
+
}) {
|
|
34
|
+
// The empty state renders INSIDE the <ol> so the rail stays visually intact.
|
|
35
|
+
const shown = limit && limit > 0 ? items.slice(0, Math.floor(limit)) : items;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<ol className="detail-timeline">
|
|
39
|
+
{shown.length === 0 ? (
|
|
40
|
+
<li className="detail-timeline-empty dim">{emptyText}</li>
|
|
41
|
+
) : shown.map((item, i) => (
|
|
42
|
+
<li key={`${item.title}-${i}`} className={`detail-timeline-item tone-${normalizeTone(item.tone, 'Timeline')}`}>
|
|
43
|
+
<div>
|
|
44
|
+
<div className="detail-timeline-title">{item.title}</div>
|
|
45
|
+
{item.body != null && item.body !== '' && <div className="detail-timeline-body">{item.body}</div>}
|
|
46
|
+
</div>
|
|
47
|
+
{item.meta && <span className="detail-timeline-meta">{item.meta}</span>}
|
|
48
|
+
</li>
|
|
49
|
+
))}
|
|
50
|
+
</ol>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// portal-mojo/ui — detail primitives: the small building blocks admin record
|
|
2
|
+
// pages are composed from. Ported from web-mojo's src/core/views/data/*
|
|
3
|
+
// primitives + the admin metadata section. Styles live in the consuming app
|
|
4
|
+
// (apps/portal/src/theme/detail-primitives.css); docs in
|
|
5
|
+
// packages/portal-mojo/docs/detail-primitives.md.
|
|
6
|
+
export * from './StatusPanel';
|
|
7
|
+
export * from './FlowStrip';
|
|
8
|
+
export * from './Timeline';
|
|
9
|
+
export * from './KnownFieldsCard';
|
|
10
|
+
export * from './DataView';
|
|
11
|
+
export * from './MetadataSection';
|
|
12
|
+
export * from './StackTraceView';
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
// field-registry — the field-type registry: every non-builtin `Field.type`
|
|
2
|
+
// string resolves here to a renderer that binds the Field def + the form's
|
|
3
|
+
// controlled value/commit pipeline to the real component. Board #1278;
|
|
4
|
+
// web-mojo sources: src/core/forms/inputs/index.js (INPUT_TYPES +
|
|
5
|
+
// PRECISION_ALIASES + createInput) and FormBuilder.js's field-type switch.
|
|
6
|
+
// This is also the FormPlugins replacement (PLAN "pattern, not port"):
|
|
7
|
+
// `registerFieldType` is the app-extension seam.
|
|
8
|
+
//
|
|
9
|
+
// Contracts:
|
|
10
|
+
// · One value pipeline. A renderer receives the form-state value and ONE
|
|
11
|
+
// `commit(next)` — every component here fires on COMMIT (selection,
|
|
12
|
+
// blur/Enter settle, toggle), never per keystroke, so a commit is always
|
|
13
|
+
// a save-worthy gesture for FormView's 300ms batch.
|
|
14
|
+
// · State holds the WIRE shape (see field-wire.ts). Date-ish renderers
|
|
15
|
+
// convert at their own edge: render via wireToField, commit via
|
|
16
|
+
// fieldToWire. No other layer converts.
|
|
17
|
+
// · Unknown types fall back to a text input WITH one console.warn — the
|
|
18
|
+
// SchemaForm/FormView surfaces call `warnUnknownFieldType` so the rule
|
|
19
|
+
// (and its warn-once set) lives in one place.
|
|
20
|
+
// · Renderers are React COMPONENTS (rendered via <Renderer/>), so a
|
|
21
|
+
// binding may hold hooks/state of its own.
|
|
22
|
+
import { useState } from 'react';
|
|
23
|
+
import type { ComponentType } from 'react';
|
|
24
|
+
import type { Field, FieldValue, FieldValues } from '../client/types';
|
|
25
|
+
import { CollectionMultiSelect } from './CollectionMultiSelect';
|
|
26
|
+
import { CollectionSelect } from './CollectionSelect';
|
|
27
|
+
import { ComboBox, type ComboValue } from './ComboBox';
|
|
28
|
+
import { AddressField } from './AddressField';
|
|
29
|
+
import { MultiSelectDropdown, type MultiSelectValue } from './MultiSelectDropdown';
|
|
30
|
+
import { TagInput } from './TagInput';
|
|
31
|
+
import { DatePicker } from './date/DatePicker';
|
|
32
|
+
import { DateRangePicker } from './date/DateRangePicker';
|
|
33
|
+
import { DateTimePicker } from './date/DateTimePicker';
|
|
34
|
+
import { TimePicker, type TimeValue } from './date/TimePicker';
|
|
35
|
+
import { TimezoneSelect } from './date/TimezoneSelect';
|
|
36
|
+
import { FileField, ImageField, type FileFieldOwnerResult } from './FileField';
|
|
37
|
+
import {
|
|
38
|
+
emptyFieldValue,
|
|
39
|
+
fieldPrecision,
|
|
40
|
+
fieldToWire,
|
|
41
|
+
wireToField,
|
|
42
|
+
} from './field-wire';
|
|
43
|
+
|
|
44
|
+
// ── The registry ──────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/** What a form surface hands a registry renderer. */
|
|
47
|
+
export interface RegistryFieldProps {
|
|
48
|
+
field: Field;
|
|
49
|
+
/** The form-state value (WIRE-shaped — field-wire.ts is the converter). */
|
|
50
|
+
value: FieldValue;
|
|
51
|
+
/** Paint the error border (the surface renders the error text itself). */
|
|
52
|
+
invalid?: boolean;
|
|
53
|
+
/** Surface-level lock (the field's own `disabled` is also honored). */
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
/** Stable control metadata supplied by schema-driven form surfaces. */
|
|
56
|
+
controlId?: string;
|
|
57
|
+
ariaDescribedBy?: string;
|
|
58
|
+
/** Optional focus registration; composite renderers may adopt it. */
|
|
59
|
+
focusTarget?: (node: HTMLElement | null) => void;
|
|
60
|
+
/** THE change pipeline: called once per commit with the next state value. */
|
|
61
|
+
commit: (value: FieldValue) => void;
|
|
62
|
+
/** Atomic multi-field commit, filtered to fields declared by the surface. */
|
|
63
|
+
commitPatch: (patch: FieldValues) => void;
|
|
64
|
+
/** Active transfer reporting lets the owning form block submit/navigation/dismiss. */
|
|
65
|
+
onPendingChange?: (pending: boolean) => void;
|
|
66
|
+
/** Authoritative owner-save reconciliation for relation attachment. */
|
|
67
|
+
ownerResult?: FileFieldOwnerResult;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A renderer is a function component over RegistryFieldProps. */
|
|
71
|
+
export type FieldTypeRenderer = ComponentType<RegistryFieldProps>;
|
|
72
|
+
|
|
73
|
+
const registry = new Map<string, FieldTypeRenderer>();
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Register (or replace — idempotent under HMR) a renderer for one or more
|
|
77
|
+
* type names. Names match EXACTLY (no case folding): a typo'd type must fall
|
|
78
|
+
* through to the loud unknown-type fallback, not silently half-match.
|
|
79
|
+
*/
|
|
80
|
+
export function registerFieldType(names: string | string[], renderer: FieldTypeRenderer): void {
|
|
81
|
+
for (const name of Array.isArray(names) ? names : [names]) {
|
|
82
|
+
registry.set(name, renderer);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The renderer for a type, or null (→ the caller warns + text-input falls back). */
|
|
87
|
+
export function resolveFieldRenderer(type: string): FieldTypeRenderer | null {
|
|
88
|
+
return registry.get(type) ?? null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Registered names, registration order (docs/tooling; not a render path). */
|
|
92
|
+
export function registeredFieldTypes(): string[] {
|
|
93
|
+
return [...registry.keys()];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// One warn per (field, type) shared by every surface — an unknown type is a
|
|
97
|
+
// config bug, not a render event (same policy as SchemaSelect's values).
|
|
98
|
+
const warnedUnknownTypes = new Set<string>();
|
|
99
|
+
export function warnUnknownFieldType(surface: string, field: Field): void {
|
|
100
|
+
const key = `${field.name}:${field.type}`;
|
|
101
|
+
if (warnedUnknownTypes.has(key)) return;
|
|
102
|
+
warnedUnknownTypes.add(key);
|
|
103
|
+
console.warn(`${surface}: unknown field type "${field.type}" on "${field.name}" — rendering a text input`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Value coercion helpers (state → component props) ──────────────────
|
|
107
|
+
|
|
108
|
+
/** State value → id list for the multi-pick components. Accepts real arrays,
|
|
109
|
+
* a bare scalar, and the CSV/flattened string an autosave snapshot
|
|
110
|
+
* round-trip produces (form-autosave's toDisplay String()s scalars). */
|
|
111
|
+
function toIdArray(value: FieldValue): Array<string | number> {
|
|
112
|
+
if (value == null || value === '' || value === false) return [];
|
|
113
|
+
if (Array.isArray(value)) return value;
|
|
114
|
+
if (typeof value === 'boolean') return [];
|
|
115
|
+
if (typeof value === 'number') return [value];
|
|
116
|
+
return String(value).split(',').map((s) => s.trim()).filter((s) => s !== '');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** State value → the scalar the single-pick components hold (null = none). */
|
|
120
|
+
function toScalar(value: FieldValue): string | number | null {
|
|
121
|
+
if (value == null || value === '' || typeof value === 'boolean') return null;
|
|
122
|
+
if (Array.isArray(value)) return value.length ? value[0]! : null;
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** collection types: the endpoint a Field names (model wins), or null. */
|
|
127
|
+
function fieldEndpoint(field: Field): string | null {
|
|
128
|
+
return field.model?.endpoint ?? field.endpoint ?? null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// One warn per field for config-level misses (missing endpoint etc.).
|
|
132
|
+
const warnedConfig = new Set<string>();
|
|
133
|
+
function warnConfigOnce(field: Field, note: string): void {
|
|
134
|
+
if (warnedConfig.has(field.name)) return;
|
|
135
|
+
warnedConfig.add(field.name);
|
|
136
|
+
console.warn(`field-registry: ${note} (field "${field.name}")`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The last-resort control — also what a misconfigured binding degrades to
|
|
140
|
+
* (never "render nothing"). Commits on blur/Enter like FormView text. */
|
|
141
|
+
function FallbackTextInput({ field, value, invalid, disabled, commit }: Pick<
|
|
142
|
+
RegistryFieldProps, 'field' | 'value' | 'invalid' | 'disabled' | 'commit'
|
|
143
|
+
>) {
|
|
144
|
+
const committed = value == null || typeof value === 'object' ? '' : String(value);
|
|
145
|
+
const [draft, setDraft] = useState<string | null>(null);
|
|
146
|
+
const shown = draft ?? committed;
|
|
147
|
+
const settle = () => {
|
|
148
|
+
if (draft != null && draft !== committed) commit(draft);
|
|
149
|
+
setDraft(null);
|
|
150
|
+
};
|
|
151
|
+
return (
|
|
152
|
+
<input
|
|
153
|
+
className={`input${invalid ? ' input-invalid' : ''}`}
|
|
154
|
+
type="text"
|
|
155
|
+
placeholder={field.placeholder}
|
|
156
|
+
value={shown}
|
|
157
|
+
disabled={disabled || field.disabled}
|
|
158
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
159
|
+
onBlur={settle}
|
|
160
|
+
onKeyDown={(e) => {
|
|
161
|
+
if (e.key === 'Enter') {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
settle();
|
|
164
|
+
}
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Standard bindings (web-mojo INPUT_TYPES + FormBuilder aliases) ────
|
|
171
|
+
|
|
172
|
+
registerFieldType('file', function FileRegistryField({ field, value, invalid, disabled, controlId, ariaDescribedBy, commit, onPendingChange, ownerResult }) {
|
|
173
|
+
return <FileField value={value} onChange={commit} accept={field.accept} maxFileSize={field.maxFileSize} destination={field.uploadDestination} disabled={disabled || field.disabled} invalid={invalid} controlId={controlId} ariaDescribedBy={ariaDescribedBy} onPendingChange={onPendingChange} ownerResult={ownerResult} onOrphan={field.onUploadOrphan} />;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
registerFieldType('image', function ImageRegistryField({ field, value, invalid, disabled, controlId, ariaDescribedBy, commit, onPendingChange, ownerResult }) {
|
|
177
|
+
return <ImageField value={value} onChange={commit} accept={field.accept} maxFileSize={field.maxFileSize} destination={field.uploadDestination} disabled={disabled || field.disabled} invalid={invalid} controlId={controlId} ariaDescribedBy={ariaDescribedBy} onPendingChange={onPendingChange} ownerResult={ownerResult} onOrphan={field.onUploadOrphan} edit={field.imageEdit} requireEdit={field.imageEditRequired} />;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// tag | tags → TagInput. State/wire: CSV string (the load-bearing shape —
|
|
181
|
+
// django-mojo models split the stored string).
|
|
182
|
+
registerFieldType(['tag', 'tags'], function TagField({ field, value, disabled, commit }) {
|
|
183
|
+
return (
|
|
184
|
+
<TagInput
|
|
185
|
+
value={Array.isArray(value) ? value.map(String) : String(value ?? '')}
|
|
186
|
+
placeholder={field.placeholder}
|
|
187
|
+
maxTags={field.maxTags}
|
|
188
|
+
allowDuplicates={field.allowDuplicates}
|
|
189
|
+
separator={field.separator}
|
|
190
|
+
disabled={disabled || field.disabled}
|
|
191
|
+
onChange={(csv) => commit(csv)}
|
|
192
|
+
/>
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// multiselect → MultiSelectDropdown. State/wire: array of option values.
|
|
197
|
+
registerFieldType('multiselect', function MultiSelectField({ field, value, disabled, commit }) {
|
|
198
|
+
return (
|
|
199
|
+
<MultiSelectDropdown
|
|
200
|
+
options={(field.options ?? []).map((o) => ({ value: o.value, label: o.label, disabled: o.disabled }))}
|
|
201
|
+
value={toIdArray(value) as MultiSelectValue[]}
|
|
202
|
+
placeholder={field.placeholder}
|
|
203
|
+
disabled={disabled || field.disabled}
|
|
204
|
+
onChange={(values) => commit(values)}
|
|
205
|
+
/>
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// collection → CollectionSelect. State/wire: the picked row's id, null = none.
|
|
210
|
+
registerFieldType('collection', function CollectionField({ field, value, disabled, commit }) {
|
|
211
|
+
const endpoint = fieldEndpoint(field);
|
|
212
|
+
if (!endpoint) {
|
|
213
|
+
warnConfigOnce(field, 'type "collection" needs `model` or `endpoint` — rendering a text input');
|
|
214
|
+
return <FallbackTextInput field={field} value={value} disabled={disabled} commit={commit} />;
|
|
215
|
+
}
|
|
216
|
+
return (
|
|
217
|
+
<CollectionSelect
|
|
218
|
+
endpoint={endpoint}
|
|
219
|
+
value={toScalar(value)}
|
|
220
|
+
labelField={field.labelField}
|
|
221
|
+
valueField={field.valueField}
|
|
222
|
+
maxItems={field.maxItems}
|
|
223
|
+
placeholder={field.placeholder}
|
|
224
|
+
debounceMs={field.debounceMs}
|
|
225
|
+
emptyFetch={field.emptyFetch}
|
|
226
|
+
defaultParams={field.defaultParams}
|
|
227
|
+
requiresActiveGroup={field.requiresActiveGroup}
|
|
228
|
+
disabled={disabled || field.disabled}
|
|
229
|
+
onChange={(id) => commit(id ?? null)}
|
|
230
|
+
/>
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// collectionmultiselect → CollectionMultiSelect. State/wire: array of ids.
|
|
235
|
+
registerFieldType(['collectionmultiselect', 'collection-multiselect'], function CollectionMultiField({ field, value, disabled, commit }) {
|
|
236
|
+
const endpoint = fieldEndpoint(field);
|
|
237
|
+
if (!endpoint) {
|
|
238
|
+
warnConfigOnce(field, 'type "collectionmultiselect" needs `model` or `endpoint` — rendering a text input');
|
|
239
|
+
return <FallbackTextInput field={field} value={value} disabled={disabled} commit={commit} />;
|
|
240
|
+
}
|
|
241
|
+
return (
|
|
242
|
+
<CollectionMultiSelect
|
|
243
|
+
endpoint={endpoint}
|
|
244
|
+
value={toIdArray(value)}
|
|
245
|
+
labelField={field.labelField}
|
|
246
|
+
valueField={field.valueField}
|
|
247
|
+
enableSearch={field.enableSearch}
|
|
248
|
+
searchPlaceholder={field.placeholder}
|
|
249
|
+
defaultParams={field.defaultParams}
|
|
250
|
+
requiresActiveGroup={field.requiresActiveGroup}
|
|
251
|
+
disabled={disabled || field.disabled}
|
|
252
|
+
onChange={(ids) => commit(ids)}
|
|
253
|
+
/>
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// combo | combobox | autocomplete → ComboBox. State/wire: the committed
|
|
258
|
+
// value (string/number; allowCustom lets free text through as-is).
|
|
259
|
+
registerFieldType(['combo', 'combobox', 'autocomplete'], function ComboField({ field, value, disabled, commit }) {
|
|
260
|
+
return (
|
|
261
|
+
<ComboBox
|
|
262
|
+
value={toScalar(value) as ComboValue | null}
|
|
263
|
+
options={(field.options ?? []).map((o) => ({ value: o.value, label: o.label, description: o.description }))}
|
|
264
|
+
placeholder={field.placeholder}
|
|
265
|
+
allowCustom={field.allowCustom}
|
|
266
|
+
showDescription={field.showDescription}
|
|
267
|
+
maxSuggestions={field.maxSuggestions}
|
|
268
|
+
disabled={disabled || field.disabled}
|
|
269
|
+
required={field.required}
|
|
270
|
+
onChange={(v) => commit(v)}
|
|
271
|
+
/>
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
registerFieldType('address', function AddressRegistryField({
|
|
276
|
+
field, value, invalid, disabled, controlId, ariaDescribedBy,
|
|
277
|
+
focusTarget, commit, commitPatch,
|
|
278
|
+
}) {
|
|
279
|
+
return (
|
|
280
|
+
<AddressField
|
|
281
|
+
fieldName={field.name}
|
|
282
|
+
fields={field.addressFields}
|
|
283
|
+
value={typeof value === 'string' ? value : String(value ?? '')}
|
|
284
|
+
country={field.country}
|
|
285
|
+
minChars={field.minChars}
|
|
286
|
+
debounceMs={field.debounceMs}
|
|
287
|
+
maxSuggestions={field.maxSuggestions}
|
|
288
|
+
placeholder={field.placeholder}
|
|
289
|
+
disabled={disabled || field.disabled}
|
|
290
|
+
required={field.required}
|
|
291
|
+
invalid={invalid}
|
|
292
|
+
id={controlId}
|
|
293
|
+
ariaDescribedBy={ariaDescribedBy}
|
|
294
|
+
focusTarget={focusTarget}
|
|
295
|
+
onCommit={(next) => commit(next)}
|
|
296
|
+
onPatch={commitPatch}
|
|
297
|
+
/>
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// datepicker | monthpicker | yearpicker → DatePicker. The ALIAS pre-sets
|
|
302
|
+
// precision; explicit Field.precision wins (createInput parity). State/wire:
|
|
303
|
+
// epoch seconds (UTC midnight) — or the canonical string under
|
|
304
|
+
// outputFormat:'date'. Control: canonical YYYY-MM-DD / YYYY-MM / YYYY.
|
|
305
|
+
registerFieldType(['datepicker', 'monthpicker', 'yearpicker'], function DateField({ field, value, invalid, disabled, commit }) {
|
|
306
|
+
return (
|
|
307
|
+
<DatePicker
|
|
308
|
+
value={wireToField(field, value) as string | null}
|
|
309
|
+
precision={fieldPrecision(field)}
|
|
310
|
+
displayFormat={field.displayFormat}
|
|
311
|
+
placeholder={field.placeholder}
|
|
312
|
+
min={field.min != null ? String(field.min) : null}
|
|
313
|
+
max={field.max != null ? String(field.max) : null}
|
|
314
|
+
required={field.required}
|
|
315
|
+
invalid={invalid}
|
|
316
|
+
disabled={disabled || field.disabled}
|
|
317
|
+
onChange={(e) => commit(fieldToWire(field, e.value))}
|
|
318
|
+
/>
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// daterange | monthrange | yearrange → DateRangePicker. ONE field name;
|
|
323
|
+
// state/wire: [startEpoch, endEpoch] (or canonical pair under
|
|
324
|
+
// outputFormat:'date'), null when empty — the picker commits
|
|
325
|
+
// complete-or-cleared only. (Deviation from web-mojo's startName/endName
|
|
326
|
+
// twin wire keys — split columns should use two datepicker fields.)
|
|
327
|
+
registerFieldType(['daterange', 'monthrange', 'yearrange'], function DateRangeField({ field, value, disabled, commit }) {
|
|
328
|
+
const pair = wireToField(field, value) as [string, string] | null;
|
|
329
|
+
return (
|
|
330
|
+
<DateRangePicker
|
|
331
|
+
start={pair?.[0] ?? ''}
|
|
332
|
+
end={pair?.[1] ?? ''}
|
|
333
|
+
precision={fieldPrecision(field)}
|
|
334
|
+
months={field.months}
|
|
335
|
+
presets={field.presets ?? null}
|
|
336
|
+
displayFormat={field.displayFormat}
|
|
337
|
+
separator={field.separator}
|
|
338
|
+
placeholder={field.placeholder}
|
|
339
|
+
min={field.min != null ? String(field.min) : null}
|
|
340
|
+
max={field.max != null ? String(field.max) : null}
|
|
341
|
+
required={field.required}
|
|
342
|
+
disabled={disabled || field.disabled}
|
|
343
|
+
onChange={(e) => commit(e.start && e.end ? fieldToWire(field, [e.start, e.end]) : null)}
|
|
344
|
+
/>
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// timepicker → TimePicker, with the REAL TimezoneSelect in its
|
|
349
|
+
// renderTimezoneSelect slot (the component's own <select> fallback is for
|
|
350
|
+
// standalone use only). State/wire: the serialized time STRING —
|
|
351
|
+
// outputFormat 'iso' (default, '14:30-07:00') or 'iana'
|
|
352
|
+
// ('14:30 America/Los_Angeles'); '' when empty. Times are not epochs.
|
|
353
|
+
registerFieldType('timepicker', function TimeField({ field, value, invalid, disabled, commit }) {
|
|
354
|
+
const committed = typeof value === 'string' ? value : '';
|
|
355
|
+
return (
|
|
356
|
+
<TimePicker
|
|
357
|
+
value={committed}
|
|
358
|
+
format={field.timeFormat}
|
|
359
|
+
step={field.step}
|
|
360
|
+
min={field.min != null ? String(field.min) : null}
|
|
361
|
+
max={field.max != null ? String(field.max) : null}
|
|
362
|
+
placeholder={field.placeholder}
|
|
363
|
+
timezone={field.timezone}
|
|
364
|
+
timezones={field.timezones ?? null}
|
|
365
|
+
outputFormat={field.outputFormat === 'iana' ? 'iana' : 'iso'}
|
|
366
|
+
required={field.required}
|
|
367
|
+
invalid={invalid}
|
|
368
|
+
disabled={disabled || field.disabled}
|
|
369
|
+
renderTimezoneSelect={(slot) => (
|
|
370
|
+
<TimezoneSelect
|
|
371
|
+
value={slot.value}
|
|
372
|
+
timezones={slot.timezones ?? null}
|
|
373
|
+
disabled={slot.disabled}
|
|
374
|
+
onChange={(e) => slot.onChange(e.value)}
|
|
375
|
+
/>
|
|
376
|
+
)}
|
|
377
|
+
onChange={(e: { value: TimeValue }) => commit(typeof e.value === 'string' ? e.value : '')}
|
|
378
|
+
/>
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// timezone → TimezoneSelect. State/wire: the IANA zone string ('' = unset;
|
|
383
|
+
// the picker DISPLAYS the local zone for an empty value — resolveTimezone
|
|
384
|
+
// is the helper when a form must post that effective default).
|
|
385
|
+
registerFieldType('timezone', function TimezoneField({ field, value, disabled, commit }) {
|
|
386
|
+
return (
|
|
387
|
+
<TimezoneSelect
|
|
388
|
+
value={typeof value === 'string' && value !== '' ? value : null}
|
|
389
|
+
timezones={field.timezones ?? null}
|
|
390
|
+
placeholder={field.placeholder}
|
|
391
|
+
maxSuggestions={field.maxSuggestions}
|
|
392
|
+
required={field.required}
|
|
393
|
+
disabled={disabled || field.disabled}
|
|
394
|
+
onChange={(e) => commit(e.value)}
|
|
395
|
+
/>
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// datetimepicker — the real #1273 DateTimePicker (seam activated at merge:
|
|
400
|
+
// the control renders the picker; state stays in the wire shape via
|
|
401
|
+
// field-wire on both edges, exactly as every date-ish renderer here).
|
|
402
|
+
registerFieldType('datetimepicker', function DateTimeField({ field, value, invalid, disabled, commit }) {
|
|
403
|
+
return (
|
|
404
|
+
<DateTimePicker
|
|
405
|
+
value={wireToField(field, value) as string | null}
|
|
406
|
+
timeFormat={field.timeFormat}
|
|
407
|
+
displayFormat={field.displayFormat}
|
|
408
|
+
timezone={field.timezones ?? field.timezone}
|
|
409
|
+
placeholder={field.placeholder}
|
|
410
|
+
required={field.required}
|
|
411
|
+
invalid={invalid}
|
|
412
|
+
disabled={disabled || field.disabled}
|
|
413
|
+
onChange={(e) => {
|
|
414
|
+
if (e.value == null) return commit(null);
|
|
415
|
+
// This binding never sets outputFormat:'object', so e.value is a
|
|
416
|
+
// string; the object branch keeps the type honest regardless.
|
|
417
|
+
const s = typeof e.value === 'string'
|
|
418
|
+
? e.value
|
|
419
|
+
: `${e.value.date} ${e.value.time}${e.value.timezone ? ` ${e.value.timezone}` : ''}`;
|
|
420
|
+
commit(fieldToWire(field, s));
|
|
421
|
+
}}
|
|
422
|
+
/>
|
|
423
|
+
);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Re-exported so form surfaces (and apps) reach the value-shape helpers from
|
|
427
|
+
// the registry module they already import.
|
|
428
|
+
export { emptyFieldValue, fieldPrecision, fieldToWire, wireToField };
|