Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Upload series (#327) #496

Merged
merged 9 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/src/graphql/uploads.sdl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const schema = gql`
signedUrl: String
latestValidation: UploadValidation
subrecipientUploads: [SubrecipientUpload!]
seriesUploads: [Upload]
}

type Query {
Expand Down
112 changes: 67 additions & 45 deletions api/src/services/uploads/uploads.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { Upload } from '@prisma/client'

import {
deleteUploadFile,
s3UploadFilePutSignedUrl,
Expand Down Expand Up @@ -105,7 +103,7 @@ describe('uploads', () => {
'returns all uploads in their agency for organization staff',
async (scenario: StandardScenario) => {
mockCurrentUser(scenario.user.three)
const result: Upload[] = await uploads()
const result = await uploads()

const uploadsBelongToOrg = await uploadsBelongToOrganization(
result,
Expand Down Expand Up @@ -152,7 +150,7 @@ describe('uploads', () => {
})

scenario('updates an upload', async (scenario: StandardScenario) => {
const original = (await upload({ id: scenario.upload.one.id })) as Upload
const original = await upload({ id: scenario.upload.one.id })
const result = await updateUpload({
id: original.id,
input: { filename: 'String2' },
Expand All @@ -169,55 +167,79 @@ describe('uploads', () => {
expect(deleteUploadFile).not.toHaveBeenCalled()
})

scenario(
'returns the latest validation for an upload when there are multiple validations',
async (scenario: StandardScenario) => {
const uploadIdOne = scenario.upload.one.id
const latestValidationOfScenarioOne =
await UploadRelationResolver.latestValidation(
{},
{
root: {
id: uploadIdOne,
agencyId: scenario.upload.one.agencyId,
createdAt: scenario.upload.one.createdAt,
filename: scenario.upload.one.filename,
reportingPeriodId: scenario.upload.one.reportingPeriodId,
updatedAt: scenario.upload.one.updatedAt,
uploadedById: scenario.upload.one.uploadedById,
},
context: undefined,
info: undefined,
}
describe('Uploads resolvers', () => {
scenario(
'latest validations: returns the latest validation for an upload when there are multiple validations',
async (scenario: StandardScenario) => {
const uploadIdOne = scenario.upload.one.id
const latestValidationOfScenarioOne =
await UploadRelationResolver.latestValidation(
{},
{
root: {
id: uploadIdOne,
agencyId: scenario.upload.one.agencyId,
createdAt: scenario.upload.one.createdAt,
filename: scenario.upload.one.filename,
reportingPeriodId: scenario.upload.one.reportingPeriodId,
updatedAt: scenario.upload.one.updatedAt,
uploadedById: scenario.upload.one.uploadedById,
},
context: undefined,
info: undefined,
}
)

const uploadIdTwo = scenario.upload.two.id
const latestValidationOfScenarioTwo =
await UploadRelationResolver.latestValidation(
{},
{
root: {
id: uploadIdTwo,
agencyId: scenario.upload.two.agencyId,
createdAt: scenario.upload.two.createdAt,
filename: scenario.upload.two.filename,
reportingPeriodId: scenario.upload.two.reportingPeriodId,
updatedAt: scenario.upload.two.updatedAt,
uploadedById: scenario.upload.two.uploadedById,
},
context: undefined,
info: undefined,
}
)

expect(latestValidationOfScenarioOne?.createdAt).toEqual(
new Date('2024-01-27T10:32:00.000Z')
)
expect(latestValidationOfScenarioTwo?.createdAt).toEqual(
new Date('2024-01-29T18:13:25.000Z')
)
}
)

const uploadIdTwo = scenario.upload.two.id
const latestValidationOfScenarioTwo =
await UploadRelationResolver.latestValidation(
scenario(
'uploads resolver: correctly returns series uploads',
async (scenario: StandardScenario) => {
const results = await UploadRelationResolver.seriesUploads(
{},
{
root: {
id: uploadIdTwo,
agencyId: scenario.upload.two.agencyId,
createdAt: scenario.upload.two.createdAt,
filename: scenario.upload.two.filename,
reportingPeriodId: scenario.upload.two.reportingPeriodId,
updatedAt: scenario.upload.two.updatedAt,
uploadedById: scenario.upload.two.uploadedById,
...scenario.upload.one,
},
context: undefined,
info: undefined,
}
)

expect(latestValidationOfScenarioOne?.createdAt).toEqual(
new Date('2024-01-27T10:32:00.000Z')
)
expect(latestValidationOfScenarioTwo?.createdAt).toEqual(
new Date('2024-01-29T18:13:25.000Z')
)
}
)
expect(results).toHaveLength(2)
expect(results.map((seriesUpload) => seriesUpload.id)).toEqual([
scenario.upload.two.id,
scenario.upload.one.id,
])
}
)
})
})

describe('downloads', () => {
Expand Down Expand Up @@ -335,7 +357,7 @@ describe('getValidUploadsInCurrentPeriod', () => {

const uploads = await getValidUploadsInCurrentPeriod(
scenario.organization.one,
upload.reportingPeriodId
scenario.reportingPeriod.one
)

expect(uploads.map((upload) => upload.id)).toContain(upload.id)
Expand All @@ -352,7 +374,7 @@ describe('getValidUploadsInCurrentPeriod', () => {

const uploads = await getValidUploadsInCurrentPeriod(
scenario.organization.two,
upload.reportingPeriodId
scenario.reportingPeriod.one
)

expect(uploads.map((upload) => upload.id)).toContain(upload.id)
Expand All @@ -368,7 +390,7 @@ describe('getValidUploadsInCurrentPeriod', () => {

const uploads = await getValidUploadsInCurrentPeriod(
scenario.organization.two,
upload.reportingPeriodId
scenario.reportingPeriod.one
)

expect(uploads.map((upload) => upload.id)).not.toContain(upload.id)
Expand Down
19 changes: 19 additions & 0 deletions api/src/services/uploads/uploads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import cloneDeep from 'lodash/cloneDeep'
import type {
QueryResolvers,
MutationResolvers,
ResolversTypes,
UploadRelationResolvers,
} from 'types/graphql'
import { v4 as uuidv4 } from 'uuid'
Expand Down Expand Up @@ -131,6 +132,7 @@ export const downloadUploadFile: MutationResolvers['downloadUploadFile'] =
return signedUrl
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const Upload: UploadRelationResolvers = {
uploadedBy: (_obj, { root }) => {
return db.upload.findUnique({ where: { id: root?.id } }).uploadedBy()
Expand Down Expand Up @@ -158,6 +160,23 @@ export const Upload: UploadRelationResolvers = {
})
return latestValidation
},
seriesUploads: async (
_obj,
{ root }
): Promise<ResolversTypes['Upload'][]> => {
return db.upload.findMany({
where: {
AND: {
agencyId: root?.agencyId,
expenditureCategoryId: root?.expenditureCategoryId,
reportingPeriodId: root?.reportingPeriodId,
},
},
orderBy: {
createdAt: 'desc',
},
})
},
}
type UploadsWithValidationsAndExpenditureCategory = Prisma.UploadGetPayload<{
include: { validations: true; expenditureCategory: true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useMemo, useCallback } from 'react'

import Button from 'react-bootstrap/Button'
import Modal from 'react-bootstrap/Modal'
import type { FindReportingPeriodsWithCertification } from 'types/graphql'
import { useAuth } from 'web/src/auth'

import { useMutation } from '@redwoodjs/web'
Expand Down Expand Up @@ -136,7 +137,9 @@ const ReportingPeriodsList = ({
</Button>
</Modal.Footer>
</Modal>
<TableBuilder
<TableBuilder<
FindReportingPeriodsWithCertification['reportingPeriodsWithCertification']
>
data={reportingPeriods}
columns={columns}
filterableInputs={['name']}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { createColumnHelper } from '@tanstack/react-table'
import type { FindReportingPeriodsWithCertification } from 'types/graphql'

import { Link, routes } from '@redwoodjs/router'

import { formatDateString } from 'src/utils'

const columnHelper = createColumnHelper()
const columnHelper =
createColumnHelper<
FindReportingPeriodsWithCertification['reportingPeriodsWithCertification']
>()

export const columnDefs = ({ certificationDisplay, canEdit, isUSDRAdmin }) => {
const columns = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Subrecipient } from 'types/graphql'
import { FindSubrecipients } from 'types/graphql'

import { Link, routes } from '@redwoodjs/router'

type SubrecipientUpload =
| Subrecipient['validSubrecipientUploads'][number]
| Subrecipient['invalidSubrecipientUploads'][number]
| FindSubrecipients['subrecipients'][number]['validSubrecipientUploads'][number]
| FindSubrecipients['subrecipients'][number]['invalidSubrecipientUploads'][number]

interface SubrecipientTableUploadLinksDisplayProps {
validSubrecipientUploads: Subrecipient['validSubrecipientUploads']
invalidSubrecipientUploads: Subrecipient['invalidSubrecipientUploads']
validSubrecipientUploads: FindSubrecipients['subrecipients'][number]['validSubrecipientUploads']
invalidSubrecipientUploads: FindSubrecipients['subrecipients'][number]['invalidSubrecipientUploads']
}

const SubrecipientTableUploadLinksDisplay = ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { FindSubrecipients } from 'types/graphql'

import TableBuilder from 'src/components/TableBuilder/TableBuilder'

import { columnDefs } from './columns'
Expand All @@ -6,7 +8,7 @@ const Subrecipients = ({ subrecipients }) => {
const filterableInputs = ['uei', 'tin']

return (
<TableBuilder
<TableBuilder<FindSubrecipients['subrecipients'][number]>
data={subrecipients}
columns={columnDefs}
filterableInputs={filterableInputs}
Expand Down
9 changes: 6 additions & 3 deletions web/src/components/Subrecipient/Subrecipients/columns.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createColumnHelper, ColumnDef } from '@tanstack/react-table'
import { ParsedSubrecipient, Subrecipient } from 'types/graphql'
import { ParsedSubrecipient, FindSubrecipients } from 'types/graphql'

import SubrecipientTableUploadLinksDisplay from 'src/components/Subrecipient/SubrecipientTableUploadLinksDisplay/SubrecipientTableUploadLinksDisplay'
import { formatDateString, formatPhoneNumber } from 'src/utils'

const columnHelper = createColumnHelper<Subrecipient>()
const columnHelper =
createColumnHelper<FindSubrecipients['subrecipients'][number]>()

function formatDetails(
parsedSubrecipient: ParsedSubrecipient | null | undefined
Expand Down Expand Up @@ -46,7 +47,9 @@ const getUEI = (ueiTinCombo: string) => ueiTinCombo.split('_')[0]

const getTIN = (ueiTinCombo: string) => ueiTinCombo.split('_')[1]

export const columnDefs: ColumnDef<Subrecipient>[] = [
export const columnDefs: ColumnDef<
FindSubrecipients['subrecipients'][number]
>[] = [
columnHelper.accessor((row) => getUEI(row.ueiTinCombo), {
id: 'uei',
cell: (info) => info.getValue() ?? '',
Expand Down
28 changes: 25 additions & 3 deletions web/src/components/TableBuilder/TableBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
getSortedRowModel,
getFilteredRowModel,
ColumnFiltersState,
ColumnDef,
} from '@tanstack/react-table'
import { Button, Form } from 'react-bootstrap'
import Table from 'react-bootstrap/Table'
Expand All @@ -18,11 +19,32 @@
and sorting functionality.
For documentation, visit: https://tanstack.com/table/v8/docs/introduction
*/
function TableBuilder({ data, columns, filterableInputs = [], globalFilter }) {

interface GlobalFilter {
label: string
checked: boolean
loading?: boolean
onChange: () => void
}

interface TableBuilderProps<T> {
data: T[]
columns: ColumnDef<T>[]
filterableInputs?: string[]
globalFilter?: GlobalFilter
}

function TableBuilder<T extends object>({
data,
columns,
filterableInputs = [],
globalFilter,
}: TableBuilderProps<T>) {

Check warning on line 43 in web/src/components/TableBuilder/TableBuilder.tsx

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Delete `⏎`
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [sorting, setSorting] = useState([])

const table = useReactTable({
const table = useReactTable<T>({
data,
columns: columns,
state: {
Expand Down Expand Up @@ -60,7 +82,7 @@
}

return (
<div className="p-2">
<div className="pt-2">
<Table striped borderless>
<thead>
{(!!filterableInputs.length || globalFilter) && (
Expand Down
19 changes: 14 additions & 5 deletions web/src/components/TableBuilder/TableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@
const TableRow = ({ row }) => {
return (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="border border-slate-700">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
{row.getVisibleCells().map((cell) => {
const cellProps =
typeof cell.column.columnDef.meta?.getCellProps === 'function'
? cell.column.columnDef.meta.getCellProps(cell)
: {}

cellProps.className = `${cellProps.className || ''

Check warning on line 12 in web/src/components/TableBuilder/TableRow.tsx

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Insert `⏎··········`
} border border-slate-700`

Check warning on line 13 in web/src/components/TableBuilder/TableRow.tsx

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Delete `··`
return (
<td key={cell.id} {...cellProps}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
)
})}
</tr>
)
}
Expand Down
Loading
Loading