Skip to content

Commit dbf2d8f

Browse files
committed
Add UI changes to accomodate fileset Choice using group_with.
1 parent 44e202e commit dbf2d8f

17 files changed

+905
-756
lines changed

Diff for: app/assets/js/components/Work/Fileset/ActionButtons/Access.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function MediaButtons({ fileSet }) {
5050

5151
return (
5252
<div className="buttons is-grouped is-right">
53-
{isAuthorized() && (
53+
{!fileSet.group_with && isAuthorized() && (
5454
<Button
5555
data-testid="edit-structure-button"
5656
onClick={() =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/** @jsx jsx */
2+
3+
import React, { useEffect } from "react";
4+
import { css, jsx } from "@emotion/react";
5+
6+
const WorkFilesetActionButtonsGroupAdd = ({
7+
fileSetId,
8+
candidateFileSets,
9+
handleUpdateFileSet,
10+
iiifServerUrl,
11+
}) => {
12+
const addRef = React.useRef(null);
13+
const inputRef = React.useRef(null);
14+
const [isOpen, setIsOpen] = React.useState(false);
15+
const [filteredCandidateFileSets, setFilteredCandidateFileSets] =
16+
React.useState(candidateFileSets);
17+
18+
useEffect(() => {
19+
const handleClick = (e) => {
20+
if (addRef.current.contains(e.target)) {
21+
return;
22+
}
23+
24+
setIsOpen(false);
25+
};
26+
27+
const handleEscape = (e) => {
28+
if (e.key === "Escape") {
29+
setIsOpen(false);
30+
}
31+
};
32+
33+
document.addEventListener("click", handleClick);
34+
document.addEventListener("keydown", handleEscape);
35+
36+
return () => {
37+
document.removeEventListener("click", handleClick);
38+
document.removeEventListener("keydown", handleEscape);
39+
};
40+
}, []);
41+
42+
useEffect(() => {
43+
setFilteredCandidateFileSets(candidateFileSets);
44+
}, [candidateFileSets]);
45+
46+
const handleFocus = () => {
47+
setIsOpen(!isOpen);
48+
inputRef.current.style.width = "300px";
49+
};
50+
51+
const handleBlur = () => {
52+
inputRef.current.style.width = "150px";
53+
};
54+
55+
const handleChange = (e) => {
56+
const value = e.target.value.toLowerCase().normalize();
57+
if (value === "") {
58+
setFilteredCandidateFileSets(candidateFileSets);
59+
return;
60+
}
61+
62+
// get the filtered filesets matching label or accession number
63+
const filtered = candidateFileSets.filter(
64+
(candidate) =>
65+
candidate.coreMetadata.label
66+
.toLowerCase()
67+
.normalize()
68+
.includes(value) ||
69+
candidate.accessionNumber.toLowerCase().normalize().includes(value),
70+
);
71+
72+
setFilteredCandidateFileSets(filtered);
73+
};
74+
75+
const handleOnClick = (candidateId) => {
76+
handleUpdateFileSet(candidateId, fileSetId);
77+
setIsOpen(false);
78+
};
79+
80+
const add = css`
81+
position: relative;
82+
z-index: ${isOpen ? 3 : 1};
83+
84+
input {
85+
width: 150px;
86+
transition: width 0.25s ease;
87+
}
88+
89+
input::placeholder {
90+
color: var(--colors-richBlack50) !important;
91+
}
92+
`;
93+
94+
const content = css`
95+
position: absolute;
96+
right: 0;
97+
z-index: 2;
98+
display: ${isOpen ? "block" : "none"};
99+
background: white;
100+
width: 300px;
101+
max-height: 300px;
102+
transition: all 0.25s ease;
103+
overflow-x: hidden;
104+
overflow-y: scroll;};
105+
padding: 0.5rem;
106+
107+
button.candidate-option {
108+
display: flex;
109+
width: 100%;
110+
align-items: flex-start;
111+
background: transparent;
112+
border: none;
113+
font-family: var(--fonts-sans);
114+
cursor: pointer;
115+
gap: 0.5rem;
116+
text-transform: none;
117+
margin-bottom: 0.25rem;
118+
padding: 0.5rem;
119+
font-size: 1rem;
120+
121+
div {
122+
display: flex;
123+
flex-direction: column;
124+
align-items: flex-start;
125+
gap: 0.25rem;
126+
overflow: hidden;
127+
flex-grow: 1;
128+
text-align: left;
129+
font-size: 0.8333rem;
130+
131+
label,
132+
span {
133+
width: 100%;
134+
overflow: hidden;
135+
text-overflow: ellipsis;
136+
white-space: nowrap;
137+
}
138+
}
139+
140+
&:hover,
141+
&:focus {
142+
background: #f0f0f0;
143+
144+
label {
145+
font-family: var(--fonts-sansBold);
146+
font-weight: 400;
147+
}
148+
}
149+
150+
&:last-of-type {
151+
margin-bottom: 0;
152+
}
153+
154+
figure {
155+
width: 32px;
156+
height: 32px;
157+
flex-shrink: 0;
158+
159+
img {
160+
width: 100%;
161+
height: 100%;
162+
object-fit: cover;
163+
border-radius: 0.25rem;
164+
}
165+
}
166+
}
167+
`;
168+
169+
return (
170+
<div ref={addRef} css={add} data-testid="fileset-group-add">
171+
<input
172+
ref={inputRef}
173+
className="input"
174+
type="text"
175+
placeholder="Attach filesets..."
176+
aria-haspopup="true"
177+
aria-controls="searchbox"
178+
onFocus={handleFocus}
179+
onBlur={handleBlur}
180+
onChange={handleChange}
181+
/>
182+
<div
183+
className="box"
184+
css={content}
185+
role="searchbox"
186+
aria-expanded={isOpen}
187+
>
188+
{filteredCandidateFileSets.length ? (
189+
filteredCandidateFileSets.map((candidate) => (
190+
<button
191+
key={candidate.id}
192+
value={candidate.id}
193+
className="candidate-option"
194+
data-testid="fileset-group-add-candidate"
195+
type="button"
196+
onClick={() => handleOnClick(candidate.id, fileSetId)}
197+
>
198+
<figure>
199+
<img
200+
src={`${iiifServerUrl}${candidate.id}/square/32,32/0/default.jpg`}
201+
placeholder="Fileset Image"
202+
data-testid="fileset-image"
203+
/>
204+
</figure>
205+
<div>
206+
<label>{candidate.coreMetadata.label}</label>
207+
<span className="is-muted">{candidate.accessionNumber}</span>
208+
</div>
209+
</button>
210+
))
211+
) : (
212+
<p>Applicable fileset(s) not found.</p>
213+
)}
214+
</div>
215+
</div>
216+
);
217+
};
218+
219+
export default WorkFilesetActionButtonsGroupAdd;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React from "react";
2+
import { render, screen, waitFor } from "@testing-library/react";
3+
import { mockFileSets } from "@js/mock-data/filesets";
4+
import userEvent from "@testing-library/user-event";
5+
import { WorkProvider } from "@js/context/work-context";
6+
import WorkFilesetActionButtonsGroupAdd from "./GroupAdd";
7+
8+
const mockUpdateFileSetFn = jest.fn();
9+
10+
const fileSet = mockFileSets[0];
11+
const candidateFileSets = [mockFileSets[1], mockFileSets[2]];
12+
13+
describe("WorkFilesetActionButtonsGroupAdd component", () => {
14+
beforeEach(() => {
15+
render(
16+
<WorkProvider>
17+
<WorkFilesetActionButtonsGroupAdd
18+
fileSetId={fileSet.id}
19+
candidateFileSets={candidateFileSets}
20+
handleUpdateFileSet={mockUpdateFileSetFn}
21+
iiifServerUrl="http://example.org/iiif/"
22+
/>
23+
</WorkProvider>,
24+
);
25+
});
26+
27+
it("renders the FileSet GroupAdd component", async () => {
28+
const groupAdd = await screen.findByTestId("fileset-group-add");
29+
expect(groupAdd).toBeInTheDocument();
30+
31+
// renders and input
32+
const input = groupAdd.querySelector("input");
33+
expect(input.getAttribute("placeholder")).toBe("Attach filesets...");
34+
35+
// renders the searchbox expanded as false
36+
const searchbox = groupAdd.querySelector("div[role='searchbox']");
37+
expect(searchbox.getAttribute("aria-expanded")).toBe("false");
38+
});
39+
40+
it("renders the handles user interactions", async () => {
41+
const groupAdd = await screen.findByTestId("fileset-group-add");
42+
const input = groupAdd.querySelector("input");
43+
const searchbox = groupAdd.querySelector("div[role='searchbox']");
44+
45+
// focus the input and type
46+
const user = userEvent.setup();
47+
await user.click(input);
48+
49+
// expands the searchbox on focus
50+
expect(searchbox.getAttribute("aria-expanded")).toBe("true");
51+
52+
// renders candidate options
53+
const candidatesDefaultState = await screen.findAllByTestId(
54+
"fileset-group-add-candidate",
55+
);
56+
expect(candidatesDefaultState).toHaveLength(2);
57+
58+
// users types in the input to filter
59+
await user.type(input, "2572813");
60+
const candidatesFiltered = await screen.findAllByTestId(
61+
"fileset-group-add-candidate",
62+
);
63+
expect(candidatesFiltered).toHaveLength(1);
64+
65+
const filteredCandidate = candidatesFiltered[0];
66+
expect(filteredCandidate).toHaveTextContent(
67+
"inu-dil-41913a91-037f-494b-9113-06004a8a98fb.jpg",
68+
);
69+
expect(filteredCandidate).toHaveTextContent("Voyager:2572813_FILE_0");
70+
expect(filteredCandidate.querySelector("img")).toHaveAttribute(
71+
"src",
72+
"http://example.org/iiif/109b9a5c-3c6f-4a98-b98b-12402b871dc7/square/32,32/0/default.jpg",
73+
);
74+
75+
// click the candidate to add
76+
await user.click(filteredCandidate);
77+
expect(mockUpdateFileSetFn).toHaveBeenCalledWith(
78+
"109b9a5c-3c6f-4a98-b98b-12402b871dc7",
79+
fileSet.id,
80+
);
81+
});
82+
83+
it("renders message if no applicable candidates are found", async () => {
84+
const groupAdd = await screen.findByTestId("fileset-group-add");
85+
const input = groupAdd.querySelector("input");
86+
const searchbox = groupAdd.querySelector("div[role='searchbox']");
87+
88+
// focus the input and type
89+
const user = userEvent.setup();
90+
await user.click(input);
91+
92+
// renders searchbox expanded, with 0 candidates, and a message
93+
await user.type(input, "foo bar");
94+
await waitFor(() => {
95+
expect(
96+
screen.queryAllByTestId("fileset-group-add-candidate"),
97+
).toHaveLength(0);
98+
});
99+
expect(searchbox).toHaveTextContent("Applicable fileset(s) not found.");
100+
});
101+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/** @jsx jsx */
2+
3+
import React from "react";
4+
import { css, jsx } from "@emotion/react";
5+
6+
const WorkFilesetActionButtonsGroupRemove = ({
7+
fileSetId,
8+
handleUpdateFileSet,
9+
}) => {
10+
const button = css`
11+
color: var(--colors-richBlack50);
12+
text-transform: none;
13+
text-decoration: underline;
14+
`;
15+
16+
const handleRemoveClick = () => {
17+
handleUpdateFileSet(fileSetId, null);
18+
};
19+
20+
return (
21+
<button
22+
className="button is-text"
23+
css={button}
24+
onClick={handleRemoveClick}
25+
data-testid="fileset-group-remove"
26+
>
27+
Detach
28+
</button>
29+
);
30+
};
31+
32+
export default WorkFilesetActionButtonsGroupRemove;

0 commit comments

Comments
 (0)