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

feat(3166): enhance search, filter, and sort on private requests #3839

Merged
merged 1 commit into from
Sep 25, 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
48 changes: 0 additions & 48 deletions app/app/api/private-cloud/requests/_operations/search.ts

This file was deleted.

29 changes: 14 additions & 15 deletions app/app/api/private-cloud/requests/search/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('Search Private Cloud Requests - Permissions', () => {
it('should successfully search 1 request by PO', async () => {
await mockSessionByEmail(PO.email);

const res1 = await searchPrivateCloudRequests({ includeInactive: true });
const res1 = await searchPrivateCloudRequests({});
expect(res1.status).toBe(200);
const dat1 = await res1.json();

Expand All @@ -71,7 +71,7 @@ describe('Search Private Cloud Requests - Permissions', () => {
it('should successfully search 1 request by TL1', async () => {
await mockSessionByEmail(TL1.email);

const res1 = await searchPrivateCloudRequests({ includeInactive: true });
const res1 = await searchPrivateCloudRequests({});
expect(res1.status).toBe(200);
const dat1 = await res1.json();

Expand All @@ -81,7 +81,7 @@ describe('Search Private Cloud Requests - Permissions', () => {
it('should successfully search 1 request by TL2', async () => {
await mockSessionByEmail(TL2.email);

const res1 = await searchPrivateCloudRequests({ includeInactive: true });
const res1 = await searchPrivateCloudRequests({});
expect(res1.status).toBe(200);
const dat1 = await res1.json();

Expand Down Expand Up @@ -113,7 +113,7 @@ describe('Search Private Cloud Requests - Permissions', () => {
it('should successfully search 1 request by the random user', async () => {
await mockSessionByEmail(RANDOM1.email);

const res1 = await searchPrivateCloudRequests({ includeInactive: true });
const res1 = await searchPrivateCloudRequests({});
expect(res1.status).toBe(200);
const dat1 = await res1.json();

Expand All @@ -123,7 +123,7 @@ describe('Search Private Cloud Requests - Permissions', () => {
it('should successfully search 1 request by PO', async () => {
await mockSessionByEmail(PO.email);

const res1 = await searchPrivateCloudRequests({ includeInactive: true });
const res1 = await searchPrivateCloudRequests({});
expect(res1.status).toBe(200);
const dat1 = await res1.json();

Expand All @@ -133,7 +133,7 @@ describe('Search Private Cloud Requests - Permissions', () => {
it('should successfully search 1 request by TL1', async () => {
await mockSessionByEmail(TL1.email);

const res1 = await searchPrivateCloudRequests({ includeInactive: true });
const res1 = await searchPrivateCloudRequests({});
expect(res1.status).toBe(200);
const dat1 = await res1.json();

Expand All @@ -143,7 +143,7 @@ describe('Search Private Cloud Requests - Permissions', () => {
it('should successfully search 1 request by TL2', async () => {
await mockSessionByEmail(TL2.email);

const res1 = await searchPrivateCloudRequests({ includeInactive: true });
const res1 = await searchPrivateCloudRequests({});
expect(res1.status).toBe(200);
const dat1 = await res1.json();

Expand All @@ -153,7 +153,7 @@ describe('Search Private Cloud Requests - Permissions', () => {
it('should successfully search 2 requests by admin', async () => {
await mockSessionByRole('admin');

const res1 = await searchPrivateCloudRequests({ includeInactive: true });
const res1 = await searchPrivateCloudRequests({});
expect(res1.status).toBe(200);
const dat1 = await res1.json();

Expand Down Expand Up @@ -209,20 +209,21 @@ describe('Search Private Cloud Requests - Validations', () => {
expect(res.status).toBe(200);
});

it('should successfully search 10 requests by admin', async () => {
it('should successfully search 11 requests by admin', async () => {
await mockSessionByRole('admin');

const res1 = await searchPrivateCloudRequests({ includeInactive: true });
const res1 = await searchPrivateCloudRequests({});
expect(res1.status).toBe(200);
const dat1 = await res1.json();

// 10 Create requests, 1 Edit request
expect(dat1.totalCount).toBe(11);
});

it('should successfully search 1 requests by admin', async () => {
await mockSessionByRole('admin');

const res1 = await searchPrivateCloudRequests({});
const res1 = await searchPrivateCloudRequests({ status: [DecisionStatus.APPROVED] });
expect(res1.status).toBe(200);
const dat1 = await res1.json();

Expand All @@ -233,9 +234,8 @@ describe('Search Private Cloud Requests - Validations', () => {
await mockSessionByRole('admin');

const res1 = await searchPrivateCloudRequests({
ministry: Ministry.AEST,
cluster: Cluster.CLAB,
includeInactive: true,
ministries: [Ministry.AEST],
clusters: [Cluster.CLAB],
});

expect(res1.status).toBe(200);
Expand All @@ -249,7 +249,6 @@ describe('Search Private Cloud Requests - Validations', () => {

const res1 = await searchPrivateCloudRequests({
search: '______name______',
includeInactive: true,
});

expect(res1.status).toBe(200);
Expand Down
30 changes: 4 additions & 26 deletions app/app/api/private-cloud/requests/search/route.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,17 @@
import _isString from 'lodash-es/isString';
import createApiHandler from '@/core/api-handler';
import { OkResponse } from '@/core/responses';
import { searchPrivateCloudRequests } from '@/queries/private-cloud-requests';
import { privateCloudRequestSearchBodySchema } from '@/validation-schemas/private-cloud';
import searchOp from '../_operations/search';

export const POST = createApiHandler({
roles: ['user'],
validations: { body: privateCloudRequestSearchBodySchema },
})(async ({ session, body }) => {
const {
licencePlate = '',
search = '',
page = 1,
pageSize = 5,
ministry = '',
cluster = '',
includeInactive = false,
sortKey,
sortOrder,
showTest,
} = body;

const data = await searchOp({
licencePlate,
const { docs, totalCount } = await searchPrivateCloudRequests({
session,
search,
page,
pageSize,
ministry,
cluster,
includeInactive,
sortKey: sortKey || undefined,
sortOrder,
isTest: showTest,
...body,
});

return OkResponse(data);
return OkResponse({ docs, totalCount });
});
Original file line number Diff line number Diff line change
@@ -1,124 +1,47 @@
import { Cluster, Ministry, Prisma } from '@prisma/client';
import { useEffect, useRef, useState } from 'react';
import { Cluster, DecisionStatus, Ministry, Prisma, RequestType } from '@prisma/client';
import { useSnapshot, subscribe } from 'valtio';
import FormToggle from '@/components/generic/checkbox/FormToggle';
import FormMultiSelect from '@/components/generic/select/FormMultiSelect';
import FormSelect from '@/components/generic/select/FormSelect';
import { clusters, productSorts, ministryOptions } from '@/constants';
import { pageState } from './state';

const productSortsNoLicencePlate = productSorts.filter((v) => v.sortKey !== 'licencePlate');

export default function FilterPanel() {
const pageSnapshot = useSnapshot(pageState);
const clusterProviderRef = useRef<HTMLSelectElement>(null);
const ministryRef = useRef<HTMLSelectElement>(null);
const sortRef = useRef<HTMLSelectElement>(null);
const toggleText = 'Show Resolved Requests';

const handleSortChange = (value: string) => {
const selectedOption = productSortsNoLicencePlate.find(
(privateSortName) => privateSortName.humanFriendlyName === value,
);
if (selectedOption) {
pageState.sortKey = selectedOption.sortKey;
pageState.sortOrder = selectedOption.sortOrder;
} else {
pageState.sortKey = '';
pageState.sortOrder = Prisma.SortOrder.desc;
}
};

const handleToggleChange = () => {
pageState.includeInactive = !pageSnapshot.includeInactive;
};

const handleClusterChange = (value: string) => {
pageState.cluster = value as Cluster;
};

const handleMinistryChange = (value: string) => {
pageState.ministry = value as Ministry;
};

const clearFilters = () => {
if (clusterProviderRef.current) {
clusterProviderRef.current.value = '';
}
if (ministryRef.current) {
ministryRef.current.value = '';
}
if (sortRef.current) {
sortRef.current.value = '';
}

pageState.cluster = undefined;
pageState.ministry = undefined;
pageState.sortKey = '';
pageState.sortOrder = Prisma.SortOrder.asc;
pageState.includeInactive = false;
};

return (
<div className="flex gap-8 mr-10">
<div className="grid auto-rows-min grid-cols-1 gap-y-8 md:grid-cols-3 md:gap-x-6">
<fieldset className="w-full md:w-48 2xl:w-96">
<FormSelect
ref={sortRef}
id="id"
label="Sort By"
options={productSortsNoLicencePlate.map((v) => ({
label: v.humanFriendlyName,
value: v.humanFriendlyName,
}))}
defaultValue={
productSortsNoLicencePlate.find(
(v) => v.sortKey === pageSnapshot.sortKey && v.sortOrder === pageSnapshot.sortOrder,
)?.humanFriendlyName
}
onChange={handleSortChange}
/>
</fieldset>
<fieldset className="w-full md:w-48 2xl:w-96">
<div className="mt-2 md:mt-0 md:ml-4">
<FormSelect
ref={clusterProviderRef}
id="cluster"
label="Cluster"
options={[{ label: 'All Clusters', value: '' }, ...clusters.map((v) => ({ label: v, value: v }))]}
defaultValue={pageSnapshot.cluster}
onChange={handleClusterChange}
/>
</div>
</fieldset>
<fieldset className="w-full md:w-48 2xl:w-96">
<div className="mt-2 md:mt-0 md:ml-4">
<FormSelect
ref={ministryRef}
id="ministry"
label="Ministry"
options={[{ label: `All Ministries`, value: '' }, ...ministryOptions]}
defaultValue={pageSnapshot.ministry}
onChange={handleMinistryChange}
/>
</div>
</fieldset>
<div></div>
<FormToggle
id="includeInactive"
label={toggleText}
checked={pageSnapshot.includeInactive}
onChange={handleToggleChange}
/>
<div className="mt-8 md:mt-7 md:ml-4">
<button
className="min-w-max w-1/2 h-9 inline-flex items-center justify-center gap-x-2 rounded-md bg-bcblue text-white px-3 text-sm font-semibold shadow-sm ring-1 ring-inset transition-all duration-500 ring-gray-300 hover:bg-[#CCCCCE]"
onClick={clearFilters}
>
Clear Filters
</button>
</div>
</div>
<div className="grid grid-cols-1 gap-y-2 md:grid-cols-12 md:gap-x-3">
<div className="col-span-6" />
<FormMultiSelect
name="type"
label="Request Type"
value={pageSnapshot.types ?? []}
data={Object.values(RequestType)}
onChange={(value) => {
pageState.types = value as RequestType[];
pageState.page = 1;
}}
classNames={{ wrapper: 'col-span-2' }}
/>
<FormMultiSelect
name="status"
label="Status"
value={pageSnapshot.status ?? []}
data={Object.values(DecisionStatus)}
onChange={(value) => {
pageState.status = value as DecisionStatus[];
pageState.page = 1;
}}
classNames={{ wrapper: 'col-span-2' }}
/>
<FormMultiSelect
name="temporary"
label="Temporary"
value={pageSnapshot.temporary ?? []}
data={['YES', 'NO']}
onChange={(value) => {
pageState.temporary = value as ('YES' | 'NO')[];
pageState.page = 1;
}}
classNames={{ wrapper: 'col-span-2' }}
/>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { proxy, useSnapshot } from 'valtio';
import { z } from 'zod';
import Table from '@/components/generic/table/Table';
import TableBodyPrivateRequests from '@/components/table/TableBodyPrivateRequests';
import { requestSortsInProduct } from '@/constants/private-cloud';
import createClientPage from '@/core/client-page';
import { processPrivateCloudRequestData } from '@/helpers/row-mapper';
import { searchPrivateCloudRequests } from '@/services/backend/private-cloud/requests';
Expand Down Expand Up @@ -45,6 +46,7 @@ export default privateCloudProductRequests(({ pathParams, queryParams, session }
page={snap.page ?? 1}
pageSize={snap.pageSize ?? 10}
search={snap.search}
sortKey={snap.sortValue}
onPagination={(page: number, pageSize: number) => {
pageState.page = page;
pageState.pageSize = pageSize;
Expand All @@ -53,6 +55,11 @@ export default privateCloudProductRequests(({ pathParams, queryParams, session }
pageState.page = 1;
pageState.search = searchTerm;
}}
onSort={(sortValue) => {
pageState.page = 1;
pageState.sortValue = sortValue;
}}
sortOptions={requestSortsInProduct.map((v) => v.label)}
filters={<FilterPanel />}
isLoading={isLoading}
>
Expand Down
Loading
Loading