Skip to content

Commit 626da75

Browse files
Added autocomplete for programs on Generate form (#1605)
1 parent b22d9df commit 626da75

File tree

7 files changed

+74
-47
lines changed

7 files changed

+74
-47
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- Updated the export button on the graph and grid pages to Awesome Icons; also added highlight effect and tooltip popup on hover
1212
- Added an autocomplete feature to the search bar `js/components/generate/GenerateForm`, also rewrote tests related to this feature
1313
- Added a route to access all POST codes stored in the database with test coverage
14+
- Added the `js/components/generate/AutocompleteDropdown.js` component to the program field of Generate
1415

1516
### 🐛 Bug fixes
1617

app/Controllers/Program.hs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ module Controllers.Program(index, retrieveProgram) where
22

33
import Config (runDb)
44
import Control.Monad.IO.Class (liftIO)
5-
import qualified Data.Text as T (Text, unlines)
5+
import qualified Data.Set as S
6+
import qualified Data.Text as T (Text, null, strip, unlines)
67
import Database.Persist (Entity)
78
import Database.Persist.Sqlite (SqlPersistM, entityVal, selectList)
89
import Database.Tables as Tables (Post, postCode, postModified)
@@ -17,7 +18,9 @@ index = do
1718
response <- liftIO $ runDb $ do
1819
programsList :: [Entity Post] <- selectList [] []
1920
let codes = map (postCode . entityVal) programsList
20-
return $ T.unlines codes :: SqlPersistM T.Text
21+
rmEmpty = filter (not . T.null . T.strip) codes
22+
rmDups = S.toList (S.fromList rmEmpty)
23+
return $ T.unlines rmDups :: SqlPersistM T.Text
2124
return $ toResponse response
2225

2326
-- | Takes a http request with a program code and sends a JSON response containing the program data

backend-test/Controllers/ProgramControllerTests.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ indexTestCases =
2222
[ ("Empty database", [], "")
2323
, ("One program", ["ASMAJ1689"], "ASMAJ1689\n")
2424
, ("Multiple programs", ["ASMAJ1689", "ASSPE1376", "ASMAJ0506", "ASMIN1165", "ASMIN2289"],
25-
"ASMAJ1689\nASSPE1376\nASMAJ0506\nASMIN1165\nASMIN2289\n")
25+
"ASMAJ0506\nASMAJ1689\nASMIN1165\nASMIN2289\nASSPE1376\n")
2626
]
2727

2828
-- | Run a test case (case, input, expected output) on the index function.

