Skip to content

Commit

Permalink
Merge pull request #3876 from webkom/refactor-modal-dropdown
Browse files Browse the repository at this point in the history
  • Loading branch information
eikhr committed May 12, 2023
2 parents 922aa1c + 16a59f6 commit d59e7dd
Show file tree
Hide file tree
Showing 35 changed files with 535 additions and 533 deletions.
204 changes: 71 additions & 133 deletions app/components/Modal/ConfirmModal.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { get } from 'lodash';
import { Component, Children, cloneElement } from 'react';
import { useState } from 'react';
import Button from 'app/components/Button';
import Icon from 'app/components/Icon';
import Flex from 'app/components/Layout/Flex';
import Modal from 'app/components/Modal';
import styles from './ConfirmModal.css';
import type { ComponentType, ReactElement, ReactNode } from 'react';
import type { ReactNode } from 'react';

type ConfirmModalProps = {
type ConfirmModalContentProps = {
onConfirm?: () => Promise<void>;
onCancel?: () => Promise<void>;
message: ReactNode;
Expand All @@ -19,21 +18,21 @@ type ConfirmModalProps = {
danger?: boolean;
};

export const ConfirmModal = ({
const ConfirmModalContent = ({
message,
onConfirm,
onCancel,
title,
disabled = false,
errorMessage = '',
errorMessage,
cancelText = 'Avbryt',
confirmText = 'Ja',
danger = true,
}: ConfirmModalProps) => (
}: ConfirmModalContentProps) => (
<Flex column gap={15}>
<Flex wrap alignItems="center" gap={10}>
<Icon name="warning" className={styles.warningIcon} />
<h2 className={danger && styles.dangerTitle}>{title}</h2>
<h2 className={danger ? styles.dangerTitle : undefined}>{title}</h2>
</Flex>
<span>{message}</span>
<div>
Expand All @@ -48,12 +47,10 @@ export const ConfirmModal = ({
</Flex>
);

type State = {
modalVisible: boolean;
working: boolean;
errorMessage: string;
};
type WithModalProps = {
type ConfirmModalProps = {
onConfirm?: () => Promise<void>;
onCancel?: () => Promise<void>;

/* Close the modal after confirm promise is resolved
* This should only be used if the component isn't automatically
* unmounted when the given promise resolves */
Expand All @@ -63,130 +60,71 @@ type WithModalProps = {
* This should only be true if the component isn't automatically
* unmounted when the given promise resolves */
closeOnCancel?: boolean;
children: ReactNode;
};
export default function withModal<Props>(
WrappedComponent: ComponentType<Props>
) {
const displayName =
WrappedComponent.displayName || WrappedComponent.name || 'Unknown';
return class extends Component<WithModalProps & ConfirmModalProps, State> {
static displayName = `WithModal(${displayName})`;
state = {
modalVisible: false,
working: false,
errorMessage: '',
};
toggleModal = () => {
this.setState((state) => ({
modalVisible: !state.modalVisible,
}));
this.stopWorking();
this.resetError();
};
hideModal = () => {
this.setState({
modalVisible: false,
});
};
startWorking = () => {
this.setState({
working: true,
});
};
stopWorking = () => {
this.setState({
working: false,
});
};
setErrorMessage = (errorMessage: string) => {
this.setState({
errorMessage,
});
};
resetError = () => {
this.setState({
errorMessage: '',
});
};

render() {
const {
onConfirm = () => Promise.resolve(),
onCancel = () => Promise.resolve(),
message,
title,
closeOnCancel = true,
closeOnConfirm = false,
...props
} = this.props;
children: (props: { openConfirmModal: () => void }) => ReactNode;

const wrapAction = (
action: () => Promise<any>,
closeWhenDone: boolean
) => {
return () => {
const onResolve = closeWhenDone
? (result) => {
this.stopWorking();
this.hideModal();
this.resetError();
return result;
}
: (result) => result;
// The following props are only passed on to ConfirmModalContent
message: ReactNode;
title: string;
cancelText?: string;
confirmText?: string;
danger?: boolean;
};

const onError = (error) => {
this.stopWorking();
const errorMessage =
get(error, ['meta', 'errorMessage']) || 'Det skjedde en feil...';
this.setErrorMessage(errorMessage);
throw error;
};
export const ConfirmModal = ({
onConfirm = async () => {},
onCancel = async () => {},
closeOnCancel = true,
closeOnConfirm = false,
children,
...contentProps
}: ConfirmModalProps) => {
const [modalVisible, setModalVisible] = useState(false);
const [working, setWorking] = useState(false);
const [errorMessage, setErrorMessage] = useState<string>();

this.resetError();
this.startWorking();
return action().then(onResolve, onError);
};
};
const wrapAction = <T,>(action: () => Promise<T>, closeWhenDone: boolean) => {
return async () => {
setErrorMessage(undefined);
setWorking(true);

const modalOnConfirm = wrapAction(onConfirm, closeOnConfirm);
const modalOnCancel = wrapAction(onCancel, closeOnCancel);
const { working, errorMessage } = this.state;
return (
<>
<WrappedComponent
{...(props as Record<string, any>)}
onClick={this.toggleModal}
/>
<Modal
closeOnBackdropClick={!working}
show={this.state.modalVisible}
onHide={this.toggleModal}
>
<ConfirmModal
onCancel={modalOnCancel}
onConfirm={modalOnConfirm}
message={message}
title={title}
disabled={working}
errorMessage={errorMessage}
/>
</Modal>
</>
);
}
try {
const result = await action();
if (closeWhenDone) {
setWorking(false);
setModalVisible(false);
setErrorMessage(undefined);
}
return result;
} catch (error) {
setWorking(false);
setErrorMessage(
(error as any)?.meta?.errorMessage || 'Det skjedde en feil...'
);
throw error;
}
};
};
}

const ChildrenWithProps = ({
children,
...restProps
}: {
children: ReactElement | ReactElement[];
}): ReactElement => (
<>
{Children.map(children, (child) => cloneElement(child, { ...restProps }))}
</>
);
const modalOnConfirm = wrapAction(onConfirm, closeOnConfirm);
const modalOnCancel = wrapAction(onCancel, closeOnCancel);

export const ConfirmModalWithParent = withModal(ChildrenWithProps);
return (
<>
{children({ openConfirmModal: () => setModalVisible(true) })}
<Modal
closeOnBackdropClick={!working}
show={modalVisible}
onHide={() => setModalVisible(false)}
>
<ConfirmModalContent
onConfirm={modalOnConfirm}
onCancel={modalOnCancel}
disabled={working}
errorMessage={errorMessage}
{...contentProps}
/>
</Modal>
</>
);
};
7 changes: 3 additions & 4 deletions app/components/Modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { ReactNode } from 'react';
type Props = {
show: boolean;
children: ReactNode;
onHide: () => any;
onHide: () => void;
backdrop?: boolean;
closeOnBackdropClick?: boolean;
keyboard?: boolean;
Expand Down Expand Up @@ -37,11 +37,11 @@ const Modal = ({
backdrop={backdrop}
onHide={onHide}
keyboard={keyboard}
renderBackdrop={(props: { onClick: (...args: Array<any>) => any }) => (
renderBackdrop={(props) => (
<div
{...props}
className={backdropClassName || styles.backdrop}
onClick={closeOnBackdropClick ? props.onClick : null}
onClick={closeOnBackdropClick ? props.onClick : undefined}
/>
)}
>
Expand All @@ -55,5 +55,4 @@ const Modal = ({
</ReactModal>
);

export { default as ConfirmModal } from './ConfirmModal';
export default Modal;

0 comments on commit d59e7dd

Please sign in to comment.