Skip to content

Commit aa9f2b2

Browse files
authored
WCMS-20201: Data Dictionary Table improvements: filter, sort, other updates (#213)
1 parent 98d72d5 commit aa9f2b2

File tree

10 files changed

+175
-44
lines changed

10 files changed

+175
-44
lines changed

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@civicactions/cmsds-open-data-components",
3-
"version": "3.1.9",
3+
"version": "3.1.10",
44
"description": "Components for the open data catalog frontend using CMS Design System",
55
"main": "dist/main.js",
66
"source": "src/index.ts",

src/components/DataDictionaryTable/index.tsx

+78-22
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,104 @@
11
import React, { useState } from 'react';
2-
import { useReactTable, flexRender, getCoreRowModel, getPaginationRowModel } from '@tanstack/react-table';
3-
import { Table, TableHead, TableRow, TableCell, TableBody, Pagination } from '@cmsgov/design-system';
2+
import { useReactTable, flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, SortingState, getFilteredRowModel, ColumnFiltersState } from '@tanstack/react-table';
3+
import { useMediaQuery } from 'react-responsive';
4+
import { Table, TableHead, TableRow, TableCell, TableBody, Pagination, Dropdown, DropdownChangeObject } from '@cmsgov/design-system';
45
import HeaderResizeElement from '../Datatable/HeaderResizeElement';
56
import './dataDictionaryTable.scss';
67

7-
const DataDictionaryTable = ({tableColumns, tableData, count, pageSize} :
8-
{tableColumns: Array<any>, tableData: Array<any>, count: number, pageSize: number}
8+
const DataDictionaryTable = ({tableColumns, tableData, pageSize, columnFilters} :
9+
{tableColumns: Array<any>, tableData: Array<any>, pageSize: number, columnFilters?: ColumnFiltersState}
910
) => {
10-
const [pagination, setPagination] = useState({
11-
pageIndex: 1,
12-
pageSize: pageSize,
13-
});
14-
const [ariaLiveFeedback, setAriaLiveFeedback] = useState('')
11+
const [sorting, setSorting] = useState<SortingState>([])
12+
const [ariaLiveFeedback, setAriaLiveFeedback] = useState('');
13+
14+
const mobile = useMediaQuery({ minWidth: 0, maxWidth: 544 });
15+
16+
const sortElement = (isSorted : string) => {
17+
if(isSorted === 'asc') {
18+
return 'dc-c-sort--asc'
19+
}
20+
if(isSorted === 'desc') {
21+
return 'dc-c-sort--desc'
22+
}
23+
return 'dc-c-sort--default'
24+
}
1525

1626
const table = useReactTable({
1727
data: tableData,
1828
columns: tableColumns,
1929
columnResizeMode: 'onChange',
2030
getCoreRowModel: getCoreRowModel(),
31+
getFilteredRowModel: getFilteredRowModel(),
2132
getPaginationRowModel: getPaginationRowModel(),
22-
onPaginationChange: setPagination,
33+
getSortedRowModel: getSortedRowModel(),
34+
onSortingChange: setSorting,
2335
state: {
24-
pagination: pagination
36+
sorting,
37+
columnFilters
2538
}
2639
});
2740

41+
const sortOptions = [
42+
{value: 'default', label: 'No Sort'},
43+
{value: 'titleasc', label: 'Title A-Z'},
44+
{value: 'titledesc', label: 'Title Z-A'},
45+
{value: 'typeasc', label: 'Type A-Z'},
46+
{value: 'typedesc', label: 'Type Z-A'},
47+
];
48+
49+
const sortStatesLookup : {[key: string] : Array<any>} = {
50+
default: [],
51+
titleasc: [{id: 'titleResizable', desc: false}],
52+
titledesc: [{id: 'titleResizable', desc: true}],
53+
typeasc: [{id: 'type', desc: false}],
54+
typedesc: [{id: 'type', desc: true}]
55+
}
2856
return (
2957
<div>
58+
{mobile && (
59+
<div className="ds-u-margin-bottom--3 ds-l-col--12 ds-l-sm-col--6">
60+
<Dropdown
61+
labelClassName="ds-u-margin-top--1 ds-u-sm-margin-top--0"
62+
options={sortOptions}
63+
label="Sort"
64+
value={Object.keys(sortStatesLookup).find(key => {
65+
return JSON.stringify(sortStatesLookup[key]) == JSON.stringify(sorting);
66+
})}
67+
name="dc-data-dictionary-type"
68+
onChange={(e: DropdownChangeObject) => {
69+
setSorting(sortStatesLookup[e.target.value])
70+
}}
71+
/>
72+
</div>
73+
)}
3074
<div className="dc-c-datadictionary-table">
31-
<Table className="dc-c-datatable" {...{style:{width: '100%'}}} >
75+
<Table className="dc-c-datatable" {...{style:{width: '100%'}}} stackable>
3276
<TableHead className="dc-thead--truncated dc-thead--resizeable">
3377
{table.getHeaderGroups().map(headerGroup => (
3478
<TableRow key={"header" + headerGroup.id}>
3579
{headerGroup.headers.map(header => {
3680
return (header.id === "titleResizable") ? (
37-
<HeaderResizeElement key={header.id + "_resize"} table={table} header={header} setAriaLiveFeedback={setAriaLiveFeedback} />
81+
<HeaderResizeElement key={header.id + "_resize"} table={table} header={header} setAriaLiveFeedback={setAriaLiveFeedback} sortElement={sortElement} />
3882
) : (
3983
<TableCell
4084
{...{
4185
key: header.id
4286
}}
43-
className="ds-u-border-y--2 ds-u-border--dark ds-u-border-x--0"
87+
className= {`ds-u-border-y--2 ds-u-border--dark ds-u-border-x--0`}
88+
id={'dataDictionary_' + header.id}
4489
>
4590
{flexRender(header.column.columnDef.header, header.getContext()) as React.ReactNode}
91+
{header.id === 'type' && (
92+
<button
93+
onClick={header.column.getToggleSortingHandler()}
94+
{...{
95+
className: header.column.getCanSort()
96+
? `cursor-pointer select-none ds-u-focus-visible ${sortElement(header.column.getIsSorted() as string)}`
97+
: '',
98+
}}
99+
aria-label={`${header.column.columnDef.header} sort order`}
100+
/>
101+
)}
46102
</TableCell>
47103
)
48104
}) }
@@ -60,10 +116,13 @@ const DataDictionaryTable = ({tableColumns, tableData, count, pageSize} :
60116
{...{
61117
key: cell.id,
62118
style: {
63-
maxWidth: cell.column.getSize(),
119+
maxWidth: mobile ? '100%' : cell.column.getSize(),
120+
whiteSpace: cell.column.id === "description" ? 'pre-wrap' : 'normal'
64121
},
65122
}}
66123
className={`${cell.column.id === 'titleResizable' ? 'ds-u-word-break' : ''}`}
124+
headers={'dataDictionary_' + cell.column.id}
125+
stackedTitle={cell.column.id === 'titleResizable' ? 'Title' : cell.column.columnDef.header as string}
67126
>
68127
{flexRender(cell.column.columnDef.cell, cell.getContext()) as React.ReactNode}
69128
</TableCell>
@@ -76,16 +135,13 @@ const DataDictionaryTable = ({tableColumns, tableData, count, pageSize} :
76135
</Table>
77136
<div className='sr-only' aria-live='assertive' aria-atomic='true'>{ariaLiveFeedback}</div>
78137
</div>
79-
{count > pageSize ? (
138+
{table.getRowCount() > pageSize ? (
80139
<Pagination
81-
totalPages={Math.ceil(count / pagination.pageSize)}
82-
currentPage={pagination.pageIndex + 1}
140+
totalPages={table.getPageCount()}
141+
currentPage={table.getState().pagination.pageIndex + 1}
83142
onPageChange={(evt, page) => {
84143
evt.preventDefault();
85-
setPagination({
86-
pageIndex: page - 1,
87-
pageSize: pageSize
88-
})
144+
table.setPageIndex(page - 1)
89145
}}
90146
renderHref={(page) => {
91147
return '';

src/components/DatasetDataDictionaryTab/index.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import axios from 'axios';
77
import { DatasetDictionaryItemType } from '../../types/dataset';
88
import SitewideDataDictionaryTable from '../SitewideDataDictionaryTable';
99
import DatasetDictionaryTable from '../DatasetDictionaryTable';
10-
import { Button } from '@cmsgov/design-system';
10+
import { Button, Spinner } from '@cmsgov/design-system';
1111

1212
const DataDictionary = (
1313
{ datasetDictionaryEndpoint, datasetSitewideDictionary, title, pageSize = 20, additionalParams } :
@@ -30,7 +30,7 @@ const DataDictionary = (
3030
});
3131

3232
const datasetDictionary = data && data.data && data.data.fields && data.data.fields.length ? data.data.fields : null;
33-
33+
3434
return (
3535
<>
3636
<h2 className="ds-text-heading--2xl ds-u-margin-y--3">{title}</h2>
@@ -41,7 +41,7 @@ const DataDictionary = (
4141
<i className="fa fa-file-download ds-u-color--primary ds-u-padding-right--1"></i> View Dictionary JSON
4242
</Button>
4343
</div>
44-
<DatasetDictionaryTable datasetDictionary={datasetDictionary} pageSize={pageSize} />
44+
<DatasetDictionaryTable datasetDictionary={datasetDictionary} pageSize={pageSize}/>
4545
</>
4646
)}
4747

src/components/DatasetDictionaryTable/index.tsx

+65-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
import React from 'react';
1+
import React, { useState, useMemo } from 'react';
22
import { createColumnHelper } from '@tanstack/react-table';
3+
import { TextField, Dropdown, AccordionItem, Button, DropdownChangeObject } from '@cmsgov/design-system';
34
import { DatasetDictionaryItemType } from '../../types/dataset';
45
import DataDictionaryTable from '../DataDictionaryTable';
56
import { Tooltip, TooltipIcon } from '@cmsgov/design-system';
67
import "./dataDictionary.scss"
8+
import ClearFiltersButton from '../QueryBuilder/ClearFiltersButton';
79

810
const DatasetDictionaryTable = ({ datasetDictionary, pageSize} : {datasetDictionary: DatasetDictionaryItemType[], pageSize: number}) => {
11+
const [titleFilter, setTitleFilter ] = useState("");
12+
const [typeFilter, setTypeFilter ] = useState("all");
13+
const columnFilters = useMemo(() => [
14+
{id: "titleResizable", value: titleFilter},
15+
{id: "type", value: typeFilter === "all" ? "" : typeFilter}
16+
], [titleFilter, typeFilter])
17+
918
const tableData = datasetDictionary.map((item) => {
1019
return {
1120
titleResizable: item.title,
@@ -21,7 +30,7 @@ const DatasetDictionaryTable = ({ datasetDictionary, pageSize} : {datasetDiction
2130
<div className="dc-c-tooltip-width-override">
2231
Title
2332
<Tooltip
24-
title={"Title represents the column headers of the data file (e.g., Change_Type)"}
33+
title={"Title represents the column headers of the data file"}
2534
// @ts-ignore
2635
style={{ border: 'none', background: 'none' }}
2736
maxWidth="400px"
@@ -43,7 +52,60 @@ const DatasetDictionaryTable = ({ datasetDictionary, pageSize} : {datasetDiction
4352
}),
4453
];
4554

46-
return ( <DataDictionaryTable tableColumns={tableColumns} tableData={tableData} count={datasetDictionary.length} pageSize={pageSize} /> )
55+
const typeOptions = [
56+
{value: 'all', label: 'All Types'},
57+
{value: 'string', label: 'String'},
58+
{value: 'date', label: 'Date'},
59+
{value: 'datetime', label: 'Datetime'},
60+
{value: 'year', label: 'Year'},
61+
{value: 'integer', label: 'Integer'},
62+
{value: 'number', label: 'Number'},
63+
{value: 'boolean', label: 'Boolean'}
64+
];
65+
66+
return (
67+
<>
68+
<div className="dc-query-builder ds-u-margin-bottom--3">
69+
<div className="ds-c-accordion ds-c-accordion--bordered">
70+
<AccordionItem
71+
heading={"Data Dictionary Filters"}
72+
defaultOpen={true}
73+
>
74+
<div className="ds-u-display--flex ds-u-flex-wrap--wrap">
75+
<TextField
76+
className="ds-l-col--12 ds-l-sm-col--6"
77+
labelClassName="ds-u-margin-top--1 ds-u-sm-margin-top--0"
78+
label="Title"
79+
value={titleFilter}
80+
name="dc-data-dictionary-title"
81+
onChange={(e: Event) => setTitleFilter((e.target as HTMLInputElement).value)}
82+
/>
83+
<div className="ds-l-col--12 ds-l-sm-col--6">
84+
<Dropdown
85+
labelClassName="ds-u-margin-top--1 ds-u-sm-margin-top--0"
86+
options={typeOptions}
87+
label="Type"
88+
value={typeFilter}
89+
name="dc-data-dictionary-type"
90+
onChange={(e: DropdownChangeObject) => setTypeFilter(e.target.value)}
91+
/>
92+
</div>
93+
<div className="ds-u-float--right ds-u-padding-y--2 ds-l-col--12 ds-u-display--flex ds-u-flex-wrap--wrap ds-u-justify-content--end">
94+
<div className="ds-u-display--flex ds-u-justify-content--end ds-l-col--12 ds-l-md-col--6 ds-u-padding-x--0">
95+
<ClearFiltersButton
96+
clearFiltersFn={() => {
97+
setTitleFilter("");
98+
setTypeFilter("all");
99+
}} />
100+
</div>
101+
</div>
102+
</div>
103+
</AccordionItem>
104+
</div>
105+
</div>
106+
<DataDictionaryTable tableColumns={tableColumns} tableData={tableData} pageSize={pageSize} columnFilters={columnFilters} />
107+
</>
108+
)
47109
}
48110

49111
export default DatasetDictionaryTable;

src/components/Datatable/HeaderResizeElement.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const HeaderResizeElement = ({table, header, sortElement, setAriaLiveFeedback} :
1616
className="ds-u-border-y--2 ds-u-padding--2 ds-u-border--dark ds-u-font-weight--bold"
1717
>
1818
<div className="ds-u-display--flex">
19-
<span style={{maxWidth: header.getSize() - 16}} >
19+
<span style={{maxWidth: header.getSize() - 16}} title={header.column.columnDef.header}>
2020
{header.isPlaceholder
2121
? null
2222
: flexRender(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from "react";
2+
import { Button } from "@cmsgov/design-system";
3+
import { useMediaQuery } from "react-responsive";
4+
5+
const ClearFiltersButton = ({disabled = false, clearFiltersFn} : { disabled?: boolean, clearFiltersFn: Function}) => {
6+
const small = useMediaQuery({ minWidth: 0, maxWidth: 544 });
7+
return (
8+
<Button
9+
disabled={disabled}
10+
className="ds-u-float--right ds-l-md-col--6 ds-l-col--5"
11+
variation={small ? 'ghost' : undefined}
12+
onClick={() => clearFiltersFn()}
13+
>
14+
{small ? 'Clear all' : 'Clear all filters'}
15+
</Button>
16+
)
17+
}
18+
19+
export default ClearFiltersButton;

src/components/QueryBuilder/index.tsx

+4-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { buildOperatorOptions } from '../../templates/FilteredResource/functions
66
import QueryTitle from '../../templates/FilteredResource/QueryTitle';
77
import QueryRow from '../QueryRow';
88
import { ConditionType, SchemaType } from '../../types/dataset';
9+
import ClearFiltersButton from './ClearFiltersButton';
910

1011
type QueryBuilderPropTypes = {
1112
resource: {
@@ -188,18 +189,13 @@ const QueryBuilder = (props: QueryBuilderPropTypes) => {
188189
>
189190
Apply filters
190191
</Button>
191-
<Button
192+
<ClearFiltersButton
192193
disabled={queryConditions.length === 0}
193-
className="ds-u-float--right ds-l-md-col--6 ds-l-col--5"
194-
variation={small ? 'ghost' : undefined}
195-
onClick={() => {
194+
clearFiltersFn={() => {
196195
setQueryConditions([]);
197196
setTitleConditions([]);
198197
setConditionsCleared(true);
199-
}}
200-
>
201-
{small ? 'Clear all' : 'Clear all filters'}
202-
</Button>
198+
}} />
203199
</div>
204200
</div>
205201
</form>

src/components/SitewideDataDictionaryTable/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React from 'react';
22
import { createColumnHelper } from '@tanstack/react-table';
33
import { DatasetDictionaryItemType } from '../../types/dataset';
44
import DataDictionaryTable from '../DataDictionaryTable';
@@ -20,7 +20,7 @@ const SitewideDataDictionaryTable = ({ datasetDictionary, pageSize} : {datasetDi
2020
}),
2121
];
2222

23-
return ( <DataDictionaryTable tableColumns={tableColumns} tableData={datasetDictionary} count={datasetDictionary.length} pageSize={pageSize} /> )
23+
return ( <DataDictionaryTable tableColumns={tableColumns} tableData={datasetDictionary} pageSize={pageSize} /> )
2424
}
2525

2626
export default SitewideDataDictionaryTable;

src/templates/FilteredResource/QueryBuilder.jsx

-2
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ function updateQueryForDatastore(condition) {
3636
const QueryBuilder = ({ resource, id, includeSearchParams, customColumns }) => {
3737
const { conditions, schema, setConditions } = resource;
3838
const fields = Object.keys(schema[id].fields);
39-
const hasConditions = conditions.length > 0;
4039

41-
const [queryCount, setQueryCount] = useState(0);
4240
const [queryConditions, setQueryConditions] = useState([]);
4341
const [titleConditions, setTitleConditions] = useState([]); // Add use effect to load conditions on first load if needed
4442
const [conditionsChanged, setConditionsChanged] = useState(false);

0 commit comments

Comments
 (0)