Skip to content

Commit

Permalink
add UserList modals and wire up buttons (#718)
Browse files Browse the repository at this point in the history
* add hooks for creating, updating and deleting UserList

* add UserList dialogs to ManageListDialogs

* fix existing tests

* fix UserList validationSchema

* wire up UpsertUserListDialog to Create new list button on UserListListingPage

* wire up edit button on UserListDetailsPage

* name property on RadioChoiceField in UpsertUserListDialog should be "privacy_level" not "published"

* add tests for upsertUserList

* add tests for deleting a UserList

* change margins on form-row

* add context menu to UserListListingPage items, wire up buttons and add tests

* remove unit from 0px margin

* remove extraneous 0 in margin

* remove unnecessary omit

* correct accidental renaming of manageListDialogs

* use faker.helpers.arrayElement
  • Loading branch information
gumaerc authored Apr 3, 2024
1 parent 24c6ba8 commit 6e502a8
Show file tree
Hide file tree
Showing 15 changed files with 550 additions and 46 deletions.
43 changes: 43 additions & 0 deletions frontends/api/src/hooks/learningResources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ import type {
LearningResource,
LearningResourcesSearchApiLearningResourcesSearchRetrieveRequest as LRSearchRequest,
UserlistsApiUserlistsListRequest as ULListRequest,
UserlistsApiUserlistsCreateRequest as ULCreateRequest,
UserlistsApiUserlistsDestroyRequest as ULDestroyRequest,
UserlistsApiUserlistsItemsListRequest as ULItemsListRequest,
OfferorsApiOfferorsListRequest,
UserList,
} from "../../generated/v1"
import learningResources, { invalidateResourceQueries } from "./keyFactory"
import { ListType } from "../../common/constants"
Expand Down Expand Up @@ -220,6 +223,43 @@ const useUserListsDetail = (id: number) => {
return useQuery(learningResources.userlists._ctx.detail(id))
}

const useUserListCreate = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (params: ULCreateRequest["UserListRequest"]) =>
userListsApi.userlistsCreate({
UserListRequest: params,
}),
onSettled: () => {
queryClient.invalidateQueries(learningResources.userlists._ctx.list._def)
},
})
}
const useUserListUpdate = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (params: Pick<UserList, "id"> & Partial<UserList>) =>
userListsApi.userlistsPartialUpdate({
id: params.id,
PatchedUserListRequest: params,
}),
onSettled: (_data, _err, vars) => {
invalidateResourceQueries(queryClient, vars.id)
},
})
}

const useUserListDestroy = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (params: ULDestroyRequest) =>
userListsApi.userlistsDestroy(params),
onSettled: (_data, _err, vars) => {
invalidateResourceQueries(queryClient, vars.id)
},
})
}

