mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-02 03:25:56 +01:00
Implement confirmation dialog before repository delete
Squash commits of branch feature/confirm_repo_delete: - Implement confirmation dialog before repository delete - Change repo alert - The input does not support autocomplete Therefore it should not be announced as an autocomplete input by assistive technologies. - Change repo alert - Remove unused import - Update snapshots
This commit is contained in:
@@ -62,8 +62,8 @@ Das gewählte Repository wird zum SCM-Manager hinzugefügt und sämtliche Reposi
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Repository Informationen
|
### Repositoryinformationen
|
||||||
Die Informationsseite eines Repository zeigt die Metadaten zum Repository an. Darunter befinden sich Beschreibungen zu den unterschiedlichen Möglichkeiten wie man mit diesem Repository arbeiten kann.
|
Die Informationsseite eines Repositorys zeigt die Metadaten zum Repository an. Darunter befinden sich Beschreibungen zu den unterschiedlichen Möglichkeiten wie man mit diesem Repository arbeiten kann.
|
||||||
In der Überschrift kann der Namespace angeklickt werden, um alle Repositories aus diesem Namespace anzuzeigen.
|
In der Überschrift kann der Namespace angeklickt werden, um alle Repositories aus diesem Namespace anzuzeigen.
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -45,10 +45,10 @@ eingebunden sind. Wenn bei dem Zugriff auf ein Repository Fehler auftreten, soll
|
|||||||
Integritätsprüfung gestartet werden. Ein Teil dieser Prüfungen wird bei jedem Start des SCM-Managers ausgeführt.
|
Integritätsprüfung gestartet werden. Ein Teil dieser Prüfungen wird bei jedem Start des SCM-Managers ausgeführt.
|
||||||
|
|
||||||
Werden bei einer dieser Integritätsprüfungen Fehler gefunden, wird auf der Repository-Übersicht sowie auf den
|
Werden bei einer dieser Integritätsprüfungen Fehler gefunden, wird auf der Repository-Übersicht sowie auf den
|
||||||
Detailseiten zum Repository neben dem Namen ein Tag „fehlerhaft" angezeigt. In den Einstellungen wird zudem eine Meldung
|
Detailseiten zum Repository neben dem Namen ein Tag „fehlerhaft“ angezeigt. In den Einstellungen wird zudem eine Meldung
|
||||||
eingeblendet. Durch Klick auf diese Meldung oder die Tags wird ein Popup mit weiteren Details angezeigt.
|
eingeblendet. Durch Klick auf diese Meldung oder die Tags wird ein Popup mit weiteren Details angezeigt.
|
||||||
|
|
||||||
Der Server führt immer nur eine Prüfung zur Zeit durch. Es können jedoch für mehrere Repositories Prüfungen in die
|
Der Server führt immer nur eine Prüfung zurzeit durch. Es können jedoch für mehrere Repositories Prüfungen in die
|
||||||
Warteschlange gestellt werden, die dann nacheinander durchgeführt werden.
|
Warteschlange gestellt werden, die dann nacheinander durchgeführt werden.
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
2
gradle/changelog/delete_confirm_dialog.yaml
Normal file
2
gradle/changelog/delete_confirm_dialog.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: added
|
||||||
|
description: Confirmation dialog before repository deletion
|
||||||
@@ -17047,6 +17047,7 @@ Array [
|
|||||||
hostess’s really oblong Infinite Improbability thing into the starship against which behavior accordance with
|
hostess’s 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
|
Kakrafoon humanoid undergarment ship powered by GPP-guided bowl of petunias nothing was frequently away incredibly
|
||||||
ordinary mob.
|
ordinary mob.
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
<footer
|
<footer
|
||||||
className="modal-card-foot"
|
className="modal-card-foot"
|
||||||
@@ -17195,6 +17196,7 @@ Array [
|
|||||||
hostess’s really oblong Infinite Improbability thing into the starship against which behavior accordance with
|
hostess’s 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
|
Kakrafoon humanoid undergarment ship powered by GPP-guided bowl of petunias nothing was frequently away incredibly
|
||||||
ordinary mob.
|
ordinary mob.
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
<footer
|
<footer
|
||||||
className="modal-card-foot"
|
className="modal-card-foot"
|
||||||
|
|||||||
@@ -26,16 +26,17 @@ type Button = {
|
|||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
onClick?: () => void | null;
|
onClick?: () => void | null;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message?: string;
|
||||||
buttons: Button[];
|
buttons: Button[];
|
||||||
close?: () => void;
|
close?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ConfirmAlert: FC<Props> = ({ title, message, buttons, close }) => {
|
export const ConfirmAlert: FC<Props> = ({ title, message, buttons, close, children }) => {
|
||||||
const [showModal, setShowModal] = useState(true);
|
const [showModal, setShowModal] = useState(true);
|
||||||
const initialFocusButton = useRef<HTMLButtonElement>(null);
|
const initialFocusButton = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
@@ -54,7 +55,11 @@ export const ConfirmAlert: FC<Props> = ({ title, message, buttons, close }) => {
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const body = <>{message}</>;
|
const body = (
|
||||||
|
<>
|
||||||
|
{message} {children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const footer = (
|
const footer = (
|
||||||
<div className="field is-grouped">
|
<div className="field is-grouped">
|
||||||
@@ -63,8 +68,9 @@ export const ConfirmAlert: FC<Props> = ({ title, message, buttons, close }) => {
|
|||||||
<button
|
<button
|
||||||
className={classNames("button", button.className, button.isLoading ? "is-loading" : "")}
|
className={classNames("button", button.className, button.isLoading ? "is-loading" : "")}
|
||||||
key={index}
|
key={index}
|
||||||
|
disabled={button.disabled}
|
||||||
onClick={() => handleClickButton(button)}
|
onClick={() => handleClickButton(button)}
|
||||||
onKeyDown={e => e.key === "Enter" && handleClickButton(button)}
|
onKeyDown={(e) => e.key === "Enter" && handleClickButton(button)}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
ref={button.autofocus ? initialFocusButton : undefined}
|
ref={button.autofocus ? initialFocusButton : undefined}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -18,9 +18,11 @@ import React, { ReactNode } from "react";
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { createVariantClass, Variant } from "../../variants";
|
import { createVariantClass, Variant } from "../../variants";
|
||||||
|
|
||||||
type Props = { variant?: Variant; className?: string; children?: ReactNode };
|
type Props = { id?: string; variant?: Variant; className?: string; children?: ReactNode };
|
||||||
|
|
||||||
const FieldMessage = ({ variant, className, children }: Props) => (
|
const FieldMessage = ({ id, variant, className, children }: Props) => (
|
||||||
<p className={classNames("help", createVariantClass(variant), className)}>{children}</p>
|
<p id={id} className={classNames("help", createVariantClass(variant), className)}>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
);
|
);
|
||||||
export default FieldMessage;
|
export default FieldMessage;
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
|
|||||||
const inputId = useAriaId(id ?? props.testId);
|
const inputId = useAriaId(id ?? props.testId);
|
||||||
const helpTextId = helpText ? `input-helptext-${name}` : undefined;
|
const helpTextId = helpText ? `input-helptext-${name}` : undefined;
|
||||||
const descriptionId = descriptionText ? `input-description-${name}` : undefined;
|
const descriptionId = descriptionText ? `input-description-${name}` : undefined;
|
||||||
|
const errorId = error ? `input-error-${name}` : undefined;
|
||||||
const variant = error ? "danger" : undefined;
|
const variant = error ? "danger" : undefined;
|
||||||
return (
|
return (
|
||||||
<Field className={className}>
|
<Field className={className}>
|
||||||
@@ -74,7 +75,11 @@ const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
|
|||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</Control>
|
</Control>
|
||||||
{error ? <FieldMessage variant={variant}>{error}</FieldMessage> : null}
|
{error ? (
|
||||||
|
<FieldMessage id={errorId} variant={variant}>
|
||||||
|
{error}
|
||||||
|
</FieldMessage>
|
||||||
|
) : null}
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,6 +172,8 @@
|
|||||||
"confirmAlert": {
|
"confirmAlert": {
|
||||||
"title": "Branch löschen",
|
"title": "Branch löschen",
|
||||||
"message": "Möchten Sie den Branch \"{{branch}}\" wirklich löschen?",
|
"message": "Möchten Sie den Branch \"{{branch}}\" wirklich löschen?",
|
||||||
|
"typeNameConfirmation": "Bitte geben Sie \"{{confirmationText}}\" ein, um Ihre Entscheidung zu bestätigen.",
|
||||||
|
"inputNotMatching": "Die Eingabe stimmt nicht überein.",
|
||||||
"cancel": "Nein",
|
"cancel": "Nein",
|
||||||
"submit": "Ja"
|
"submit": "Ja"
|
||||||
}
|
}
|
||||||
@@ -515,6 +517,8 @@
|
|||||||
"confirmAlert": {
|
"confirmAlert": {
|
||||||
"title": "Repository löschen",
|
"title": "Repository löschen",
|
||||||
"message": "Soll das Repository wirklich gelöscht werden?",
|
"message": "Soll das Repository wirklich gelöscht werden?",
|
||||||
|
"confirmationText": "Geben Sie \"{{confirmationText}}\" ein, um das Löschen des Repositories zu bestätigen.",
|
||||||
|
"inputNotMatching": "Die Eingabe stimmt nicht überein.",
|
||||||
"submit": "Ja",
|
"submit": "Ja",
|
||||||
"cancel": "Nein"
|
"cancel": "Nein"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -515,6 +515,8 @@
|
|||||||
"confirmAlert": {
|
"confirmAlert": {
|
||||||
"title": "Delete Repository",
|
"title": "Delete Repository",
|
||||||
"message": "Do you really want to delete the repository?",
|
"message": "Do you really want to delete the repository?",
|
||||||
|
"confirmationText": "Enter \"{{confirmationText}}\" to confirm the deletion of the repository.",
|
||||||
|
"inputNotMatching": "The input doesn't match.",
|
||||||
"submit": "Yes",
|
"submit": "Yes",
|
||||||
"cancel": "No"
|
"cancel": "No"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Repository } from "@scm-manager/ui-types";
|
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 { useDeleteRepository } from "@scm-manager/ui-api";
|
import { useDeleteRepository } from "@scm-manager/ui-api";
|
||||||
|
import { InputField } from "@scm-manager/ui-core";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -31,9 +32,12 @@ const DeleteRepo: FC<Props> = ({ repository, confirmDialog = true }) => {
|
|||||||
const { isLoading, error, remove, isDeleted } = useDeleteRepository({
|
const { isLoading, error, remove, isDeleted } = useDeleteRepository({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
history.push("/repos/");
|
history.push("/repos/");
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
|
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
|
||||||
|
const [enteredValue, setEnteredValue] = useState("");
|
||||||
|
const [inputError, setInputError] = useState();
|
||||||
|
const [errorId, setErrorId] = useState(error ? "input-error-delete" : undefined);
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
@@ -47,29 +51,60 @@ const DeleteRepo: FC<Props> = ({ repository, confirmDialog = true }) => {
|
|||||||
|
|
||||||
const confirmDelete = () => {
|
const confirmDelete = () => {
|
||||||
setShowConfirmAlert(true);
|
setShowConfirmAlert(true);
|
||||||
|
setEnteredValue("");
|
||||||
|
setInputError(undefined);
|
||||||
|
setErrorId(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setEnteredValue(e.target.value);
|
||||||
|
if (isInvalidEnteredValue && !inputError) {
|
||||||
|
setInputError(t("deleteRepo.confirmAlert.inputNotMatching"));
|
||||||
|
setErrorId("input-error-delete");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = confirmDialog ? confirmDelete : deleteRepoCallback;
|
const action = confirmDialog ? confirmDelete : deleteRepoCallback;
|
||||||
|
|
||||||
|
const confirmationText = `${repository.namespace}/${repository.name}`;
|
||||||
|
const isInvalidEnteredValue = enteredValue !== confirmationText;
|
||||||
|
|
||||||
|
const confirmationDialog = (
|
||||||
|
<>
|
||||||
|
<InputField
|
||||||
|
name="delete"
|
||||||
|
label={t("deleteRepo.confirmAlert.confirmationText", { confirmationText: confirmationText })}
|
||||||
|
value={enteredValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
error={isInvalidEnteredValue ? inputError : undefined}
|
||||||
|
aria-describedby={errorId}
|
||||||
|
aria-invalid={errorId !== undefined}
|
||||||
|
autoComplete="off"
|
||||||
|
></InputField>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
if (showConfirmAlert) {
|
if (showConfirmAlert) {
|
||||||
return (
|
return (
|
||||||
<ConfirmAlert
|
<ConfirmAlert
|
||||||
title={t("deleteRepo.confirmAlert.title")}
|
title={t("deleteRepo.confirmAlert.title")}
|
||||||
message={t("deleteRepo.confirmAlert.message")}
|
|
||||||
buttons={[
|
buttons={[
|
||||||
{
|
{
|
||||||
label: t("deleteRepo.confirmAlert.submit"),
|
label: t("deleteRepo.confirmAlert.submit"),
|
||||||
onClick: () => deleteRepoCallback()
|
onClick: () => deleteRepoCallback(),
|
||||||
|
disabled: isInvalidEnteredValue,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
className: "is-info",
|
className: "is-info",
|
||||||
label: t("deleteRepo.confirmAlert.cancel"),
|
label: t("deleteRepo.confirmAlert.cancel"),
|
||||||
onClick: () => null,
|
onClick: () => null,
|
||||||
autofocus: true
|
autofocus: true,
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
close={() => setShowConfirmAlert(false)}
|
close={() => setShowConfirmAlert(false)}
|
||||||
/>
|
>
|
||||||
|
{confirmationDialog}
|
||||||
|
</ConfirmAlert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user