@stackcraftmx/calcita 0.31.0 → 0.32.1
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 +7 -0
- package/dist/DataTable/AuditTrailDialog.js +4 -1
- package/dist/DataTable/AuditTrailDialog.js.map +1 -1
- package/dist/DataTable/AuditTrailTable.js +4 -1
- package/dist/DataTable/AuditTrailTable.js.map +1 -1
- package/dist/DataTable/ColumnFilters/HeaderFilterPopover.js +3 -1
- package/dist/DataTable/ColumnFilters/HeaderFilterPopover.js.map +1 -1
- package/dist/DataTable/ColumnFilters/MultiSelectColumnFilter.js +3 -1
- package/dist/DataTable/ColumnFilters/MultiSelectColumnFilter.js.map +1 -1
- package/dist/DataTable/ColumnFilters/SortableFilterHeader.js +4 -1
- package/dist/DataTable/ColumnFilters/SortableFilterHeader.js.map +1 -1
- package/dist/DataTable/ColumnFilters/TextColumnFilter.js +3 -1
- package/dist/DataTable/ColumnFilters/TextColumnFilter.js.map +1 -1
- package/dist/DataTable/EmptyState.js +3 -1
- package/dist/DataTable/EmptyState.js.map +1 -1
- package/dist/DataTable/FilterChips.js +3 -1
- package/dist/DataTable/FilterChips.js.map +1 -1
- package/dist/DataTable/GoToPage.js +3 -1
- package/dist/DataTable/GoToPage.js.map +1 -1
- package/dist/DataTable/ServerTable.js +4 -1
- package/dist/DataTable/ServerTable.js.map +1 -1
- package/dist/DataTable/columns.js +3 -1
- package/dist/DataTable/columns.js.map +1 -1
- package/dist/access/Can.js +3 -1
- package/dist/access/Can.js.map +1 -1
- package/dist/access/PermissionMatrix.js +3 -1
- package/dist/access/PermissionMatrix.js.map +1 -1
- package/dist/access/RequireAccess.js +3 -1
- package/dist/access/RequireAccess.js.map +1 -1
- package/dist/account/AccountProvider.js +3 -1
- package/dist/account/AccountProvider.js.map +1 -1
- package/dist/account/AccountSecuritySection.js +3 -1
- package/dist/account/AccountSecuritySection.js.map +1 -1
- package/dist/account/DemoTotpCodeButton.js +3 -1
- package/dist/account/DemoTotpCodeButton.js.map +1 -1
- package/dist/account/ForgotPasswordScreen.js +3 -1
- package/dist/account/ForgotPasswordScreen.js.map +1 -1
- package/dist/account/ProfileScreen.js +4 -1
- package/dist/account/ProfileScreen.js.map +1 -1
- package/dist/account/ResetPasswordScreen.js +4 -1
- package/dist/account/ResetPasswordScreen.js.map +1 -1
- package/dist/account/SignInScreen.js +3 -1
- package/dist/account/SignInScreen.js.map +1 -1
- package/dist/appShell/AccountMenu.js +3 -1
- package/dist/appShell/AccountMenu.js.map +1 -1
- package/dist/appShell/AppLayout.js +4 -1
- package/dist/appShell/AppLayout.js.map +1 -1
- package/dist/appShell/AppearanceSettings.js +4 -1
- package/dist/appShell/AppearanceSettings.js.map +1 -1
- package/dist/appShell/AuthLayout.js +3 -1
- package/dist/appShell/AuthLayout.js.map +1 -1
- package/dist/appShell/BarActions.js +3 -1
- package/dist/appShell/BarActions.js.map +1 -1
- package/dist/appShell/BarSearch.js +3 -1
- package/dist/appShell/BarSearch.js.map +1 -1
- package/dist/appShell/HeaderTools.js +4 -1
- package/dist/appShell/HeaderTools.js.map +1 -1
- package/dist/appShell/MenuLinks.js +3 -1
- package/dist/appShell/MenuLinks.js.map +1 -1
- package/dist/appShell/MobileTabBar.js +3 -1
- package/dist/appShell/MobileTabBar.js.map +1 -1
- package/dist/appShell/SettingsMenu.js +3 -1
- package/dist/appShell/SettingsMenu.js.map +1 -1
- package/dist/appShell/SideMenu.js +4 -1
- package/dist/appShell/SideMenu.js.map +1 -1
- package/dist/appShell/SideMenuEntry.js +3 -1
- package/dist/appShell/SideMenuEntry.js.map +1 -1
- package/dist/appShell/SubmenuActionsMenu.js +3 -1
- package/dist/appShell/SubmenuActionsMenu.js.map +1 -1
- package/dist/appShell/TabBarEditor.js +3 -1
- package/dist/appShell/TabBarEditor.js.map +1 -1
- package/dist/appShell/TopMenu.js +3 -1
- package/dist/appShell/TopMenu.js.map +1 -1
- package/dist/appShell/Wordmark.js +3 -1
- package/dist/appShell/Wordmark.js.map +1 -1
- package/dist/appShell/pageTitle.js +3 -1
- package/dist/appShell/pageTitle.js.map +1 -1
- package/dist/auth/PasswordChecklist.js +3 -1
- package/dist/auth/PasswordChecklist.js.map +1 -1
- package/dist/calendar/EventCalendar.js +4 -1
- package/dist/calendar/EventCalendar.js.map +1 -1
- package/dist/calendar/EventDetails.js +3 -1
- package/dist/calendar/EventDetails.js.map +1 -1
- package/dist/datePickers/DateInput.js +4 -1
- package/dist/datePickers/DateInput.js.map +1 -1
- package/dist/downloads/DownloadManager.js +3 -1
- package/dist/downloads/DownloadManager.js.map +1 -1
- package/dist/feedback/ConfirmDialog.js +4 -1
- package/dist/feedback/ConfirmDialog.js.map +1 -1
- package/dist/feedback/ModalWaiting.js +3 -1
- package/dist/feedback/ModalWaiting.js.map +1 -1
- package/dist/feedback/NewBuildNotice.js +3 -1
- package/dist/feedback/NewBuildNotice.js.map +1 -1
- package/dist/filepond/FileDropzone.js +4 -1
- package/dist/filepond/FileDropzone.js.map +1 -1
- package/dist/formatting.js +8 -5
- package/dist/formatting.js.map +1 -1
- package/dist/forms/FormProblemAlert.js +3 -1
- package/dist/forms/FormProblemAlert.js.map +1 -1
- package/dist/forms/FormServerProblem.js +3 -1
- package/dist/forms/FormServerProblem.js.map +1 -1
- package/dist/forms/FormWizard.js +3 -1
- package/dist/forms/FormWizard.js.map +1 -1
- package/dist/forms/auth/ChangePasswordForm.js +3 -1
- package/dist/forms/auth/ChangePasswordForm.js.map +1 -1
- package/dist/forms/auth/ForgotPasswordForm.js +3 -1
- package/dist/forms/auth/ForgotPasswordForm.js.map +1 -1
- package/dist/forms/auth/SignInForm.js +3 -1
- package/dist/forms/auth/SignInForm.js.map +1 -1
- package/dist/forms/security/SecondStepForm.js +3 -1
- package/dist/forms/security/SecondStepForm.js.map +1 -1
- package/dist/forms/security/TwoStepVerificationCard.js +3 -1
- package/dist/forms/security/TwoStepVerificationCard.js.map +1 -1
- package/dist/hookForm/FormProblemAlert.js +3 -1
- package/dist/hookForm/FormProblemAlert.js.map +1 -1
- package/dist/hookForm/FormWizard.js +3 -1
- package/dist/hookForm/FormWizard.js.map +1 -1
- package/dist/inbox/InboxLayout.js +4 -1
- package/dist/inbox/InboxLayout.js.map +1 -1
- package/dist/inbox/MessageList.js +4 -1
- package/dist/inbox/MessageList.js.map +1 -1
- package/dist/inbox/MessageView.js +3 -1
- package/dist/inbox/MessageView.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/inputs/CatalogInput.js +4 -1
- package/dist/inputs/CatalogInput.js.map +1 -1
- package/dist/inputs/ColorInput.js +4 -1
- package/dist/inputs/ColorInput.js.map +1 -1
- package/dist/inputs/FieldFrame.js +3 -1
- package/dist/inputs/FieldFrame.js.map +1 -1
- package/dist/inputs/FormSection.js +5 -2
- package/dist/inputs/FormSection.js.map +1 -1
- package/dist/inputs/NumberInput.js +4 -1
- package/dist/inputs/NumberInput.js.map +1 -1
- package/dist/inputs/SelectInput.js +4 -1
- package/dist/inputs/SelectInput.js.map +1 -1
- package/dist/inputs/TextInput.js +2 -0
- package/dist/inputs/TextInput.js.map +1 -1
- package/dist/inputs/WizardLayout.js +4 -1
- package/dist/inputs/WizardLayout.js.map +1 -1
- package/dist/kanban/KanbanBoard.js +4 -1
- package/dist/kanban/KanbanBoard.js.map +1 -1
- package/dist/maps/MapView.js +4 -1
- package/dist/maps/MapView.js.map +1 -1
- package/dist/pwa/InstallAppSetting.js +3 -1
- package/dist/pwa/InstallAppSetting.js.map +1 -1
- package/dist/pwa/InstallInstructionsDialog.js +3 -1
- package/dist/pwa/InstallInstructionsDialog.js.map +1 -1
- package/dist/pwa/InstallPromptBanner.js +3 -1
- package/dist/pwa/InstallPromptBanner.js.map +1 -1
- package/dist/security/ActivityLog.js +3 -1
- package/dist/security/ActivityLog.js.map +1 -1
- package/dist/security/RecoveryCodesDialog.js +3 -1
- package/dist/security/RecoveryCodesDialog.js.map +1 -1
- package/dist/security/SessionsCard.js +3 -1
- package/dist/security/SessionsCard.js.map +1 -1
- package/dist/theme/createToolkitTheme.js +4 -1
- package/dist/theme/createToolkitTheme.js.map +1 -1
- package/dist/toolkit/ComponentTexts.js +3 -1
- package/dist/toolkit/ComponentTexts.js.map +1 -1
- package/dist/toolkit/ToolkitProvider.js +24 -3
- package/dist/toolkit/ToolkitProvider.js.map +1 -1
- package/dist/toolkit/toolkitContext.js.map +1 -1
- package/dist/toolkit/toolkitSettings.js +9 -0
- package/dist/toolkit/toolkitSettings.js.map +1 -1
- package/dist/toolkit/unknownProps.js +33 -0
- package/dist/toolkit/unknownProps.js.map +1 -0
- package/dist/types/DataTable/AuditTrailTable.d.ts +1 -1
- package/dist/types/DataTable/ColumnFilters/TextColumnFilter.d.ts +1 -1
- package/dist/types/DataTable/columns.d.ts +3 -1
- package/dist/types/access/Can.d.ts +1 -1
- package/dist/types/access/RequireAccess.d.ts +1 -1
- package/dist/types/account/AccountProvider.d.ts +1 -1
- package/dist/types/account/ForgotPasswordScreen.d.ts +1 -1
- package/dist/types/account/ResetPasswordScreen.d.ts +1 -1
- package/dist/types/appShell/MenuLinks.d.ts +1 -1
- package/dist/types/appShell/pageTitle.d.ts +1 -1
- package/dist/types/datePickers/DateInput.d.ts +1 -1
- package/dist/types/downloads/DownloadManager.d.ts +1 -1
- package/dist/types/formatting.d.ts +2 -1
- package/dist/types/forms/FormProblemAlert.d.ts +1 -1
- package/dist/types/forms/FormServerProblem.d.ts +1 -1
- package/dist/types/forms/FormWizard.d.ts +1 -1
- package/dist/types/forms/auth/ChangePasswordForm.d.ts +1 -1
- package/dist/types/forms/auth/ForgotPasswordForm.d.ts +1 -1
- package/dist/types/hookForm/FormProblemAlert.d.ts +1 -1
- package/dist/types/hookForm/FormWizard.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/inputs/CatalogInput.d.ts +1 -1
- package/dist/types/inputs/FieldFrame.d.ts +1 -1
- package/dist/types/inputs/NumberInput.d.ts +1 -1
- package/dist/types/inputs/TextInput.d.ts +2 -1
- package/dist/types/pwa/InstallAppSetting.d.ts +1 -1
- package/dist/types/theme/createToolkitTheme.d.ts +1 -1
- package/dist/types/toolkit/ComponentTexts.d.ts +1 -1
- package/dist/types/toolkit/ToolkitProvider.d.ts +4 -1
- package/dist/types/toolkit/toolkitContext.d.ts +2 -1
- package/dist/types/toolkit/toolkitSettings.d.ts +13 -2
- package/dist/types/toolkit/unknownProps.d.ts +23 -0
- package/dist/types/ui/actions/CopyButton.d.ts +1 -1
- package/dist/types/ui/actions/SecretField.d.ts +1 -1
- package/dist/types/ui/labels/DictionaryText.d.ts +1 -1
- package/dist/types/ui/layout/FullPageLoader.d.ts +1 -1
- package/dist/types/ui/metrics/CalendarHeatmap.d.ts +9 -8
- package/dist/types/ui/metrics/chartParts.d.ts +3 -3
- package/dist/types/ui/metrics/heatmapScale.d.ts +23 -0
- package/dist/types/ui/navigation/DocumentTitle.d.ts +1 -1
- package/dist/ui/actions/ActionsMenu.js +3 -1
- package/dist/ui/actions/ActionsMenu.js.map +1 -1
- package/dist/ui/actions/CopyButton.js +3 -1
- package/dist/ui/actions/CopyButton.js.map +1 -1
- package/dist/ui/actions/DownloadMenu.js +4 -1
- package/dist/ui/actions/DownloadMenu.js.map +1 -1
- package/dist/ui/actions/SecretField.js +3 -1
- package/dist/ui/actions/SecretField.js.map +1 -1
- package/dist/ui/actions/SelectionActions.js +3 -1
- package/dist/ui/actions/SelectionActions.js.map +1 -1
- package/dist/ui/filters/DateRangeFilter.js +3 -1
- package/dist/ui/filters/DateRangeFilter.js.map +1 -1
- package/dist/ui/filters/PeriodRangeSelector.js +3 -1
- package/dist/ui/filters/PeriodRangeSelector.js.map +1 -1
- package/dist/ui/filters/QuickFilters.js +3 -1
- package/dist/ui/filters/QuickFilters.js.map +1 -1
- package/dist/ui/filters/SegmentedControl.js +3 -1
- package/dist/ui/filters/SegmentedControl.js.map +1 -1
- package/dist/ui/labels/DictionaryText.js +3 -1
- package/dist/ui/labels/DictionaryText.js.map +1 -1
- package/dist/ui/labels/StatusChip.js +3 -1
- package/dist/ui/labels/StatusChip.js.map +1 -1
- package/dist/ui/layout/CollapsibleSection.js +3 -1
- package/dist/ui/layout/CollapsibleSection.js.map +1 -1
- package/dist/ui/layout/ContentDialog.js +3 -1
- package/dist/ui/layout/ContentDialog.js.map +1 -1
- package/dist/ui/layout/DefinitionList.js +3 -1
- package/dist/ui/layout/DefinitionList.js.map +1 -1
- package/dist/ui/layout/DetailHeader.js +3 -1
- package/dist/ui/layout/DetailHeader.js.map +1 -1
- package/dist/ui/layout/ErrorPage.js +4 -1
- package/dist/ui/layout/ErrorPage.js.map +1 -1
- package/dist/ui/layout/FullPageLoader.js +3 -1
- package/dist/ui/layout/FullPageLoader.js.map +1 -1
- package/dist/ui/layout/PageHeader.js +3 -1
- package/dist/ui/layout/PageHeader.js.map +1 -1
- package/dist/ui/layout/PageHeading.js +3 -1
- package/dist/ui/layout/PageHeading.js.map +1 -1
- package/dist/ui/layout/ProfileHeader.js +3 -1
- package/dist/ui/layout/ProfileHeader.js.map +1 -1
- package/dist/ui/layout/SectionCard.js +3 -1
- package/dist/ui/layout/SectionCard.js.map +1 -1
- package/dist/ui/metrics/BarList.js +3 -1
- package/dist/ui/metrics/BarList.js.map +1 -1
- package/dist/ui/metrics/CalendarHeatmap.js +13 -4
- package/dist/ui/metrics/CalendarHeatmap.js.map +1 -1
- package/dist/ui/metrics/ChartTooltip.js.map +1 -1
- package/dist/ui/metrics/ColumnChart.js +4 -1
- package/dist/ui/metrics/ColumnChart.js.map +1 -1
- package/dist/ui/metrics/LineChart.js +4 -1
- package/dist/ui/metrics/LineChart.js.map +1 -1
- package/dist/ui/metrics/MetricCard.js +4 -1
- package/dist/ui/metrics/MetricCard.js.map +1 -1
- package/dist/ui/metrics/TableToggle.js +3 -1
- package/dist/ui/metrics/TableToggle.js.map +1 -1
- package/dist/ui/metrics/chartParts.js +7 -3
- package/dist/ui/metrics/chartParts.js.map +1 -1
- package/dist/ui/metrics/heatmapScale.js +15 -1
- package/dist/ui/metrics/heatmapScale.js.map +1 -1
- package/dist/ui/navigation/ColorSchemeToggle.js +3 -1
- package/dist/ui/navigation/ColorSchemeToggle.js.map +1 -1
- package/dist/ui/navigation/DocumentTitle.js +3 -1
- package/dist/ui/navigation/DocumentTitle.js.map +1 -1
- package/dist/ui/navigation/FontSizeToggle.js +3 -1
- package/dist/ui/navigation/FontSizeToggle.js.map +1 -1
- package/dist/ui/navigation/LanguageToggle.js +3 -1
- package/dist/ui/navigation/LanguageToggle.js.map +1 -1
- package/dist/ui/navigation/PageBreadcrumbs.js +3 -1
- package/dist/ui/navigation/PageBreadcrumbs.js.map +1 -1
- package/dist/ui/timeline/GroupedTimeline.js +3 -1
- package/dist/ui/timeline/GroupedTimeline.js.map +1 -1
- package/dist/upload/FileDropzone.js +4 -1
- package/dist/upload/FileDropzone.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormProblemAlert.js","sources":["../../src/forms/FormProblemAlert.jsx"],"sourcesContent":["// El aviso de arriba del formulario de formik: lo que el servidor dijo que no es de un campo (sin conexión, un 409, un 500)\n// o el error de un campo que el formulario no tiene. applyServerProblem lo deja en el status de formik; aquí se pinta.\n//\n// Sin mensaje no pinta nada ni ocupa lugar.\n//\n// Se cambia desde el tema del proyecto: components.CalcitaFormProblemAlert con styleOverrides.root.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useFormikContext} from 'formik';\nimport Alert from '@mui/material/Alert';\nimport {piecePart} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y su parte (theme.components.CalcitaFormProblemAlert): el estilo del proyecto va\n// después del de MUI y el sx de la pantalla al final\nconst THEME_NAME = 'CalcitaFormProblemAlert';\nconst ProblemAlert = piecePart(THEME_NAME, 'Root', Alert);\n\n/**\n * @param {{sx?: object}} props\n */\nexport default function FormProblemAlert({sx}) {\n const {status} = useFormikContext();\n\n // guard clause: sin aviso del servidor\n if (!status) {\n return null;\n }\n return <ProblemAlert severity=\"error\" role=\"alert\" sx={sx}>{status}</ProblemAlert>;\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FormProblemAlert.js","sources":["../../src/forms/FormProblemAlert.jsx"],"sourcesContent":["// El aviso de arriba del formulario de formik: lo que el servidor dijo que no es de un campo (sin conexión, un 409, un 500)\n// o el error de un campo que el formulario no tiene. applyServerProblem lo deja en el status de formik; aquí se pinta.\n//\n// Sin mensaje no pinta nada ni ocupa lugar.\n//\n// Se cambia desde el tema del proyecto: components.CalcitaFormProblemAlert con styleOverrides.root.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useFormikContext} from 'formik';\nimport Alert from '@mui/material/Alert';\nimport {piecePart} from '@/theme/themePieces.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n// el nombre de la pieza en el tema y su parte (theme.components.CalcitaFormProblemAlert): el estilo del proyecto va\n// después del de MUI y el sx de la pantalla al final\nconst THEME_NAME = 'CalcitaFormProblemAlert';\nconst ProblemAlert = piecePart(THEME_NAME, 'Root', Alert);\n\n/**\n * @param {{sx?: object}} props\n */\nexport default function FormProblemAlert({sx, ...unknownProps}) {\n warnUnknownProps('FormProblemAlert', unknownProps);\n const {status} = useFormikContext();\n\n // guard clause: sin aviso del servidor\n if (!status) {\n return null;\n }\n return <ProblemAlert severity=\"error\" role=\"alert\" sx={sx}>{status}</ProblemAlert>;\n}\n"],"names":[],"mappings":";;;;;;AAeA,MAAM,aAAa;AACnB,MAAM,eAAe,UAAU,YAAY,QAAQ,KAAK;AAKxD,SAAwB,iBAAiB,EAAC,IAAI,GAAG,gBAAe;AAC5D,mBAAiB,oBAAoB,YAAY;AACjD,QAAM,EAAC,OAAA,IAAU,iBAAA;AAGjB,MAAI,CAAC,QAAQ;AACT,WAAO;AAAA,EACX;AACA,6BAAQ,cAAA,EAAa,UAAS,SAAQ,MAAK,SAAQ,IAAS,UAAA,QAAO;AACvE;"}
|
|
@@ -3,7 +3,9 @@ import { useEffect } from "react";
|
|
|
3
3
|
import { useFormikContext } from "formik";
|
|
4
4
|
import { splitServerFieldErrors } from "./serverProblemFields.js";
|
|
5
5
|
import { fieldErrorsOf } from "../problems.js";
|
|
6
|
-
|
|
6
|
+
import { warnUnknownProps } from "../toolkit/unknownProps.js";
|
|
7
|
+
function FormServerProblem({ error, ...unknownProps }) {
|
|
8
|
+
warnUnknownProps("FormServerProblem", unknownProps);
|
|
7
9
|
const { setFieldError, setFieldTouched, values } = useFormikContext();
|
|
8
10
|
useEffect(() => {
|
|
9
11
|
if (!error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormServerProblem.js","sources":["../../src/forms/FormServerProblem.jsx"],"sourcesContent":["// Puente entre una máquina de estados y Formik: pinta en sus campos los errores 422 que trae el error del servidor.\n//\n// Por qué existe: con XState el envío lo hace la máquina (no el onSubmit de Formik), así que el error llega en el\n// contexto de la máquina y no en los helpers de Formik. Este componente vive DENTRO del <Formik> y, cada vez que\n// llega un error nuevo, lo reparte en los campos que existen. El aviso de arriba lo pinta la pantalla desde el estado\n// de la máquina (formLevelMessageOf).\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect} from 'react';\nimport {useFormikContext} from 'formik';\n\nimport {splitServerFieldErrors} from '@/forms/serverProblemFields.js';\nimport {fieldErrorsOf} from '@/problems.js';\n\n/**\n * @param {{error: (Error & {problem?: object})|null}} props el error del contexto de la máquina (null = sin error)\n */\nexport default function FormServerProblem({error}) {\n const {setFieldError, setFieldTouched, values} = useFormikContext();\n\n useEffect(() => {\n // guard clause: sin error nuevo no hay nada que pintar\n if (!error) {\n return;\n }\n const {fieldErrors} = splitServerFieldErrors(fieldErrorsOf(error.problem), Object.keys(values));\n for (const [fieldName, message] of Object.entries(fieldErrors)) {\n // tocado primero y sin validar: si no, la validación de yup borraría el error del servidor al instante\n setFieldTouched(fieldName, true, false);\n setFieldError(fieldName, message);\n }\n // OJO: solo cuando cambia el error; values cambia con cada tecla y no debe volver a pintar el error viejo\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [error]);\n\n return null;\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FormServerProblem.js","sources":["../../src/forms/FormServerProblem.jsx"],"sourcesContent":["// Puente entre una máquina de estados y Formik: pinta en sus campos los errores 422 que trae el error del servidor.\n//\n// Por qué existe: con XState el envío lo hace la máquina (no el onSubmit de Formik), así que el error llega en el\n// contexto de la máquina y no en los helpers de Formik. Este componente vive DENTRO del <Formik> y, cada vez que\n// llega un error nuevo, lo reparte en los campos que existen. El aviso de arriba lo pinta la pantalla desde el estado\n// de la máquina (formLevelMessageOf).\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect} from 'react';\nimport {useFormikContext} from 'formik';\n\nimport {splitServerFieldErrors} from '@/forms/serverProblemFields.js';\nimport {fieldErrorsOf} from '@/problems.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n/**\n * @param {{error: (Error & {problem?: object})|null}} props el error del contexto de la máquina (null = sin error)\n */\nexport default function FormServerProblem({error, ...unknownProps}) {\n warnUnknownProps('FormServerProblem', unknownProps);\n const {setFieldError, setFieldTouched, values} = useFormikContext();\n\n useEffect(() => {\n // guard clause: sin error nuevo no hay nada que pintar\n if (!error) {\n return;\n }\n const {fieldErrors} = splitServerFieldErrors(fieldErrorsOf(error.problem), Object.keys(values));\n for (const [fieldName, message] of Object.entries(fieldErrors)) {\n // tocado primero y sin validar: si no, la validación de yup borraría el error del servidor al instante\n setFieldTouched(fieldName, true, false);\n setFieldError(fieldName, message);\n }\n // OJO: solo cuando cambia el error; values cambia con cada tecla y no debe volver a pintar el error viejo\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [error]);\n\n return null;\n}\n"],"names":[],"mappings":";;;;;;AAkBA,SAAwB,kBAAkB,EAAC,OAAO,GAAG,gBAAe;AAChE,mBAAiB,qBAAqB,YAAY;AAClD,QAAM,EAAC,eAAe,iBAAiB,OAAA,IAAU,iBAAA;AAEjD,YAAU,MAAM;AAEZ,QAAI,CAAC,OAAO;AACR;AAAA,IACJ;AACA,UAAM,EAAC,YAAA,IAAe,uBAAuB,cAAc,MAAM,OAAO,GAAG,OAAO,KAAK,MAAM,CAAC;AAC9F,eAAW,CAAC,WAAW,OAAO,KAAK,OAAO,QAAQ,WAAW,GAAG;AAE5D,sBAAgB,WAAW,MAAM,KAAK;AACtC,oBAAc,WAAW,OAAO;AAAA,IACpC;AAAA,EAGJ,GAAG,CAAC,KAAK,CAAC;AAEV,SAAO;AACX;"}
|
package/dist/forms/FormWizard.js
CHANGED
|
@@ -5,10 +5,12 @@ import { useFormikContext, getIn } from "formik";
|
|
|
5
5
|
import FormProblemAlert from "./FormProblemAlert.js";
|
|
6
6
|
import SubmitButton from "./SubmitButton.js";
|
|
7
7
|
import WizardLayout from "../inputs/WizardLayout.js";
|
|
8
|
+
import { warnUnknownProps } from "../toolkit/unknownProps.js";
|
|
8
9
|
function firstStepWithErrorOf(steps, errors) {
|
|
9
10
|
return steps.findIndex((step) => (step.fields ?? []).some((field) => getIn(errors, field)));
|
|
10
11
|
}
|
|
11
|
-
function FormWizard({ steps, submitLabel, texts }) {
|
|
12
|
+
function FormWizard({ steps, submitLabel, texts, ...unknownProps }) {
|
|
13
|
+
warnUnknownProps("FormWizard", unknownProps);
|
|
12
14
|
const { validateForm, setFieldTouched, setFieldValue, initialValues, errors, isSubmitting } = useFormikContext();
|
|
13
15
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
14
16
|
const [skippedKeys, setSkippedKeys] = useState(() => /* @__PURE__ */ new Set());
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormWizard.js","sources":["../../src/forms/FormWizard.jsx"],"sourcesContent":["// Formulario por pasos (wizard) con formik: un solo formulario partido en pasos, con pasos opcionales que se pueden omitir.\n//\n// Cómo se porta, para que la pantalla solo declare sus pasos:\n// - «Siguiente» revisa SOLO los campos del paso (fields) y no avanza si alguno tiene error; los marca como tocados para\n// que se vea por qué;\n// - «Omitir este paso» (solo en los opcionales) regresa sus campos a como empezaron: omitir es no dar ese dato, no dejar\n// lo que se escribió a medias;\n// - el último paso envía TODO el formulario (el esquema de yup completo vale para todos los pasos);\n// - si el envío falla por un campo de un paso anterior (yup o el 422 del servidor con applyServerProblem), el wizard\n// regresa solo a ese paso: un error que no se ve no se puede corregir.\n//\n// Va DENTRO de <Formik> y <Form>:\n// <Formik initialValues={…} validationSchema={…} onSubmit={…}>\n// <Form noValidate>\n// <FormWizard submitLabel=\"Enviar solicitud\" steps={[\n// {key: 'datos', label: 'Sus datos', fields: ['name', 'email'], content: <>…campos…</>},\n// {key: 'aval', label: 'Aval', optional: true, fields: ['guarantor'], content: <>…</>},\n// ]} />\n// </Form>\n// </Formik>\n//\n// Lo que se ve vive en WizardLayout (inputs/), el mismo del FormWizard de react-hook-form. Los textos («Siguiente»,\n// «Anterior»…) se cambian con `texts`: {wizard: {next: 'Continuar'}}.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useRef, useState} from 'react';\nimport {getIn, useFormikContext} from 'formik';\n\nimport FormProblemAlert from '@/forms/FormProblemAlert.jsx';\nimport SubmitButton from '@/forms/SubmitButton.jsx';\nimport WizardLayout from '@/inputs/WizardLayout.jsx';\n\n/**\n * El primer paso que tiene un campo con error; -1 si ninguno.\n *\n * @param {Array<{fields?: string[]}>} steps\n * @param {object} errors los de formik\n * @returns {number}\n */\nfunction firstStepWithErrorOf(steps, errors) {\n return steps.findIndex((step) => (step.fields ?? []).some((field) => getIn(errors, field)));\n}\n\n/**\n * @param {object} props\n * @param {Object[]} props.steps en orden\n * @param {string} props.steps[].key\n * @param {string} props.steps[].label ya traducido\n * @param {string[]} [props.steps[].fields] los campos del paso (los que revisa «Siguiente»)\n * @param {boolean} [props.steps[].optional] se puede omitir\n * @param {import('react').ReactNode} props.steps[].content los campos del paso\n * @param {string} props.submitLabel ya traducido: el botón del último paso\n * @param {object} [props.texts] los textos que cambian, con la forma de `common`: {wizard: {next, back, skip, optional, skipped, stepOf}}\n */\nexport default function FormWizard({steps, submitLabel, texts}) {\n const {validateForm, setFieldTouched, setFieldValue, initialValues, errors, isSubmitting} = useFormikContext();\n const [activeIndex, setActiveIndex] = useState(0);\n const [skippedKeys, setSkippedKeys] = useState(() => new Set());\n const wasSubmitting = useRef(false);\n const activeStep = steps[activeIndex];\n\n // al terminar un envío fallido, regresa al paso con el primer error (solo en ese momento: escribir en otro paso no\n // debe jalar a la persona de regreso)\n useEffect(() => {\n if (wasSubmitting.current && !isSubmitting) {\n const errorStepIndex = firstStepWithErrorOf(steps, errors);\n if (errorStepIndex >= 0) {\n setActiveIndex(errorStepIndex);\n }\n }\n wasSubmitting.current = isSubmitting;\n }, [isSubmitting, errors, steps]);\n\n const goNext = async () => {\n const stepFields = activeStep.fields ?? [];\n const formErrors = await validateForm();\n stepFields.forEach((field) => setFieldTouched(field, true, false));\n // guard clause: el paso tiene errores; se queda para que se corrijan\n if (stepFields.some((field) => getIn(formErrors, field))) {\n return;\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys].filter((key) => key !== activeStep.key)));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n const skipStep = () => {\n for (const field of activeStep.fields ?? []) {\n setFieldValue(field, getIn(initialValues, field), false);\n setFieldTouched(field, false, false);\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys, activeStep.key]));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n return (\n <WizardLayout steps={steps} activeIndex={activeIndex} skippedKeys={skippedKeys} isSubmitting={isSubmitting}\n onBack={() => setActiveIndex((currentIndex) => currentIndex - 1)} onNext={goNext} onSkip={skipStep}\n submitButton={<SubmitButton>{submitLabel}</SubmitButton>} problemAlert={<FormProblemAlert />} texts={texts} />\n );\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FormWizard.js","sources":["../../src/forms/FormWizard.jsx"],"sourcesContent":["// Formulario por pasos (wizard) con formik: un solo formulario partido en pasos, con pasos opcionales que se pueden omitir.\n//\n// Cómo se porta, para que la pantalla solo declare sus pasos:\n// - «Siguiente» revisa SOLO los campos del paso (fields) y no avanza si alguno tiene error; los marca como tocados para\n// que se vea por qué;\n// - «Omitir este paso» (solo en los opcionales) regresa sus campos a como empezaron: omitir es no dar ese dato, no dejar\n// lo que se escribió a medias;\n// - el último paso envía TODO el formulario (el esquema de yup completo vale para todos los pasos);\n// - si el envío falla por un campo de un paso anterior (yup o el 422 del servidor con applyServerProblem), el wizard\n// regresa solo a ese paso: un error que no se ve no se puede corregir.\n//\n// Va DENTRO de <Formik> y <Form>:\n// <Formik initialValues={…} validationSchema={…} onSubmit={…}>\n// <Form noValidate>\n// <FormWizard submitLabel=\"Enviar solicitud\" steps={[\n// {key: 'datos', label: 'Sus datos', fields: ['name', 'email'], content: <>…campos…</>},\n// {key: 'aval', label: 'Aval', optional: true, fields: ['guarantor'], content: <>…</>},\n// ]} />\n// </Form>\n// </Formik>\n//\n// Lo que se ve vive en WizardLayout (inputs/), el mismo del FormWizard de react-hook-form. Los textos («Siguiente»,\n// «Anterior»…) se cambian con `texts`: {wizard: {next: 'Continuar'}}.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useRef, useState} from 'react';\nimport {getIn, useFormikContext} from 'formik';\n\nimport FormProblemAlert from '@/forms/FormProblemAlert.jsx';\nimport SubmitButton from '@/forms/SubmitButton.jsx';\nimport WizardLayout from '@/inputs/WizardLayout.jsx';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n/**\n * El primer paso que tiene un campo con error; -1 si ninguno.\n *\n * @param {Array<{fields?: string[]}>} steps\n * @param {object} errors los de formik\n * @returns {number}\n */\nfunction firstStepWithErrorOf(steps, errors) {\n return steps.findIndex((step) => (step.fields ?? []).some((field) => getIn(errors, field)));\n}\n\n/**\n * @param {object} props\n * @param {Object[]} props.steps en orden\n * @param {string} props.steps[].key\n * @param {string} props.steps[].label ya traducido\n * @param {string[]} [props.steps[].fields] los campos del paso (los que revisa «Siguiente»)\n * @param {boolean} [props.steps[].optional] se puede omitir\n * @param {import('react').ReactNode} props.steps[].content los campos del paso\n * @param {string} props.submitLabel ya traducido: el botón del último paso\n * @param {object} [props.texts] los textos que cambian, con la forma de `common`: {wizard: {next, back, skip, optional, skipped, stepOf}}\n */\nexport default function FormWizard({steps, submitLabel, texts, ...unknownProps}) {\n warnUnknownProps('FormWizard', unknownProps);\n const {validateForm, setFieldTouched, setFieldValue, initialValues, errors, isSubmitting} = useFormikContext();\n const [activeIndex, setActiveIndex] = useState(0);\n const [skippedKeys, setSkippedKeys] = useState(() => new Set());\n const wasSubmitting = useRef(false);\n const activeStep = steps[activeIndex];\n\n // al terminar un envío fallido, regresa al paso con el primer error (solo en ese momento: escribir en otro paso no\n // debe jalar a la persona de regreso)\n useEffect(() => {\n if (wasSubmitting.current && !isSubmitting) {\n const errorStepIndex = firstStepWithErrorOf(steps, errors);\n if (errorStepIndex >= 0) {\n setActiveIndex(errorStepIndex);\n }\n }\n wasSubmitting.current = isSubmitting;\n }, [isSubmitting, errors, steps]);\n\n const goNext = async () => {\n const stepFields = activeStep.fields ?? [];\n const formErrors = await validateForm();\n stepFields.forEach((field) => setFieldTouched(field, true, false));\n // guard clause: el paso tiene errores; se queda para que se corrijan\n if (stepFields.some((field) => getIn(formErrors, field))) {\n return;\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys].filter((key) => key !== activeStep.key)));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n const skipStep = () => {\n for (const field of activeStep.fields ?? []) {\n setFieldValue(field, getIn(initialValues, field), false);\n setFieldTouched(field, false, false);\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys, activeStep.key]));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n return (\n <WizardLayout steps={steps} activeIndex={activeIndex} skippedKeys={skippedKeys} isSubmitting={isSubmitting}\n onBack={() => setActiveIndex((currentIndex) => currentIndex - 1)} onNext={goNext} onSkip={skipStep}\n submitButton={<SubmitButton>{submitLabel}</SubmitButton>} problemAlert={<FormProblemAlert />} texts={texts} />\n );\n}\n"],"names":[],"mappings":";;;;;;;;AAwCA,SAAS,qBAAqB,OAAO,QAAQ;AACzC,SAAO,MAAM,UAAU,CAAC,UAAU,KAAK,UAAU,CAAA,GAAI,KAAK,CAAC,UAAU,MAAM,QAAQ,KAAK,CAAC,CAAC;AAC9F;AAaA,SAAwB,WAAW,EAAC,OAAO,aAAa,OAAO,GAAG,gBAAe;AAC7E,mBAAiB,cAAc,YAAY;AAC3C,QAAM,EAAC,cAAc,iBAAiB,eAAe,eAAe,QAAQ,aAAA,IAAgB,iBAAA;AAC5F,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,CAAC;AAChD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,MAAM,oBAAI,KAAK;AAC9D,QAAM,gBAAgB,OAAO,KAAK;AAClC,QAAM,aAAa,MAAM,WAAW;AAIpC,YAAU,MAAM;AACZ,QAAI,cAAc,WAAW,CAAC,cAAc;AACxC,YAAM,iBAAiB,qBAAqB,OAAO,MAAM;AACzD,UAAI,kBAAkB,GAAG;AACrB,uBAAe,cAAc;AAAA,MACjC;AAAA,IACJ;AACA,kBAAc,UAAU;AAAA,EAC5B,GAAG,CAAC,cAAc,QAAQ,KAAK,CAAC;AAEhC,QAAM,SAAS,YAAY;AACvB,UAAM,aAAa,WAAW,UAAU,CAAA;AACxC,UAAM,aAAa,MAAM,aAAA;AACzB,eAAW,QAAQ,CAAC,UAAU,gBAAgB,OAAO,MAAM,KAAK,CAAC;AAEjE,QAAI,WAAW,KAAK,CAAC,UAAU,MAAM,YAAY,KAAK,CAAC,GAAG;AACtD;AAAA,IACJ;AACA,mBAAe,CAAC,gBAAgB,IAAI,IAAI,CAAC,GAAG,WAAW,EAAE,OAAO,CAAC,QAAQ,QAAQ,WAAW,GAAG,CAAC,CAAC;AACjG,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,QAAM,WAAW,MAAM;AACnB,eAAW,SAAS,WAAW,UAAU,CAAA,GAAI;AACzC,oBAAc,OAAO,MAAM,eAAe,KAAK,GAAG,KAAK;AACvD,sBAAgB,OAAO,OAAO,KAAK;AAAA,IACvC;AACA,mBAAe,CAAC,gBAAgB,oBAAI,IAAI,CAAC,GAAG,aAAa,WAAW,GAAG,CAAC,CAAC;AACzE,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAa;AAAA,MAAc;AAAA,MAA0B;AAAA,MAA0B;AAAA,MAClE,QAAQ,MAAM,eAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAQ,QAAQ;AAAA,MAC1F,cAAc,oBAAC,cAAA,EAAc,UAAA,YAAA,CAAY;AAAA,MAAiB,kCAAe,kBAAA,EAAiB;AAAA,MAAI;AAAA,IAAA;AAAA,EAAA;AAEpH;"}
|
|
@@ -13,7 +13,9 @@ import SubmitButton from "../SubmitButton.js";
|
|
|
13
13
|
import { applyServerProblem } from "../applyServerProblem.js";
|
|
14
14
|
import * as Yup from "yup";
|
|
15
15
|
import ComponentTexts from "../../toolkit/ComponentTexts.js";
|
|
16
|
-
|
|
16
|
+
import { warnUnknownProps } from "../../toolkit/unknownProps.js";
|
|
17
|
+
function ChangePasswordForm({ onSubmit, requireCurrentPassword = true, policy = DEFAULT_PASSWORD_POLICY, texts, ...unknownProps }) {
|
|
18
|
+
warnUnknownProps("ChangePasswordForm", unknownProps);
|
|
17
19
|
const { t } = useToolkitTranslation("auth", texts);
|
|
18
20
|
const [isDone, setIsDone] = useState(false);
|
|
19
21
|
const schema = useMemo(() => Yup.object({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChangePasswordForm.js","sources":["../../../src/forms/auth/ChangePasswordForm.jsx"],"sourcesContent":["// El formulario de cambiar la contraseña (formik): la actual (si se pide), la nueva con su lista de reglas que se va\n// palomeando, y la nueva otra vez. Sirve para «Mi perfil» y, sin la actual, para la pantalla del enlace del correo\n// («elegir una contraseña nueva»).\n//\n// <ChangePasswordForm onSubmit={(values) => changePassword(values)} />\n// <ChangePasswordForm requireCurrentPassword={false} onSubmit={(values) => resetPassword({token, ...values})} />\n//\n// Al cambiarla, los campos se vacían y el aviso dice que ya cambió (no se queda la contraseña escrita en pantalla).\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo, useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Alert from '@mui/material/Alert';\nimport Stack from '@mui/material/Stack';\n\nimport PasswordChecklist from '@/auth/PasswordChecklist.jsx';\nimport {DEFAULT_PASSWORD_POLICY, isPasswordAccepted} from '@/auth/passwordRules.js';\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 ComponentTexts from '@/toolkit/ComponentTexts.jsx';\n\n/**\n * @param {object} props\n * @param {(values: {currentPassword?: string, newPassword: string}) => Promise<unknown>} props.onSubmit cambia la\n * contraseña; si el servidor dice que no (la actual no es), truena con su problema\n * @param {boolean} [props.requireCurrentPassword] por omisión sí; sin ella para la pantalla del enlace del correo\n * @param {object} [props.policy] las reglas de la nueva; por omisión DEFAULT_PASSWORD_POLICY\n * @param {object} [props.texts] los textos que cambian, con la forma de `auth` en los textos del paquete\n */\nexport default function ChangePasswordForm({onSubmit, requireCurrentPassword = true, policy = DEFAULT_PASSWORD_POLICY, texts}) {\n const {t} = useToolkitTranslation('auth', texts);\n const [isDone, setIsDone] = useState(false);\n const schema = useMemo(() => Yup.object({\n currentPassword: requireCurrentPassword ? Yup.string().required() : Yup.string(),\n newPassword: Yup.string().required()\n .test('accepted', {key: 'auth:changePassword.notAccepted'}, (value) => isPasswordAccepted(value, policy))\n .test('different', {key: 'auth:changePassword.sameAsCurrent'}, (value, context) => !requireCurrentPassword || value !== context.parent.currentPassword),\n confirmPassword: Yup.string().required().oneOf([Yup.ref('newPassword')], {key: 'auth:changePassword.mismatch'}),\n }), [policy, requireCurrentPassword]);\n\n const submit = async (values, formikHelpers) => {\n setIsDone(false);\n try {\n await onSubmit(requireCurrentPassword\n ? {currentPassword: values.currentPassword, newPassword: values.newPassword}\n : {newPassword: values.newPassword});\n formikHelpers.resetForm();\n setIsDone(true);\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n return (\n <ComponentTexts namespace=\"auth\" texts={texts}>\n <Formik initialValues={{currentPassword: '', newPassword: '', confirmPassword: ''}} validationSchema={schema} onSubmit={submit}>\n {({values}) => (\n <Form noValidate>\n <Stack spacing={2}>\n {isDone && <Alert severity=\"success\" role=\"status\">{t('changePassword.done')}</Alert>}\n <FormProblemAlert />\n {requireCurrentPassword && (\n <FormTextField name=\"currentPassword\" label={t('changePassword.current')} type=\"password\" autoComplete=\"current-password\" />\n )}\n <FormTextField name=\"newPassword\" label={t('changePassword.next')} type=\"password\" autoComplete=\"new-password\" />\n <PasswordChecklist password={values.newPassword} policy={policy} />\n <FormTextField name=\"confirmPassword\" label={t('changePassword.confirm')} type=\"password\" autoComplete=\"new-password\" />\n <SubmitButton sx={{alignSelf: 'flex-start'}}>{t('changePassword.submit')}</SubmitButton>\n </Stack>\n </Form>\n )}\n </Formik>\n </ComponentTexts>\n );\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ChangePasswordForm.js","sources":["../../../src/forms/auth/ChangePasswordForm.jsx"],"sourcesContent":["// El formulario de cambiar la contraseña (formik): la actual (si se pide), la nueva con su lista de reglas que se va\n// palomeando, y la nueva otra vez. Sirve para «Mi perfil» y, sin la actual, para la pantalla del enlace del correo\n// («elegir una contraseña nueva»).\n//\n// <ChangePasswordForm onSubmit={(values) => changePassword(values)} />\n// <ChangePasswordForm requireCurrentPassword={false} onSubmit={(values) => resetPassword({token, ...values})} />\n//\n// Al cambiarla, los campos se vacían y el aviso dice que ya cambió (no se queda la contraseña escrita en pantalla).\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo, useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Alert from '@mui/material/Alert';\nimport Stack from '@mui/material/Stack';\n\nimport PasswordChecklist from '@/auth/PasswordChecklist.jsx';\nimport {DEFAULT_PASSWORD_POLICY, isPasswordAccepted} from '@/auth/passwordRules.js';\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 ComponentTexts from '@/toolkit/ComponentTexts.jsx';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n/**\n * @param {object} props\n * @param {(values: {currentPassword?: string, newPassword: string}) => Promise<unknown>} props.onSubmit cambia la\n * contraseña; si el servidor dice que no (la actual no es), truena con su problema\n * @param {boolean} [props.requireCurrentPassword] por omisión sí; sin ella para la pantalla del enlace del correo\n * @param {object} [props.policy] las reglas de la nueva; por omisión DEFAULT_PASSWORD_POLICY\n * @param {object} [props.texts] los textos que cambian, con la forma de `auth` en los textos del paquete\n */\nexport default function ChangePasswordForm({onSubmit, requireCurrentPassword = true, policy = DEFAULT_PASSWORD_POLICY, texts, ...unknownProps}) {\n warnUnknownProps('ChangePasswordForm', unknownProps);\n const {t} = useToolkitTranslation('auth', texts);\n const [isDone, setIsDone] = useState(false);\n const schema = useMemo(() => Yup.object({\n currentPassword: requireCurrentPassword ? Yup.string().required() : Yup.string(),\n newPassword: Yup.string().required()\n .test('accepted', {key: 'auth:changePassword.notAccepted'}, (value) => isPasswordAccepted(value, policy))\n .test('different', {key: 'auth:changePassword.sameAsCurrent'}, (value, context) => !requireCurrentPassword || value !== context.parent.currentPassword),\n confirmPassword: Yup.string().required().oneOf([Yup.ref('newPassword')], {key: 'auth:changePassword.mismatch'}),\n }), [policy, requireCurrentPassword]);\n\n const submit = async (values, formikHelpers) => {\n setIsDone(false);\n try {\n await onSubmit(requireCurrentPassword\n ? {currentPassword: values.currentPassword, newPassword: values.newPassword}\n : {newPassword: values.newPassword});\n formikHelpers.resetForm();\n setIsDone(true);\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n return (\n <ComponentTexts namespace=\"auth\" texts={texts}>\n <Formik initialValues={{currentPassword: '', newPassword: '', confirmPassword: ''}} validationSchema={schema} onSubmit={submit}>\n {({values}) => (\n <Form noValidate>\n <Stack spacing={2}>\n {isDone && <Alert severity=\"success\" role=\"status\">{t('changePassword.done')}</Alert>}\n <FormProblemAlert />\n {requireCurrentPassword && (\n <FormTextField name=\"currentPassword\" label={t('changePassword.current')} type=\"password\" autoComplete=\"current-password\" />\n )}\n <FormTextField name=\"newPassword\" label={t('changePassword.next')} type=\"password\" autoComplete=\"new-password\" />\n <PasswordChecklist password={values.newPassword} policy={policy} />\n <FormTextField name=\"confirmPassword\" label={t('changePassword.confirm')} type=\"password\" autoComplete=\"new-password\" />\n <SubmitButton sx={{alignSelf: 'flex-start'}}>{t('changePassword.submit')}</SubmitButton>\n </Stack>\n </Form>\n )}\n </Formik>\n </ComponentTexts>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAkCA,SAAwB,mBAAmB,EAAC,UAAU,yBAAyB,MAAM,SAAS,yBAAyB,OAAO,GAAG,gBAAe;AAC5I,mBAAiB,sBAAsB,YAAY;AACnD,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ,KAAK;AAC/C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,SAAS,QAAQ,MAAM,IAAI,OAAO;AAAA,IACpC,iBAAiB,yBAAyB,IAAI,OAAA,EAAS,SAAA,IAAa,IAAI,OAAA;AAAA,IACxE,aAAa,IAAI,OAAA,EAAS,WACrB,KAAK,YAAY,EAAC,KAAK,kCAAA,GAAoC,CAAC,UAAU,mBAAmB,OAAO,MAAM,CAAC,EACvG,KAAK,aAAa,EAAC,KAAK,oCAAA,GAAsC,CAAC,OAAO,YAAY,CAAC,0BAA0B,UAAU,QAAQ,OAAO,eAAe;AAAA,IAC1J,iBAAiB,IAAI,SAAS,SAAA,EAAW,MAAM,CAAC,IAAI,IAAI,aAAa,CAAC,GAAG,EAAC,KAAK,gCAA+B;AAAA,EAAA,CACjH,GAAG,CAAC,QAAQ,sBAAsB,CAAC;AAEpC,QAAM,SAAS,OAAO,QAAQ,kBAAkB;AAC5C,cAAU,KAAK;AACf,QAAI;AACA,YAAM,SAAS,yBACT,EAAC,iBAAiB,OAAO,iBAAiB,aAAa,OAAO,YAAA,IAC9D,EAAC,aAAa,OAAO,aAAY;AACvC,oBAAc,UAAA;AACd,gBAAU,IAAI;AAAA,IAClB,SAAS,OAAO;AACZ,yBAAmB,OAAO,EAAC,GAAG,eAAe,QAAO;AAAA,IACxD;AAAA,EACJ;AAEA,SACI,oBAAC,gBAAA,EAAe,WAAU,QAAO,OAC7B,UAAA,oBAAC,QAAA,EAAO,eAAe,EAAC,iBAAiB,IAAI,aAAa,IAAI,iBAAiB,GAAA,GAAK,kBAAkB,QAAQ,UAAU,QACnH,UAAA,CAAC,EAAC,OAAA,MACC,oBAAC,MAAA,EAAK,YAAU,MACZ,UAAA,qBAAC,OAAA,EAAM,SAAS,GACX,UAAA;AAAA,IAAA,UAAU,oBAAC,SAAM,UAAS,WAAU,MAAK,UAAU,UAAA,EAAE,qBAAqB,EAAA,CAAE;AAAA,wBAC5E,kBAAA,EAAiB;AAAA,IACjB,0BACG,oBAAC,eAAA,EAAc,MAAK,mBAAkB,OAAO,EAAE,wBAAwB,GAAG,MAAK,YAAW,cAAa,mBAAA,CAAmB;AAAA,IAE9H,oBAAC,eAAA,EAAc,MAAK,eAAc,OAAO,EAAE,qBAAqB,GAAG,MAAK,YAAW,cAAa,eAAA,CAAe;AAAA,IAC/G,oBAAC,mBAAA,EAAkB,UAAU,OAAO,aAAa,QAAgB;AAAA,IACjE,oBAAC,eAAA,EAAc,MAAK,mBAAkB,OAAO,EAAE,wBAAwB,GAAG,MAAK,YAAW,cAAa,eAAA,CAAe;AAAA,IACtH,oBAAC,gBAAa,IAAI,EAAC,WAAW,aAAA,GAAgB,UAAA,EAAE,uBAAuB,EAAA,CAAE;AAAA,EAAA,GAC7E,EAAA,CACJ,GAER,GACJ;AAER;"}
|
|
@@ -13,7 +13,9 @@ import FormTextField from "../FormTextField.js";
|
|
|
13
13
|
import SubmitButton from "../SubmitButton.js";
|
|
14
14
|
import { applyServerProblem } from "../applyServerProblem.js";
|
|
15
15
|
import * as Yup from "yup";
|
|
16
|
-
|
|
16
|
+
import { warnUnknownProps } from "../../toolkit/unknownProps.js";
|
|
17
|
+
function ForgotPasswordForm({ onSubmit, signInPath, texts, ...unknownProps }) {
|
|
18
|
+
warnUnknownProps("ForgotPasswordForm", unknownProps);
|
|
17
19
|
const { t } = useToolkitTranslation("auth", texts);
|
|
18
20
|
const { Link: RouterLink } = useToolkitNavigation();
|
|
19
21
|
const [sentToEmail, setSentToEmail] = useState(null);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ForgotPasswordForm.js","sources":["../../../src/forms/auth/ForgotPasswordForm.jsx"],"sourcesContent":["// El formulario de «¿Olvidó su contraseña?» (formik): el correo, y al enviarlo, el aviso de que revise su correo en el\n// mismo lugar, con la liga para regresar.\n//\n// OJO, a propósito: el aviso dice «si {{correo}} tiene una cuenta», aunque el sistema sepa si la tiene. Decir «ese correo\n// no existe» le confirma a cualquiera qué correos sí tienen cuenta (enumeración de cuentas); el backend también contesta\n// igual en los dos casos.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo, useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport Alert from '@mui/material/Alert';\nimport AlertTitle from '@mui/material/AlertTitle';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\n\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';\n\n/**\n * @param {object} props\n * @param {(values: {email: string}) => Promise<unknown>} props.onSubmit pide el enlace; si el servidor falla, truena con su\n * problema\n * @param {string} [props.signInPath] la liga para regresar a iniciar sesión\n * @param {object} [props.texts] los textos que cambian, con la forma de `auth` en los textos del paquete\n */\nexport default function ForgotPasswordForm({onSubmit, signInPath, texts}) {\n const {t} = useToolkitTranslation('auth', texts);\n const {Link: RouterLink} = useToolkitNavigation();\n const [sentToEmail, setSentToEmail] = useState(null);\n const schema = useMemo(() => Yup.object({email: Yup.string().trim().required().email()}), []);\n\n const backToSignIn = signInPath && <Link component={RouterLink} to={signInPath} variant=\"body2\">{t('forgotPassword.backToSignIn')}</Link>;\n\n const submit = async (values, formikHelpers) => {\n const email = values.email.trim();\n try {\n await onSubmit({email});\n setSentToEmail(email);\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n if (sentToEmail) {\n return (\n <Stack spacing={2}>\n <Alert severity=\"success\" role=\"status\">\n <AlertTitle>{t('forgotPassword.sentTitle')}</AlertTitle>\n {t('forgotPassword.sent', {email: sentToEmail})}\n </Alert>\n {backToSignIn}\n </Stack>\n );\n }\n\n return (\n <Formik initialValues={{email: ''}} validationSchema={schema} onSubmit={submit}>\n <Form noValidate>\n <Stack spacing={2}>\n <FormProblemAlert />\n <FormTextField name=\"email\" label={t('forgotPassword.email')} type=\"email\" autoComplete=\"username\" autoFocus />\n <SubmitButton fullWidth>{t('forgotPassword.submit')}</SubmitButton>\n {backToSignIn}\n </Stack>\n </Form>\n </Formik>\n );\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ForgotPasswordForm.js","sources":["../../../src/forms/auth/ForgotPasswordForm.jsx"],"sourcesContent":["// El formulario de «¿Olvidó su contraseña?» (formik): el correo, y al enviarlo, el aviso de que revise su correo en el\n// mismo lugar, con la liga para regresar.\n//\n// OJO, a propósito: el aviso dice «si {{correo}} tiene una cuenta», aunque el sistema sepa si la tiene. Decir «ese correo\n// no existe» le confirma a cualquiera qué correos sí tienen cuenta (enumeración de cuentas); el backend también contesta\n// igual en los dos casos.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo, useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport Alert from '@mui/material/Alert';\nimport AlertTitle from '@mui/material/AlertTitle';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\n\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 {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n/**\n * @param {object} props\n * @param {(values: {email: string}) => Promise<unknown>} props.onSubmit pide el enlace; si el servidor falla, truena con su\n * problema\n * @param {string} [props.signInPath] la liga para regresar a iniciar sesión\n * @param {object} [props.texts] los textos que cambian, con la forma de `auth` en los textos del paquete\n */\nexport default function ForgotPasswordForm({onSubmit, signInPath, texts, ...unknownProps}) {\n warnUnknownProps('ForgotPasswordForm', unknownProps);\n const {t} = useToolkitTranslation('auth', texts);\n const {Link: RouterLink} = useToolkitNavigation();\n const [sentToEmail, setSentToEmail] = useState(null);\n const schema = useMemo(() => Yup.object({email: Yup.string().trim().required().email()}), []);\n\n const backToSignIn = signInPath && <Link component={RouterLink} to={signInPath} variant=\"body2\">{t('forgotPassword.backToSignIn')}</Link>;\n\n const submit = async (values, formikHelpers) => {\n const email = values.email.trim();\n try {\n await onSubmit({email});\n setSentToEmail(email);\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n if (sentToEmail) {\n return (\n <Stack spacing={2}>\n <Alert severity=\"success\" role=\"status\">\n <AlertTitle>{t('forgotPassword.sentTitle')}</AlertTitle>\n {t('forgotPassword.sent', {email: sentToEmail})}\n </Alert>\n {backToSignIn}\n </Stack>\n );\n }\n\n return (\n <Formik initialValues={{email: ''}} validationSchema={schema} onSubmit={submit}>\n <Form noValidate>\n <Stack spacing={2}>\n <FormProblemAlert />\n <FormTextField name=\"email\" label={t('forgotPassword.email')} type=\"email\" autoComplete=\"username\" autoFocus />\n <SubmitButton fullWidth>{t('forgotPassword.submit')}</SubmitButton>\n {backToSignIn}\n </Stack>\n </Form>\n </Formik>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;AA+BA,SAAwB,mBAAmB,EAAC,UAAU,YAAY,OAAO,GAAG,gBAAe;AACvF,mBAAiB,sBAAsB,YAAY;AACnD,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ,KAAK;AAC/C,QAAM,EAAC,MAAM,WAAA,IAAc,qBAAA;AAC3B,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,IAAI;AACnD,QAAM,SAAS,QAAQ,MAAM,IAAI,OAAO,EAAC,OAAO,IAAI,OAAA,EAAS,KAAA,EAAO,WAAW,MAAA,GAAQ,GAAG,EAAE;AAE5F,QAAM,eAAe,cAAc,oBAAC,MAAA,EAAK,WAAW,YAAY,IAAI,YAAY,SAAQ,SAAS,UAAA,EAAE,6BAA6B,GAAE;AAElI,QAAM,SAAS,OAAO,QAAQ,kBAAkB;AAC5C,UAAM,QAAQ,OAAO,MAAM,KAAA;AAC3B,QAAI;AACA,YAAM,SAAS,EAAC,OAAM;AACtB,qBAAe,KAAK;AAAA,IACxB,SAAS,OAAO;AACZ,yBAAmB,OAAO,EAAC,GAAG,eAAe,QAAO;AAAA,IACxD;AAAA,EACJ;AAEA,MAAI,aAAa;AACb,WACI,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,MAAA,qBAAC,OAAA,EAAM,UAAS,WAAU,MAAK,UAC3B,UAAA;AAAA,QAAA,oBAAC,YAAA,EAAY,UAAA,EAAE,0BAA0B,EAAA,CAAE;AAAA,QAC1C,EAAE,uBAAuB,EAAC,OAAO,aAAY;AAAA,MAAA,GAClD;AAAA,MACC;AAAA,IAAA,GACL;AAAA,EAER;AAEA,6BACK,QAAA,EAAO,eAAe,EAAC,OAAO,GAAA,GAAK,kBAAkB,QAAQ,UAAU,QACpE,8BAAC,MAAA,EAAK,YAAU,MACZ,UAAA,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,IAAA,oBAAC,kBAAA,EAAiB;AAAA,IAClB,oBAAC,eAAA,EAAc,MAAK,SAAQ,OAAO,EAAE,sBAAsB,GAAG,MAAK,SAAQ,cAAa,YAAW,WAAS,KAAA,CAAC;AAAA,wBAC5G,cAAA,EAAa,WAAS,MAAE,UAAA,EAAE,uBAAuB,GAAE;AAAA,IACnD;AAAA,EAAA,EAAA,CACL,GACJ,GACJ;AAER;"}
|
|
@@ -14,6 +14,7 @@ import SubmitButton from "../SubmitButton.js";
|
|
|
14
14
|
import { applyServerProblem } from "../applyServerProblem.js";
|
|
15
15
|
import * as Yup from "yup";
|
|
16
16
|
import { piecePart, usePieceProps } from "../../theme/themePieces.js";
|
|
17
|
+
import { warnUnknownProps } from "../../toolkit/unknownProps.js";
|
|
17
18
|
const THEME_NAME = "CalcitaSignInForm";
|
|
18
19
|
const SignInFormFooter = piecePart(THEME_NAME, "Footer", Stack);
|
|
19
20
|
function RememberMeField({ label }) {
|
|
@@ -21,7 +22,8 @@ function RememberMeField({ label }) {
|
|
|
21
22
|
return /* @__PURE__ */ jsx(FormControlLabel, { label, control: /* @__PURE__ */ jsx(Checkbox, { ...field }) });
|
|
22
23
|
}
|
|
23
24
|
function SignInForm(props) {
|
|
24
|
-
const { onSubmit, forgotPasswordPath, showRememberMe = true, initialEmail = "", texts } = usePieceProps(props, THEME_NAME);
|
|
25
|
+
const { onSubmit, forgotPasswordPath, showRememberMe = true, initialEmail = "", texts, ...unknownProps } = usePieceProps(props, THEME_NAME);
|
|
26
|
+
warnUnknownProps(THEME_NAME, unknownProps);
|
|
25
27
|
const { t } = useToolkitTranslation("auth", texts);
|
|
26
28
|
const { Link: RouterLink } = useToolkitNavigation();
|
|
27
29
|
const schema = useMemo(() => Yup.object({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SignInForm.js","sources":["../../../src/forms/auth/SignInForm.jsx"],"sourcesContent":["// El formulario de iniciar sesión (formik): correo, contraseña, «mantener la sesión abierta» y la liga a «¿Olvidó su\n// contraseña?». La pantalla solo dice qué hacer con los datos:\n//\n// <AuthLayout title={t('auth:signIn.title')}>\n// <SignInForm onSubmit={(values) => signIn(values)} forgotPasswordPath=\"/forgot-password\" />\n// </AuthLayout>\n//\n// Si onSubmit truena con el problema del servidor (RFC 9457; un 401 «correo o contraseña incorrectos», un 429 «espere\n// un minuto»), el aviso sale arriba del formulario y lo de cada campo, en su campo (applyServerProblem).\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaSignInForm con defaultProps y\n// styleOverrides de sus partes: footer.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo} from 'react';\nimport {Form, Formik, useField} from 'formik';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport Checkbox from '@mui/material/Checkbox';\nimport FormControlLabel from '@mui/material/FormControlLabel';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\n\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 {piecePart, usePieceProps} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaSignInForm)\nconst THEME_NAME = 'CalcitaSignInForm';\nconst SignInFormFooter = piecePart(THEME_NAME, 'Footer', Stack);\n\n/** «Mantener la sesión abierta», conectado a formik. */\nfunction RememberMeField({label}) {\n const [field] = useField({name: 'remember', type: 'checkbox'});\n return <FormControlLabel label={label} control={<Checkbox {...field} />} />;\n}\n\n/**\n * @param {object} props\n * @param {(values: {email: string, password: string, remember: boolean}) => Promise<unknown>} props.onSubmit entra; si\n * el servidor dice que no, truena con su problema\n * @param {string} [props.forgotPasswordPath] sin ella no sale la liga de «¿Olvidó su contraseña?»\n * @param {boolean} [props.showRememberMe] por omisión sí\n * @param {string} [props.initialEmail]\n * @param {object} [props.texts] los textos que cambian, con la forma de `auth` en los textos del paquete\n */\nexport default function SignInForm(props) {\n // lo que el proyecto puso en su tema (components.CalcitaSignInForm.defaultProps) y encima lo que llega aquí\n const {onSubmit, forgotPasswordPath, showRememberMe = true, initialEmail = '', texts} = usePieceProps(props, THEME_NAME);\n const {t} = useToolkitTranslation('auth', texts);\n const {Link: RouterLink} = useToolkitNavigation();\n // el esquema se arma al pintar y no al importar: yup toma sus mensajes al armarlo (buildYupLocale ya corrió)\n const schema = useMemo(() => Yup.object({\n email: Yup.string().trim().required().email(),\n password: Yup.string().required(),\n }), []);\n\n const submit = async (values, formikHelpers) => {\n try {\n await onSubmit({...values, email: values.email.trim()});\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n return (\n <Formik initialValues={{email: initialEmail, password: '', remember: false}} validationSchema={schema} onSubmit={submit}>\n <Form noValidate>\n <Stack spacing={2}>\n <FormProblemAlert />\n <FormTextField name=\"email\" label={t('signIn.email')} type=\"email\" autoComplete=\"username\" autoFocus />\n <FormTextField name=\"password\" label={t('signIn.password')} type=\"password\" autoComplete=\"current-password\" />\n <SignInFormFooter direction=\"row\" ownSx={{alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 1}}>\n {showRememberMe ? <RememberMeField label={t('signIn.remember')} /> : <span />}\n {forgotPasswordPath && (\n <Link component={RouterLink} to={forgotPasswordPath} variant=\"body2\">{t('signIn.forgot')}</Link>\n )}\n </SignInFormFooter>\n <SubmitButton fullWidth>{t('signIn.submit')}</SubmitButton>\n </Stack>\n </Form>\n </Formik>\n );\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SignInForm.js","sources":["../../../src/forms/auth/SignInForm.jsx"],"sourcesContent":["// El formulario de iniciar sesión (formik): correo, contraseña, «mantener la sesión abierta» y la liga a «¿Olvidó su\n// contraseña?». La pantalla solo dice qué hacer con los datos:\n//\n// <AuthLayout title={t('auth:signIn.title')}>\n// <SignInForm onSubmit={(values) => signIn(values)} forgotPasswordPath=\"/forgot-password\" />\n// </AuthLayout>\n//\n// Si onSubmit truena con el problema del servidor (RFC 9457; un 401 «correo o contraseña incorrectos», un 429 «espere\n// un minuto»), el aviso sale arriba del formulario y lo de cada campo, en su campo (applyServerProblem).\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaSignInForm con defaultProps y\n// styleOverrides de sus partes: footer.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo} from 'react';\nimport {Form, Formik, useField} from 'formik';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport Checkbox from '@mui/material/Checkbox';\nimport FormControlLabel from '@mui/material/FormControlLabel';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\n\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 {piecePart, usePieceProps} from '@/theme/themePieces.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaSignInForm)\nconst THEME_NAME = 'CalcitaSignInForm';\nconst SignInFormFooter = piecePart(THEME_NAME, 'Footer', Stack);\n\n/** «Mantener la sesión abierta», conectado a formik. */\nfunction RememberMeField({label}) {\n const [field] = useField({name: 'remember', type: 'checkbox'});\n return <FormControlLabel label={label} control={<Checkbox {...field} />} />;\n}\n\n/**\n * @param {object} props\n * @param {(values: {email: string, password: string, remember: boolean}) => Promise<unknown>} props.onSubmit entra; si\n * el servidor dice que no, truena con su problema\n * @param {string} [props.forgotPasswordPath] sin ella no sale la liga de «¿Olvidó su contraseña?»\n * @param {boolean} [props.showRememberMe] por omisión sí\n * @param {string} [props.initialEmail]\n * @param {object} [props.texts] los textos que cambian, con la forma de `auth` en los textos del paquete\n */\nexport default function SignInForm(props) {\n // lo que el proyecto puso en su tema (components.CalcitaSignInForm.defaultProps) y encima lo que llega aquí\n const {onSubmit, forgotPasswordPath, showRememberMe = true, initialEmail = '', texts, ...unknownProps} = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('auth', texts);\n const {Link: RouterLink} = useToolkitNavigation();\n // el esquema se arma al pintar y no al importar: yup toma sus mensajes al armarlo (buildYupLocale ya corrió)\n const schema = useMemo(() => Yup.object({\n email: Yup.string().trim().required().email(),\n password: Yup.string().required(),\n }), []);\n\n const submit = async (values, formikHelpers) => {\n try {\n await onSubmit({...values, email: values.email.trim()});\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n return (\n <Formik initialValues={{email: initialEmail, password: '', remember: false}} validationSchema={schema} onSubmit={submit}>\n <Form noValidate>\n <Stack spacing={2}>\n <FormProblemAlert />\n <FormTextField name=\"email\" label={t('signIn.email')} type=\"email\" autoComplete=\"username\" autoFocus />\n <FormTextField name=\"password\" label={t('signIn.password')} type=\"password\" autoComplete=\"current-password\" />\n <SignInFormFooter direction=\"row\" ownSx={{alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 1}}>\n {showRememberMe ? <RememberMeField label={t('signIn.remember')} /> : <span />}\n {forgotPasswordPath && (\n <Link component={RouterLink} to={forgotPasswordPath} variant=\"body2\">{t('signIn.forgot')}</Link>\n )}\n </SignInFormFooter>\n <SubmitButton fullWidth>{t('signIn.submit')}</SubmitButton>\n </Stack>\n </Form>\n </Formik>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAgCA,MAAM,aAAa;AACnB,MAAM,mBAAmB,UAAU,YAAY,UAAU,KAAK;AAG9D,SAAS,gBAAgB,EAAC,SAAQ;AAC9B,QAAM,CAAC,KAAK,IAAI,SAAS,EAAC,MAAM,YAAY,MAAM,YAAW;AAC7D,SAAO,oBAAC,oBAAiB,OAAc,6BAAU,UAAA,EAAU,GAAG,OAAO,GAAI;AAC7E;AAWA,SAAwB,WAAW,OAAO;AAEtC,QAAM,EAAC,UAAU,oBAAoB,iBAAiB,MAAM,eAAe,IAAI,OAAO,GAAG,aAAA,IAAgB,cAAc,OAAO,UAAU;AACxI,mBAAiB,YAAY,YAAY;AACzC,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ,KAAK;AAC/C,QAAM,EAAC,MAAM,WAAA,IAAc,qBAAA;AAE3B,QAAM,SAAS,QAAQ,MAAM,IAAI,OAAO;AAAA,IACpC,OAAO,IAAI,OAAA,EAAS,OAAO,SAAA,EAAW,MAAA;AAAA,IACtC,UAAU,IAAI,OAAA,EAAS,SAAA;AAAA,EAAS,CACnC,GAAG,CAAA,CAAE;AAEN,QAAM,SAAS,OAAO,QAAQ,kBAAkB;AAC5C,QAAI;AACA,YAAM,SAAS,EAAC,GAAG,QAAQ,OAAO,OAAO,MAAM,KAAA,GAAO;AAAA,IAC1D,SAAS,OAAO;AACZ,yBAAmB,OAAO,EAAC,GAAG,eAAe,QAAO;AAAA,IACxD;AAAA,EACJ;AAEA,SACI,oBAAC,UAAO,eAAe,EAAC,OAAO,cAAc,UAAU,IAAI,UAAU,MAAA,GAAQ,kBAAkB,QAAQ,UAAU,QAC7G,UAAA,oBAAC,MAAA,EAAK,YAAU,MACZ,UAAA,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,IAAA,oBAAC,kBAAA,EAAiB;AAAA,IAClB,oBAAC,eAAA,EAAc,MAAK,SAAQ,OAAO,EAAE,cAAc,GAAG,MAAK,SAAQ,cAAa,YAAW,WAAS,KAAA,CAAC;AAAA,IACrG,oBAAC,eAAA,EAAc,MAAK,YAAW,OAAO,EAAE,iBAAiB,GAAG,MAAK,YAAW,cAAa,mBAAA,CAAmB;AAAA,IAC5G,qBAAC,kBAAA,EAAiB,WAAU,OAAM,OAAO,EAAC,YAAY,UAAU,gBAAgB,iBAAiB,UAAU,QAAQ,KAAK,KACnH,UAAA;AAAA,MAAA,iBAAiB,oBAAC,mBAAgB,OAAO,EAAE,iBAAiB,GAAG,wBAAM,QAAA,CAAA,CAAK;AAAA,MAC1E,sBACG,oBAAC,MAAA,EAAK,WAAW,YAAY,IAAI,oBAAoB,SAAQ,SAAS,UAAA,EAAE,eAAe,EAAA,CAAE;AAAA,IAAA,GAEjG;AAAA,wBACC,cAAA,EAAa,WAAS,MAAE,UAAA,EAAE,eAAe,EAAA,CAAE;AAAA,EAAA,EAAA,CAChD,GACJ,GACJ;AAER;"}
|
|
@@ -17,6 +17,7 @@ import * as Yup from "yup";
|
|
|
17
17
|
import { TOTP_CODE_MIN_LENGTH, TOTP_CODE_MAX_LENGTH, TOTP_CODE_PATTERN, RECOVERY_CODE_MIN_LENGTH, RECOVERY_CODE_MAX_LENGTH, RECOVERY_CODE_PATTERN, normalizeTotpCode, normalizeRecoveryCode } from "./securityRules.js";
|
|
18
18
|
import { useToolkitTranslation } from "../../toolkit/useToolkitTranslation.js";
|
|
19
19
|
import { piecePart, usePieceProps } from "../../theme/themePieces.js";
|
|
20
|
+
import { warnUnknownProps } from "../../toolkit/unknownProps.js";
|
|
20
21
|
const THEME_NAME = "CalcitaSecondStepForm";
|
|
21
22
|
const SecondStepActions = piecePart(THEME_NAME, "Actions", Stack);
|
|
22
23
|
const SECOND_STEP_METHOD = Object.freeze({ passkey: "passkey", totp: "totp", recoveryCode: "recoveryCode" });
|
|
@@ -26,7 +27,8 @@ function isOutOfAttempts(error) {
|
|
|
26
27
|
return error?.code === ACCOUNT_LOCKED_CODE || error?.status === UNAUTHENTICATED_STATUS && !Number.isInteger(error?.problem?.remaining_attempts);
|
|
27
28
|
}
|
|
28
29
|
function SecondStepForm(props) {
|
|
29
|
-
const { methods, canUsePasskeys = false, onUsePasskey, onSubmitCode, onCancel, footer, texts } = usePieceProps(props, THEME_NAME);
|
|
30
|
+
const { methods, canUsePasskeys = false, onUsePasskey, onSubmitCode, onCancel, footer, texts, ...unknownProps } = usePieceProps(props, THEME_NAME);
|
|
31
|
+
warnUnknownProps(THEME_NAME, unknownProps);
|
|
30
32
|
const { t } = useToolkitTranslation("security", texts);
|
|
31
33
|
const hasApp = methods.includes(SECOND_STEP_METHOD.totp);
|
|
32
34
|
const hasRecovery = methods.includes(SECOND_STEP_METHOD.recoveryCode);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SecondStepForm.js","sources":["../../../src/forms/security/SecondStepForm.jsx"],"sourcesContent":["// El segundo paso al entrar: la contraseña fue correcta y la cuenta tiene verificación en dos pasos.\n//\n// Los métodos van en el orden de su fuerza: primero la llave de acceso (no se la roba una página falsa), luego el código de\n// la app, y el de recuperación detrás de un enlace (es para emergencias y se gasta). Cada código equivocado cuenta para el\n// bloqueo de la cuenta: el servidor dice cuántos intentos quedan (problem.remaining_attempts) y se le dice a la persona.\n// Si se acabaron los intentos o se venció el tiempo (un 401 sin intentos, o code account_locked), se regresa a empezar.\n//\n// La pantalla no sabe del servidor: el proyecto le pasa qué hacer.\n//\n// <SecondStepForm methods={['passkey', 'totp', 'recoveryCode']} canUsePasskeys={passkeysSupported()}\n// onUsePasskey={async () => finishWith(await api.post('/auth/second-step/passkey',\n// await signWithPasskey(await api.get('/auth/second-step/passkey-options'))))}\n// onSubmitCode={async ({method, code}) => finishWith(await api.post(`/auth/second-step/${method}`, {code}))}\n// onCancel={(message) => backToSignIn(message)} />\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaSecondStepForm con defaultProps y\n// styleOverrides de sus partes: actions.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport Alert from '@mui/material/Alert';\nimport Button from '@mui/material/Button';\nimport Divider from '@mui/material/Divider';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport FingerprintOutlinedIcon from '@mui/icons-material/FingerprintOutlined';\n\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 {\n RECOVERY_CODE_MAX_LENGTH, RECOVERY_CODE_MIN_LENGTH, RECOVERY_CODE_PATTERN, TOTP_CODE_MAX_LENGTH, TOTP_CODE_MIN_LENGTH, TOTP_CODE_PATTERN,\n normalizeRecoveryCode, normalizeTotpCode,\n} from '@/forms/security/securityRules.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaSecondStepForm)\nconst THEME_NAME = 'CalcitaSecondStepForm';\nconst SecondStepActions = piecePart(THEME_NAME, 'Actions', Stack);\n\n/** Las formas de confirmar el segundo paso, como las manda el servidor. */\nexport const SECOND_STEP_METHOD = Object.freeze({passkey: 'passkey', totp: 'totp', recoveryCode: 'recoveryCode'});\n\nconst UNAUTHENTICATED_STATUS = 401;\nconst ACCOUNT_LOCKED_CODE = 'account_locked';\n\n/**\n * ¿Se acabó la oportunidad (bloqueo o se venció el tiempo)? Entonces hay que empezar de nuevo desde la contraseña.\n *\n * @param {Error & {status?: number, code?: string, problem?: object}} error\n * @returns {boolean}\n */\nfunction isOutOfAttempts(error) {\n return error?.code === ACCOUNT_LOCKED_CODE\n || (error?.status === UNAUTHENTICATED_STATUS && !Number.isInteger(error?.problem?.remaining_attempts));\n}\n\n/**\n * @param {object} props\n * @param {string[]} props.methods los que tiene la cuenta (SECOND_STEP_METHOD), como los mandó el servidor\n * @param {boolean} [props.canUsePasskeys] passkeysSupported() de /passkeys\n * @param {() => Promise<unknown>} [props.onUsePasskey] firma con la llave y termina de entrar; truena si no se pudo\n * @param {(input: {method: string, code: string}) => Promise<unknown>} props.onSubmitCode manda el código ya limpio y\n * termina de entrar; truena con el ApiError si no\n * @param {(message: string|null) => void} props.onCancel regresa a escribir la contraseña (con el motivo, si lo hay)\n * @param {import('react').ReactNode} [props.footer] abajo del botón, dentro del formulario: puede llenar el código con\n * useFormikContext (p. ej. el atajo de los ejemplos)\n * @param {object} [props.texts] los textos que cambian, con la forma de `security` en los textos del paquete\n */\nexport default function SecondStepForm(props) {\n // lo que el proyecto puso en su tema (components.CalcitaSecondStepForm.defaultProps) y encima lo que llega aquí\n const {methods, canUsePasskeys = false, onUsePasskey, onSubmitCode, onCancel, footer, texts} = usePieceProps(props, THEME_NAME);\n const {t} = useToolkitTranslation('security', texts);\n const hasApp = methods.includes(SECOND_STEP_METHOD.totp);\n const hasRecovery = methods.includes(SECOND_STEP_METHOD.recoveryCode);\n const hasPasskey = methods.includes(SECOND_STEP_METHOD.passkey) && canUsePasskeys && Boolean(onUsePasskey);\n const [method, setMethod] = useState(hasApp ? SECOND_STEP_METHOD.totp : SECOND_STEP_METHOD.recoveryCode);\n const [passkeyError, setPasskeyError] = useState(null);\n const [isUsingPasskey, setIsUsingPasskey] = useState(false);\n const isApp = method === SECOND_STEP_METHOD.totp;\n\n // el esquema se arma al pintar: yup toma sus mensajes al armarlo\n const schema = isApp\n ? Yup.object({code: Yup.string().trim().required(t('twoStep.codeRequired')).min(TOTP_CODE_MIN_LENGTH).max(TOTP_CODE_MAX_LENGTH)\n .matches(TOTP_CODE_PATTERN, t('twoStep.codeFormat'))})\n : Yup.object({code: Yup.string().trim().required(t('secondStep.recoveryCodeRequired')).min(RECOVERY_CODE_MIN_LENGTH)\n .max(RECOVERY_CODE_MAX_LENGTH).matches(RECOVERY_CODE_PATTERN, t('secondStep.recoveryCodeFormat'))});\n\n const usePasskey = async () => {\n setPasskeyError(null);\n setIsUsingPasskey(true);\n try {\n await onUsePasskey();\n } catch (error) {\n // cancelar el aviso del sistema no es un error que enseñar\n if (!error?.cancelled) {\n setPasskeyError(error?.message);\n }\n } finally {\n setIsUsingPasskey(false);\n }\n };\n\n const submit = async (values, formikHelpers) => {\n formikHelpers.setStatus(null);\n try {\n const code = isApp ? normalizeTotpCode(values.code) : normalizeRecoveryCode(values.code);\n await onSubmitCode({method, code});\n } catch (error) {\n formikHelpers.setFieldValue('code', '', false);\n if (isOutOfAttempts(error)) {\n onCancel(error?.message ?? null);\n return;\n }\n applyServerProblem(error, {...formikHelpers, values});\n // cuántos intentos quedan, junto al mensaje del servidor\n const remainingAttempts = error?.problem?.remaining_attempts;\n if (Number.isInteger(remainingAttempts)) {\n formikHelpers.setStatus(`${error.message} ${t('secondStep.remainingAttempts', {count: remainingAttempts})}`);\n }\n }\n };\n\n return (\n <Stack spacing={2.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">{t('secondStep.intro')}</Typography>\n\n {hasPasskey && (\n <>\n <Button variant=\"contained\" size=\"large\" startIcon={<FingerprintOutlinedIcon />} onClick={usePasskey} disabled={isUsingPasskey}>\n {t('secondStep.usePasskey')}\n </Button>\n {passkeyError && <Alert severity=\"error\" role=\"alert\">{passkeyError}</Alert>}\n {(hasApp || hasRecovery) && <Divider>{t('secondStep.or')}</Divider>}\n </>\n )}\n\n {(hasApp || hasRecovery) && (\n // key: al cambiar de método, el formulario empieza limpio\n <Formik key={method} initialValues={{code: ''}} validationSchema={schema} onSubmit={submit}>\n <Stack component={Form} noValidate spacing={2}>\n <FormProblemAlert />\n {isApp ? (\n <FormTextField name=\"code\" label={t('secondStep.appCodeLabel')} autoFocus autoComplete=\"one-time-code\" clearable={false}\n helperText={t('secondStep.appCodeHelp')}\n slotProps={{htmlInput: {inputMode: 'numeric', maxLength: TOTP_CODE_MAX_LENGTH}}} />\n ) : (\n <FormTextField name=\"code\" label={t('secondStep.recoveryCodeLabel')} autoFocus autoComplete=\"off\" clearable={false}\n helperText={t('secondStep.recoveryCodeHelp')} slotProps={{htmlInput: {maxLength: RECOVERY_CODE_MAX_LENGTH}}} />\n )}\n <SubmitButton fullWidth>{t('secondStep.confirm')}</SubmitButton>\n {footer}\n </Stack>\n </Formik>\n )}\n {!hasApp && !hasRecovery && footer}\n\n <SecondStepActions spacing={0.5} ownSx={{alignItems: 'center'}}>\n {hasApp && hasRecovery && (\n <Link component=\"button\" type=\"button\" variant=\"body2\"\n onClick={() => setMethod(isApp ? SECOND_STEP_METHOD.recoveryCode : SECOND_STEP_METHOD.totp)}>\n {isApp ? t('secondStep.useRecovery') : t('secondStep.useApp')}\n </Link>\n )}\n <Link component=\"button\" type=\"button\" variant=\"body2\" onClick={() => onCancel(null)}>{t('secondStep.otherAccount')}</Link>\n </SecondStepActions>\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SecondStepForm.js","sources":["../../../src/forms/security/SecondStepForm.jsx"],"sourcesContent":["// El segundo paso al entrar: la contraseña fue correcta y la cuenta tiene verificación en dos pasos.\n//\n// Los métodos van en el orden de su fuerza: primero la llave de acceso (no se la roba una página falsa), luego el código de\n// la app, y el de recuperación detrás de un enlace (es para emergencias y se gasta). Cada código equivocado cuenta para el\n// bloqueo de la cuenta: el servidor dice cuántos intentos quedan (problem.remaining_attempts) y se le dice a la persona.\n// Si se acabaron los intentos o se venció el tiempo (un 401 sin intentos, o code account_locked), se regresa a empezar.\n//\n// La pantalla no sabe del servidor: el proyecto le pasa qué hacer.\n//\n// <SecondStepForm methods={['passkey', 'totp', 'recoveryCode']} canUsePasskeys={passkeysSupported()}\n// onUsePasskey={async () => finishWith(await api.post('/auth/second-step/passkey',\n// await signWithPasskey(await api.get('/auth/second-step/passkey-options'))))}\n// onSubmitCode={async ({method, code}) => finishWith(await api.post(`/auth/second-step/${method}`, {code}))}\n// onCancel={(message) => backToSignIn(message)} />\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaSecondStepForm con defaultProps y\n// styleOverrides de sus partes: actions.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport Alert from '@mui/material/Alert';\nimport Button from '@mui/material/Button';\nimport Divider from '@mui/material/Divider';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport FingerprintOutlinedIcon from '@mui/icons-material/FingerprintOutlined';\n\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 {\n RECOVERY_CODE_MAX_LENGTH, RECOVERY_CODE_MIN_LENGTH, RECOVERY_CODE_PATTERN, TOTP_CODE_MAX_LENGTH, TOTP_CODE_MIN_LENGTH, TOTP_CODE_PATTERN,\n normalizeRecoveryCode, normalizeTotpCode,\n} from '@/forms/security/securityRules.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaSecondStepForm)\nconst THEME_NAME = 'CalcitaSecondStepForm';\nconst SecondStepActions = piecePart(THEME_NAME, 'Actions', Stack);\n\n/** Las formas de confirmar el segundo paso, como las manda el servidor. */\nexport const SECOND_STEP_METHOD = Object.freeze({passkey: 'passkey', totp: 'totp', recoveryCode: 'recoveryCode'});\n\nconst UNAUTHENTICATED_STATUS = 401;\nconst ACCOUNT_LOCKED_CODE = 'account_locked';\n\n/**\n * ¿Se acabó la oportunidad (bloqueo o se venció el tiempo)? Entonces hay que empezar de nuevo desde la contraseña.\n *\n * @param {Error & {status?: number, code?: string, problem?: object}} error\n * @returns {boolean}\n */\nfunction isOutOfAttempts(error) {\n return error?.code === ACCOUNT_LOCKED_CODE\n || (error?.status === UNAUTHENTICATED_STATUS && !Number.isInteger(error?.problem?.remaining_attempts));\n}\n\n/**\n * @param {object} props\n * @param {string[]} props.methods los que tiene la cuenta (SECOND_STEP_METHOD), como los mandó el servidor\n * @param {boolean} [props.canUsePasskeys] passkeysSupported() de /passkeys\n * @param {() => Promise<unknown>} [props.onUsePasskey] firma con la llave y termina de entrar; truena si no se pudo\n * @param {(input: {method: string, code: string}) => Promise<unknown>} props.onSubmitCode manda el código ya limpio y\n * termina de entrar; truena con el ApiError si no\n * @param {(message: string|null) => void} props.onCancel regresa a escribir la contraseña (con el motivo, si lo hay)\n * @param {import('react').ReactNode} [props.footer] abajo del botón, dentro del formulario: puede llenar el código con\n * useFormikContext (p. ej. el atajo de los ejemplos)\n * @param {object} [props.texts] los textos que cambian, con la forma de `security` en los textos del paquete\n */\nexport default function SecondStepForm(props) {\n // lo que el proyecto puso en su tema (components.CalcitaSecondStepForm.defaultProps) y encima lo que llega aquí\n const {methods, canUsePasskeys = false, onUsePasskey, onSubmitCode, onCancel, footer, texts, ...unknownProps} = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('security', texts);\n const hasApp = methods.includes(SECOND_STEP_METHOD.totp);\n const hasRecovery = methods.includes(SECOND_STEP_METHOD.recoveryCode);\n const hasPasskey = methods.includes(SECOND_STEP_METHOD.passkey) && canUsePasskeys && Boolean(onUsePasskey);\n const [method, setMethod] = useState(hasApp ? SECOND_STEP_METHOD.totp : SECOND_STEP_METHOD.recoveryCode);\n const [passkeyError, setPasskeyError] = useState(null);\n const [isUsingPasskey, setIsUsingPasskey] = useState(false);\n const isApp = method === SECOND_STEP_METHOD.totp;\n\n // el esquema se arma al pintar: yup toma sus mensajes al armarlo\n const schema = isApp\n ? Yup.object({code: Yup.string().trim().required(t('twoStep.codeRequired')).min(TOTP_CODE_MIN_LENGTH).max(TOTP_CODE_MAX_LENGTH)\n .matches(TOTP_CODE_PATTERN, t('twoStep.codeFormat'))})\n : Yup.object({code: Yup.string().trim().required(t('secondStep.recoveryCodeRequired')).min(RECOVERY_CODE_MIN_LENGTH)\n .max(RECOVERY_CODE_MAX_LENGTH).matches(RECOVERY_CODE_PATTERN, t('secondStep.recoveryCodeFormat'))});\n\n const usePasskey = async () => {\n setPasskeyError(null);\n setIsUsingPasskey(true);\n try {\n await onUsePasskey();\n } catch (error) {\n // cancelar el aviso del sistema no es un error que enseñar\n if (!error?.cancelled) {\n setPasskeyError(error?.message);\n }\n } finally {\n setIsUsingPasskey(false);\n }\n };\n\n const submit = async (values, formikHelpers) => {\n formikHelpers.setStatus(null);\n try {\n const code = isApp ? normalizeTotpCode(values.code) : normalizeRecoveryCode(values.code);\n await onSubmitCode({method, code});\n } catch (error) {\n formikHelpers.setFieldValue('code', '', false);\n if (isOutOfAttempts(error)) {\n onCancel(error?.message ?? null);\n return;\n }\n applyServerProblem(error, {...formikHelpers, values});\n // cuántos intentos quedan, junto al mensaje del servidor\n const remainingAttempts = error?.problem?.remaining_attempts;\n if (Number.isInteger(remainingAttempts)) {\n formikHelpers.setStatus(`${error.message} ${t('secondStep.remainingAttempts', {count: remainingAttempts})}`);\n }\n }\n };\n\n return (\n <Stack spacing={2.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">{t('secondStep.intro')}</Typography>\n\n {hasPasskey && (\n <>\n <Button variant=\"contained\" size=\"large\" startIcon={<FingerprintOutlinedIcon />} onClick={usePasskey} disabled={isUsingPasskey}>\n {t('secondStep.usePasskey')}\n </Button>\n {passkeyError && <Alert severity=\"error\" role=\"alert\">{passkeyError}</Alert>}\n {(hasApp || hasRecovery) && <Divider>{t('secondStep.or')}</Divider>}\n </>\n )}\n\n {(hasApp || hasRecovery) && (\n // key: al cambiar de método, el formulario empieza limpio\n <Formik key={method} initialValues={{code: ''}} validationSchema={schema} onSubmit={submit}>\n <Stack component={Form} noValidate spacing={2}>\n <FormProblemAlert />\n {isApp ? (\n <FormTextField name=\"code\" label={t('secondStep.appCodeLabel')} autoFocus autoComplete=\"one-time-code\" clearable={false}\n helperText={t('secondStep.appCodeHelp')}\n slotProps={{htmlInput: {inputMode: 'numeric', maxLength: TOTP_CODE_MAX_LENGTH}}} />\n ) : (\n <FormTextField name=\"code\" label={t('secondStep.recoveryCodeLabel')} autoFocus autoComplete=\"off\" clearable={false}\n helperText={t('secondStep.recoveryCodeHelp')} slotProps={{htmlInput: {maxLength: RECOVERY_CODE_MAX_LENGTH}}} />\n )}\n <SubmitButton fullWidth>{t('secondStep.confirm')}</SubmitButton>\n {footer}\n </Stack>\n </Formik>\n )}\n {!hasApp && !hasRecovery && footer}\n\n <SecondStepActions spacing={0.5} ownSx={{alignItems: 'center'}}>\n {hasApp && hasRecovery && (\n <Link component=\"button\" type=\"button\" variant=\"body2\"\n onClick={() => setMethod(isApp ? SECOND_STEP_METHOD.recoveryCode : SECOND_STEP_METHOD.totp)}>\n {isApp ? t('secondStep.useRecovery') : t('secondStep.useApp')}\n </Link>\n )}\n <Link component=\"button\" type=\"button\" variant=\"body2\" onClick={() => onCancel(null)}>{t('secondStep.otherAccount')}</Link>\n </SecondStepActions>\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -38,6 +38,7 @@ import Tag from "../../ui/labels/Tag.js";
|
|
|
38
38
|
import SectionCard from "../../ui/layout/SectionCard.js";
|
|
39
39
|
import ComponentTexts from "../../toolkit/ComponentTexts.js";
|
|
40
40
|
import { piecePart, usePieceProps } from "../../theme/themePieces.js";
|
|
41
|
+
import { warnUnknownProps } from "../../toolkit/unknownProps.js";
|
|
41
42
|
const THEME_NAME = "CalcitaTwoStepVerification";
|
|
42
43
|
const TwoStepQr = piecePart(THEME_NAME, "Qr", Box);
|
|
43
44
|
const TwoStepSecret = piecePart(THEME_NAME, "Secret", Stack);
|
|
@@ -200,7 +201,8 @@ function SettingRow({ Icon, title, caption, captionColor = "text.secondary", act
|
|
|
200
201
|
] });
|
|
201
202
|
}
|
|
202
203
|
function TwoStepVerificationCard(props) {
|
|
203
|
-
const { security, onSecurityChange, actions, canUsePasskeys = false, account, passkeyNotice, texts } = usePieceProps(props, THEME_NAME);
|
|
204
|
+
const { security, onSecurityChange, actions, canUsePasskeys = false, account, passkeyNotice, texts, ...unknownProps } = usePieceProps(props, THEME_NAME);
|
|
205
|
+
warnUnknownProps(THEME_NAME, unknownProps);
|
|
204
206
|
const { t } = useToolkitTranslation("security", texts);
|
|
205
207
|
const { formatDateTime } = useFormatters();
|
|
206
208
|
const [dialog, setDialog] = useState(null);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TwoStepVerificationCard.js","sources":["../../../src/forms/security/TwoStepVerificationCard.jsx"],"sourcesContent":["// «Verificación en dos pasos» de «Mi perfil»: las llaves de acceso, la app autenticadora y los códigos de recuperación.\n//\n// Lo que QUITA protección (desactivar la app, quitar una llave, generar códigos nuevos) pide la contraseña: una sesión que\n// alguien dejó abierta no debe bastar para desarmar la cuenta. Activar no la pide.\n//\n// La tarjeta no sabe del servidor: el proyecto le pasa el estado (security) y qué hacer en cada caso (actions); cada\n// acción regresa {security, recoveryCodes?} y la tarjeta avisa el estado nuevo con onSecurityChange. Las llaves de acceso\n// las crea el proyecto con createPasskey (@stackcraftmx/calcita/passkeys) dentro de actions.addPasskey: así esta puerta no\n// carga la librería de llaves.\n//\n// <TwoStepVerificationCard security={security} onSecurityChange={setSecurity} account={session.email}\n// canUsePasskeys={passkeysSupported()} actions={securityActions} />\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaTwoStepVerification con defaultProps y\n// styleOverrides de sus partes: qr, secret, secretCode, header, headerTitle, headerText, menuIcon.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport Alert from '@mui/material/Alert';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Dialog from '@mui/material/Dialog';\nimport DialogActions from '@mui/material/DialogActions';\nimport DialogContent from '@mui/material/DialogContent';\nimport DialogContentText from '@mui/material/DialogContentText';\nimport DialogTitle from '@mui/material/DialogTitle';\nimport Divider from '@mui/material/Divider';\nimport IconButton from '@mui/material/IconButton';\nimport List from '@mui/material/List';\nimport ListItem from '@mui/material/ListItem';\nimport ListItemIcon from '@mui/material/ListItemIcon';\nimport ListItemText from '@mui/material/ListItemText';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';\nimport FingerprintOutlinedIcon from '@mui/icons-material/FingerprintOutlined';\nimport PhonelinkLockOutlinedIcon from '@mui/icons-material/PhonelinkLockOutlined';\nimport VpnKeyOutlinedIcon from '@mui/icons-material/VpnKeyOutlined';\n\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 {\n CURRENT_PASSWORD_MAX_LENGTH, PASSKEY_NAME_MAX_LENGTH, TOTP_CODE_MAX_LENGTH, TOTP_CODE_MIN_LENGTH, TOTP_CODE_PATTERN, normalizeTotpCode,\n} from '@/forms/security/securityRules.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport RecoveryCodesDialog from '@/security/RecoveryCodesDialog.jsx';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport CopyButton from '@/ui/actions/CopyButton.jsx';\nimport Tag from '@/ui/labels/Tag.jsx';\nimport SectionCard from '@/ui/layout/SectionCard.jsx';\nimport ComponentTexts from '@/toolkit/ComponentTexts.jsx';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaTwoStepVerification)\nconst THEME_NAME = 'CalcitaTwoStepVerification';\nconst TwoStepQr = piecePart(THEME_NAME, 'Qr', Box);\nconst TwoStepSecret = piecePart(THEME_NAME, 'Secret', Stack);\nconst TwoStepSecretCode = piecePart(THEME_NAME, 'SecretCode', Box);\nconst TwoStepHeader = piecePart(THEME_NAME, 'Header', Stack);\nconst TwoStepHeaderTitle = piecePart(THEME_NAME, 'HeaderTitle', Stack);\nconst TwoStepHeaderText = piecePart(THEME_NAME, 'HeaderText', Box);\nconst TwoStepMenuIcon = piecePart(THEME_NAME, 'MenuIcon', ListItemIcon);\n\n// los códigos que quedan desde donde se avisa en ámbar que ya son pocos\nconst FEW_CODES_LEFT = 2;\nconst QR_SIZE_PIXELS = 200;\nconst CODE_FONT = 'ui-monospace, SFMono-Regular, Menlo, monospace';\n\n/**\n * Lo que hace un formulario de la tarjeta con un error: un ApiError va a su campo o arriba (applyServerProblem); un\n * error del navegador (la llave de acceso) va arriba; si la persona solo canceló, nada.\n *\n * @param {Error & {cancelled?: boolean, status?: number}} error\n * @param {object} formikHelpers\n * @param {object} values\n */\nfunction showFormError(error, formikHelpers, values) {\n if (error?.cancelled) {\n return;\n }\n if (error?.status === undefined) {\n formikHelpers.setStatus(error?.message);\n return;\n }\n applyServerProblem(error, {...formikHelpers, values});\n}\n\n/** Confirmar con la contraseña antes de quitar protección. */\nfunction PasswordDialog({open, title, description, confirmLabel, onConfirm, onClose}) {\n const {t} = useToolkitTranslation('security');\n const passwordSchema = Yup.object({password: Yup.string().max(CURRENT_PASSWORD_MAX_LENGTH).required(t('twoStep.passwordRequired'))});\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <Formik initialValues={{password: ''}} validationSchema={passwordSchema} enableReinitialize\n onSubmit={async (values, helpers) => {\n try {\n await onConfirm(values.password);\n } catch (error) {\n showFormError(error, helpers, values);\n }\n }}>\n {() => (\n <Form noValidate>\n <DialogTitle>{title}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>{description}</DialogContentText>\n <FormProblemAlert />\n <FormTextField name=\"password\" label={t('twoStep.passwordLabel')} type=\"password\" autoComplete=\"current-password\" autoFocus />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{t('common:actions.cancel')}</Button>\n <SubmitButton size=\"medium\" color=\"error\">{confirmLabel}</SubmitButton>\n </DialogActions>\n </Form>\n )}\n </Formik>\n </Dialog>\n );\n}\n\n/** Activar la app: escanear el QR (o teclear la clave) y confirmar con un código. */\nfunction TotpDialog({setup, onConfirm, onClose}) {\n const {t} = useToolkitTranslation('security');\n const codeSchema = Yup.object({\n code: Yup.string().trim().required(t('twoStep.codeRequired')).min(TOTP_CODE_MIN_LENGTH).max(TOTP_CODE_MAX_LENGTH)\n .matches(TOTP_CODE_PATTERN, t('twoStep.codeFormat')),\n });\n\n return (\n <Dialog open={Boolean(setup)} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <Formik initialValues={{code: ''}} validationSchema={codeSchema}\n onSubmit={async (values, helpers) => {\n try {\n await onConfirm(normalizeTotpCode(values.code));\n } catch (error) {\n showFormError(error, helpers, values);\n }\n }}>\n {() => (\n <Form noValidate>\n <DialogTitle>{t('twoStep.totpDialogTitle')}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <Typography variant=\"body2\">{t('twoStep.totpStep1')}</Typography>\n {setup?.qrImageUrl && (\n <TwoStepQr component=\"img\" src={setup.qrImageUrl} alt={t('twoStep.totpQrAlt')}\n ownSx={{width: QR_SIZE_PIXELS, height: QR_SIZE_PIXELS, alignSelf: 'center', borderRadius: 1, bgcolor: 'common.white'}} />\n )}\n <Typography variant=\"caption\" color=\"text.secondary\">{t('twoStep.totpCantScan')}</Typography>\n <TwoStepSecret direction=\"row\" spacing={1} ownSx={{alignItems: 'center'}}>\n <TwoStepSecretCode component=\"code\" ownSx={{fontFamily: CODE_FONT, fontSize: 13, wordBreak: 'break-all'}}>{setup?.secret}</TwoStepSecretCode>\n <CopyButton value={setup?.secret ?? ''} />\n </TwoStepSecret>\n <Typography variant=\"body2\">{t('twoStep.totpStep2')}</Typography>\n <FormProblemAlert />\n <FormTextField name=\"code\" label={t('twoStep.codeLabel')} autoComplete=\"one-time-code\" clearable={false}\n slotProps={{htmlInput: {inputMode: 'numeric', maxLength: TOTP_CODE_MAX_LENGTH}}} />\n {setup?.extra}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{t('common:actions.cancel')}</Button>\n <SubmitButton size=\"medium\">{t('twoStep.activate')}</SubmitButton>\n </DialogActions>\n </Form>\n )}\n </Formik>\n </Dialog>\n );\n}\n\n/** Nombrar y crear una llave nueva (el navegador pide la huella, el rostro o el PIN); notice va arriba del nombre. */\nfunction PasskeyDialog({open, onAdd, onClose, notice}) {\n const {t} = useToolkitTranslation('security');\n const nameSchema = Yup.object({name: Yup.string().trim().max(PASSKEY_NAME_MAX_LENGTH)});\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <Formik initialValues={{name: ''}} validationSchema={nameSchema}\n onSubmit={async (values, helpers) => {\n try {\n await onAdd(values.name.trim());\n } catch (error) {\n showFormError(error, helpers, values);\n }\n }}>\n {() => (\n <Form noValidate>\n <DialogTitle>{t('twoStep.passkeyDialogTitle')}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>{t('twoStep.passkeyDialogText')}</DialogContentText>\n {notice && <Alert severity=\"info\">{notice}</Alert>}\n <FormProblemAlert />\n <FormTextField name=\"name\" label={t('twoStep.passkeyName')} placeholder={t('twoStep.passkeyNamePlaceholder')} autoFocus\n slotProps={{htmlInput: {maxLength: PASSKEY_NAME_MAX_LENGTH}}} />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{t('common:actions.cancel')}</Button>\n <SubmitButton size=\"medium\" startIcon={<FingerprintOutlinedIcon />}>{t('twoStep.createPasskey')}</SubmitButton>\n </DialogActions>\n </Form>\n )}\n </Formik>\n </Dialog>\n );\n}\n\n/**\n * Un renglón de la tarjeta: ícono, nombre, explicación chica y su botón a la derecha.\n *\n * @param {{Icon: import('react').ElementType, title: string, caption?: import('react').ReactNode, captionColor?: string, action?: import('react').ReactNode}} props\n */\nfunction SettingRow({Icon, title, caption, captionColor = 'text.secondary', action}) {\n return (\n <TwoStepHeader direction=\"row\" spacing={1} ownSx={{alignItems: 'center', justifyContent: 'space-between'}}>\n <TwoStepHeaderTitle direction=\"row\" spacing={1.5} ownSx={{alignItems: 'center', minWidth: 0}}>\n <Icon color=\"action\" />\n <TwoStepHeaderText ownSx={{minWidth: 0}}>\n <Typography variant=\"subtitle2\">{title}</Typography>\n {caption && <Typography variant=\"caption\" color={captionColor} component=\"p\">{caption}</Typography>}\n </TwoStepHeaderText>\n </TwoStepHeaderTitle>\n {action}\n </TwoStepHeader>\n );\n}\n\n/**\n * @param {object} props\n * @param {{isEnabled: boolean, enabledAt?: string|null, passkeys: Array<{id: string, name: string, createdAt: string, lastUsedAt?: string|null}>, totp: {isEnabled: boolean, enabledAt?: string|null}, recoveryCodesRemaining: number}} props.security\n * @param {(security: {isEnabled: boolean, enabledAt?: string|null, passkeys: Array<{id: string, name: string, createdAt: string, lastUsedAt?: string|null}>, totp: {isEnabled: boolean, enabledAt?: string|null}, recoveryCodesRemaining: number}) => void} props.onSecurityChange\n * @param {object} props.actions lo que hace el proyecto en cada caso; cada una regresa {security, recoveryCodes?}\n * @param {(name: string) => Promise<{security: object, recoveryCodes?: string[]}>} props.actions.addPasskey crea la llave\n * (createPasskey) y la guarda en el servidor\n * @param {(passkey: {id: string, name: string}, password: string) => Promise<{security: object}>} props.actions.removePasskey\n * @param {() => Promise<{secret: string, qrImageUrl: string, extra?: import('react').ReactNode}>} props.actions.startTotp\n * la clave y su QR; extra va abajo del campo (p. ej. un atajo de los ejemplos)\n * @param {(code: string) => Promise<{security: object, recoveryCodes?: string[]}>} props.actions.confirmTotp\n * @param {(password: string) => Promise<{security: object}>} props.actions.disableTotp\n * @param {(password: string) => Promise<{security: object, recoveryCodes: string[]}>} props.actions.regenerateRecoveryCodes\n * @param {boolean} [props.canUsePasskeys] passkeysSupported() de /passkeys; sin él no se ofrece agregar llaves\n * @param {string} [props.account] la cuenta (el correo), para el archivo de los códigos\n * @param {import('react').ReactNode} [props.passkeyNotice] un aviso en el diálogo de crear la llave, antes de que el\n * navegador la guarde (p. ej. el de la demostración: la llave queda de verdad en el llavero de la persona)\n * @param {object} [props.texts] los textos que cambian, con la forma de `security` en los textos del paquete\n */\nexport default function TwoStepVerificationCard(props) {\n // lo que el proyecto puso en su tema (components.CalcitaTwoStepVerification.defaultProps) y encima lo que llega aquí\n const {security, onSecurityChange, actions, canUsePasskeys = false, account, passkeyNotice, texts} = usePieceProps(props, THEME_NAME);\n const {t} = useToolkitTranslation('security', texts);\n const {formatDateTime} = useFormatters();\n // qué diálogo está abierto: uno a la vez\n const [dialog, setDialog] = useState(null);\n const [totpSetup, setTotpSetup] = useState(null);\n const [recoveryCodes, setRecoveryCodes] = useState(null);\n const [error, setError] = useState(null);\n\n const close = () => setDialog(null);\n // lo que regresa cada acción: el estado nuevo, los códigos si hubo, el aviso y cerrar el diálogo\n const apply = (result, message) => {\n onSecurityChange(result.security);\n if (result.recoveryCodes?.length) {\n setRecoveryCodes(result.recoveryCodes);\n }\n if (message) {\n toastSuccess(message);\n }\n close();\n };\n\n const startTotp = async () => {\n setError(null);\n try {\n setTotpSetup(await actions.startTotp());\n } catch (caught) {\n setError(caught.message);\n }\n };\n\n const passkeyBeingRemoved = dialog?.passkey;\n const codesLeft = security.recoveryCodesRemaining;\n\n return (\n <ComponentTexts namespace=\"security\" texts={texts}>\n <SectionCard title={t('twoStep.title')}>\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">{t('twoStep.description')}</Typography>\n <Box>\n {security.isEnabled\n ? <Tag label={t('twoStep.active')} color=\"success\" tooltip={t('twoStep.activeSince', {date: formatDateTime(security.enabledAt)})} />\n : <Tag label={t('twoStep.inactive')} color=\"warning\" tooltip={t('twoStep.inactiveTooltip')} />}\n </Box>\n {error && <Alert severity=\"error\" onClose={() => setError(null)}>{error}</Alert>}\n\n <Divider />\n <SettingRow Icon={FingerprintOutlinedIcon} title={t('twoStep.passkeysTitle')}\n caption={security.passkeys.length === 0 ? (canUsePasskeys ? t('twoStep.passkeysEmpty') : t('passkeys.unsupported')) : null}\n action={canUsePasskeys && <Button size=\"small\" onClick={() => setDialog({kind: 'passkey'})}>{t('twoStep.add')}</Button>} />\n {security.passkeys.length > 0 && (\n <List dense disablePadding>\n {security.passkeys.map((passkey) => (\n <ListItem key={passkey.id} disableGutters secondaryAction={(\n <Tooltip title={t('twoStep.removePasskeyTooltip')}>\n <IconButton edge=\"end\" aria-label={t('twoStep.removePasskey', {name: passkey.name})}\n onClick={() => setDialog({kind: 'removePasskey', passkey})}>\n <DeleteOutlineOutlinedIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n )}>\n <TwoStepMenuIcon ownSx={{minWidth: 32}}><VpnKeyOutlinedIcon fontSize=\"small\" /></TwoStepMenuIcon>\n <ListItemText primary={passkey.name}\n secondary={passkey.lastUsedAt\n ? t('twoStep.passkeyUsed', {date: formatDateTime(passkey.lastUsedAt)})\n : t('twoStep.passkeyCreated', {date: formatDateTime(passkey.createdAt)})} />\n </ListItem>\n ))}\n </List>\n )}\n\n <Divider />\n <SettingRow Icon={PhonelinkLockOutlinedIcon} title={t('twoStep.appTitle')}\n caption={security.totp.isEnabled ? t('twoStep.appActiveSince', {date: formatDateTime(security.totp.enabledAt)}) : t('twoStep.appDescription')}\n action={security.totp.isEnabled\n ? <Button size=\"small\" color=\"error\" onClick={() => setDialog({kind: 'disableTotp'})}>{t('twoStep.deactivate')}</Button>\n : <Button size=\"small\" onClick={startTotp}>{t('twoStep.activate')}</Button>} />\n\n {/* los códigos de recuperación solo existen con la verificación activa */}\n {security.isEnabled && (\n <>\n <Divider />\n <SettingRow Icon={VpnKeyOutlinedIcon} title={t('twoStep.codesTitle')}\n caption={codesLeft ? t('twoStep.codesRemaining', {count: codesLeft}) : t('twoStep.codesNone')}\n captionColor={codesLeft <= FEW_CODES_LEFT ? 'warning.main' : 'text.secondary'}\n action={<Button size=\"small\" onClick={() => setDialog({kind: 'regenerateCodes'})}>{t('twoStep.regenerate')}</Button>} />\n </>\n )}\n </Stack>\n\n <PasskeyDialog open={dialog?.kind === 'passkey'} onClose={close} notice={passkeyNotice}\n onAdd={async (name) => apply(await actions.addPasskey(name), t('twoStep.passkeyAdded'))} />\n <TotpDialog setup={totpSetup} onClose={() => setTotpSetup(null)}\n onConfirm={async (code) => {\n const result = await actions.confirmTotp(code);\n setTotpSetup(null);\n apply(result, t('twoStep.appActivated'));\n }} />\n <PasswordDialog open={dialog?.kind === 'disableTotp'} onClose={close} title={t('twoStep.disableTitle')}\n description={t('twoStep.disableText')} confirmLabel={t('twoStep.deactivate')}\n onConfirm={async (password) => apply(await actions.disableTotp(password), t('twoStep.appDeactivated'))} />\n <PasswordDialog open={dialog?.kind === 'removePasskey'} onClose={close}\n title={t('twoStep.removeTitle', {name: passkeyBeingRemoved?.name ?? ''})} description={t('twoStep.removeText')}\n confirmLabel={t('twoStep.remove')}\n onConfirm={async (password) => apply(await actions.removePasskey(passkeyBeingRemoved, password), t('twoStep.passkeyRemoved'))} />\n <PasswordDialog open={dialog?.kind === 'regenerateCodes'} onClose={close} title={t('twoStep.regenerateTitle')}\n description={t('twoStep.regenerateText')} confirmLabel={t('twoStep.generate')}\n onConfirm={async (password) => apply(await actions.regenerateRecoveryCodes(password))} />\n <RecoveryCodesDialog codes={recoveryCodes} account={account} onClose={() => setRecoveryCodes(null)} />\n </SectionCard>\n </ComponentTexts>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TwoStepVerificationCard.js","sources":["../../../src/forms/security/TwoStepVerificationCard.jsx"],"sourcesContent":["// «Verificación en dos pasos» de «Mi perfil»: las llaves de acceso, la app autenticadora y los códigos de recuperación.\n//\n// Lo que QUITA protección (desactivar la app, quitar una llave, generar códigos nuevos) pide la contraseña: una sesión que\n// alguien dejó abierta no debe bastar para desarmar la cuenta. Activar no la pide.\n//\n// La tarjeta no sabe del servidor: el proyecto le pasa el estado (security) y qué hacer en cada caso (actions); cada\n// acción regresa {security, recoveryCodes?} y la tarjeta avisa el estado nuevo con onSecurityChange. Las llaves de acceso\n// las crea el proyecto con createPasskey (@stackcraftmx/calcita/passkeys) dentro de actions.addPasskey: así esta puerta no\n// carga la librería de llaves.\n//\n// <TwoStepVerificationCard security={security} onSecurityChange={setSecurity} account={session.email}\n// canUsePasskeys={passkeysSupported()} actions={securityActions} />\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaTwoStepVerification con defaultProps y\n// styleOverrides de sus partes: qr, secret, secretCode, header, headerTitle, headerText, menuIcon.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport Alert from '@mui/material/Alert';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Dialog from '@mui/material/Dialog';\nimport DialogActions from '@mui/material/DialogActions';\nimport DialogContent from '@mui/material/DialogContent';\nimport DialogContentText from '@mui/material/DialogContentText';\nimport DialogTitle from '@mui/material/DialogTitle';\nimport Divider from '@mui/material/Divider';\nimport IconButton from '@mui/material/IconButton';\nimport List from '@mui/material/List';\nimport ListItem from '@mui/material/ListItem';\nimport ListItemIcon from '@mui/material/ListItemIcon';\nimport ListItemText from '@mui/material/ListItemText';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';\nimport FingerprintOutlinedIcon from '@mui/icons-material/FingerprintOutlined';\nimport PhonelinkLockOutlinedIcon from '@mui/icons-material/PhonelinkLockOutlined';\nimport VpnKeyOutlinedIcon from '@mui/icons-material/VpnKeyOutlined';\n\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 {\n CURRENT_PASSWORD_MAX_LENGTH, PASSKEY_NAME_MAX_LENGTH, TOTP_CODE_MAX_LENGTH, TOTP_CODE_MIN_LENGTH, TOTP_CODE_PATTERN, normalizeTotpCode,\n} from '@/forms/security/securityRules.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport RecoveryCodesDialog from '@/security/RecoveryCodesDialog.jsx';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport CopyButton from '@/ui/actions/CopyButton.jsx';\nimport Tag from '@/ui/labels/Tag.jsx';\nimport SectionCard from '@/ui/layout/SectionCard.jsx';\nimport ComponentTexts from '@/toolkit/ComponentTexts.jsx';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaTwoStepVerification)\nconst THEME_NAME = 'CalcitaTwoStepVerification';\nconst TwoStepQr = piecePart(THEME_NAME, 'Qr', Box);\nconst TwoStepSecret = piecePart(THEME_NAME, 'Secret', Stack);\nconst TwoStepSecretCode = piecePart(THEME_NAME, 'SecretCode', Box);\nconst TwoStepHeader = piecePart(THEME_NAME, 'Header', Stack);\nconst TwoStepHeaderTitle = piecePart(THEME_NAME, 'HeaderTitle', Stack);\nconst TwoStepHeaderText = piecePart(THEME_NAME, 'HeaderText', Box);\nconst TwoStepMenuIcon = piecePart(THEME_NAME, 'MenuIcon', ListItemIcon);\n\n// los códigos que quedan desde donde se avisa en ámbar que ya son pocos\nconst FEW_CODES_LEFT = 2;\nconst QR_SIZE_PIXELS = 200;\nconst CODE_FONT = 'ui-monospace, SFMono-Regular, Menlo, monospace';\n\n/**\n * Lo que hace un formulario de la tarjeta con un error: un ApiError va a su campo o arriba (applyServerProblem); un\n * error del navegador (la llave de acceso) va arriba; si la persona solo canceló, nada.\n *\n * @param {Error & {cancelled?: boolean, status?: number}} error\n * @param {object} formikHelpers\n * @param {object} values\n */\nfunction showFormError(error, formikHelpers, values) {\n if (error?.cancelled) {\n return;\n }\n if (error?.status === undefined) {\n formikHelpers.setStatus(error?.message);\n return;\n }\n applyServerProblem(error, {...formikHelpers, values});\n}\n\n/** Confirmar con la contraseña antes de quitar protección. */\nfunction PasswordDialog({open, title, description, confirmLabel, onConfirm, onClose}) {\n const {t} = useToolkitTranslation('security');\n const passwordSchema = Yup.object({password: Yup.string().max(CURRENT_PASSWORD_MAX_LENGTH).required(t('twoStep.passwordRequired'))});\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <Formik initialValues={{password: ''}} validationSchema={passwordSchema} enableReinitialize\n onSubmit={async (values, helpers) => {\n try {\n await onConfirm(values.password);\n } catch (error) {\n showFormError(error, helpers, values);\n }\n }}>\n {() => (\n <Form noValidate>\n <DialogTitle>{title}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>{description}</DialogContentText>\n <FormProblemAlert />\n <FormTextField name=\"password\" label={t('twoStep.passwordLabel')} type=\"password\" autoComplete=\"current-password\" autoFocus />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{t('common:actions.cancel')}</Button>\n <SubmitButton size=\"medium\" color=\"error\">{confirmLabel}</SubmitButton>\n </DialogActions>\n </Form>\n )}\n </Formik>\n </Dialog>\n );\n}\n\n/** Activar la app: escanear el QR (o teclear la clave) y confirmar con un código. */\nfunction TotpDialog({setup, onConfirm, onClose}) {\n const {t} = useToolkitTranslation('security');\n const codeSchema = Yup.object({\n code: Yup.string().trim().required(t('twoStep.codeRequired')).min(TOTP_CODE_MIN_LENGTH).max(TOTP_CODE_MAX_LENGTH)\n .matches(TOTP_CODE_PATTERN, t('twoStep.codeFormat')),\n });\n\n return (\n <Dialog open={Boolean(setup)} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <Formik initialValues={{code: ''}} validationSchema={codeSchema}\n onSubmit={async (values, helpers) => {\n try {\n await onConfirm(normalizeTotpCode(values.code));\n } catch (error) {\n showFormError(error, helpers, values);\n }\n }}>\n {() => (\n <Form noValidate>\n <DialogTitle>{t('twoStep.totpDialogTitle')}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <Typography variant=\"body2\">{t('twoStep.totpStep1')}</Typography>\n {setup?.qrImageUrl && (\n <TwoStepQr component=\"img\" src={setup.qrImageUrl} alt={t('twoStep.totpQrAlt')}\n ownSx={{width: QR_SIZE_PIXELS, height: QR_SIZE_PIXELS, alignSelf: 'center', borderRadius: 1, bgcolor: 'common.white'}} />\n )}\n <Typography variant=\"caption\" color=\"text.secondary\">{t('twoStep.totpCantScan')}</Typography>\n <TwoStepSecret direction=\"row\" spacing={1} ownSx={{alignItems: 'center'}}>\n <TwoStepSecretCode component=\"code\" ownSx={{fontFamily: CODE_FONT, fontSize: 13, wordBreak: 'break-all'}}>{setup?.secret}</TwoStepSecretCode>\n <CopyButton value={setup?.secret ?? ''} />\n </TwoStepSecret>\n <Typography variant=\"body2\">{t('twoStep.totpStep2')}</Typography>\n <FormProblemAlert />\n <FormTextField name=\"code\" label={t('twoStep.codeLabel')} autoComplete=\"one-time-code\" clearable={false}\n slotProps={{htmlInput: {inputMode: 'numeric', maxLength: TOTP_CODE_MAX_LENGTH}}} />\n {setup?.extra}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{t('common:actions.cancel')}</Button>\n <SubmitButton size=\"medium\">{t('twoStep.activate')}</SubmitButton>\n </DialogActions>\n </Form>\n )}\n </Formik>\n </Dialog>\n );\n}\n\n/** Nombrar y crear una llave nueva (el navegador pide la huella, el rostro o el PIN); notice va arriba del nombre. */\nfunction PasskeyDialog({open, onAdd, onClose, notice}) {\n const {t} = useToolkitTranslation('security');\n const nameSchema = Yup.object({name: Yup.string().trim().max(PASSKEY_NAME_MAX_LENGTH)});\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <Formik initialValues={{name: ''}} validationSchema={nameSchema}\n onSubmit={async (values, helpers) => {\n try {\n await onAdd(values.name.trim());\n } catch (error) {\n showFormError(error, helpers, values);\n }\n }}>\n {() => (\n <Form noValidate>\n <DialogTitle>{t('twoStep.passkeyDialogTitle')}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>{t('twoStep.passkeyDialogText')}</DialogContentText>\n {notice && <Alert severity=\"info\">{notice}</Alert>}\n <FormProblemAlert />\n <FormTextField name=\"name\" label={t('twoStep.passkeyName')} placeholder={t('twoStep.passkeyNamePlaceholder')} autoFocus\n slotProps={{htmlInput: {maxLength: PASSKEY_NAME_MAX_LENGTH}}} />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{t('common:actions.cancel')}</Button>\n <SubmitButton size=\"medium\" startIcon={<FingerprintOutlinedIcon />}>{t('twoStep.createPasskey')}</SubmitButton>\n </DialogActions>\n </Form>\n )}\n </Formik>\n </Dialog>\n );\n}\n\n/**\n * Un renglón de la tarjeta: ícono, nombre, explicación chica y su botón a la derecha.\n *\n * @param {{Icon: import('react').ElementType, title: string, caption?: import('react').ReactNode, captionColor?: string, action?: import('react').ReactNode}} props\n */\nfunction SettingRow({Icon, title, caption, captionColor = 'text.secondary', action}) {\n return (\n <TwoStepHeader direction=\"row\" spacing={1} ownSx={{alignItems: 'center', justifyContent: 'space-between'}}>\n <TwoStepHeaderTitle direction=\"row\" spacing={1.5} ownSx={{alignItems: 'center', minWidth: 0}}>\n <Icon color=\"action\" />\n <TwoStepHeaderText ownSx={{minWidth: 0}}>\n <Typography variant=\"subtitle2\">{title}</Typography>\n {caption && <Typography variant=\"caption\" color={captionColor} component=\"p\">{caption}</Typography>}\n </TwoStepHeaderText>\n </TwoStepHeaderTitle>\n {action}\n </TwoStepHeader>\n );\n}\n\n/**\n * @param {object} props\n * @param {{isEnabled: boolean, enabledAt?: string|null, passkeys: Array<{id: string, name: string, createdAt: string, lastUsedAt?: string|null}>, totp: {isEnabled: boolean, enabledAt?: string|null}, recoveryCodesRemaining: number}} props.security\n * @param {(security: {isEnabled: boolean, enabledAt?: string|null, passkeys: Array<{id: string, name: string, createdAt: string, lastUsedAt?: string|null}>, totp: {isEnabled: boolean, enabledAt?: string|null}, recoveryCodesRemaining: number}) => void} props.onSecurityChange\n * @param {object} props.actions lo que hace el proyecto en cada caso; cada una regresa {security, recoveryCodes?}\n * @param {(name: string) => Promise<{security: object, recoveryCodes?: string[]}>} props.actions.addPasskey crea la llave\n * (createPasskey) y la guarda en el servidor\n * @param {(passkey: {id: string, name: string}, password: string) => Promise<{security: object}>} props.actions.removePasskey\n * @param {() => Promise<{secret: string, qrImageUrl: string, extra?: import('react').ReactNode}>} props.actions.startTotp\n * la clave y su QR; extra va abajo del campo (p. ej. un atajo de los ejemplos)\n * @param {(code: string) => Promise<{security: object, recoveryCodes?: string[]}>} props.actions.confirmTotp\n * @param {(password: string) => Promise<{security: object}>} props.actions.disableTotp\n * @param {(password: string) => Promise<{security: object, recoveryCodes: string[]}>} props.actions.regenerateRecoveryCodes\n * @param {boolean} [props.canUsePasskeys] passkeysSupported() de /passkeys; sin él no se ofrece agregar llaves\n * @param {string} [props.account] la cuenta (el correo), para el archivo de los códigos\n * @param {import('react').ReactNode} [props.passkeyNotice] un aviso en el diálogo de crear la llave, antes de que el\n * navegador la guarde (p. ej. el de la demostración: la llave queda de verdad en el llavero de la persona)\n * @param {object} [props.texts] los textos que cambian, con la forma de `security` en los textos del paquete\n */\nexport default function TwoStepVerificationCard(props) {\n // lo que el proyecto puso en su tema (components.CalcitaTwoStepVerification.defaultProps) y encima lo que llega aquí\n const {security, onSecurityChange, actions, canUsePasskeys = false, account, passkeyNotice, texts, ...unknownProps} = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('security', texts);\n const {formatDateTime} = useFormatters();\n // qué diálogo está abierto: uno a la vez\n const [dialog, setDialog] = useState(null);\n const [totpSetup, setTotpSetup] = useState(null);\n const [recoveryCodes, setRecoveryCodes] = useState(null);\n const [error, setError] = useState(null);\n\n const close = () => setDialog(null);\n // lo que regresa cada acción: el estado nuevo, los códigos si hubo, el aviso y cerrar el diálogo\n const apply = (result, message) => {\n onSecurityChange(result.security);\n if (result.recoveryCodes?.length) {\n setRecoveryCodes(result.recoveryCodes);\n }\n if (message) {\n toastSuccess(message);\n }\n close();\n };\n\n const startTotp = async () => {\n setError(null);\n try {\n setTotpSetup(await actions.startTotp());\n } catch (caught) {\n setError(caught.message);\n }\n };\n\n const passkeyBeingRemoved = dialog?.passkey;\n const codesLeft = security.recoveryCodesRemaining;\n\n return (\n <ComponentTexts namespace=\"security\" texts={texts}>\n <SectionCard title={t('twoStep.title')}>\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">{t('twoStep.description')}</Typography>\n <Box>\n {security.isEnabled\n ? <Tag label={t('twoStep.active')} color=\"success\" tooltip={t('twoStep.activeSince', {date: formatDateTime(security.enabledAt)})} />\n : <Tag label={t('twoStep.inactive')} color=\"warning\" tooltip={t('twoStep.inactiveTooltip')} />}\n </Box>\n {error && <Alert severity=\"error\" onClose={() => setError(null)}>{error}</Alert>}\n\n <Divider />\n <SettingRow Icon={FingerprintOutlinedIcon} title={t('twoStep.passkeysTitle')}\n caption={security.passkeys.length === 0 ? (canUsePasskeys ? t('twoStep.passkeysEmpty') : t('passkeys.unsupported')) : null}\n action={canUsePasskeys && <Button size=\"small\" onClick={() => setDialog({kind: 'passkey'})}>{t('twoStep.add')}</Button>} />\n {security.passkeys.length > 0 && (\n <List dense disablePadding>\n {security.passkeys.map((passkey) => (\n <ListItem key={passkey.id} disableGutters secondaryAction={(\n <Tooltip title={t('twoStep.removePasskeyTooltip')}>\n <IconButton edge=\"end\" aria-label={t('twoStep.removePasskey', {name: passkey.name})}\n onClick={() => setDialog({kind: 'removePasskey', passkey})}>\n <DeleteOutlineOutlinedIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n )}>\n <TwoStepMenuIcon ownSx={{minWidth: 32}}><VpnKeyOutlinedIcon fontSize=\"small\" /></TwoStepMenuIcon>\n <ListItemText primary={passkey.name}\n secondary={passkey.lastUsedAt\n ? t('twoStep.passkeyUsed', {date: formatDateTime(passkey.lastUsedAt)})\n : t('twoStep.passkeyCreated', {date: formatDateTime(passkey.createdAt)})} />\n </ListItem>\n ))}\n </List>\n )}\n\n <Divider />\n <SettingRow Icon={PhonelinkLockOutlinedIcon} title={t('twoStep.appTitle')}\n caption={security.totp.isEnabled ? t('twoStep.appActiveSince', {date: formatDateTime(security.totp.enabledAt)}) : t('twoStep.appDescription')}\n action={security.totp.isEnabled\n ? <Button size=\"small\" color=\"error\" onClick={() => setDialog({kind: 'disableTotp'})}>{t('twoStep.deactivate')}</Button>\n : <Button size=\"small\" onClick={startTotp}>{t('twoStep.activate')}</Button>} />\n\n {/* los códigos de recuperación solo existen con la verificación activa */}\n {security.isEnabled && (\n <>\n <Divider />\n <SettingRow Icon={VpnKeyOutlinedIcon} title={t('twoStep.codesTitle')}\n caption={codesLeft ? t('twoStep.codesRemaining', {count: codesLeft}) : t('twoStep.codesNone')}\n captionColor={codesLeft <= FEW_CODES_LEFT ? 'warning.main' : 'text.secondary'}\n action={<Button size=\"small\" onClick={() => setDialog({kind: 'regenerateCodes'})}>{t('twoStep.regenerate')}</Button>} />\n </>\n )}\n </Stack>\n\n <PasskeyDialog open={dialog?.kind === 'passkey'} onClose={close} notice={passkeyNotice}\n onAdd={async (name) => apply(await actions.addPasskey(name), t('twoStep.passkeyAdded'))} />\n <TotpDialog setup={totpSetup} onClose={() => setTotpSetup(null)}\n onConfirm={async (code) => {\n const result = await actions.confirmTotp(code);\n setTotpSetup(null);\n apply(result, t('twoStep.appActivated'));\n }} />\n <PasswordDialog open={dialog?.kind === 'disableTotp'} onClose={close} title={t('twoStep.disableTitle')}\n description={t('twoStep.disableText')} confirmLabel={t('twoStep.deactivate')}\n onConfirm={async (password) => apply(await actions.disableTotp(password), t('twoStep.appDeactivated'))} />\n <PasswordDialog open={dialog?.kind === 'removePasskey'} onClose={close}\n title={t('twoStep.removeTitle', {name: passkeyBeingRemoved?.name ?? ''})} description={t('twoStep.removeText')}\n confirmLabel={t('twoStep.remove')}\n onConfirm={async (password) => apply(await actions.removePasskey(passkeyBeingRemoved, password), t('twoStep.passkeyRemoved'))} />\n <PasswordDialog open={dialog?.kind === 'regenerateCodes'} onClose={close} title={t('twoStep.regenerateTitle')}\n description={t('twoStep.regenerateText')} confirmLabel={t('twoStep.generate')}\n onConfirm={async (password) => apply(await actions.regenerateRecoveryCodes(password))} />\n <RecoveryCodesDialog codes={recoveryCodes} account={account} onClose={() => setRecoveryCodes(null)} />\n </SectionCard>\n </ComponentTexts>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -3,9 +3,11 @@ import { jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { useFormContext, useFormState } from "react-hook-form";
|
|
4
4
|
import Alert from "@mui/material/Alert";
|
|
5
5
|
import { piecePart } from "../theme/themePieces.js";
|
|
6
|
+
import { warnUnknownProps } from "../toolkit/unknownProps.js";
|
|
6
7
|
const THEME_NAME = "CalcitaFormProblemAlert";
|
|
7
8
|
const ProblemAlert = piecePart(THEME_NAME, "Root", Alert);
|
|
8
|
-
function FormProblemAlert({ control, sx }) {
|
|
9
|
+
function FormProblemAlert({ control, sx, ...unknownProps }) {
|
|
10
|
+
warnUnknownProps("FormProblemAlert", unknownProps);
|
|
9
11
|
const formContext = useFormContext();
|
|
10
12
|
const { errors } = useFormState({ control: control ?? formContext?.control });
|
|
11
13
|
const message = errors.root?.server?.message;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormProblemAlert.js","sources":["../../src/hookForm/FormProblemAlert.jsx"],"sourcesContent":["// El aviso de arriba del formulario de react-hook-form: lo que el servidor dijo que no es de un campo. applyServerProblem\n// lo deja en errors.root.server; aquí se pinta. Sin mensaje no pinta nada ni ocupa lugar.\n//\n// Se cambia desde el tema del proyecto: components.CalcitaFormProblemAlert con styleOverrides.root.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useFormContext, useFormState} from 'react-hook-form';\nimport Alert from '@mui/material/Alert';\nimport {piecePart} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y su parte (theme.components.CalcitaFormProblemAlert): el estilo del proyecto va\n// después del de MUI y el sx de la pantalla al final\nconst THEME_NAME = 'CalcitaFormProblemAlert';\nconst ProblemAlert = piecePart(THEME_NAME, 'Root', Alert);\n\n/**\n * @param {{control?: object, sx?: object}} props control: el de useForm, si la pantalla no usa FormProvider\n */\nexport default function FormProblemAlert({control, sx}) {\n const formContext = useFormContext();\n const {errors} = useFormState({control: control ?? formContext?.control});\n const message = errors.root?.server?.message;\n\n // guard clause: sin aviso del servidor\n if (!message) {\n return null;\n }\n return <ProblemAlert severity=\"error\" role=\"alert\" sx={sx}>{message}</ProblemAlert>;\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FormProblemAlert.js","sources":["../../src/hookForm/FormProblemAlert.jsx"],"sourcesContent":["// El aviso de arriba del formulario de react-hook-form: lo que el servidor dijo que no es de un campo. applyServerProblem\n// lo deja en errors.root.server; aquí se pinta. Sin mensaje no pinta nada ni ocupa lugar.\n//\n// Se cambia desde el tema del proyecto: components.CalcitaFormProblemAlert con styleOverrides.root.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useFormContext, useFormState} from 'react-hook-form';\nimport Alert from '@mui/material/Alert';\nimport {piecePart} from '@/theme/themePieces.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n// el nombre de la pieza en el tema y su parte (theme.components.CalcitaFormProblemAlert): el estilo del proyecto va\n// después del de MUI y el sx de la pantalla al final\nconst THEME_NAME = 'CalcitaFormProblemAlert';\nconst ProblemAlert = piecePart(THEME_NAME, 'Root', Alert);\n\n/**\n * @param {{control?: object, sx?: object}} props control: el de useForm, si la pantalla no usa FormProvider\n */\nexport default function FormProblemAlert({control, sx, ...unknownProps}) {\n warnUnknownProps('FormProblemAlert', unknownProps);\n const formContext = useFormContext();\n const {errors} = useFormState({control: control ?? formContext?.control});\n const message = errors.root?.server?.message;\n\n // guard clause: sin aviso del servidor\n if (!message) {\n return null;\n }\n return <ProblemAlert severity=\"error\" role=\"alert\" sx={sx}>{message}</ProblemAlert>;\n}\n"],"names":[],"mappings":";;;;;;AAaA,MAAM,aAAa;AACnB,MAAM,eAAe,UAAU,YAAY,QAAQ,KAAK;AAKxD,SAAwB,iBAAiB,EAAC,SAAS,IAAI,GAAG,gBAAe;AACrE,mBAAiB,oBAAoB,YAAY;AACjD,QAAM,cAAc,eAAA;AACpB,QAAM,EAAC,WAAU,aAAa,EAAC,SAAS,WAAW,aAAa,SAAQ;AACxE,QAAM,UAAU,OAAO,MAAM,QAAQ;AAGrC,MAAI,CAAC,SAAS;AACV,WAAO;AAAA,EACX;AACA,6BAAQ,cAAA,EAAa,UAAS,SAAQ,MAAK,SAAQ,IAAS,UAAA,SAAQ;AACxE;"}
|
|
@@ -5,7 +5,9 @@ import { useFormContext } from "react-hook-form";
|
|
|
5
5
|
import FormProblemAlert from "./FormProblemAlert.js";
|
|
6
6
|
import SubmitButton from "./SubmitButton.js";
|
|
7
7
|
import WizardLayout from "../inputs/WizardLayout.js";
|
|
8
|
-
|
|
8
|
+
import { warnUnknownProps } from "../toolkit/unknownProps.js";
|
|
9
|
+
function FormWizard({ steps, submitLabel, texts, ...unknownProps }) {
|
|
10
|
+
warnUnknownProps("FormWizard", unknownProps);
|
|
9
11
|
const { trigger, resetField, getFieldState, formState } = useFormContext();
|
|
10
12
|
const { isSubmitting, submitCount } = formState;
|
|
11
13
|
const [activeIndex, setActiveIndex] = useState(0);
|