Merge pull request #1349 from scm-manager/feature/modal-rework

Feature/modal rework
This commit is contained in:
Konstantin Schaper
2020-10-07 18:22:19 +02:00
committed by GitHub
12 changed files with 399 additions and 899 deletions

View File

@@ -23,6 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Permissions can be specified for namespaces ([#1335](https://github.com/scm-manager/scm-manager/pull/1335))
- Show update info on admin information page ([#1342](https://github.com/scm-manager/scm-manager/pull/1342))
### Changed
- Rework modal to use react portal ([#1349](https://github.com/scm-manager/scm-manager/pull/1349))
### Fixed
- Missing synchronization during repository creation ([#1328](https://github.com/scm-manager/scm-manager/pull/1328))
- Missing BranchCreatedEvent for mercurial ([#1334](https://github.com/scm-manager/scm-manager/pull/1334))

View File

@@ -47242,575 +47242,30 @@ exports[`Storyshots MarkdownView Xml Code Block 1`] = `
</div>
`;
exports[`Storyshots Modal|ConfirmAlert Default 1`] = `
<div
className="modal is-active"
>
<div
className="modal-background"
/>
<div
className="modal-card"
>
<header
className="modal-card-head has-background-light"
>
<p
className="modal-card-title is-marginless"
>
Are you sure about that?
</p>
exports[`Storyshots Modal|ConfirmAlert Default 1`] = `null`;
exports[`Storyshots Modal|ConfirmAlert WithButton 1`] = `
Array [
<button
aria-label="close"
className="delete"
onClick={[Function]}
/>
</header>
<section
className="modal-card-body"
>
Mind-paralyzing change needed improbability vortex machine sorts sought same theory upending job just allows
hostesss really oblong Infinite Improbability thing into the starship against which behavior accordance with
Kakrafoon humanoid undergarment ship powered by GPP-guided bowl of petunias nothing was frequently away incredibly
ordinary mob.
</section>
<footer
className="modal-card-foot"
>
Open ConfirmAlert
</button>,
<div
className="field is-grouped"
>
<p
className="control"
>
<a
className="button is-info is-outlined"
onClick={[Function]}
>
Cancel
</a>
</p>
<p
className="control"
>
<a
className="button is-info"
onClick={[Function]}
>
Submit
</a>
</p>
</div>
</footer>
</div>
</div>
id="modalRoot"
/>,
]
`;
exports[`Storyshots Modal|Modal Closeable 1`] = `
<div
className="modal is-active"
>
<div
className="modal-background"
/>
<div
className="modal-card"
>
<header
className="modal-card-head has-background-light"
>
<p
className="modal-card-title is-marginless"
>
Hitchhiker Modal
</p>
<button
aria-label="close"
className="delete"
onClick={[Function]}
/>
</header>
<section
className="modal-card-body"
>
<p>
Mind-paralyzing change needed improbability vortex machine sorts sought same theory upending job just allows
hostesss really oblong Infinite Improbability thing into the starship against which behavior accordance.with
Kakrafoon humanoid undergarment ship powered by GPP-guided bowl of petunias nothing was frequently away incredibly
ordinary mob.
</p>
</section>
</div>
</div>
`;
exports[`Storyshots Modal|Modal Closeable 1`] = `null`;
exports[`Storyshots Modal|Modal Default 1`] = `
<div
className="modal is-active"
>
<div
className="modal-background"
/>
<div
className="modal-card"
>
<header
className="modal-card-head has-background-light"
>
<p
className="modal-card-title is-marginless"
>
Hitchhiker Modal
</p>
<button
aria-label="close"
className="delete"
onClick={[Function]}
/>
</header>
<section
className="modal-card-body"
>
<p>
Mind-paralyzing change needed improbability vortex machine sorts sought same theory upending job just allows
hostesss really oblong Infinite Improbability thing into the starship against which behavior accordance.with
Kakrafoon humanoid undergarment ship powered by GPP-guided bowl of petunias nothing was frequently away incredibly
ordinary mob.
</p>
</section>
</div>
</div>
`;
exports[`Storyshots Modal|Modal Default 1`] = `null`;
exports[`Storyshots Modal|Modal Long content 1`] = `
<div
className="modal is-active"
>
<div
className="modal-background"
/>
<div
className="modal-card"
>
<header
className="modal-card-head has-background-light"
>
<p
className="modal-card-title is-marginless"
>
Hitchhiker Modal
</p>
<button
aria-label="close"
className="delete"
onClick={[Function]}
/>
</header>
<section
className="modal-card-body"
>
<h1
className="title"
>
Marvin
</h1>
<h2
className="subtitle"
>
The Paranoid Android
</h2>
<hr />
<div
className="notification is-info"
>
exports[`Storyshots Modal|Modal Long content 1`] = `null`;
The following content comes from the awesome
exports[`Storyshots Modal|Modal With form elements 1`] = `null`;
<a
href="https://hitchhikers.fandom.com/wiki/Main_Page"
rel="noopener noreferrer"
target="_blank"
>
Hitchhikers Wiki
</a>
</div>
<hr />
<div
className="has-text-centered"
>
<img
alt="Marvin"
src="https://vignette.wikia.nocookie.net/hitchhikers/images/a/a4/Marvin.jpg/revision/latest/scale-to-width-down/150?cb=20100530114055"
/>
</div>
<hr />
<p
className="content"
>
Marvin, more fully known as Marvin the Paranoid Android, is an incredibly brilliant but overwhelmingly depressed robot manufactured by the Sirius Cybernetics Corporation and unwilling servant to the crew of the Heart of Gold.
</p>
<hr />
<div
className="content"
>
<h4>
Physical Appearance
</h4>
<p>
In the novels, Marvin is described thusly: "...though it was beautifully constructed and polished it looked somehow as if the various parts of its more or less humanoid body didn't quite fit properly. In fact, they fit perfectly well, but something in its bearing suggested that they might have fitted better."
</p>
<p>
On the radio show, there's no physical description of Marvin, though his voice is digitally altered to sound more robotic, and any scene that focuses on him is accompanied by sounds of mechanical clanking and hissing.
</p>
<p>
In the TV series, Marvin is built in the style of a 1950's robot similar to Robbie the Robot from Forbidden Planet or Twiki from Buck Rogers. His body is blocky and angular, with a pair of clamp-claw hands, shuffling feet and a squarish head with a dour face.
</p>
<p>
In the movie, Marvin is a short, stout robot built of smooth, white plastic. His arms are much longer than his legs, and his head is a massive sphere with only a pair of triangle eyes for a face. His large head and simian-like proportions give Marvin a perpetual slouch, adding to his melancholy personality. At the start of the film his eyes glow, but at the end he is shot but unharmed, leaving a hole in his head and dimming his eyes. This is probably the most depressing and unacceptable manifestation of Marvin ever conceived, and thus paradoxically the most accurate.
</p>
</div>
<hr />
<div
className="content"
>
<h4>
Personality
</h4>
<p>
Marvin the robot has a prototype version of the Genuine People Personality (GPP) software from SCC, allowing him sentience and the ability to feel emotions and develop a personality. He's also incredibly smart, having a "brain the size of a planet" capable of computing extremely complex mathematics, as well as solving difficult problems and operating high-tech devices.
</p>
<p>
However, despite being so smart, Marvin is typically made to perform menial tasks and labour such as escorting people, opening doors, picking up pieces of paper, and other tasks well beneath his skills. Even extremely hard tasks, such as computing for the vast Krikkit robot army, are trivial for Marvin. All this leaves him extremely bored, frustrated, and overwhelmingly depressed. Because of this, all modern GPP-capable machines, such as Eddie the computer and the Heart of Gold's automatic doors, are programmed to be extremely cheerful and happy, much to Marvin's disgust.
</p>
<p>
Marvin hates everyone and everything he comes into contact with, having no respect for anybody and will criticise and insult others at any opportunity, or otherwise rant and complain for hours on end about his own problems, such as the terrible pain he suffers in all the diodes down his left side. His contempt for everyone is often justified, as almost every person he comes across, even those who consider him a friend, (such as Arthur and Trillian, who treat him more kindly than Ford and Zaphod) treat Marvin as an expendable servant, even sending him to his death more than once (such as when Zaphod ordered Marvin to fight the gigantic, heavy-duty Frogstar Scout Robot Class D so he could escape). Being a robot, he still does what he's told (he won't enjoy it, nor will he let you forget it, but he'll do it anyway), though he'd much rather sulk in a corner by himself.
</p>
<p>
Several times in the series Marvin ends up alone and isolated for extremely long periods of time, sometimes spanning millions of years, either by sheer bad luck (such as the explosion that propelled everyone but Marvin to Milliways in the far-off future) or because his unpleasantly depressing personality drives them away or, in more than one case, makes them commit suicide. In his spare time (which he has a lot of), Marvin will attempt to occupy himself by composing songs and writing poetry. Of course, none of them are particularly cheerful, or even that good.
</p>
</div>
</section>
</div>
</div>
`;
exports[`Storyshots Modal|Modal With form elements 1`] = `
<div
className="modal is-active"
>
<div
className="modal-background"
/>
<div
className="modal-card"
>
<header
className="modal-card-head has-background-light"
>
<p
className="modal-card-title is-marginless"
>
Hitchhiker Modal
</p>
<button
aria-label="close"
className="delete"
onClick={[Function]}
/>
</header>
<section
className="modal-card-body"
>
<div
className="Modalstories__RadioList-sc-2lb0wg-1 hFfBOw"
>
<label
className="Radio__StyledRadio-ays4vp-0 hrHCWE radio"
>
<input
checked={true}
onChange={[Function]}
type="radio"
/>
One
<span
className="tooltip has-tooltip-right Help__HelpTooltip-ykmmew-0 cYhfno is-inline-block has-tooltip-multiline"
data-tooltip="The first one"
>
<i
className="fas fa-question-circle has-text-blue-light"
/>
</span>
</label>
<label
className="Radio__StyledRadio-ays4vp-0 hrHCWE radio"
>
<input
checked={false}
onChange={[Function]}
type="radio"
/>
Two
<span
className="tooltip has-tooltip-right Help__HelpTooltip-ykmmew-0 cYhfno is-inline-block has-tooltip-multiline"
data-tooltip="The second one"
>
<i
className="fas fa-question-circle has-text-blue-light"
/>
</span>
</label>
</div>
<hr />
<p>
Mind-paralyzing change needed improbability vortex machine sorts sought same theory upending job just allows
hostesss really oblong Infinite Improbability thing into the starship against which behavior accordance.with
Kakrafoon humanoid undergarment ship powered by GPP-guided bowl of petunias nothing was frequently away incredibly
ordinary mob.
</p>
<hr />
<div
className="field"
>
<label
className="label"
>
Text
</label>
<div
className="control"
>
<textarea
className="textarea"
disabled={false}
onChange={[Function]}
onKeyDown={[Function]}
/>
</div>
</div>
<hr />
<div
className="field is-grouped"
>
<div
className="control"
>
<button
className="button is-default"
onClick={[Function]}
type="button"
>
One
</button>
</div>
<div
className="control"
>
<button
className="button is-default"
onClick={[Function]}
type="button"
>
Two
</button>
</div>
</div>
</section>
</div>
</div>
`;
exports[`Storyshots Modal|Modal With long tooltips 1`] = `
<div
className="modal is-active"
>
<div
className="modal-background"
/>
<div
className="modal-card"
>
<header
className="modal-card-head has-background-light"
>
<p
className="modal-card-title is-marginless"
>
Hitchhiker Modal
</p>
<button
aria-label="close"
className="delete"
onClick={[Function]}
/>
</header>
<section
className="modal-card-body"
>
<div
className="notification is-info"
>
This story exists because we had a problem, that long tooltips causes a horizontal scrollbar on the modal.
</div>
<hr />
<p>
The following elements will have a verly long help text, which has triggered the scrollbar in the past.
</p>
<hr />
<div
className="Modalstories__TopAndBottomMargin-sc-2lb0wg-0 bfocSI"
>
<div
className="field"
>
<div
className="control"
onClick={[Function]}
onKeyDown={[Function]}
>
<label
className="checkbox"
>
<span
className="gwt-Anchor"
tabIndex={0}
>
<i
className="is-outlined fa-check-square has-text-link fa"
/>
</span>
Checkbox
<span
className="tooltip has-tooltip-right Help__HelpTooltip-ykmmew-0 cYhfno is-inline-block has-tooltip-multiline"
data-tooltip="Mind-paralyzing change needed improbability vortex machine sorts sought same theory upending job just allows
hostesss really oblong Infinite Improbability thing into the starship against which behavior accordance.with
Kakrafoon humanoid undergarment ship powered by GPP-guided bowl of petunias nothing was frequently away incredibly
ordinary mob."
>
<i
className="fas fa-question-circle has-text-blue-light"
/>
</span>
</label>
</div>
</div>
</div>
<hr />
<div
className="Modalstories__TopAndBottomMargin-sc-2lb0wg-0 bfocSI"
>
<label
className="Radio__StyledRadio-ays4vp-0 hrHCWE radio"
>
<input
checked={false}
onChange={[Function]}
type="radio"
/>
Radio button
<span
className="tooltip has-tooltip-right Help__HelpTooltip-ykmmew-0 cYhfno is-inline-block has-tooltip-multiline"
data-tooltip="Mind-paralyzing change needed improbability vortex machine sorts sought same theory upending job just allows
hostesss really oblong Infinite Improbability thing into the starship against which behavior accordance.with
Kakrafoon humanoid undergarment ship powered by GPP-guided bowl of petunias nothing was frequently away incredibly
ordinary mob."
>
<i
className="fas fa-question-circle has-text-blue-light"
/>
</span>
</label>
</div>
<hr />
<div
className="Modalstories__TopAndBottomMargin-sc-2lb0wg-0 bfocSI"
>
<div
className="field"
>
<label
className="label"
>
Input
<span
className="tooltip has-tooltip-right Help__HelpTooltip-ykmmew-0 cYhfno is-inline-block has-tooltip-multiline"
data-tooltip="Mind-paralyzing change needed improbability vortex machine sorts sought same theory upending job just allows
hostesss really oblong Infinite Improbability thing into the starship against which behavior accordance.with
Kakrafoon humanoid undergarment ship powered by GPP-guided bowl of petunias nothing was frequently away incredibly
ordinary mob."
>
<i
className="fas fa-question-circle has-text-blue-light"
/>
</span>
</label>
<div
className="control"
>
<input
className="input"
onChange={[Function]}
onKeyPress={[Function]}
placeholder=""
type="text"
/>
</div>
</div>
</div>
<hr />
<div
className="Modalstories__TopAndBottomMargin-sc-2lb0wg-0 bfocSI"
>
<div
className="field"
>
<label
className="label"
>
Textarea
<span
className="tooltip has-tooltip-right Help__HelpTooltip-ykmmew-0 cYhfno is-inline-block has-tooltip-multiline"
data-tooltip="Mind-paralyzing change needed improbability vortex machine sorts sought same theory upending job just allows
hostesss really oblong Infinite Improbability thing into the starship against which behavior accordance.with
Kakrafoon humanoid undergarment ship powered by GPP-guided bowl of petunias nothing was frequently away incredibly
ordinary mob."
>
<i
className="fas fa-question-circle has-text-blue-light"
/>
</span>
</label>
<div
className="control"
>
<textarea
className="textarea"
disabled={false}
onChange={[Function]}
onKeyDown={[Function]}
/>
</div>
</div>
</div>
<hr />
<p>
If this modal has no horizontal scrollbar the issue is fixed
</p>
</section>
</div>
</div>
`;
exports[`Storyshots Modal|Modal With long tooltips 1`] = `null`;
exports[`Storyshots Navigation|Secondary Active when match 1`] = `
<div

View File

@@ -25,7 +25,7 @@
import { storiesOf } from "@storybook/react";
import { MemoryRouter } from "react-router-dom";
import * as React from "react";
import ConfirmAlert from "./ConfirmAlert";
import ConfirmAlert, { confirmAlert } from "./ConfirmAlert";
const body =
"Mind-paralyzing change needed improbability vortex machine sorts sought same theory upending job just allows\n " +
@@ -40,11 +40,21 @@ const buttons = [
onClick: () => null
},
{
label: "Submit",
onClick: () => {}
label: "Submit"
}
];
storiesOf("Modal|ConfirmAlert", module)
.addDecorator(story => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
.add("Default", () => <ConfirmAlert message={body} title={"Are you sure about that?"} buttons={buttons} />);
.add("Default", () => <ConfirmAlert message={body} title={"Are you sure about that?"} buttons={buttons} />)
.add("WithButton", () => {
const buttonClick = () => {
confirmAlert({ message: body, title: "Are you sure about that?", buttons });
};
return (
<>
<button onClick={buttonClick}>Open ConfirmAlert</button>
<div id="modalRoot" />
</>
);
});

View File

@@ -22,6 +22,7 @@
* SOFTWARE.
*/
import * as React from "react";
import { FC, useState } from "react";
import ReactDOM from "react-dom";
import Modal from "./Modal";
import classNames from "classnames";
@@ -29,33 +30,34 @@ import classNames from "classnames";
type Button = {
className?: string;
label: string;
onClick: () => void | null;
onClick?: () => void | null;
};
type Props = {
title: string;
message: string;
buttons: Button[];
close?: () => void;
};
class ConfirmAlert extends React.Component<Props> {
handleClickButton = (button: Button) => {
export const ConfirmAlert: FC<Props> = ({ title, message, buttons, close }) => {
const [showModal, setShowModal] = useState(true);
const onClose = () => {
if (typeof close === "function") {
close();
} else {
setShowModal(false);
}
};
const handleClickButton = (button: Button) => {
if (button.onClick) {
button.onClick();
}
this.close();
onClose();
};
close = () => {
const container = document.getElementById("modalRoot");
if (container) {
ReactDOM.unmountComponentAtNode(container);
}
};
render() {
const { title, message, buttons } = this.props;
const body = <>{message}</>;
const footer = (
@@ -65,7 +67,7 @@ class ConfirmAlert extends React.Component<Props> {
<a
className={classNames("button", "is-info", button.className)}
key={i}
onClick={() => this.handleClickButton(button)}
onClick={() => handleClickButton(button)}
>
{button.label}
</a>
@@ -74,14 +76,25 @@ class ConfirmAlert extends React.Component<Props> {
</div>
);
return <Modal title={title} closeFunction={() => this.close()} body={body} active={true} footer={footer} />;
}
}
return (
(showModal && <Modal title={title} closeFunction={onClose} body={body} active={true} footer={footer} />) || null
);
};
/**
* @deprecated Please use {@link ConfirmAlert} directly.
*/
export function confirmAlert(properties: Props) {
const root = document.getElementById("modalRoot");
if (root) {
ReactDOM.render(<ConfirmAlert {...properties} />, root);
const close = () => {
const container = document.getElementById("modalRoot");
if (container) {
ReactDOM.unmountComponentAtNode(container);
}
};
const props = { ...properties, close };
ReactDOM.render(<ConfirmAlert {...props} />, root);
}
}

View File

@@ -22,7 +22,10 @@
* SOFTWARE.
*/
import * as React from "react";
import {FC} from "react";
import classNames from "classnames";
import usePortalRootElement from "../usePortalRootElement";
import ReactDOM from "react-dom";
type Props = {
title: string;
@@ -31,16 +34,15 @@ type Props = {
footer?: any;
active: boolean;
className?: string;
headColor: string;
headColor?: string;
};
class Modal extends React.Component<Props> {
static defaultProps = {
headColor: "light"
};
export const Modal: FC<Props> = ({ title, closeFunction, body, footer, active, className, headColor = "light" }) => {
const portalRootElement = usePortalRootElement("modalsRoot");
render() {
const { title, closeFunction, body, footer, active, className, headColor } = this.props;
if (!portalRootElement) {
return null;
}
const isActive = active ? "is-active" : null;
@@ -49,7 +51,7 @@ class Modal extends React.Component<Props> {
showFooter = <footer className="modal-card-foot">{footer}</footer>;
}
return (
const modalElement = (
<div className={classNames("modal", className, isActive)}>
<div className="modal-background" />
<div className="modal-card">
@@ -62,7 +64,8 @@ class Modal extends React.Component<Props> {
</div>
</div>
);
}
}
return ReactDOM.createPortal(modalElement, portalRootElement);
};
export default Modal;

View File

@@ -30,5 +30,10 @@ jest.mock("react-i18next", () => ({
t: (key: string) => key
};
return Component;
},
useTranslation: (ns: string) => {
return [
(key: string) => key
];
}
}));

View File

@@ -21,69 +21,68 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import React, { FC, useState } from "react";
import { connect } from "react-redux";
import { compose } from "redux";
import { withRouter } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { History } from "history";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { RepositoryRole } from "@scm-manager/ui-types";
import { confirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { ConfirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { deleteRole, getDeleteRoleFailure, isDeleteRolePending } from "../modules/roles";
type Props = WithTranslation & {
type Props = {
loading: boolean;
error: Error;
role: RepositoryRole;
confirmDialog?: boolean;
deleteRole: (role: RepositoryRole, callback?: () => void) => void;
// context props
history: History;
};
class DeleteRepositoryRole extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
const DeleteRepositoryRole: FC<Props> = ({ confirmDialog = true, deleteRole, role, loading, error }: Props) => {
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
const [t] = useTranslation("admin");
const history = useHistory();
const roleDeleted = () => {
history.push("/admin/roles/");
};
roleDeleted = () => {
this.props.history.push("/admin/roles/");
const deleteRoleCallback = () => {
deleteRole(role, roleDeleted);
};
deleteRole = () => {
this.props.deleteRole(this.props.role, this.roleDeleted);
const confirmDelete = () => {
setShowConfirmAlert(true);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("repositoryRole.delete.confirmAlert.title"),
message: t("repositoryRole.delete.confirmAlert.message"),
buttons: [
const isDeletable = () => {
return role._links.delete;
};
const action = confirmDialog ? confirmDelete : deleteRoleCallback;
if (!isDeletable()) {
return null;
}
if (showConfirmAlert) {
return (
<ConfirmAlert
title={t("repositoryRole.delete.confirmAlert.title")}
message={t("repositoryRole.delete.confirmAlert.message")}
buttons={[
{
className: "is-outlined",
label: t("repositoryRole.delete.confirmAlert.submit"),
onClick: () => this.deleteRole()
onClick: () => deleteRoleCallback()
},
{
label: t("repositoryRole.delete.confirmAlert.cancel"),
onClick: () => null
}
]
});
};
isDeletable = () => {
return this.props.role._links.delete;
};
render() {
const { loading, error, confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deleteRole;
if (!this.isDeletable()) {
return null;
]}
close={() => setShowConfirmAlert(false)}
/>
);
}
return (
@@ -93,8 +92,7 @@ class DeleteRepositoryRole extends React.Component<Props> {
<Level right={<DeleteButton label={t("repositoryRole.delete.button")} action={action} loading={loading} />} />
</>
);
}
}
};
const mapStateToProps = (state: any, ownProps: Props) => {
const loading = isDeleteRolePending(state, ownProps.role.name);
@@ -113,8 +111,4 @@ const mapDispatchToProps = (dispatch: any) => {
};
};
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withRouter,
withTranslation("admin")
)(DeleteRepositoryRole);
export default connect(mapStateToProps, mapDispatchToProps)(DeleteRepositoryRole);

View File

@@ -21,69 +21,68 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import React, { FC, useState } from "react";
import { connect } from "react-redux";
import { compose } from "redux";
import { withRouter } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { History } from "history";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Group } from "@scm-manager/ui-types";
import { confirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { ConfirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { deleteGroup, getDeleteGroupFailure, isDeleteGroupPending } from "../modules/groups";
type Props = WithTranslation & {
type Props = {
loading: boolean;
error: Error;
group: Group;
confirmDialog?: boolean;
deleteGroup: (group: Group, callback?: () => void) => void;
// context props
history: History;
};
export class DeleteGroup extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
export const DeleteGroup: FC<Props> = ({ confirmDialog = true, group, deleteGroup, loading, error }) => {
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
const [t] = useTranslation("groups");
const history = useHistory();
const deleteGroupCallback = () => {
deleteGroup(group, groupDeleted);
};
deleteGroup = () => {
this.props.deleteGroup(this.props.group, this.groupDeleted);
const groupDeleted = () => {
history.push("/groups/");
};
groupDeleted = () => {
this.props.history.push("/groups/");
const confirmDelete = () => {
setShowConfirmAlert(true);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("deleteGroup.confirmAlert.title"),
message: t("deleteGroup.confirmAlert.message"),
buttons: [
const isDeletable = () => {
return group._links.delete;
};
const action = confirmDialog ? confirmDelete : deleteGroupCallback;
if (!isDeletable()) {
return null;
}
if (showConfirmAlert) {
return (
<ConfirmAlert
title={t("deleteGroup.confirmAlert.title")}
message={t("deleteGroup.confirmAlert.message")}
buttons={[
{
className: "is-outlined",
label: t("deleteGroup.confirmAlert.submit"),
onClick: () => this.deleteGroup()
onClick: () => deleteGroupCallback()
},
{
label: t("deleteGroup.confirmAlert.cancel"),
onClick: () => null
}
]
});
};
isDeletable = () => {
return this.props.group._links.delete;
};
render() {
const { loading, error, confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deleteGroup;
if (!this.isDeletable()) {
return null;
]}
close={() => setShowConfirmAlert(false)}
/>
);
}
return (
@@ -93,8 +92,7 @@ export class DeleteGroup extends React.Component<Props> {
<Level right={<DeleteButton label={t("deleteGroup.button")} action={action} loading={loading} />} />
</>
);
}
}
};
const mapStateToProps = (state: any, ownProps: Props) => {
const loading = isDeleteGroupPending(state, ownProps.group.name);
@@ -113,8 +111,4 @@ const mapDispatchToProps = (dispatch: any) => {
};
};
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withRouter,
withTranslation("groups")
)(DeleteGroup);
export default connect(mapStateToProps, mapDispatchToProps)(DeleteGroup);

View File

@@ -21,17 +21,15 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import React, { FC, useState } from "react";
import { connect } from "react-redux";
import { compose } from "redux";
import { RouteComponentProps, withRouter } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Repository } from "@scm-manager/ui-types";
import { confirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { ConfirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { deleteRepo, getDeleteRepoFailure, isDeleteRepoPending } from "../modules/repos";
type Props = RouteComponentProps &
WithTranslation & {
type Props = {
loading: boolean;
error: Error;
repository: Repository;
@@ -39,48 +37,52 @@ type Props = RouteComponentProps &
deleteRepo: (p1: Repository, p2: () => void) => void;
};
class DeleteRepo extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
const DeleteRepo: FC<Props> = ({ confirmDialog = true, repository, deleteRepo, loading, error }: Props) => {
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
const [t] = useTranslation("repos");
const history = useHistory();
const deleted = () => {
history.push("/repos/");
};
deleted = () => {
this.props.history.push("/repos/");
const deleteRepoCallback = () => {
deleteRepo(repository, deleted);
};
deleteRepo = () => {
this.props.deleteRepo(this.props.repository, this.deleted);
const confirmDelete = () => {
setShowConfirmAlert(true);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("deleteRepo.confirmAlert.title"),
message: t("deleteRepo.confirmAlert.message"),
buttons: [
const isDeletable = () => {
return repository._links.delete;
};
const action = confirmDialog ? confirmDelete : deleteRepoCallback;
if (!isDeletable()) {
return null;
}
if (showConfirmAlert) {
return (
<ConfirmAlert
title={t("deleteRepo.confirmAlert.title")}
message={t("deleteRepo.confirmAlert.message")}
buttons={[
{
className: "is-outlined",
label: t("deleteRepo.confirmAlert.submit"),
onClick: () => this.deleteRepo()
onClick: () => deleteRepoCallback()
},
{
label: t("deleteRepo.confirmAlert.cancel"),
onClick: () => null
}
]
});
};
isDeletable = () => {
return this.props.repository._links.delete;
};
render() {
const { loading, error, confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deleteRepo;
if (!this.isDeletable()) {
return null;
]}
close={() => setShowConfirmAlert(false)}
/>
);
}
return (
@@ -98,8 +100,7 @@ class DeleteRepo extends React.Component<Props> {
/>
</>
);
}
}
};
const mapStateToProps = (state: any, ownProps: Props) => {
const { namespace, name } = ownProps.repository;
@@ -119,4 +120,4 @@ const mapDispatchToProps = (dispatch: any) => {
};
};
export default compose(connect(mapStateToProps, mapDispatchToProps), withRouter, withTranslation("repos"))(DeleteRepo);
export default connect(mapStateToProps, mapDispatchToProps)(DeleteRepo);

View File

@@ -21,16 +21,19 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
import React, { FC } from "react";
// eslint-disable-next-line no-restricted-imports
import { mount, shallow } from "@scm-manager/ui-tests/enzyme-router";
// eslint-disable-next-line no-restricted-imports
import "@scm-manager/ui-tests/enzyme";
// eslint-disable-next-line no-restricted-imports
import "@scm-manager/ui-tests/i18n";
import DeletePermissionButton from "./DeletePermissionButton";
import { confirmAlert } from "@scm-manager/ui-components";
jest.mock("@scm-manager/ui-components", () => ({
confirmAlert: jest.fn(),
ConfirmAlert: (({ children }) => <div className="modal">{children}</div>) as FC<never>,
DeleteButton: require.requireActual("@scm-manager/ui-components").DeleteButton
}));
@@ -40,6 +43,9 @@ describe("DeletePermissionButton", () => {
_links: {}
};
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-empty-function
const navLink = shallow(<DeletePermissionButton permission={permission} deletePermission={() => {}} />);
expect(navLink.text()).toBe("");
});
@@ -53,6 +59,9 @@ describe("DeletePermissionButton", () => {
}
};
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-empty-function
const deleteIcon = mount(<DeletePermissionButton permission={permission} deletePermission={() => {}} />);
expect(deleteIcon.html()).not.toBe("");
});
@@ -66,10 +75,13 @@ describe("DeletePermissionButton", () => {
}
};
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-empty-function
const button = mount(<DeletePermissionButton permission={permission} deletePermission={() => {}} />);
button.find(".fa-trash").simulate("click");
expect(confirmAlert.mock.calls.length).toBe(1);
expect(button.find(".modal")).toBeTruthy();
});
it("should call the delete permission function with delete url", () => {
@@ -82,11 +94,14 @@ describe("DeletePermissionButton", () => {
};
let calledUrl = null;
function capture(permission) {
calledUrl = permission._links.delete.href;
}
const button = mount(
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
<DeletePermissionButton permission={permission} confirmDialog={false} deletePermission={capture} />
);
button.find(".fa-trash").simulate("click");

View File

@@ -21,12 +21,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import React, { FC, useState } from "react";
import { useTranslation } from "react-i18next";
import { Permission } from "@scm-manager/ui-types";
import { confirmAlert } from "@scm-manager/ui-components";
import { ConfirmAlert } from "@scm-manager/ui-components";
type Props = WithTranslation & {
type Props = {
permission: Permission;
namespace: string;
repoName: string;
@@ -35,45 +35,55 @@ type Props = WithTranslation & {
loading: boolean;
};
class DeletePermissionButton extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
const DeletePermissionButton: FC<Props> = ({
confirmDialog = true,
permission,
namespace,
deletePermission,
repoName
}) => {
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
const [t] = useTranslation("repos");
const deletePermissionCallback = () => {
deletePermission(permission, namespace, repoName);
};
deletePermission = () => {
this.props.deletePermission(this.props.permission, this.props.namespace, this.props.repoName);
const confirmDelete = () => {
setShowConfirmAlert(true);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("permission.delete-permission-button.confirm-alert.title"),
message: t("permission.delete-permission-button.confirm-alert.message"),
buttons: [
const isDeletable = () => {
return permission._links.delete;
};
const action = confirmDialog ? confirmDelete : deletePermissionCallback;
if (!isDeletable()) {
return null;
}
if (showConfirmAlert) {
return (
<ConfirmAlert
title={t("permission.delete-permission-button.confirm-alert.title")}
message={t("permission.delete-permission-button.confirm-alert.message")}
buttons={[
{
className: "is-outlined",
label: t("permission.delete-permission-button.confirm-alert.submit"),
onClick: () => this.deletePermission()
onClick: () => deletePermissionCallback()
},
{
label: t("permission.delete-permission-button.confirm-alert.cancel"),
onClick: () => null
}
]
});
};
isDeletable = () => {
return this.props.permission._links.delete;
};
render() {
const { confirmDialog } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deletePermission;
if (!this.isDeletable()) {
return null;
]}
close={() => setShowConfirmAlert(false)}
/>
);
}
return (
<a className="level-item" onClick={action}>
<span className="icon is-small">
@@ -81,7 +91,6 @@ class DeletePermissionButton extends React.Component<Props> {
</span>
</a>
);
}
}
};
export default withTranslation("repos")(DeletePermissionButton);
export default DeletePermissionButton;

View File

@@ -21,69 +21,68 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import React, { FC, useState } from "react";
import { connect } from "react-redux";
import { compose } from "redux";
import { withRouter } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { History } from "history";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { User } from "@scm-manager/ui-types";
import { confirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { ConfirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { deleteUser, getDeleteUserFailure, isDeleteUserPending } from "../modules/users";
type Props = WithTranslation & {
type Props = {
loading: boolean;
error: Error;
user: User;
confirmDialog?: boolean;
deleteUser: (user: User, callback?: () => void) => void;
// context props
history: History;
};
class DeleteUser extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
const DeleteUser: FC<Props> = ({ confirmDialog = true, loading, error, user, deleteUser }) => {
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
const [t] = useTranslation("users");
const history = useHistory();
const userDeleted = () => {
history.push("/users/");
};
userDeleted = () => {
this.props.history.push("/users/");
const deleteUserCallback = () => {
deleteUser(user, userDeleted);
};
deleteUser = () => {
this.props.deleteUser(this.props.user, this.userDeleted);
const confirmDelete = () => {
setShowConfirmAlert(true);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("deleteUser.confirmAlert.title"),
message: t("deleteUser.confirmAlert.message"),
buttons: [
const isDeletable = () => {
return user._links.delete;
};
const action = confirmDialog ? confirmDelete : deleteUserCallback;
if (!isDeletable()) {
return null;
}
if (showConfirmAlert) {
return (
<ConfirmAlert
title={t("deleteUser.confirmAlert.title")}
message={t("deleteUser.confirmAlert.message")}
buttons={[
{
className: "is-outlined",
label: t("deleteUser.confirmAlert.submit"),
onClick: () => this.deleteUser()
onClick: () => deleteUserCallback()
},
{
label: t("deleteUser.confirmAlert.cancel"),
onClick: () => null
}
]
});
};
isDeletable = () => {
return this.props.user._links.delete;
};
render() {
const { loading, error, confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deleteUser;
if (!this.isDeletable()) {
return null;
]}
close={() => setShowConfirmAlert(false)}
/>
);
}
return (
@@ -93,8 +92,7 @@ class DeleteUser extends React.Component<Props> {
<Level right={<DeleteButton label={t("deleteUser.button")} action={action} loading={loading} />} />
</>
);
}
}
};
const mapStateToProps = (state: any, ownProps: Props) => {
const loading = isDeleteUserPending(state, ownProps.user.name);
@@ -113,4 +111,4 @@ const mapDispatchToProps = (dispatch: any) => {
};
};
export default compose(connect(mapStateToProps, mapDispatchToProps), withRouter, withTranslation("users"))(DeleteUser);
export default connect(mapStateToProps, mapDispatchToProps)(DeleteUser);