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] Add filters to pool table #10423

Merged
merged 6 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
50 changes: 37 additions & 13 deletions api/app/Models/Pool.php
Original file line number Diff line number Diff line change
Expand Up @@ -339,19 +339,6 @@ public static function scopeTeam(Builder $query, ?string $team): Builder
return $query;
}

public static function scopeStreams(Builder $query, ?array $streams): Builder
{
if (! empty($streams)) {
$query->where(function ($query) use ($streams) {
foreach ($streams as $stream) {
$query->orWhere('stream', $stream);
}
});
}

return $query;
}

public static function scopeNotArchived(Builder $query)
{
$query->where(function ($query) {
Expand Down Expand Up @@ -417,6 +404,43 @@ public static function scopeGeneralSearch(Builder $query, ?string $term): Builde
return $query;
}

public static function scopePublishingGroups(Builder $query, ?array $publishingGroups): Builder
{
if (! empty($publishingGroups)) {
$query->whereIn('publishing_group', $publishingGroups);
}

return $query;
}

public static function scopeStreams(Builder $query, ?array $streams): Builder
{
if (! empty($streams)) {
$query->whereIn('stream', $streams);
}

return $query;
}

public static function scopeClassifications(Builder $query, ?array $classifications): Builder
{
if (empty($classifications)) {
return $query;
}

$query->whereHas('classification', function ($query) use ($classifications) {
$query->where(function ($query) use ($classifications) {
foreach ($classifications as $classification) {
$query->orWhere(function ($query) use ($classification) {
$query->where('group', $classification['group'])->where('level', $classification['level']);
});
}
});
});

return $query;
}

/**
* Custom sort to handle issues with how laravel aliases
* aggregate selects and orderBys for json fields in `lighthouse-php`
Expand Down
10 changes: 1 addition & 9 deletions api/app/Models/PoolCandidate.php
Original file line number Diff line number Diff line change
Expand Up @@ -273,15 +273,7 @@ public static function scopeAppliedClassifications(Builder $query, ?array $class
}

$query->whereHas('pool', function ($query) use ($classifications) {
$query->whereHas('classification', function ($query) use ($classifications) {
$query->where(function ($query) use ($classifications) {
foreach ($classifications as $classification) {
$query->orWhere(function ($query) use ($classification) {
$query->where('group', $classification['group'])->where('level', $classification['level']);
});
}
});
});
Pool::scopeClassifications($query, $classifications);
});

return $query;
Expand Down
2 changes: 2 additions & 0 deletions api/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,8 @@ input PoolFilterInput {
team: String @scope
streams: [PoolStream!] @scope
statuses: [PoolStatus!] @scope
publishingGroups: [PublishingGroup!] @scope
classifications: [ClassificationFilterInput!] @scope
}

input PoolTeamDisplayNameOrderByInput {
Expand Down
2 changes: 2 additions & 0 deletions api/storage/app/lighthouse-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,8 @@ input PoolFilterInput {
team: String
streams: [PoolStream!]
statuses: [PoolStatus!]
publishingGroups: [PublishingGroup!]
classifications: [ClassificationFilterInput!]
}

input PoolTeamDisplayNameOrderByInput {
Expand Down
53 changes: 53 additions & 0 deletions api/tests/Feature/PoolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use App\Enums\PoolStatus;
use App\Enums\PoolStream;
use App\Enums\PublishingGroup;
use App\Enums\SkillCategory;
use App\Models\Classification;
use App\Models\Pool;
Expand Down Expand Up @@ -1020,6 +1021,58 @@ public function testPoolStreamsScope(): void
assertSame(2, count($res->json('data.poolsPaginated.data')));
}

/**
* @group paginated
*/
public function testPublishingGroupsScope(): void
{
$IT = Pool::factory()->published()->create([
'publishing_group' => PublishingGroup::IT_JOBS->name,
]);

$IAP = Pool::factory()->published()->create([
'publishing_group' => PublishingGroup::IAP->name,
]);

Pool::factory()->published()->create([
'publishing_group' => PublishingGroup::EXECUTIVE_JOBS->name,
]);

$res = $this->graphQL(/** @lang GraphQL */
'
query ScopePoolName($where: PoolFilterInput) {
poolsPaginated(where: $where) {
data {
id
publishingGroup
}
}
}
',
[
'where' => [
'publishingGroups' => [
PublishingGroup::IT_JOBS->name,
PublishingGroup::IAP->name,
],
],
]
)->assertJsonFragment([
'data' => [
[
'id' => $IT->id,
'publishingGroup' => PublishingGroup::IT_JOBS->name,
],
[
'id' => $IAP->id,
'publishingGroup' => PublishingGroup::IAP->name,
],
],
]);

assertSame(2, count($res->json('data.poolsPaginated.data')));
}

/**
* @group paginated
*/
Expand Down
109 changes: 109 additions & 0 deletions apps/web/src/pages/Pools/IndexPoolPage/components/PoolFilterDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { useIntl } from "react-intl";
import { useQuery } from "urql";

import { Combobox, enumToOptions } from "@gc-digital-talent/forms";
import {
PoolStatus,
PoolStream,
PublishingGroup,
Scalars,
graphql,
} from "@gc-digital-talent/graphql";
import { unpackMaybes } from "@gc-digital-talent/helpers";
import {
commonMessages,
getPoolStatus,
getPoolStream,
getPublishingGroup,
} from "@gc-digital-talent/i18n";

import FilterDialog, {
CommonFilterDialogProps,
} from "~/components/FilterDialog/FilterDialog";
import adminMessages from "~/messages/adminMessages";

export type FormValues = {
publishingGroups: PublishingGroup[];
statuses: PoolStatus[];
classifications: Scalars["UUID"]["output"][];
streams: PoolStream[];
};

const PoolFilterDialog_Query = graphql(/* GraphQL */ `
query PoolFilterDialog {
classifications {
group
level
}
}
`);

const PoolFilterDialog = ({
onSubmit,
resetValues,
initialValues,
}: CommonFilterDialogProps<FormValues>) => {
const intl = useIntl();
const [{ data, fetching }] = useQuery({
query: PoolFilterDialog_Query,
});

return (
<FilterDialog<FormValues>
options={{ defaultValues: initialValues }}
{...{ resetValues, onSubmit }}
>
<div
data-h2-display="base(grid)"
data-h2-gap="base(x1)"
data-h2-grid-template-columns="p-tablet(repeat(2, 1fr))"
>
<Combobox
id="publishingGroups"
name="publishingGroups"
isMulti
label={intl.formatMessage(adminMessages.publishingGroups)}
options={enumToOptions(PublishingGroup).map(({ value }) => ({
value,
label: intl.formatMessage(getPublishingGroup(value)),
}))}
/>{" "}
<Combobox
id="statuses"
name="statuses"
isMulti
label={intl.formatMessage(commonMessages.status)}
options={enumToOptions(PoolStatus).map(({ value }) => ({
value,
label: intl.formatMessage(getPoolStatus(value)),
}))}
/>
<Combobox
id="streams"
name="streams"
isMulti
label={intl.formatMessage(adminMessages.streams)}
options={enumToOptions(PoolStream).map(({ value }) => ({
value,
label: intl.formatMessage(getPoolStream(value)),
}))}
/>{" "}
<Combobox
id="classifications"
name="classifications"
{...{ fetching }}
isMulti
label={intl.formatMessage(adminMessages.classifications)}
options={unpackMaybes(data?.classifications).map(
({ group, level }) => ({
value: `${group}-${level}`,
label: `${group}-0${level}`,
}),
)}
/>
</div>
</FilterDialog>
);
};

export default PoolFilterDialog;
56 changes: 51 additions & 5 deletions apps/web/src/pages/Pools/IndexPoolPage/components/PoolTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
} from "@tanstack/react-table";
import { useIntl } from "react-intl";
import { useQuery } from "urql";
import { useState, useMemo } from "react";
import { useState, useMemo, useRef } from "react";
import { SubmitHandler } from "react-hook-form";
import isEqual from "lodash/isEqual";

import { unpackMaybes } from "@gc-digital-talent/helpers";
import {
Expand All @@ -17,13 +19,16 @@ import {
getPoolStream,
getLocale,
} from "@gc-digital-talent/i18n";
import { graphql, Pool } from "@gc-digital-talent/graphql";
import { graphql, Pool, PoolFilterInput } from "@gc-digital-talent/graphql";

import useRoutes from "~/hooks/useRoutes";
import Table, {
getTableStateFromSearchParams,
} from "~/components/Table/ResponsiveTable/ResponsiveTable";
import { INITIAL_STATE } from "~/components/Table/ResponsiveTable/constants";
import {
INITIAL_STATE,
SEARCH_PARAM_KEY,
} from "~/components/Table/ResponsiveTable/constants";
import { SearchState } from "~/components/Table/ResponsiveTable/types";
import accessors from "~/components/Table/accessors";
import cells from "~/components/Table/cells";
Expand All @@ -43,7 +48,10 @@ import {
transformPoolInput,
getTeamDisplayNameSort,
getOrderByClause,
transformPoolFilterInputToFormValues,
transformFormValuesToFilterInput,
} from "./helpers";
import PoolFilterDialog, { FormValues } from "./PoolFilterDialog";

const columnHelper = createColumnHelper<Pool>();

Expand Down Expand Up @@ -114,9 +122,10 @@ const PoolTable_Query = graphql(/* GraphQL */ `

interface PoolTableProps {
title: string;
initialFilterInput?: PoolFilterInput;
}

const PoolTable = ({ title }: PoolTableProps) => {
const PoolTable = ({ title, initialFilterInput }: PoolTableProps) => {
const intl = useIntl();
const locale = getLocale(intl);
const paths = useRoutes();
Expand All @@ -135,6 +144,16 @@ const PoolTable = ({ title }: PoolTableProps) => {
const [sortState, setSortState] = useState<SortingState | undefined>(
initialState.sortState ?? [{ id: "createdDate", desc: false }],
);
const searchParams = new URLSearchParams(window.location.search);
const filtersEncoded = searchParams.get(SEARCH_PARAM_KEY.FILTERS);
const initialFilters: PoolFilterInput = useMemo(
() => (filtersEncoded ? JSON.parse(filtersEncoded) : initialFilterInput),
[filtersEncoded, initialFilterInput],
);
const filterRef = useRef<PoolFilterInput | undefined>(initialFilters);
const [filterState, setFilterState] = useState<PoolFilterInput | undefined>(
initialFilters,
);

const handlePaginationStateChange = ({
pageIndex,
Expand All @@ -160,6 +179,20 @@ const PoolTable = ({ title }: PoolTableProps) => {
});
};

const handleFilterSubmit: SubmitHandler<FormValues> = (data) => {
setPaginationState((previous) => ({
...previous,
pageIndex: 0,
}));
const transformedData: PoolFilterInput =
transformFormValuesToFilterInput(data);

setFilterState(transformedData);
if (!isEqual(transformedData, filterRef.current)) {
filterRef.current = transformedData;
}
};

const columns = [
columnHelper.accessor("id", {
id: "id",
Expand Down Expand Up @@ -299,7 +332,7 @@ const PoolTable = ({ title }: PoolTableProps) => {
const [{ data, fetching }] = useQuery({
query: PoolTable_Query,
variables: {
where: transformPoolInput({ search: searchState }),
where: transformPoolInput({ search: searchState, filters: filterState }),
page: paginationState.pageIndex,
first: paginationState.pageSize,
orderByTeamDisplayName: getTeamDisplayNameSort(sortState, locale),
Expand Down Expand Up @@ -333,6 +366,19 @@ const PoolTable = ({ title }: PoolTableProps) => {
onSortChange: setSortState,
initialState: defaultState.sortState,
}}
filter={{
initialState: initialFilterInput,
state: filterRef.current,
component: (
<PoolFilterDialog
onSubmit={handleFilterSubmit}
resetValues={transformPoolFilterInputToFormValues(
initialFilterInput,
)}
initialValues={transformPoolFilterInputToFormValues(initialFilters)}
/>
),
}}
pagination={{
internal: false,
initialState: INITIAL_STATE.paginationState,
Expand Down
Loading
Loading