feat(web): inline status-aware errors on term/authority edit rows + delete dialog (#63)
This commit is contained in:
@@ -441,7 +441,7 @@ export function useUpdateTerm() {
|
|||||||
if (response.status !== 204) throw new HttpError(response.status);
|
if (response.status !== 204) throw new HttpError(response.status);
|
||||||
},
|
},
|
||||||
onSuccess: (_d, { vocabularyId }) => qc.invalidateQueries({ queryKey: ["terms", vocabularyId] }),
|
onSuccess: (_d, { vocabularyId }) => qc.invalidateQueries({ queryKey: ["terms", vocabularyId] }),
|
||||||
meta: { successMessage: "toast.saved" },
|
meta: { successMessage: "toast.saved", suppressErrorToast: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,7 +518,7 @@ export function useUpdateAuthority() {
|
|||||||
if (response.status !== 204) throw new HttpError(response.status);
|
if (response.status !== 204) throw new HttpError(response.status);
|
||||||
},
|
},
|
||||||
onSuccess: (_d, { kind }) => qc.invalidateQueries({ queryKey: ["authorities", kind] }),
|
onSuccess: (_d, { kind }) => qc.invalidateQueries({ queryKey: ["authorities", kind] }),
|
||||||
meta: { successMessage: "toast.saved" },
|
meta: { successMessage: "toast.saved", suppressErrorToast: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { components } from "../api/schema";
|
|||||||
import { useUpdateAuthority, useDeleteAuthority } from "../api/queries";
|
import { useUpdateAuthority, useDeleteAuthority } from "../api/queries";
|
||||||
import { LabelEditor } from "../components/label-editor";
|
import { LabelEditor } from "../components/label-editor";
|
||||||
import { DeleteConfirmDialog } from "../components/delete-confirm-dialog";
|
import { DeleteConfirmDialog } from "../components/delete-confirm-dialog";
|
||||||
|
import { MutationError } from "../components/mutation-error";
|
||||||
import { ExternalUriLink } from "../components/external-uri-link";
|
import { ExternalUriLink } from "../components/external-uri-link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -56,6 +57,7 @@ export function AuthorityRow({ authority, kind, lang }: { authority: AuthorityVi
|
|||||||
{t("form.cancel")}
|
{t("form.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<MutationError error={updateAuthority.error} />
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -71,6 +73,7 @@ export function AuthorityRow({ authority, kind, lang }: { authority: AuthorityVi
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
updateAuthority.reset();
|
||||||
setLabels(authority.labels as LabelInput[]);
|
setLabels(authority.labels as LabelInput[]);
|
||||||
setUri(authority.external_uri ?? "");
|
setUri(authority.external_uri ?? "");
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { InUseError } from "../api/queries";
|
import { errorMessageKey } from "../api/error-message";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
@@ -37,7 +37,8 @@ export function DeleteConfirmDialog({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Keep the dialog open; show the blocking reason. Never let the rejected
|
// Keep the dialog open; show the blocking reason. Never let the rejected
|
||||||
// mutation escape as an unhandled rejection.
|
// mutation escape as an unhandled rejection.
|
||||||
setMessage(err instanceof InUseError ? t("actions.inUse", { count: err.count }) : t("form.rejected"));
|
const { key, opts } = errorMessageKey(err);
|
||||||
|
setMessage(t(key, opts));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { expect, test } from "vitest";
|
||||||
|
import { screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { http, HttpResponse } from "msw";
|
||||||
|
|
||||||
|
import type { TermView } from "../test/fixtures";
|
||||||
|
import { server } from "../test/server";
|
||||||
|
import { renderApp } from "../test/render";
|
||||||
|
import { TermRow } from "./term-row";
|
||||||
|
|
||||||
|
const term: TermView = {
|
||||||
|
id: "t1",
|
||||||
|
external_uri: null,
|
||||||
|
labels: [{ lang: "en", label: "Bronze" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
test("a failed term update shows an inline error and keeps the row editable", async () => {
|
||||||
|
server.use(
|
||||||
|
http.patch("/api/admin/vocabularies/:id/terms/:term_id", () => new HttpResponse(null, { status: 403 })),
|
||||||
|
);
|
||||||
|
renderApp(
|
||||||
|
<ul>
|
||||||
|
<TermRow vocabularyId="v1" term={term} lang="en" />
|
||||||
|
</ul>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: /edit/i }));
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: /save/i }));
|
||||||
|
|
||||||
|
expect(await screen.findByRole("alert")).toHaveTextContent(/permission/i);
|
||||||
|
expect(screen.getByRole("button", { name: /save/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("re-entering edit after a failure clears the stale error", async () => {
|
||||||
|
server.use(
|
||||||
|
http.patch("/api/admin/vocabularies/:id/terms/:term_id", () => new HttpResponse(null, { status: 403 })),
|
||||||
|
);
|
||||||
|
renderApp(
|
||||||
|
<ul>
|
||||||
|
<TermRow vocabularyId="v1" term={term} lang="en" />
|
||||||
|
</ul>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: /edit/i }));
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: /save/i }));
|
||||||
|
expect(await screen.findByRole("alert")).toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: /cancel/i }));
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: /edit/i }));
|
||||||
|
|
||||||
|
expect(screen.queryByRole("alert")).toBeNull();
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ import type { components } from "../api/schema";
|
|||||||
import { useUpdateTerm, useDeleteTerm } from "../api/queries";
|
import { useUpdateTerm, useDeleteTerm } from "../api/queries";
|
||||||
import { LabelEditor } from "../components/label-editor";
|
import { LabelEditor } from "../components/label-editor";
|
||||||
import { DeleteConfirmDialog } from "../components/delete-confirm-dialog";
|
import { DeleteConfirmDialog } from "../components/delete-confirm-dialog";
|
||||||
|
import { MutationError } from "../components/mutation-error";
|
||||||
import { ExternalUriLink } from "../components/external-uri-link";
|
import { ExternalUriLink } from "../components/external-uri-link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -50,6 +51,7 @@ export function TermRow({ vocabularyId, term, lang }: { vocabularyId: string; te
|
|||||||
{t("form.cancel")}
|
{t("form.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<MutationError error={updateTerm.error} />
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -65,6 +67,7 @@ export function TermRow({ vocabularyId, term, lang }: { vocabularyId: string; te
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
updateTerm.reset();
|
||||||
setLabels(term.labels as LabelInput[]);
|
setLabels(term.labels as LabelInput[]);
|
||||||
setUri(term.external_uri ?? "");
|
setUri(term.external_uri ?? "");
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user