Skip to content

Commit

Permalink
Connect ag-grid sort with the API (#1492)
Browse files Browse the repository at this point in the history
* Connect ag-grid sort with the API

* Fix code style issues with Prettier

* Updated changelog

* Fix code style issues with Prettier

* Add empty values as last

* Fix code style issues with Prettier

* Add missing OrderBy enum definitions

* Add migrating empty strings to nulls in applicant.last_name and applicant.first_name

* Fix code style issues with Prettier

* Fix 1625746856273-migrate-empty-strings-to-nulls-in-app SQL queries

* Map firstName and lastName empty strings to nulls

* Update sorting compare method

* Add applicant firstName and lastName MinLength(1) validators

* Fix e2e tests (missing lastName in test fixtures)

* Update var name

* Update order type

* Update types

Co-authored-by: Dominik Barcikowski <dominik@airnauts.com>
Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
Co-authored-by: Michal Plebanski <michalp@airnauts.com>
  • Loading branch information
4 people committed Jul 13, 2021
1 parent f90479b commit 34cd0ce
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 29 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ All notable changes to this project will be documented in this file. The format

### Frontend

- Added:

- AG-grid sorting now is connected with the backend sorting ([#1083](https://github.com/bloom-housing/bloom/issues/1083)) (Michał Plebański)

### UI Components

- Added:
Expand Down
26 changes: 23 additions & 3 deletions backend/core/src/applications/applications.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,38 @@ import { ApplicationCsvExporter } from "../csv/application-csv-exporter"
import { applicationPreferenceApiExtraModels } from "./application-preference-api-extra-models"
import { ListingsService } from "../listings/listings.service"

enum OrderByParam {
export enum OrderByParam {
firstName = "applicant.firstName",
lastName = "applicant.lastName",
submissionDate = "application.submissionDate",
createdAt = "application.createdAt",
}

enum OrderParam {
export enum OrderParam {
ASC = "ASC",
DESC = "DESC",
}

class ApplicationsApiExtraModel {
@Expose()
@ApiProperty({
enum: Object.keys(OrderByParam),
example: "createdAt",
default: "createdAt",
required: false,
})
orderBy?: OrderByParam

@Expose()
@ApiProperty({
enum: OrderParam,
example: "DESC",
default: "DESC",
required: false,
})
order?: OrderParam
}

export class PaginatedApplicationListQueryParams extends PaginationQueryParams {
@Expose()
@ApiProperty({
Expand Down Expand Up @@ -163,7 +183,7 @@ export class ApplicationsCsvListQueryParams extends PaginatedApplicationListQuer
groups: [ValidationsGroupsEnum.default, ValidationsGroupsEnum.partners],
})
)
@ApiExtraModels(...applicationPreferenceApiExtraModels)
@ApiExtraModels(...applicationPreferenceApiExtraModels, ApplicationsApiExtraModel)
export class ApplicationsController {
constructor(
private readonly applicationsService: ApplicationsService,
Expand Down
2 changes: 1 addition & 1 deletion backend/core/src/applications/applications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export class ApplicationsService {
userId: (qb, { userId }) => qb.andWhere("application.user_id = :uid", { uid: userId }),
listingId: (qb, { listingId }) =>
qb.andWhere("application.listing_id = :lid", { lid: listingId }),
orderBy: (qb, { orderBy, order }) => qb.orderBy(orderBy, order),
orderBy: (qb, { orderBy, order }) => qb.orderBy(orderBy, order, "NULLS LAST"),
search: (qb, { search }) =>
qb.andWhere(
`to_tsvector('english', REGEXP_REPLACE(concat_ws(' ', applicant, alternateContact.emailAddress), '[_]|[-]', '/', 'g')) @@ to_tsquery(CONCAT(CAST(plainto_tsquery(REGEXP_REPLACE(:search, '[_]|[-]', '/', 'g')) as text), ':*'))`,
Expand Down
3 changes: 3 additions & 0 deletions backend/core/src/applications/entities/applicant.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
MaxLength,
ValidateNested,
IsEmail,
MinLength,
} from "class-validator"
import { Address } from "../../shared/entities/address.entity"
import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum"
Expand All @@ -21,6 +22,7 @@ export class Applicant extends AbstractEntity {
@IsOptional({ groups: [ValidationsGroupsEnum.partners] })
@IsString({ groups: [ValidationsGroupsEnum.default] })
@MaxLength(64, { groups: [ValidationsGroupsEnum.default] })
@MinLength(1, { groups: [ValidationsGroupsEnum.default] })
firstName?: string | null

@Column({ type: "text", nullable: true })
Expand All @@ -35,6 +37,7 @@ export class Applicant extends AbstractEntity {
@IsOptional({ groups: [ValidationsGroupsEnum.partners] })
@IsString({ groups: [ValidationsGroupsEnum.default] })
@MaxLength(64, { groups: [ValidationsGroupsEnum.default] })
@MinLength(1, { groups: [ValidationsGroupsEnum.default] })
lastName?: string | null

@Column({ type: "text", nullable: true })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {MigrationInterface, QueryRunner} from "typeorm";

export class migrateEmptyStringsToNullsInApp1625746856273 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`UPDATE applicant SET last_name = NULL where last_name = ''`)
await queryRunner.query(`UPDATE applicant SET first_name = NULL where first_name = ''`)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`UPDATE applicant SET first_name = '' where first_name = NULL`)
await queryRunner.query(`UPDATE applicant SET last_name = '' where last_name = NULL`)
}

}
2 changes: 1 addition & 1 deletion backend/core/test/lib/get-test-app-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const getTestAppBody: (listingId?: string) => ApplicationUpdate = (listin
applicant: {
firstName: "Applicant",
middleName: "Middlename",
lastName: "",
lastName: "lastName",
birthMonth: "",
birthDay: "",
birthYear: "",
Expand Down
18 changes: 18 additions & 0 deletions backend/core/types/src/backend-swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2122,6 +2122,14 @@ export interface AddressInput {
value: AddressCreate;
}

export interface ApplicationsApiExtraModel {
/** */
orderBy?: EnumApplicationsApiExtraModelOrderBy;

/** */
order?: EnumApplicationsApiExtraModelOrder;
}

export interface PaginationMeta {
/** */
currentPage: number;
Expand Down Expand Up @@ -4447,6 +4455,16 @@ export enum InputType {
'address' = 'address',
'hhMemberSelect' = 'hhMemberSelect'
}
export enum EnumApplicationsApiExtraModelOrderBy {
'firstName' = 'firstName',
'lastName' = 'lastName',
'submissionDate' = 'submissionDate',
'createdAt' = 'createdAt'
}
export enum EnumApplicationsApiExtraModelOrder {
'ASC' = 'ASC',
'DESC' = 'DESC'
}
export enum EnumListingFilterParamsComparison {
'=' = '=',
'<>' = '<>'
Expand Down
7 changes: 6 additions & 1 deletion sites/partners/lib/formatApplicationData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import moment from "moment"

type GetAddressType = Omit<Address, "id" | "createdAt" | "updatedAt">

function getAddress(condition: boolean, addressData?: GetAddressType): GetAddressType {
const getAddress = (condition: boolean, addressData?: GetAddressType): GetAddressType => {
const blankAddress: GetAddressType = {
street: "",
street2: "",
Expand All @@ -39,6 +39,8 @@ function getAddress(condition: boolean, addressData?: GetAddressType): GetAddres
return condition ? addressData : blankAddress
}

const mapEmptyStringToNull = (value: string) => (value === "" ? null : value)

interface FormData extends FormTypes {
householdMembers: HouseholdMember[]
submissionType: ApplicationSubmissionType
Expand Down Expand Up @@ -81,6 +83,9 @@ export const mapFormToApi = (data: FormData, listingId: string, editMode: boolea
const workInRegion: string | null = applicantData?.workInRegion || null
const emailAddress: string | null = applicantData?.emailAddress || null

applicantData.firstName = mapEmptyStringToNull(applicantData.firstName)
applicantData.lastName = mapEmptyStringToNull(applicantData.firstName)

const workAddress = getAddress(
applicantData?.workInRegion === YesNoAnswer.Yes,
applicantData?.workAddress
Expand Down
39 changes: 27 additions & 12 deletions sites/partners/lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { useContext } from "react"
import useSWR, { mutate } from "swr"

import { AuthContext } from "@bloom-housing/ui-components"
import {
EnumApplicationsApiExtraModelOrder,
EnumApplicationsApiExtraModelOrderBy,
} from "@bloom-housing/backend-core/types"

type UseSingleApplicationDataProps = {
listingId: string
Expand Down Expand Up @@ -39,20 +43,27 @@ export function useApplicationsData(
pageIndex: number,
limit = 10,
listingId: string,
search: string
search: string,
orderBy?: EnumApplicationsApiExtraModelOrderBy,
order?: EnumApplicationsApiExtraModelOrder
) {
const { applicationsService } = useContext(AuthContext)

const searchParams = new URLSearchParams()
searchParams.append("listingId", listingId)
searchParams.append("page", pageIndex.toString())
searchParams.append("limit", limit.toString())
const queryParams = new URLSearchParams()
queryParams.append("listingId", listingId)
queryParams.append("page", pageIndex.toString())
queryParams.append("limit", limit.toString())

if (search) {
searchParams.append("search", search)
queryParams.append("search", search)
}

const endpoint = `${process.env.backendApiBase}/applications?${searchParams.toString()}`
if (orderBy) {
queryParams.append("orderBy", search)
queryParams.append("order", order ?? EnumApplicationsApiExtraModelOrder.ASC)
}

const endpoint = `${process.env.backendApiBase}/applications?${queryParams.toString()}`

const params = {
listingId,
Expand All @@ -64,6 +75,10 @@ export function useApplicationsData(
Object.assign(params, { search })
}

if (orderBy) {
Object.assign(params, { orderBy, order: order ?? "ASC" })
}

const fetcher = () => applicationsService.list(params)
const { data, error } = useSWR(endpoint, fetcher)

Expand Down Expand Up @@ -95,12 +110,12 @@ export function useFlaggedApplicationsList({
}: UseSingleApplicationDataProps) {
const { applicationFlaggedSetsService } = useContext(AuthContext)

const searchParams = new URLSearchParams()
searchParams.append("listingId", listingId)
searchParams.append("page", page.toString())
searchParams.append("limit", limit.toString())
const queryParams = new URLSearchParams()
queryParams.append("listingId", listingId)
queryParams.append("page", page.toString())
queryParams.append("limit", limit.toString())

const endpoint = `${process.env.backendApiBase}/applicationFlaggedSets?${searchParams.toString()}`
const endpoint = `${process.env.backendApiBase}/applicationFlaggedSets?${queryParams.toString()}`

const fetcher = () =>
applicationFlaggedSetsService.list({
Expand Down
57 changes: 53 additions & 4 deletions sites/partners/pages/listings/[id]/applications/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useMemo, useContext } from "react"
import React, { useState, useEffect, useRef, useMemo, useContext, useCallback } from "react"
import { useRouter } from "next/router"
import moment from "moment"
import Head from "next/head"
Expand Down Expand Up @@ -26,6 +26,15 @@ import { useForm } from "react-hook-form"
import { AgGridReact } from "ag-grid-react"
import { getColDefs } from "../../../../src/applications/ApplicationsColDefs"
import { GridOptions, ColumnApi, ColumnState } from "ag-grid-community"
import {
EnumApplicationsApiExtraModelOrder,
EnumApplicationsApiExtraModelOrderBy,
} from "@bloom-housing/backend-core/types"

type ApplicationsListSortOptions = {
orderBy: EnumApplicationsApiExtraModelOrderBy
order: EnumApplicationsApiExtraModelOrder
}

const ApplicationsList = () => {
const COLUMN_STATE_KEY = "column-state"
Expand All @@ -45,8 +54,21 @@ const ApplicationsList = () => {
const [itemsPerPage, setItemsPerPage] = useState<number>(AG_PER_PAGE_OPTIONS[0])
const [currentPage, setCurrentPage] = useState<number>(1)

/* OrderBy columns */
const [sortOptions, setSortOptions] = useState<ApplicationsListSortOptions>({
orderBy: null,
order: null,
})

const listingId = router.query.id as string
const { appsData } = useApplicationsData(currentPage, itemsPerPage, listingId, delayedFilterValue)
const { appsData } = useApplicationsData(
currentPage,
itemsPerPage,
listingId,
delayedFilterValue,
sortOptions.orderBy,
sortOptions.order
)
const { listingDto } = useSingleListingData(listingId)
const countyCode = listingDto?.countyCode
const listingName = listingDto?.name
Expand Down Expand Up @@ -154,8 +176,35 @@ const ApplicationsList = () => {
}
}

// update table items order on sort change
const initialLoadOnSort = useRef<boolean>(false)
const onSortChange = useCallback((columns: ColumnState[]) => {
// prevent multiple fetch on initial render
if (!initialLoadOnSort.current) {
initialLoadOnSort.current = true
return
}

const sortedBy = columns.find((col) => col.sort)
const { colId, sort } = sortedBy || {}

const allowedSortColIds: string[] = Object.values(EnumApplicationsApiExtraModelOrderBy)

if (allowedSortColIds.includes(colId)) {
const name = EnumApplicationsApiExtraModelOrderBy[colId]

setSortOptions({
orderBy: name,
order: sort.toUpperCase() as EnumApplicationsApiExtraModelOrder,
})
}
}, [])

const gridOptions: GridOptions = {
onSortChanged: (params) => saveColumnState(params.columnApi),
onSortChanged: (params) => {
saveColumnState(params.columnApi)
onSortChange(params.columnApi.getColumnState())
},
onColumnMoved: (params) => saveColumnState(params.columnApi),
components: {
formatLinkCell: formatLinkCell,
Expand Down Expand Up @@ -236,7 +285,7 @@ const ApplicationsList = () => {
headerHeight={83}
rowHeight={58}
suppressPaginationPanel={true}
paginationPageSize={8}
paginationPageSize={AG_PER_PAGE_OPTIONS[0]}
suppressScrollOnNewData={true}
></AgGridReact>

Expand Down
Loading

0 comments on commit 34cd0ce

Please sign in to comment.