From c28cba92e2192b1be8c18f8923fcb95225c8b02c Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 17 Aug 2020 21:35:11 -0700 Subject: [PATCH] Make modals programatically controllable via a HOC This allows entire components to be unmounted when the modal is hidden without affecting the fade in/out of the modal itself. This also makes it easier to programatically dismiss a modal without having to copy the visibility all over the place, and makes working with props much simpler in those modal components --- .../dashboard/AccountApiContainer.tsx | 6 +- .../components/dashboard/ApiKeyModal.tsx | 38 +++++++++ .../dashboard/forms/CreateApiKeyForm.tsx | 28 ++----- .../components/elements/ConfirmationModal.tsx | 52 ++++++------ .../scripts/components/elements/Fade.tsx | 6 +- .../scripts/components/elements/Modal.tsx | 12 ++- .../server/backups/BackupContextMenu.tsx | 6 +- .../server/files/MassActionsBar.tsx | 2 +- .../server/schedules/DeleteScheduleButton.tsx | 6 +- .../server/schedules/ScheduleTaskRow.tsx | 2 +- .../server/settings/ReinstallServerBox.tsx | 4 +- .../server/users/RemoveSubuserButton.tsx | 4 +- resources/scripts/context/ModalContext.ts | 15 ++++ resources/scripts/hoc/asModal.tsx | 81 +++++++++++++++++++ 14 files changed, 192 insertions(+), 70 deletions(-) create mode 100644 resources/scripts/components/dashboard/ApiKeyModal.tsx create mode 100644 resources/scripts/context/ModalContext.ts create mode 100644 resources/scripts/hoc/asModal.tsx diff --git a/resources/scripts/components/dashboard/AccountApiContainer.tsx b/resources/scripts/components/dashboard/AccountApiContainer.tsx index c80a51a2..304fe563 100644 --- a/resources/scripts/components/dashboard/AccountApiContainer.tsx +++ b/resources/scripts/components/dashboard/AccountApiContainer.tsx @@ -61,21 +61,19 @@ export default () => { - {deleteIdentifier && { doDeletion(deleteIdentifier); setDeleteIdentifier(''); }} - onDismissed={() => setDeleteIdentifier('')} + onModalDismissed={() => setDeleteIdentifier('')} > Are you sure you wish to delete this API key? All requests using it will immediately be invalidated and will fail. - } { keys.length === 0 ?

diff --git a/resources/scripts/components/dashboard/ApiKeyModal.tsx b/resources/scripts/components/dashboard/ApiKeyModal.tsx new file mode 100644 index 00000000..db511edb --- /dev/null +++ b/resources/scripts/components/dashboard/ApiKeyModal.tsx @@ -0,0 +1,38 @@ +import React, { useContext } from 'react'; +import tw from 'twin.macro'; +import Button from '@/components/elements/Button'; +import asModal from '@/hoc/asModal'; +import ModalContext from '@/context/ModalContext'; + +interface Props { + apiKey: string; +} + +const ApiKeyModal = ({ apiKey }: Props) => { + const { dismiss } = useContext(ModalContext); + + return ( + <> +

Your API Key

+

+ The API key you have requested is shown below. Please store this in a safe location, it will not be + shown again. +

+
+                {apiKey}
+            
+
+ +
+ + ); +}; + +ApiKeyModal.displayName = 'ApiKeyModal'; + +export default asModal({ + closeOnEscape: false, + closeOnBackground: false, +})(ApiKeyModal); diff --git a/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx b/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx index 3e52e68a..9022ae6c 100644 --- a/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx +++ b/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import { Field, Form, Formik, FormikHelpers } from 'formik'; import { object, string } from 'yup'; import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper'; -import Modal from '@/components/elements/Modal'; import createApiKey from '@/api/account/createApiKey'; import { Actions, useStoreActions } from 'easy-peasy'; import { ApplicationStore } from '@/state'; @@ -13,6 +12,7 @@ import tw from 'twin.macro'; import Button from '@/components/elements/Button'; import Input, { Textarea } from '@/components/elements/Input'; import styled from 'styled-components/macro'; +import ApiKeyModal from '@/components/dashboard/ApiKeyModal'; interface Values { description: string; @@ -44,29 +44,11 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => { return ( <> - 0} - onDismissed={() => setApiKey('')} - closeOnEscape={false} - closeOnBackground={false} - > -

Your API Key

-

- The API key you have requested is shown below. Please store this in a safe location, it will not be - shown again. -

-
-                    {apiKey}
-                
-
- -
-
+ onModalDismissed={() => setApiKey('')} + apiKey={apiKey} + /> void; showSpinnerOverlay?: boolean; -} & RequiredModalProps; +}; -const ConfirmationModal = ({ title, appear, children, visible, buttonText, onConfirmed, showSpinnerOverlay, onDismissed }: Props) => ( - onDismissed()} - > -

{title}

-

{children}

