Skip to content

Commit c28cba9

Browse files
committed
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
1 parent d41b86f commit c28cba9

File tree

14 files changed

+192
-70
lines changed

14 files changed

+192
-70
lines changed

resources/scripts/components/dashboard/AccountApiContainer.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,19 @@ export default () => {
6161
</ContentBox>
6262
<ContentBox title={'API Keys'} css={tw`ml-10 flex-1`}>
6363
<SpinnerOverlay visible={loading}/>
64-
{deleteIdentifier &&
6564
<ConfirmationModal
66-
visible
65+
visible={!!deleteIdentifier}
6766
title={'Confirm key deletion'}
6867
buttonText={'Yes, delete key'}
6968
onConfirmed={() => {
7069
doDeletion(deleteIdentifier);
7170
setDeleteIdentifier('');
7271
}}
73-
onDismissed={() => setDeleteIdentifier('')}
72+
onModalDismissed={() => setDeleteIdentifier('')}
7473
>
7574
Are you sure you wish to delete this API key? All requests using it will immediately be
7675
invalidated and will fail.
7776
</ConfirmationModal>
78-
}
7977
{
8078
keys.length === 0 ?
8179
<p css={tw`text-center text-sm`}>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React, { useContext } from 'react';
2+
import tw from 'twin.macro';
3+
import Button from '@/components/elements/Button';
4+
import asModal from '@/hoc/asModal';
5+
import ModalContext from '@/context/ModalContext';
6+
7+
interface Props {
8+
apiKey: string;
9+
}
10+
11+
const ApiKeyModal = ({ apiKey }: Props) => {
12+
const { dismiss } = useContext(ModalContext);
13+
14+
return (
15+
<>
16+
<h3 css={tw`mb-6`}>Your API Key</h3>
17+
<p css={tw`text-sm mb-6`}>
18+
The API key you have requested is shown below. Please store this in a safe location, it will not be
19+
shown again.
20+
</p>
21+
<pre css={tw`text-sm bg-neutral-900 rounded py-2 px-4 font-mono`}>
22+
<code css={tw`font-mono`}>{apiKey}</code>
23+
</pre>
24+
<div css={tw`flex justify-end mt-6`}>
25+
<Button type={'button'} onClick={() => dismiss()}>
26+
Close
27+
</Button>
28+
</div>
29+
</>
30+
);
31+
};
32+
33+
ApiKeyModal.displayName = 'ApiKeyModal';
34+
35+
export default asModal({
36+
closeOnEscape: false,
37+
closeOnBackground: false,
38+
})(ApiKeyModal);

resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import React, { useState } from 'react';
22
import { Field, Form, Formik, FormikHelpers } from 'formik';
33
import { object, string } from 'yup';
44
import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper';
5-
import Modal from '@/components/elements/Modal';
65
import createApiKey from '@/api/account/createApiKey';
76
import { Actions, useStoreActions } from 'easy-peasy';
87
import { ApplicationStore } from '@/state';
@@ -13,6 +12,7 @@ import tw from 'twin.macro';
1312
import Button from '@/components/elements/Button';
1413
import Input, { Textarea } from '@/components/elements/Input';
1514
import styled from 'styled-components/macro';
15+
import ApiKeyModal from '@/components/dashboard/ApiKeyModal';
1616

1717
interface Values {
1818
description: string;
@@ -44,29 +44,11 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
4444

4545
return (
4646
<>
47-
<Modal
47+
<ApiKeyModal
4848
visible={apiKey.length > 0}
49-
onDismissed={() => setApiKey('')}
50-
closeOnEscape={false}
51-
closeOnBackground={false}
52-
>
53-
<h3 css={tw`mb-6`}>Your API Key</h3>
54-
<p css={tw`text-sm mb-6`}>
55-
The API key you have requested is shown below. Please store this in a safe location, it will not be
56-
shown again.
57-
</p>
58-
<pre css={tw`text-sm bg-neutral-900 rounded py-2 px-4 font-mono`}>
59-
<code css={tw`font-mono`}>{apiKey}</code>
60-
</pre>
61-
<div css={tw`flex justify-end mt-6`}>
62-
<Button
63-
type={'button'}
64-
onClick={() => setApiKey('')}
65-
>
66-
Close
67-
</Button>
68-
</div>
69-
</Modal>
49+
onModalDismissed={() => setApiKey('')}
50+
apiKey={apiKey}
51+
/>
7052
<Formik
7153
onSubmit={submit}
7254
initialValues={{ description: '', allowedIps: '' }}
Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,40 @@
1-
import React from 'react';
2-
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
1+
import React, { useContext, useEffect } from 'react';
32
import tw from 'twin.macro';
43
import Button from '@/components/elements/Button';
4+
import asModal from '@/hoc/asModal';
5+
import ModalContext from '@/context/ModalContext';
56

67
type Props = {
78
title: string;
89
buttonText: string;
910
children: string;
1011
onConfirmed: () => void;
1112
showSpinnerOverlay?: boolean;
12-
} & RequiredModalProps;
13+
};
1314

14-
const ConfirmationModal = ({ title, appear, children, visible, buttonText, onConfirmed, showSpinnerOverlay, onDismissed }: Props) => (
15-
<Modal
16-
appear={appear || true}
17-
visible={visible}
18-
showSpinnerOverlay={showSpinnerOverlay}
19-
onDismissed={() => onDismissed()}
20-
>
21-
<h2 css={tw`text-2xl mb-6`}>{title}</h2>
22-
<p css={tw`text-sm`}>{children}</p>
23-
<div css={tw`flex items-center justify-end mt-8`}>
24-
<Button isSecondary onClick={() => onDismissed()}>
25-
Cancel
26-
</Button>
27-
<Button color={'red'} css={tw`ml-4`} onClick={() => onConfirmed()}>
28-
{buttonText}
29-
</Button>
30-
</div>
31-
</Modal>
32-
);
15+
const ConfirmationModal = ({ title, children, buttonText, onConfirmed, showSpinnerOverlay }: Props) => {
16+
const { dismiss, toggleSpinner } = useContext(ModalContext);
3317

34-
export default ConfirmationModal;
18+
useEffect(() => {
19+
toggleSpinner(showSpinnerOverlay);
20+
}, [ showSpinnerOverlay ]);
21+
22+
return (
23+
<>
24+
<h2 css={tw`text-2xl mb-6`}>{title}</h2>
25+
<p css={tw`text-sm`}>{children}</p>
26+
<div css={tw`flex items-center justify-end mt-8`}>
27+
<Button isSecondary onClick={() => dismiss()}>
28+
Cancel
29+
</Button>
30+
<Button color={'red'} css={tw`ml-4`} onClick={() => onConfirmed()}>
31+
{buttonText}
32+
</Button>
33+
</div>
34+
</>
35+
);
36+
};
37+
38+
ConfirmationModal.displayName = 'ConfirmationModal';
39+
40+
export default asModal()(ConfirmationModal);

resources/scripts/components/elements/Fade.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ interface Props extends Omit<CSSTransitionProps, 'timeout' | 'classNames'> {
88
}
99

1010
const Container = styled.div<{ timeout: number }>`
11-
.fade-enter, .fade-exit {
11+
.fade-enter, .fade-exit, .fade-appear {
1212
will-change: opacity;
1313
}
1414
15-
.fade-enter {
15+
.fade-enter, .fade-appear {
1616
${tw`opacity-0`};
1717
18-
&.fade-enter-active {
18+
&.fade-enter-active, &.fade-appear-active {
1919
${tw`opacity-100 transition-opacity ease-in`};
2020
transition-duration: ${props => props.timeout}ms;
2121
}

resources/scripts/components/elements/Modal.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface RequiredModalProps {
1313
top?: boolean;
1414
}
1515

16-
interface Props extends RequiredModalProps {
16+
export interface ModalProps extends RequiredModalProps {
1717
dismissable?: boolean;
1818
closeOnEscape?: boolean;
1919
closeOnBackground?: boolean;
@@ -40,7 +40,7 @@ const ModalContainer = styled.div<{ alignTop?: boolean }>`
4040
}
4141
`;
4242

43-
const Modal: React.FC<Props> = ({ visible, appear, dismissable, showSpinnerOverlay, top = true, closeOnBackground = true, closeOnEscape = true, onDismissed, children }) => {
43+
const Modal: React.FC<ModalProps> = ({ visible, appear, dismissable, showSpinnerOverlay, top = true, closeOnBackground = true, closeOnEscape = true, onDismissed, children }) => {
4444
const [ render, setRender ] = useState(visible);
4545

4646
const isDismissable = useMemo(() => {
@@ -62,7 +62,13 @@ const Modal: React.FC<Props> = ({ visible, appear, dismissable, showSpinnerOverl
6262
}, [ render ]);
6363

6464
return (
65-
<Fade timeout={150} appear={appear} in={render} unmountOnExit onExited={onDismissed}>
65+
<Fade
66+
in={render}
67+
timeout={150}
68+
appear={appear || true}
69+
unmountOnExit
70+
onExited={() => onDismissed()}
71+
>
6672
<ModalMask
6773
onClick={e => {
6874
if (isDismissable && closeOnBackground) {

resources/scripts/components/server/backups/BackupContextMenu.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,16 @@ export default ({ backup }: Props) => {
6565
checksum={backup.sha256Hash}
6666
/>
6767
}
68-
{deleteVisible &&
6968
<ConfirmationModal
69+
visible={deleteVisible}
7070
title={'Delete this backup?'}
7171
buttonText={'Yes, delete backup'}
7272
onConfirmed={() => doDeletion()}
73-
visible={deleteVisible}
74-
onDismissed={() => setDeleteVisible(false)}
73+
onModalDismissed={() => setDeleteVisible(false)}
7574
>
7675
Are you sure you wish to delete this backup? This is a permanent operation and the backup cannot
7776
be recovered once deleted.
7877
</ConfirmationModal>
79-
}
8078
<SpinnerOverlay visible={loading} fixed/>
8179
<DropdownMenu
8280
renderToggle={onClick => (

resources/scripts/components/server/files/MassActionsBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ const MassActionsBar = () => {
7272
title={'Delete these files?'}
7373
buttonText={'Yes, Delete Files'}
7474
onConfirmed={onClickConfirmDeletion}
75-
onDismissed={() => setShowConfirm(false)}
75+
onModalDismissed={() => setShowConfirm(false)}
7676
>
7777
Deleting files is a permanent operation, you cannot undo this action.
7878
</ConfirmationModal>

resources/scripts/components/server/schedules/DeleteScheduleButton.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ export default ({ scheduleId, onDeleted }: Props) => {
3939
return (
4040
<>
4141
<ConfirmationModal
42-
showSpinnerOverlay={isLoading}
42+
visible={visible}
4343
title={'Delete schedule?'}
4444
buttonText={'Yes, delete schedule'}
4545
onConfirmed={onDelete}
46-
visible={visible}
47-
onDismissed={() => setVisible(false)}
46+
showSpinnerOverlay={isLoading}
47+
onModalDismissed={() => setVisible(false)}
4848
>
4949
Are you sure you want to delete this schedule? All tasks will be removed and any running processes
5050
will be terminated.

resources/scripts/components/server/schedules/ScheduleTaskRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export default ({ schedule, task }: Props) => {
6969
buttonText={'Delete Task'}
7070
onConfirmed={onConfirmDeletion}
7171
visible={visible}
72-
onDismissed={() => setVisible(false)}
72+
onModalDismissed={() => setVisible(false)}
7373
>
7474
Are you sure you want to delete this task? This action cannot be undone.
7575
</ConfirmationModal>

0 commit comments

Comments
 (0)