diff --git a/express-api/src/constants/index.ts b/express-api/src/constants/index.ts index 25344efdf..c9c8140ce 100644 --- a/express-api/src/constants/index.ts +++ b/express-api/src/constants/index.ts @@ -2,11 +2,13 @@ import networking from '@/constants/networking'; import switches from '@/constants/switches'; import urls from '@/constants/urls'; import * as errors from '@/constants/errors'; +import * as types from '@/constants/types'; const constants = { ...networking, ...switches, ...urls, ...errors, + ...types, }; export default constants; diff --git a/express-api/src/constants/types.ts b/express-api/src/constants/types.ts new file mode 100644 index 000000000..9527a998b --- /dev/null +++ b/express-api/src/constants/types.ts @@ -0,0 +1 @@ +export type SortOrders = 'ASC' | 'DESC'; diff --git a/express-api/src/controllers/projects/projectsController.ts b/express-api/src/controllers/projects/projectsController.ts index 9d004a541..c717ea61e 100644 --- a/express-api/src/controllers/projects/projectsController.ts +++ b/express-api/src/controllers/projects/projectsController.ts @@ -308,11 +308,11 @@ export const filterProjects = async (req: Request, res: Response) => { const filter = ProjectFilterSchema.safeParse(req.query); const includeRelations = req.query.includeRelations === 'true'; const forExcelExport = req.query.excelExport === 'true'; - const kcUser = req.user as unknown as SSOUser; if (!filter.success) { return res.status(400).send('Could not parse filter.'); } const filterResult = filter.data; + const kcUser = req.user as unknown as SSOUser; if (!(isAdmin(kcUser) || isAuditor(kcUser))) { // get array of user's agencies const usersAgencies = await userServices.getAgencies(kcUser.preferred_username); @@ -321,7 +321,7 @@ export const filterProjects = async (req: Request, res: Response) => { // Get projects associated with agencies of the requesting user const projects = forExcelExport ? await projectServices.getProjectsForExport(filterResult as ProjectFilter, includeRelations) - : await projectServices.getProjects(filterResult as ProjectFilter, includeRelations); + : await projectServices.getProjects(filterResult as ProjectFilter); return res.status(200).send(projects); }; diff --git a/express-api/src/controllers/properties/propertiesController.ts b/express-api/src/controllers/properties/propertiesController.ts index 7a8f8e5e1..a8943bd93 100644 --- a/express-api/src/controllers/properties/propertiesController.ts +++ b/express-api/src/controllers/properties/propertiesController.ts @@ -186,7 +186,7 @@ export const getPropertyUnion = async (req: Request, res: Response) => { if (!(isAdmin(kcUser) || isAuditor(kcUser))) { // get array of user's agencies const usersAgencies = await userServices.getAgencies(kcUser.preferred_username); - filterResult.agencyId = usersAgencies; + filterResult.agencyIds = usersAgencies; } const properties = await propertyServices.getPropertiesUnion(filterResult); return res.status(200).send(properties); diff --git a/express-api/src/controllers/properties/propertiesSchema.ts b/express-api/src/controllers/properties/propertiesSchema.ts index f3ff38254..e089ea017 100644 --- a/express-api/src/controllers/properties/propertiesSchema.ts +++ b/express-api/src/controllers/properties/propertiesSchema.ts @@ -42,12 +42,13 @@ export const PropertyUnionFilterSchema = z.object({ status: z.string().optional(), classification: z.string().optional(), agency: z.string().optional(), - agencyId: z.array(z.number().int().nonnegative()).optional(), + agencyIds: z.array(z.number().int().nonnegative()).optional(), propertyType: z.string().optional(), address: z.string().optional(), administrativeArea: z.string().optional(), landArea: z.string().optional(), updatedOn: z.string().optional(), + quickFilter: z.string().optional(), sortKey: z.string().optional(), sortOrder: z.string().optional(), page: z.coerce.number().optional(), diff --git a/express-api/src/services/projects/projectSchema.ts b/express-api/src/services/projects/projectSchema.ts index 2f2c00d16..a128aece8 100644 --- a/express-api/src/services/projects/projectSchema.ts +++ b/express-api/src/services/projects/projectSchema.ts @@ -5,7 +5,7 @@ export const ProjectFilterSchema = z.object({ name: z.string().optional(), statusId: z.coerce.number().nonnegative().optional(), status: z.string().optional(), - agencyId: z.union([z.number().optional(), z.array(z.number().int().nonnegative()).optional()]), + agencyId: z.array(z.number().int().nonnegative()).optional(), agency: z.string().optional(), page: z.coerce.number().optional(), updatedOn: z.string().optional(), diff --git a/express-api/src/services/projects/projectsServices.ts b/express-api/src/services/projects/projectsServices.ts index ded19b262..e84a1c949 100644 --- a/express-api/src/services/projects/projectsServices.ts +++ b/express-api/src/services/projects/projectsServices.ts @@ -15,6 +15,7 @@ import { ProjectTask } from '@/typeorm/Entities/ProjectTask'; import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode'; import logger from '@/utilities/winstonLogger'; import { + Brackets, DeepPartial, FindManyOptions, FindOptionsOrder, @@ -33,6 +34,8 @@ import { constructFindOptionFromQuery } from '@/utilities/helperFunctions'; import { ProjectTimestamp } from '@/typeorm/Entities/ProjectTimestamp'; import { ProjectMonetary } from '@/typeorm/Entities/ProjectMonetary'; import { NotificationQueue } from '@/typeorm/Entities/NotificationQueue'; +import { SortOrders } from '@/constants/types'; +import { ProjectJoin } from '@/typeorm/Entities/views/ProjectJoinView'; const projectRepo = AppDataSource.getRepository(Project); @@ -773,9 +776,10 @@ const sortKeyMapping = ( const collectFindOptions = (filter: ProjectFilter) => { const options = []; + // TODO: Add market value and updated by searches if (filter.name) options.push(constructFindOptionFromQuery('Name', filter.name)); - if (filter.agency) options.push({ Agency: constructFindOptionFromQuery('Name', filter.agency) }); - if (filter.status) options.push({ Status: constructFindOptionFromQuery('Name', filter.status) }); + if (filter.agency) options.push(constructFindOptionFromQuery('Agency', filter.agency)); + if (filter.status) options.push(constructFindOptionFromQuery('Status', filter.status)); if (filter.projectNumber) { options.push(constructFindOptionFromQuery('ProjectNumber', filter.projectNumber)); } @@ -783,35 +787,48 @@ const collectFindOptions = (filter: ProjectFilter) => { return options; }; -const getProjects = async (filter: ProjectFilter, includeRelations: boolean = false) => { - const queryOptions: FindManyOptions = { - relations: { - Agency: { - Parent: includeRelations, - }, - Status: includeRelations, - UpdatedBy: includeRelations, - }, - select: { - Agency: { - Name: true, - Parent: { - Name: true, - }, - }, - Status: { - Name: true, - }, - UpdatedBy: { Id: true, FirstName: true, LastName: true }, - }, - where: collectFindOptions(filter), - take: filter.quantity, - skip: (filter.page ?? 0) * (filter.quantity ?? 0), - order: sortKeyMapping(filter.sortKey, filter.sortOrder as FindOptionsOrderValue), - }; +// Because leftJoinAndSelect is used, sort uses the Entity column name, not database column name +const sortKeyTranslator: Record = { + ProjectNumber: 'project_number', + Name: 'name', + Status: 'status_name', + Agency: 'agency_name', + NetBook: 'net_book', + Market: 'market', + UpdatedOn: 'updated_on', + UpdatedBy: 'user_full_name', +}; - const projects = await projectRepo.find(queryOptions); - return projects; +const getProjects = async (filter: ProjectFilter) => { + const options = collectFindOptions(filter); + const query = AppDataSource.getRepository(ProjectJoin) + .createQueryBuilder() + .where( + new Brackets((qb) => { + options.forEach((option) => qb.orWhere(option)); + }), + ); + + // Restricts based on user's agencies + if (filter.agencyId?.length) { + query.andWhere('agency_id IN(:...list)', { + list: filter.agencyId, + }); + } + + if (filter.quantity) query.take(filter.quantity); + if (filter.page && filter.quantity) query.skip((filter.page ?? 0) * (filter.quantity ?? 0)); + if (filter.sortKey && filter.sortOrder) { + if (sortKeyTranslator[filter.sortKey]) { + query.orderBy( + sortKeyTranslator[filter.sortKey], + filter.sortOrder.toUpperCase() as SortOrders, + ); + } else { + logger.error('PropertyUnion Service - Invalid Sort Key'); + } + } + return await query.getMany(); }; const getProjectsForExport = async (filter: ProjectFilter, includeRelations: boolean = false) => { diff --git a/express-api/src/services/properties/propertiesServices.ts b/express-api/src/services/properties/propertiesServices.ts index 4fc74b9a1..77bd6a858 100644 --- a/express-api/src/services/properties/propertiesServices.ts +++ b/express-api/src/services/properties/propertiesServices.ts @@ -1,4 +1,5 @@ import { AppDataSource } from '@/appDataSource'; +import { SortOrders } from '@/constants/types'; import { MapFilter, PropertyUnionFilter } from '@/controllers/properties/propertiesSchema'; import { Building } from '@/typeorm/Entities/Building'; import { Parcel } from '@/typeorm/Entities/Parcel'; @@ -9,7 +10,14 @@ import { constructFindOptionFromQueryPid, } from '@/utilities/helperFunctions'; import logger from '@/utilities/winstonLogger'; -import { Brackets, FindOptionsOrder, FindOptionsOrderValue, ILike, In } from 'typeorm'; +import { + Brackets, + FindOptionsOrder, + FindOptionsOrderValue, + FindOptionsWhere, + ILike, + In, +} from 'typeorm'; const propertiesFuzzySearch = async (keyword: string, limit?: number, agencyIds?: number[]) => { const parcelsQuery = await AppDataSource.getRepository(Parcel) @@ -117,8 +125,7 @@ export const sortKeyMapping = ( return { [sortKey]: sortDirection }; }; -type SortOrders = 'ASC' | 'DESC'; - +// No joins, so database column names are used for sort const sortKeyTranslator: Record = { Agency: 'agency_name', PID: 'pid', @@ -159,12 +166,33 @@ const getPropertiesUnion = async (filter: PropertyUnionFilter) => { ); // Restricts based on user's agencies - if (filter.agencyId?.length) { - query.andWhere('agency_id IN(:list)', { - list: filter.agencyId.join(','), + if (filter.agencyIds?.length) { + query.andWhere('agency_id IN(:...list)', { + list: filter.agencyIds, }); } + // Add quickfilter part + if (filter.quickFilter) { + // TODO: Make this more concise + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const quickFilterOptions: FindOptionsWhere[] = []; + quickFilterOptions.push(constructFindOptionFromQuery('Agency', filter.quickFilter)); + quickFilterOptions.push(constructFindOptionFromQuery('PID', filter.quickFilter)); // Cannot use PID constructor, always true with strings + quickFilterOptions.push(constructFindOptionFromQuery('PIN', filter.quickFilter)); + quickFilterOptions.push(constructFindOptionFromQuery('Address', filter.quickFilter)); + quickFilterOptions.push(constructFindOptionFromQuery('UpdatedOn', filter.quickFilter)); + quickFilterOptions.push(constructFindOptionFromQuery('Classification', filter.quickFilter)); + quickFilterOptions.push(constructFindOptionFromQuery('LandArea', filter.quickFilter)); + quickFilterOptions.push(constructFindOptionFromQuery('AdministrativeArea', filter.quickFilter)); + quickFilterOptions.push(constructFindOptionFromQuery('PropertyType', filter.quickFilter)); + query.andWhere( + new Brackets((qb) => { + quickFilterOptions.forEach((option) => qb.orWhere(option)); + }), + ); + } + if (filter.quantity) query.take(filter.quantity); if (filter.page && filter.quantity) query.skip((filter.page ?? 0) * (filter.quantity ?? 0)); if (filter.sortKey && filter.sortOrder) { diff --git a/express-api/src/typeorm/Entities/views/ProjectJoinView.ts b/express-api/src/typeorm/Entities/views/ProjectJoinView.ts new file mode 100644 index 000000000..dd20d2b30 --- /dev/null +++ b/express-api/src/typeorm/Entities/views/ProjectJoinView.ts @@ -0,0 +1,64 @@ +import { ViewColumn, ViewEntity } from 'typeorm'; + +@ViewEntity({ + materialized: false, + expression: ` + SELECT + p.id, + p.project_number, + p.name, + p.status_id, + p.agency_id, + p.market, + p.net_book, + ps."name" AS status_name, + agc."name" AS agency_name, + u.first_name AS user_first_name, + u.last_name AS user_last_name, + u.last_name || ', ' || u.first_name AS user_full_name, + ps.updated_on +FROM + project p +LEFT JOIN agency agc ON + p.agency_id = agc.id +LEFT JOIN project_status ps ON + p.status_id = ps.id +LEFT JOIN "user" u ON + p.updated_by_id = u.id +WHERE p.deleted_on IS NULL; + `, +}) +export class ProjectJoin { + @ViewColumn({ name: 'id' }) + Id: number; + + @ViewColumn({ name: 'project_number' }) + ProjectNumber: string; + + @ViewColumn({ name: 'name' }) + Name: string; + + @ViewColumn({ name: 'status_id' }) + StatusId: number; + + @ViewColumn({ name: 'agency_id' }) + AgencyId: number; + + @ViewColumn({ name: 'agency_name' }) + Agency: string; + + @ViewColumn({ name: 'status_name' }) + Status: string; + + @ViewColumn({ name: 'market' }) + Market: string; + + @ViewColumn({ name: 'net_book' }) + NetBook: string; + + @ViewColumn({ name: 'user_full_name' }) + UpdatedBy: string; + + @ViewColumn({ name: 'updated_on' }) + UpdatedOn: Date; +} diff --git a/express-api/src/typeorm/Migrations/1720047791272-CreateProjectJoinView.ts b/express-api/src/typeorm/Migrations/1720047791272-CreateProjectJoinView.ts new file mode 100644 index 000000000..45b007e6c --- /dev/null +++ b/express-api/src/typeorm/Migrations/1720047791272-CreateProjectJoinView.ts @@ -0,0 +1,50 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateProjectJoinView1720047791272 implements MigrationInterface { + name = 'CreateProjectJoinView1720047791272'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE VIEW "project_join" AS + SELECT + p.id, + p.project_number, + p.name, + p.status_id, + p.agency_id, + p.market, + p.net_book, + ps."name" AS status_name, + agc."name" AS agency_name, + u.first_name AS user_first_name, + u.last_name AS user_last_name, + u.last_name || ', ' || u.first_name AS user_full_name, + ps.updated_on +FROM + project p +LEFT JOIN agency agc ON + p.agency_id = agc.id +LEFT JOIN project_status ps ON + p.status_id = ps.id +LEFT JOIN "user" u ON + p.updated_by_id = u.id +WHERE p.deleted_on IS NULL; + `); + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'public', + 'VIEW', + 'project_join', + 'SELECT\n\tp.id,\n\tp.project_number,\n\tp.name,\n\tp.status_id,\n\tp.agency_id,\n\tp.market,\n\tp.net_book,\n\tps."name" AS status_name,\n\tagc."name" AS agency_name,\n\tu.first_name AS user_first_name,\n\tu.last_name AS user_last_name,\n\tu.last_name || \', \' || u.first_name AS user_full_name,\n\tps.updated_on \nFROM\n\tproject p\nLEFT JOIN agency agc ON\n\tp.agency_id = agc.id\nLEFT JOIN project_status ps ON\n\tp.status_id = ps.id\nLEFT JOIN "user" u ON \n\tp.updated_by_id = u.id\nWHERE p.deleted_on IS NULL;', + ], + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'project_join', 'public'], + ); + await queryRunner.query(`DROP VIEW "project_join"`); + } +} diff --git a/express-api/src/typeorm/entitiesIndex.ts b/express-api/src/typeorm/entitiesIndex.ts index b6d4b5898..cc64baca9 100644 --- a/express-api/src/typeorm/entitiesIndex.ts +++ b/express-api/src/typeorm/entitiesIndex.ts @@ -46,6 +46,7 @@ import { ProjectTimestamp } from './Entities/ProjectTimestamp'; import { MonetaryType } from './Entities/MonetaryType'; import { TimestampType } from './Entities/TimestampType'; import { PropertyUnion } from './Entities/views/PropertyUnionView'; +import { ProjectJoin } from './Entities/views/ProjectJoinView'; const views = [BuildingRelations, MapProperties]; @@ -96,5 +97,6 @@ export default [ WorkflowProjectStatus, NoteType, PropertyUnion, + ProjectJoin, ...views, ]; diff --git a/express-api/tests/testUtils/factories.ts b/express-api/tests/testUtils/factories.ts index 82b3cfbf8..22278d158 100644 --- a/express-api/tests/testUtils/factories.ts +++ b/express-api/tests/testUtils/factories.ts @@ -51,6 +51,7 @@ import { PropertyType } from '@/typeorm/Entities/PropertyType'; import { ProjectType } from '@/typeorm/Entities/ProjectType'; import { ProjectStatus } from '@/typeorm/Entities/ProjectStatus'; import { PropertyUnion } from '@/typeorm/Entities/views/PropertyUnionView'; +import { ProjectJoin } from '@/typeorm/Entities/views/ProjectJoinView'; export class MockRes { statusValue: any; @@ -656,6 +657,24 @@ export const produceProject = ( return project; }; +export const produceProjectJoin = (props?: Partial) => { + const project: ProjectJoin = { + Id: faker.number.int(), + ProjectNumber: 'SPP-' + faker.number.int(), + Name: faker.company.name(), + StatusId: faker.number.int(), + AgencyId: faker.number.int(), + Agency: faker.company.name(), + Status: faker.commerce.department(), + Market: '$' + faker.number.int(), + NetBook: '$' + faker.number.int(), + UpdatedBy: faker.person.fullName(), + UpdatedOn: new Date(), + ...props, + }; + return project; +}; + export const produceRisk = (props?: Partial): ProjectRisk => { const risk: ProjectRisk = { Id: faker.number.int(), diff --git a/express-api/tests/unit/controllers/projects/projectsController.test.ts b/express-api/tests/unit/controllers/projects/projectsController.test.ts index 249516bc4..be2211db3 100644 --- a/express-api/tests/unit/controllers/projects/projectsController.test.ts +++ b/express-api/tests/unit/controllers/projects/projectsController.test.ts @@ -191,7 +191,7 @@ describe('UNIT - Testing controllers for users routes.', () => { projectNumber: '123', name: 'Project Name', statusId: 1, - agencyId: 1, + agencyId: [1], }; const result = ProjectFilterSchema.safeParse(validFilter); diff --git a/express-api/tests/unit/services/projects/projectsServices.test.ts b/express-api/tests/unit/services/projects/projectsServices.test.ts index d7953a8a0..ebd1c47fb 100644 --- a/express-api/tests/unit/services/projects/projectsServices.test.ts +++ b/express-api/tests/unit/services/projects/projectsServices.test.ts @@ -15,6 +15,7 @@ import { ProjectNote } from '@/typeorm/Entities/ProjectNote'; import { ProjectProperty } from '@/typeorm/Entities/ProjectProperty'; import { ProjectTask } from '@/typeorm/Entities/ProjectTask'; import { ProjectTimestamp } from '@/typeorm/Entities/ProjectTimestamp'; +import { ProjectJoin } from '@/typeorm/Entities/views/ProjectJoinView'; import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode'; import { faker } from '@faker-js/faker'; import { @@ -24,6 +25,7 @@ import { produceNotificationQueue, produceParcel, produceProject, + produceProjectJoin, produceProjectMonetary, produceProjectProperty, produceProjectTask, @@ -227,6 +229,19 @@ const _queryRunner = jest.spyOn(AppDataSource, 'createQueryRunner').mockReturnVa manager: _mockEntityManager, }); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const projectJoinQueryBuilder: any = { + orderBy: () => projectJoinQueryBuilder, + andWhere: () => projectJoinQueryBuilder, + where: () => projectJoinQueryBuilder, + take: () => projectJoinQueryBuilder, + skip: () => projectJoinQueryBuilder, + getMany: () => [produceProjectJoin()], +}; +jest + .spyOn(AppDataSource.getRepository(ProjectJoin), 'createQueryBuilder') + .mockImplementation(() => projectJoinQueryBuilder); + jest.mock('@/services/notifications/notificationServices', () => ({ generateProjectNotifications: jest.fn(async () => [produceNotificationQueue()]), sendNotification: jest.fn(async () => produceNotificationQueue()), @@ -604,7 +619,7 @@ describe('UNIT - Project Services', () => { it('should return projects based on filter conditions', async () => { const filter = { statusId: 1, - agencyId: 3, + agencyId: [3], quantity: 10, page: 0, agency: 'contains,aaa', @@ -616,23 +631,8 @@ describe('UNIT - Project Services', () => { sortKey: 'Status', }; - _projectFind.mockImplementationOnce(async () => { - const mockProjects: Project[] = [ - produceProject({ Id: 1, Name: 'Project 1', StatusId: 1, AgencyId: 3 }), - produceProject({ Id: 2, Name: 'Project 2', StatusId: 4, AgencyId: 14 }), - ]; - // Check if the project matches the filter conditions - return mockProjects.filter( - (project) => - filter.statusId === project.StatusId && filter.agencyId === project.AgencyId, - ); - }); - // Call the service function - const projects = await projectServices.getProjects(filter, true); // Pass the mocked projectRepo - - // Assertions - expect(_projectFind).toHaveBeenCalled(); + const projects = await projectServices.getProjects(filter); // Pass the mocked projectRepo // Returned project should be the one based on the agency and status id in the filter expect(projects.length).toEqual(1); }); @@ -645,7 +645,7 @@ describe('UNIT - Project Services', () => { it('should return projects based on filter conditions', async () => { const filter = { statusId: 1, - agencyId: 3, + agencyId: [3], quantity: 10, page: 0, }; @@ -658,7 +658,7 @@ describe('UNIT - Project Services', () => { // Check if the project matches the filter conditions return mockProjects.filter( (project) => - filter.statusId === project.StatusId && filter.agencyId === project.AgencyId, + filter.statusId === project.StatusId && filter.agencyId.includes(project.AgencyId), ); }); diff --git a/express-api/tests/unit/services/properties/propertyServices.test.ts b/express-api/tests/unit/services/properties/propertyServices.test.ts index c8816bec1..6e90dfa61 100644 --- a/express-api/tests/unit/services/properties/propertyServices.test.ts +++ b/express-api/tests/unit/services/properties/propertyServices.test.ts @@ -95,7 +95,7 @@ describe('UNIT - Property Services', () => { landArea: 'startsWith,1', address: 'contains,742 Evergreen Terr.', classification: 'contains,core', - agencyId: [1], + agencyIds: [1], quantity: 2, page: 1, updatedOn: 'after,' + new Date(), diff --git a/react-app/src/components/projects/ProjectsTable.tsx b/react-app/src/components/projects/ProjectsTable.tsx index bacc52db1..66f6ec2bc 100644 --- a/react-app/src/components/projects/ProjectsTable.tsx +++ b/react-app/src/components/projects/ProjectsTable.tsx @@ -8,9 +8,7 @@ import { import { CustomListSubheader, CustomMenuItem, FilterSearchDataGrid } from '../table/DataTable'; import React, { MutableRefObject, useContext } from 'react'; import { dateFormatter, projectStatusChipFormatter } from '@/utilities/formatters'; -import { Agency } from '@/hooks/api/useAgencyApi'; import { GridApiCommunity } from '@mui/x-data-grid/internals'; -import { User } from '@/hooks/api/useUsersApi'; import { useNavigate } from 'react-router-dom'; import { Project } from '@/hooks/api/useProjectsApi'; import { NoteTypes } from '@/constants/noteTypes'; @@ -48,14 +46,12 @@ const ProjectsTable = () => { headerName: 'Status', flex: 1, maxWidth: 250, - valueGetter: (value: any) => value?.Name ?? 'N/A', renderCell: (params) => projectStatusChipFormatter(params.value ?? 'N/A'), }, { field: 'Agency', headerName: 'Agency', flex: 1, - valueGetter: (value: Agency) => value?.Name ?? '', }, { field: 'NetBook', @@ -82,7 +78,6 @@ const ProjectsTable = () => { headerName: 'Updated By', flex: 1, maxWidth: 150, - valueGetter: (user: User) => `${user?.FirstName ?? ''} ${user?.LastName ?? ''}`, }, ]; diff --git a/react-app/src/components/table/DataTable.tsx b/react-app/src/components/table/DataTable.tsx index 8aa2b6934..98ffc0d9d 100644 --- a/react-app/src/components/table/DataTable.tsx +++ b/react-app/src/components/table/DataTable.tsx @@ -250,14 +250,16 @@ export const FilterSearchDataGrid = (props: FilterSearchDataGridProps) => { filterObj[asCamelCase] = f.operator; } } - } else if (quickFilter) { + } + if (quickFilter) { const keyword = quickFilter[0]; - for (const fieldName of tableApiRef.current.getAllColumns().map((col) => col.field)) { - if (keyword != undefined) { - const asCamelCase = fieldName.charAt(0).toLowerCase() + fieldName.slice(1); - filterObj[asCamelCase] = `contains,${keyword}`; - } - } + if (keyword) filterObj['quickFilter'] = `contains,${keyword}`; + // for (const fieldName of tableApiRef.current.getAllColumns().map((col) => col.field)) { + // if (keyword != undefined) { + // const asCamelCase = fieldName.charAt(0).toLowerCase() + fieldName.slice(1); + // filterObj[asCamelCase] = `contains,${keyword}`; + // } + // } } setDataSourceLoading(true); props @@ -364,55 +366,60 @@ export const FilterSearchDataGrid = (props: FilterSearchDataGridProps) => { useLayoutEffect(() => { const query = getQuery(); // If query strings exist, prioritize that for preset filters, etc. - const model: ITableModelCollection = { - pagination: { page: DEFAULT_PAGE, pageSize: DEFAULT_PAGESIZE }, - sort: undefined, - filter: undefined, - quickFilter: undefined, - }; + // const model: ITableModelCollection = { + // pagination: { page: DEFAULT_PAGE, pageSize: DEFAULT_PAGESIZE }, + // sort: undefined, + // filter: undefined, + // quickFilter: undefined, + // }; if (Boolean(Object.keys(query).length)) { - // Set keyword filter - if (query.keywordFilter) { - setKeywordSearchContents(query.keywordFilter); - updateSearchValue(query.keywordFilter); - model.quickFilter = query.keywordFilter.split(' ').filter((e) => e); - } // Set quick select filter if (query.quickSelectFilter) { setSelectValue(query.quickSelectFilter); props.onPresetFilterChange(query.quickSelectFilter, tableApiRef); } // Set other column filter - if (query.columnFilterName && query.columnFilterValue && query.columnFilterMode) { - model.quickFilter = undefined; + if ( + (query.columnFilterName && query.columnFilterValue && query.columnFilterMode) || + query.quickSelectFilter + ) { + // model.quickFilter = undefined; const modelObj: GridFilterModel = { - items: [ + items: undefined, + quickFilterValues: undefined, + }; + if (query.columnFilterName && query.columnFilterValue && query.columnFilterMode) { + modelObj.items = [ { value: query.columnFilterValue, operator: query.columnFilterMode, field: query.columnFilterName, }, - ], - }; - model.filter = modelObj; + ]; + } + if (query.keywordFilter) { + setKeywordSearchContents(query.keywordFilter); + modelObj.quickFilterValues = query.keywordFilter.split(' ').filter((a) => a !== ''); + } + //model.filter = modelObj; tableApiRef.current.setFilterModel(modelObj); } // Set sorting options if (query.columnSortName && query.columnSortValue) { - model.sort = [{ field: query.columnSortName, sort: query.columnSortValue }]; + //model.sort = [{ field: query.columnSortName, sort: query.columnSortValue }]; tableApiRef.current.setSortModel([ { field: query.columnSortName, sort: query.columnSortValue }, ]); } //Set pagination if (query.page && query.pageSize) { - model.pagination = { page: Number(query.page), pageSize: Number(query.pageSize) }; + //model.pagination = { page: Number(query.page), pageSize: Number(query.pageSize) }; tableApiRef.current.setPaginationModel({ page: Number(query.page), pageSize: Number(query.pageSize), }); } - setTableModel(model); + //setTableModel(model); } else { // Setting the table's state from sessionStorage cookies const model: ITableModelCollection = { @@ -479,16 +486,16 @@ export const FilterSearchDataGrid = (props: FilterSearchDataGridProps) => { tableApiRef.current.setQuickFilterValues(newValue.split(' ').filter((word) => word !== '')); const defaultpagesize = { page: 0, pageSize: tableModel.pagination.pageSize }; tableApiRef.current.setPaginationModel(defaultpagesize); - setQuery(defaultpagesize); - setTableModel({ - ...tableModel, - pagination: defaultpagesize, - quickFilter: newValue.split(' ').filter((word) => word !== ''), - }); - setQuery({ - ...defaultpagesize, - keywordFilter: newValue, - }); + // console.log(`updateSearchValue: ${JSON.stringify(tableModel)}`); + // setTableModel({ + // ...tableModel, + // pagination: defaultpagesize, + // quickFilter: newValue.split(' ').filter((word) => word !== ''), + // }); + // setQuery({ + // ...defaultpagesize, + // keywordFilter: newValue, + // }); }, 300); }, [tableApiRef]); @@ -577,7 +584,8 @@ export const FilterSearchDataGrid = (props: FilterSearchDataGridProps) => { onChange={(e) => { setKeywordSearchContents(''); setSelectValue(e.target.value); - setQuery({ quickSelectFilter: e.target.value, keywordFilter: undefined }); + setQuery({ quickSelectFilter: e.target.value, keywordFilter: undefined }); // Clear keywordFilter too + setTableModel({ ...tableModel, filter: undefined }); // Clear existing column filters props.onPresetFilterChange(`${e.target.value}`, tableApiRef); }} sx={{ width: '10em', marginLeft: '0.5em' }} @@ -596,30 +604,36 @@ export const FilterSearchDataGrid = (props: FilterSearchDataGridProps) => { }} onFilterModelChange={(e) => { // Can only filter by 1 at a time without DataGrid Pro + const model: ITableModelCollection = {}; if (e.items.length > 0) { const item = e.items.at(0); - setTableModel({ - ...tableModel, - pagination: { page: 0, pageSize: tableModel.pagination.pageSize }, - filter: e, - }); + model.filter = e; setQuery({ columnFilterName: item.field, columnFilterValue: item.value, columnFilterMode: item.operator, }); } else { - setTableModel({ - ...tableModel, - pagination: { page: 0, pageSize: tableModel.pagination.pageSize }, - filter: undefined, - }); + model.filter = e; setQuery({ columnFilterName: undefined, columnFilterValue: undefined, columnFilterMode: undefined, }); } + + if (e.quickFilterValues) { + model.quickFilter = e.quickFilterValues; + setQuery({ keywordFilter: e.quickFilterValues.join(' ') }); + } else { + model.quickFilter = undefined; + setQuery({ keywordFilter: undefined }); + } + setTableModel({ + ...tableModel, + ...model, + pagination: { page: 0, pageSize: DEFAULT_PAGESIZE }, + }); // Get the filter items from MUI, filter out blanks, set state setGridFilterItems(e.items.filter((item) => item.value)); }}