Skip to content

Commit

Permalink
adds synonyms moderation front end
Browse files Browse the repository at this point in the history
adds validation and error handling

fixes tests

adds modal warning prior to delete

changes removal button to words instead of only icons

code review change requests

removing unused className

formatting

autofill moderator synonym eventId selector

adding create sad path test

removing feature flag check

adding a 3 second debounce
  • Loading branch information
Courey committed Oct 9, 2024
1 parent 54ea8ce commit 01a90ab
Show file tree
Hide file tree
Showing 8 changed files with 772 additions and 157 deletions.
162 changes: 150 additions & 12 deletions __tests__/synonyms.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,65 @@ import type { AWSError, DynamoDB } from 'aws-sdk'
import * as awsSDKMock from 'aws-sdk-mock'
import crypto from 'crypto'

import type { Circular } from '~/routes/circulars/circulars.lib'
import { createSynonyms, putSynonyms } from '~/routes/synonyms/synonyms.server'

jest.mock('@architect/functions')
const synonymId = 'abcde-abcde-abcde-abcde-abcde'
const exampleCirculars = [
{
Items: [
{
circularId: 1234556,
subject: 'subject 1',
body: 'very intelligent things',
eventId: 'eventId1',
createdOn: 12345567,
submitter: 'steve',
} as Circular,
],
},
{
Items: [
{
circularId: 1230000,
subject: 'subject 2',
body: 'more intelligent things',
eventId: 'eventId2',
createdOn: 12345560,
submitter: 'steve',
} as Circular,
],
},
{ Items: [] },
]