const useInfiniteUserListItems = (
params: ULItemsListRequest,
options: Pick<UseQueryOptions, "enabled"> = {},
Expand Down Expand Up @@ -306,6 +346,9 @@ export {
useLearningResourcesSearch,
useUserListList,
useUserListsDetail,
useUserListCreate,
useUserListUpdate,
useUserListDestroy,
useInfiniteUserListItems,
useOfferorsList,
useListItemMove,
Expand Down
3 changes: 3 additions & 0 deletions frontends/api/src/test-utils/factories/userLists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Factory, makePaginatedFactory } from "ol-test-utilities"
import {
MicroUserListRelationship,
PaginatedUserListRelationshipList,
PrivacyLevelEnum,
UserList,
UserListRelationship,
} from "api"
Expand All @@ -12,6 +13,8 @@ const userList: Factory<UserList> = (overrides = {}) => {
const list: UserList = {
id: faker.helpers.unique(faker.datatype.number),
title: faker.helpers.unique(faker.lorem.words),
description: faker.helpers.unique(faker.lorem.paragraph),
privacy_level: faker.helpers.arrayElement(Object.values(PrivacyLevelEnum)),
item_count: 4,
image: {},
author: faker.helpers.unique(faker.datatype.number),
Expand Down
2 changes: 1 addition & 1 deletion frontends/mit-open/src/GlobalStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const formCss = css`
form .form-row,
.form-header .form-row {
margin: 10px 10px 24px 0;
margin: 10px 0 24px;
}
.MuiDialogContent-root {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
within,
act,
} from "../../test-utils"
import { manageLearningPathDialogs } from "@/page-components/ManageListDialogs/ManageListDialogs"
import { manageListDialogs } from "@/page-components/ManageListDialogs/ManageListDialogs"
import { waitForElementToBeRemoved } from "@testing-library/react"
import {
LearningPathRelationship,
Expand Down Expand Up @@ -163,7 +163,7 @@ describe("AddToListDialog", () => {
test("Clicking 'Create a new list' opens the create list dialog", async () => {
// Don't actually open the 'Create List' modal, or we'll need to mock API responses.
const createList = jest
.spyOn(manageLearningPathDialogs, "upsert")
.spyOn(manageListDialogs, "upsertLearningPath")
.mockImplementationOnce(jest.fn())

setup()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
useLearningpathRelationshipCreate,
useLearningpathRelationshipDestroy,
} from "api/hooks/learningResources"
import { manageLearningPathDialogs } from "@/page-components/ManageListDialogs/ManageListDialogs"
import { manageListDialogs } from "@/page-components/ManageListDialogs/ManageListDialogs"

type AddToListDialogProps = {
resourceId: number
Expand Down Expand Up @@ -189,7 +189,7 @@ const AddToListDialogInner: React.FC<AddToListDialogProps> = ({
})}
<ListItem className="add-to-list-new">
<ListItemButton
onClick={() => manageLearningPathDialogs.upsert()}
onClick={() => manageListDialogs.upsertLearningPath()}
>
<AddIcon />
<ListItemText primary="Create a new list" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { faker } from "@faker-js/faker/locale/en"
import { factories, urls, makeRequest } from "api/test-utils"
import type {
LearningPathResource,
PaginatedLearningResourceTopicList,
import {
PrivacyLevelEnum,
type LearningPathResource,
type PaginatedLearningResourceTopicList,
type UserList,
} from "api"
import { allowConsoleErrors, getDescriptionFor } from "ol-test-utilities"
import { manageLearningPathDialogs } from "./ManageListDialogs"
import { manageListDialogs } from "./ManageListDialogs"
import {
screen,
renderWithProviders,
Expand Down Expand Up @@ -39,6 +41,11 @@ const inputs = {
const element = screen.getByLabelText(value ? "Public" : "Private")
return element as HTMLInputElement
},
privacy_level: (value?: string) => {
invariant(value !== undefined)
const element = screen.getByDisplayValue(value)
return element as HTMLInputElement
},
title: () => screen.getByLabelText("Title", { exact: false }),
description: () => screen.getByLabelText("Description", { exact: false }),
topics: () => screen.getByLabelText("Subjects", { exact: false }),
Expand All @@ -47,7 +54,7 @@ const inputs = {
delete: () => screen.getByRole("button", { name: "Yes, delete" }),
}

describe("manageListDialogs.upsert", () => {
describe("manageListDialogs.upsertLearningPath", () => {
const setup = ({
resource,
topics = factories.learningResources.topics({ count: 10 }),
Expand All @@ -70,7 +77,7 @@ describe("manageListDialogs.upsert", () => {
renderWithProviders(null, opts)

act(() => {
manageLearningPathDialogs.upsert(resource)
manageListDialogs.upsertLearningPath(resource)
})

return { topics }
Expand Down Expand Up @@ -211,12 +218,145 @@ describe("manageListDialogs.upsert", () => {
})
})

describe("manageListDialogs.destroy", () => {
describe("manageListDialogs.upsertUserList", () => {
const setup = ({
userList,
opts = {
user: { is_authenticated: true },
},
}: {
userList?: UserList
opts?: Partial<TestAppOptions>
} = {}) => {
renderWithProviders(null, opts)

act(() => {
manageListDialogs.upsertUserList(userList)
})
}

test.each([
{
userList: undefined,
expectedTitle: "Create User List",
},
{
userList: factories.userLists.userList(),
expectedTitle: "Edit User List",
},
])(
"Dialog title is $expectedTitle when userList=$userList",
async ({ userList, expectedTitle }) => {
setup({ userList })
const dialog = screen.getByRole("heading", { name: expectedTitle })
expect(dialog).toBeVisible()
},
)

test("'Cancel' closes dialog (and does not make request)", async () => {
// behavior does not depend on stafflist / userlist, so just pick one
setup({
userList: factories.userLists.userList(),
})
const dialog = screen.getByRole("dialog")
await user.click(inputs.cancel())
expect(makeRequest).not.toHaveBeenCalledWith(
"patch",
expect.anything(),
expect.anything(),
)
await waitForElementToBeRemoved(dialog)
})

test("Validates required fields", async () => {
setup()
await user.click(inputs.submit())

const titleInput = inputs.title()
const titleFeedback = getDescriptionFor(titleInput)
expect(titleInput).toBeInvalid()
expect(titleFeedback).toHaveTextContent("Title is required.")

const descriptionInput = inputs.description()
const descriptionFeedback = getDescriptionFor(descriptionInput)
expect(descriptionInput).toBeInvalid()
expect(descriptionFeedback).toHaveTextContent("Description is required.")
})

test("Form defaults are set", () => {
setup()
expect(inputs.title()).toHaveValue("")
expect(inputs.description()).toHaveValue("")
expect(inputs.privacy_level(PrivacyLevelEnum.Private).checked).toBe(true)
expect(inputs.privacy_level(PrivacyLevelEnum.Unlisted).checked).toBe(false)
})

test("Editing form values", async () => {
const userList = factories.userLists.userList()
setup({ userList: userList })
const patch = {
title: faker.lorem.words(),
description: faker.lorem.paragraph(),
privacy_level: PrivacyLevelEnum.Unlisted,
}

// Title
expect(inputs.title()).toHaveValue(userList.title)
await user.click(inputs.title())
await user.clear(inputs.title())
await user.paste(patch.title)

// Description
expect(inputs.description()).toHaveValue(userList.description)
await user.click(inputs.description())
await user.clear(inputs.description())
await user.paste(patch.description)

// Privacy Level
expect(inputs.privacy_level(PrivacyLevelEnum.Private).checked).toBe(true)
expect(inputs.privacy_level(PrivacyLevelEnum.Unlisted).checked).toBe(false)
await user.click(inputs.privacy_level(patch.privacy_level))

// Submit
const patchUrl = urls.userLists.details({ id: userList.id })
setMockResponse.patch(patchUrl, { ...userList, ...patch })
await user.click(inputs.submit())

expect(makeRequest).toHaveBeenCalledWith(
"patch",
patchUrl,
expect.objectContaining({ ...patch }),
)
})

test("Displays overall error if form validates but API call fails", async () => {
allowConsoleErrors()
const userList = factories.userLists.userList()
await setup({ userList: userList })

const patchUrl = urls.userLists.details({ id: userList.id })
setMockResponse.patch(patchUrl, {}, { code: 408 })
await user.click(inputs.submit())

expect(makeRequest).toHaveBeenCalledWith(
"patch",
patchUrl,
expect.anything(),
)
const alertMessage = await screen.findByRole("alert")

expect(alertMessage).toHaveTextContent(
"There was a problem saving your list.",
)
})
})

describe("manageListDialogs.destroyLearningPath", () => {
const setup = () => {
const resource = factories.learningResources.learningPath()
renderWithProviders(null)
act(() => {
manageLearningPathDialogs.destroy(resource)
manageListDialogs.destroyLearningPath(resource)
})
return { resource }
}
Expand Down Expand Up @@ -249,3 +389,42 @@ describe("manageListDialogs.destroy", () => {
await waitForElementToBeRemoved(dialog)
})
})

describe("manageListDialogs.destroyUserList", () => {
const setup = () => {
const userList = factories.userLists.userList()
renderWithProviders(null)
act(() => {
manageListDialogs.destroyUserList(userList)
})
return { userList: userList }
}

test("Dialog title is 'Delete list'", async () => {
setup()
const dialog = screen.getByRole("heading", { name: "Delete User List" })
expect(dialog).toBeVisible()
})

test("Deleting a $label calls correct API", async () => {
const { userList } = setup()

const dialog = screen.getByRole("dialog")
const url = urls.userLists.details({ id: userList.id })
setMockResponse.delete(url, undefined)
await user.click(inputs.delete())

expect(makeRequest).toHaveBeenCalledWith("delete", url, undefined)
await waitForElementToBeRemoved(dialog)
})

test("Clicking cancel does not delete list", async () => {
setup()

const dialog = screen.getByRole("dialog")
await user.click(inputs.cancel())

expect(makeRequest).not.toHaveBeenCalled()
await waitForElementToBeRemoved(dialog)
})
})
Loading

0 comments on commit 6e502a8

Please sign in to comment.