Skip to content
This repository has been archived by the owner on Jul 17, 2022. It is now read-only.

Commit

Permalink
Add the ability to filter shipments by status (#165)
Browse files Browse the repository at this point in the history
* Added a way to filter shipments by status

* Added tests
  • Loading branch information
deammer authored Jun 15, 2021
1 parent 5babee4 commit da73883
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 42 deletions.
4 changes: 3 additions & 1 deletion frontend/src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import ButtonIcon from './ButtonIcon'
*
*/

export type ButtonVariant = 'default' | 'primary' | 'danger'

export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
/**
* Use this in a table or list to avoid increasing the height of the container.
Expand All @@ -24,7 +26,7 @@ export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
* @param default This is the default button. Use another style if the button requires a different visual weight.
* @param primary Used to highlight the most important actions. Use sparingly! Avoid showing multiple primary buttons in the same section.
*/
variant?: 'default' | 'primary' | 'danger'
variant?: ButtonVariant
/**
* An optional way to pass a ref down to the <button> element
*/
Expand Down
20 changes: 11 additions & 9 deletions frontend/src/components/DropdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { FunctionComponent, ReactNode, useRef, useState } from 'react'
import cx from 'classnames'
import DropdownMenuText from './DropdownMenuText'
import { FunctionComponent, ReactNode, useRef, useState } from 'react'
import Button, { ButtonVariant } from './Button'
import DropdownMenuButton from './DropdownMenuButton'
import DropdownMenuDivider from './DropdownMenuDivider'
import DropdownMenuText from './DropdownMenuText'
import ChevronIcon from './icons/ChevronIcon'

interface Props {
/**
* An optional way to style the button that triggers the dropdown
*/
buttonClassname?: string
buttonVariant?: ButtonVariant
/**
* The content of the trigger
*/
Expand Down Expand Up @@ -45,7 +47,7 @@ enum MenuAnim {
*/
const DropdownMenu: FunctionComponent<Props> & NestedComponents = ({
position = 'left',
buttonClassname,
buttonVariant,
label,
children,
}) => {
Expand Down Expand Up @@ -104,12 +106,12 @@ const DropdownMenu: FunctionComponent<Props> & NestedComponents = ({
}

return (
<div className="relative">
<button type="button" onClick={toggleMenu} className={buttonClassname}>
{label}
</button>
<div className="relative inline-block">
<Button type="button" onClick={toggleMenu} variant={buttonVariant}>
{label} <ChevronIcon direction="down" className="ml-2 w-5 h-5 -mr-2" />
</Button>
<div
className={cx('absolute', {
className={cx('absolute min-w-full mt-1', {
hidden: !showMenu && animation === MenuAnim.Hidden,
'right-0': position === 'right',
'left-0': position === 'left',
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/TopNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const TopNavigation: FunctionComponent<Props> = ({ hideControls }) => {
{!hideControls && user && (
<div className="flex items-center text-white">
<DropdownMenu
buttonClassname="p-2"
buttonVariant="primary"
position="right"
label={<UserIcon className="w-6 h-6" />}
>
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/components/forms/CheckboxField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { FunctionComponent, InputHTMLAttributes } from 'react'

type Props = InputHTMLAttributes<HTMLInputElement> & {
label: string
}

const CheckboxField: FunctionComponent<Props> = ({ label, ...otherProps }) => {
return (
<label className="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" {...otherProps} />
<span>{label}</span>
</label>
)
}

export default CheckboxField
10 changes: 10 additions & 0 deletions frontend/src/data/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OfferStatus,
PalletType,
PaymentStatus,
ShipmentStatus,
ShippingRoute,
} from '../types/api-types'
import { enumValues } from '../utils/types'
Expand Down Expand Up @@ -45,6 +46,15 @@ export const OFFER_STATUS_OPTIONS = enumValues(OfferStatus).map((routeKey) => ({
value: routeKey,
}))

export const SHIPMENT_STATUS_OPTIONS = [
{ label: 'Abandoned', value: ShipmentStatus.Abandoned },
{ label: 'Announced', value: ShipmentStatus.Announced },
{ label: 'Complete', value: ShipmentStatus.Complete },
{ label: 'In progress', value: ShipmentStatus.InProgress },
{ label: 'Open', value: ShipmentStatus.Open },
{ label: 'In staging', value: ShipmentStatus.Staging },
] as const

export const PALLET_PAYMENT_STATUS_OPTIONS = [
{
label: 'Not yet initiated',
Expand Down
53 changes: 46 additions & 7 deletions frontend/src/pages/shipments/ShipmentList.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import cx from 'classnames'
import { FunctionComponent, useMemo } from 'react'
import { FunctionComponent, useMemo, useState } from 'react'
import { Link } from 'react-router-dom'
import { Column, useSortBy, useTable } from 'react-table'
import Badge from '../../components/Badge'
import ButtonLink from '../../components/ButtonLink'
import DropdownMenu from '../../components/DropdownMenu'
import CheckboxField from '../../components/forms/CheckboxField'
import TableHeader from '../../components/table/TableHeader'
import { SHIPMENT_STATUS_OPTIONS } from '../../data/constants'
import LayoutWithNav from '../../layouts/LayoutWithNav'
import { AllShipmentsQuery, useAllShipmentsQuery } from '../../types/api-types'
import {
AllShipmentsQuery,
ShipmentStatus,
useAllShipmentsQuery,
} from '../../types/api-types'
import {
formatLabelMonth,
formatShipmentName,
Expand Down Expand Up @@ -47,7 +54,24 @@ const COLUMNS: Column<AllShipmentsQuery['listShipments'][0]>[] = [
]

const ShipmentList: FunctionComponent = () => {
const { data, error } = useAllShipmentsQuery()
const [shipmentStatuses, setShipmentStatuses] = useState([
ShipmentStatus.Open,
ShipmentStatus.Staging,
ShipmentStatus.Announced,
ShipmentStatus.InProgress,
])

const { data, error } = useAllShipmentsQuery({
variables: { status: shipmentStatuses },
})

const toggleShipmentStatus = (shipmentStatus: ShipmentStatus) => {
if (shipmentStatuses.includes(shipmentStatus)) {
setShipmentStatuses(shipmentStatuses.filter((s) => s !== shipmentStatus))
} else {
setShipmentStatuses([...shipmentStatuses, shipmentStatus])
}
}

// We must memoize the data for react-table to function properly
const shipments = useMemo(() => data?.listShipments || [], [data])
Expand All @@ -63,11 +87,26 @@ const ShipmentList: FunctionComponent = () => {
return (
<LayoutWithNav>
<div className="max-w-5xl mx-auto bg-white border-l border-r border-gray-200 min-h-content">
<header className="p-6 border-b border-gray-200 md:flex items-center justify-between">
<h1 className="text-navy-800 text-3xl mb-4 md:mb-0">Shipments</h1>
<ButtonLink to={ROUTES.SHIPMENT_CREATE}>Create shipment</ButtonLink>
<header className="p-6 border-b border-gray-200">
<div className="md:flex items-center justify-between">
<h1 className="text-navy-800 text-3xl mb-4 md:mb-0">Shipments</h1>
<ButtonLink to={ROUTES.SHIPMENT_CREATE}>Create shipment</ButtonLink>
</div>
<div className="mt-4">
<DropdownMenu label="Shipment status" position="right">
{SHIPMENT_STATUS_OPTIONS.map((status) => (
<DropdownMenu.Text key={status.value}>
<CheckboxField
label={status.label}
checked={shipmentStatuses.includes(status.value)}
onChange={() => toggleShipmentStatus(status.value)}
/>
</DropdownMenu.Text>
))}
</DropdownMenu>
</div>
</header>
<main className="pb-20 overflow-x-auto">
<main className="pb-20 overflow-x-auto min-h-half-screen">
{error && (
<div className="p-4 rounded bg-red-50 mb-6 text-red-800">
<p className="font-semibold">Error:</p>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/shipments/allShipmentsQuery.graphql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
query AllShipments {
listShipments {
query AllShipments($status: [ShipmentStatus!]) {
listShipments(status: $status) {
id
shippingRoute
labelYear
Expand Down
2 changes: 1 addition & 1 deletion schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ type Query {
listGroups: [Group!]!

shipment(id: Int!): Shipment!
listShipments: [Shipment!]!
listShipments(status: [ShipmentStatus!]): [Shipment!]!

offer(id: Int!): Offer!
listOffers(shipmentId: Int!): [Offer!]!
Expand Down
13 changes: 12 additions & 1 deletion src/resolvers/shipment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,18 @@ import {
import validateEnumMembership from './validateEnumMembership'

// Shipment query resolvers
const listShipments: QueryResolvers['listShipments'] = async () => {
const listShipments: QueryResolvers['listShipments'] = async (
_,
{ status },
) => {
if (status && status.length) {
return Shipment.findAll({
where: {
status,
},
})
}

return Shipment.findAll()
}

Expand Down
69 changes: 49 additions & 20 deletions src/tests/shipments_api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,27 @@ describe('Shipments API', () => {
})

describe('listShipments', () => {
it('lists existing shipments', async () => {
const shipment1 = await createShipment({
let shipment1: Shipment, shipment2: Shipment

const LIST_SHIPMENTS = gql`
query listShipments($status: [ShipmentStatus!]) {
listShipments(status: $status) {
id
status
sendingHub {
id
name
}
receivingHub {
id
name
}
}
}
`

beforeEach(async () => {
shipment1 = await createShipment({
shippingRoute: ShippingRoute.UkToFr,
labelYear: nextYear,
labelMonth: 1,
Expand All @@ -287,32 +306,17 @@ describe('Shipments API', () => {
status: ShipmentStatus.Open,
})

const shipment2 = await createShipment({
shipment2 = await createShipment({
shippingRoute: ShippingRoute.UkToFr,
labelYear: nextYear + 1,
labelMonth: 6,
sendingHubId: group2.id,
receivingHubId: group1.id,
status: ShipmentStatus.InProgress,
})
})

const LIST_SHIPMENTS = gql`
query listShipments {
listShipments {
id
status
sendingHub {
id
name
}
receivingHub {
id
name
}
}
}
`

it('lists existing shipments', async () => {
const res = await testServer.query<{ listShipments: Shipment[] }>({
query: LIST_SHIPMENTS,
})
Expand Down Expand Up @@ -345,6 +349,31 @@ describe('Shipments API', () => {
},
])
})

it('filters shipments by status', async () => {
const res = await testServer.query<{ listShipments: Shipment[] }>({
query: LIST_SHIPMENTS,
variables: {
status: [ShipmentStatus.Open],
},
})

expect(res.errors).toBeUndefined()
expect(res?.data?.listShipments).toIncludeSameMembers([
{
id: shipment1.id,
status: shipment1.status,
sendingHub: {
id: group1.id,
name: group1.name,
},
receivingHub: {
id: group2.id,
name: group2.name,
},
},
])
})
})

describe('shipment', () => {
Expand Down

0 comments on commit da73883

Please sign in to comment.