js/components/generate/AutocompleteDropdown.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,21 @@ export default function AutocompleteDropdown({
1616
const [optionList, setOptionList] = useState([])
1717

1818
useEffect(() => {
19-
fetch("/courses")
20-
.then(response => response.text())
21-
.then(data => {
22-
const courses = data.split("\n").map(course => course.substring(0, 8))
23-
setOptionList(courses)
24-
})
19+
if (id == "courses") {
20+
fetch("/courses")
21+
.then(response => response.text())
22+
.then(data => {
23+
const courses = data.split("\n").map(course => course.substring(0, 8))
24+
setOptionList(courses)
25+
})
26+
} else if (id == "programs") {
27+
fetch("/programs")
28+
.then(response => response.text())
29+
.then(data => {
30+
const programs = data.split("\n")
31+
setOptionList(programs)
32+
})
33+
}
2534
}, [])
2635

2736
return (

js/components/generate/GenerateForm.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default class GenerateForm extends React.Component {
1414
this.state = {
1515
fceCount: 0,
1616
selectedCourses: [],
17+
selectedPrograms: [],
1718
}
1819

1920
this.graph = React.createRef()
@@ -31,6 +32,10 @@ export default class GenerateForm extends React.Component {
3132
this.setState({ selectedCourses: newCourse })
3233
}
3334

35+
handleProgramsChange = newProgram => {
36+
this.setState({ selectedPrograms: newProgram })
37+
}
38+
3439
handleSubmit = (values, { setErrors }) => {
3540
const data = {}
3641

@@ -399,7 +404,7 @@ export default class GenerateForm extends React.Component {
399404
component="div"
400405
/>
401406
</div>
402-
<h1 className="chosen-courses">
407+
<h1 className="dropdown-chosen">
403408
Selected Courses: {this.state.selectedCourses.join(", ")}
404409
</h1>
405410
</>
@@ -420,11 +425,13 @@ export default class GenerateForm extends React.Component {
420425
></a>
421426
<Tooltip id="programs-tooltip" place="right" />
422427
</div>
423-
<Field
428+
<AutocompleteDropdown
424429
id="programs"
430+
aria-label="programs"
425431
name="programs"
426-
type="text"
427432
placeholder="e.g., ASMAJ1689, ASFOC1689B"
433+
onSelectedChange={this.handleProgramsChange}
434+
className="autocomplete"
428435
/>
429436
<div className="error-container">
430437
<ErrorMessage
@@ -433,6 +440,9 @@ export default class GenerateForm extends React.Component {
433440
component="div"
434441
/>
435442
</div>
443+
<h1 className="dropdown-chosen">
444+
Selected Programs: {this.state.selectedPrograms.join(", ")}
445+
</h1>
436446
</>
437447
)}
438448
</div>

js/components/generate/__tests__/generate.test.js

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -244,66 +244,70 @@ describe("Handle invalid department inputs appropriately", () => {
244244
})
245245
})
246246

247-
describe("Handle invalid program inputs appropriately", () => {
247+
describe("Handle an incorrect program input appropriately", () => {
248248
beforeEach(() => {
249+
cleanup()
250+
fetchMock.restore()
251+
fetchMock.get("/programs", "")
252+
fetchMock.get("/courses", "")
249253
render(<GenerateForm />)
250254
})
251-
it.each([
252-
{
253-
programInputText: "ASMAJ1234, asdasdasd, ABCDE1234",
254-
expectedWarning: "Invalid program code: asdasdasd",
255-
},
256-
{
257-
programInputText: "ASMIN1689 ASFOC1689F",
258-
expectedWarning: "Invalid program code: ASMIN1689 ASFOC1689F",
259-
},
260-
{
261-
programInputText: "",
262-
expectedWarning: "Cannot generate graph – no programs entered!",
263-
},
264-
{
265-
programInputText: " ",
266-
expectedWarning: "Cannot generate graph – no programs entered!",
267-
},
268-
])(".$programInputText", async ({ programInputText, expectedWarning }) => {
255+
256+
it("Entering no text should return an error", async () => {
269257
const user = userEvent.setup()
258+
const categorySelect = screen.getByDisplayValue("Courses")
259+
await user.selectOptions(categorySelect, "programs")
260+
const genButton = screen.getByText("Generate")
261+
await user.click(genButton)
262+
const errorMessage = await screen.findByText(
263+
"Cannot generate graph – no programs entered!"
264+
)
265+
expect(errorMessage).not.toBeNull()
266+
})
270267

268+
it("Entering blank text should return an error", async () => {
269+
const user = userEvent.setup()
271270
const categorySelect = screen.getByDisplayValue("Courses")
272271
await user.selectOptions(categorySelect, "programs")
272+
const coursesInputField = screen.getByRole("combobox", { name: "programs" })
273+
await user.click(coursesInputField)
274+
await user.tripleClick(coursesInputField)
273275

274-
const programsInputField = screen.getByPlaceholderText(
275-
"e.g., ASMAJ1689, ASFOC1689B"
276-
)
277-
await user.click(programsInputField)
278-
await user.tripleClick(programsInputField)
279-
if (programInputText === "") {
280-
programInputText = "{Backspace}"
281-
}
282-
await user.keyboard(programInputText)
276+
await user.keyboard(" ")
277+
278+
expect(
279+
screen.queryByText("Cannot generate graph – no programs entered!")
280+
).toBeNull()
283281

284-
expect(screen.queryByText(expectedWarning)).toBeNull()
285282
const genButton = screen.getByText("Generate")
286283
await user.click(genButton)
287-
288-
const errorMessage = await screen.findByText(expectedWarning)
284+
const errorMessage = await screen.findByText(
285+
"Cannot generate graph – no programs entered!"
286+
)
289287
expect(errorMessage).not.toBeNull()
290288
})
291289
})
292290

293291
it("No warning for valid program input strings", async () => {
294-
const user = userEvent.setup()
292+
cleanup()
293+
fetchMock.restore()
294+
fetchMock.get("/courses", "")
295+
fetchMock.get("/programs", "ASFOC1689D\n")
295296
render(<GenerateForm />)
296297

298+
const user = userEvent.setup()
297299
const categorySelect = screen.getByDisplayValue("Courses")
298300
await user.selectOptions(categorySelect, "programs")
299301

300302
const programInputText = "ASFOC1689D"
301-
const programsInputField = screen.getByPlaceholderText("e.g., ASMAJ1689, ASFOC1689B")
303+
const programsInputField = screen.getByRole("combobox", { name: "programs" })
302304
await user.click(programsInputField)
303305
await user.tripleClick(programsInputField)
304306
await user.keyboard(programInputText)
307+
await user.keyboard("{ArrowDown}{enter}")
308+
await expect(screen.findByText("Selected Programs: ASFOC1689D")).toBeDefined()
305309
expect(screen.queryByText("Invalid Program Input")).toBeNull()
306310
const genButton = screen.getByText("Generate")
307311
await user.click(genButton)
308-
await expect(screen.findByText(/invalid/i)).rejects.toThrow()
312+
await expect(screen.findByText("Invalid Program Input")).rejects.toThrow()
309313
})

style/app.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2079,7 +2079,7 @@ main-filter input {
20792079
font-size: 1.3rem;
20802080
}
20812081

2082-
.chosen-courses {
2082+
.dropdown-chosen {
20832083
color: #5c497e;
20842084
font-size: 0.8rem;
20852085
}

0 commit comments

Comments
 (0)