-
- - -
-
-); +const ConfirmationModal = ({ title, children, buttonText, onConfirmed, showSpinnerOverlay }: Props) => { + const { dismiss, toggleSpinner } = useContext(ModalContext); -export default ConfirmationModal; + useEffect(() => { + toggleSpinner(showSpinnerOverlay); + }, [ showSpinnerOverlay ]); + + return ( + <> +

{title}

+

{children}

+
+ + +
+ + ); +}; + +ConfirmationModal.displayName = 'ConfirmationModal'; + +export default asModal()(ConfirmationModal); diff --git a/resources/scripts/components/elements/Fade.tsx b/resources/scripts/components/elements/Fade.tsx index 62850283..2b9c3efa 100644 --- a/resources/scripts/components/elements/Fade.tsx +++ b/resources/scripts/components/elements/Fade.tsx @@ -8,14 +8,14 @@ interface Props extends Omit { } const Container = styled.div<{ timeout: number }>` - .fade-enter, .fade-exit { + .fade-enter, .fade-exit, .fade-appear { will-change: opacity; } - .fade-enter { + .fade-enter, .fade-appear { ${tw`opacity-0`}; - &.fade-enter-active { + &.fade-enter-active, &.fade-appear-active { ${tw`opacity-100 transition-opacity ease-in`}; transition-duration: ${props => props.timeout}ms; } diff --git a/resources/scripts/components/elements/Modal.tsx b/resources/scripts/components/elements/Modal.tsx index f242fbac..de0be05e 100644 --- a/resources/scripts/components/elements/Modal.tsx +++ b/resources/scripts/components/elements/Modal.tsx @@ -13,7 +13,7 @@ export interface RequiredModalProps { top?: boolean; } -interface Props extends RequiredModalProps { +export interface ModalProps extends RequiredModalProps { dismissable?: boolean; closeOnEscape?: boolean; closeOnBackground?: boolean; @@ -40,7 +40,7 @@ const ModalContainer = styled.div<{ alignTop?: boolean }>` } `; -const Modal: React.FC = ({ visible, appear, dismissable, showSpinnerOverlay, top = true, closeOnBackground = true, closeOnEscape = true, onDismissed, children }) => { +const Modal: React.FC = ({ visible, appear, dismissable, showSpinnerOverlay, top = true, closeOnBackground = true, closeOnEscape = true, onDismissed, children }) => { const [ render, setRender ] = useState(visible); const isDismissable = useMemo(() => { @@ -62,7 +62,13 @@ const Modal: React.FC = ({ visible, appear, dismissable, showSpinnerOverl }, [ render ]); return ( - + onDismissed()} + > { if (isDismissable && closeOnBackground) { diff --git a/resources/scripts/components/server/backups/BackupContextMenu.tsx b/resources/scripts/components/server/backups/BackupContextMenu.tsx index 54a45b9d..d9c74150 100644 --- a/resources/scripts/components/server/backups/BackupContextMenu.tsx +++ b/resources/scripts/components/server/backups/BackupContextMenu.tsx @@ -65,18 +65,16 @@ export default ({ backup }: Props) => { checksum={backup.sha256Hash} /> } - {deleteVisible && doDeletion()} - visible={deleteVisible} - onDismissed={() => setDeleteVisible(false)} + onModalDismissed={() => setDeleteVisible(false)} > Are you sure you wish to delete this backup? This is a permanent operation and the backup cannot be recovered once deleted. - } ( diff --git a/resources/scripts/components/server/files/MassActionsBar.tsx b/resources/scripts/components/server/files/MassActionsBar.tsx index 6df43ecc..40067d03 100644 --- a/resources/scripts/components/server/files/MassActionsBar.tsx +++ b/resources/scripts/components/server/files/MassActionsBar.tsx @@ -72,7 +72,7 @@ const MassActionsBar = () => { title={'Delete these files?'} buttonText={'Yes, Delete Files'} onConfirmed={onClickConfirmDeletion} - onDismissed={() => setShowConfirm(false)} + onModalDismissed={() => setShowConfirm(false)} > Deleting files is a permanent operation, you cannot undo this action. diff --git a/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx b/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx index 26d86652..19806038 100644 --- a/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx +++ b/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx @@ -39,12 +39,12 @@ export default ({ scheduleId, onDeleted }: Props) => { return ( <> setVisible(false)} + showSpinnerOverlay={isLoading} + onModalDismissed={() => setVisible(false)} > Are you sure you want to delete this schedule? All tasks will be removed and any running processes will be terminated. diff --git a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx index fb1136f7..b14a24ea 100644 --- a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx +++ b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx @@ -69,7 +69,7 @@ export default ({ schedule, task }: Props) => { buttonText={'Delete Task'} onConfirmed={onConfirmDeletion} visible={visible} - onDismissed={() => setVisible(false)} + onModalDismissed={() => setVisible(false)} > Are you sure you want to delete this task? This action cannot be undone. diff --git a/resources/scripts/components/server/settings/ReinstallServerBox.tsx b/resources/scripts/components/server/settings/ReinstallServerBox.tsx index 1b7b44de..0c1ce8ec 100644 --- a/resources/scripts/components/server/settings/ReinstallServerBox.tsx +++ b/resources/scripts/components/server/settings/ReinstallServerBox.tsx @@ -46,10 +46,10 @@ export default () => { reinstall()} + onConfirmed={reinstall} showSpinnerOverlay={isSubmitting} visible={modalVisible} - onDismissed={() => setModalVisible(false)} + onModalDismissed={() => setModalVisible(false)} > Your server will be stopped and some files may be deleted or modified during this process, are you sure you wish to continue? diff --git a/resources/scripts/components/server/users/RemoveSubuserButton.tsx b/resources/scripts/components/server/users/RemoveSubuserButton.tsx index 976c6417..f6481dfc 100644 --- a/resources/scripts/components/server/users/RemoveSubuserButton.tsx +++ b/resources/scripts/components/server/users/RemoveSubuserButton.tsx @@ -35,19 +35,17 @@ export default ({ subuser }: { subuser: Subuser }) => { return ( <> - {showConfirmation && doDeletion()} - onDismissed={() => setShowConfirmation(false)} + onModalDismissed={() => setShowConfirmation(false)} > Are you sure you wish to remove this subuser? They will have all access to this server revoked immediately. - }