Refactor kebab modal tracking logic in delete/cancel buttons

This commit is contained in:
mabashian
2020-09-10 13:36:05 -04:00
parent c8a07309ee
commit 0fc6affe85
3 changed files with 187 additions and 240 deletions

View File

@@ -1,4 +1,4 @@
import React, { Fragment, useEffect, useState } from 'react'; import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
@@ -51,12 +51,6 @@ function DataListToolbar({
setKebabIsOpen(false); setKebabIsOpen(false);
}; };
useEffect(() => {
if (!kebabModalIsOpen) {
setKebabIsOpen(false);
}
}, [kebabModalIsOpen]);
return ( return (
<Toolbar <Toolbar
id={`${qsConfig.namespace}-list-toolbar`} id={`${qsConfig.namespace}-list-toolbar`}
@@ -98,7 +92,7 @@ function DataListToolbar({
</ToolbarToggleGroup> </ToolbarToggleGroup>
{showExpandCollapse && ( {showExpandCollapse && (
<ToolbarGroup> <ToolbarGroup>
<Fragment> <>
<ToolbarItem> <ToolbarItem>
<ExpandCollapse <ExpandCollapse
isCompact={isCompact} isCompact={isCompact}
@@ -106,7 +100,7 @@ function DataListToolbar({
onExpand={onExpand} onExpand={onExpand}
/> />
</ToolbarItem> </ToolbarItem>
</Fragment> </>
</ToolbarGroup> </ToolbarGroup>
)} )}
{advancedSearchShown && ( {advancedSearchShown && (
@@ -114,7 +108,12 @@ function DataListToolbar({
<KebabifiedProvider <KebabifiedProvider
value={{ value={{
isKebabified: true, isKebabified: true,
onKebabModalChange: isOpen => setKebabModalIsOpen(isOpen), onKebabModalChange: isOpen => {
if (kebabIsOpen && kebabModalIsOpen && !isOpen) {
setKebabIsOpen(false);
}
setKebabModalIsOpen(isOpen);
},
}} }}
> >
<Dropdown <Dropdown
@@ -127,7 +126,7 @@ function DataListToolbar({
}} }}
/> />
} }
isOpen={kebabIsOpen} isOpen={kebabIsOpen || kebabModalIsOpen}
isPlain isPlain
dropdownItems={additionalControls} dropdownItems={additionalControls}
/> />

View File

@@ -1,9 +1,9 @@
import React, { useState } from 'react'; import React, { useContext, useState } from 'react';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { arrayOf, func } from 'prop-types'; import { arrayOf, func } from 'prop-types';
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core'; import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
import { Kebabified } from '../../contexts/Kebabified'; import { KebabifiedContext } from '../../contexts/Kebabified';
import AlertModal from '../AlertModal'; import AlertModal from '../AlertModal';
import { Job } from '../../types'; import { Job } from '../../types';
@@ -12,13 +12,21 @@ function cannotCancel(job) {
} }
function JobListCancelButton({ i18n, jobsToCancel, onCancel }) { function JobListCancelButton({ i18n, jobsToCancel, onCancel }) {
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const numJobsToCancel = jobsToCancel.length; const numJobsToCancel = jobsToCancel.length;
const zeroOrOneJobSelected = numJobsToCancel < 2; const zeroOrOneJobSelected = numJobsToCancel < 2;
const handleCancel = () => { const handleCancelJob = () => {
onCancel(); onCancel();
setIsModalOpen(false); toggleModal();
};
const toggleModal = () => {
if (isKebabified) {
onKebabModalChange(!isModalOpen);
}
setIsModalOpen(!isModalOpen);
}; };
const renderTooltip = () => { const renderTooltip = () => {
@@ -61,96 +69,74 @@ function JobListCancelButton({ i18n, jobsToCancel, onCancel }) {
); );
return ( return (
<Kebabified> <>
{({ isKebabified, onKebabModalChange }) => ( {isKebabified ? (
<> <DropdownItem
{isKebabified ? ( key="cancel-job"
<DropdownItem isDisabled={isDisabled}
key="cancel-job" component="button"
onClick={toggleModal}
>
{cancelJobText}
</DropdownItem>
) : (
<Tooltip content={renderTooltip()} position="top">
<div>
<Button
variant="secondary"
aria-label={cancelJobText}
onClick={toggleModal}
isDisabled={isDisabled} isDisabled={isDisabled}
component="button"
onClick={() => {
onKebabModalChange(true);
setIsModalOpen(true);
}}
> >
{cancelJobText} {cancelJobText}
</DropdownItem> </Button>
) : ( </div>
<Tooltip content={renderTooltip()} position="top"> </Tooltip>
<div>
<Button
variant="secondary"
aria-label={cancelJobText}
onClick={() => setIsModalOpen(true)}
isDisabled={isDisabled}
>
{cancelJobText}
</Button>
</div>
</Tooltip>
)}
{isModalOpen && (
<AlertModal
variant="danger"
title={cancelJobText}
isOpen={isModalOpen}
onClose={() => {
if (isKebabified) {
onKebabModalChange(false);
}
setIsModalOpen(false);
}}
actions={[
<Button
id="cancel-job-confirm-button"
key="delete"
variant="danger"
aria-label={cancelJobText}
onClick={() => {
if (isKebabified) {
onKebabModalChange(false);
}
handleCancel();
}}
>
{cancelJobText}
</Button>,
<Button
id="cancel-job-return-button"
key="cancel"
variant="secondary"
aria-label={i18n._(t`Return`)}
onClick={() => {
if (isKebabified) {
onKebabModalChange(false);
}
setIsModalOpen(false);
}}
>
{i18n._(t`Return`)}
</Button>,
]}
>
<div>
{i18n._(
'{numJobsToCancel, plural, one {This action will cancel the following job:} other {This action will cancel the following jobs:}}',
{
numJobsToCancel,
}
)}
</div>
{jobsToCancel.map(job => (
<span key={job.id}>
<strong>{job.name}</strong>
<br />
</span>
))}
</AlertModal>
)}
</>
)} )}
</Kebabified> {isModalOpen && (
<AlertModal
variant="danger"
title={cancelJobText}
isOpen={isModalOpen}
onClose={toggleModal}
actions={[
<Button
id="cancel-job-confirm-button"
key="delete"
variant="danger"
aria-label={cancelJobText}
onClick={handleCancelJob}
>
{cancelJobText}
</Button>,
<Button
id="cancel-job-return-button"
key="cancel"
variant="secondary"
aria-label={i18n._(t`Return`)}
onClick={toggleModal}
>
{i18n._(t`Return`)}
</Button>,
]}
>
<div>
{i18n._(
'{numJobsToCancel, plural, one {This action will cancel the following job:} other {This action will cancel the following jobs:}}',
{
numJobsToCancel,
}
)}
</div>
{jobsToCancel.map(job => (
<span key={job.id}>
<strong>{job.name}</strong>
<br />
</span>
))}
</AlertModal>
)}
</>
); );
} }

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react'; import React, { useContext, useState } from 'react';
import { import {
func, func,
bool, bool,
@@ -12,7 +12,7 @@ import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import AlertModal from '../AlertModal'; import AlertModal from '../AlertModal';
import { Kebabified } from '../../contexts/Kebabified'; import { KebabifiedContext } from '../../contexts/Kebabified';
const requireNameOrUsername = props => { const requireNameOrUsername = props => {
const { name, username } = props; const { name, username } = props;
@@ -59,53 +59,29 @@ function cannotDelete(item) {
return !item.summary_fields.user_capabilities.delete; return !item.summary_fields.user_capabilities.delete;
} }
class ToolbarDeleteButton extends React.Component { function ToolbarDeleteButton({
static propTypes = { itemsToDelete,
onDelete: func.isRequired, pluralizedItemName,
itemsToDelete: arrayOf(ItemToDelete).isRequired, errorMessage,
pluralizedItemName: string, onDelete,
errorMessage: string, i18n,
}; }) {
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
const [isModalOpen, setIsModalOpen] = useState(false);
static defaultProps = { const handleDelete = () => {
pluralizedItemName: 'Items',
errorMessage: '',
};
constructor(props) {
super(props);
this.state = {
isModalOpen: false,
};
this.handleConfirmDelete = this.handleConfirmDelete.bind(this);
this.handleCancelDelete = this.handleCancelDelete.bind(this);
this.handleDelete = this.handleDelete.bind(this);
}
handleConfirmDelete() {
this.setState({ isModalOpen: true });
}
handleCancelDelete() {
this.setState({ isModalOpen: false });
}
handleDelete() {
const { onDelete } = this.props;
onDelete(); onDelete();
this.setState({ isModalOpen: false }); toggleModal();
} };
renderTooltip() { const toggleModal = () => {
const { if (isKebabified) {
itemsToDelete, onKebabModalChange(!isModalOpen);
pluralizedItemName, }
errorMessage, setIsModalOpen(!isModalOpen);
i18n, };
} = this.props;
const renderTooltip = () => {
const itemsUnableToDelete = itemsToDelete const itemsUnableToDelete = itemsToDelete
.filter(cannotDelete) .filter(cannotDelete)
.map(item => item.name) .map(item => item.name)
@@ -125,103 +101,89 @@ class ToolbarDeleteButton extends React.Component {
return i18n._(t`Delete`); return i18n._(t`Delete`);
} }
return i18n._(t`Select a row to delete`); return i18n._(t`Select a row to delete`);
} };
render() { const modalTitle = i18n._(t`Delete ${pluralizedItemName}?`);
const { itemsToDelete, pluralizedItemName, i18n } = this.props;
const { isModalOpen } = this.state;
const modalTitle = i18n._(t`Delete ${pluralizedItemName}?`);
const isDisabled = const isDisabled =
itemsToDelete.length === 0 || itemsToDelete.some(cannotDelete); itemsToDelete.length === 0 || itemsToDelete.some(cannotDelete);
// NOTE: Once PF supports tooltips on disabled elements, // NOTE: Once PF supports tooltips on disabled elements,
// we can delete the extra <div> around the <DeleteButton> below. // we can delete the extra <div> around the <DeleteButton> below.
// See: https://github.com/patternfly/patternfly-react/issues/1894 // See: https://github.com/patternfly/patternfly-react/issues/1894
return ( return (
<Kebabified> <>
{({ isKebabified, onKebabModalChange }) => ( {isKebabified ? (
<Fragment> <DropdownItem
{isKebabified ? ( key="add"
<DropdownItem isDisabled={isDisabled}
key="add" component="button"
isDisabled={isDisabled} onClick={toggleModal}
component="button" >
onClick={() => { {i18n._(t`Delete`)}
onKebabModalChange(true); </DropdownItem>
this.handleConfirmDelete(); ) : (
}} <Tooltip content={renderTooltip()} position="top">
> <div>
{i18n._(t`Delete`)} <Button
</DropdownItem> variant="secondary"
) : ( aria-label={i18n._(t`Delete`)}
<Tooltip content={this.renderTooltip()} position="top"> onClick={toggleModal}
<div> isDisabled={isDisabled}
<Button >
variant="secondary" {i18n._(t`Delete`)}
aria-label={i18n._(t`Delete`)} </Button>
onClick={this.handleConfirmDelete} </div>
isDisabled={isDisabled} </Tooltip>
> )}
{i18n._(t`Delete`)} {isModalOpen && (
</Button> <AlertModal
</div> variant="danger"
</Tooltip> title={modalTitle}
)} isOpen={isModalOpen}
{isModalOpen && ( onClose={toggleModal}
<AlertModal actions={[
variant="danger" <Button
title={modalTitle} key="delete"
isOpen={isModalOpen} variant="danger"
onClose={() => { aria-label={i18n._(t`confirm delete`)}
if (isKebabified) { onClick={handleDelete}
onKebabModalChange(false); >
} {i18n._(t`Delete`)}
this.handleCancelDelete(); </Button>,
}} <Button
actions={[ key="cancel"
<Button variant="secondary"
key="delete" aria-label={i18n._(t`cancel delete`)}
variant="danger" onClick={toggleModal}
aria-label={i18n._(t`confirm delete`)} >
onClick={() => { {i18n._(t`Cancel`)}
if (isKebabified) { </Button>,
onKebabModalChange(false); ]}
} >
this.handleDelete(); <div>{i18n._(t`This action will delete the following:`)}</div>
}} {itemsToDelete.map(item => (
> <span key={item.id}>
{i18n._(t`Delete`)} <strong>{item.name || item.username}</strong>
</Button>, <br />
<Button </span>
key="cancel" ))}
variant="secondary" </AlertModal>
aria-label={i18n._(t`cancel delete`)} )}
onClick={() => { </>
if (isKebabified) { );
onKebabModalChange(false);
}
this.handleCancelDelete();
}}
>
{i18n._(t`Cancel`)}
</Button>,
]}
>
<div>{i18n._(t`This action will delete the following:`)}</div>
{itemsToDelete.map(item => (
<span key={item.id}>
<strong>{item.name || item.username}</strong>
<br />
</span>
))}
</AlertModal>
)}
</Fragment>
)}
</Kebabified>
);
}
} }
ToolbarDeleteButton.propTypes = {
onDelete: func.isRequired,
itemsToDelete: arrayOf(ItemToDelete).isRequired,
pluralizedItemName: string,
errorMessage: string,
};
ToolbarDeleteButton.defaultProps = {
pluralizedItemName: 'Items',
errorMessage: '',
};
export default withI18n()(ToolbarDeleteButton); export default withI18n()(ToolbarDeleteButton);