Skip to content

Commit f2ec518

Browse files
authored
Merge pull request #5009 from nulib/deploy/staging
Deploy v9.12.0 to production
2 parents fde911d + dc038e9 commit f2ec518

File tree

16 files changed

+1574
-1067
lines changed

16 files changed

+1574
-1067
lines changed

app/assets/js/components/Work/Tabs/Structure/Transcription/Modal.jsx

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import React, { useContext, useEffect, useState } from "react";
22
import PropTypes from "prop-types";
33
import CloverImage from "@samvera/clover-iiif/image";
4-
import { Button } from "@nulib/design-system";
4+
import { Button, Notification } from "@nulib/design-system";
55
import classNames from "classnames";
66
import { useWorkDispatch, useWorkState } from "@js/context/work-context";
77
import { IIIFContext } from "@js/components/IIIF/IIIFProvider";
88
import { IIIF_SIZES } from "@js/services/global-vars";
99
import { toastWrapper } from "@js/services/helpers";
1010
import { useMutation } from "@apollo/client";
11-
import { UPDATE_FILE_SET_ANNOTATION } from "@js/components/Work/Tabs/Structure/Transcription/transcription.gql";
11+
import {
12+
UPDATE_FILE_SET_ANNOTATION,
13+
DELETE_FILE_SET_ANNOTATION,
14+
} from "@js/components/Work/Tabs/Structure/Transcription/transcription.gql";
1215
import WorkTabsStructureTranscriptionWorkflow from "@js/components/Work/Tabs/Structure/Transcription/Workflow";
1316

