Skip to content

Commit

Permalink
feat: add column sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
matthieu-foucault committed Jan 20, 2022
1 parent db7a73b commit 5c42eaa
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 61 deletions.
111 changes: 111 additions & 0 deletions app/components/Table/SortableHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faSort,
faCaretUp,
faCaretDown,
} from "@fortawesome/free-solid-svg-icons";
import { useRouter } from "next/router";
import camelToSnakeCase from "lib/helpers/camelToSnakeCase";

interface Props {
columnName: string;
displayName: string;
sortable: boolean;
hasTableHeader: boolean;
}

const SORT_DIRECTION = ["ASC", "DESC"];
const SORT_ICONS = [faCaretDown, faCaretUp];

const SortableHeader: React.FC<Props> = ({
columnName,
displayName,
sortable,
}) => {
const router = useRouter();
const sortDirectionIndex = SORT_DIRECTION.indexOf(
router.query.direction?.toString()
);
const [currentSortDirection, setCurrentSortDirection] = useState(
Math.max(sortDirectionIndex, 0)
);

const getOrderbyString = (orderColumnName, sortDirection) => {
return (
camelToSnakeCase(orderColumnName).toUpperCase() +
"_" +
SORT_DIRECTION[sortDirection]
);
};

const triggerSort = (sortColumnName) => {
//Cycle
const sortDirection = (currentSortDirection + 1) % 2;
setCurrentSortDirection(sortDirection);

const url = {
pathname: router.pathname,
query: {
...router.query,
orderBy: getOrderbyString(sortColumnName, sortDirection),
},
};

router.replace(url, url, { shallow: true });
};

return (
<th onClick={() => sortable && triggerSort(columnName)}>
<span>{displayName}</span>
{sortable && (
<span role="button">
<FontAwesomeIcon
color="white"
icon={
router.query.orderBy ===
getOrderbyString(columnName, currentSortDirection)
? SORT_ICONS[currentSortDirection]
: faSort
}
/>
</span>
)}
<style>{`
table.bc-table th {
position: relative;
cursor: pointer;
background-color: #003366;
color: white;
text-align: left;
padding: 0.5rem;
height: 4rem;
}
th:not(last-child) {
border-right: 1px solid #ccc;
}
th:first-child {
border-top-left-radius: 0.25rem;
border-left: 1px solid #003366;
border-top: 1px solid #003366;
}
th:last-child {
border-top-right-radius: 0.25rem;
border-right: 1px solid #003366;
border-top: 1px solid #003366;
}
span[role="button"] {
height: 100%;
position: absolute;
right: 0.75em;
}
`}</style>
</th>
);
};

export default SortableHeader;
58 changes: 17 additions & 41 deletions app/components/Table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,16 @@ import React, { useMemo } from "react";
import FilterRow from "./FilterRow";
import { FilterArgs, PageArgs, TableFilter } from "./Filters";
import Pagination from "./Pagination";

interface Column {
title: string;
}
import SortableHeader from "./SortableHeader";

interface Props {
columns: Column[];
filters?: TableFilter[];
filters: TableFilter[];
paginated?: boolean;
totalRowCount?: number;
emptyStateContents?: JSX.Element | string;
}

