@stackcraftmx/calcita 0.23.0 → 0.26.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/README.md +127 -22
- package/cli/projectFiles.js +5 -3
- package/cli/templates/project/README.md.tpl +28 -0
- package/cli/templates/project/public/manifest.webmanifest.tpl +6 -6
- package/cli/templates/project/src/app/AppProviders.jsx.tpl +9 -4
- package/cli/templates/project/src/app/appRouter.jsx.tpl +4 -2
- package/cli/templates/project/src/app/appSettings.js.tpl +14 -0
- package/cli/templates/project/src/core/AppLogo.jsx.tpl +3 -1
- package/cli/templates/project/src/core/index.js.tpl +1 -0
- package/cli/templates/project/src/core/publicPaths.js.tpl +24 -0
- package/cli/templates/project/vite.config.js.tpl +27 -1
- package/dist/DataTable/AuditTrailTable.js +5 -3
- package/dist/DataTable/AuditTrailTable.js.map +1 -1
- package/dist/DataTable/ColumnFilters/MultiSelectColumnFilter.js +2 -1
- package/dist/DataTable/ColumnFilters/MultiSelectColumnFilter.js.map +1 -1
- package/dist/DataTable/EmptyState.js +2 -2
- package/dist/DataTable/EmptyState.js.map +1 -1
- package/dist/DataTable/FilterChips.js +2 -2
- package/dist/DataTable/FilterChips.js.map +1 -1
- package/dist/DataTable/GoToPage.js +4 -3
- package/dist/DataTable/GoToPage.js.map +1 -1
- package/dist/DataTable/ServerTable.js +153 -83
- package/dist/DataTable/ServerTable.js.map +1 -1
- package/dist/DataTable/rowSelection.js +61 -0
- package/dist/DataTable/rowSelection.js.map +1 -0
- package/dist/access/PermissionMatrix.js +3 -2
- package/dist/access/PermissionMatrix.js.map +1 -1
- package/dist/account/AccountSecuritySection.js +2 -2
- package/dist/account/AccountSecuritySection.js.map +1 -1
- package/dist/account/ForgotPasswordScreen.js +5 -4
- package/dist/account/ForgotPasswordScreen.js.map +1 -1
- package/dist/account/ProfileScreen.js +39 -13
- package/dist/account/ProfileScreen.js.map +1 -1
- package/dist/account/ResetPasswordScreen.js +3 -2
- package/dist/account/ResetPasswordScreen.js.map +1 -1
- package/dist/account/SignInScreen.js +7 -6
- package/dist/account/SignInScreen.js.map +1 -1
- package/dist/account/accountConnector.js.map +1 -1
- package/dist/account/restAccountConnector.js +2 -1
- package/dist/account/restAccountConnector.js.map +1 -1
- package/dist/adapters/reactRouterRoutes.js +7 -6
- package/dist/adapters/reactRouterRoutes.js.map +1 -1
- package/dist/api/createApiClient.js +40 -5
- package/dist/api/createApiClient.js.map +1 -1
- package/dist/appShell/AccountMenu.js +15 -23
- package/dist/appShell/AccountMenu.js.map +1 -1
- package/dist/appShell/AppLayout.js +42 -15
- package/dist/appShell/AppLayout.js.map +1 -1
- package/dist/appShell/AppearanceSettings.js +29 -6
- package/dist/appShell/AppearanceSettings.js.map +1 -1
- package/dist/appShell/AuthLayout.js +2 -0
- package/dist/appShell/AuthLayout.js.map +1 -1
- package/dist/appShell/BarActionButton.js +3 -3
- package/dist/appShell/BarActionButton.js.map +1 -1
- package/dist/appShell/BarActions.js +19 -8
- package/dist/appShell/BarActions.js.map +1 -1
- package/dist/appShell/BarCapsule.js +19 -4
- package/dist/appShell/BarCapsule.js.map +1 -1
- package/dist/appShell/BarSearch.js +92 -0
- package/dist/appShell/BarSearch.js.map +1 -0
- package/dist/appShell/HeaderTools.js +4 -2
- package/dist/appShell/HeaderTools.js.map +1 -1
- package/dist/appShell/MenuLinks.js +36 -0
- package/dist/appShell/MenuLinks.js.map +1 -0
- package/dist/appShell/MobileTabBar.js +6 -4
- package/dist/appShell/MobileTabBar.js.map +1 -1
- package/dist/appShell/SettingsMenu.js +10 -5
- package/dist/appShell/SettingsMenu.js.map +1 -1
- package/dist/appShell/SideMenu.js +5 -3
- package/dist/appShell/SideMenu.js.map +1 -1
- package/dist/appShell/SideMenuEntry.js +3 -3
- package/dist/appShell/SideMenuEntry.js.map +1 -1
- package/dist/appShell/SubmenuActionsMenu.js +3 -4
- package/dist/appShell/SubmenuActionsMenu.js.map +1 -1
- package/dist/appShell/TopMenu.js +5 -6
- package/dist/appShell/TopMenu.js.map +1 -1
- package/dist/appShell/Wordmark.js +11 -0
- package/dist/appShell/Wordmark.js.map +1 -0
- package/dist/appShell/menuItemText.js +4 -0
- package/dist/appShell/menuItemText.js.map +1 -1
- package/dist/appShell/shellSizes.js +2 -0
- package/dist/appShell/shellSizes.js.map +1 -1
- package/dist/auth/PasswordChecklist.js +2 -2
- package/dist/auth/PasswordChecklist.js.map +1 -1
- package/dist/calendar/EventCalendar.js +16 -5
- package/dist/calendar/EventCalendar.js.map +1 -1
- package/dist/calendar/EventDetails.js +74 -0
- package/dist/calendar/EventDetails.js.map +1 -0
- package/dist/calendar/calendarEvents.js +31 -0
- package/dist/calendar/calendarEvents.js.map +1 -0
- package/dist/calendar.js +8 -1
- package/dist/calendar.js.map +1 -1
- package/dist/charts.js +10 -1
- package/dist/charts.js.map +1 -1
- package/dist/datePickers/DateInput.js +3 -2
- package/dist/datePickers/DateInput.js.map +1 -1
- package/dist/downloads/DownloadManager.js +14 -20
- package/dist/downloads/DownloadManager.js.map +1 -1
- package/dist/downloads/downloadQueue.js +4 -2
- package/dist/downloads/downloadQueue.js.map +1 -1
- package/dist/downloads/downloadsMachine.js +4 -1
- package/dist/downloads/downloadsMachine.js.map +1 -1
- package/dist/downloads/fileTypes.js +22 -0
- package/dist/downloads/fileTypes.js.map +1 -0
- package/dist/feedback/ConfirmDialog.js +3 -2
- package/dist/feedback/ConfirmDialog.js.map +1 -1
- package/dist/feedback/ModalWaiting.js +2 -2
- package/dist/feedback/ModalWaiting.js.map +1 -1
- package/dist/feedback/NewBuildNotice.js +2 -2
- package/dist/feedback/NewBuildNotice.js.map +1 -1
- package/dist/filepond/FileDropzone.js +5 -3
- package/dist/filepond/FileDropzone.js.map +1 -1
- package/dist/formatting.js +171 -143
- package/dist/formatting.js.map +1 -1
- package/dist/forms/FormWizard.js +18 -45
- package/dist/forms/FormWizard.js.map +1 -1
- package/dist/forms/auth/ChangePasswordForm.js +5 -4
- package/dist/forms/auth/ChangePasswordForm.js.map +1 -1
- package/dist/forms/auth/ForgotPasswordForm.js +2 -2
- package/dist/forms/auth/ForgotPasswordForm.js.map +1 -1
- package/dist/forms/auth/SignInForm.js +2 -2
- package/dist/forms/auth/SignInForm.js.map +1 -1
- package/dist/forms/security/SecondStepForm.js +2 -2
- package/dist/forms/security/SecondStepForm.js.map +1 -1
- package/dist/forms/security/TwoStepVerificationCard.js +8 -6
- package/dist/forms/security/TwoStepVerificationCard.js.map +1 -1
- package/dist/hookForm/FormWizard.js +59 -0
- package/dist/hookForm/FormWizard.js.map +1 -0
- package/dist/inbox/InboxLayout.js +44 -8
- package/dist/inbox/InboxLayout.js.map +1 -1
- package/dist/inbox/MessageList.js +71 -21
- package/dist/inbox/MessageList.js.map +1 -1
- package/dist/inbox/MessageView.js +80 -29
- package/dist/inbox/MessageView.js.map +1 -1
- package/dist/inbox/inboxDates.js +4 -3
- package/dist/inbox/inboxDates.js.map +1 -1
- package/dist/index.js +163 -136
- package/dist/index.js.map +1 -1
- package/dist/inputs/CatalogInput.js +8 -4
- package/dist/inputs/CatalogInput.js.map +1 -1
- package/dist/inputs/ColorInput.js +3 -2
- package/dist/inputs/ColorInput.js.map +1 -1
- package/dist/inputs/FormSection.js +3 -3
- package/dist/inputs/FormSection.js.map +1 -1
- package/dist/inputs/NumberInput.js +3 -2
- package/dist/inputs/NumberInput.js.map +1 -1
- package/dist/inputs/SelectInput.js +29 -11
- package/dist/inputs/SelectInput.js.map +1 -1
- package/dist/inputs/TextInput.js +6 -2
- package/dist/inputs/TextInput.js.map +1 -1
- package/dist/inputs/WizardLayout.js +54 -0
- package/dist/inputs/WizardLayout.js.map +1 -0
- package/dist/kanban/KanbanBoard.js +239 -14
- package/dist/kanban/KanbanBoard.js.map +1 -1
- package/dist/kanban/kanbanCards.js +19 -0
- package/dist/kanban/kanbanCards.js.map +1 -0
- package/dist/kanban.js +7 -1
- package/dist/kanban.js.map +1 -1
- package/dist/locales/en.js +74 -1
- package/dist/locales/en.js.map +1 -1
- package/dist/locales/es.js +74 -1
- package/dist/locales/es.js.map +1 -1
- package/dist/maps/MapView.js +28 -8
- package/dist/maps/MapView.js.map +1 -1
- package/dist/navigation/appMenu.js +13 -1
- package/dist/navigation/appMenu.js.map +1 -1
- package/dist/observability/logger.js +18 -0
- package/dist/observability/logger.js.map +1 -1
- package/dist/observability/usePublishedBuildWatch.js +4 -3
- package/dist/observability/usePublishedBuildWatch.js.map +1 -1
- package/dist/pwa/InstallAppSetting.js +6 -5
- package/dist/pwa/InstallAppSetting.js.map +1 -1
- package/dist/pwa/InstallInstructionsDialog.js +2 -2
- package/dist/pwa/InstallInstructionsDialog.js.map +1 -1
- package/dist/pwa/InstallPromptBanner.js +15 -9
- package/dist/pwa/InstallPromptBanner.js.map +1 -1
- package/dist/pwa/useInstallPrompt.js +2 -1
- package/dist/pwa/useInstallPrompt.js.map +1 -1
- package/dist/react-hook-form.js +6 -4
- package/dist/react-hook-form.js.map +1 -1
- package/dist/security/ActivityLog.js +4 -3
- package/dist/security/ActivityLog.js.map +1 -1
- package/dist/security/RecoveryCodesDialog.js +4 -3
- package/dist/security/RecoveryCodesDialog.js.map +1 -1
- package/dist/security/SessionsCard.js +8 -6
- package/dist/security/SessionsCard.js.map +1 -1
- package/dist/table.js +2 -0
- package/dist/table.js.map +1 -1
- package/dist/theme/components/MuiAppBar.js +6 -5
- package/dist/theme/components/MuiAppBar.js.map +1 -1
- package/dist/theme/components/MuiDialog.js +13 -0
- package/dist/theme/components/MuiDialog.js.map +1 -0
- package/dist/theme/components/glassComponents.js +32 -31
- package/dist/theme/components/glassComponents.js.map +1 -1
- package/dist/theme/components/minimalComponents.js +8 -4
- package/dist/theme/components/minimalComponents.js.map +1 -1
- package/dist/theme/createToolkitTheme.js.map +1 -1
- package/dist/theme/glassSurface.js +36 -38
- package/dist/theme/glassSurface.js.map +1 -1
- package/dist/theme/lightDarkStyles.js +48 -0
- package/dist/theme/lightDarkStyles.js.map +1 -0
- package/dist/theme/toolkitComponents.js +2 -0
- package/dist/theme/toolkitComponents.js.map +1 -1
- package/dist/toolkit/ComponentTexts.js +13 -0
- package/dist/toolkit/ComponentTexts.js.map +1 -0
- package/dist/toolkit/ToolkitProvider.js +36 -28
- package/dist/toolkit/ToolkitProvider.js.map +1 -1
- package/dist/toolkit/browserNavigation.js +19 -20
- package/dist/toolkit/browserNavigation.js.map +1 -1
- package/dist/toolkit/componentTextsContext.js +15 -0
- package/dist/toolkit/componentTextsContext.js.map +1 -0
- package/dist/toolkit/hashNavigation.js +40 -0
- package/dist/toolkit/hashNavigation.js.map +1 -0
- package/dist/toolkit/navigationPaths.js +49 -0
- package/dist/toolkit/navigationPaths.js.map +1 -0
- package/dist/toolkit/toolkitSettings.js +24 -4
- package/dist/toolkit/toolkitSettings.js.map +1 -1
- package/dist/toolkit/useFormatters.js +12 -0
- package/dist/toolkit/useFormatters.js.map +1 -0
- package/dist/toolkit/useToolkitTranslation.js +11 -7
- package/dist/toolkit/useToolkitTranslation.js.map +1 -1
- package/dist/types/DataTable/AuditTrailTable.d.ts +3 -1
- package/dist/types/DataTable/EmptyState.d.ts +7 -2
- package/dist/types/DataTable/FilterChips.d.ts +3 -1
- package/dist/types/DataTable/GoToPage.d.ts +7 -2
- package/dist/types/DataTable/SelectionBar.d.ts +32 -0
- package/dist/types/DataTable/ServerTable.d.ts +64 -3
- package/dist/types/DataTable/index.d.ts +1 -1
- package/dist/types/DataTable/rowSelection.d.ts +113 -0
- package/dist/types/access/PermissionMatrix.d.ts +3 -1
- package/dist/types/account/AccountSecuritySection.d.ts +3 -1
- package/dist/types/account/ForgotPasswordScreen.d.ts +3 -1
- package/dist/types/account/ProfileScreen.d.ts +31 -1
- package/dist/types/account/ResetPasswordScreen.d.ts +3 -1
- package/dist/types/account/SignInScreen.d.ts +3 -1
- package/dist/types/account/accountConnector.d.ts +7 -2
- package/dist/types/adapters/reactRouterRoutes.d.ts +14 -2
- package/dist/types/api/createApiClient.d.ts +46 -1
- package/dist/types/appShell/AppLayout.d.ts +15 -1
- package/dist/types/appShell/AppearanceSettings.d.ts +8 -1
- package/dist/types/appShell/BarActionButton.d.ts +2 -2
- package/dist/types/appShell/BarActions.d.ts +6 -1
- package/dist/types/appShell/BarCapsule.d.ts +8 -0
- package/dist/types/appShell/BarSearch.d.ts +24 -0
- package/dist/types/appShell/MenuLinks.d.ts +17 -0
- package/dist/types/appShell/MobileTabBar.d.ts +4 -1
- package/dist/types/appShell/SettingsMenu.d.ts +3 -1
- package/dist/types/appShell/SideMenu.d.ts +3 -1
- package/dist/types/appShell/Wordmark.d.ts +6 -0
- package/dist/types/appShell/menuItemText.d.ts +16 -0
- package/dist/types/appShell/shellSizes.d.ts +1 -0
- package/dist/types/auth/PasswordChecklist.d.ts +3 -1
- package/dist/types/calendar/EventCalendar.d.ts +18 -1
- package/dist/types/calendar/EventDetails.d.ts +36 -0
- package/dist/types/calendar/calendarEvents.d.ts +27 -0
- package/dist/types/calendar.d.ts +2 -0
- package/dist/types/charts.d.ts +4 -0
- package/dist/types/datePickers/DateInput.d.ts +3 -1
- package/dist/types/downloads/DownloadManager.d.ts +7 -1
- package/dist/types/downloads/downloadQueue.d.ts +8 -3
- package/dist/types/downloads/fileTypes.d.ts +39 -0
- package/dist/types/downloads/index.d.ts +1 -0
- package/dist/types/feedback/ConfirmDialog.d.ts +3 -1
- package/dist/types/feedback/ModalWaiting.d.ts +7 -2
- package/dist/types/feedback/NewBuildNotice.d.ts +3 -1
- package/dist/types/filepond/FileDropzone.d.ts +3 -1
- package/dist/types/formatting.d.ts +45 -90
- package/dist/types/forms/FormWizard.d.ts +3 -1
- package/dist/types/forms/auth/ChangePasswordForm.d.ts +3 -1
- package/dist/types/forms/auth/ForgotPasswordForm.d.ts +3 -1
- package/dist/types/forms/auth/SignInForm.d.ts +3 -1
- package/dist/types/forms/security/SecondStepForm.d.ts +3 -1
- package/dist/types/forms/security/TwoStepVerificationCard.d.ts +3 -1
- package/dist/types/hookForm/FormWizard.d.ts +22 -0
- package/dist/types/inbox/InboxLayout.d.ts +14 -2
- package/dist/types/inbox/MessageList.d.ts +30 -5
- package/dist/types/inbox/MessageView.d.ts +35 -3
- package/dist/types/inbox/inboxDates.d.ts +6 -1
- package/dist/types/index.d.ts +15 -8
- package/dist/types/inputs/CatalogInput.d.ts +9 -5
- package/dist/types/inputs/ColorInput.d.ts +3 -1
- package/dist/types/inputs/FormSection.d.ts +2 -2
- package/dist/types/inputs/NumberInput.d.ts +3 -1
- package/dist/types/inputs/SelectInput.d.ts +9 -5
- package/dist/types/inputs/TextInput.d.ts +3 -1
- package/dist/types/inputs/WizardLayout.d.ts +34 -0
- package/dist/types/kanban/KanbanBoard.d.ts +61 -6
- package/dist/types/kanban/kanbanCards.d.ts +22 -0
- package/dist/types/kanban.d.ts +1 -0
- package/dist/types/locales/en.d.ts +77 -0
- package/dist/types/locales/es.d.ts +77 -0
- package/dist/types/maps/MapView.d.ts +12 -3
- package/dist/types/navigation/appMenu.d.ts +8 -0
- package/dist/types/observability/logger.d.ts +13 -0
- package/dist/types/observability/usePublishedBuildWatch.d.ts +3 -1
- package/dist/types/pwa/InstallAppSetting.d.ts +7 -1
- package/dist/types/pwa/InstallInstructionsDialog.d.ts +3 -1
- package/dist/types/pwa/InstallPromptBanner.d.ts +10 -1
- package/dist/types/pwa/useInstallPrompt.d.ts +3 -2
- package/dist/types/react-hook-form.d.ts +1 -0
- package/dist/types/security/ActivityLog.d.ts +3 -1
- package/dist/types/security/RecoveryCodesDialog.d.ts +6 -1
- package/dist/types/security/SessionsCard.d.ts +3 -1
- package/dist/types/table.d.ts +3 -1
- package/dist/types/theme/components/MuiAppBar.d.ts +3 -1
- package/dist/types/theme/components/MuiDialog.d.ts +9 -0
- package/dist/types/theme/components/glassComponents.d.ts +5 -0
- package/dist/types/theme/lightDarkStyles.d.ts +8 -0
- package/dist/types/toolkit/ComponentTexts.d.ts +11 -0
- package/dist/types/toolkit/ToolkitProvider.d.ts +18 -2
- package/dist/types/toolkit/browserNavigation.d.ts +23 -28
- package/dist/types/toolkit/componentTextsContext.d.ts +20 -0
- package/dist/types/toolkit/hashNavigation.d.ts +12 -0
- package/dist/types/toolkit/navigationPaths.d.ts +43 -0
- package/dist/types/toolkit/toolkitSettings.d.ts +49 -4
- package/dist/types/toolkit/useFormatters.d.ts +6 -0
- package/dist/types/toolkit/useToolkitTranslation.d.ts +2 -1
- package/dist/types/ui/actions/ActionsMenu.d.ts +20 -0
- package/dist/types/ui/actions/CopyButton.d.ts +3 -1
- package/dist/types/ui/actions/DownloadMenu.d.ts +8 -3
- package/dist/types/ui/actions/SecretField.d.ts +5 -1
- package/dist/types/ui/actions/SelectionActions.d.ts +39 -0
- package/dist/types/ui/filters/DateRangeFilter.d.ts +3 -1
- package/dist/types/ui/filters/QuickFilters.d.ts +3 -1
- package/dist/types/ui/index.d.ts +1 -1
- package/dist/types/ui/labels/tagOf.d.ts +13 -0
- package/dist/types/ui/layout/CollapsibleSection.d.ts +4 -1
- package/dist/types/ui/layout/ContentDialog.d.ts +3 -1
- package/dist/types/ui/layout/ErrorPage.d.ts +14 -1
- package/dist/types/ui/layout/FullPageLoader.d.ts +7 -1
- package/dist/types/ui/layout/PageHeading.d.ts +3 -1
- package/dist/types/ui/metrics/BarList.d.ts +7 -1
- package/dist/types/ui/metrics/CalendarHeatmap.d.ts +7 -1
- package/dist/types/ui/metrics/ChartTooltip.d.ts +9 -3
- package/dist/types/ui/metrics/ColumnChart.d.ts +23 -11
- package/dist/types/ui/metrics/LineChart.d.ts +41 -0
- package/dist/types/ui/metrics/MetricCard.d.ts +9 -1
- package/dist/types/ui/metrics/TableToggle.d.ts +9 -2
- package/dist/types/ui/metrics/chartParts.d.ts +112 -0
- package/dist/types/ui/navigation/BackButton.d.ts +3 -1
- package/dist/types/ui/navigation/ColorSchemeToggle.d.ts +8 -2
- package/dist/types/ui/navigation/FontSizeToggle.d.ts +7 -2
- package/dist/types/ui/navigation/LanguageToggle.d.ts +8 -3
- package/dist/types/ui/navigation/PageBreadcrumbs.d.ts +5 -2
- package/dist/types/ui/timeline/GroupedTimeline.d.ts +4 -1
- package/dist/types/upload/FileDropzone.d.ts +3 -1
- package/dist/ui/actions/ActionsMenu.js +51 -0
- package/dist/ui/actions/ActionsMenu.js.map +1 -0
- package/dist/ui/actions/CopyButton.js +2 -2
- package/dist/ui/actions/CopyButton.js.map +1 -1
- package/dist/ui/actions/DownloadMenu.js +11 -12
- package/dist/ui/actions/DownloadMenu.js.map +1 -1
- package/dist/ui/actions/SecretField.js +8 -4
- package/dist/ui/actions/SecretField.js.map +1 -1
- package/dist/ui/actions/SelectionActions.js +142 -0
- package/dist/ui/actions/SelectionActions.js.map +1 -0
- package/dist/ui/filters/DateRangeFilter.js +4 -3
- package/dist/ui/filters/DateRangeFilter.js.map +1 -1
- package/dist/ui/filters/QuickFilters.js +2 -2
- package/dist/ui/filters/QuickFilters.js.map +1 -1
- package/dist/ui/labels/tagOf.js +7 -0
- package/dist/ui/labels/tagOf.js.map +1 -0
- package/dist/ui/layout/CollapsibleSection.js +6 -2
- package/dist/ui/layout/CollapsibleSection.js.map +1 -1
- package/dist/ui/layout/ContentDialog.js +2 -2
- package/dist/ui/layout/ContentDialog.js.map +1 -1
- package/dist/ui/layout/ErrorPage.js +21 -7
- package/dist/ui/layout/ErrorPage.js.map +1 -1
- package/dist/ui/layout/FullPageLoader.js +2 -2
- package/dist/ui/layout/FullPageLoader.js.map +1 -1
- package/dist/ui/layout/PageHeading.js +2 -2
- package/dist/ui/layout/PageHeading.js.map +1 -1
- package/dist/ui/metrics/BarList.js +18 -13
- package/dist/ui/metrics/BarList.js.map +1 -1
- package/dist/ui/metrics/CalendarHeatmap.js +11 -6
- package/dist/ui/metrics/CalendarHeatmap.js.map +1 -1
- package/dist/ui/metrics/ChartTooltip.js +1 -1
- package/dist/ui/metrics/ChartTooltip.js.map +1 -1
- package/dist/ui/metrics/ColumnChart.js +59 -63
- package/dist/ui/metrics/ColumnChart.js.map +1 -1
- package/dist/ui/metrics/LineChart.js +117 -0
- package/dist/ui/metrics/LineChart.js.map +1 -0
- package/dist/ui/metrics/MetricCard.js +41 -8
- package/dist/ui/metrics/MetricCard.js.map +1 -1
- package/dist/ui/metrics/TableToggle.js +4 -2
- package/dist/ui/metrics/TableToggle.js.map +1 -1
- package/dist/ui/metrics/chartParts.js +93 -0
- package/dist/ui/metrics/chartParts.js.map +1 -0
- package/dist/ui/navigation/BackButton.js +2 -2
- package/dist/ui/navigation/BackButton.js.map +1 -1
- package/dist/ui/navigation/ColorSchemeToggle.js +6 -3
- package/dist/ui/navigation/ColorSchemeToggle.js.map +1 -1
- package/dist/ui/navigation/FontSizeToggle.js +3 -2
- package/dist/ui/navigation/FontSizeToggle.js.map +1 -1
- package/dist/ui/navigation/LanguageToggle.js +40 -19
- package/dist/ui/navigation/LanguageToggle.js.map +1 -1
- package/dist/ui/navigation/PageBreadcrumbs.js +2 -2
- package/dist/ui/navigation/PageBreadcrumbs.js.map +1 -1
- package/dist/ui/timeline/GroupedTimeline.js +10 -8
- package/dist/ui/timeline/GroupedTimeline.js.map +1 -1
- package/dist/upload/FileDropzone.js +5 -3
- package/dist/upload/FileDropzone.js.map +1 -1
- package/package.json +16 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProfileScreen.js","sources":["../../src/account/ProfileScreen.jsx"],"sourcesContent":["// Mi perfil, completo: quién está dentro (ProfileHeader con sus perfiles como chips) y tres pestañas:\n// - Sus datos: nombre y teléfono (el correo se ve pero no se cambia aquí); al guardar se vuelve a leer la sesión, así la\n// barra y el menú de la cuenta ya dicen el nombre nuevo;\n// - Seguridad: AccountSecuritySection (contraseña, dos pasos, sesiones y bitácora);\n// - Preferencias: AppearanceSettings (tamaño de letra, idioma y modo de color).\n// La pestaña abierta va en la dirección (?tab=): se puede compartir o recargar sin perderla. Lo del servidor sale del\n// conector de la cuenta.\n//\n// <ProfileScreen />\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo} from 'react';\nimport {Form, Formik} from 'formik';\nimport Stack from '@mui/material/Stack';\nimport Tab from '@mui/material/Tab';\nimport Tabs from '@mui/material/Tabs';\nimport Typography from '@mui/material/Typography';\n\nimport AccountSecuritySection from '@/account/AccountSecuritySection.jsx';\nimport {useAccount} from '@/account/AccountProvider.jsx';\nimport AppearanceSettings from '@/appShell/AppearanceSettings.jsx';\nimport {toastSuccess} from '@/feedback/toast.jsx';\nimport FormProblemAlert from '@/forms/FormProblemAlert.jsx';\nimport FormTextField from '@/forms/FormTextField.jsx';\nimport SubmitButton from '@/forms/SubmitButton.jsx';\nimport {applyServerProblem} from '@/forms/applyServerProblem.js';\nimport {Yup} from '@/forms/validation.js';\nimport TextInput from '@/inputs/TextInput.jsx';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport ProfileHeader from '@/ui/layout/ProfileHeader.jsx';\nimport SectionCard from '@/ui/layout/SectionCard.jsx';\n\n/** Las pestañas, en el orden en que se pintan. */\nexport const PROFILE_TAB = Object.freeze({data: 'data', security: 'security', preferences: 'preferences'});\nconst TAB_PARAM = 'tab';\n// el teléfono: dígitos, con + opcional al inicio (formato internacional); lo largo que permite el plan de numeración\nconst PHONE_PATTERN = /^\\+?\\d{7,15}$/;\n// lo ancho de los formularios: un renglón cómodo, no toda la pantalla\nconst FORM_MAX_WIDTH = 520;\n\n/** Sus datos: nombre y teléfono. */\nfunction ProfileDataSection() {\n const {t} = useToolkitTranslation('account');\n const {connector, session, refreshSession} = useAccount();\n // el esquema se arma al pintar: yup toma sus mensajes al armarlo\n const schema = useMemo(() => Yup.object({\n name: Yup.string().trim().required(),\n phone: Yup.string().trim().matches(PHONE_PATTERN, {message: {key: 'account:profile.data.phoneInvalid'}, excludeEmptyString: true}),\n }), []);\n\n const save = async (values, formikHelpers) => {\n try {\n const response = await connector.updateProfile({name: values.name.trim(), phone: values.phone.trim()});\n await refreshSession();\n toastSuccess(response?.message ?? t('profile.data.saved'));\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n return (\n <SectionCard title={t('profile.data.title')}>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{mb: 2}}>{t('profile.data.text')}</Typography>\n <Formik initialValues={{name: session.user.name ?? '', phone: session.user.phone ?? ''}} validationSchema={schema} onSubmit={save}>\n <Form noValidate>\n <Stack spacing={2} sx={{maxWidth: FORM_MAX_WIDTH}}>\n <FormProblemAlert />\n <FormTextField name=\"name\" label={t('profile.data.name')} autoComplete=\"name\" />\n {/* el correo se ve con el mismo estilo de los demás campos, pero no se cambia aquí */}\n <TextInput name=\"email\" label={t('profile.data.email')} value={session.user.email} onChange={() => {}}\n helperText={t('profile.data.emailHelp')} disabled />\n <FormTextField name=\"phone\" label={t('profile.data.phone')} type=\"tel\" autoComplete=\"tel\" inputMode=\"tel\" />\n <SubmitButton sx={{alignSelf: 'flex-start'}}>{t('profile.data.save')}</SubmitButton>\n </Stack>\n </Form>\n </Formik>\n </SectionCard>\n );\n}\n\n/**\n * @param {object} props\n * @param {boolean} [props.showColorScheme] el ajuste de claro u oscuro en Preferencias; sin modo oscuro, false\n */\nexport default function ProfileScreen({showColorScheme = true}) {\n const {t} = useToolkitTranslation('account');\n const {session} = useAccount();\n const {useNavigate, useLocation} = useToolkitNavigation();\n const navigate = useNavigate();\n const {pathname, search} = useLocation();\n const requestedTab = new URLSearchParams(search).get(TAB_PARAM);\n const activeTab = Object.values(PROFILE_TAB).includes(requestedTab) ? requestedTab : PROFILE_TAB.data;\n\n // guard clause: sin sesión no hay perfil que enseñar (el marco de la app ya lleva a entrar)\n if (!session) {\n return null;\n }\n\n return (\n <Stack spacing={2}>\n <ProfileHeader name={session.user.name} email={session.user.email} subtitle={session.user.subtitle} avatarUrl={session.user.avatarUrl}\n chips={session.labels ?? []} />\n <Tabs value={activeTab} onChange={(event, tab) => navigate(`${pathname}?${TAB_PARAM}=${tab}`, {replace: true})}\n aria-label={t('profile.tabs.label')}>\n {Object.values(PROFILE_TAB).map((tab) => <Tab key={tab} value={tab} label={t(`profile.tabs.${tab}`)} />)}\n </Tabs>\n {activeTab === PROFILE_TAB.data && <ProfileDataSection />}\n {activeTab === PROFILE_TAB.security && <AccountSecuritySection />}\n {activeTab === PROFILE_TAB.preferences && (\n <SectionCard title={t('profile.preferences.title')}>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{mb: 1}}>{t('profile.preferences.text')}</Typography>\n <Stack sx={{maxWidth: FORM_MAX_WIDTH, mx: -1}}>\n <AppearanceSettings showColorScheme={showColorScheme} />\n </Stack>\n </SectionCard>\n )}\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProfileScreen.js","sources":["../../src/account/ProfileScreen.jsx"],"sourcesContent":["// Mi perfil, completo: quién está dentro (ProfileHeader con sus perfiles como chips) y tres pestañas:\n// - Sus datos: nombre y teléfono (el correo se ve pero no se cambia aquí); al guardar se vuelve a leer la sesión, así la\n// barra y el menú de la cuenta ya dicen el nombre nuevo;\n// - Seguridad: AccountSecuritySection (contraseña, dos pasos, sesiones y bitácora);\n// - Preferencias: AppearanceSettings (tamaño de letra, idioma y modo de color).\n// La pestaña abierta va en la dirección (?tab=): se puede compartir o recargar sin perderla. Lo del servidor sale del\n// conector de la cuenta.\n//\n// Se extiende sin tocar el framework (cada proyecto tiene su perfil «especial»):\n// - extraFields: campos propios en «Sus datos» (el área, el número de empleado…), con su valor de inicio sacado del usuario\n// de la sesión, sus reglas de yup y sus campos. Al guardar viajan a connector.updateProfile junto con name y phone: un\n// backend que ya existe recibe lo suyo con sus nombres;\n// - extraTabs: pestañas propias después de las de la casa (facturación, notificaciones…); hiddenTabs esconde las de la casa;\n// - eventos: onTabChange y onProfileSaved (lo guardado y lo que contestó el servidor);\n// - texts: cualquier texto, con la forma de `account` en los textos del paquete.\n//\n// <ProfileScreen />\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo} from 'react';\nimport {Form, Formik} from 'formik';\nimport Stack from '@mui/material/Stack';\nimport Tab from '@mui/material/Tab';\nimport Tabs from '@mui/material/Tabs';\nimport Typography from '@mui/material/Typography';\n\nimport AccountSecuritySection from '@/account/AccountSecuritySection.jsx';\nimport {useAccount} from '@/account/AccountProvider.jsx';\nimport AppearanceSettings from '@/appShell/AppearanceSettings.jsx';\nimport {toastSuccess} from '@/feedback/toast.jsx';\nimport FormProblemAlert from '@/forms/FormProblemAlert.jsx';\nimport FormTextField from '@/forms/FormTextField.jsx';\nimport SubmitButton from '@/forms/SubmitButton.jsx';\nimport {applyServerProblem} from '@/forms/applyServerProblem.js';\nimport {Yup} from '@/forms/validation.js';\nimport TextInput from '@/inputs/TextInput.jsx';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport ProfileHeader from '@/ui/layout/ProfileHeader.jsx';\nimport SectionCard from '@/ui/layout/SectionCard.jsx';\n\n/** Las pestañas, en el orden en que se pintan. */\nexport const PROFILE_TAB = Object.freeze({data: 'data', security: 'security', preferences: 'preferences'});\nconst TAB_PARAM = 'tab';\n// el teléfono: dígitos, con + opcional al inicio (formato internacional); lo largo que permite el plan de numeración\nconst PHONE_PATTERN = /^\\+?\\d{7,15}$/;\n// lo ancho de los formularios: un renglón cómodo, no toda la pantalla\nconst FORM_MAX_WIDTH = 520;\n\n/**\n * Sus datos: nombre, teléfono y los campos propios del proyecto (extraFields).\n *\n * @param {object} props\n * @param {object} [props.extraFields]\n * @param {Function} [props.onProfileSaved]\n * @param {object} [props.texts]\n */\nfunction ProfileDataSection({extraFields, onProfileSaved, texts}) {\n const {t} = useToolkitTranslation('account', texts);\n const {connector, session, refreshSession} = useAccount();\n const extraRules = extraFields?.rules;\n // el esquema se arma al pintar: yup toma sus mensajes al armarlo. Las reglas propias se suman a las de la casa\n const schema = useMemo(() => Yup.object({\n name: Yup.string().trim().required(),\n phone: Yup.string().trim().matches(PHONE_PATTERN, {message: {key: 'account:profile.data.phoneInvalid'}, excludeEmptyString: true}),\n ...extraRules,\n }), [extraRules]);\n const initialValues = {name: session.user.name ?? '', phone: session.user.phone ?? '', ...extraFields?.initialValues?.(session.user)};\n\n const save = async (values, formikHelpers) => {\n // lo de la casa limpio de espacios; lo propio, tal como lo dejaron sus campos\n const profile = {...values, name: values.name.trim(), phone: values.phone.trim()};\n try {\n const response = await connector.updateProfile(profile);\n await refreshSession();\n toastSuccess(response?.message ?? t('profile.data.saved'));\n onProfileSaved?.({values: profile, response});\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n return (\n <SectionCard title={t('profile.data.title')}>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{mb: 2}}>{t('profile.data.text')}</Typography>\n <Formik initialValues={initialValues} validationSchema={schema} onSubmit={save}>\n <Form noValidate>\n <Stack spacing={2} sx={{maxWidth: FORM_MAX_WIDTH}}>\n <FormProblemAlert />\n <FormTextField name=\"name\" label={t('profile.data.name')} autoComplete=\"name\" />\n {/* el correo se ve con el mismo estilo de los demás campos, pero no se cambia aquí */}\n <TextInput name=\"email\" label={t('profile.data.email')} value={session.user.email} onChange={() => {}}\n helperText={t('profile.data.emailHelp')} disabled />\n <FormTextField name=\"phone\" label={t('profile.data.phone')} type=\"tel\" autoComplete=\"tel\" inputMode=\"tel\" />\n {extraFields?.fields}\n <SubmitButton sx={{alignSelf: 'flex-start'}}>{t('profile.data.save')}</SubmitButton>\n </Stack>\n </Form>\n </Formik>\n </SectionCard>\n );\n}\n\n/** @typedef {import('@/account/accountConnector.js').AccountUser} AccountUser */\n\n/**\n * @param {object} props\n * @param {boolean} [props.showColorScheme] el ajuste de claro u oscuro en Preferencias; sin modo oscuro, false\n * @param {{initialValues?: (user: AccountUser & Record<string, any>) => Record<string, any>, rules?: Record<string, any>, fields: import('react').ReactNode}} [props.extraFields]\n * campos propios en «Sus datos», debajo del teléfono: sus valores de inicio salen del usuario de la sesión (lo que su\n * conector ponga en session.user), sus reglas de yup y sus campos (FormTextField, FormSelectField… de /forms). Al guardar\n * viajan a connector.updateProfile junto con name y phone\n * @param {Array<{key: string, label: string, content: import('react').ReactNode}>} [props.extraTabs] pestañas propias,\n * después de las de la casa; label ya traducido\n * @param {Array<'data'|'security'|'preferences'>} [props.hiddenTabs] pestañas de la casa que no se ven (PROFILE_TAB)\n * @param {(tab: string) => void} [props.onTabChange] cada que se cambia de pestaña\n * @param {(saved: {values: Record<string, any>, response: unknown}) => void} [props.onProfileSaved] después de guardar «Sus datos»\n * @param {object} [props.texts] los textos que cambian, con la forma de `account` en los textos del paquete:\n * {profile: {tabs: {data: 'Mis datos'}}}\n */\nexport default function ProfileScreen({\n showColorScheme = true, extraFields, extraTabs = [], hiddenTabs = [], onTabChange, onProfileSaved, texts,\n}) {\n const {t} = useToolkitTranslation('account', texts);\n const {session} = useAccount();\n const {useNavigate, useLocation} = useToolkitNavigation();\n const navigate = useNavigate();\n const {pathname, search} = useLocation();\n // las pestañas que se ven: las de la casa que no se escondieron y luego las del proyecto\n const tabs = [\n ...Object.values(PROFILE_TAB).filter((tab) => !hiddenTabs.includes(tab)).map((tab) => ({key: tab, label: t(`profile.tabs.${tab}`)})),\n ...extraTabs.map(({key, label}) => ({key, label})),\n ];\n const requestedTab = new URLSearchParams(search).get(TAB_PARAM);\n const activeTab = tabs.some((tab) => tab.key === requestedTab) ? requestedTab : tabs[0]?.key;\n const activeExtraTab = extraTabs.find((tab) => tab.key === activeTab);\n const changeTab = (tab) => {\n navigate(`${pathname}?${TAB_PARAM}=${tab}`, {replace: true});\n onTabChange?.(tab);\n };\n\n // guard clause: sin sesión no hay perfil que enseñar (el marco de la app ya lleva a entrar)\n if (!session) {\n return null;\n }\n\n return (\n <Stack spacing={2}>\n <ProfileHeader name={session.user.name} email={session.user.email} subtitle={session.user.subtitle} avatarUrl={session.user.avatarUrl}\n chips={session.labels ?? []} />\n <Tabs value={activeTab} onChange={(event, tab) => changeTab(tab)} aria-label={t('profile.tabs.label')}\n variant=\"scrollable\" allowScrollButtonsMobile>\n {tabs.map((tab) => <Tab key={tab.key} value={tab.key} label={tab.label} />)}\n </Tabs>\n {activeTab === PROFILE_TAB.data && <ProfileDataSection extraFields={extraFields} onProfileSaved={onProfileSaved} texts={texts} />}\n {activeTab === PROFILE_TAB.security && <AccountSecuritySection />}\n {activeTab === PROFILE_TAB.preferences && (\n <SectionCard title={t('profile.preferences.title')}>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{mb: 1}}>{t('profile.preferences.text')}</Typography>\n <Stack sx={{maxWidth: FORM_MAX_WIDTH, mx: -1}}>\n <AppearanceSettings showColorScheme={showColorScheme} />\n </Stack>\n </SectionCard>\n )}\n {activeExtraTab?.content}\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -16,9 +16,10 @@ function ResetPasswordScreen({
|
|
|
16
16
|
logo,
|
|
17
17
|
brandName,
|
|
18
18
|
showLanguageToggle = true,
|
|
19
|
-
showColorSchemeToggle = true
|
|
19
|
+
showColorSchemeToggle = true,
|
|
20
|
+
texts
|
|
20
21
|
}) {
|
|
21
|
-
const { t } = useToolkitTranslation("account");
|
|
22
|
+
const { t } = useToolkitTranslation("account", texts);
|
|
22
23
|
const { connector } = useAccount();
|
|
23
24
|
const { useLocation, Link } = useToolkitNavigation();
|
|
24
25
|
const { search } = useLocation();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResetPasswordScreen.js","sources":["../../src/account/ResetPasswordScreen.jsx"],"sourcesContent":["// Elegir una contraseña nueva desde el enlace del correo, completa: ChangePasswordForm sin la contraseña actual (quien llega\n// aquí la olvidó). El enlace trae su clave (?token=) y el servidor la revisa al guardar (connector.resetPassword). Sin\n// clave en la dirección, se dice que el enlace no sirve y cómo pedir otro.\n//\n// <ResetPasswordScreen signInPath=\"/sign-in\" forgotPasswordPath=\"/forgot-password\" logo={<AppLogo />} />\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport Alert from '@mui/material/Alert';\nimport Button from '@mui/material/Button';\nimport Stack from '@mui/material/Stack';\n\nimport {useAccount} from '@/account/AccountProvider.jsx';\nimport {AuthLayout} from '@/appShell/AuthLayout.jsx';\nimport ChangePasswordForm from '@/forms/auth/ChangePasswordForm.jsx';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\n\n/**\n * @param {object} props\n * @param {string} props.signInPath a dónde ir al terminar\n * @param {string} [props.forgotPasswordPath] para pedir otro enlace si este no sirve\n * @param {string} [props.tokenParam] cómo se llama la clave en la dirección; por omisión 'token'\n * @param {import('react').ReactNode} [props.logo]\n * @param {string} [props.brandName]\n * @param {boolean} [props.showLanguageToggle]\n * @param {boolean} [props.showColorSchemeToggle]\n */\nexport default function ResetPasswordScreen({\n signInPath, forgotPasswordPath, tokenParam = 'token', logo, brandName, showLanguageToggle = true, showColorSchemeToggle = true,\n}) {\n const {t} = useToolkitTranslation('account');\n const {connector} = useAccount();\n const {useLocation, Link} = useToolkitNavigation();\n const {search} = useLocation();\n const [isDone, setIsDone] = useState(false);\n const token = new URLSearchParams(search).get(tokenParam);\n\n const save = async ({newPassword}) => {\n await connector.resetPassword({token, newPassword});\n setIsDone(true);\n };\n\n return (\n <AuthLayout logo={logo} brandName={brandName} title={t('reset.title')} description={t('reset.subtitle')}\n showLanguageToggle={showLanguageToggle} showColorSchemeToggle={showColorSchemeToggle}>\n {!token && (\n <Stack spacing={2}>\n <Alert severity=\"error\">{t('reset.missingToken')}</Alert>\n {forgotPasswordPath && <Button component={Link} to={forgotPasswordPath} variant=\"contained\">{t('reset.askAgain')}</Button>}\n </Stack>\n )}\n {token && isDone && (\n <Stack spacing={2}>\n <Alert severity=\"success\" role=\"status\">{t('reset.done')}</Alert>\n <Button component={Link} to={signInPath} variant=\"contained\">{t('reset.goToSignIn')}</Button>\n </Stack>\n )}\n {token && !isDone && <ChangePasswordForm requireCurrentPassword={false} onSubmit={save} />}\n </AuthLayout>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"ResetPasswordScreen.js","sources":["../../src/account/ResetPasswordScreen.jsx"],"sourcesContent":["// Elegir una contraseña nueva desde el enlace del correo, completa: ChangePasswordForm sin la contraseña actual (quien llega\n// aquí la olvidó). El enlace trae su clave (?token=) y el servidor la revisa al guardar (connector.resetPassword). Sin\n// clave en la dirección, se dice que el enlace no sirve y cómo pedir otro.\n//\n// <ResetPasswordScreen signInPath=\"/sign-in\" forgotPasswordPath=\"/forgot-password\" logo={<AppLogo />} />\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport Alert from '@mui/material/Alert';\nimport Button from '@mui/material/Button';\nimport Stack from '@mui/material/Stack';\n\nimport {useAccount} from '@/account/AccountProvider.jsx';\nimport {AuthLayout} from '@/appShell/AuthLayout.jsx';\nimport ChangePasswordForm from '@/forms/auth/ChangePasswordForm.jsx';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\n\n/**\n * @param {object} props\n * @param {string} props.signInPath a dónde ir al terminar\n * @param {string} [props.forgotPasswordPath] para pedir otro enlace si este no sirve\n * @param {string} [props.tokenParam] cómo se llama la clave en la dirección; por omisión 'token'\n * @param {import('react').ReactNode} [props.logo]\n * @param {string} [props.brandName]\n * @param {boolean} [props.showLanguageToggle]\n * @param {boolean} [props.showColorSchemeToggle]\n * @param {object} [props.texts] los textos que cambian, con la forma de `account` en los textos del paquete\n */\nexport default function ResetPasswordScreen({\n signInPath, forgotPasswordPath, tokenParam = 'token', logo, brandName, showLanguageToggle = true, showColorSchemeToggle = true, texts,\n}) {\n const {t} = useToolkitTranslation('account', texts);\n const {connector} = useAccount();\n const {useLocation, Link} = useToolkitNavigation();\n const {search} = useLocation();\n const [isDone, setIsDone] = useState(false);\n const token = new URLSearchParams(search).get(tokenParam);\n\n const save = async ({newPassword}) => {\n await connector.resetPassword({token, newPassword});\n setIsDone(true);\n };\n\n return (\n <AuthLayout logo={logo} brandName={brandName} title={t('reset.title')} description={t('reset.subtitle')}\n showLanguageToggle={showLanguageToggle} showColorSchemeToggle={showColorSchemeToggle}>\n {!token && (\n <Stack spacing={2}>\n <Alert severity=\"error\">{t('reset.missingToken')}</Alert>\n {forgotPasswordPath && <Button component={Link} to={forgotPasswordPath} variant=\"contained\">{t('reset.askAgain')}</Button>}\n </Stack>\n )}\n {token && isDone && (\n <Stack spacing={2}>\n <Alert severity=\"success\" role=\"status\">{t('reset.done')}</Alert>\n <Button component={Link} to={signInPath} variant=\"contained\">{t('reset.goToSignIn')}</Button>\n </Stack>\n )}\n {token && !isDone && <ChangePasswordForm requireCurrentPassword={false} onSubmit={save} />}\n </AuthLayout>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;AA6BA,SAAwB,oBAAoB;AAAA,EACxC;AAAA,EAAY;AAAA,EAAoB,aAAa;AAAA,EAAS;AAAA,EAAM;AAAA,EAAW,qBAAqB;AAAA,EAAM,wBAAwB;AAAA,EAAM;AACpI,GAAG;AACC,QAAM,EAAC,EAAA,IAAK,sBAAsB,WAAW,KAAK;AAClD,QAAM,EAAC,UAAA,IAAa,WAAA;AACpB,QAAM,EAAC,aAAa,KAAA,IAAQ,qBAAA;AAC5B,QAAM,EAAC,OAAA,IAAU,YAAA;AACjB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,QAAQ,IAAI,gBAAgB,MAAM,EAAE,IAAI,UAAU;AAExD,QAAM,OAAO,OAAO,EAAC,kBAAiB;AAClC,UAAM,UAAU,cAAc,EAAC,OAAO,aAAY;AAClD,cAAU,IAAI;AAAA,EAClB;AAEA,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAW;AAAA,MAAY;AAAA,MAAsB,OAAO,EAAE,aAAa;AAAA,MAAG,aAAa,EAAE,gBAAgB;AAAA,MAC1F;AAAA,MAAwC;AAAA,MAC/C,UAAA;AAAA,QAAA,CAAC,SACE,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,UAAA,oBAAC,OAAA,EAAM,UAAS,SAAS,UAAA,EAAE,oBAAoB,GAAE;AAAA,UAChD,sBAAsB,oBAAC,QAAA,EAAO,WAAW,MAAM,IAAI,oBAAoB,SAAQ,aAAa,UAAA,EAAE,gBAAgB,EAAA,CAAE;AAAA,QAAA,GACrH;AAAA,QAEH,SAAS,UACN,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,UAAA,oBAAC,SAAM,UAAS,WAAU,MAAK,UAAU,UAAA,EAAE,YAAY,GAAE;AAAA,UACzD,oBAAC,QAAA,EAAO,WAAW,MAAM,IAAI,YAAY,SAAQ,aAAa,UAAA,EAAE,kBAAkB,EAAA,CAAE;AAAA,QAAA,GACxF;AAAA,QAEH,SAAS,CAAC,UAAU,oBAAC,sBAAmB,wBAAwB,OAAO,UAAU,KAAA,CAAM;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGpG;"}
|
|
@@ -11,8 +11,9 @@ import { readRedirectParam } from "../navigation.js";
|
|
|
11
11
|
import { signWithPasskey, passkeysSupported } from "../security/passkeys.js";
|
|
12
12
|
import { useToolkitNavigation } from "../toolkit/toolkitContext.js";
|
|
13
13
|
import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
import ComponentTexts from "../toolkit/ComponentTexts.js";
|
|
15
|
+
function SignInScreen({ homePath = "/", forgotPasswordPath, logo, brandName, footer, showLanguageToggle = true, showColorSchemeToggle = true, texts }) {
|
|
16
|
+
const { t } = useToolkitTranslation("auth", texts);
|
|
16
17
|
const { connector, session, refreshSession } = useAccount();
|
|
17
18
|
const { useNavigate, useLocation } = useToolkitNavigation();
|
|
18
19
|
const navigate = useNavigate();
|
|
@@ -50,7 +51,7 @@ function SignInScreen({ homePath = "/", forgotPasswordPath, logo, brandName, foo
|
|
|
50
51
|
}
|
|
51
52
|
if (secondStep) {
|
|
52
53
|
const showsDemoCode = typeof connector.demoTotpCode === "function" && secondStep.methods.includes(SECOND_STEP_METHOD.totp);
|
|
53
|
-
return /* @__PURE__ */ jsx(
|
|
54
|
+
return /* @__PURE__ */ jsx(ComponentTexts, { namespace: "auth", texts, children: /* @__PURE__ */ jsx(
|
|
54
55
|
AuthLayout,
|
|
55
56
|
{
|
|
56
57
|
logo,
|
|
@@ -76,9 +77,9 @@ function SignInScreen({ homePath = "/", forgotPasswordPath, logo, brandName, foo
|
|
|
76
77
|
}
|
|
77
78
|
)
|
|
78
79
|
}
|
|
79
|
-
);
|
|
80
|
+
) });
|
|
80
81
|
}
|
|
81
|
-
return /* @__PURE__ */ jsxs(
|
|
82
|
+
return /* @__PURE__ */ jsx(ComponentTexts, { namespace: "auth", texts, children: /* @__PURE__ */ jsxs(
|
|
82
83
|
AuthLayout,
|
|
83
84
|
{
|
|
84
85
|
logo,
|
|
@@ -93,7 +94,7 @@ function SignInScreen({ homePath = "/", forgotPasswordPath, logo, brandName, foo
|
|
|
93
94
|
/* @__PURE__ */ jsx(SignInForm, { onSubmit: signIn, forgotPasswordPath })
|
|
94
95
|
]
|
|
95
96
|
}
|
|
96
|
-
);
|
|
97
|
+
) });
|
|
97
98
|
}
|
|
98
99
|
export {
|
|
99
100
|
SignInScreen as default
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SignInScreen.js","sources":["../../src/account/SignInScreen.jsx"],"sourcesContent":["// Entrar, completo: la contraseña (SignInForm) y, si la cuenta tiene verificación en dos pasos, el segundo paso\n// (SecondStepForm: la llave de acceso, el código de la app o uno de recuperación). Al terminar se vuelve a leer la sesión y\n// se va a la pantalla que se quería abrir (readRedirectParam: solo si es de esta app) o al inicio.\n//\n// Lo del servidor sale del conector de la cuenta (useAccount): en el proyecto la pantalla solo se pone en su ruta.\n//\n// <SignInScreen forgotPasswordPath=\"/forgot-password\" logo={<AppLogo />} />\n//\n// footer va abajo de la tarjeta. Si es una función, recibe signIn para entrar desde un botón propio (p. ej. «Entrar con la\n// cuenta de ejemplo»), con el mismo segundo paso; si falla, el motivo sale arriba del formulario.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useState} from 'react';\nimport Alert from '@mui/material/Alert';\n\nimport {useAccount} from '@/account/AccountProvider.jsx';\nimport DemoTotpCodeButton from '@/account/DemoTotpCodeButton.jsx';\nimport {AuthLayout} from '@/appShell/AuthLayout.jsx';\nimport SignInForm from '@/forms/auth/SignInForm.jsx';\nimport SecondStepForm, {SECOND_STEP_METHOD} from '@/forms/security/SecondStepForm.jsx';\nimport {readRedirectParam} from '@/navigation.js';\nimport {passkeysSupported, signWithPasskey} from '@/security/passkeys.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\n\n/**\n * @param {object} props\n * @param {string} [props.homePath] a dónde ir si no se quería abrir otra pantalla; por omisión '/'\n * @param {string} [props.forgotPasswordPath] sin ella no sale «¿Olvidó su contraseña?»\n * @param {import('react').ReactNode} [props.logo]\n * @param {string} [props.brandName] por omisión el appName de ToolkitProvider\n * @param {import('react').ReactNode | ((helpers: {signIn: (credentials: {email: string, password: string, remember?: boolean}) => Promise<void>}) => import('react').ReactNode)} [props.footer]\n * @param {boolean} [props.showLanguageToggle] por omisión sí\n * @param {boolean} [props.showColorSchemeToggle] por omisión sí\n */\nexport default function SignInScreen({homePath = '/', forgotPasswordPath, logo, brandName, footer, showLanguageToggle = true, showColorSchemeToggle = true}) {\n const {t} = useToolkitTranslation('auth');\n const {connector, session, refreshSession} = useAccount();\n const {useNavigate, useLocation} = useToolkitNavigation();\n const navigate = useNavigate();\n const {search} = useLocation();\n // con verificación en dos pasos: los métodos del segundo paso (la contraseña ya fue correcta)\n const [secondStep, setSecondStep] = useState(null);\n const [notice, setNotice] = useState(null);\n const destination = readRedirectParam(search) ?? homePath;\n\n // ya hay sesión (acaba de entrar, o abrió esta pantalla estando dentro): a donde iba\n useEffect(() => {\n if (session) {\n navigate(destination, {replace: true});\n }\n }, [session, destination, navigate]);\n\n // después de la contraseña: el segundo paso si lo pide el servidor; si no, ya está dentro\n const signIn = async (credentials) => {\n setNotice(null);\n const result = await connector.signIn({remember: false, ...credentials});\n if (result?.secondStep) {\n setSecondStep(result.secondStep);\n return;\n }\n await refreshSession();\n };\n // desde un botón propio del footer: el error no tiene formulario donde salir, sale arriba\n const signInFromFooter = async (credentials) => {\n try {\n await signIn(credentials);\n } catch (error) {\n setNotice(error?.message ?? null);\n }\n };\n // se acabaron los intentos o se venció el tiempo: de regreso a la contraseña, con el motivo\n const backToPassword = (message) => {\n setSecondStep(null);\n setNotice(message);\n };\n\n if (session) {\n return null;\n }\n\n if (secondStep) {\n const showsDemoCode = typeof connector.demoTotpCode === 'function' && secondStep.methods.includes(SECOND_STEP_METHOD.totp);\n return (\n <AuthLayout logo={logo} brandName={brandName} title={t('security:twoStep.title')}\n
|
|
1
|
+
{"version":3,"file":"SignInScreen.js","sources":["../../src/account/SignInScreen.jsx"],"sourcesContent":["// Entrar, completo: la contraseña (SignInForm) y, si la cuenta tiene verificación en dos pasos, el segundo paso\n// (SecondStepForm: la llave de acceso, el código de la app o uno de recuperación). Al terminar se vuelve a leer la sesión y\n// se va a la pantalla que se quería abrir (readRedirectParam: solo si es de esta app) o al inicio.\n//\n// Lo del servidor sale del conector de la cuenta (useAccount): en el proyecto la pantalla solo se pone en su ruta.\n//\n// <SignInScreen forgotPasswordPath=\"/forgot-password\" logo={<AppLogo />} />\n//\n// footer va abajo de la tarjeta. Si es una función, recibe signIn para entrar desde un botón propio (p. ej. «Entrar con la\n// cuenta de ejemplo»), con el mismo segundo paso; si falla, el motivo sale arriba del formulario.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useState} from 'react';\nimport Alert from '@mui/material/Alert';\n\nimport {useAccount} from '@/account/AccountProvider.jsx';\nimport DemoTotpCodeButton from '@/account/DemoTotpCodeButton.jsx';\nimport {AuthLayout} from '@/appShell/AuthLayout.jsx';\nimport SignInForm from '@/forms/auth/SignInForm.jsx';\nimport SecondStepForm, {SECOND_STEP_METHOD} from '@/forms/security/SecondStepForm.jsx';\nimport {readRedirectParam} from '@/navigation.js';\nimport {passkeysSupported, signWithPasskey} from '@/security/passkeys.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport ComponentTexts from '@/toolkit/ComponentTexts.jsx';\n\n/**\n * @param {object} props\n * @param {string} [props.homePath] a dónde ir si no se quería abrir otra pantalla; por omisión '/'\n * @param {string} [props.forgotPasswordPath] sin ella no sale «¿Olvidó su contraseña?»\n * @param {import('react').ReactNode} [props.logo]\n * @param {string} [props.brandName] por omisión el appName de ToolkitProvider\n * @param {import('react').ReactNode | ((helpers: {signIn: (credentials: {email: string, password: string, remember?: boolean}) => Promise<void>}) => import('react').ReactNode)} [props.footer]\n * @param {boolean} [props.showLanguageToggle] por omisión sí\n * @param {boolean} [props.showColorSchemeToggle] por omisión sí\n * @param {object} [props.texts] los textos que cambian, con la forma de `auth` en los textos del paquete\n */\nexport default function SignInScreen({homePath = '/', forgotPasswordPath, logo, brandName, footer, showLanguageToggle = true, showColorSchemeToggle = true, texts}) {\n const {t} = useToolkitTranslation('auth', texts);\n const {connector, session, refreshSession} = useAccount();\n const {useNavigate, useLocation} = useToolkitNavigation();\n const navigate = useNavigate();\n const {search} = useLocation();\n // con verificación en dos pasos: los métodos del segundo paso (la contraseña ya fue correcta)\n const [secondStep, setSecondStep] = useState(null);\n const [notice, setNotice] = useState(null);\n const destination = readRedirectParam(search) ?? homePath;\n\n // ya hay sesión (acaba de entrar, o abrió esta pantalla estando dentro): a donde iba\n useEffect(() => {\n if (session) {\n navigate(destination, {replace: true});\n }\n }, [session, destination, navigate]);\n\n // después de la contraseña: el segundo paso si lo pide el servidor; si no, ya está dentro\n const signIn = async (credentials) => {\n setNotice(null);\n const result = await connector.signIn({remember: false, ...credentials});\n if (result?.secondStep) {\n setSecondStep(result.secondStep);\n return;\n }\n await refreshSession();\n };\n // desde un botón propio del footer: el error no tiene formulario donde salir, sale arriba\n const signInFromFooter = async (credentials) => {\n try {\n await signIn(credentials);\n } catch (error) {\n setNotice(error?.message ?? null);\n }\n };\n // se acabaron los intentos o se venció el tiempo: de regreso a la contraseña, con el motivo\n const backToPassword = (message) => {\n setSecondStep(null);\n setNotice(message);\n };\n\n if (session) {\n return null;\n }\n\n if (secondStep) {\n const showsDemoCode = typeof connector.demoTotpCode === 'function' && secondStep.methods.includes(SECOND_STEP_METHOD.totp);\n return (\n <ComponentTexts namespace=\"auth\" texts={texts}>\n <AuthLayout logo={logo} brandName={brandName} title={t('security:twoStep.title')}\n showLanguageToggle={showLanguageToggle} showColorSchemeToggle={showColorSchemeToggle}>\n <SecondStepForm methods={secondStep.methods} canUsePasskeys={passkeysSupported()}\n onUsePasskey={async () => {\n await connector.signInWithPasskey(await signWithPasskey(await connector.secondStepPasskeyOptions()));\n await refreshSession();\n }}\n onSubmitCode={async ({method, code}) => {\n await connector.signInSecondStep({method, code});\n await refreshSession();\n }}\n onCancel={backToPassword}\n footer={showsDemoCode && <DemoTotpCodeButton loadCode={connector.demoTotpCode} />} />\n </AuthLayout>\n </ComponentTexts>\n );\n }\n\n return (\n <ComponentTexts namespace=\"auth\" texts={texts}>\n <AuthLayout logo={logo} brandName={brandName} title={t('signIn.title')} description={t('signIn.subtitle')}\n showLanguageToggle={showLanguageToggle} showColorSchemeToggle={showColorSchemeToggle}\n footer={typeof footer === 'function' ? footer({signIn: signInFromFooter}) : footer}>\n {notice && <Alert severity=\"warning\" sx={{mb: 2}}>{notice}</Alert>}\n <SignInForm onSubmit={signIn} forgotPasswordPath={forgotPasswordPath} />\n </AuthLayout>\n </ComponentTexts>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;AAqCA,SAAwB,aAAa,EAAC,WAAW,KAAK,oBAAoB,MAAM,WAAW,QAAQ,qBAAqB,MAAM,wBAAwB,MAAM,SAAQ;AAChK,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ,KAAK;AAC/C,QAAM,EAAC,WAAW,SAAS,eAAA,IAAkB,WAAA;AAC7C,QAAM,EAAC,aAAa,YAAA,IAAe,qBAAA;AACnC,QAAM,WAAW,YAAA;AACjB,QAAM,EAAC,OAAA,IAAU,YAAA;AAEjB,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,IAAI;AACjD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,IAAI;AACzC,QAAM,cAAc,kBAAkB,MAAM,KAAK;AAGjD,YAAU,MAAM;AACZ,QAAI,SAAS;AACT,eAAS,aAAa,EAAC,SAAS,KAAA,CAAK;AAAA,IACzC;AAAA,EACJ,GAAG,CAAC,SAAS,aAAa,QAAQ,CAAC;AAGnC,QAAM,SAAS,OAAO,gBAAgB;AAClC,cAAU,IAAI;AACd,UAAM,SAAS,MAAM,UAAU,OAAO,EAAC,UAAU,OAAO,GAAG,aAAY;AACvE,QAAI,QAAQ,YAAY;AACpB,oBAAc,OAAO,UAAU;AAC/B;AAAA,IACJ;AACA,UAAM,eAAA;AAAA,EACV;AAEA,QAAM,mBAAmB,OAAO,gBAAgB;AAC5C,QAAI;AACA,YAAM,OAAO,WAAW;AAAA,IAC5B,SAAS,OAAO;AACZ,gBAAU,OAAO,WAAW,IAAI;AAAA,IACpC;AAAA,EACJ;AAEA,QAAM,iBAAiB,CAAC,YAAY;AAChC,kBAAc,IAAI;AAClB,cAAU,OAAO;AAAA,EACrB;AAEA,MAAI,SAAS;AACT,WAAO;AAAA,EACX;AAEA,MAAI,YAAY;AACZ,UAAM,gBAAgB,OAAO,UAAU,iBAAiB,cAAc,WAAW,QAAQ,SAAS,mBAAmB,IAAI;AACzH,WACI,oBAAC,gBAAA,EAAe,WAAU,QAAO,OAC7B,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QAAW;AAAA,QAAY;AAAA,QAAsB,OAAO,EAAE,wBAAwB;AAAA,QACnE;AAAA,QAAwC;AAAA,QAChD,UAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YAAe,SAAS,WAAW;AAAA,YAAS,gBAAgB,kBAAA;AAAA,YAC7C,cAAc,YAAY;AACtB,oBAAM,UAAU,kBAAkB,MAAM,gBAAgB,MAAM,UAAU,yBAAA,CAA0B,CAAC;AACnG,oBAAM,eAAA;AAAA,YACV;AAAA,YACA,cAAc,OAAO,EAAC,QAAQ,WAAU;AACpC,oBAAM,UAAU,iBAAiB,EAAC,QAAQ,MAAK;AAC/C,oBAAM,eAAA;AAAA,YACV;AAAA,YACA,UAAU;AAAA,YACV,QAAQ,iBAAiB,oBAAC,oBAAA,EAAmB,UAAU,UAAU,aAAA,CAAc;AAAA,UAAA;AAAA,QAAA;AAAA,MAAI;AAAA,IAAA,GAE3G;AAAA,EAER;AAEA,SACI,oBAAC,gBAAA,EAAe,WAAU,QAAO,OAC7B,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MAAW;AAAA,MAAY;AAAA,MAAsB,OAAO,EAAE,cAAc;AAAA,MAAG,aAAa,EAAE,iBAAiB;AAAA,MAC5F;AAAA,MAAwC;AAAA,MACxC,QAAQ,OAAO,WAAW,aAAa,OAAO,EAAC,QAAQ,iBAAA,CAAiB,IAAI;AAAA,MACnF,UAAA;AAAA,QAAA,UAAU,oBAAC,SAAM,UAAS,WAAU,IAAI,EAAC,IAAI,EAAA,GAAK,UAAA,OAAA,CAAO;AAAA,QAC1D,oBAAC,YAAA,EAAW,UAAU,QAAQ,mBAAA,CAAwC;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAE9E;AAER;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accountConnector.js","sources":["../../src/account/accountConnector.js"],"sourcesContent":["// El conector de la cuenta: lo ÚNICO que un proyecto escribe para tener entrar (con su segundo paso), «¿Olvidó su\n// contraseña?», elegir una contraseña nueva, Mi perfil y su seguridad (@stackcraftmx/calcita/account). Las pantallas solo\n// llaman estas funciones; cada una habla con el backend del proyecto.\n//\n// Tres formas de tenerlo:\n// - createRestAccountConnector(apiClient): si el backend sigue el contrato REST (restAccountConnector.js), ya está;\n// - createDemoAccountConnector() de @stackcraftmx/calcita/account-demo: todo en la memoria del navegador, para arrancar\n// sin backend;\n// - uno propio: un objeto con estas funciones, o el REST con las que no coincidan cambiadas ({...rest, signIn: …}).\n//\n// Cada función regresa una promesa y, si el servidor dice que no, truena con un ApiError (RFC 9457): la pantalla enseña su\n// mensaje, y lo de cada campo en su campo. Las llaves van en camelCase: la traducción desde el backend va en el conector.\n//\n// Funciones puras, no renderizan: function (regla de la casa). Pruebas en accountConnector.test.js.\n\n/**\n * Quien está dentro, como lo pintan la barra y Mi perfil.\n *\n * @typedef {object} AccountUser\n * @property {string} id\n * @property {string} name\n * @property {string} email\n * @property {string} [subtitle] el renglón bajo el nombre (puesto · sucursal)\n * @property {string} [phone]\n * @property {string} [avatarUrl]\n */\n\n/**\n * La sesión: quién está dentro y sus permisos (los lee createAccessChecker).\n *\n * @typedef {object} AccountSession\n * @property {AccountUser} user\n * @property {string[]} [roles]\n * @property {string[]} [abilities]\n * @property {string[]} [labels] sus perfiles o etiquetas, ya traducidos (salen como chips en Mi perfil)\n */\n\n/**\n * El estado de la verificación en dos pasos (el que pinta TwoStepVerificationCard).\n *\n * @typedef {object} AccountSecurity\n * @property {boolean} isEnabled\n * @property {string|null} [enabledAt]\n * @property {Array<{id: string, name: string, createdAt: string, lastUsedAt?: string|null}>} passkeys\n * @property {{isEnabled: boolean, enabledAt?: string|null}} totp\n * @property {number} recoveryCodesRemaining\n */\n\n/**\n * Lo que regresa un cambio de seguridad: el estado nuevo y, si se generaron, los códigos de recuperación (se enseñan una\n * sola vez).\n *\n * @typedef {object} AccountSecurityChange\n * @property {AccountSecurity} security\n * @property {string[]} [recoveryCodes]\n */\n\n/**\n * Un equipo donde la cuenta está abierta.\n *\n * @typedef {object} AccountDeviceSession\n * @property {string} id\n * @property {string|null} [browser]\n * @property {string|null} [platform]\n * @property {boolean} [isMobile]\n * @property {string} startedAt instante ISO\n * @property {string|null} [network] a medias (187.190.x.x), nunca la IP completa\n * @property {boolean} [isCurrent]\n */\n\n/**\n * Un renglón de la bitácora de la cuenta.\n *\n * @typedef {object} AccountEvent\n * @property {string} id\n * @property {string} kind uno de ACCOUNT_EVENT_KIND\n * @property {string} occurredAt instante ISO\n * @property {string|null} [device]\n * @property {string|null} [network]\n */\n\n/**\n * El conector completo.\n *\n * @typedef {object} AccountConnector\n * @property {() => Promise<AccountSession|null>} loadSession quién está dentro; null si nadie\n * @property {(credentials: {email: string, password: string, remember: boolean}) => Promise<{secondStep: {methods: string[]}|null}>} signIn\n * con verificación en dos pasos regresa los métodos del segundo paso (SECOND_STEP_METHOD); sin ella, secondStep null\n * y ya está dentro\n * @property {(input: {method: string, code: string}) => Promise<void>} signInSecondStep el código de la app (totp) o uno\n * de recuperación (recoveryCode)\n * @property {() => Promise<object>} secondStepPasskeyOptions las opciones de WebAuthn para firmar (JSON del servidor)\n * @property {(credential: any) => Promise<void>} signInWithPasskey la firma del navegador (JSON de WebAuthn)\n * @property {() => Promise<void>} signOut\n * @property {(input: {email: string}) => Promise<void>} requestPasswordReset contesta igual exista o no el correo\n * @property {(input: {token: string, newPassword: string}) => Promise<void>} resetPassword desde el enlace del correo\n * @property {(input: {name: string, phone: string}) => Promise<{message?: string}|void>} updateProfile\n * @property {(input: {currentPassword?: string, newPassword: string}) => Promise<void>} changePassword\n * @property {() => Promise<AccountSecurity>} loadSecurity\n * @property {() => Promise<{secret: string, qrImageUrl?: string|null}>} startTotp una clave nueva (todavía sin activar)\n * @property {(code: string) => Promise<AccountSecurityChange>} confirmTotp\n * @property {(password: string) => Promise<AccountSecurityChange>} disableTotp\n * @property {() => Promise<object>} passkeyRegistrationOptions las opciones de WebAuthn para crear una llave\n * @property {(input: {name: string, credential: any}) => Promise<AccountSecurityChange>} savePasskey\n * @property {(input: {passkeyId: string, password: string}) => Promise<AccountSecurityChange>} removePasskey\n * @property {(password: string) => Promise<AccountSecurityChange>} regenerateRecoveryCodes\n * @property {() => Promise<AccountDeviceSession[]>} listSessions\n * @property {() => Promise<number>} closeOtherSessions cuántas cerró\n * @property {() => Promise<AccountEvent[]>} listActivity de lo más nuevo a lo más viejo\n * @property {() => Promise<string|null>} [demoTotpCode] SOLO en la demostración: el código que enseñaría la app ahorita\n * (con él sale el botón «Usar el código de ejemplo»)\n */\n\n/** Las funciones que todo conector trae (demoTotpCode es opcional: solo la demostración). */\nexport const ACCOUNT_CONNECTOR_FUNCTIONS = Object.freeze([\n 'loadSession', 'signIn', 'signInSecondStep', 'secondStepPasskeyOptions', 'signInWithPasskey', 'signOut', 'requestPasswordReset',\n 'resetPassword', 'updateProfile', 'changePassword', 'loadSecurity', 'startTotp', 'confirmTotp', 'disableTotp',\n 'passkeyRegistrationOptions', 'savePasskey', 'removePasskey', 'regenerateRecoveryCodes', 'listSessions', 'closeOtherSessions',\n 'listActivity',\n]);\n\n/**\n * Las funciones que le faltan al conector (un error de quien lo escribió: se avisa al arrancar, no al picar un botón).\n *\n * @param {object|null|undefined} connector\n * @returns {string[]}\n */\nexport function missingConnectorFunctionsOf(connector) {\n return ACCOUNT_CONNECTOR_FUNCTIONS.filter((name) => typeof connector?.[name] !== 'function');\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"accountConnector.js","sources":["../../src/account/accountConnector.js"],"sourcesContent":["// El conector de la cuenta: lo ÚNICO que un proyecto escribe para tener entrar (con su segundo paso), «¿Olvidó su\n// contraseña?», elegir una contraseña nueva, Mi perfil y su seguridad (@stackcraftmx/calcita/account). Las pantallas solo\n// llaman estas funciones; cada una habla con el backend del proyecto.\n//\n// Tres formas de tenerlo:\n// - createRestAccountConnector(apiClient): si el backend sigue el contrato REST (restAccountConnector.js), ya está;\n// - createDemoAccountConnector() de @stackcraftmx/calcita/account-demo: todo en la memoria del navegador, para arrancar\n// sin backend;\n// - uno propio: un objeto con estas funciones, o el REST con las que no coincidan cambiadas ({...rest, signIn: …}).\n//\n// Cada función regresa una promesa y, si el servidor dice que no, truena con un ApiError (RFC 9457): la pantalla enseña su\n// mensaje, y lo de cada campo en su campo. Las llaves van en camelCase: la traducción desde el backend va en el conector.\n//\n// Funciones puras, no renderizan: function (regla de la casa). Pruebas en accountConnector.test.js.\n\n/**\n * Quien está dentro, como lo pintan la barra y Mi perfil.\n *\n * @typedef {object} AccountUser\n * @property {string} id\n * @property {string} name\n * @property {string} email\n * @property {string} [subtitle] el renglón bajo el nombre (puesto · sucursal)\n * @property {string} [phone]\n * @property {string} [avatarUrl]\n */\n\n/**\n * La sesión: quién está dentro y sus permisos (los lee createAccessChecker).\n *\n * @typedef {object} AccountSession\n * @property {AccountUser} user\n * @property {string[]} [roles]\n * @property {string[]} [abilities]\n * @property {string[]} [labels] sus perfiles o etiquetas, ya traducidos (salen como chips en Mi perfil)\n */\n\n/**\n * El estado de la verificación en dos pasos (el que pinta TwoStepVerificationCard).\n *\n * @typedef {object} AccountSecurity\n * @property {boolean} isEnabled\n * @property {string|null} [enabledAt]\n * @property {Array<{id: string, name: string, createdAt: string, lastUsedAt?: string|null}>} passkeys\n * @property {{isEnabled: boolean, enabledAt?: string|null}} totp\n * @property {number} recoveryCodesRemaining\n */\n\n/**\n * Lo que regresa un cambio de seguridad: el estado nuevo y, si se generaron, los códigos de recuperación (se enseñan una\n * sola vez).\n *\n * @typedef {object} AccountSecurityChange\n * @property {AccountSecurity} security\n * @property {string[]} [recoveryCodes]\n */\n\n/**\n * Un equipo donde la cuenta está abierta.\n *\n * @typedef {object} AccountDeviceSession\n * @property {string} id\n * @property {string|null} [browser]\n * @property {string|null} [platform]\n * @property {boolean} [isMobile]\n * @property {string} startedAt instante ISO\n * @property {string|null} [network] a medias (187.190.x.x), nunca la IP completa\n * @property {boolean} [isCurrent]\n */\n\n/**\n * Un renglón de la bitácora de la cuenta.\n *\n * @typedef {object} AccountEvent\n * @property {string} id\n * @property {string} kind uno de ACCOUNT_EVENT_KIND\n * @property {string} occurredAt instante ISO\n * @property {string|null} [device]\n * @property {string|null} [network]\n */\n\n/**\n * El conector completo.\n *\n * @typedef {object} AccountConnector\n * @property {() => Promise<AccountSession|null>} loadSession quién está dentro; null si nadie\n * @property {(credentials: {email: string, password: string, remember: boolean}) => Promise<{secondStep: {methods: string[]}|null}>} signIn\n * con verificación en dos pasos regresa los métodos del segundo paso (SECOND_STEP_METHOD); sin ella, secondStep null\n * y ya está dentro\n * @property {(input: {method: string, code: string}) => Promise<void>} signInSecondStep el código de la app (totp) o uno\n * de recuperación (recoveryCode)\n * @property {() => Promise<object>} secondStepPasskeyOptions las opciones de WebAuthn para firmar (JSON del servidor)\n * @property {(credential: any) => Promise<void>} signInWithPasskey la firma del navegador (JSON de WebAuthn)\n * @property {() => Promise<void>} signOut\n * @property {(input: {email: string}) => Promise<void>} requestPasswordReset contesta igual exista o no el correo\n * @property {(input: {token: string, newPassword: string}) => Promise<void>} resetPassword desde el enlace del correo\n * @property {(input: {name: string, phone: string} & Record<string, any>) => Promise<{message?: string}|void>} updateProfile name, phone y\n * los campos propios de ProfileScreen (extraFields)\n * @property {(input: {currentPassword?: string, newPassword: string}) => Promise<void>} changePassword\n * @property {() => Promise<AccountSecurity>} loadSecurity\n * @property {() => Promise<{secret: string, qrImageUrl?: string|null}>} startTotp una clave nueva (todavía sin activar)\n * @property {(code: string) => Promise<AccountSecurityChange>} confirmTotp\n * @property {(password: string) => Promise<AccountSecurityChange>} disableTotp\n * @property {() => Promise<object>} passkeyRegistrationOptions las opciones de WebAuthn para crear una llave\n * @property {(input: {name: string, credential: any}) => Promise<AccountSecurityChange>} savePasskey\n * @property {(input: {passkeyId: string, password: string}) => Promise<AccountSecurityChange>} removePasskey\n * @property {(password: string) => Promise<AccountSecurityChange>} regenerateRecoveryCodes\n * @property {() => Promise<AccountDeviceSession[]>} listSessions\n * @property {() => Promise<number>} closeOtherSessions cuántas cerró\n * @property {() => Promise<AccountEvent[]>} listActivity de lo más nuevo a lo más viejo\n * @property {() => Promise<string|null>} [demoTotpCode] SOLO en la demostración: el código que enseñaría la app ahorita\n * (con él sale el botón «Usar el código de ejemplo»)\n */\n\n/** Las funciones que todo conector trae (demoTotpCode es opcional: solo la demostración). */\nexport const ACCOUNT_CONNECTOR_FUNCTIONS = Object.freeze([\n 'loadSession', 'signIn', 'signInSecondStep', 'secondStepPasskeyOptions', 'signInWithPasskey', 'signOut', 'requestPasswordReset',\n 'resetPassword', 'updateProfile', 'changePassword', 'loadSecurity', 'startTotp', 'confirmTotp', 'disableTotp',\n 'passkeyRegistrationOptions', 'savePasskey', 'removePasskey', 'regenerateRecoveryCodes', 'listSessions', 'closeOtherSessions',\n 'listActivity',\n]);\n\n/**\n * Las funciones que le faltan al conector (un error de quien lo escribió: se avisa al arrancar, no al picar un botón).\n *\n * @param {object|null|undefined} connector\n * @returns {string[]}\n */\nexport function missingConnectorFunctionsOf(connector) {\n return ACCOUNT_CONNECTOR_FUNCTIONS.filter((name) => typeof connector?.[name] !== 'function');\n}\n"],"names":[],"mappings":"AAmHY,MAAC,8BAA8B,OAAO,OAAO;AAAA,EACrD;AAAA,EAAe;AAAA,EAAU;AAAA,EAAoB;AAAA,EAA4B;AAAA,EAAqB;AAAA,EAAW;AAAA,EACzG;AAAA,EAAiB;AAAA,EAAiB;AAAA,EAAkB;AAAA,EAAgB;AAAA,EAAa;AAAA,EAAe;AAAA,EAChG;AAAA,EAA8B;AAAA,EAAe;AAAA,EAAiB;AAAA,EAA2B;AAAA,EAAgB;AAAA,EACzG;AACJ,CAAC;AAQM,SAAS,4BAA4B,WAAW;AACnD,SAAO,4BAA4B,OAAO,CAAC,SAAS,OAAO,YAAY,IAAI,MAAM,UAAU;AAC/F;"}
|
|
@@ -72,7 +72,8 @@ function createRestAccountConnector(apiClient, { paths = {} } = {}) {
|
|
|
72
72
|
resetPassword: async ({ token, newPassword }) => {
|
|
73
73
|
await apiClient.post(path.resetPassword, { token, new_password: newPassword });
|
|
74
74
|
},
|
|
75
|
-
|
|
75
|
+
// todo lo del formulario viaja: name, phone y los campos propios de ProfileScreen (extraFields) con sus nombres
|
|
76
|
+
updateProfile: async (profile) => await apiClient.put(path.profile, profile) ?? {},
|
|
76
77
|
changePassword: async ({ currentPassword, newPassword }) => {
|
|
77
78
|
await apiClient.put(path.password, { current_password: currentPassword, new_password: newPassword });
|
|
78
79
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"restAccountConnector.js","sources":["../../src/account/restAccountConnector.js"],"sourcesContent":["// El conector de la cuenta para un backend REST que sigue este contrato (llaves en snake_case, errores RFC 9457). Si el\n// backend lo sigue, conectar la cuenta es una línea:\n//\n// export const accountConnector = createRestAccountConnector(apiClient);\n//\n// Si alguna ruta se llama distinto, se cambia en `paths`; si alguna respuesta tiene otra forma, se reemplaza esa función:\n//\n// export const accountConnector = {...createRestAccountConnector(apiClient), signIn: async (credentials) => …};\n//\n// El contrato (las rutas por omisión, ACCOUNT_REST_PATHS):\n//\n// GET /auth/session → {user, roles, abilities, labels} | 401 si nadie está dentro\n// POST /auth/sign-in {email, password, remember} → {second_step: {methods} | null}\n// POST /auth/second-step {method, code} → 204 (method: totp | recovery_code)\n// GET /auth/second-step/passkey-options → opciones de WebAuthn para firmar\n// POST /auth/second-step/passkey {credential} → 204\n// POST /auth/sign-out → 204\n// POST /auth/forgot-password {email} → 204 (igual exista o no el correo)\n// POST /auth/reset-password {token, new_password} → 204\n// PUT /account {name, phone} → {message?}\n// PUT /account/password {current_password, new_password} → 204\n// GET /account/security → el estado de la verificación (abajo)\n// POST /account/security/totp → {secret, qr_image_url}\n// POST /account/security/totp/confirm {code} → {security, recovery_codes?}\n// DELETE /account/security/totp {password} → {security}\n// GET /account/security/passkeys/registration-options → opciones de WebAuthn para crear\n// POST /account/security/passkeys {name, credential} → {security, recovery_codes?}\n// DELETE /account/security/passkeys/{id} {password} → {security}\n// POST /account/security/recovery-codes {password} → {security, recovery_codes}\n// GET /account/sessions → [{id, browser, platform, is_mobile, started_at, network, is_current}]\n// POST /account/sessions/close-others → {closed}\n// GET /account/activity → [{id, kind, occurred_at, device, network}]\n//\n// El estado de la verificación: {is_enabled, enabled_at, passkeys: [{id, name, created_at, last_used_at}],\n// totp: {is_enabled, enabled_at}, recovery_codes_remaining}. Las opciones y la firma de WebAuthn viajan tal cual (las arma\n// y las revisa la librería del servidor: py_webauthn, @simplewebauthn/server, web-auth/webauthn-lib).\n//\n// Funciones puras, no renderizan: function (regla de la casa). Pruebas en restAccountConnector.test.js.\n\n/** Las rutas del contrato; se pegan a la baseUrl del cliente (/api + /auth/session). */\nexport const ACCOUNT_REST_PATHS = Object.freeze({\n session: '/auth/session',\n signIn: '/auth/sign-in',\n secondStep: '/auth/second-step',\n secondStepPasskeyOptions: '/auth/second-step/passkey-options',\n secondStepPasskey: '/auth/second-step/passkey',\n signOut: '/auth/sign-out',\n forgotPassword: '/auth/forgot-password',\n resetPassword: '/auth/reset-password',\n profile: '/account',\n password: '/account/password',\n security: '/account/security',\n totp: '/account/security/totp',\n totpConfirm: '/account/security/totp/confirm',\n passkeyRegistrationOptions: '/account/security/passkeys/registration-options',\n passkeys: '/account/security/passkeys',\n recoveryCodes: '/account/security/recovery-codes',\n sessions: '/account/sessions',\n closeOtherSessions: '/account/sessions/close-others',\n activity: '/account/activity',\n});\n\n// cómo se llama cada método del segundo paso en el contrato (snake_case) y en las pantallas (SECOND_STEP_METHOD)\nconst SECOND_STEP_METHOD_IN_API = Object.freeze({passkey: 'passkey', totp: 'totp', recoveryCode: 'recovery_code'});\nconst SECOND_STEP_METHOD_FROM_API = Object.freeze({passkey: 'passkey', totp: 'totp', recovery_code: 'recoveryCode'});\n\n/**\n * El estado de la verificación, del contrato a camelCase.\n *\n * @param {object} response\n * @returns {import('@/account/accountConnector.js').AccountSecurity}\n */\nexport function securityFromResponse(response) {\n return {\n isEnabled: Boolean(response.is_enabled),\n enabledAt: response.enabled_at ?? null,\n passkeys: (response.passkeys ?? []).map((passkey) => ({\n id: passkey.id, name: passkey.name, createdAt: passkey.created_at, lastUsedAt: passkey.last_used_at ?? null,\n })),\n totp: {isEnabled: Boolean(response.totp?.is_enabled), enabledAt: response.totp?.enabled_at ?? null},\n recoveryCodesRemaining: response.recovery_codes_remaining ?? 0,\n };\n}\n\n/**\n * Lo que regresa un cambio de seguridad: {security, recovery_codes?} → {security, recoveryCodes?}.\n *\n * @param {object} response\n * @returns {import('@/account/accountConnector.js').AccountSecurityChange}\n */\nexport function securityChangeFromResponse(response) {\n return {security: securityFromResponse(response.security), recoveryCodes: response.recovery_codes ?? undefined};\n}\n\n/**\n * @param {{get: Function, post: Function, put: Function, delete: Function}} apiClient el de createApiClient\n * @param {{paths?: Partial<Record<keyof typeof ACCOUNT_REST_PATHS, string>>}} [options] las rutas que se llaman distinto\n * @returns {import('@/account/accountConnector.js').AccountConnector}\n */\nexport function createRestAccountConnector(apiClient, {paths = {}} = {}) {\n const path = {...ACCOUNT_REST_PATHS, ...paths};\n\n return {\n loadSession: async () => {\n try {\n // sin el aviso de «sesión vencida»: aquí que no haya sesión es una respuesta normal\n return await apiClient.get(path.session, {skipUnauthenticatedHandler: true});\n } catch (error) {\n if (error?.isUnauthenticated) {\n return null;\n }\n throw error;\n }\n },\n signIn: async ({email, password, remember}) => {\n const response = await apiClient.post(path.signIn, {email, password, remember}, {skipUnauthenticatedHandler: true});\n const methods = response?.second_step?.methods;\n return {secondStep: methods ? {methods: methods.map((method) => SECOND_STEP_METHOD_FROM_API[method] ?? method)} : null};\n },\n signInSecondStep: async ({method, code}) => {\n await apiClient.post(path.secondStep, {method: SECOND_STEP_METHOD_IN_API[method] ?? method, code}, {skipUnauthenticatedHandler: true});\n },\n secondStepPasskeyOptions: () => apiClient.get(path.secondStepPasskeyOptions, {skipUnauthenticatedHandler: true}),\n signInWithPasskey: async (credential) => {\n await apiClient.post(path.secondStepPasskey, {credential}, {skipUnauthenticatedHandler: true});\n },\n signOut: async () => {\n await apiClient.post(path.signOut, undefined, {skipUnauthenticatedHandler: true});\n },\n requestPasswordReset: async ({email}) => {\n await apiClient.post(path.forgotPassword, {email});\n },\n resetPassword: async ({token, newPassword}) => {\n await apiClient.post(path.resetPassword, {token, new_password: newPassword});\n },\n updateProfile: async ({name, phone}) => (await apiClient.put(path.profile, {name, phone})) ?? {},\n changePassword: async ({currentPassword, newPassword}) => {\n await apiClient.put(path.password, {current_password: currentPassword, new_password: newPassword});\n },\n loadSecurity: async () => securityFromResponse(await apiClient.get(path.security)),\n startTotp: async () => {\n const response = await apiClient.post(path.totp);\n return {secret: response.secret, qrImageUrl: response.qr_image_url ?? null};\n },\n confirmTotp: async (code) => securityChangeFromResponse(await apiClient.post(path.totpConfirm, {code})),\n disableTotp: async (password) => securityChangeFromResponse(await apiClient.delete(path.totp, {body: {password}})),\n passkeyRegistrationOptions: () => apiClient.get(path.passkeyRegistrationOptions),\n savePasskey: async ({name, credential}) => securityChangeFromResponse(await apiClient.post(path.passkeys, {name, credential})),\n removePasskey: async ({passkeyId, password}) => securityChangeFromResponse(\n await apiClient.delete(`${path.passkeys}/${encodeURIComponent(passkeyId)}`, {body: {password}}),\n ),\n regenerateRecoveryCodes: async (password) => securityChangeFromResponse(await apiClient.post(path.recoveryCodes, {password})),\n listSessions: async () => (await apiClient.get(path.sessions)).map((session) => ({\n id: session.id, browser: session.browser, platform: session.platform, isMobile: Boolean(session.is_mobile),\n startedAt: session.started_at, network: session.network, isCurrent: Boolean(session.is_current),\n })),\n closeOtherSessions: async () => (await apiClient.post(path.closeOtherSessions))?.closed ?? 0,\n listActivity: async () => (await apiClient.get(path.activity)).map((event) => ({\n id: event.id, kind: event.kind, occurredAt: event.occurred_at, device: event.device, network: event.network,\n })),\n };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"restAccountConnector.js","sources":["../../src/account/restAccountConnector.js"],"sourcesContent":["// El conector de la cuenta para un backend REST que sigue este contrato (llaves en snake_case, errores RFC 9457). Si el\n// backend lo sigue, conectar la cuenta es una línea:\n//\n// export const accountConnector = createRestAccountConnector(apiClient);\n//\n// Si alguna ruta se llama distinto, se cambia en `paths`; si alguna respuesta tiene otra forma, se reemplaza esa función:\n//\n// export const accountConnector = {...createRestAccountConnector(apiClient), signIn: async (credentials) => …};\n//\n// El contrato (las rutas por omisión, ACCOUNT_REST_PATHS):\n//\n// GET /auth/session → {user, roles, abilities, labels} | 401 si nadie está dentro\n// POST /auth/sign-in {email, password, remember} → {second_step: {methods} | null}\n// POST /auth/second-step {method, code} → 204 (method: totp | recovery_code)\n// GET /auth/second-step/passkey-options → opciones de WebAuthn para firmar\n// POST /auth/second-step/passkey {credential} → 204\n// POST /auth/sign-out → 204\n// POST /auth/forgot-password {email} → 204 (igual exista o no el correo)\n// POST /auth/reset-password {token, new_password} → 204\n// PUT /account {name, phone} → {message?}\n// PUT /account/password {current_password, new_password} → 204\n// GET /account/security → el estado de la verificación (abajo)\n// POST /account/security/totp → {secret, qr_image_url}\n// POST /account/security/totp/confirm {code} → {security, recovery_codes?}\n// DELETE /account/security/totp {password} → {security}\n// GET /account/security/passkeys/registration-options → opciones de WebAuthn para crear\n// POST /account/security/passkeys {name, credential} → {security, recovery_codes?}\n// DELETE /account/security/passkeys/{id} {password} → {security}\n// POST /account/security/recovery-codes {password} → {security, recovery_codes}\n// GET /account/sessions → [{id, browser, platform, is_mobile, started_at, network, is_current}]\n// POST /account/sessions/close-others → {closed}\n// GET /account/activity → [{id, kind, occurred_at, device, network}]\n//\n// El estado de la verificación: {is_enabled, enabled_at, passkeys: [{id, name, created_at, last_used_at}],\n// totp: {is_enabled, enabled_at}, recovery_codes_remaining}. Las opciones y la firma de WebAuthn viajan tal cual (las arma\n// y las revisa la librería del servidor: py_webauthn, @simplewebauthn/server, web-auth/webauthn-lib).\n//\n// Funciones puras, no renderizan: function (regla de la casa). Pruebas en restAccountConnector.test.js.\n\n/** Las rutas del contrato; se pegan a la baseUrl del cliente (/api + /auth/session). */\nexport const ACCOUNT_REST_PATHS = Object.freeze({\n session: '/auth/session',\n signIn: '/auth/sign-in',\n secondStep: '/auth/second-step',\n secondStepPasskeyOptions: '/auth/second-step/passkey-options',\n secondStepPasskey: '/auth/second-step/passkey',\n signOut: '/auth/sign-out',\n forgotPassword: '/auth/forgot-password',\n resetPassword: '/auth/reset-password',\n profile: '/account',\n password: '/account/password',\n security: '/account/security',\n totp: '/account/security/totp',\n totpConfirm: '/account/security/totp/confirm',\n passkeyRegistrationOptions: '/account/security/passkeys/registration-options',\n passkeys: '/account/security/passkeys',\n recoveryCodes: '/account/security/recovery-codes',\n sessions: '/account/sessions',\n closeOtherSessions: '/account/sessions/close-others',\n activity: '/account/activity',\n});\n\n// cómo se llama cada método del segundo paso en el contrato (snake_case) y en las pantallas (SECOND_STEP_METHOD)\nconst SECOND_STEP_METHOD_IN_API = Object.freeze({passkey: 'passkey', totp: 'totp', recoveryCode: 'recovery_code'});\nconst SECOND_STEP_METHOD_FROM_API = Object.freeze({passkey: 'passkey', totp: 'totp', recovery_code: 'recoveryCode'});\n\n/**\n * El estado de la verificación, del contrato a camelCase.\n *\n * @param {object} response\n * @returns {import('@/account/accountConnector.js').AccountSecurity}\n */\nexport function securityFromResponse(response) {\n return {\n isEnabled: Boolean(response.is_enabled),\n enabledAt: response.enabled_at ?? null,\n passkeys: (response.passkeys ?? []).map((passkey) => ({\n id: passkey.id, name: passkey.name, createdAt: passkey.created_at, lastUsedAt: passkey.last_used_at ?? null,\n })),\n totp: {isEnabled: Boolean(response.totp?.is_enabled), enabledAt: response.totp?.enabled_at ?? null},\n recoveryCodesRemaining: response.recovery_codes_remaining ?? 0,\n };\n}\n\n/**\n * Lo que regresa un cambio de seguridad: {security, recovery_codes?} → {security, recoveryCodes?}.\n *\n * @param {object} response\n * @returns {import('@/account/accountConnector.js').AccountSecurityChange}\n */\nexport function securityChangeFromResponse(response) {\n return {security: securityFromResponse(response.security), recoveryCodes: response.recovery_codes ?? undefined};\n}\n\n/**\n * @param {{get: Function, post: Function, put: Function, delete: Function}} apiClient el de createApiClient\n * @param {{paths?: Partial<Record<keyof typeof ACCOUNT_REST_PATHS, string>>}} [options] las rutas que se llaman distinto\n * @returns {import('@/account/accountConnector.js').AccountConnector}\n */\nexport function createRestAccountConnector(apiClient, {paths = {}} = {}) {\n const path = {...ACCOUNT_REST_PATHS, ...paths};\n\n return {\n loadSession: async () => {\n try {\n // sin el aviso de «sesión vencida»: aquí que no haya sesión es una respuesta normal\n return await apiClient.get(path.session, {skipUnauthenticatedHandler: true});\n } catch (error) {\n if (error?.isUnauthenticated) {\n return null;\n }\n throw error;\n }\n },\n signIn: async ({email, password, remember}) => {\n const response = await apiClient.post(path.signIn, {email, password, remember}, {skipUnauthenticatedHandler: true});\n const methods = response?.second_step?.methods;\n return {secondStep: methods ? {methods: methods.map((method) => SECOND_STEP_METHOD_FROM_API[method] ?? method)} : null};\n },\n signInSecondStep: async ({method, code}) => {\n await apiClient.post(path.secondStep, {method: SECOND_STEP_METHOD_IN_API[method] ?? method, code}, {skipUnauthenticatedHandler: true});\n },\n secondStepPasskeyOptions: () => apiClient.get(path.secondStepPasskeyOptions, {skipUnauthenticatedHandler: true}),\n signInWithPasskey: async (credential) => {\n await apiClient.post(path.secondStepPasskey, {credential}, {skipUnauthenticatedHandler: true});\n },\n signOut: async () => {\n await apiClient.post(path.signOut, undefined, {skipUnauthenticatedHandler: true});\n },\n requestPasswordReset: async ({email}) => {\n await apiClient.post(path.forgotPassword, {email});\n },\n resetPassword: async ({token, newPassword}) => {\n await apiClient.post(path.resetPassword, {token, new_password: newPassword});\n },\n // todo lo del formulario viaja: name, phone y los campos propios de ProfileScreen (extraFields) con sus nombres\n updateProfile: async (profile) => (await apiClient.put(path.profile, profile)) ?? {},\n changePassword: async ({currentPassword, newPassword}) => {\n await apiClient.put(path.password, {current_password: currentPassword, new_password: newPassword});\n },\n loadSecurity: async () => securityFromResponse(await apiClient.get(path.security)),\n startTotp: async () => {\n const response = await apiClient.post(path.totp);\n return {secret: response.secret, qrImageUrl: response.qr_image_url ?? null};\n },\n confirmTotp: async (code) => securityChangeFromResponse(await apiClient.post(path.totpConfirm, {code})),\n disableTotp: async (password) => securityChangeFromResponse(await apiClient.delete(path.totp, {body: {password}})),\n passkeyRegistrationOptions: () => apiClient.get(path.passkeyRegistrationOptions),\n savePasskey: async ({name, credential}) => securityChangeFromResponse(await apiClient.post(path.passkeys, {name, credential})),\n removePasskey: async ({passkeyId, password}) => securityChangeFromResponse(\n await apiClient.delete(`${path.passkeys}/${encodeURIComponent(passkeyId)}`, {body: {password}}),\n ),\n regenerateRecoveryCodes: async (password) => securityChangeFromResponse(await apiClient.post(path.recoveryCodes, {password})),\n listSessions: async () => (await apiClient.get(path.sessions)).map((session) => ({\n id: session.id, browser: session.browser, platform: session.platform, isMobile: Boolean(session.is_mobile),\n startedAt: session.started_at, network: session.network, isCurrent: Boolean(session.is_current),\n })),\n closeOtherSessions: async () => (await apiClient.post(path.closeOtherSessions))?.closed ?? 0,\n listActivity: async () => (await apiClient.get(path.activity)).map((event) => ({\n id: event.id, kind: event.kind, occurredAt: event.occurred_at, device: event.device, network: event.network,\n })),\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -8,12 +8,12 @@ import { createLogger } from "../observability/logger.js";
|
|
|
8
8
|
import ErrorPage, { ERROR_PAGE_KIND } from "../ui/layout/ErrorPage.js";
|
|
9
9
|
const log = createLogger("navigation");
|
|
10
10
|
const NOT_FOUND_STATUS = 404;
|
|
11
|
-
function AccessOutlet({ access }) {
|
|
11
|
+
function AccessOutlet({ access, fallback }) {
|
|
12
12
|
const parentContext = useOutletContext();
|
|
13
|
-
return /* @__PURE__ */ jsx(RequireAccess, { access, children: /* @__PURE__ */ jsx(Outlet, { context: parentContext }) });
|
|
13
|
+
return /* @__PURE__ */ jsx(RequireAccess, { access, fallback, children: /* @__PURE__ */ jsx(Outlet, { context: parentContext }) });
|
|
14
14
|
}
|
|
15
|
-
function withAccess(access, routes) {
|
|
16
|
-
return { element: /* @__PURE__ */ jsx(AccessOutlet, { access }), children: routes };
|
|
15
|
+
function withAccess(access, routes, { fallback } = {}) {
|
|
16
|
+
return { element: /* @__PURE__ */ jsx(AccessOutlet, { access, fallback }), children: routes };
|
|
17
17
|
}
|
|
18
18
|
function useIsNavigating() {
|
|
19
19
|
return useNavigation().state === "loading";
|
|
@@ -28,7 +28,7 @@ function withChunkRecovery(routes) {
|
|
|
28
28
|
function reloadPage() {
|
|
29
29
|
window.location.reload();
|
|
30
30
|
}
|
|
31
|
-
function RouteErrorScreen() {
|
|
31
|
+
function RouteErrorScreen({ onError, ...errorPageProps } = {}) {
|
|
32
32
|
const error = useRouteError();
|
|
33
33
|
const isNotFound = isRouteErrorResponse(error) && error.status === NOT_FOUND_STATUS;
|
|
34
34
|
const kind = isNotFound ? ERROR_PAGE_KIND.notFound : isStaleChunkError(error) ? ERROR_PAGE_KIND.newVersion : ERROR_PAGE_KIND.serverError;
|
|
@@ -36,8 +36,9 @@ function RouteErrorScreen() {
|
|
|
36
36
|
if (!isNotFound) {
|
|
37
37
|
log.error("una pantalla no se pudo pintar", { name: error?.name, message: error?.message ?? String(error) });
|
|
38
38
|
}
|
|
39
|
+
onError?.(error, kind);
|
|
39
40
|
}, [error, isNotFound]);
|
|
40
|
-
return /* @__PURE__ */ jsx(ErrorPage, { kind, onRetry: isNotFound ? void 0 : reloadPage });
|
|
41
|
+
return /* @__PURE__ */ jsx(ErrorPage, { kind, onRetry: isNotFound ? void 0 : reloadPage, ...errorPageProps });
|
|
41
42
|
}
|
|
42
43
|
export {
|
|
43
44
|
RouteErrorScreen,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reactRouterRoutes.js","sources":["../../src/adapters/reactRouterRoutes.jsx"],"sourcesContent":["// Lo que conecta los permisos y la barra con react-router (@stackcraftmx/calcita/react-router):\n//\n// withAccess(access, routes) un grupo de rutas con la regla del menu.js de su módulo: quien no puede ve la página de\n// «sin permiso» en lugar de la pantalla\n// useIsNavigating() la pantalla siguiente se está descargando (rutas lazy): para la rayita de AppLayout\n// withChunkRecovery(routes) las rutas lazy recargan UNA vez si su archivo es de una versión que ya no existe\n// RouteErrorScreen la pantalla de error de una ruta (errorElement / ErrorBoundary del router): «Hay una\n// versión nueva» o «Algo falló», con su botón para recargar\n//\n// import {access} from '@/modules/users/menu.js';\n// import {routes} from '@/modules/users/routes.js';\n// children: [withAccess(access, routes)]\n//\n// OJO: useIsNavigating solo funciona con un router de datos (createBrowserRouter); con <BrowserRouter> no hay estado de\n// navegación.\n//\n// Genéricos del paquete: function (regla de la casa).\nimport {useEffect} from 'react';\nimport {Outlet, isRouteErrorResponse, useNavigation, useOutletContext, useRouteError} from 'react-router';\n\nimport RequireAccess from '@/access/RequireAccess.jsx';\nimport {isStaleChunkError, recoverFromStaleChunk} from '@/api/staleChunks.js';\nimport {createLogger} from '@/observability/logger.js';\nimport ErrorPage, {ERROR_PAGE_KIND} from '@/ui/layout/ErrorPage.jsx';\n\nconst log = createLogger('navigation');\nconst NOT_FOUND_STATUS = 404;\n\n/**\n * El grupo con permiso: RequireAccess alrededor de las rutas hijas.\n *\n * OJO, trampa pagada: un <Outlet /> nuevo corta el contexto que la ruta de arriba le pasa a sus hijas (<Outlet\n * context={...} />, useOutletContext); el inicio de los ejemplos tronaba sin el menú. Aquí se pasa tal cual.\n *\n * @param {{access: object|undefined}} props\n */\nfunction AccessOutlet({access}) {\n const parentContext = useOutletContext();\n return <RequireAccess access={access}><Outlet context={parentContext} /></RequireAccess>;\n}\n\n/**\n * Un grupo de rutas sin camino propio que pide permiso antes de pintar a sus hijas.\n *\n * @param {object|undefined} access la regla (accessRules.js); sin ella, el grupo es de todos\n * @param {object[]} routes las rutas del módulo\n * @returns {object} la ruta de react-router\n */\nexport function withAccess(access, routes) {\n return {element: <AccessOutlet access={access} />, children: routes};\n}\n\n/**\n * ¿Se está descargando la pantalla siguiente?\n *\n * @returns {boolean}\n */\nexport function useIsNavigating() {\n return useNavigation().state === 'loading';\n}\n\n/**\n * Las rutas con la recuperación de archivos viejos (staleChunks.js): cada `lazy`, también la de las hijas, recarga la\n * página UNA vez si el archivo de su pantalla ya no existe porque se publicó otra versión. Lo demás de la ruta no cambia.\n *\n * @param {object[]} routes\n * @returns {object[]}\n */\nexport function withChunkRecovery(routes) {\n return routes.map((route) => ({\n ...route,\n ...(typeof route.lazy === 'function' && {lazy: () => route.lazy().catch(recoverFromStaleChunk)}),\n ...(route.children && {children: withChunkRecovery(route.children)}),\n }));\n}\n\n/** Recargar la página: trae la versión publicada. */\nfunction reloadPage() {\n window.location.reload();\n}\n\n/**\n * La pantalla de error de una ruta (la que el router enseña cuando la pantalla truena o no se pudo descargar): una ruta\n * que no existe es «Esta página no existe»; un archivo de otra versión, «Hay una versión nueva»; lo demás, «Algo falló de\n * nuestro lado». Las dos últimas con su botón para recargar.\n *\n * createBrowserRouter([{path: '/', element: <App />, ErrorBoundary: RouteErrorScreen, children: [...]}])\n */\nexport function RouteErrorScreen() {\n const error = useRouteError();\n const isNotFound = isRouteErrorResponse(error) && error.status === NOT_FOUND_STATUS;\n const kind = isNotFound ? ERROR_PAGE_KIND.notFound : (isStaleChunkError(error) ? ERROR_PAGE_KIND.newVersion : ERROR_PAGE_KIND.serverError);\n\n useEffect(() => {\n if (!isNotFound) {\n log.error('una pantalla no se pudo pintar', {name: error?.name, message: error?.message ?? String(error)});\n }\n }, [error, isNotFound]);\n\n return <ErrorPage kind={kind} onRetry={isNotFound ? undefined : reloadPage} />;\n}\n"],"names":[],"mappings":";;;;;;;;AAyBA,MAAM,MAAM,aAAa,YAAY;AACrC,MAAM,mBAAmB;AAUzB,SAAS,aAAa,EAAC,
|
|
1
|
+
{"version":3,"file":"reactRouterRoutes.js","sources":["../../src/adapters/reactRouterRoutes.jsx"],"sourcesContent":["// Lo que conecta los permisos y la barra con react-router (@stackcraftmx/calcita/react-router):\n//\n// withAccess(access, routes) un grupo de rutas con la regla del menu.js de su módulo: quien no puede ve la página de\n// «sin permiso» en lugar de la pantalla\n// useIsNavigating() la pantalla siguiente se está descargando (rutas lazy): para la rayita de AppLayout\n// withChunkRecovery(routes) las rutas lazy recargan UNA vez si su archivo es de una versión que ya no existe\n// RouteErrorScreen la pantalla de error de una ruta (errorElement / ErrorBoundary del router): «Hay una\n// versión nueva» o «Algo falló», con su botón para recargar\n//\n// import {access} from '@/modules/users/menu.js';\n// import {routes} from '@/modules/users/routes.js';\n// children: [withAccess(access, routes)]\n//\n// OJO: useIsNavigating solo funciona con un router de datos (createBrowserRouter); con <BrowserRouter> no hay estado de\n// navegación.\n//\n// Genéricos del paquete: function (regla de la casa).\nimport {useEffect} from 'react';\nimport {Outlet, isRouteErrorResponse, useNavigation, useOutletContext, useRouteError} from 'react-router';\n\nimport RequireAccess from '@/access/RequireAccess.jsx';\nimport {isStaleChunkError, recoverFromStaleChunk} from '@/api/staleChunks.js';\nimport {createLogger} from '@/observability/logger.js';\nimport ErrorPage, {ERROR_PAGE_KIND} from '@/ui/layout/ErrorPage.jsx';\n\nconst log = createLogger('navigation');\nconst NOT_FOUND_STATUS = 404;\n\n/**\n * El grupo con permiso: RequireAccess alrededor de las rutas hijas.\n *\n * OJO, trampa pagada: un <Outlet /> nuevo corta el contexto que la ruta de arriba le pasa a sus hijas (<Outlet\n * context={...} />, useOutletContext); el inicio de los ejemplos tronaba sin el menú. Aquí se pasa tal cual.\n *\n * @param {{access: object|undefined, fallback?: import('react').ReactNode}} props\n */\nfunction AccessOutlet({access, fallback}) {\n const parentContext = useOutletContext();\n return <RequireAccess access={access} fallback={fallback}><Outlet context={parentContext} /></RequireAccess>;\n}\n\n/**\n * Un grupo de rutas sin camino propio que pide permiso antes de pintar a sus hijas.\n *\n * @param {object|undefined} access la regla (accessRules.js); sin ella, el grupo es de todos\n * @param {object[]} routes las rutas del módulo\n * @param {{fallback?: import('react').ReactNode}} [options] fallback: lo que ve quien no tiene permiso; por omisión la página\n * de «sin permiso» de la casa (con ErrorPage y sus props se arma una propia: un botón para pedir acceso)\n * @returns {object} la ruta de react-router\n */\nexport function withAccess(access, routes, {fallback} = {}) {\n return {element: <AccessOutlet access={access} fallback={fallback} />, children: routes};\n}\n\n/**\n * ¿Se está descargando la pantalla siguiente?\n *\n * @returns {boolean}\n */\nexport function useIsNavigating() {\n return useNavigation().state === 'loading';\n}\n\n/**\n * Las rutas con la recuperación de archivos viejos (staleChunks.js): cada `lazy`, también la de las hijas, recarga la\n * página UNA vez si el archivo de su pantalla ya no existe porque se publicó otra versión. Lo demás de la ruta no cambia.\n *\n * @param {object[]} routes\n * @returns {object[]}\n */\nexport function withChunkRecovery(routes) {\n return routes.map((route) => ({\n ...route,\n ...(typeof route.lazy === 'function' && {lazy: () => route.lazy().catch(recoverFromStaleChunk)}),\n ...(route.children && {children: withChunkRecovery(route.children)}),\n }));\n}\n\n/** Recargar la página: trae la versión publicada. */\nfunction reloadPage() {\n window.location.reload();\n}\n\n/**\n * La pantalla de error de una ruta (la que el router enseña cuando la pantalla truena o no se pudo descargar): una ruta\n * que no existe es «Esta página no existe»; un archivo de otra versión, «Hay una versión nueva»; lo demás, «Algo falló de\n * nuestro lado». Las dos últimas con su botón para recargar.\n *\n * createBrowserRouter([{path: '/', element: <App />, ErrorBoundary: RouteErrorScreen, children: [...]}])\n *\n * Con props (como errorElement) se extiende igual que ErrorPage, y onError recibe lo que tronó (para Sentry o su aviso):\n * {errorElement: <RouteErrorScreen onError={(error) => reportError(error)} extraActions={<SupportButton />} />}\n *\n * @param {object} [props] las de ErrorPage (extraActions, actions, children, icon, homePath, onGoHome, texts) y:\n * @param {(error: unknown, kind: string) => void} [props.onError] cada error que llega aquí, con su tipo (ERROR_PAGE_KIND)\n */\nexport function RouteErrorScreen({onError, ...errorPageProps} = {}) {\n const error = useRouteError();\n const isNotFound = isRouteErrorResponse(error) && error.status === NOT_FOUND_STATUS;\n const kind = isNotFound ? ERROR_PAGE_KIND.notFound : (isStaleChunkError(error) ? ERROR_PAGE_KIND.newVersion : ERROR_PAGE_KIND.serverError);\n\n useEffect(() => {\n if (!isNotFound) {\n log.error('una pantalla no se pudo pintar', {name: error?.name, message: error?.message ?? String(error)});\n }\n onError?.(error, kind);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [error, isNotFound]);\n\n return <ErrorPage kind={kind} onRetry={isNotFound ? undefined : reloadPage} {...errorPageProps} />;\n}\n"],"names":[],"mappings":";;;;;;;;AAyBA,MAAM,MAAM,aAAa,YAAY;AACrC,MAAM,mBAAmB;AAUzB,SAAS,aAAa,EAAC,QAAQ,YAAW;AACtC,QAAM,gBAAgB,iBAAA;AACtB,SAAO,oBAAC,iBAAc,QAAgB,UAAoB,8BAAC,QAAA,EAAO,SAAS,eAAe,EAAA,CAAE;AAChG;AAWO,SAAS,WAAW,QAAQ,QAAQ,EAAC,SAAA,IAAY,CAAA,GAAI;AACxD,SAAO,EAAC,SAAS,oBAAC,cAAA,EAAa,QAAgB,SAAA,CAAoB,GAAI,UAAU,OAAA;AACrF;AAOO,SAAS,kBAAkB;AAC9B,SAAO,cAAA,EAAgB,UAAU;AACrC;AASO,SAAS,kBAAkB,QAAQ;AACtC,SAAO,OAAO,IAAI,CAAC,WAAW;AAAA,IAC1B,GAAG;AAAA,IACH,GAAI,OAAO,MAAM,SAAS,cAAc,EAAC,MAAM,MAAM,MAAM,KAAA,EAAO,MAAM,qBAAqB,EAAA;AAAA,IAC7F,GAAI,MAAM,YAAY,EAAC,UAAU,kBAAkB,MAAM,QAAQ,EAAA;AAAA,EAAC,EACpE;AACN;AAGA,SAAS,aAAa;AAClB,SAAO,SAAS,OAAA;AACpB;AAeO,SAAS,iBAAiB,EAAC,SAAS,GAAG,eAAA,IAAkB,CAAA,GAAI;AAChE,QAAM,QAAQ,cAAA;AACd,QAAM,aAAa,qBAAqB,KAAK,KAAK,MAAM,WAAW;AACnE,QAAM,OAAO,aAAa,gBAAgB,WAAY,kBAAkB,KAAK,IAAI,gBAAgB,aAAa,gBAAgB;AAE9H,YAAU,MAAM;AACZ,QAAI,CAAC,YAAY;AACb,UAAI,MAAM,kCAAkC,EAAC,MAAM,OAAO,MAAM,SAAS,OAAO,WAAW,OAAO,KAAK,EAAA,CAAE;AAAA,IAC7G;AACA,cAAU,OAAO,IAAI;AAAA,EAEzB,GAAG,CAAC,OAAO,UAAU,CAAC;AAEtB,SAAO,oBAAC,aAAU,MAAY,SAAS,aAAa,SAAY,YAAa,GAAG,gBAAgB;AACpG;"}
|
|
@@ -54,6 +54,10 @@ function retryAfterSecondsOf(retryAfterHeader) {
|
|
|
54
54
|
const retryAt = Date.parse(retryAfterHeader);
|
|
55
55
|
return Number.isNaN(retryAt) ? null : Math.max(0, Math.ceil((retryAt - Date.now()) / 1e3));
|
|
56
56
|
}
|
|
57
|
+
function toProblemOrNull(toProblem, body, response) {
|
|
58
|
+
const problem = body === null ? null : toProblem(body, response);
|
|
59
|
+
return problem && typeof problem === "object" && !Array.isArray(problem) ? problem : null;
|
|
60
|
+
}
|
|
57
61
|
async function readProblem(response) {
|
|
58
62
|
try {
|
|
59
63
|
const body = JSON.parse(await response.text());
|
|
@@ -62,6 +66,10 @@ async function readProblem(response) {
|
|
|
62
66
|
return null;
|
|
63
67
|
}
|
|
64
68
|
}
|
|
69
|
+
async function apiErrorOf(response, { toProblem = (body) => body } = {}) {
|
|
70
|
+
const problem = toProblemOrNull(toProblem, await readProblem(response), response);
|
|
71
|
+
return new ApiError({ status: response.status, problem, retryAfterSeconds: retryAfterSecondsOf(response.headers.get("retry-after")) });
|
|
72
|
+
}
|
|
65
73
|
function combinedSignalOf(signal, timeoutMilliseconds) {
|
|
66
74
|
const signals = [signal, timeoutMilliseconds > 0 ? AbortSignal.timeout(timeoutMilliseconds) : null].filter(Boolean);
|
|
67
75
|
if (signals.length < 2) {
|
|
@@ -93,8 +101,20 @@ function createApiClient({
|
|
|
93
101
|
csrf = null,
|
|
94
102
|
timeoutMilliseconds: defaultTimeoutMilliseconds = DEFAULT_TIMEOUT_MILLISECONDS,
|
|
95
103
|
onUnauthenticated,
|
|
104
|
+
onRequest,
|
|
105
|
+
onResponse,
|
|
106
|
+
onError,
|
|
107
|
+
toProblem = (body) => body,
|
|
108
|
+
mapResponse = (body) => body,
|
|
96
109
|
fetch: fetchImplementation
|
|
97
110
|
} = {}) {
|
|
111
|
+
const notify = (eventName, handler, ...eventArguments) => {
|
|
112
|
+
try {
|
|
113
|
+
handler?.(...eventArguments);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
log.error(`el evento ${eventName} del proyecto tronó`, { message: error?.message });
|
|
116
|
+
}
|
|
117
|
+
};
|
|
98
118
|
const fetchOf = () => fetchImplementation ?? globalThis.fetch;
|
|
99
119
|
const csrfCookieName = csrf?.cookieName ?? "XSRF-TOKEN";
|
|
100
120
|
const csrfHeaderName = csrf?.headerName ?? "X-XSRF-TOKEN";
|
|
@@ -137,27 +157,39 @@ function createApiClient({
|
|
|
137
157
|
throw new ApiError({ status: API_ERROR_STATUS.offline });
|
|
138
158
|
}
|
|
139
159
|
};
|
|
140
|
-
const request = async (method, path, {
|
|
160
|
+
const request = async (method, path, options = {}) => {
|
|
161
|
+
const url = apiUrlOf(baseUrl, path, options.params);
|
|
162
|
+
try {
|
|
163
|
+
return await requestOnce(method, url, options);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (error instanceof ApiError) {
|
|
166
|
+
notify("onError", onError, error, { method, url });
|
|
167
|
+
}
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const requestOnce = async (method, url, {
|
|
141
172
|
body,
|
|
142
|
-
params,
|
|
143
173
|
signal,
|
|
144
174
|
headers,
|
|
145
175
|
parse = API_RESPONSE_PARSE.json,
|
|
146
176
|
skipUnauthenticatedHandler = false,
|
|
147
177
|
timeoutMilliseconds = defaultTimeoutMilliseconds
|
|
148
178
|
} = {}) => {
|
|
149
|
-
|
|
179
|
+
notify("onRequest", onRequest, { method, url });
|
|
180
|
+
const startedAt = Date.now();
|
|
150
181
|
let response = await send(method, url, { body, signal, headers, timeoutMilliseconds });
|
|
151
182
|
if (response.status === CSRF_TOKEN_MISMATCH_STATUS && csrf?.refreshPath) {
|
|
152
183
|
log.info("token CSRF vencido: se renueva y se reintenta", { method, url });
|
|
153
184
|
await send("GET", apiUrlOf(baseUrl, csrf.refreshPath), { signal, timeoutMilliseconds });
|
|
154
185
|
response = await send(method, url, { body, signal, headers, timeoutMilliseconds });
|
|
155
186
|
}
|
|
187
|
+
notify("onResponse", onResponse, { method, url, status: response.status, durationMilliseconds: Date.now() - startedAt });
|
|
156
188
|
if (publishedBuildHeader) {
|
|
157
189
|
reportPublishedBuild(response.headers.get(publishedBuildHeader));
|
|
158
190
|
}
|
|
159
191
|
if (!response.ok) {
|
|
160
|
-
const problem = await readProblem(response);
|
|
192
|
+
const problem = toProblemOrNull(toProblem, await readProblem(response), response);
|
|
161
193
|
const requestedLanguage = headersOf(url, void 0, headers)["Accept-Language"];
|
|
162
194
|
const responseLanguage = response.headers.get("content-language");
|
|
163
195
|
const apiError = new ApiError({
|
|
@@ -188,12 +220,14 @@ function createApiClient({
|
|
|
188
220
|
if (!text) {
|
|
189
221
|
return null;
|
|
190
222
|
}
|
|
223
|
+
let json;
|
|
191
224
|
try {
|
|
192
|
-
|
|
225
|
+
json = JSON.parse(text);
|
|
193
226
|
} catch {
|
|
194
227
|
log.error(`${method} ${url} respondió algo que no es JSON`, { contentType: response.headers.get("content-type") });
|
|
195
228
|
throw new ApiError({ status: SERVER_ERROR_STATUS });
|
|
196
229
|
}
|
|
230
|
+
return mapResponse(json, { method, url });
|
|
197
231
|
};
|
|
198
232
|
const download = async (path, { params, signal, onDownloadProgress, fallbackFileName = "descarga", timeoutMilliseconds = NO_TIMEOUT } = {}) => {
|
|
199
233
|
const response = await request("GET", path, { params, signal, parse: API_RESPONSE_PARSE.response, timeoutMilliseconds });
|
|
@@ -219,6 +253,7 @@ function createApiClient({
|
|
|
219
253
|
}
|
|
220
254
|
export {
|
|
221
255
|
API_RESPONSE_PARSE,
|
|
256
|
+
apiErrorOf,
|
|
222
257
|
apiUrlOf,
|
|
223
258
|
createApiClient,
|
|
224
259
|
retryAfterSecondsOf
|