describe('createSynonyms', () => {
beforeAll(() => {
beforeEach(() => {
const mockBatchWrite = jest.fn()
const mockQuery = jest.fn()

const mockClient = {
batchWrite: mockBatchWrite,
query: mockQuery,
}
;(tables as unknown as jest.Mock).mockResolvedValue({

;(tables as unknown as jest.Mock).mockReturnValue({
_doc: mockClient,
name: () => {
return 'synonyms'
},
circulars: {
query: mockQuery
.mockReturnValueOnce(exampleCirculars[0])
.mockReturnValueOnce(exampleCirculars[1]),
},
})

jest.spyOn(crypto, 'randomUUID').mockReturnValue(synonymId)
})

afterAll(() => {
afterEach(() => {
jest.restoreAllMocks()
})

Expand All @@ -48,12 +85,51 @@ describe('createSynonyms', () => {

expect(result).toBe(synonymId)
})

test('createSynonyms with nonexistent eventId throws Response 400', async () => {
const mockBatchWriteItem = jest.fn(
(
params: DynamoDB.DocumentClient.BatchWriteItemInput,
callback: (
err: AWSError | null,
data?: DynamoDB.DocumentClient.BatchWriteItemOutput
) => void
) => {
expect(params.RequestItems.synonyms).toBeDefined()
callback(null, {})
}
)
awsSDKMock.mock('DynamoDB', 'batchWriteItem', mockBatchWriteItem)

const synonymousEventIds = ['eventId1', 'nope']
try {
await createSynonyms(synonymousEventIds)
} catch (error) {
// eslint-disable-next-line jest/no-conditional-expect
expect(error).toBeInstanceOf(Response)
const convertedError = error as Response
// eslint-disable-next-line jest/no-conditional-expect
expect(convertedError.status).toBe(400)
const errorMessage = await convertedError.text()
// eslint-disable-next-line jest/no-conditional-expect
expect(errorMessage).toBe('eventId does not exist')
}
})
})

describe('putSynonyms', () => {
const mockBatchWrite = jest.fn()
const mockQuery = jest.fn()

beforeAll(() => {
jest.spyOn(crypto, 'randomUUID').mockReturnValue(synonymId)
})

afterAll(() => {
jest.restoreAllMocks()
awsSDKMock.restore('DynamoDB')
})
test('putSynonyms should not write to DynamoDB if no additions or subtractions', async () => {
const mockClient = {
batchWrite: mockBatchWrite,
}
Expand All @@ -63,23 +139,60 @@ describe('putSynonyms', () => {
return 'synonyms'
},
})

jest.spyOn(crypto, 'randomUUID').mockReturnValue(synonymId)
})

afterAll(() => {
jest.restoreAllMocks()
awsSDKMock.restore('DynamoDB')
})
test('putSynonyms should not write to DynamoDB if no additions or subtractions', async () => {
awsSDKMock.mock('DynamoDB.DocumentClient', 'batchWrite', mockBatchWrite)

await putSynonyms({ synonymId })

expect(mockBatchWrite).not.toHaveBeenCalled()
})

test('putSynonyms should throw 400 response if there are invalid additions', async () => {
const mockClient = {
batchWrite: mockBatchWrite,
query: mockQuery,
}

;(tables as unknown as jest.Mock).mockReturnValue({
_doc: mockClient,
name: () => {
return 'synonyms'
},
circulars: {
query: mockQuery.mockReturnValueOnce(exampleCirculars[2]),
},
})
awsSDKMock.mock('DynamoDB.DocumentClient', 'batchWrite', mockBatchWrite)
try {
await putSynonyms({ synonymId, additions: ["doesn't exist"] })
} catch (error) {
// eslint-disable-next-line jest/no-conditional-expect
expect(error).toBeInstanceOf(Response)
const convertedError = error as Response
// eslint-disable-next-line jest/no-conditional-expect
expect(convertedError.status).toBe(400)
const errorMessage = await convertedError.text()
// eslint-disable-next-line jest/no-conditional-expect
expect(errorMessage).toBe('eventId does not exist')
}
})

test('putSynonyms should write to DynamoDB if there are additions', async () => {
const mockClient = {
batchWrite: mockBatchWrite,
query: mockQuery,
}

;(tables as unknown as jest.Mock).mockReturnValue({
_doc: mockClient,
name: () => {
return 'synonyms'
},
circulars: {
query: mockQuery
.mockReturnValueOnce(exampleCirculars[0])
.mockReturnValueOnce(exampleCirculars[1]),
},
})
awsSDKMock.mock('DynamoDB.DocumentClient', 'batchWrite', mockBatchWrite)
const additions = ['eventId1', 'eventId2']
await putSynonyms({ synonymId, additions })
Expand Down Expand Up @@ -109,6 +222,15 @@ describe('putSynonyms', () => {
})

test('putSynonyms should write to DynamoDB if there are subtractions', async () => {
const mockClient = {
batchWrite: mockBatchWrite,
}
;(tables as unknown as jest.Mock).mockResolvedValue({
_doc: mockClient,
name: () => {
return 'synonyms'
},
})
awsSDKMock.mock('DynamoDB.DocumentClient', 'batchWrite', mockBatchWrite)

const subtractions = ['eventId3', 'eventId4']
Expand All @@ -126,6 +248,22 @@ describe('putSynonyms', () => {
})

test('putSynonyms should write to DynamoDB if there are additions and subtractions', async () => {
const mockClient = {
batchWrite: mockBatchWrite,
query: mockQuery,
}

;(tables as unknown as jest.Mock).mockReturnValue({
_doc: mockClient,
name: () => {
return 'synonyms'
},
circulars: {
query: mockQuery
.mockReturnValueOnce(exampleCirculars[0])
.mockReturnValueOnce(exampleCirculars[1]),
},
})
awsSDKMock.mock('DynamoDB.DocumentClient', 'batchWrite', mockBatchWrite)

const additions = ['eventId1', 'eventId2']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function getPageLink({
return searchString && `?${searchString}`
}

export default function ({
export default function GCNPagination({
page,
totalPages,
...queryStringProps
Expand Down
61 changes: 61 additions & 0 deletions app/components/pagination/PaginationSelectionFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*!
* Copyright © 2023 United States Government as represented by the
* Administrator of the National Aeronautics and Space Administration.
* All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
import { useSubmit } from '@remix-run/react'
import { Select } from '@trussworks/react-uswds'

import GCNPagination from './GCNPagination'

export default function PaginationSelectionFooter({
page,
totalPages,
limit,
query,
form,
}: {
page: number
totalPages: number
limit?: number
query?: string
form: string
}) {
const submit = useSubmit()
return (
<div className="display-flex flex-row flex-wrap">
<div className="display-flex flex-align-self-center margin-right-2 width-auto">
<div>
<Select
id="limit"
title="Number of results per page"
className="width-auto height-5 padding-y-0 margin-y-0"
name="limit"
defaultValue="100"
form={form}
onChange={({ target: { form } }) => {
submit(form)
}}
>
<option value="10">10 / page</option>
<option value="20">20 / page</option>
<option value="50">50 / page</option>
<option value="100">100 / page</option>
</Select>
</div>
</div>
<div className="display-flex flex-fill">
{totalPages > 1 && (
<GCNPagination
query={query}
page={page}
limit={limit}
totalPages={totalPages}
/>
)}
</div>
</div>
)
}
52 changes: 9 additions & 43 deletions app/routes/circulars._archive._index/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,7 @@ import {
useSearchParams,
useSubmit,
} from '@remix-run/react'
import {
Alert,
Button,
Icon,
Label,
Select,
TextInput,
} from '@trussworks/react-uswds'
import { Alert, Button, Icon, Label, TextInput } from '@trussworks/react-uswds'
import clamp from 'lodash/clamp'
import { useId, useState } from 'react'

Expand All @@ -41,13 +34,13 @@ import {
putVersion,
search,
} from '../circulars/circulars.server'
import CircularPagination from './CircularPagination'
import CircularsHeader from './CircularsHeader'
import CircularsIndex from './CircularsIndex'
import { DateSelector } from './DateSelectorMenu'
import { SortSelector } from './SortSelectorButton'
import Hint from '~/components/Hint'
import { ToolbarButtonGroup } from '~/components/ToolbarButtonGroup'
import PaginationSelectionFooter from '~/components/pagination/PaginationSelectionFooter'
import { origin } from '~/lib/env.server'
import { getFormDataString } from '~/lib/utils'
import { postZendeskRequest } from '~/lib/zendesk.server'
Expand Down Expand Up @@ -281,40 +274,13 @@ export default function () {
totalItems={totalItems}
query={query}
/>
<div className="display-flex flex-row flex-wrap">
<div className="display-flex flex-align-self-center margin-right-2 width-auto">
<div>
<Select
id="limit"
title="Number of results per page"
className="width-auto height-5 padding-y-0 margin-y-0"
name="limit"
defaultValue="100"
form={formId}
onChange={({ target: { form } }) => {
submit(form)
}}
>
<option value="10">10 / page</option>
<option value="20">20 / page</option>
<option value="50">50 / page</option>
<option value="100">100 / page</option>
</Select>
</div>
</div>
<div className="display-flex flex-fill">
{totalPages > 1 && (
<CircularPagination
query={query}
page={page}
limit={parseInt(limit)}
totalPages={totalPages}
startDate={startDate}
endDate={endDate}
/>
)}
</div>
</div>
<PaginationSelectionFooter
query={query}
page={page}
limit={parseInt(limit)}
totalPages={totalPages}
form={formId}
/>
</>
)}
</>
Expand Down
Loading

0 comments on commit 01a90ab

Please sign in to comment.