mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 15:59:41 +02:00
refactor(ui/modals): replace bootbox with react solution [EE-4541] (#8010)
This commit is contained in:
parent
392c7f74b8
commit
e66dea44e3
111 changed files with 1330 additions and 1562 deletions
56
app/react/components/modals/Dialog.tsx
Normal file
56
app/react/components/modals/Dialog.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { ButtonOptions, ModalType } from './types';
|
||||
import { openModal } from './open-modal';
|
||||
import { Modal, OnSubmit } from './Modal';
|
||||
|
||||
export interface DialogOptions<T> {
|
||||
title?: ReactNode;
|
||||
message: ReactNode;
|
||||
modalType?: ModalType;
|
||||
buttons: Array<ButtonOptions<T>>;
|
||||
}
|
||||
|
||||
interface Props<T> extends DialogOptions<T> {
|
||||
onSubmit: OnSubmit<T>;
|
||||
}
|
||||
|
||||
export function Dialog<T>({
|
||||
buttons,
|
||||
message,
|
||||
title,
|
||||
onSubmit,
|
||||
modalType,
|
||||
}: Props<T>) {
|
||||
const ariaLabel = requireString(title) || requireString(message) || 'Dialog';
|
||||
|
||||
return (
|
||||
<Modal onDismiss={() => onSubmit()} aria-label={ariaLabel}>
|
||||
{title && <Modal.Header title={title} modalType={modalType} />}
|
||||
<Modal.Body>{message}</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{buttons.map((button, index) => (
|
||||
<Button
|
||||
onClick={() => onSubmit(button.value)}
|
||||
className={button.className}
|
||||
color={button.color}
|
||||
key={index}
|
||||
size="medium"
|
||||
>
|
||||
{button.label}
|
||||
</Button>
|
||||
))}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function requireString(value: ReactNode) {
|
||||
return typeof value === 'string' ? value : undefined;
|
||||
}
|
||||
|
||||
export async function openDialog<T>(options: DialogOptions<T>) {
|
||||
return openModal<DialogOptions<T>, T>(Dialog, options);
|
||||
}
|
74
app/react/components/modals/SwitchPrompt.tsx
Normal file
74
app/react/components/modals/SwitchPrompt.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { ReactNode, useState } from 'react';
|
||||
|
||||
import { SwitchField } from '@@/form-components/SwitchField';
|
||||
|
||||
import { ModalType, type ButtonOptions } from './types';
|
||||
import { openModal } from './open-modal';
|
||||
import { OnSubmit } from './Modal/types';
|
||||
import { Dialog } from './Dialog';
|
||||
import { buildCancelButton, buildConfirmButton } from './utils';
|
||||
|
||||
function SwitchPrompt({
|
||||
onSubmit,
|
||||
title,
|
||||
confirmButton = buildConfirmButton('OK'),
|
||||
switchLabel,
|
||||
modalType,
|
||||
message,
|
||||
defaultValue = false,
|
||||
}: {
|
||||
onSubmit: OnSubmit<{ value: boolean }>;
|
||||
title: string;
|
||||
switchLabel: string;
|
||||
confirmButton?: ButtonOptions<true>;
|
||||
modalType?: ModalType;
|
||||
message?: ReactNode;
|
||||
defaultValue?: boolean;
|
||||
}) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
modalType={modalType}
|
||||
title={title}
|
||||
message={
|
||||
<>
|
||||
{message && <div className="mb-3">{message}</div>}
|
||||
<SwitchField
|
||||
name="value"
|
||||
label={switchLabel}
|
||||
checked={value}
|
||||
onChange={setValue}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
onSubmit={(confirm) => onSubmit(confirm ? { value } : undefined)}
|
||||
buttons={[buildCancelButton(), confirmButton]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export async function openSwitchPrompt(
|
||||
title: string,
|
||||
switchLabel: string,
|
||||
{
|
||||
confirmButton,
|
||||
modalType,
|
||||
message,
|
||||
defaultValue,
|
||||
}: {
|
||||
confirmButton?: ButtonOptions<true>;
|
||||
modalType?: ModalType;
|
||||
message?: ReactNode;
|
||||
defaultValue?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
return openModal(SwitchPrompt, {
|
||||
confirmButton,
|
||||
title,
|
||||
switchLabel,
|
||||
modalType,
|
||||
message,
|
||||
defaultValue,
|
||||
});
|
||||
}
|
80
app/react/components/modals/confirm.ts
Normal file
80
app/react/components/modals/confirm.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { openDialog, DialogOptions } from './Dialog';
|
||||
import { OnSubmit, ModalType } from './Modal';
|
||||
import { ButtonOptions } from './types';
|
||||
import { buildCancelButton, buildConfirmButton } from './utils';
|
||||
|
||||
export type ConfirmCallback = OnSubmit<boolean>;
|
||||
|
||||
export interface ConfirmOptions
|
||||
extends Omit<DialogOptions<boolean>, 'title' | 'buttons'> {
|
||||
title: string;
|
||||
confirmButton?: ButtonOptions<true>;
|
||||
cancelButtonLabel?: string;
|
||||
}
|
||||
|
||||
export async function openConfirm({
|
||||
confirmButton = buildConfirmButton(),
|
||||
cancelButtonLabel,
|
||||
...options
|
||||
}: ConfirmOptions) {
|
||||
const result = await openDialog({
|
||||
...options,
|
||||
buttons: [buildCancelButton(cancelButtonLabel), confirmButton],
|
||||
});
|
||||
return !!result;
|
||||
}
|
||||
|
||||
export function confirm(options: ConfirmOptions) {
|
||||
return openConfirm(options);
|
||||
}
|
||||
|
||||
export function confirmDestructive(options: Omit<ConfirmOptions, 'modalType'>) {
|
||||
return openConfirm({
|
||||
...options,
|
||||
modalType: ModalType.Destructive,
|
||||
});
|
||||
}
|
||||
|
||||
export function confirmWebEditorDiscard() {
|
||||
return openConfirm({
|
||||
modalType: ModalType.Warn,
|
||||
title: 'Are you sure?',
|
||||
message:
|
||||
'You currently have unsaved changes in the editor. Are you sure you want to leave?',
|
||||
confirmButton: buildConfirmButton('Yes', 'danger'),
|
||||
});
|
||||
}
|
||||
|
||||
export function confirmDelete(message: string) {
|
||||
return confirmDestructive({
|
||||
title: 'Are you sure?',
|
||||
message,
|
||||
confirmButton: buildConfirmButton('Remove', 'danger'),
|
||||
});
|
||||
}
|
||||
|
||||
export async function confirmUpdate(
|
||||
message: string,
|
||||
callback: ConfirmCallback
|
||||
) {
|
||||
const result = await openConfirm({
|
||||
title: 'Are you sure?',
|
||||
modalType: ModalType.Warn,
|
||||
message,
|
||||
confirmButton: buildConfirmButton('Update'),
|
||||
});
|
||||
|
||||
callback(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function confirmChangePassword() {
|
||||
return openConfirm({
|
||||
modalType: ModalType.Warn,
|
||||
title: 'Are you sure?',
|
||||
message:
|
||||
'You will be logged out after the password change. Do you want to change your password?',
|
||||
confirmButton: buildConfirmButton('Change'),
|
||||
});
|
||||
}
|
4
app/react/components/modals/index.ts
Normal file
4
app/react/components/modals/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export { Modal } from './Modal';
|
||||
export { openModal } from './open-modal';
|
||||
export { ModalType } from './types';
|
||||
export { type OnSubmit } from './Modal/types';
|
29
app/react/components/modals/open-modal.tsx
Normal file
29
app/react/components/modals/open-modal.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { ComponentType } from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
|
||||
import '@reach/dialog/styles.css';
|
||||
import { OnSubmit } from './Modal/types';
|
||||
|
||||
let counter = 0;
|
||||
export async function openModal<TProps, TResult>(
|
||||
Modal: ComponentType<{ onSubmit: OnSubmit<TResult> } & TProps>,
|
||||
props: TProps = {} as TProps
|
||||
) {
|
||||
const modal = document.createElement('div');
|
||||
counter += 1;
|
||||
modal.id = `dialog-${counter}`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const result = await new Promise<TResult | undefined>((resolve) => {
|
||||
render(
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<Modal {...props} onSubmit={(result) => resolve(result)} />,
|
||||
modal
|
||||
);
|
||||
});
|
||||
|
||||
unmountComponentAtNode(modal);
|
||||
document.body.removeChild(modal);
|
||||
|
||||
return result;
|
||||
}
|
20
app/react/components/modals/types.ts
Normal file
20
app/react/components/modals/types.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { ComponentProps } from 'react';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
export interface ButtonOptions<TValue = undefined> {
|
||||
label: string;
|
||||
className?: string;
|
||||
color?: ComponentProps<typeof Button>['color'];
|
||||
value?: TValue;
|
||||
}
|
||||
|
||||
export interface ButtonsOptions<T> {
|
||||
confirm: ButtonOptions<T>;
|
||||
cancel?: ButtonOptions<T>;
|
||||
}
|
||||
|
||||
export enum ModalType {
|
||||
Warn = 'warning',
|
||||
Destructive = 'error',
|
||||
}
|
20
app/react/components/modals/utils.ts
Normal file
20
app/react/components/modals/utils.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { ComponentProps } from 'react';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { ButtonOptions } from './types';
|
||||
|
||||
export function buildConfirmButton(
|
||||
label = 'Confirm',
|
||||
color: ComponentProps<typeof Button>['color'] = 'primary'
|
||||
): ButtonOptions<true> {
|
||||
return { label, color, value: true };
|
||||
}
|
||||
|
||||
export function buildCancelButton(label = 'Cancel'): ButtonOptions<false> {
|
||||
return {
|
||||
label,
|
||||
color: 'default',
|
||||
value: false,
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue