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,329 @@
|
|
|
1
|
+
import { mojoList, mojoSave } from './client';
|
|
2
|
+
|
|
3
|
+
export type RecordFeedId = number | string;
|
|
4
|
+
export type RecordFeedTimestamp = number | string;
|
|
5
|
+
export type RecordFeedAdapterKind = 'ticket-note' | 'incident-history';
|
|
6
|
+
|
|
7
|
+
export interface FileReference {
|
|
8
|
+
id: number;
|
|
9
|
+
filename: string;
|
|
10
|
+
content_type: string;
|
|
11
|
+
category: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RecordFeedNoteDraft {
|
|
15
|
+
text: string;
|
|
16
|
+
attachment?: FileReference;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RecordFeedAuthor {
|
|
20
|
+
id: RecordFeedId | null;
|
|
21
|
+
name: string;
|
|
22
|
+
avatarUrl?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RecordFeedItemBase {
|
|
26
|
+
id: RecordFeedId;
|
|
27
|
+
created: RecordFeedTimestamp;
|
|
28
|
+
content: string;
|
|
29
|
+
author: RecordFeedAuthor;
|
|
30
|
+
metadata: Record<string, unknown>;
|
|
31
|
+
/** Wire-shaped django-mojo row; sensitive-domain adapters may sanitize it. */
|
|
32
|
+
raw: Record<string, unknown>;
|
|
33
|
+
attachment?: FileReference | null;
|
|
34
|
+
/** Client-only marker used by adapter-mode optimistic rows. */
|
|
35
|
+
pending?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RecordFeedComment extends RecordFeedItemBase {
|
|
39
|
+
kind: 'comment';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RecordFeedAssistantNote extends RecordFeedItemBase {
|
|
43
|
+
kind: 'assistant';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RecordFeedSystemEvent extends RecordFeedItemBase {
|
|
47
|
+
kind: 'system';
|
|
48
|
+
event?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RecordFeedStatusEvent extends RecordFeedItemBase {
|
|
52
|
+
kind: 'status';
|
|
53
|
+
from: string | null;
|
|
54
|
+
to: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type RecordFeedItem =
|
|
58
|
+
| RecordFeedComment
|
|
59
|
+
| RecordFeedAssistantNote
|
|
60
|
+
| RecordFeedSystemEvent
|
|
61
|
+
| RecordFeedStatusEvent;
|
|
62
|
+
|
|
63
|
+
export interface RecordFeedPage {
|
|
64
|
+
/** Chronological: oldest of the latest window first. */
|
|
65
|
+
items: RecordFeedItem[];
|
|
66
|
+
count: number;
|
|
67
|
+
hasEarlier: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type RecordFeedQueryKey = readonly [
|
|
71
|
+
'record-feed',
|
|
72
|
+
RecordFeedAdapterKind,
|
|
73
|
+
RecordFeedId,
|
|
74
|
+
RecordFeedId | null,
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export interface RecordFeedAdapter {
|
|
78
|
+
kind: RecordFeedAdapterKind;
|
|
79
|
+
parentId: RecordFeedId;
|
|
80
|
+
groupId: RecordFeedId | null;
|
|
81
|
+
/** Stable, structural and record-scoped. Safe for exact Query operations. */
|
|
82
|
+
queryKey: RecordFeedQueryKey;
|
|
83
|
+
fetch(): Promise<RecordFeedPage>;
|
|
84
|
+
addNote(draft: RecordFeedNoteDraft): Promise<RecordFeedItem>;
|
|
85
|
+
/** Sanitizes optimistic cache and mutation variables for sensitive domains. */
|
|
86
|
+
sanitizeDraft?(text: string): string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface FeedAdapterOptions {
|
|
90
|
+
groupId?: RecordFeedId | null;
|
|
91
|
+
sanitizeRow?: (row: Record<string, unknown>) => Record<string, unknown>;
|
|
92
|
+
sanitizeText?: (text: string) => string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface WireFeedUser {
|
|
96
|
+
id?: RecordFeedId | null;
|
|
97
|
+
display_name?: unknown;
|
|
98
|
+
username?: unknown;
|
|
99
|
+
email?: unknown;
|
|
100
|
+
avatar?: unknown;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface WireFeedRow extends Record<string, unknown> {
|
|
104
|
+
id?: unknown;
|
|
105
|
+
created?: unknown;
|
|
106
|
+
user?: unknown;
|
|
107
|
+
note?: unknown;
|
|
108
|
+
metadata?: unknown;
|
|
109
|
+
kind?: unknown;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function objectOrEmpty(value: unknown): Record<string, unknown> {
|
|
113
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
114
|
+
? value as Record<string, unknown>
|
|
115
|
+
: {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function safeFileReference(value: unknown): FileReference | null {
|
|
119
|
+
const raw = objectOrEmpty(value);
|
|
120
|
+
const id = raw.id;
|
|
121
|
+
if (typeof id !== 'number' || !Number.isSafeInteger(id) || id <= 0
|
|
122
|
+
|| typeof raw.filename !== 'string' || !raw.filename
|
|
123
|
+
|| typeof raw.content_type !== 'string' || !raw.content_type
|
|
124
|
+
|| (raw.category !== null && raw.category !== undefined && typeof raw.category !== 'string')) return null;
|
|
125
|
+
return { id, filename: raw.filename, content_type: raw.content_type, category: raw.category == null ? null : raw.category };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Rebuild media before any domain sanitizer, normalizer, or cache sees it. */
|
|
129
|
+
export function safeRecordFeedRow(raw: Record<string, unknown>): Record<string, unknown> {
|
|
130
|
+
return { ...raw, media: safeFileReference(raw.media) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function optionalText(value: unknown): string | null {
|
|
134
|
+
if (value === null || value === undefined || value === '') return null;
|
|
135
|
+
return String(value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function avatarUrl(value: unknown): string | undefined {
|
|
139
|
+
if (typeof value === 'string' && value) return value;
|
|
140
|
+
const avatar = objectOrEmpty(value);
|
|
141
|
+
return typeof avatar.url === 'string' && avatar.url ? avatar.url : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeAuthor(value: unknown, fallback: string): RecordFeedAuthor {
|
|
145
|
+
const user = objectOrEmpty(value) as WireFeedUser;
|
|
146
|
+
const name = optionalText(user.display_name)
|
|
147
|
+
?? optionalText(user.username)
|
|
148
|
+
?? optionalText(user.email)
|
|
149
|
+
?? fallback;
|
|
150
|
+
return {
|
|
151
|
+
id: typeof user.id === 'number' || typeof user.id === 'string' ? user.id : null,
|
|
152
|
+
name,
|
|
153
|
+
avatarUrl: avatarUrl(user.avatar),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function structuredStatus(metadata: Record<string, unknown>): { from: string | null; to: string | null } | null {
|
|
158
|
+
const nested = objectOrEmpty(metadata.status_change);
|
|
159
|
+
const isStatus = metadata.type === 'status_change' || Object.keys(nested).length > 0;
|
|
160
|
+
if (!isStatus) return null;
|
|
161
|
+
const from = optionalText(metadata.old_status ?? nested.old_status ?? nested.from);
|
|
162
|
+
const to = optionalText(metadata.new_status ?? nested.new_status ?? nested.to);
|
|
163
|
+
// A type marker without either structured endpoint is not enough. In
|
|
164
|
+
// particular, never recover a transition by regexing the prose note.
|
|
165
|
+
return from !== null || to !== null ? { from, to } : null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isCanonicalAssistant(row: WireFeedRow, metadata: Record<string, unknown>, content: string): boolean {
|
|
169
|
+
if (content.startsWith('[LLM Agent]')) return true;
|
|
170
|
+
if (row.kind === 'handler:llm') return true;
|
|
171
|
+
if (metadata.handler === 'llm' || metadata.origin === 'llm') return true;
|
|
172
|
+
const action = objectOrEmpty(metadata.action);
|
|
173
|
+
return action.handler === 'llm';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isLegacyAssistant(row: WireFeedRow, metadata: Record<string, unknown>): boolean {
|
|
177
|
+
// These older ticket records encoded assistant context without a handler.
|
|
178
|
+
// Only null-user rows qualify; null user on its own is always System.
|
|
179
|
+
if (row.user !== null && row.user !== undefined) return false;
|
|
180
|
+
const action = objectOrEmpty(metadata.action);
|
|
181
|
+
if (Object.keys(action).length > 0) return true;
|
|
182
|
+
return metadata.type === 'context' && metadata.references !== null && metadata.references !== undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Normalize ticket notes and incident history through one strict precedence:
|
|
187
|
+
* structured status → canonical LLM → legacy null-user LLM → user → system.
|
|
188
|
+
*/
|
|
189
|
+
export function normalizeRecordFeedItem(raw: Record<string, unknown>): RecordFeedItem {
|
|
190
|
+
const safeRaw = safeRecordFeedRow(raw);
|
|
191
|
+
const row = safeRaw as WireFeedRow;
|
|
192
|
+
const metadata = objectOrEmpty(row.metadata);
|
|
193
|
+
const content = optionalText(row.note) ?? '';
|
|
194
|
+
const id = typeof row.id === 'number' || typeof row.id === 'string'
|
|
195
|
+
? row.id
|
|
196
|
+
: `missing-${optionalText(row.created) ?? 'record'}`;
|
|
197
|
+
const created = typeof row.created === 'number' || typeof row.created === 'string'
|
|
198
|
+
? row.created
|
|
199
|
+
: 0;
|
|
200
|
+
const status = structuredStatus(metadata);
|
|
201
|
+
|
|
202
|
+
if (status) {
|
|
203
|
+
return {
|
|
204
|
+
id, created, content, metadata, raw: safeRaw, attachment: safeFileReference(safeRaw.media),
|
|
205
|
+
kind: 'status', from: status.from, to: status.to,
|
|
206
|
+
author: normalizeAuthor(row.user, 'System'),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (isCanonicalAssistant(row, metadata, content) || isLegacyAssistant(row, metadata)) {
|
|
211
|
+
return {
|
|
212
|
+
id, created, content, metadata, raw: safeRaw, attachment: safeFileReference(safeRaw.media),
|
|
213
|
+
kind: 'assistant',
|
|
214
|
+
author: normalizeAuthor(row.user, 'AI Agent'),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (row.user !== null && row.user !== undefined) {
|
|
219
|
+
return {
|
|
220
|
+
id, created, content, metadata, raw: safeRaw, attachment: safeFileReference(safeRaw.media),
|
|
221
|
+
kind: 'comment',
|
|
222
|
+
author: normalizeAuthor(row.user, 'Unknown user'),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
id, created, content, metadata, raw: safeRaw, attachment: safeFileReference(safeRaw.media),
|
|
228
|
+
kind: 'system', event: optionalText(row.kind) ?? optionalText(metadata.type) ?? undefined,
|
|
229
|
+
author: normalizeAuthor(null, 'System'),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function recordFeedQueryKey(
|
|
234
|
+
kind: RecordFeedAdapterKind,
|
|
235
|
+
parentId: RecordFeedId,
|
|
236
|
+
groupId: RecordFeedId | null = null,
|
|
237
|
+
): RecordFeedQueryKey {
|
|
238
|
+
return ['record-feed', kind, parentId, groupId] as const;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
interface AdapterConfig {
|
|
242
|
+
kind: RecordFeedAdapterKind;
|
|
243
|
+
endpoint: string;
|
|
244
|
+
parentId: RecordFeedId;
|
|
245
|
+
groupId: RecordFeedId | null;
|
|
246
|
+
postKind?: string;
|
|
247
|
+
sanitizeRow?: (row: Record<string, unknown>) => Record<string, unknown>;
|
|
248
|
+
sanitizeText?: (text: string) => string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function makeAdapter(config: AdapterConfig): RecordFeedAdapter {
|
|
252
|
+
const { kind, endpoint, parentId, groupId, postKind, sanitizeRow, sanitizeText } = config;
|
|
253
|
+
const queryKey = recordFeedQueryKey(kind, parentId, groupId);
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
kind,
|
|
257
|
+
parentId,
|
|
258
|
+
groupId,
|
|
259
|
+
queryKey,
|
|
260
|
+
sanitizeDraft: (text) => sanitizeText ? sanitizeText(text) : text,
|
|
261
|
+
async fetch() {
|
|
262
|
+
const page = await mojoList<Record<string, unknown>>(endpoint, {
|
|
263
|
+
parent: parentId,
|
|
264
|
+
...(groupId === null ? {} : { group: groupId }),
|
|
265
|
+
graph: 'default',
|
|
266
|
+
sort: '-created',
|
|
267
|
+
start: 0,
|
|
268
|
+
size: 100,
|
|
269
|
+
});
|
|
270
|
+
const newestFirst = page.rows.map((row) => {
|
|
271
|
+
const rebuilt = safeRecordFeedRow(row);
|
|
272
|
+
return normalizeRecordFeedItem(sanitizeRow ? sanitizeRow(rebuilt) : rebuilt);
|
|
273
|
+
});
|
|
274
|
+
return {
|
|
275
|
+
items: newestFirst.reverse(),
|
|
276
|
+
count: page.count,
|
|
277
|
+
hasEarlier: page.count > page.rows.length,
|
|
278
|
+
};
|
|
279
|
+
},
|
|
280
|
+
async addNote(draft) {
|
|
281
|
+
const safeText = sanitizeText ? sanitizeText(draft.text) : draft.text;
|
|
282
|
+
const raw = await mojoSave<Record<string, unknown>>(endpoint, null, {
|
|
283
|
+
parent: parentId,
|
|
284
|
+
// django-mojo's generic REST saver consumes ForeignKeys as a
|
|
285
|
+
// bare primary key (`{"group": 5}`), permission-checks the
|
|
286
|
+
// related row, then assigns it. Keep create scope identical
|
|
287
|
+
// to the group-aware list/cache scope.
|
|
288
|
+
...(groupId === null ? {} : { group: groupId }),
|
|
289
|
+
note: safeText,
|
|
290
|
+
...(draft.attachment ? { media: draft.attachment.id } : {}),
|
|
291
|
+
...(postKind ? { kind: postKind } : {}),
|
|
292
|
+
});
|
|
293
|
+
const rebuilt = safeRecordFeedRow(raw);
|
|
294
|
+
const safeRaw = sanitizeRow ? sanitizeRow(rebuilt) : rebuilt;
|
|
295
|
+
return normalizeRecordFeedItem(safeRaw);
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Ticket comments and ticket status-change records. */
|
|
301
|
+
export function createTicketNoteAdapter(
|
|
302
|
+
parentId: RecordFeedId,
|
|
303
|
+
options: FeedAdapterOptions = {},
|
|
304
|
+
): RecordFeedAdapter {
|
|
305
|
+
return makeAdapter({
|
|
306
|
+
kind: 'ticket-note',
|
|
307
|
+
endpoint: '/api/incident/ticket/note',
|
|
308
|
+
parentId,
|
|
309
|
+
groupId: options.groupId ?? null,
|
|
310
|
+
sanitizeRow: options.sanitizeRow,
|
|
311
|
+
sanitizeText: options.sanitizeText,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Incident comments and automated incident history. */
|
|
316
|
+
export function createIncidentHistoryAdapter(
|
|
317
|
+
parentId: RecordFeedId,
|
|
318
|
+
options: FeedAdapterOptions = {},
|
|
319
|
+
): RecordFeedAdapter {
|
|
320
|
+
return makeAdapter({
|
|
321
|
+
kind: 'incident-history',
|
|
322
|
+
endpoint: '/api/incident/incident/history',
|
|
323
|
+
parentId,
|
|
324
|
+
groupId: options.groupId ?? null,
|
|
325
|
+
postKind: 'comment',
|
|
326
|
+
sanitizeRow: options.sanitizeRow,
|
|
327
|
+
sanitizeText: options.sanitizeText,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { downloadBlob, mojoList } from './client';
|
|
2
|
+
import type { Params } from './types';
|
|
3
|
+
|
|
4
|
+
export interface SafeExportField<T> {
|
|
5
|
+
key: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
value?: (row: T) => unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SafeExporterOptions<T> {
|
|
11
|
+
endpoint: string;
|
|
12
|
+
filename: string;
|
|
13
|
+
fields: readonly SafeExportField<T>[];
|
|
14
|
+
sanitizeRow: (row: T) => T;
|
|
15
|
+
pageSize?: number;
|
|
16
|
+
maxRows?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function scalar(value: unknown): string | number | boolean | null {
|
|
20
|
+
if (value == null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value ?? null;
|
|
21
|
+
return JSON.stringify(value) ?? String(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function csvCell(value: unknown): string {
|
|
25
|
+
const text = String(value ?? '');
|
|
26
|
+
return /[",\r\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build a bounded, client-side exporter for domains whose server formats
|
|
31
|
+
* expose unsafe fields. Rows are sanitized before they are accumulated and
|
|
32
|
+
* only the declared projection can reach the resulting file.
|
|
33
|
+
*/
|
|
34
|
+
export function createSafeExporter<T>(options: SafeExporterOptions<T>) {
|
|
35
|
+
const pageSize = Math.min(250, Math.max(1, options.pageSize ?? 100));
|
|
36
|
+
const maxRows = Math.max(pageSize, options.maxRows ?? 10_000);
|
|
37
|
+
return async (format: 'csv' | 'json', normalizedParams: Params): Promise<void> => {
|
|
38
|
+
const base: Params = { ...normalizedParams };
|
|
39
|
+
for (const key of ['start', 'size', 'page', 'download_format', 'filename']) delete base[key];
|
|
40
|
+
const projected: Record<string, string | number | boolean | null>[] = [];
|
|
41
|
+
let start = 0;
|
|
42
|
+
let count = Infinity;
|
|
43
|
+
while (start < count && projected.length < maxRows) {
|
|
44
|
+
const page = await mojoList<T>(options.endpoint, { ...base, start, size: pageSize });
|
|
45
|
+
count = page.count;
|
|
46
|
+
for (const wireRow of page.rows) {
|
|
47
|
+
const row = options.sanitizeRow(wireRow);
|
|
48
|
+
const record: Record<string, string | number | boolean | null> = {};
|
|
49
|
+
for (const field of options.fields) {
|
|
50
|
+
const value = field.value
|
|
51
|
+
? field.value(row)
|
|
52
|
+
: (row as unknown as Record<string, unknown>)[field.key];
|
|
53
|
+
record[field.label ?? field.key] = scalar(value);
|
|
54
|
+
}
|
|
55
|
+
projected.push(record);
|
|
56
|
+
if (projected.length >= maxRows) break;
|
|
57
|
+
}
|
|
58
|
+
if (!page.rows.length) break;
|
|
59
|
+
start += page.rows.length;
|
|
60
|
+
}
|
|
61
|
+
if (count > maxRows) throw new Error(`Export exceeds the ${maxRows.toLocaleString()} row safety limit; narrow the filters and try again.`);
|
|
62
|
+
const filename = `${options.filename}.${format}`;
|
|
63
|
+
if (format === 'json') {
|
|
64
|
+
downloadBlob(new Blob([JSON.stringify(projected, null, 2)], { type: 'application/json' }), filename);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const headers = options.fields.map((field) => field.label ?? field.key);
|
|
68
|
+
const body = [headers.map(csvCell).join(','), ...projected.map((row) => headers.map((key) => csvCell(row[key])).join(','))].join('\r\n');
|
|
69
|
+
downloadBlob(new Blob([body], { type: 'text/csv;charset=utf-8' }), filename);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// Shared types for the mojo portal baseline.
|
|
2
|
+
|
|
3
|
+
// Type-only — erased at compile time; zod ships as a portal-mojo dependency
|
|
4
|
+
// (the single sanctioned dependency, added with B3 FormView validation).
|
|
5
|
+
import type { ZodType } from 'zod';
|
|
6
|
+
|
|
7
|
+
/** django-mojo list response body (inside the envelope): rows + paging meta. */
|
|
8
|
+
export interface MojoList<T> {
|
|
9
|
+
rows: T[];
|
|
10
|
+
count: number;
|
|
11
|
+
start: number;
|
|
12
|
+
size: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Wire params: everything the server understands is a flat query dict. */
|
|
16
|
+
export type Params = Record<string, string | number | boolean | null | undefined>;
|
|
17
|
+
|
|
18
|
+
// ── SchemaForm field language (data side) ─────────────────────────────
|
|
19
|
+
// The TYPES live here because model definitions (defineModel) CARRY form
|
|
20
|
+
// configs as plain data — web-mojo's User.ADD_FORM/EDIT_FORM heritage.
|
|
21
|
+
// Rendering stays in portal-mojo/ui (SchemaForm/formModal re-export these).
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One value as a controlled form input holds it (server rows may carry
|
|
25
|
+
* numbers/null). Arrays joined B4 (#1278) for the list-valued registry types
|
|
26
|
+
* (multiselect ids, collection-multiselect ids, daterange pairs) — they ride
|
|
27
|
+
* the JSON save body as real lists.
|
|
28
|
+
*/
|
|
29
|
+
export type FieldValue = string | number | boolean | null | Array<string | number>;
|
|
30
|
+
export type FieldValues = Record<string, FieldValue>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Declarative conditional visibility — FormBuilder.js:863-888 semantics:
|
|
34
|
+
* the controlling field's value is String()-coerced and matched against the
|
|
35
|
+
* allowed list; `negate` flips the result.
|
|
36
|
+
*/
|
|
37
|
+
export interface ShowWhenRule {
|
|
38
|
+
/** Controlling field name (dotted names allowed, e.g. 'metadata.kind'). */
|
|
39
|
+
field: string;
|
|
40
|
+
value: FieldValue | FieldValue[];
|
|
41
|
+
negate?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** Either the declarative rule or a predicate over the live form values. */
|
|
44
|
+
export type ShowWhen = ShowWhenRule | ((values: FieldValues) => boolean);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Field types. The first row are the BUILTINS SchemaForm/FormView render
|
|
48
|
+
* inline; everything after resolves through the ui field-type REGISTRY
|
|
49
|
+
* (ui/field-registry — web-mojo INPUT_TYPES + FormBuilder aliases carried
|
|
50
|
+
* verbatim). `(string & {})` keeps the union open for app-registered types
|
|
51
|
+
* (registerFieldType) without losing autocompletion on the known names.
|
|
52
|
+
* An unknown name renders a text input WITH one console.warn — never nothing.
|
|
53
|
+
*/
|
|
54
|
+
export type FieldType =
|
|
55
|
+
| 'text' | 'email' | 'tel' | 'select' | 'switch' | 'textarea'
|
|
56
|
+
| 'file' | 'image'
|
|
57
|
+
| 'tag' | 'tags'
|
|
58
|
+
| 'multiselect'
|
|
59
|
+
| 'collection' | 'collectionmultiselect' | 'collection-multiselect'
|
|
60
|
+
| 'combo' | 'combobox' | 'autocomplete'
|
|
61
|
+
| 'address'
|
|
62
|
+
| 'datepicker' | 'monthpicker' | 'yearpicker'
|
|
63
|
+
| 'daterange' | 'monthrange' | 'yearrange'
|
|
64
|
+
| 'timepicker' | 'datetimepicker' | 'timezone'
|
|
65
|
+
| (string & {});
|
|
66
|
+
|
|
67
|
+
/** A select/multiselect/combo option as Field data carries it. */
|
|
68
|
+
export interface FieldOption {
|
|
69
|
+
value: string | number;
|
|
70
|
+
label: string;
|
|
71
|
+
/** ComboBox: muted second line in the dropdown. */
|
|
72
|
+
description?: string;
|
|
73
|
+
/** MultiSelectDropdown: row refuses to toggle. */
|
|
74
|
+
disabled?: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Structural image-editor options kept client-side so model field data never imports ui. */
|
|
78
|
+
export interface FieldImageEditOptions {
|
|
79
|
+
filename?: string;
|
|
80
|
+
title?: string;
|
|
81
|
+
size?: 'sm' | 'md' | 'lg';
|
|
82
|
+
startMode?: string;
|
|
83
|
+
modes?: readonly string[];
|
|
84
|
+
maxHistory?: number;
|
|
85
|
+
saveText?: string;
|
|
86
|
+
crop?: {
|
|
87
|
+
aspectRatio?: number | null;
|
|
88
|
+
minCropSize?: number;
|
|
89
|
+
fixedCropSize?: { width: number; height: number } | null;
|
|
90
|
+
cropAndScale?: { width: number; height: number } | null;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Structural mirrors of ui/date/PresetRail's preset types (client code must
|
|
95
|
+
// not import ui). String-returning `range()` (canonical YYYY-MM-DD strings)
|
|
96
|
+
// is assignable to the rail's ParsedDate|string contract by covariance.
|
|
97
|
+
export interface FieldPreset { label: string; range: () => { start: string; end: string }; divider?: undefined }
|
|
98
|
+
export interface FieldPresetDivider { divider: true; label?: undefined; range?: undefined }
|
|
99
|
+
/** 'default' picks the precision's built-in quick-range list. */
|
|
100
|
+
export type FieldPresets = 'default' | true | Array<FieldPreset | FieldPresetDivider>;
|
|
101
|
+
|
|
102
|
+
export interface Field {
|
|
103
|
+
/**
|
|
104
|
+
* Field name — the wire key. Dotted names ('permissions.manage_users')
|
|
105
|
+
* read/write nested dict values; FormView expands them to partial dicts
|
|
106
|
+
* on save (django-mojo MERGES dict bodies into JSONFields — rest.py
|
|
107
|
+
* on_rest_update_jsonfield).
|
|
108
|
+
*/
|
|
109
|
+
name: string;
|
|
110
|
+
type: FieldType;
|
|
111
|
+
label: string;
|
|
112
|
+
placeholder?: string;
|
|
113
|
+
required?: boolean;
|
|
114
|
+
help?: string;
|
|
115
|
+
columns?: 6 | 12;
|
|
116
|
+
options?: FieldOption[];
|
|
117
|
+
/** Conditional visibility. Hidden fields never submit/save and their errors clear. */
|
|
118
|
+
showWhen?: ShowWhen;
|
|
119
|
+
/**
|
|
120
|
+
* Per-field zod schema, validated against the COMMITTED input value
|
|
121
|
+
* (string for text-ish types, boolean for switch). A failing parse blocks
|
|
122
|
+
* the save of that field only; the first issue's message shows in the
|
|
123
|
+
* field's error slot. Server-side validation stays authoritative.
|
|
124
|
+
*/
|
|
125
|
+
schema?: ZodType;
|
|
126
|
+
/** Locks the control (registry types; builtins ignore it today). */
|
|
127
|
+
disabled?: boolean;
|
|
128
|
+
|
|
129
|
+
// file / image — one completed django-mojo File relation id or null.
|
|
130
|
+
/** Picker accept grammar (MIME values, wildcards, or .extensions). */
|
|
131
|
+
accept?: string | string[];
|
|
132
|
+
/** Local selection guard. Server policy remains authoritative. */
|
|
133
|
+
maxFileSize?: number;
|
|
134
|
+
/** Explicit upload destination. Omitted keys use the caller's personal scope. */
|
|
135
|
+
uploadDestination?: { fileManagerId?: number; groupId?: number; use?: string };
|
|
136
|
+
/** Called once when a completed upload is abandoned without authoritative attachment. */
|
|
137
|
+
onUploadOrphan?: (fileId: number) => void;
|
|
138
|
+
/** Image-only pre-upload edit. Omitted/false preserves the direct upload path. */
|
|
139
|
+
imageEdit?: boolean | FieldImageEditOptions;
|
|
140
|
+
/** Require an edited result; Cancel retains the local selection for retry/discard. */
|
|
141
|
+
imageEditRequired?: boolean;
|
|
142
|
+
|
|
143
|
+
// ── Registry-type props (B4 #1278) — see docs/forms.md value table ──
|
|
144
|
+
/** date/range pickers: grid precision. EXPLICIT value beats the
|
|
145
|
+
* monthpicker/yearpicker/monthrange/yearrange alias mapping. */
|
|
146
|
+
precision?: 'day' | 'month' | 'year';
|
|
147
|
+
/**
|
|
148
|
+
* Wire shape override at the field-wire boundary (date/datetime types):
|
|
149
|
+
* 'epoch' (default — django-mojo datetimes are epoch SECONDS) or 'date'
|
|
150
|
+
* (canonical YYYY-MM-DD string — DateFields serialize OUT this way).
|
|
151
|
+
* timepicker reuses the prop for its serialization: 'iso' (default)
|
|
152
|
+
* or 'iana'.
|
|
153
|
+
*/
|
|
154
|
+
outputFormat?: 'epoch' | 'date' | 'iso' | 'iana';
|
|
155
|
+
/** date/range pickers: trigger display tokens (YYYY MMM DD …). */
|
|
156
|
+
displayFormat?: string;
|
|
157
|
+
/** Bounds: canonical date strings for pickers, time strings for timepicker. */
|
|
158
|
+
min?: string | number;
|
|
159
|
+
max?: string | number;
|
|
160
|
+
/** daterange: the quick-range rail. */
|
|
161
|
+
presets?: FieldPresets;
|
|
162
|
+
/** daterange, day precision: 1 or 2 panes (default 2). */
|
|
163
|
+
months?: 1 | 2;
|
|
164
|
+
/** tag: the CSV joiner (one char). daterange: the display separator. */
|
|
165
|
+
separator?: string;
|
|
166
|
+
/** timepicker/datetimepicker clock: '24h' (default) | '12h'. */
|
|
167
|
+
timeFormat?: '12h' | '24h';
|
|
168
|
+
/** timepicker: minute stepping increment. */
|
|
169
|
+
step?: number;
|
|
170
|
+
/** timepicker/datetimepicker: enable the zone picker (an array is also the list). */
|
|
171
|
+
timezone?: boolean | string[];
|
|
172
|
+
/** Zone list for timezone/timepicker types (overrides the engine list). */
|
|
173
|
+
timezones?: string[];
|
|
174
|
+
|
|
175
|
+
// collection / collectionmultiselect — the server-backed pickers.
|
|
176
|
+
/** Any defineModel definition (structurally: carries the endpoint). */
|
|
177
|
+
model?: { endpoint: string };
|
|
178
|
+
/** Bare endpoint when no model definition exists. */
|
|
179
|
+
endpoint?: string;
|
|
180
|
+
/** Row field shown as the label (dot paths ok). Default 'name'. */
|
|
181
|
+
labelField?: string;
|
|
182
|
+
/** Row field used as the id (dot paths ok). Default 'id'. */
|
|
183
|
+
valueField?: string;
|
|
184
|
+
/** collection: rows fetched per search (`size` wire param). */
|
|
185
|
+
maxItems?: number;
|
|
186
|
+
/** collection: fetch the unsearched first page on open. Default true. */
|
|
187
|
+
emptyFetch?: boolean;
|
|
188
|
+
/** Search debounce ms (collection types). */
|
|
189
|
+
debounceMs?: number;
|
|
190
|
+
/** Fold the active group in as the `group` wire param (fetch HELD without one). */
|
|
191
|
+
requiresActiveGroup?: boolean;
|
|
192
|
+
/** Extra wire params merged into every picker fetch. */
|
|
193
|
+
defaultParams?: Params | (() => Params | null | undefined);
|
|
194
|
+
/** collectionmultiselect: search input above the list. */
|
|
195
|
+
enableSearch?: boolean;
|
|
196
|
+
|
|
197
|
+
// tag / combo extras.
|
|
198
|
+
maxTags?: number;
|
|
199
|
+
allowDuplicates?: boolean;
|
|
200
|
+
/** combo: typed text not matching an option commits as-is. Default true. */
|
|
201
|
+
allowCustom?: boolean;
|
|
202
|
+
/** combo: render option descriptions. */
|
|
203
|
+
showDescription?: boolean;
|
|
204
|
+
/** combo/timezone: cap on rendered suggestions. */
|
|
205
|
+
maxSuggestions?: number;
|
|
206
|
+
|
|
207
|
+
// address — provider details → declared form fields. address1 defaults
|
|
208
|
+
// to this field's own name; every other destination is opt-in.
|
|
209
|
+
addressFields?: Partial<Record<
|
|
210
|
+
'address1' | 'address2' | 'city' | 'state' | 'state_code' |
|
|
211
|
+
'postal_code' | 'country' | 'country_code' | 'latitude' |
|
|
212
|
+
'longitude' | 'formatted_address' | 'place_id', string
|
|
213
|
+
>>;
|
|
214
|
+
/** ISO country constraint for provider suggestions. Default US. */
|
|
215
|
+
country?: string;
|
|
216
|
+
/** Minimum characters before address suggestions. Default 3. */
|
|
217
|
+
minChars?: number;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* SchemaForm's value map. Widened with B4 (#1278) from string|boolean to the
|
|
222
|
+
* full FieldValue union — registry types put lists (multiselect ids,
|
|
223
|
+
* daterange pairs), numbers (epoch dates, collection ids) and null (cleared
|
|
224
|
+
* pickers) in form state, and the submit payload carries them verbatim.
|
|
225
|
+
*/
|
|
226
|
+
export type FormData = Record<string, FieldValue>;
|
|
227
|
+
|
|
228
|
+
/** A named model form — exactly what ui's formModal renders. */
|
|
229
|
+
export interface ModelForm {
|
|
230
|
+
title: string;
|
|
231
|
+
fields: Field[];
|
|
232
|
+
submitText?: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* django-mojo datetimes serialize as EPOCH SECONDS (measured against a live
|
|
237
|
+
* server, not assumed) — fmt.date/relative accept them directly.
|
|
238
|
+
*/
|
|
239
|
+
export type MojoTimestamp = number | null;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The default-graph user row exactly as /api/user serializes it (verified
|
|
243
|
+
* live against mverify 2026-08-04). Note what is NOT here: no `role`, no
|
|
244
|
+
* `created`, no passkey/MFA fields (those ride only the `me` graph).
|
|
245
|
+
*/
|
|
246
|
+
export interface User {
|
|
247
|
+
id: number;
|
|
248
|
+
display_name: string | null; // null on real rows (system/deleted accounts)
|
|
249
|
+
username: string;
|
|
250
|
+
email: string;
|
|
251
|
+
phone_number: string | null;
|
|
252
|
+
first_name?: string;
|
|
253
|
+
last_name?: string;
|
|
254
|
+
is_active: boolean;
|
|
255
|
+
is_superuser: boolean;
|
|
256
|
+
is_email_verified: boolean;
|
|
257
|
+
is_phone_verified: boolean;
|
|
258
|
+
is_dob_verified?: boolean;
|
|
259
|
+
is_online: boolean;
|
|
260
|
+
last_login: MojoTimestamp;
|
|
261
|
+
last_activity: MojoTimestamp;
|
|
262
|
+
permissions: Record<string, unknown>;
|
|
263
|
+
metadata: Record<string, unknown>;
|
|
264
|
+
dob?: string | null;
|
|
265
|
+
/** Capability-free relation shape retained in Query caches. */
|
|
266
|
+
avatar?: { id: number } | null;
|
|
267
|
+
org?: { id: number; name: string } | number | null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Strip avatar URLs/thumbnails/capabilities before a User-shaped row enters a cache. */
|
|
271
|
+
export function sanitizeAvatarRelation(value: unknown): { id: number } | null {
|
|
272
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return { id: value };
|
|
273
|
+
if (value != null && typeof value === 'object' && !Array.isArray(value)) {
|
|
274
|
+
const id = (value as Record<string, unknown>).id;
|
|
275
|
+
if (typeof id === 'number' && Number.isSafeInteger(id) && id > 0) return { id };
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|