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:
Till-André Diegeler
2025-07-24 13:27:04 +02:00
parent 31232d802f
commit 9c8c5ab9ce
10 changed files with 76 additions and 18 deletions

View File

@@ -62,8 +62,8 @@ Das gewählte Repository wird zum SCM-Manager hinzugefügt und sämtliche Reposi
![Repository importieren](assets/import-repository.png)
### Repository Informationen
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.
### Repositoryinformationen
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.
![Repository-Information](assets/repository-information.png)

View File

@@ -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.
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.
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.
![Repository-Settings-General-Health-Check](assets/repository-settings-general-health-check.png)

View File

@@ -0,0 +1,2 @@
- type: added
description: Confirmation dialog before repository deletion

View File

@@ -17047,6 +17047,7 @@ Array [
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"
@@ -17195,6 +17196,7 @@ Array [
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"

View File

@@ -26,16 +26,17 @@ type Button = {
isLoading?: boolean;
onClick?: () => void | null;
autofocus?: boolean;
disabled?: boolean;
};
type Props = {
title: string;
message: string;
message?: string;
buttons: Button[];
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 initialFocusButton = useRef<HTMLButtonElement>(null);
@@ -54,7 +55,11 @@ export const ConfirmAlert: FC<Props> = ({ title, message, buttons, close }) => {
onClose();
};
const body = <>{message}</>;
const body = (
<>
{message} {children}
</>
);
const footer = (
<div className="field is-grouped">
@@ -63,8 +68,9 @@ export const ConfirmAlert: FC<Props> = ({ title, message, buttons, close }) => {
<button
className={classNames("button", button.className, button.isLoading ? "is-loading" : "")}
key={index}
disabled={button.disabled}
onClick={() => handleClickButton(button)}
onKeyDown={e => e.key === "Enter" && handleClickButton(button)}
onKeyDown={(e) => e.key === "Enter" && handleClickButton(button)}
tabIndex={0}
ref={button.autofocus ? initialFocusButton : undefined}
>

View File

@@ -18,9 +18,11 @@ import React, { ReactNode } from "react";
import classNames from "classnames";
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) => (
<p className={classNames("help", createVariantClass(variant), className)}>{children}</p>
const FieldMessage = ({ id, variant, className, children }: Props) => (
<p id={id} className={classNames("help", createVariantClass(variant), className)}>
{children}
</p>
);
export default FieldMessage;

View File

@@ -46,6 +46,7 @@ const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
const inputId = useAriaId(id ?? props.testId);
const helpTextId = helpText ? `input-helptext-${name}` : undefined;
const descriptionId = descriptionText ? `input-description-${name}` : undefined;
const errorId = error ? `input-error-${name}` : undefined;
const variant = error ? "danger" : undefined;
return (
<Field className={className}>
@@ -74,7 +75,11 @@ const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
</span>
) : null}
</Control>
{error ? <FieldMessage variant={variant}>{error}</FieldMessage> : null}
{error ? (
<FieldMessage id={errorId} variant={variant}>
{error}
</FieldMessage>
) : null}
</Field>
);
}

View File

@@ -172,6 +172,8 @@
"confirmAlert": {
"title": "Branch 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",
"submit": "Ja"
}
@@ -515,6 +517,8 @@
"confirmAlert": {
"title": "Repository löschen",
"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",
"cancel": "Nein"
}

View File

@@ -515,6 +515,8 @@
"confirmAlert": {
"title": "Delete 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",
"cancel": "No"
}

View File

@@ -20,6 +20,7 @@ import { useTranslation } from "react-i18next";
import { Repository } from "@scm-manager/ui-types";
import { ConfirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { useDeleteRepository } from "@scm-manager/ui-api";
import { InputField } from "@scm-manager/ui-core";
type Props = {
repository: Repository;
@@ -31,9 +32,12 @@ const DeleteRepo: FC<Props> = ({ repository, confirmDialog = true }) => {
const { isLoading, error, remove, isDeleted } = useDeleteRepository({
onSuccess: () => {
history.push("/repos/");
}
},
});
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");
useEffect(() => {
if (isDeleted) {
@@ -47,29 +51,60 @@ const DeleteRepo: FC<Props> = ({ repository, confirmDialog = true }) => {
const confirmDelete = () => {
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 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) {
return (
<ConfirmAlert
title={t("deleteRepo.confirmAlert.title")}
message={t("deleteRepo.confirmAlert.message")}
buttons={[
{
label: t("deleteRepo.confirmAlert.submit"),
onClick: () => deleteRepoCallback()
onClick: () => deleteRepoCallback(),
disabled: isInvalidEnteredValue,
},
{
className: "is-info",
label: t("deleteRepo.confirmAlert.cancel"),
onClick: () => null,
autofocus: true
}
autofocus: true,
},
]}
close={() => setShowConfirmAlert(false)}
/>
>
{confirmationDialog}
</ConfirmAlert>
);
}