Use a modal manager

This commit is contained in:
Jamie Curnow
2025-10-14 17:49:56 +10:00
parent e6f7ae3fba
commit 7af01d0fc7
32 changed files with 291 additions and 251 deletions

View File

@@ -1,4 +1,5 @@
import cn from "classnames";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
@@ -10,11 +11,14 @@ import { intl, T } from "src/locale";
import { validateString } from "src/modules/Validations";
import { showSuccess } from "src/notifications";
interface Props {
const showAccessListModal = (id: number | "new") => {
EasyModal.show(AccessListModal, { id });
};
interface Props extends InnerModalProps {
id: number | "new";
onClose: () => void;
}
export function AccessListModal({ id, onClose }: Props) {
const AccessListModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useAccessList(id, ["items", "clients"]);
const { mutate: setAccessList } = useSetAccessList();
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
@@ -69,7 +73,7 @@ export function AccessListModal({ id, onClose }: Props) {
onError: (err: any) => setErrorMsg(<T id={err.message} />),
onSuccess: () => {
showSuccess(intl.formatMessage({ id: "notification.access-saved" }));
onClose();
remove();
},
onSettled: () => {
setIsSubmitting(false);
@@ -82,7 +86,7 @@ export function AccessListModal({ id, onClose }: Props) {
const toggleEnabled = cn(toggleClasses, "bg-cyan");
return (
<Modal show onHide={onClose} animation={false}>
<Modal show={visible} onHide={remove}>
{!isLoading && error && (
<Alert variant="danger" className="m-3">
{error?.message || "Unknown error"}
@@ -263,7 +267,7 @@ export function AccessListModal({ id, onClose }: Props) {
</div>
</Modal.Body>
<Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>
<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" />
</Button>
<Button
@@ -283,4 +287,6 @@ export function AccessListModal({ id, onClose }: Props) {
)}
</Modal>
);
}
});
export { showAccessListModal };

View File

@@ -1,3 +1,4 @@
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
@@ -7,11 +8,14 @@ import { Button } from "src/components";
import { intl, T } from "src/locale";
import { validateString } from "src/modules/Validations";
interface Props {
userId: number | "me";
onClose: () => void;
const showChangePasswordModal = (id: number | "me") => {
EasyModal.show(ChangePasswordModal, { id });
};
interface Props extends InnerModalProps {
id: number | "me";
}
export function ChangePasswordModal({ userId, onClose }: Props) {
const ChangePasswordModal = EasyModal.create(({ id, visible, remove }: Props) => {
const [error, setError] = useState<ReactNode | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -27,8 +31,8 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
setError(null);
try {
await updateAuth(userId, values.new, values.current);
onClose();
await updateAuth(id, values.new, values.current);
remove();
} catch (err: any) {
setError(<T id={err.message} />);
}
@@ -37,7 +41,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
};
return (
<Modal show onHide={onClose} animation={false}>
<Modal show={visible} onHide={remove}>
<Formik
initialValues={
{
@@ -142,7 +146,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
</div>
</Modal.Body>
<Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>
<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" />
</Button>
<Button
@@ -161,4 +165,6 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
</Formik>
</Modal>
);
}
});
export { showChangePasswordModal };

View File

@@ -1,4 +1,5 @@
import { IconSettings } from "@tabler/icons-react";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
@@ -15,11 +16,14 @@ import { useDeadHost, useSetDeadHost } from "src/hooks";
import { intl, T } from "src/locale";
import { showSuccess } from "src/notifications";
interface Props {
const showDeadHostModal = (id: number | "new") => {
EasyModal.show(DeadHostModal, { id });
};
interface Props extends InnerModalProps {
id: number | "new";
onClose: () => void;
}
export function DeadHostModal({ id, onClose }: Props) {
const DeadHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useDeadHost(id);
const { mutate: setDeadHost } = useSetDeadHost();
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
@@ -39,7 +43,7 @@ export function DeadHostModal({ id, onClose }: Props) {
onError: (err: any) => setErrorMsg(<T id={err.message} />),
onSuccess: () => {
showSuccess(intl.formatMessage({ id: "notification.dead-host-saved" }));
onClose();
remove();
},
onSettled: () => {
setIsSubmitting(false);
@@ -49,7 +53,7 @@ export function DeadHostModal({ id, onClose }: Props) {
};
return (
<Modal show onHide={onClose} animation={false}>
<Modal show={visible} onHide={remove}>
{!isLoading && error && (
<Alert variant="danger" className="m-3">
{error?.message || "Unknown error"}
@@ -145,7 +149,7 @@ export function DeadHostModal({ id, onClose }: Props) {
</div>
</Modal.Body>
<Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>
<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" />
</Button>
<Button
@@ -165,4 +169,6 @@ export function DeadHostModal({ id, onClose }: Props) {
)}
</Modal>
);
}
});
export { showDeadHostModal };

View File

@@ -1,18 +1,25 @@
import { useQueryClient } from "@tanstack/react-query";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { Button } from "src/components";
import { T } from "src/locale";
interface Props {
interface ShowProps {
title: string;
children: ReactNode;
onConfirm: () => Promise<void> | void;
onClose: () => void;
invalidations?: any[];
}
export function DeleteConfirmModal({ title, children, onConfirm, onClose, invalidations }: Props) {
interface Props extends InnerModalProps, ShowProps {}
const showDeleteConfirmModal = (props: ShowProps) => {
EasyModal.show(DeleteConfirmModal, props);
};
const DeleteConfirmModal = EasyModal.create(({ title, children, onConfirm, invalidations, visible, remove }: Props) => {
const queryClient = useQueryClient();
const [error, setError] = useState<ReactNode | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -23,7 +30,7 @@ export function DeleteConfirmModal({ title, children, onConfirm, onClose, invali
setError(null);
try {
await onConfirm();
onClose();
remove();
// invalidate caches as requested
invalidations?.forEach((inv) => {
queryClient.invalidateQueries({ queryKey: inv });
@@ -35,7 +42,7 @@ export function DeleteConfirmModal({ title, children, onConfirm, onClose, invali
};
return (
<Modal show onHide={onClose} animation={false}>
<Modal show={visible} onHide={remove}>
<Modal.Header closeButton>
<Modal.Title>
<T id={title} />
@@ -48,7 +55,7 @@ export function DeleteConfirmModal({ title, children, onConfirm, onClose, invali
{children}
</Modal.Body>
<Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>
<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" />
</Button>
<Button
@@ -65,4 +72,6 @@ export function DeleteConfirmModal({ title, children, onConfirm, onClose, invali
</Modal.Footer>
</Modal>
);
}
});
export { showDeleteConfirmModal };

View File

@@ -1,18 +1,22 @@
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { Button, EventFormatter, GravatarFormatter, Loading } from "src/components";
import { useAuditLog } from "src/hooks";
import { T } from "src/locale";
interface Props {
const showEventDetailsModal = (id: number) => {
EasyModal.show(EventDetailsModal, { id });
};
interface Props extends InnerModalProps {
id: number;
onClose: () => void;
}
export function EventDetailsModal({ id, onClose }: Props) {
const EventDetailsModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useAuditLog(id);
return (
<Modal show onHide={onClose} animation={false}>
<Modal show={visible} onHide={remove}>
{!isLoading && error && (
<Alert variant="danger" className="m-3">
{error?.message || "Unknown error"}
@@ -41,7 +45,7 @@ export function EventDetailsModal({ id, onClose }: Props) {
</div>
</Modal.Body>
<Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose}>
<Button data-bs-dismiss="modal" onClick={remove}>
<T id="close" />
</Button>
</Modal.Footer>
@@ -49,4 +53,6 @@ export function EventDetailsModal({ id, onClose }: Props) {
)}
</Modal>
);
}
});
export { showEventDetailsModal };

View File

@@ -1,5 +1,6 @@
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 { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
@@ -9,14 +10,17 @@ import { Button, Loading } from "src/components";
import { useUser } from "src/hooks";
import { T } from "src/locale";
interface Props {
userId: number;
onClose: () => void;
const showPermissionsModal = (id: number) => {
EasyModal.show(PermissionsModal, { id });
};
interface Props extends InnerModalProps {
id: number;
}
export function PermissionsModal({ userId, onClose }: Props) {
const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
const queryClient = useQueryClient();
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
const { data, isLoading, error } = useUser(userId);
const { data, isLoading, error } = useUser(id);
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => {
@@ -24,8 +28,8 @@ export function PermissionsModal({ userId, onClose }: Props) {
setIsSubmitting(true);
setErrorMsg(null);
try {
await setPermissions(userId, values);
onClose();
await setPermissions(id, values);
remove();
queryClient.invalidateQueries({ queryKey: ["users"] });
queryClient.invalidateQueries({ queryKey: ["user"] });
} catch (err: any) {
@@ -86,7 +90,7 @@ export function PermissionsModal({ userId, onClose }: Props) {
const isAdmin = data?.roles.indexOf("admin") !== -1;
return (
<Modal show onHide={onClose} animation={false}>
<Modal show={visible} onHide={remove}>
{!isLoading && error && (
<Alert variant="danger" className="m-3">
{error?.message || "Unknown error"}
@@ -216,7 +220,7 @@ export function PermissionsModal({ userId, onClose }: Props) {
)}
</Modal.Body>
<Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>
<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" />
</Button>
<Button
@@ -236,4 +240,6 @@ export function PermissionsModal({ userId, onClose }: Props) {
)}
</Modal>
);
}
});
export { showPermissionsModal };

View File

@@ -1,5 +1,6 @@
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 { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
@@ -18,11 +19,14 @@ import { intl, T } from "src/locale";
import { validateNumber, validateString } from "src/modules/Validations";
import { showSuccess } from "src/notifications";
interface Props {
const showProxyHostModal = (id: number | "new") => {
EasyModal.show(ProxyHostModal, { id });
};
interface Props extends InnerModalProps {
id: number | "new";
onClose: () => void;
}
export function ProxyHostModal({ id, onClose }: Props) {
const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useProxyHost(id);
const { mutate: setProxyHost } = useSetProxyHost();
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
@@ -42,7 +46,7 @@ export function ProxyHostModal({ id, onClose }: Props) {
onError: (err: any) => setErrorMsg(<T id={err.message} />),
onSuccess: () => {
showSuccess(intl.formatMessage({ id: "notification.proxy-host-saved" }));
onClose();
remove();
},
onSettled: () => {
setIsSubmitting(false);
@@ -52,7 +56,7 @@ export function ProxyHostModal({ id, onClose }: Props) {
};
return (
<Modal show onHide={onClose} animation={false}>
<Modal show={visible} onHide={remove}>
{!isLoading && error && (
<Alert variant="danger" className="m-3">
{error?.message || "Unknown error"}
@@ -341,7 +345,7 @@ export function ProxyHostModal({ id, onClose }: Props) {
</div>
</Modal.Body>
<Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>
<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" />
</Button>
<Button
@@ -361,4 +365,6 @@ export function ProxyHostModal({ id, onClose }: Props) {
)}
</Modal>
);
}
});
export { showProxyHostModal };

View File

@@ -1,5 +1,6 @@
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 { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
@@ -17,11 +18,14 @@ import { intl, T } from "src/locale";
import { validateString } from "src/modules/Validations";
import { showSuccess } from "src/notifications";
interface Props {
const showRedirectionHostModal = (id: number | "new") => {
EasyModal.show(RedirectionHostModal, { id });
};
interface Props extends InnerModalProps {
id: number | "new";
onClose: () => void;
}
export function RedirectionHostModal({ id, onClose }: Props) {
const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useRedirectionHost(id);
const { mutate: setRedirectionHost } = useSetRedirectionHost();
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
@@ -41,7 +45,7 @@ export function RedirectionHostModal({ id, onClose }: Props) {
onError: (err: any) => setErrorMsg(<T id={err.message} />),
onSuccess: () => {
showSuccess(intl.formatMessage({ id: "notification.redirection-host-saved" }));
onClose();
remove();
},
onSettled: () => {
setIsSubmitting(false);
@@ -51,7 +55,7 @@ export function RedirectionHostModal({ id, onClose }: Props) {
};
return (
<Modal show onHide={onClose} animation={false}>
<Modal show={visible} onHide={remove}>
{!isLoading && error && (
<Alert variant="danger" className="m-3">
{error?.message || "Unknown error"}
@@ -275,7 +279,7 @@ export function RedirectionHostModal({ id, onClose }: Props) {
</div>
</Modal.Body>
<Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>
<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" />
</Button>
<Button
@@ -295,4 +299,6 @@ export function RedirectionHostModal({ id, onClose }: Props) {
)}
</Modal>
);
}
});
export { showRedirectionHostModal };

View File

@@ -1,3 +1,4 @@
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { generate } from "generate-password-browser";
import { type ReactNode, useState } from "react";
@@ -8,21 +9,24 @@ import { Button } from "src/components";
import { intl, T } from "src/locale";
import { validateString } from "src/modules/Validations";
interface Props {
userId: number;
onClose: () => void;
const showSetPasswordModal = (id: number) => {
EasyModal.show(SetPasswordModal, { id });
};
interface Props extends InnerModalProps {
id: number;
}
export function SetPasswordModal({ userId, onClose }: Props) {
const SetPasswordModal = EasyModal.create(({ id, visible, remove }: Props) => {
const [error, setError] = useState<ReactNode | null>(null);
const [showPassword, setShowPassword] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const _onSubmit = async (values: any, { setSubmitting }: any) => {
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setError(null);
try {
await updateAuth(userId, values.new);
onClose();
await updateAuth(id, values.new);
remove();
} catch (err: any) {
setError(<T id={err.message} />);
}
@@ -31,14 +35,14 @@ export function SetPasswordModal({ userId, onClose }: Props) {
};
return (
<Modal show onHide={onClose} animation={false}>
<Modal show={visible} onHide={remove}>
<Formik
initialValues={
{
new: "",
} as any
}
onSubmit={_onSubmit}
onSubmit={onSubmit}
>
{() => (
<Form>
@@ -110,7 +114,7 @@ export function SetPasswordModal({ userId, onClose }: Props) {
</div>
</Modal.Body>
<Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>
<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" />
</Button>
<Button
@@ -129,4 +133,6 @@ export function SetPasswordModal({ userId, onClose }: Props) {
</Formik>
</Modal>
);
}
});
export { showSetPasswordModal };

View File

@@ -1,3 +1,4 @@
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
@@ -8,11 +9,14 @@ import { intl, T } from "src/locale";
import { validateNumber, validateString } from "src/modules/Validations";
import { showSuccess } from "src/notifications";
interface Props {
const showStreamModal = (id: number | "new") => {
EasyModal.show(StreamModal, { id });
};
interface Props extends InnerModalProps {
id: number | "new";
onClose: () => void;
}
export function StreamModal({ id, onClose }: Props) {
const StreamModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useStream(id);
const { mutate: setStream } = useSetStream();
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
@@ -32,7 +36,7 @@ export function StreamModal({ id, onClose }: Props) {
onError: (err: any) => setErrorMsg(<T id={err.message} />),
onSuccess: () => {
showSuccess(intl.formatMessage({ id: "notification.stream-saved" }));
onClose();
remove();
},
onSettled: () => {
setIsSubmitting(false);
@@ -42,7 +46,7 @@ export function StreamModal({ id, onClose }: Props) {
};
return (
<Modal show onHide={onClose} animation={false}>
<Modal show={visible} onHide={remove}>
{!isLoading && error && (
<Alert variant="danger" className="m-3">
{error?.message || "Unknown error"}
@@ -296,7 +300,7 @@ export function StreamModal({ id, onClose }: Props) {
</div>
</Modal.Body>
<Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>
<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" />
</Button>
<Button
@@ -316,4 +320,6 @@ export function StreamModal({ id, onClose }: Props) {
)}
</Modal>
);
}
});
export { showStreamModal };

View File

@@ -1,3 +1,4 @@
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { useState } from "react";
import { Alert } from "react-bootstrap";
@@ -8,12 +9,15 @@ import { intl, T } from "src/locale";
import { validateEmail, validateString } from "src/modules/Validations";
import { showSuccess } from "src/notifications";
interface Props {
userId: number | "me" | "new";
onClose: () => void;
const showUserModal = (id: number | "me" | "new") => {
EasyModal.show(UserModal, { id });
};
interface Props extends InnerModalProps {
id: number | "me" | "new";
}
export function UserModal({ userId, onClose }: Props) {
const { data, isLoading, error } = useUser(userId);
const UserModal = EasyModal.create(({ id, visible, remove }: Props) => {
const { data, isLoading, error } = useUser(id);
const { data: currentUser, isLoading: currentIsLoading } = useUser("me");
const { mutate: setUser } = useSetUser();
const [errorMsg, setErrorMsg] = useState<string | null>(null);
@@ -25,7 +29,7 @@ export function UserModal({ userId, onClose }: Props) {
setErrorMsg(null);
const { ...payload } = {
id: userId === "new" ? undefined : userId,
id: id === "new" ? undefined : id,
roles: [],
...values,
};
@@ -45,7 +49,7 @@ export function UserModal({ userId, onClose }: Props) {
onError: (err: any) => setErrorMsg(err.message),
onSuccess: () => {
showSuccess(intl.formatMessage({ id: "notification.user-saved" }));
onClose();
remove();
},
onSettled: () => {
setIsSubmitting(false);
@@ -55,7 +59,7 @@ export function UserModal({ userId, onClose }: Props) {
};
return (
<Modal show onHide={onClose} animation={false}>
<Modal show={visible} onHide={remove}>
{!isLoading && error && (
<Alert variant="danger" className="m-3">
{error?.message || "Unknown error"}
@@ -218,7 +222,7 @@ export function UserModal({ userId, onClose }: Props) {
) : null}
</Modal.Body>
<Modal.Footer>
<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}>
<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
<T id="cancel" />
</Button>
<Button
@@ -238,4 +242,6 @@ export function UserModal({ userId, onClose }: Props) {
)}
</Modal>
);
}
});
export { showUserModal };