From 4ebcbc9b13aa7cd24712ba9390964f10a90ca78f Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:09:41 +0100 Subject: [PATCH 01/16] first step --- web/messages/en/modal.json | 10 +- web/messages/en/users.json | 1 + .../UsersOverviewPage/UsersOverviewPage.tsx | 2 + .../pages/UsersOverviewPage/UsersTable.tsx | 13 ++ .../AddNewDeviceModal/AddNewDeviceModal.tsx | 139 ++++++++++++++++++ .../modals/AddNewDeviceModal/style.scss | 7 + .../useAddNewDeviceModal.tsx | 20 +++ web/src/shared/defguard-ui | 2 +- .../shared/hooks/modalControls/modalTypes.ts | 5 + web/src/shared/hooks/modalControls/types.ts | 4 + 10 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx create mode 100644 web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/style.scss create mode 100644 web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/useAddNewDeviceModal.tsx diff --git a/web/messages/en/modal.json b/web/messages/en/modal.json index 26ad3a3b8..a879cd642 100644 --- a/web/messages/en/modal.json +++ b/web/messages/en/modal.json @@ -75,6 +75,13 @@ "modal_add_api_token_copy_warning": "Please copy and save the API token below now. You won't be able to see it again.", "modal_rename_api_title": "Rename API token", "modal_add_user_title": "Add new user", + "modal_add_new_device_title": "Add new device", + "modal_add_new_device_subtitle": "Device activation settings", + "modal_add_new_device_subtitle_description": "Choose how to deliver the activation token and configure IP assignment for the user's device.", + "modal_add_new_device_choice_email_title": "Send token by email", + "modal_add_new_device_choice_email_content": "The device activation token will be sent to the user's email. You can use the one specified in the user's profile or enter an email manually.", + "modal_add_new_device_choice_manual_title": "Delivery token yourself", + "modal_add_new_device_choice_manual_content": "At the next step, you will receive an activation token and a URL, which you can share with the user in any way that is convenient for you.", "modal_add_vector_destination_title": "Add Vector destination", "modal_add_logstash_destination_title": "Add Logstash destination", "modal_logstash_destination_title": "Logstash", @@ -122,5 +129,6 @@ "modal_ce_webhook_edit_title": "Edit Webhook", "modal_ce_webhook_events_title": "Trigger events", "modal_ce_webhook_events_text": "", - "modal_assign_users_groups_title": "Assign groups to selected users" + "modal_assign_users_groups_title": "Assign groups to selected users", + "modal_add_new_device_error_no_option": "Please select at least one option" } diff --git a/web/messages/en/users.json b/web/messages/en/users.json index 75741282f..d0bfcbfd8 100644 --- a/web/messages/en/users.json +++ b/web/messages/en/users.json @@ -24,6 +24,7 @@ "users_row_menu_edit": "Edit details", "users_row_menu_change_password": "Change password", "users_row_menu_edit_groups": "Edit groups", + "user_row_menu_add_new_device": "Add new device", "users_row_menu_initiate_self_enrollment": "Initiate self-enrollment", "modal_edit_user_groups_title": "Edit user groups" } diff --git a/web/src/pages/UsersOverviewPage/UsersOverviewPage.tsx b/web/src/pages/UsersOverviewPage/UsersOverviewPage.tsx index 38335b2de..1be119ceb 100644 --- a/web/src/pages/UsersOverviewPage/UsersOverviewPage.tsx +++ b/web/src/pages/UsersOverviewPage/UsersOverviewPage.tsx @@ -6,6 +6,7 @@ import { AddAuthKeyModal } from '../../shared/components/modals/AddAuthKeyModal/ import { ChangePasswordModal } from '../../shared/components/modals/ChangePasswordModal/ChangePasswordModal'; import { TableSkeleton } from '../../shared/components/skeleton/TableSkeleton/TableSkeleton'; import { TablePageLayout } from '../../shared/layout/TablePageLayout/TablePageLayout'; +import { AddNewDeviceModal } from './modals/AddNewDeviceModal/AddNewDeviceModal'; import { AddUserModal } from './modals/AddUserModal/AddUserModal'; import { AssignUsersToGroupsModal } from './modals/AssignUsersToGroupsModal/AssignUsersToGroupsModal'; import { EditUserModal } from './modals/EditUserModal/EditUserModal'; @@ -22,6 +23,7 @@ export const UsersOverviewPage = () => { + diff --git a/web/src/pages/UsersOverviewPage/UsersTable.tsx b/web/src/pages/UsersOverviewPage/UsersTable.tsx index 19a04992d..5a9e83045 100644 --- a/web/src/pages/UsersOverviewPage/UsersTable.tsx +++ b/web/src/pages/UsersOverviewPage/UsersTable.tsx @@ -376,6 +376,19 @@ export const UsersTable = () => { ], }); } + if (rowData.enrolled) { + menuItems.splice(1, 0, { + items: [ + { + text: m.user_row_menu_add_new_device(), + icon: IconKind.AddDevice, + onClick: () => { + openModal(ModalName.AddNewDevice, rowData); + }, + }, + ], + }); + } return ( diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx new file mode 100644 index 000000000..3800a08ac --- /dev/null +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx @@ -0,0 +1,139 @@ +import './style.scss'; +import { useEffect, useState } from 'react'; +import { m } from '../../../../paraglide/messages'; +import type { User } from '../../../../shared/api/types'; +import { AppText } from '../../../../shared/defguard-ui/components/AppText/AppText'; +import { FieldError } from '../../../../shared/defguard-ui/components/FieldError/FieldError'; +import { Modal } from '../../../../shared/defguard-ui/components/Modal/Modal'; +import { ModalControls } from '../../../../shared/defguard-ui/components/ModalControls/ModalControls'; +import { SectionSelect } from '../../../../shared/defguard-ui/components/SectionSelect/SectionSelect'; +import { SizedBox } from '../../../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { + TextStyle, + ThemeSpacing, + ThemeVariable, +} from '../../../../shared/defguard-ui/types'; +import { isPresent } from '../../../../shared/defguard-ui/utils/isPresent'; +import { useAppForm } from '../../../../shared/form'; +import { + subscribeCloseModal, + subscribeOpenModal, +} from '../../../../shared/hooks/modalControls/modalsSubjects'; +import { ModalName } from '../../../../shared/hooks/modalControls/modalTypes'; +import { useApp } from '../../../../shared/hooks/useApp'; + +const modalName = ModalName.AddNewDevice; + +type DeliveryMethod = 'email' | 'manual'; + +export const AddNewDeviceModal = () => { + const [isOpen, setOpen] = useState(false); + const [user, setUser] = useState(null); + + useEffect(() => { + const openSub = subscribeOpenModal(modalName, (data) => { + setUser(data); + setOpen(true); + }); + const closeSub = subscribeCloseModal(modalName, () => setOpen(false)); + return () => { + openSub.unsubscribe(); + closeSub.unsubscribe(); + }; + }, []); + + return ( + setOpen(false)} + afterClose={() => { + setUser(null); + setOpen(false); + }} + > + {isPresent(user) && setOpen(false)} user={user} />} + + ); +}; + +const EnrollmentChoice = ({ onClose, user }: { onClose: () => void; user: User }) => { + const smtpEnabled = useApp((s) => s.appInfo.smtp_enabled); + const [selected, setSelected] = useState(null); + const [submitAttempted, setSubmitAttempted] = useState(false); + + const form = useAppForm({ + defaultValues: { + email: user.email ?? '', + }, + }); + + return ( + <> +
+ {m.modal_add_new_device_subtitle()} + + + {m.modal_add_new_device_subtitle_description()} + +
+ + + { + if (smtpEnabled) setSelected('email'); + }} + > + {selected === 'email' && ( + + {(field) => } + + )} + + + + setSelected('manual')} + /> + {submitAttempted && selected === null && ( + <> + + + + )} + { + setSubmitAttempted(true); + if (selected === null) return; + // next step will be implemented here + }, + }} + /> + + ); +}; diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/style.scss b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/style.scss new file mode 100644 index 000000000..07c43da66 --- /dev/null +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/style.scss @@ -0,0 +1,7 @@ +#add-user-modal { + form { + & > p { + font: var(--t-body-sm-500); + } + } +} diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/useAddNewDeviceModal.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/useAddNewDeviceModal.tsx new file mode 100644 index 000000000..00d7558d0 --- /dev/null +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/useAddNewDeviceModal.tsx @@ -0,0 +1,20 @@ +import { create } from 'zustand'; + +interface StoreValues { + isOpen: boolean; +} + +const defaults: StoreValues = { + isOpen: false, +}; + +interface Store extends StoreValues { + open: () => void; + reset: () => void; +} + +export const useAddNewDeviceModal = create((set) => ({ + ...defaults, + reset: () => set(defaults), + open: () => set({ isOpen: true }), +})); diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index 7db2a91bd..58115291f 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 7db2a91bd1744ab345a07e38d20c45724ccfa9ee +Subproject commit 58115291f6b101771e88cd9bc3f6fdb7a122f022 diff --git a/web/src/shared/hooks/modalControls/modalTypes.ts b/web/src/shared/hooks/modalControls/modalTypes.ts index 6ee2592ad..37178ca02 100644 --- a/web/src/shared/hooks/modalControls/modalTypes.ts +++ b/web/src/shared/hooks/modalControls/modalTypes.ts @@ -55,6 +55,7 @@ export const ModalName = { EditLogStreaming: 'editLogStreaming', DeleteLogStreaming: 'deleteLogStreaming', SelfEnrollmentToken: 'selfEnrollmentToken', + AddNewDevice: 'addNewDevice', } as const; export type ModalNameValue = (typeof ModalName)[keyof typeof ModalName]; @@ -180,6 +181,10 @@ const modalOpenArgsSchema = z.discriminatedUnion('name', [ name: z.literal(ModalName.LicenseExpired), data: z.custom(), }), + z.object({ + name: z.literal(ModalName.AddNewDevice), + data: z.custom(), + }), ]); export type ModalOpenEvent = z.infer; diff --git a/web/src/shared/hooks/modalControls/types.ts b/web/src/shared/hooks/modalControls/types.ts index 6e5005d5b..fd6e9851b 100644 --- a/web/src/shared/hooks/modalControls/types.ts +++ b/web/src/shared/hooks/modalControls/types.ts @@ -64,6 +64,10 @@ export interface OpenEnrollmentTokenModal { enrollmentResponse: StartEnrollmentResponse; } +export interface OpenAddNewDeviceModal { + user: User; +} + export interface OpenCEWebhookModal { webhook?: Webhook; } From 3d6067627bf5b9f01c82083106bdda0f3676943f Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:22:45 +0100 Subject: [PATCH 02/16] add second step --- web/messages/en/modal.json | 10 +- .../AddNewDeviceModal/AddNewDeviceModal.tsx | 78 ++++++++++++-- .../DeliveryTokenStep/DeliveryTokenStep.tsx | 100 ++++++++++++++++++ .../steps/DeliveryTokenStep/style.scss | 42 ++++++++ 4 files changed, 217 insertions(+), 13 deletions(-) create mode 100644 web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/DeliveryTokenStep.tsx create mode 100644 web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/style.scss diff --git a/web/messages/en/modal.json b/web/messages/en/modal.json index a879cd642..9bb6348ec 100644 --- a/web/messages/en/modal.json +++ b/web/messages/en/modal.json @@ -80,7 +80,7 @@ "modal_add_new_device_subtitle_description": "Choose how to deliver the activation token and configure IP assignment for the user's device.", "modal_add_new_device_choice_email_title": "Send token by email", "modal_add_new_device_choice_email_content": "The device activation token will be sent to the user's email. You can use the one specified in the user's profile or enter an email manually.", - "modal_add_new_device_choice_manual_title": "Delivery token yourself", + "modal_add_new_device_choice_manual_title": "Deliver token by yourself", "modal_add_new_device_choice_manual_content": "At the next step, you will receive an activation token and a URL, which you can share with the user in any way that is convenient for you.", "modal_add_vector_destination_title": "Add Vector destination", "modal_add_logstash_destination_title": "Add Logstash destination", @@ -130,5 +130,11 @@ "modal_ce_webhook_events_title": "Trigger events", "modal_ce_webhook_events_text": "", "modal_assign_users_groups_title": "Assign groups to selected users", - "modal_add_new_device_error_no_option": "Please select at least one option" + "modal_add_new_device_error_no_option": "Please select at least one option", + "modal_add_new_device_delivery_title": "Share activation credentials", + "modal_add_new_device_delivery_subtitle": "Share the following URL and token with the user to configure their Defguard desktop or mobile client.", + "modal_add_new_device_delivery_download_pdf": "Download instructions (PDF)", + "modal_add_new_device_delivery_pdf_title": "Download file with activation instructions", + "modal_add_new_device_delivery_pdf_subtitle": "Download the file with user's personalized instructions and activation data.", + "modal_add_new_device_delivery_qr_scan": "Scan QR code bellow to activate Defguard mobile application. If you haven't installed the mobile app, click one of the buttons bellow." } diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx index 3800a08ac..d50639489 100644 --- a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx @@ -1,7 +1,9 @@ import './style.scss'; import { useEffect, useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; import { m } from '../../../../paraglide/messages'; -import type { User } from '../../../../shared/api/types'; +import api from '../../../../shared/api/api'; +import type { StartEnrollmentResponse, User } from '../../../../shared/api/types'; import { AppText } from '../../../../shared/defguard-ui/components/AppText/AppText'; import { FieldError } from '../../../../shared/defguard-ui/components/FieldError/FieldError'; import { Modal } from '../../../../shared/defguard-ui/components/Modal/Modal'; @@ -21,6 +23,8 @@ import { } from '../../../../shared/hooks/modalControls/modalsSubjects'; import { ModalName } from '../../../../shared/hooks/modalControls/modalTypes'; import { useApp } from '../../../../shared/hooks/useApp'; +import { Snackbar } from '../../../../shared/defguard-ui/providers/snackbar/snackbar'; +import { DeliveryTokenStep } from './steps/DeliveryTokenStep/DeliveryTokenStep'; const modalName = ModalName.AddNewDevice; @@ -29,6 +33,16 @@ type DeliveryMethod = 'email' | 'manual'; export const AddNewDeviceModal = () => { const [isOpen, setOpen] = useState(false); const [user, setUser] = useState(null); + const [enrollmentData, setEnrollmentData] = useState(null); + + const handleClose = () => { + setOpen(false); + }; + + const handleAfterClose = () => { + setUser(null); + setEnrollmentData(null); + }; useEffect(() => { const openSub = subscribeOpenModal(modalName, (data) => { @@ -47,26 +61,67 @@ export const AddNewDeviceModal = () => { id="add-new-device-modal" title={m.modal_add_new_device_title()} isOpen={isOpen} - onClose={() => setOpen(false)} - afterClose={() => { - setUser(null); - setOpen(false); - }} + onClose={handleClose} + afterClose={handleAfterClose} > - {isPresent(user) && setOpen(false)} user={user} />} + {isPresent(enrollmentData) ? ( + + ) : ( + isPresent(user) && ( + + ) + )} ); }; -const EnrollmentChoice = ({ onClose, user }: { onClose: () => void; user: User }) => { +const EnrollmentChoice = ({ + onClose, + user, + onEnrollmentReady, +}: { + onClose: () => void; + user: User; + onEnrollmentReady: (data: StartEnrollmentResponse) => void; +}) => { const smtpEnabled = useApp((s) => s.appInfo.smtp_enabled); const [selected, setSelected] = useState(null); const [submitAttempted, setSubmitAttempted] = useState(false); + const { mutateAsync: startClientActivation, isPending } = useMutation({ + mutationFn: api.user.startClientActivation, + onError: (error) => { + Snackbar.error(m.failed_to_start_enrollment()); + console.error(error); + }, + }); + const form = useAppForm({ defaultValues: { email: user.email ?? '', }, + onSubmit: async ({ value }) => { + if (!isPresent(selected)) return; + if (selected === 'manual') { + const { data } = await startClientActivation({ + username: user.username, + send_enrollment_notification: false, + }); + onEnrollmentReady(data); + } else { + await startClientActivation({ + username: user.username, + send_enrollment_notification: true, + email: value.email, + }); + Snackbar.success(m.sucessfull_enrollment_email()); + onClose(); + } + }, }); return ( @@ -114,7 +169,7 @@ const EnrollmentChoice = ({ onClose, user }: { onClose: () => void; user: User } data-testid="add-new-device-manually" onClick={() => setSelected('manual')} /> - {submitAttempted && selected === null && ( + {submitAttempted && !isPresent(selected) && ( <> @@ -127,10 +182,11 @@ const EnrollmentChoice = ({ onClose, user }: { onClose: () => void; user: User } }} submitProps={{ text: m.controls_submit(), + loading: isPending, onClick: () => { setSubmitAttempted(true); - if (selected === null) return; - // next step will be implemented here + if (!isPresent(selected)) return; + form.handleSubmit(); }, }} /> diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/DeliveryTokenStep.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/DeliveryTokenStep.tsx new file mode 100644 index 000000000..e3d9e7a8d --- /dev/null +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/DeliveryTokenStep.tsx @@ -0,0 +1,100 @@ +import { useMemo } from 'react'; +import { m } from '../../../../../../paraglide/messages'; +import type { StartEnrollmentResponse } from '../../../../../../shared/api/types'; +import { externalLink } from '../../../../../../shared/constants'; +import { Button } from '../../../../../../shared/defguard-ui/components/Button/Button'; +import { CopyField } from '../../../../../../shared/defguard-ui/components/CopyField/CopyField'; +import { Divider } from '../../../../../../shared/defguard-ui/components/Divider/Divider'; +import { ModalControls } from '../../../../../../shared/defguard-ui/components/ModalControls/ModalControls'; +import { SizedBox } from '../../../../../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { ThemeSpacing } from '../../../../../../shared/defguard-ui/types'; +import { QRCodeCanvas } from 'qrcode.react'; +import './style.scss'; +import { isPresent } from '../../../../../../shared/defguard-ui/utils/isPresent'; + +type Props = { + enrollmentData: StartEnrollmentResponse; + onClose: () => void; +}; + +export const DeliveryTokenStep = ({ enrollmentData, onClose }: Props) => { + const qrData = useMemo(() => { + if (!enrollmentData) return null; + return btoa( + JSON.stringify({ + url: enrollmentData.enrollment_url, + token: enrollmentData.enrollment_token, + }), + ); + }, [enrollmentData.enrollment_url, enrollmentData.enrollment_token]); + + return ( +
+
+

{m.modal_add_new_device_delivery_pdf_title()}

+

{m.modal_add_new_device_delivery_pdf_subtitle()}

+ +
+ +
+

{m.modal_add_new_device_delivery_title()}

+ + + + +
+ + + + + +
+
+ {isPresent(qrData) && } +
+
+

{m.modal_add_new_device_delivery_qr_scan()}

+ + +
+
+ +
+ ); +}; diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/style.scss b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/style.scss new file mode 100644 index 000000000..3adfbfff5 --- /dev/null +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/style.scss @@ -0,0 +1,42 @@ +@use '@scssutils' as *; + +#add-new-device-delivery-step { + .section-title { + font: var(--t-body-sm-500); + } + + .section-subtitle { + font: var(--t-body-sm-400); + color: var(--fg-muted); + padding-top: var(--spacing-xs); + } + + .download-pdf { + padding-bottom: var(--spacing-lg); + } + + .share-credentials { + padding: var(--spacing-lg) 0; + } + + .qr-section { + display: grid; + grid-template-columns: 165px 1fr; + align-items: center; + column-gap: var(--spacing-4xl); + padding: var(--spacing-lg) 0; + + .mobile-download { + & > p { + font: var(--t-body-sm-400); + color: var(--fg-neutral); + } + + .links { + display: flex; + flex-flow: row wrap; + gap: var(--spacing-md); + } + } + } +} From 8a14579bd310ae6c66549f4b5bc871fbf8b8d593 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:07:04 +0100 Subject: [PATCH 03/16] remove qr and pdf --- web/messages/en/modal.json | 7 +-- .../AddNewDeviceModal/AddNewDeviceModal.tsx | 45 +++++++++++++- .../DeliveryTokenStep/DeliveryTokenStep.tsx | 60 ------------------- .../steps/DeliveryTokenStep/style.scss | 35 +---------- 4 files changed, 44 insertions(+), 103 deletions(-) diff --git a/web/messages/en/modal.json b/web/messages/en/modal.json index 9bb6348ec..0b06f741b 100644 --- a/web/messages/en/modal.json +++ b/web/messages/en/modal.json @@ -131,10 +131,5 @@ "modal_ce_webhook_events_text": "", "modal_assign_users_groups_title": "Assign groups to selected users", "modal_add_new_device_error_no_option": "Please select at least one option", - "modal_add_new_device_delivery_title": "Share activation credentials", - "modal_add_new_device_delivery_subtitle": "Share the following URL and token with the user to configure their Defguard desktop or mobile client.", - "modal_add_new_device_delivery_download_pdf": "Download instructions (PDF)", - "modal_add_new_device_delivery_pdf_title": "Download file with activation instructions", - "modal_add_new_device_delivery_pdf_subtitle": "Download the file with user's personalized instructions and activation data.", - "modal_add_new_device_delivery_qr_scan": "Scan QR code bellow to activate Defguard mobile application. If you haven't installed the mobile app, click one of the buttons bellow." + "modal_add_new_device_delivery_title": "Share activation credentials" } diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx index d50639489..9fa8848c9 100644 --- a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx @@ -1,6 +1,7 @@ import './style.scss'; -import { useEffect, useState } from 'react'; import { useMutation } from '@tanstack/react-query'; +import { useEffect, useMemo, useState } from 'react'; +import z from 'zod'; import { m } from '../../../../paraglide/messages'; import api from '../../../../shared/api/api'; import type { StartEnrollmentResponse, User } from '../../../../shared/api/types'; @@ -10,6 +11,7 @@ import { Modal } from '../../../../shared/defguard-ui/components/Modal/Modal'; import { ModalControls } from '../../../../shared/defguard-ui/components/ModalControls/ModalControls'; import { SectionSelect } from '../../../../shared/defguard-ui/components/SectionSelect/SectionSelect'; import { SizedBox } from '../../../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { Snackbar } from '../../../../shared/defguard-ui/providers/snackbar/snackbar'; import { TextStyle, ThemeSpacing, @@ -17,13 +19,13 @@ import { } from '../../../../shared/defguard-ui/types'; import { isPresent } from '../../../../shared/defguard-ui/utils/isPresent'; import { useAppForm } from '../../../../shared/form'; +import { formChangeLogic } from '../../../../shared/formLogic'; import { subscribeCloseModal, subscribeOpenModal, } from '../../../../shared/hooks/modalControls/modalsSubjects'; import { ModalName } from '../../../../shared/hooks/modalControls/modalTypes'; import { useApp } from '../../../../shared/hooks/useApp'; -import { Snackbar } from '../../../../shared/defguard-ui/providers/snackbar/snackbar'; import { DeliveryTokenStep } from './steps/DeliveryTokenStep/DeliveryTokenStep'; const modalName = ModalName.AddNewDevice; @@ -33,7 +35,9 @@ type DeliveryMethod = 'email' | 'manual'; export const AddNewDeviceModal = () => { const [isOpen, setOpen] = useState(false); const [user, setUser] = useState(null); - const [enrollmentData, setEnrollmentData] = useState(null); + const [enrollmentData, setEnrollmentData] = useState( + null, + ); const handleClose = () => { setOpen(false); @@ -100,10 +104,39 @@ const EnrollmentChoice = ({ }, }); + const formSchema = useMemo( + () => + z + .object({ + email: z.string(), + }) + .superRefine((values, ctx) => { + if (selected === 'email') { + const result = z + .email(m.form_error_email()) + .min(1, m.form_error_required()) + .safeParse(values.email); + if (!result.success) { + ctx.addIssue({ + code: 'custom', + path: ['email'], + message: result.error.issues[0].message, + }); + } + } + }), + [selected], + ); + const form = useAppForm({ defaultValues: { email: user.email ?? '', }, + validationLogic: formChangeLogic, + validators: { + onSubmit: formSchema, + onChange: formSchema, + }, onSubmit: async ({ value }) => { if (!isPresent(selected)) return; if (selected === 'manual') { @@ -124,6 +157,12 @@ const EnrollmentChoice = ({ }, }); + useEffect(() => { + if (!form.state.isPristine) { + form.validateAllFields('change'); + } + }, [form]); + return ( <>
diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/DeliveryTokenStep.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/DeliveryTokenStep.tsx index e3d9e7a8d..c9d0dfaa9 100644 --- a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/DeliveryTokenStep.tsx +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/DeliveryTokenStep.tsx @@ -1,16 +1,10 @@ -import { useMemo } from 'react'; import { m } from '../../../../../../paraglide/messages'; import type { StartEnrollmentResponse } from '../../../../../../shared/api/types'; -import { externalLink } from '../../../../../../shared/constants'; -import { Button } from '../../../../../../shared/defguard-ui/components/Button/Button'; import { CopyField } from '../../../../../../shared/defguard-ui/components/CopyField/CopyField'; -import { Divider } from '../../../../../../shared/defguard-ui/components/Divider/Divider'; import { ModalControls } from '../../../../../../shared/defguard-ui/components/ModalControls/ModalControls'; import { SizedBox } from '../../../../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../../../../shared/defguard-ui/types'; -import { QRCodeCanvas } from 'qrcode.react'; import './style.scss'; -import { isPresent } from '../../../../../../shared/defguard-ui/utils/isPresent'; type Props = { enrollmentData: StartEnrollmentResponse; @@ -18,32 +12,8 @@ type Props = { }; export const DeliveryTokenStep = ({ enrollmentData, onClose }: Props) => { - const qrData = useMemo(() => { - if (!enrollmentData) return null; - return btoa( - JSON.stringify({ - url: enrollmentData.enrollment_url, - token: enrollmentData.enrollment_token, - }), - ); - }, [enrollmentData.enrollment_url, enrollmentData.enrollment_token]); - return (
-
-

{m.modal_add_new_device_delivery_pdf_title()}

-

{m.modal_add_new_device_delivery_pdf_subtitle()}

- -
-

{m.modal_add_new_device_delivery_title()}

@@ -58,36 +28,6 @@ export const DeliveryTokenStep = ({ enrollmentData, onClose }: Props) => { copyTooltip={m.controls_copy_clipboard()} text={enrollmentData.enrollment_token} /> -
- - - - - -
-
- {isPresent(qrData) && } -
-
-

{m.modal_add_new_device_delivery_qr_scan()}

- - -
p { - font: var(--t-body-sm-400); - color: var(--fg-neutral); - } - - .links { - display: flex; - flex-flow: row wrap; - gap: var(--spacing-md); - } - } + padding-top: var(--spacing-lg) 0; } } From bcf0cc8889d5e34247f81c2ddccf9cc19e90b1f4 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:26:55 +0100 Subject: [PATCH 04/16] update submodule --- web/src/shared/defguard-ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index 96b3e1f59..399f50894 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 96b3e1f5922f43b4d9de3c1b210c36c02d592f46 +Subproject commit 399f508945f60ffbf8db7247ae7e052ac1d9e424 From 7c16c3f47cce17582fae78e6e963262f2ee87f2b Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:34:09 +0100 Subject: [PATCH 05/16] add newline --- web/src/shared/defguard-ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index 399f50894..146e89289 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 399f508945f60ffbf8db7247ae7e052ac1d9e424 +Subproject commit 146e8928933e5e941bffc51631e923f99be24a7d From 7c8ba6c5d60318b95763ef3739c0269cda631f56 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:40:34 +0100 Subject: [PATCH 06/16] lint again --- web/src/shared/defguard-ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index 146e89289..589b5f1fd 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 146e8928933e5e941bffc51631e923f99be24a7d +Subproject commit 589b5f1fd1e8477e8d1e38015445787ea7bf8070 From c85f331f0d31870f75ca8cc2a79b35357e9780e4 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:56:28 +0100 Subject: [PATCH 07/16] change name, use closeModal() --- .../AddNewDeviceModal/AddNewDeviceModal.tsx | 23 ++++++------------- .../DeliverTokenStep.tsx} | 7 +++--- .../style.scss | 0 3 files changed, 11 insertions(+), 19 deletions(-) rename web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/{DeliveryTokenStep/DeliveryTokenStep.tsx => DeliverTokenStep/DeliverTokenStep.tsx} (82%) rename web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/{DeliveryTokenStep => DeliverTokenStep}/style.scss (100%) diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx index 9fa8848c9..559fd35bd 100644 --- a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx @@ -21,12 +21,13 @@ import { isPresent } from '../../../../shared/defguard-ui/utils/isPresent'; import { useAppForm } from '../../../../shared/form'; import { formChangeLogic } from '../../../../shared/formLogic'; import { + closeModal, subscribeCloseModal, subscribeOpenModal, } from '../../../../shared/hooks/modalControls/modalsSubjects'; import { ModalName } from '../../../../shared/hooks/modalControls/modalTypes'; import { useApp } from '../../../../shared/hooks/useApp'; -import { DeliveryTokenStep } from './steps/DeliveryTokenStep/DeliveryTokenStep'; +import { DeliverTokenStep } from './steps/DeliverTokenStep/DeliverTokenStep'; const modalName = ModalName.AddNewDevice; @@ -39,10 +40,6 @@ export const AddNewDeviceModal = () => { null, ); - const handleClose = () => { - setOpen(false); - }; - const handleAfterClose = () => { setUser(null); setEnrollmentData(null); @@ -65,18 +62,14 @@ export const AddNewDeviceModal = () => { id="add-new-device-modal" title={m.modal_add_new_device_title()} isOpen={isOpen} - onClose={handleClose} + onClose={() => closeModal(modalName)} afterClose={handleAfterClose} > {isPresent(enrollmentData) ? ( - + ) : ( isPresent(user) && ( - + ) )} @@ -84,11 +77,9 @@ export const AddNewDeviceModal = () => { }; const EnrollmentChoice = ({ - onClose, user, onEnrollmentReady, }: { - onClose: () => void; user: User; onEnrollmentReady: (data: StartEnrollmentResponse) => void; }) => { @@ -152,7 +143,7 @@ const EnrollmentChoice = ({ email: value.email, }); Snackbar.success(m.sucessfull_enrollment_email()); - onClose(); + closeModal(modalName); } }, }); @@ -217,7 +208,7 @@ const EnrollmentChoice = ({ closeModal(modalName), }} submitProps={{ text: m.controls_submit(), diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/DeliveryTokenStep.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliverTokenStep/DeliverTokenStep.tsx similarity index 82% rename from web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/DeliveryTokenStep.tsx rename to web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliverTokenStep/DeliverTokenStep.tsx index c9d0dfaa9..0ae56ec7b 100644 --- a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/DeliveryTokenStep.tsx +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliverTokenStep/DeliverTokenStep.tsx @@ -4,14 +4,15 @@ import { CopyField } from '../../../../../../shared/defguard-ui/components/CopyF import { ModalControls } from '../../../../../../shared/defguard-ui/components/ModalControls/ModalControls'; import { SizedBox } from '../../../../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../../../../shared/defguard-ui/types'; +import { closeModal } from '../../../../../../shared/hooks/modalControls/modalsSubjects'; +import { ModalName } from '../../../../../../shared/hooks/modalControls/modalTypes'; import './style.scss'; type Props = { enrollmentData: StartEnrollmentResponse; - onClose: () => void; }; -export const DeliveryTokenStep = ({ enrollmentData, onClose }: Props) => { +export const DeliverTokenStep = ({ enrollmentData }: Props) => { return (
@@ -32,7 +33,7 @@ export const DeliveryTokenStep = ({ enrollmentData, onClose }: Props) => { closeModal(ModalName.AddNewDevice), }} />
diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/style.scss b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliverTokenStep/style.scss similarity index 100% rename from web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliveryTokenStep/style.scss rename to web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliverTokenStep/style.scss From 13f3e79b629fda038dfd6db66b979bdfec3f9827 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Feb 2026 10:48:48 +0100 Subject: [PATCH 08/16] merge --- web/src/shared/defguard-ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index 96b3e1f59..589b5f1fd 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 96b3e1f5922f43b4d9de3c1b210c36c02d592f46 +Subproject commit 589b5f1fd1e8477e8d1e38015445787ea7bf8070 From 37234da0da99f29e399ef179e0ea03ac9d8cca90 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:43:57 +0100 Subject: [PATCH 09/16] apply suggestions --- .../AddNewDeviceModal/AddNewDeviceModal.tsx | 53 +++++++++---------- .../DeliverTokenStep/DeliverTokenStep.tsx | 19 ++++--- .../useAddNewDeviceModal.tsx | 20 ------- 3 files changed, 37 insertions(+), 55 deletions(-) delete mode 100644 web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/useAddNewDeviceModal.tsx diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx index 559fd35bd..e96c6bcd0 100644 --- a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx @@ -5,10 +5,11 @@ import z from 'zod'; import { m } from '../../../../paraglide/messages'; import api from '../../../../shared/api/api'; import type { StartEnrollmentResponse, User } from '../../../../shared/api/types'; +import { Controls } from '../../../../shared/components/Controls/Controls'; import { AppText } from '../../../../shared/defguard-ui/components/AppText/AppText'; +import { Button } from '../../../../shared/defguard-ui/components/Button/Button'; import { FieldError } from '../../../../shared/defguard-ui/components/FieldError/FieldError'; import { Modal } from '../../../../shared/defguard-ui/components/Modal/Modal'; -import { ModalControls } from '../../../../shared/defguard-ui/components/ModalControls/ModalControls'; import { SectionSelect } from '../../../../shared/defguard-ui/components/SectionSelect/SectionSelect'; import { SizedBox } from '../../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { Snackbar } from '../../../../shared/defguard-ui/providers/snackbar/snackbar'; @@ -65,13 +66,10 @@ export const AddNewDeviceModal = () => { onClose={() => closeModal(modalName)} afterClose={handleAfterClose} > - {isPresent(enrollmentData) ? ( - - ) : ( - isPresent(user) && ( - - ) + {isPresent(user) && !isPresent(enrollmentData) && ( + )} + {isPresent(enrollmentData) && } ); }; @@ -148,12 +146,6 @@ const EnrollmentChoice = ({ }, }); - useEffect(() => { - if (!form.state.isPristine) { - form.validateAllFields('change'); - } - }, [form]); - return ( <>
@@ -205,21 +197,26 @@ const EnrollmentChoice = ({ )} - closeModal(modalName), - }} - submitProps={{ - text: m.controls_submit(), - loading: isPending, - onClick: () => { - setSubmitAttempted(true); - if (!isPresent(selected)) return; - form.handleSubmit(); - }, - }} - /> + + +
+
+
); }; diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliverTokenStep/DeliverTokenStep.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliverTokenStep/DeliverTokenStep.tsx index 0ae56ec7b..493952fc1 100644 --- a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliverTokenStep/DeliverTokenStep.tsx +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliverTokenStep/DeliverTokenStep.tsx @@ -1,7 +1,8 @@ import { m } from '../../../../../../paraglide/messages'; import type { StartEnrollmentResponse } from '../../../../../../shared/api/types'; +import { Controls } from '../../../../../../shared/components/Controls/Controls'; +import { Button } from '../../../../../../shared/defguard-ui/components/Button/Button'; import { CopyField } from '../../../../../../shared/defguard-ui/components/CopyField/CopyField'; -import { ModalControls } from '../../../../../../shared/defguard-ui/components/ModalControls/ModalControls'; import { SizedBox } from '../../../../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../../../../shared/defguard-ui/types'; import { closeModal } from '../../../../../../shared/hooks/modalControls/modalsSubjects'; @@ -30,12 +31,16 @@ export const DeliverTokenStep = ({ enrollmentData }: Props) => { text={enrollmentData.enrollment_token} />
- closeModal(ModalName.AddNewDevice), - }} - /> + + +
+
+
); }; diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/useAddNewDeviceModal.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/useAddNewDeviceModal.tsx deleted file mode 100644 index 00d7558d0..000000000 --- a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/useAddNewDeviceModal.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { create } from 'zustand'; - -interface StoreValues { - isOpen: boolean; -} - -const defaults: StoreValues = { - isOpen: false, -}; - -interface Store extends StoreValues { - open: () => void; - reset: () => void; -} - -export const useAddNewDeviceModal = create((set) => ({ - ...defaults, - reset: () => set(defaults), - open: () => set({ isOpen: true }), -})); From 0c5b0cee985f9cf6aa8e943150c7cd36a408cdc4 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:36:30 +0100 Subject: [PATCH 10/16] apply suggestions --- .../modals/AddNewDeviceModal/AddNewDeviceModal.tsx | 4 ++-- web/src/shared/defguard-ui | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx index e96c6bcd0..18a2f212b 100644 --- a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx @@ -174,11 +174,11 @@ const EnrollmentChoice = ({ if (smtpEnabled) setSelected('email'); }} > - {selected === 'email' && ( + {selected === 'email' ? ( {(field) => } - )} + ) : null} diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index 589b5f1fd..bcd578be9 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 589b5f1fd1e8477e8d1e38015445787ea7bf8070 +Subproject commit bcd578be991efafc9cf91fc851d70d2761224cac From 400de2b2e2683f9371db613bf9f75697c4b01cc9 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:55:44 +0100 Subject: [PATCH 11/16] linter --- web/src/shared/defguard-ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index bcd578be9..d75ea77e8 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit bcd578be991efafc9cf91fc851d70d2761224cac +Subproject commit d75ea77e88ec1e7c4120bcb9db351cd496db3cc8 From 77b9123b7e061c33cd3fb17e4d803c34491a87e5 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:10:03 +0100 Subject: [PATCH 12/16] use fold --- .../modals/AddNewDeviceModal/AddNewDeviceModal.tsx | 6 ++++-- web/src/shared/defguard-ui | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx index 18a2f212b..3e21f3fbe 100644 --- a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx @@ -9,6 +9,7 @@ import { Controls } from '../../../../shared/components/Controls/Controls'; import { AppText } from '../../../../shared/defguard-ui/components/AppText/AppText'; import { Button } from '../../../../shared/defguard-ui/components/Button/Button'; import { FieldError } from '../../../../shared/defguard-ui/components/FieldError/FieldError'; +import { Fold } from '../../../../shared/defguard-ui/components/Fold/Fold'; import { Modal } from '../../../../shared/defguard-ui/components/Modal/Modal'; import { SectionSelect } from '../../../../shared/defguard-ui/components/SectionSelect/SectionSelect'; import { SizedBox } from '../../../../shared/defguard-ui/components/SizedBox/SizedBox'; @@ -174,11 +175,12 @@ const EnrollmentChoice = ({ if (smtpEnabled) setSelected('email'); }} > - {selected === 'email' ? ( + + {(field) => } - ) : null} + diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index d75ea77e8..e941622f1 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit d75ea77e88ec1e7c4120bcb9db351cd496db3cc8 +Subproject commit e941622f1af6e3a8502967eee027bad3ae8e3fdd From f91ab4dd7288c81ef85d9db8c8fc9ac087c95fd5 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:27:40 +0100 Subject: [PATCH 13/16] edit scss --- web/src/shared/defguard-ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index e941622f1..282c7eb25 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit e941622f1af6e3a8502967eee027bad3ae8e3fdd +Subproject commit 282c7eb25f278537dad282af5d15bb94b64b5b83 From 6c1f2ad388c39607500e1b2440e5263a6ddd3ef1 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:30:41 +0100 Subject: [PATCH 14/16] change submodule --- web/src/shared/defguard-ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index 282c7eb25..3552857ad 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 282c7eb25f278537dad282af5d15bb94b64b5b83 +Subproject commit 3552857ad6f778a305bfabe464e7a0e14b19e515 From 7a02dc34421fa01fdc31f1133177376c1241ca8e Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:59:47 +0100 Subject: [PATCH 15/16] use form isSubmitting, use type="submit" --- .../AddNewDeviceModal/AddNewDeviceModal.tsx | 128 +++++++++--------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx index 3e21f3fbe..a0750fde9 100644 --- a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx @@ -1,4 +1,5 @@ import './style.scss'; +import { useStore } from '@tanstack/react-form'; import { useMutation } from '@tanstack/react-query'; import { useEffect, useMemo, useState } from 'react'; import z from 'zod'; @@ -8,7 +9,6 @@ import type { StartEnrollmentResponse, User } from '../../../../shared/api/types import { Controls } from '../../../../shared/components/Controls/Controls'; import { AppText } from '../../../../shared/defguard-ui/components/AppText/AppText'; import { Button } from '../../../../shared/defguard-ui/components/Button/Button'; -import { FieldError } from '../../../../shared/defguard-ui/components/FieldError/FieldError'; import { Fold } from '../../../../shared/defguard-ui/components/Fold/Fold'; import { Modal } from '../../../../shared/defguard-ui/components/Modal/Modal'; import { SectionSelect } from '../../../../shared/defguard-ui/components/SectionSelect/SectionSelect'; @@ -83,10 +83,11 @@ const EnrollmentChoice = ({ onEnrollmentReady: (data: StartEnrollmentResponse) => void; }) => { const smtpEnabled = useApp((s) => s.appInfo.smtp_enabled); - const [selected, setSelected] = useState(null); - const [submitAttempted, setSubmitAttempted] = useState(false); + const [selected, setSelected] = useState(() => + smtpEnabled ? 'email' : 'manual', + ); - const { mutateAsync: startClientActivation, isPending } = useMutation({ + const { mutateAsync: startClientActivation } = useMutation({ mutationFn: api.user.startClientActivation, onError: (error) => { Snackbar.error(m.failed_to_start_enrollment()); @@ -128,7 +129,6 @@ const EnrollmentChoice = ({ onChange: formSchema, }, onSubmit: async ({ value }) => { - if (!isPresent(selected)) return; if (selected === 'manual') { const { data } = await startClientActivation({ username: user.username, @@ -146,6 +146,7 @@ const EnrollmentChoice = ({ } }, }); + const isSubmitting = useStore(form.store, (s) => s.isSubmitting); return ( <> @@ -157,68 +158,67 @@ const EnrollmentChoice = ({
- - { - if (smtpEnabled) setSelected('email'); - }} - > - - - - {(field) => } - - - - - - setSelected('manual')} - /> - {submitAttempted && !isPresent(selected) && ( - <> - - - - )} - - -
-
-
+ + +
+
+
+ + ); }; From 11d7bc59d3b705441f1ae49ece2008ef95c9fa7f Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:06:09 +0100 Subject: [PATCH 16/16] delete translation --- web/messages/en/modal.json | 1 - 1 file changed, 1 deletion(-) diff --git a/web/messages/en/modal.json b/web/messages/en/modal.json index c7e24e879..8f0164d0d 100644 --- a/web/messages/en/modal.json +++ b/web/messages/en/modal.json @@ -130,7 +130,6 @@ "modal_ce_webhook_events_title": "Trigger events", "modal_ce_webhook_events_text": "", "modal_assign_users_groups_title": "Assign groups to selected users", - "modal_add_new_device_error_no_option": "Please select at least one option", "modal_add_new_device_delivery_title": "Share activation credentials", "modal_assign_user_ip_title": "{firstName} {lastName} IP settings", "modal_assign_user_ip_title_fallback": "IP settings",