1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 08:19:40 +02:00

refactor(ui/modals): replace bootbox with react solution [EE-4541] (#8010)

This commit is contained in:
Chaim Lev-Ari 2023-02-14 13:49:41 +05:30 committed by GitHub
parent 392c7f74b8
commit e66dea44e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
111 changed files with 1330 additions and 1562 deletions

View file

@ -22,6 +22,7 @@ type Color =
| 'light'
| 'dangerlight'
| 'warninglight'
| 'warning'
| 'none';
type Size = 'xsmall' | 'small' | 'medium' | 'large';

View file

@ -67,9 +67,10 @@ export function SingleSelect<TValue = string>({
isClearable,
bindToBody,
}: SingleProps<TValue>) {
const selectedValue = value
? _.first(findSelectedOptions<TValue>(options, value))
: null;
const selectedValue =
value || (typeof value === 'number' && value === 0)
? _.first(findSelectedOptions<TValue>(options, value))
: null;
return (
<ReactSelect<Option<TValue>>
@ -94,7 +95,8 @@ function findSelectedOptions<TValue>(
value: TValue | readonly TValue[]
) {
const valueArr = Array.isArray(value) ? value : [value];
return _.compact(
const values = _.compact(
options.flatMap((option) => {
if (isGroup(option)) {
return option.options.find((option) => valueArr.includes(option.value));
@ -107,6 +109,8 @@ function findSelectedOptions<TValue>(
return null;
})
);
return values;
}
export function MultiSelect<TValue = string>({

View file

@ -1,7 +1,6 @@
/* switch box */
.switch,
.bootbox-checkbox-list > .checkbox > label {
.switch {
--switch-size: 24px;
}
@ -13,8 +12,7 @@
display: none;
}
.switch i,
.bootbox-form .checkbox i {
.switch i {
display: inline-block;
vertical-align: middle;
cursor: pointer;
@ -27,8 +25,7 @@
box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.5);
}
.switch i:before,
.bootbox-form .checkbox i:before {
.switch i:before {
display: block;
content: '';
width: var(--switch-size);
@ -38,8 +35,7 @@
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.5);
}
.switch :checked + i,
.bootbox-form .checkbox :checked ~ i {
.switch :checked + i {
padding-right: 0;
padding-left: var(--switch-size);
-webkit-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;

View 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);
}

View 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,
});
}

View 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'),
});
}

View file

@ -0,0 +1,4 @@
export { Modal } from './Modal';
export { openModal } from './open-modal';
export { ModalType } from './types';
export { type OnSubmit } from './Modal/types';

View 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;
}

View 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',
}

View 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,
};
}