const Table: React.FC<Props> = ({
columns,
filters,
paginated,
totalRowCount,
Expand Down Expand Up @@ -89,24 +84,29 @@ const Table: React.FC<Props> = ({
{/* class name is used to increase specificity of CSS selectors and override defaults */}
<thead>
<tr>
{columns.map((c) => (
<th key={c.title}>{c.title}</th>
{filters.map((filter) => (
<SortableHeader
key={filter.title + "-header"}
columnName={filter.sortColumnName}
displayName={filter.title}
sortable={filter.isSortEnabled}
hasTableHeader={filter.hasTableHeader}
/>
))}
</tr>
{filters?.length > 0 && (
<FilterRow
filterArgs={filterArgs}
filters={filters}
onSubmit={applyFilterArgs}
/>
)}

<FilterRow
filterArgs={filterArgs}
filters={filters}
onSubmit={applyFilterArgs}
/>
</thead>
<tbody>
{rows.length > 0 ? (
rows
) : (
<tr>
<td colSpan={columns.length}>{emptyStateContents}</td>
<td colSpan={filters.length}>{emptyStateContents}</td>
</tr>
)}
</tbody>
Expand All @@ -131,30 +131,6 @@ const Table: React.FC<Props> = ({
border-spacing: 0;
}
table.bc-table th {
background-color: #003366;
color: white;
text-align: left;
padding: 0.5rem;
height: 4rem;
}
th:not(last-child) {
border-right: 1px solid #ccc;
}
th:first-child {
border-top-left-radius: 0.25rem;
border-left: 1px solid #003366;
border-top: 1px solid #003366;
}
th:last-child {
border-top-right-radius: 0.25rem;
border-right: 1px solid #003366;
border-top: 1px solid #003366;
}
:global(tr:nth-child(even)) {
background-color: #f5f5f5;
}
Expand Down
7 changes: 7 additions & 0 deletions app/lib/helpers/camelToSnakeCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const camelToSnakeCase = (str: string) => {
return str.replace(/([A-Z])/g, function (_match, group) {
return "_" + group.toLowerCase();
});
};

export default camelToSnakeCase;
31 changes: 15 additions & 16 deletions app/pages/cif/projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import { useRouter } from "next/router";
import { getProjectRevisionPageRoute } from "pageRoutes";
import Table from "components/Table";
import ProjectTableRow from "components/Project/ProjectTableRow";
import { SortOnlyFilter, TextFilter } from "components/Table/Filters";
import {
DisplayOnlyFilter,
NoHeaderFilter,
SortOnlyFilter,
TextFilter,
} from "components/Table/Filters";

export const ProjectsQuery = graphql`
query projectsQuery(
Expand All @@ -18,6 +23,7 @@ export const ProjectsQuery = graphql`
$rfpNumber: String
$offset: Int
$pageSize: Int
$orderBy: [ProjectsOrderBy!]
) {
session {
...DefaultLayout_session
Expand All @@ -37,6 +43,7 @@ export const ProjectsQuery = graphql`
}
rfpNumber: { includesInsensitive: $rfpNumber }
}
orderBy: $orderBy
) {
totalCount
edges {
Expand All @@ -49,23 +56,16 @@ export const ProjectsQuery = graphql`
}
`;

const tableColumns = [
{ title: "Project Name" },
{ title: "Operator Trade Name" },
{ title: "RFP ID" },
{ title: "Status" },
{ title: "Assigned To" },
{ title: "Funding Request" },
{ title: "Actions" },
];

const tableFilters = [
new TextFilter("Project Name", "projectName"),
new TextFilter("Operator Trade Name", "operatorTradeName"),
new TextFilter("Operator Trade Name", "operatorTradeName", {
sortable: false,
}),
new TextFilter("RFP ID", "rfpNumber"),
new TextFilter("Status", "status"),
new SortOnlyFilter("Assigned To", "assignedTo"),
new SortOnlyFilter("Funding Request", "fundingRequest"),
new TextFilter("Status", "status", { sortable: false }),
new DisplayOnlyFilter("Assigned To"),
new SortOnlyFilter("Funding Request", "totalFundingRequest"),
new NoHeaderFilter(),
];

export function Projects({ preloadedQuery }: RelayProps<{}, projectsQuery>) {
Expand Down Expand Up @@ -113,7 +113,6 @@ export function Projects({ preloadedQuery }: RelayProps<{}, projectsQuery>) {
<Table
paginated
totalRowCount={allProjects.totalCount}
columns={tableColumns}
filters={tableFilters}
>
{allProjects.edges.map(({ node }) => (
Expand Down
4 changes: 2 additions & 2 deletions app/tests/unit/components/Table/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Table from "components/Table";
describe("The Table Component", () => {
it("renders the provided columns and rows", () => {
render(
<Table columns={[{ title: "col A" }, { title: "col B" }]}>
<Table filters={[{ title: "col A" }, { title: "col B" }]}>
<tr>
<td>A 1</td>
<td>B 1</td>
Expand All @@ -24,7 +24,7 @@ describe("The Table Component", () => {
it("renders the provided empty state contents when there are no rows", () => {
render(
<Table
columns={[{ title: "col A" }, { title: "col B" }]}
filters={[{ title: "col A" }, { title: "col B" }]}
emptyStateContents="nothing to see here"
/>
);
Expand Down
5 changes: 3 additions & 2 deletions schema/data/dev/003_cif_project.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ begin;
do $$
begin
for project_id in 1..50 loop
insert into cif.project(operator_id, funding_stream_rfp_id, project_status_id, rfp_number, summary, project_name) values
insert into cif.project(operator_id, funding_stream_rfp_id, project_status_id, rfp_number, summary, project_name, total_funding_request) values
(
project_id % 3 + 1,
1,
project_id % 3 + 1,
lpad(project_id::text, 3, '0'),
'lorem ipsum dolor sit amet consectetur adipiscing elit',
'test project ' || lpad(project_id::text, 3, '0')
'test project ' || lpad(project_id::text, 3, '0'),
project_id * 1000
);
end loop;
end
Expand Down

0 comments on commit 5c42eaa

Please sign in to comment.