diff --git a/frontend/src/components/Form/BasicAuthFields.tsx b/frontend/src/components/Form/BasicAuthFields.tsx index d8d33d159..0d5548490 100644 --- a/frontend/src/components/Form/BasicAuthFields.tsx +++ b/frontend/src/components/Form/BasicAuthFields.tsx @@ -38,8 +38,7 @@ export function BasicAuthFields({ initialValues, name = "items" }: Props) { }; const setFormField = (newValues: AccessListItem[]) => { - const filtered = newValues.filter((v: AccessListItem) => v?.username?.trim() !== ""); - setFieldValue(name, filtered); + setFieldValue(name, newValues); }; return ( diff --git a/frontend/src/components/Form/FormModal.drafts.test.tsx b/frontend/src/components/Form/FormModal.drafts.test.tsx new file mode 100644 index 000000000..7e1e11633 --- /dev/null +++ b/frontend/src/components/Form/FormModal.drafts.test.tsx @@ -0,0 +1,63 @@ +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { Form } from "formik"; +import type { ReactNode } from "react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { BasicAuthFields } from "./BasicAuthFields"; +import { FormModal } from "./FormModal"; +import { LocationsFields } from "./LocationsFields"; + +vi.mock("./AccessField", () => ({ AccessField: () => null })); + +const renderDraft = (initialValues: Record, children: ReactNode) => { + const onHide = vi.fn(); + render( + + {({ onHide: hide }) => ( +
+ {children} + +
+ )} +
, + ); + return onHide; +}; + +describe("FormModal local drafts", () => { + beforeEach(() => { + vi.stubGlobal( + "confirm", + vi.fn(() => false), + ); + }); + + afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); + }); + + it("protects a location draft without a path", () => { + const onHide = renderDraft({ locations: [] }, ); + fireEvent.click(screen.getByRole("button", { name: "Add Location" })); + fireEvent.change(screen.getByPlaceholderText("eg: 10.0.0.1/path/"), { target: { value: "example.com" } }); + + fireEvent.click(screen.getByRole("button", { name: "Cancel" })); + + expect(window.confirm).toHaveBeenCalledOnce(); + expect(onHide).not.toHaveBeenCalled(); + }); + + it("protects a basic auth password draft without a username", () => { + const onHide = renderDraft({ items: [] }, ); + fireEvent.change(document.querySelector('input[type="password"]') as Element, { + target: { value: "secret" }, + }); + + fireEvent.click(screen.getByRole("button", { name: "Cancel" })); + + expect(window.confirm).toHaveBeenCalledOnce(); + expect(onHide).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/components/Form/FormModal.test.tsx b/frontend/src/components/Form/FormModal.test.tsx new file mode 100644 index 000000000..de4080596 --- /dev/null +++ b/frontend/src/components/Form/FormModal.test.tsx @@ -0,0 +1,82 @@ +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { Field, Form } from "formik"; +import Modal from "react-bootstrap/Modal"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { FormModal } from "./FormModal"; + +const renderModal = (onHide = vi.fn(), preventHide = false) => { + render( + + {({ onHide: hide }) => ( +
+ + + + + + )} +
, + ); + return onHide; +}; + +describe("FormModal", () => { + beforeEach(() => { + vi.stubGlobal("confirm", vi.fn()); + }); + + afterEach(() => { + cleanup(); + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("dismisses a pristine form without confirmation", () => { + const onHide = renderModal(); + const confirm = vi.spyOn(window, "confirm"); + + fireEvent.click(screen.getByRole("button", { name: "Cancel" })); + + expect(confirm).not.toHaveBeenCalled(); + expect(onHide).toHaveBeenCalledOnce(); + }); + + it("keeps a dirty form open when dismissal is cancelled", () => { + const onHide = renderModal(); + const confirm = vi.spyOn(window, "confirm").mockReturnValue(false); + fireEvent.change(screen.getByLabelText("Name"), { target: { value: "example.com" } }); + + fireEvent.click(screen.getByRole("button", { name: "Cancel" })); + fireEvent.click(screen.getByRole("button", { name: "Close" })); + fireEvent.click(document.querySelector(".modal") as Element); + fireEvent.keyDown(document, { key: "Escape", keyCode: 27 }); + + expect(confirm).toHaveBeenCalledTimes(4); + expect(onHide).not.toHaveBeenCalled(); + expect(screen.getByLabelText("Name")).toHaveValue("example.com"); + }); + + it("dismisses a dirty form after confirmation", () => { + const onHide = renderModal(); + vi.spyOn(window, "confirm").mockReturnValue(true); + fireEvent.change(screen.getByLabelText("Name"), { target: { value: "example.com" } }); + + fireEvent.click(screen.getByRole("button", { name: "Cancel" })); + + expect(onHide).toHaveBeenCalledOnce(); + }); + + it("does not offer to discard changes while submission is in progress", () => { + const onHide = renderModal(vi.fn(), true); + const confirm = vi.spyOn(window, "confirm"); + fireEvent.change(screen.getByLabelText("Name"), { target: { value: "example.com" } }); + + fireEvent.click(screen.getByRole("button", { name: "Close" })); + + expect(confirm).not.toHaveBeenCalled(); + expect(onHide).not.toHaveBeenCalled(); + expect(screen.getByLabelText("Name")).toHaveValue("example.com"); + }); +}); diff --git a/frontend/src/components/Form/FormModal.tsx b/frontend/src/components/Form/FormModal.tsx new file mode 100644 index 000000000..d9698521d --- /dev/null +++ b/frontend/src/components/Form/FormModal.tsx @@ -0,0 +1,55 @@ +import { Formik, type FormikConfig, type FormikProps, type FormikValues } from "formik"; +import { type ReactNode, useEffect } from "react"; +import Modal from "react-bootstrap/Modal"; +import { intl } from "src/locale"; + +const confirmDiscardChanges = (dirty: boolean) => + !dirty || + window.confirm( + intl.formatMessage({ + id: "form.unsaved-changes", + defaultMessage: "You have unsaved changes. Are you sure you want to discard them?", + }), + ); + +const FormDirtyState = ({ dirty, onChange }: { dirty: boolean; onChange: (dirty: boolean) => void }) => { + useEffect(() => { + onChange(dirty); + return () => onChange(false); + }, [dirty, onChange]); + + return null; +}; + +interface FormModalProps extends Omit, "children"> { + show: boolean; + onHide: () => void; + preventHide?: boolean; + children: (props: FormikProps & { onHide: () => void }) => ReactNode; +} + +const FormModal = ({ + show, + onHide, + preventHide = false, + children, + ...formikProps +}: FormModalProps) => ( + + {(formikProps) => { + const handleHide = () => { + if (!preventHide && confirmDiscardChanges(formikProps.dirty)) { + onHide(); + } + }; + + return ( + + {children({ ...formikProps, onHide: handleHide })} + + ); + }} + +); + +export { confirmDiscardChanges, FormDirtyState, FormModal }; diff --git a/frontend/src/components/Form/LocationsFields.tsx b/frontend/src/components/Form/LocationsFields.tsx index a0f64c239..0f8d83033 100644 --- a/frontend/src/components/Form/LocationsFields.tsx +++ b/frontend/src/components/Form/LocationsFields.tsx @@ -88,8 +88,10 @@ export function LocationsFields({ initialValues, name = "locations" }: Props) { }; const setFormField = (newRows: Row[]) => { - const filtered = newRows.map((r: Row) => r.value).filter((v: ProxyLocation) => v?.path?.trim() !== ""); - setFieldValue(name, filtered); + setFieldValue( + name, + newRows.map((r: Row) => r.value), + ); }; const forwardSummary = (item: ProxyLocation) => { diff --git a/frontend/src/components/Form/index.ts b/frontend/src/components/Form/index.ts index f218b2e33..8530d6f6d 100644 --- a/frontend/src/components/Form/index.ts +++ b/frontend/src/components/Form/index.ts @@ -3,6 +3,7 @@ export * from "./AccessField"; export * from "./BasicAuthFields"; export * from "./DNSProviderFields"; export * from "./DomainNamesField"; +export * from "./FormModal"; export * from "./LocationsFields"; export * from "./NginxConfigField"; export * from "./SSLCertificateField"; diff --git a/frontend/src/locale/src/en.json b/frontend/src/locale/src/en.json index 11d09ea71..14c1d5b44 100644 --- a/frontend/src/locale/src/en.json +++ b/frontend/src/locale/src/en.json @@ -437,6 +437,9 @@ "footer.github-fork": { "defaultMessage": "Fork me on Github" }, + "form.unsaved-changes": { + "defaultMessage": "You have unsaved changes. Are you sure you want to discard them?" + }, "host.flags.block-exploits": { "defaultMessage": "Block Common Exploits" }, diff --git a/frontend/src/locale/src/et.json b/frontend/src/locale/src/et.json index 316ede752..2301c5a46 100644 --- a/frontend/src/locale/src/et.json +++ b/frontend/src/locale/src/et.json @@ -437,6 +437,9 @@ "footer.github-fork": { "defaultMessage": "Tee GitHubis fork" }, + "form.unsaved-changes": { + "defaultMessage": "Sul on salvestamata muudatusi. Kas oled kindel, et soovid need tühistada?" + }, "host.flags.block-exploits": { "defaultMessage": "Blokeeri levinud ründed" }, diff --git a/frontend/src/modals/AccessListModal.tsx b/frontend/src/modals/AccessListModal.tsx index 79537f5cd..01b68d3d6 100644 --- a/frontend/src/modals/AccessListModal.tsx +++ b/frontend/src/modals/AccessListModal.tsx @@ -1,11 +1,12 @@ import cn from "classnames"; import EasyModal, { type InnerModalProps } from "ez-modal-react"; -import { Field, Form, Formik } from "formik"; +import { Field, Form } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; import type { AccessList, AccessListClient, AccessListItem } from "src/api/backend"; import { AccessClientFields, BasicAuthFields, Button, Loading } from "src/components"; +import { FormModal } from "src/components/Form"; import { useAccessList, useSetAccessList } from "src/hooks"; import { intl, T } from "src/locale"; import { validateString } from "src/modules/Validations"; @@ -25,13 +26,16 @@ const AccessListModal = EasyModal.create(({ id, visible, remove }: Props) => { const [isSubmitting, setIsSubmitting] = useState(false); const validate = (values: any): string | null => { + const items = (values.items || []).filter((i: AccessListItem) => i.username?.trim() !== ""); + const clients = (values.clients || []).filter((i: AccessListClient) => i.address?.trim() !== ""); + // either Auths or Clients must be defined - if (values.items?.length === 0 && values.clients?.length === 0) { + if (items.length === 0 && clients.length === 0) { return intl.formatMessage({ id: "error.access.at-least-one" }); } // ensure the items don't contain the same username twice - const usernames = values.items.map((i: any) => i.username); + const usernames = items.map((i: AccessListItem) => i.username); const uniqueUsernames = Array.from(new Set(usernames)); if (usernames.length !== uniqueUsernames.length) { return intl.formatMessage({ id: "error.access.duplicate-usernames" }); @@ -58,10 +62,12 @@ const AccessListModal = EasyModal.create(({ id, visible, remove }: Props) => { }; // Filter out "items" to only use the "username" and "password" fields - payload.items = (values.items || []).map((i: AccessListItem) => ({ - username: i.username, - password: i.password, - })); + payload.items = (values.items || []) + .filter((i: AccessListItem) => i.username?.trim() !== "") + .map((i: AccessListItem) => ({ + username: i.username, + password: i.password, + })); // Filter out "clients" to only use the "directive" and "address" fields payload.clients = (values.clients || []).map((i: AccessListClient) => ({ @@ -86,15 +92,22 @@ const AccessListModal = EasyModal.create(({ id, visible, remove }: Props) => { const toggleEnabled = cn(toggleClasses, "bg-cyan"); return ( - - {!isLoading && error && ( - - {error?.message || "Unknown error"} - + <> + {(isLoading || (error && !data)) && ( + + {!isLoading && error && ( + + {error?.message || "Unknown error"} + + )} + {isLoading && } + )} - {isLoading && } {!isLoading && data && ( - { } onSubmit={onSubmit} > - {({ setFieldValue }: any) => ( + {({ onHide, setFieldValue }: any) => (
@@ -267,7 +280,7 @@ const AccessListModal = EasyModal.create(({ id, visible, remove }: Props) => { - - - - - )} -
-
+ + {({ onHide }) => ( +
+ + + + + + + setError(null)} dismissible> + {error} + +
+ + {({ field, form }: any) => ( +
+ + + {form.errors.name ? ( +
+ {form.errors.current && form.touched.current + ? form.errors.current + : null} +
+ ) : null} +
+ )} +
+
+
+ + {({ field, form }: any) => ( +
+ + + {form.errors.new ? ( +
+ {form.errors.new && form.touched.new ? form.errors.new : null} +
+ ) : null} +
+ )} +
+
+
+ + {({ field, form }: any) => ( +
+ + {form.errors.confirm ? ( +
+ {form.errors.confirm && form.touched.confirm + ? form.errors.confirm + : null} +
+ ) : null} + +
+ )} +
+
+
+ + + + +
+ )} +
); }); diff --git a/frontend/src/modals/CustomCertificateModal.tsx b/frontend/src/modals/CustomCertificateModal.tsx index deab1c523..c373a8d36 100644 --- a/frontend/src/modals/CustomCertificateModal.tsx +++ b/frontend/src/modals/CustomCertificateModal.tsx @@ -1,12 +1,13 @@ import { IconAlertTriangle } from "@tabler/icons-react"; import { useQueryClient } from "@tanstack/react-query"; import EasyModal, { type InnerModalProps } from "ez-modal-react"; -import { Field, Form, Formik } from "formik"; +import { Field, Form } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; import { type Certificate, createCertificate, uploadCertificate, validateCertificate } from "src/api/backend"; import { Button } from "src/components"; +import { FormModal } from "src/components/Form"; import { T } from "src/locale"; import { validateString } from "src/modules/Validations"; import { showObjectSuccess } from "src/notifications"; @@ -57,175 +58,176 @@ const CustomCertificateModal = EasyModal.create(({ visible, remove }: InnerModal }; return ( - - - {() => ( -
- - - - - - - setErrorMsg(null)} dismissible> - {errorMsg} - -
-
-

- - -

- - {({ field, form }: any) => ( -
- - - {form.errors.niceName ? ( -
- {form.errors.niceName && form.touched.niceName - ? form.errors.niceName - : null} -
- ) : null} -
- )} -
- - {({ field, form }: any) => ( -
- - { - form.setFieldValue( - field.name, - event.currentTarget.files?.length - ? event.currentTarget.files[0] - : null, - ); - }} - /> - {form.errors.certificateKey ? ( -
- {form.errors.certificateKey && form.touched.certificateKey - ? form.errors.certificateKey - : null} -
- ) : null} -
- )} -
- - {({ field, form }: any) => ( -
- - { - form.setFieldValue( - field.name, - event.currentTarget.files?.length - ? event.currentTarget.files[0] - : null, - ); - }} - /> - {form.errors.certificate ? ( -
- {form.errors.certificate && form.touched.certificate - ? form.errors.certificate - : null} -
- ) : null} -
- )} -
- - {({ field, form }: any) => ( -
- - { - form.setFieldValue( - field.name, - event.currentTarget.files?.length - ? event.currentTarget.files[0] - : null, - ); - }} - /> - {form.errors.intermediateCertificate ? ( -
- {form.errors.intermediateCertificate && - form.touched.intermediateCertificate - ? form.errors.intermediateCertificate - : null} -
- ) : null} -
- )} -
-
+ + {({ onHide }) => ( + + + + + + + + setErrorMsg(null)} dismissible> + {errorMsg} + +
+
+

+ + +

+ + {({ field, form }: any) => ( +
+ + + {form.errors.niceName ? ( +
+ {form.errors.niceName && form.touched.niceName + ? form.errors.niceName + : null} +
+ ) : null} +
+ )} +
+ + {({ field, form }: any) => ( +
+ + { + form.setFieldValue( + field.name, + event.currentTarget.files?.length + ? event.currentTarget.files[0] + : null, + ); + }} + /> + {form.errors.certificateKey ? ( +
+ {form.errors.certificateKey && form.touched.certificateKey + ? form.errors.certificateKey + : null} +
+ ) : null} +
+ )} +
+ + {({ field, form }: any) => ( +
+ + { + form.setFieldValue( + field.name, + event.currentTarget.files?.length + ? event.currentTarget.files[0] + : null, + ); + }} + /> + {form.errors.certificate ? ( +
+ {form.errors.certificate && form.touched.certificate + ? form.errors.certificate + : null} +
+ ) : null} +
+ )} +
+ + {({ field, form }: any) => ( +
+ + { + form.setFieldValue( + field.name, + event.currentTarget.files?.length + ? event.currentTarget.files[0] + : null, + ); + }} + /> + {form.errors.intermediateCertificate ? ( +
+ {form.errors.intermediateCertificate && + form.touched.intermediateCertificate + ? form.errors.intermediateCertificate + : null} +
+ ) : null} +
+ )} +
- - - - - - - )} - - +
+
+ + + + + + )} +
); }); diff --git a/frontend/src/modals/DNSCertificateModal.tsx b/frontend/src/modals/DNSCertificateModal.tsx index bca8df8d3..be4cf3c22 100644 --- a/frontend/src/modals/DNSCertificateModal.tsx +++ b/frontend/src/modals/DNSCertificateModal.tsx @@ -1,11 +1,12 @@ import { useQueryClient } from "@tanstack/react-query"; import EasyModal, { type InnerModalProps } from "ez-modal-react"; -import { Form, Formik, Field } from "formik"; +import { Field, Form } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; import { createCertificate } from "src/api/backend"; import { Button, DNSProviderFields, DomainNamesField } from "src/components"; +import { FormModal } from "src/components/Form"; import { T } from "src/locale"; import { showObjectSuccess } from "src/notifications"; @@ -36,77 +37,78 @@ const DNSCertificateModal = EasyModal.create(({ visible, remove }: InnerModalPro }; return ( - - - {() => ( -
- - - - - - - setErrorMsg(null)} dismissible> - {errorMsg} - -
-
- - - {({ field }: any) => ( -
- - - - - -
- )} -
- -
+ + {({ onHide }) => ( + + + + + + + + setErrorMsg(null)} dismissible> + {errorMsg} + +
+
+ + + {({ field }: any) => ( +
+ + + + + +
+ )} +
+
- - - - - - - )} - - +
+
+ + + + + + )} +
); }); diff --git a/frontend/src/modals/DeadHostModal.tsx b/frontend/src/modals/DeadHostModal.tsx index c9dce9a1c..8205936a5 100644 --- a/frontend/src/modals/DeadHostModal.tsx +++ b/frontend/src/modals/DeadHostModal.tsx @@ -1,6 +1,6 @@ import { IconSettings } from "@tabler/icons-react"; import EasyModal, { type InnerModalProps } from "ez-modal-react"; -import { Form, Formik } from "formik"; +import { Form } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; @@ -12,6 +12,7 @@ import { SSLCertificateField, SSLOptionsFields, } from "src/components"; +import { FormModal } from "src/components/Form"; import { useDeadHost, useSetDeadHost } from "src/hooks"; import { T } from "src/locale"; import { showObjectSuccess } from "src/notifications"; @@ -53,15 +54,22 @@ const DeadHostModal = EasyModal.create(({ id, visible, remove }: Props) => { }; return ( - - {!isLoading && error && ( - - {error?.message || "Unknown error"} - + <> + {(isLoading || (error && !data)) && ( + + {!isLoading && error && ( + + {error?.message || "Unknown error"} + + )} + {isLoading && } + )} - {isLoading && } {!isLoading && data && ( - { } onSubmit={onSubmit} > - {() => ( + {({ onHide }) => (
@@ -149,7 +157,7 @@ const DeadHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
-
+
+ + +
+ -
- - -
- - - )} - - + +
+
+ + )} + ); }); diff --git a/frontend/src/modals/PermissionsModal.tsx b/frontend/src/modals/PermissionsModal.tsx index d363de9ed..4f6bc0fab 100644 --- a/frontend/src/modals/PermissionsModal.tsx +++ b/frontend/src/modals/PermissionsModal.tsx @@ -1,12 +1,13 @@ import { useQueryClient } from "@tanstack/react-query"; import cn from "classnames"; import EasyModal, { type InnerModalProps } from "ez-modal-react"; -import { Field, Form, Formik } from "formik"; +import { Field, Form } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; import { setPermissions } from "src/api/backend"; import { Button, Loading } from "src/components"; +import { FormModal } from "src/components/Form"; import { useUser } from "src/hooks"; import { T } from "src/locale"; import styles from "./PermissionsModal.module.css"; @@ -133,15 +134,22 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => { const isAdmin = data?.roles.indexOf("admin") !== -1; return ( - - {!isLoading && error && ( - - {error?.message || "Unknown error"} - + <> + {(isLoading || (error && !data)) && ( + + {!isLoading && error && ( + + {error?.message || "Unknown error"} + + )} + {isLoading && } + )} - {isLoading && } {!isLoading && data && ( - { } onSubmit={onSubmit} > - {() => ( + {({ onHide }) => (
@@ -263,7 +271,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => { )} - @@ -368,9 +377,9 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => { )} -
+ )} -
+ ); }); diff --git a/frontend/src/modals/RedirectionHostModal.tsx b/frontend/src/modals/RedirectionHostModal.tsx index 9a0d95ce8..f9af6ccf9 100644 --- a/frontend/src/modals/RedirectionHostModal.tsx +++ b/frontend/src/modals/RedirectionHostModal.tsx @@ -1,7 +1,7 @@ import { IconSettings } from "@tabler/icons-react"; import cn from "classnames"; import EasyModal, { type InnerModalProps } from "ez-modal-react"; -import { Field, Form, Formik } from "formik"; +import { Field, Form } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; @@ -13,6 +13,7 @@ import { SSLCertificateField, SSLOptionsFields, } from "src/components"; +import { FormModal } from "src/components/Form"; import { useRedirectionHost, useSetRedirectionHost } from "src/hooks"; import { T } from "src/locale"; import { validateString } from "src/modules/Validations"; @@ -55,15 +56,22 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) = }; return ( - - {!isLoading && error && ( - - {error?.message || "Unknown error"} - + <> + {(isLoading || (error && !data)) && ( + + {!isLoading && error && ( + + {error?.message || "Unknown error"} + + )} + {isLoading && } + )} - {isLoading && } {!isLoading && data && ( - - {() => ( + {({ onHide }) => (
@@ -326,7 +334,7 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) = - - - - - )} -
-
+ + {({ onHide }) => ( +
+ + + + + + + setError(null)} dismissible> + {error} + +
+ + {({ field, form }: any) => ( + <> +

+ + { + e.preventDefault(); + form.setFieldValue( + field.name, + generate({ + length: 12, + numbers: true, + }), + ); + setShowPassword(true); + }} + > + + {" "} + —{" "} + { + e.preventDefault(); + setShowPassword(!showPassword); + }} + > + + + +

+
+ + + {form.errors.new ? ( +
+ {form.errors.new && form.touched.new ? form.errors.new : null} +
+ ) : null} +
+ + )} +
+
+
+ + + + +
+ )} +
); }); diff --git a/frontend/src/modals/StreamModal.tsx b/frontend/src/modals/StreamModal.tsx index b87a259c4..417ae3103 100644 --- a/frontend/src/modals/StreamModal.tsx +++ b/frontend/src/modals/StreamModal.tsx @@ -1,9 +1,10 @@ import EasyModal, { type InnerModalProps } from "ez-modal-react"; -import { Field, Form, Formik } from "formik"; +import { Field, Form } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; import { Button, Loading, SSLCertificateField, SSLOptionsFields } from "src/components"; +import { FormModal } from "src/components/Form"; import { useSetStream, useStream } from "src/hooks"; import { intl, T } from "src/locale"; import { validateNumber, validateString } from "src/modules/Validations"; @@ -46,15 +47,22 @@ const StreamModal = EasyModal.create(({ id, visible, remove }: Props) => { }; return ( - - {!isLoading && error && ( - - {error?.message || "Unknown error"} - + <> + {(isLoading || (error && !data)) && ( + + {!isLoading && error && ( + + {error?.message || "Unknown error"} + + )} + {isLoading && } + )} - {isLoading && } {!isLoading && data && ( - { } onSubmit={onSubmit} > - {({ setFieldValue }: any) => ( + {({ onHide, setFieldValue }: any) => (
@@ -302,7 +310,7 @@ const StreamModal = EasyModal.create(({ id, visible, remove }: Props) => { -