1417
function WorkTabsStructureTranscriptionModal({ isActive }) {
1518
const [textArea, setTextArea] = useState();
19+
const [confirmDelete, setConfirmDelete] = useState(false);
1620

1721
const dispatch = useWorkDispatch();
1822
const iiifServerUrl = useContext(IIIFContext);
@@ -24,6 +28,7 @@ function WorkTabsStructureTranscriptionModal({ isActive }) {
2428
const iiifImageUrl = `${iiifServerUrl}${fileSetId}${IIIF_SIZES.IIIF_FULL}`;
2529

2630
const [updateFileSetAnnotation] = useMutation(UPDATE_FILE_SET_ANNOTATION);
31+
const [deleteFileSetAnnotation] = useMutation(DELETE_FILE_SET_ANNOTATION);
2732

2833
useEffect(() => {
2934
const textAreaElement = document.getElementById(
@@ -33,6 +38,7 @@ function WorkTabsStructureTranscriptionModal({ isActive }) {
3338
}, [isActive]);
3439

3540
const handleClose = () => {
41+
setConfirmDelete(false);
3642
dispatch({
3743
type: "toggleTranscriptionModal",
3844
fileSetId: null,
@@ -61,6 +67,37 @@ function WorkTabsStructureTranscriptionModal({ isActive }) {
6167
});
6268
};
6369

70+
const handleDeleteTranscription = () => {
71+
const annotationId = textArea?.getAttribute("data-annotation-id");
72+
73+
if (!annotationId) {
74+
toastWrapper("is-danger", "No transcription to delete");
75+
return;
76+
}
77+
78+
setConfirmDelete(true);
79+
};
80+
81+
const handleConfirmDelete = () => {
82+
const annotationId = textArea?.getAttribute("data-annotation-id");
83+
84+
deleteFileSetAnnotation({
85+
variables: {
86+
annotationId: annotationId,
87+
},
88+
onCompleted: () => {
89+
handleClose();
90+
toastWrapper("is-success", "Transcription successfully deleted");
91+
},
92+
onError: (error) => {
93+
toastWrapper(
94+
"is-danger",
95+
"Error deleting transcription: " + error.message,
96+
);
97+
},
98+
});
99+
};
100+
64101
const handleHasTranscriptionCallback = () => {
65102
const textAreaElement = document.getElementById(
66103
"file-set-transcription-textarea",
@@ -85,9 +122,16 @@ function WorkTabsStructureTranscriptionModal({ isActive }) {
85122
</header>
86123

87124
<section className="modal-card-body">
125+
{confirmDelete && (
126+
<Notification isDanger>
127+
Are you sure you want to delete this transcription? This action
128+
cannot be undone.
129+
</Notification>
130+
)}
131+
88132
<div
89133
className="is-flex is-justify-content-space-between"
90-
style={{ gap: "1rem" }}
134+
style={{ gap: "1rem", display: confirmDelete ? "none" : "flex" }}
91135
>
92136
<div style={{ width: "50%", flexShrink: 0 }}>
93137
<div
@@ -118,15 +162,28 @@ function WorkTabsStructureTranscriptionModal({ isActive }) {
118162
</section>
119163

120164
<footer className="modal-card-foot buttons is-justify-content-space-between">
165+
{!confirmDelete && hasTextArea && (
166+
<div>
167+
<Button isDanger onClick={handleDeleteTranscription}>
168+
Delete Transcription
169+
</Button>
170+
</div>
171+
)}
121172
<div className="is-flex is-justify-content-flex-end is-flex-grow-1">
122173
<Button onClick={handleClose}>Cancel</Button>
123-
<Button
124-
isPrimary
125-
disabled={!hasTextArea}
126-
onClick={handleSaveTranscription}
127-
>
128-
Save
129-
</Button>
174+
{confirmDelete ? (
175+
<Button isPrimary onClick={handleConfirmDelete}>
176+
Yes, delete
177+
</Button>
178+
) : (
179+
<Button
180+
isPrimary
181+
disabled={!hasTextArea}
182+
onClick={handleSaveTranscription}
183+
>
184+
Save
185+
</Button>
186+
)}
130187
</div>
131188
</footer>
132189
</div>

app/assets/js/components/Work/Tabs/Structure/Transcription/Modal.test.jsx

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import { IIIFContext } from "@js/components/IIIF/IIIFProvider";
66
import { useWorkDispatch, useWorkState } from "@js/context/work-context";
77
import { toastWrapper } from "@js/services/helpers";
88
import WorkTabsStructureTranscriptionModal from "./Modal";
9-
import { UPDATE_FILE_SET_ANNOTATION } from "./transcription.gql";
9+
import {
10+
UPDATE_FILE_SET_ANNOTATION,
11+
DELETE_FILE_SET_ANNOTATION,
12+
} from "./transcription.gql";
1013

1114
// --- Mocks ---
1215

@@ -18,11 +21,12 @@ jest.mock("@samvera/clover-iiif/image", () => {
1821
};
1922
});
2023

21-
// Simple stub for Button
24+
// Simple stub for Button and Notification
2225
jest.mock("@nulib/design-system", () => {
2326
const React = require("react");
2427
return {
2528
Button: ({ children, ...props }) => <button {...props}>{children}</button>,
29+
Notification: ({ children, ...props }) => <div {...props}>{children}</div>,
2630
};
2731
});
2832

@@ -197,4 +201,98 @@ describe("WorkTabsStructureTranscriptionModal", () => {
197201
fileSetId: null,
198202
});
199203
});
204+
205+
it("calls delete mutation and closes modal on successful delete", async () => {
206+
const mutationMocks = [
207+
{
208+
request: {
209+
query: DELETE_FILE_SET_ANNOTATION,
210+
variables: {
211+
annotationId: "ann-1",
212+
},
213+
},
214+
result: {
215+
data: {
216+
deleteFileSetAnnotation: {
217+
id: "ann-1",
218+
fileSetId: "fs-123",
219+
},
220+
},
221+
},
222+
},
223+
];
224+
225+
renderModal({ mocks: mutationMocks });
226+
227+
const deleteButton = await screen.findByRole("button", {
228+
name: /delete transcription/i,
229+
});
230+
231+
// Click delete to show confirmation
232+
fireEvent.click(deleteButton);
233+
234+
// Verify confirmation notification appears
235+
expect(
236+
screen.getByText(/are you sure you want to delete this transcription/i),
237+
).toBeInTheDocument();
238+
239+
// Click "Yes, delete" to confirm
240+
const confirmButton = await screen.findByRole("button", {
241+
name: /yes, delete/i,
242+
});
243+
fireEvent.click(confirmButton);
244+
245+
await waitFor(() => {
246+
expect(toastWrapper).toHaveBeenCalledWith(
247+
"is-success",
248+
"Transcription successfully deleted",
249+
);
250+
});
251+
252+
expect(mockDispatch).toHaveBeenCalledWith({
253+
type: "toggleTranscriptionModal",
254+
fileSetId: null,
255+
});
256+
});
257+
258+
it("shows error toast when delete mutation fails", async () => {
259+
const mutationMocks = [
260+
{
261+
request: {
262+
query: DELETE_FILE_SET_ANNOTATION,
263+
variables: {
264+
annotationId: "ann-1",
265+
},
266+
},
267+
error: new Error("Delete failed"),
268+
},
269+
];
270+
271+
renderModal({ mocks: mutationMocks });
272+
273+
const deleteButton = await screen.findByRole("button", {
274+
name: /delete transcription/i,
275+
});
276+
277+
// Click delete to show confirmation
278+
fireEvent.click(deleteButton);
279+
280+
// Click "Yes, delete" to confirm
281+
const confirmButton = await screen.findByRole("button", {
282+
name: /yes, delete/i,
283+
});
284+
fireEvent.click(confirmButton);
285+
286+
await waitFor(() => {
287+
expect(toastWrapper).toHaveBeenCalledWith(
288+
"is-danger",
289+
"Error deleting transcription: Delete failed",
290+
);
291+
});
292+
293+
expect(mockDispatch).not.toHaveBeenCalledWith({
294+
type: "toggleTranscriptionModal",
295+
fileSetId: null,
296+
});
297+
});
200298
});

app/assets/js/components/Work/Tabs/Structure/Transcription/transcription.gql.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,12 @@ export const UPDATE_FILE_SET_ANNOTATION = gql`
6161
}
6262
}
6363
`;
64+
65+
export const DELETE_FILE_SET_ANNOTATION = gql`
66+
mutation deleteFileSetAnnotation($annotationId: ID!) {
67+
deleteFileSetAnnotation(annotationId: $annotationId) {
68+
id
69+
fileSetId
70+
}
71+
}
72+
`;

app/assets/js/context/code-list-context.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -168,18 +168,18 @@ function CodeListProvider({ children }) {
168168
visibilityData,
169169
isLoading: Boolean(
170170
behaviorLoading ||
171-
fileSetRoleLoading ||
172-
authorityLoading ||
173-
libraryUnitLoading ||
174-
licenseLoading ||
175-
marcLoading ||
176-
notesLoading ||
177-
preservationLevelLoading ||
178-
relatedUrlLoading ||
179-
rightsStatementLoading ||
180-
statusLoading ||
181-
subjectRoleLoading ||
182-
visibilityLoading,
171+
fileSetRoleLoading ||
172+
authorityLoading ||
173+
libraryUnitLoading ||
174+
licenseLoading ||
175+
marcLoading ||
176+
notesLoading ||
177+
preservationLevelLoading ||
178+
relatedUrlLoading ||
179+
rightsStatementLoading ||
180+
statusLoading ||
181+
subjectRoleLoading ||
182+
visibilityLoading,
183183
),
184184
}}
185185
>

app/assets/js/services/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function formatSimpleISODate(date: string) {
3131
export function isUrlValid(url: string) {
3232
return Boolean(
3333
url.match(URL_PATTERN_MATCH) &&
34-
URL_PATTERN_START.some((validStart) => url.startsWith(validStart)),
34+
URL_PATTERN_START.some((validStart) => url.startsWith(validStart)),
3535
);
3636
}
3737

0 commit comments

Comments
 (0)