Skip to content

Commit e85e595

Browse files
committed
feat: add filters and sorting functionality to the team table
1 parent 9aee1e4 commit e85e595

File tree

6 files changed

+311
-1
lines changed

6 files changed

+311
-1
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { FC } from 'react';
2+
import {
3+
Dropdown, Form, Icon, Stack,
4+
} from '@openedx/paragon';
5+
import { FilterList } from '@openedx/paragon/icons';
6+
7+
interface MultipleChoiceFilterProps {
8+
Header: string;
9+
filterChoices: Array<{ name: string; number: number; value: string }>;
10+
filterValue: string[] | undefined;
11+
setFilter: (value: string[]) => void;
12+
}
13+
14+
const MultipleChoiceFilter: FC<MultipleChoiceFilterProps> = ({
15+
Header, filterChoices, filterValue, setFilter,
16+
}) => {
17+
const checkedBoxes = filterValue || [];
18+
19+
const changeCheckbox = (value) => {
20+
if (checkedBoxes.includes(value)) {
21+
const newCheckedBoxes = checkedBoxes.filter((val) => val !== value);
22+
return setFilter(newCheckedBoxes);
23+
}
24+
checkedBoxes.push(value);
25+
return setFilter(checkedBoxes);
26+
};
27+
28+
return (
29+
<Dropdown>
30+
<Dropdown.Toggle variant="outline-primary">
31+
<Stack direction="horizontal" gap={2}>
32+
<Icon color="primary" src={FilterList} />
33+
{Header}
34+
</Stack>
35+
</Dropdown.Toggle>
36+
37+
<Dropdown.Menu>
38+
<Form.CheckboxSet
39+
className="pgn__dropdown-filter-checkbox-group"
40+
name={Header}
41+
aria-label={Header}
42+
value={checkedBoxes}
43+
>
44+
{filterChoices.map(({
45+
name, number, value,
46+
}) => (
47+
<Form.Checkbox
48+
className="m-2"
49+
key={name}
50+
value={name}
51+
onChange={() => changeCheckbox(value)}
52+
aria-label={name}
53+
>
54+
<Stack direction="horizontal" gap={2}>
55+
{`${name} (${number || 0})`}
56+
</Stack>
57+
</Form.Checkbox>
58+
))}
59+
</Form.CheckboxSet>
60+
</Dropdown.Menu>
61+
</Dropdown>
62+
);
63+
};
64+
65+
export default MultipleChoiceFilter;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { FC } from 'react';
2+
import {
3+
Form,
4+
Icon,
5+
} from '@openedx/paragon';
6+
import { Search } from '@openedx/paragon/icons';
7+
8+
interface SearchFilterProps {
9+
filterValue: string[];
10+
setFilter: (value: string[]) => void;
11+
placeholder: string;
12+
}
13+
14+
const SearchFilter: FC<SearchFilterProps> = ({
15+
filterValue, setFilter, placeholder,
16+
}) => (
17+
<Form.Control
18+
className="mw-xs mr-0"
19+
trailingElement={<Icon src={Search} />}
20+
value={filterValue || ''}
21+
type="text"
22+
onChange={e => {
23+
setFilter(e.target.value || undefined); // Set undefined to remove the filter entirely
24+
}}
25+
placeholder={placeholder}
26+
/>
27+
);
28+
29+
export default SearchFilter;
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import {
2+
useContext, useState, useMemo, useCallback,
3+
useEffect,
4+
FC,
5+
} from 'react';
6+
import {
7+
DataTableContext,
8+
Dropdown,
9+
Icon,
10+
Stack,
11+
} from '@openedx/paragon';
12+
import { SwapVert } from '@openedx/paragon/icons';
13+
14+
interface SortOption {
15+
id: string;
16+
desc: boolean;
17+
label: string;
18+
}
19+
20+
interface SortByOptions {
21+
[key: string]: Omit<SortOption, 'label'>;
22+
}
23+
24+
const SORT_BY_OPTIONS: SortByOptions = {
25+
'name-a-z': { id: 'username', desc: false },
26+
'name-z-a': { id: 'username', desc: true },
27+
newest: { id: 'createdAt', desc: true },
28+
oldest: { id: 'createdAt', desc: false },
29+
};
30+
31+
const SORT_LABELS: Record<string, string> = {
32+
'name-a-z': 'Name A-Z',
33+
'name-z-a': 'Name Z-A',
34+
newest: 'Newest',
35+
oldest: 'Oldest',
36+
};
37+
38+
const SortDropdown: FC = () => {
39+
const { toggleSortBy, state } = useContext<DataTableContext>(DataTableContext);
40+
const [sortOrder, setSortOrder] = useState<string | undefined>(undefined);
41+
42+
// Get current sort state from DataTable context
43+
const currentSort = useMemo(() => {
44+
if (!state?.sortBy?.length) { return undefined; }
45+
46+
const activeSortBy = state.sortBy[0];
47+
return Object.entries(SORT_BY_OPTIONS).find(
48+
([, option]) => option.id === activeSortBy.id && option.desc === activeSortBy.desc,
49+
)?.[0];
50+
}, [state?.sortBy]);
51+
52+
// Update local state when external sort changes
53+
useEffect(() => {
54+
setSortOrder(currentSort);
55+
}, [currentSort]);
56+
57+
const handleChangeSortBy = useCallback((newSortOrder: string) => {
58+
if (!SORT_BY_OPTIONS[newSortOrder]) {
59+
console.warn(`Invalid sort option: ${newSortOrder}`);
60+
return;
61+
}
62+
63+
setSortOrder(newSortOrder);
64+
const { id, desc } = SORT_BY_OPTIONS[newSortOrder];
65+
toggleSortBy(id, desc);
66+
}, [toggleSortBy]);
67+
68+
const sortOptions = useMemo(
69+
() => Object.entries(SORT_BY_OPTIONS).map(([key, option]) => ({
70+
key,
71+
...option,
72+
label: SORT_LABELS[key],
73+
})),
74+
[],
75+
);
76+
77+
const currentSortLabel = sortOrder ? SORT_LABELS[sortOrder] : 'Sort';
78+
79+
return (
80+
<Dropdown onSelect={handleChangeSortBy}>
81+
<Dropdown.Toggle
82+
variant="outline-primary"
83+
aria-label={`Sort options. Currently sorted by: ${currentSortLabel}`}
84+
>
85+
<Stack direction="horizontal" gap={2}>
86+
<Icon color="primary" src={SwapVert} />
87+
{currentSortLabel}
88+
</Stack>
89+
</Dropdown.Toggle>
90+
91+
<Dropdown.Menu>
92+
{sortOptions.map(({ key, label }) => (
93+
<Dropdown.Item
94+
key={key}
95+
active={sortOrder === key}
96+
eventKey={key}
97+
aria-label={`Sort by ${label}`}
98+
>
99+
{label}
100+
</Dropdown.Item>
101+
))}
102+
</Dropdown.Menu>
103+
</Dropdown>
104+
);
105+
};
106+
107+
export default SortDropdown;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useContext } from 'react';
2+
import {
3+
breakpoints,
4+
DataTable, DataTableContext,
5+
CheckboxFilter,
6+
Stack,
7+
TextFilter,
8+
useWindowSize,
9+
} from '@openedx/paragon';
10+
11+
import MultipleChoiceFilter from './MultipleChoiceFilter';
12+
import SortDropdown from './SortDropdown';
13+
import SearchFilter from './SearchFilter';
14+
15+
const TableControlBar = () => {
16+
const {
17+
columns,
18+
} = useContext<DataTableContext>(DataTableContext);
19+
20+
const availableFilters = columns.filter((column) => column.canFilter);
21+
22+
const columnTextFilterHeaders = columns
23+
.filter((column) => column.Filter === TextFilter)
24+
.map((column) => column.Header);
25+
26+
const isSmallScreen = useWindowSize().width! < breakpoints.medium.minWidth!;
27+
28+
return (
29+
<div className="pgn__data-table-status-bar">
30+
<Stack className="mb-3" direction={isSmallScreen ? 'vertical' : 'horizontal'} gap={3}>
31+
32+
{availableFilters.map((column) => {
33+
if (column.Filter === CheckboxFilter) {
34+
return <MultipleChoiceFilter {...column} />;
35+
}
36+
37+
if (column.Filter === TextFilter) {
38+
return (
39+
<SearchFilter
40+
filterValue={column.filterValue}
41+
setFilter={column.setFilter}
42+
placeholder={`Search by ${columnTextFilterHeaders.map((header) => header).join(' or ')}`}
43+
/>
44+
);
45+
}
46+
47+
return null;
48+
})}
49+
50+
<SortDropdown />
51+
</Stack>
52+
53+
<DataTable.RowStatus />
54+
<DataTable.BulkActions />
55+
</div>
56+
);
57+
};
58+
59+
export default TableControlBar;

src/authz-module/libraries-manager/components/TeamTable.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import { useNavigate } from 'react-router-dom';
22
import { useIntl } from '@edx/frontend-platform/i18n';
33
import {
44
DataTable, Button, Chip, Skeleton,
5+
TextFilter,
6+
CheckboxFilter,
57
} from '@openedx/paragon';
68
import { Edit } from '@openedx/paragon/icons';
79
import { TableCellValue, TeamMember } from '@src/types';
810
import { ROUTES } from '@src/authz-module/constants';
911
import { useTeamMembers } from '@src/authz-module/data/hooks';
1012
import { useLibraryAuthZ } from '../context';
1113
import messages from './messages';
14+
import TableControlBar from './TableControlBar';
1215

1316
const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({
1417
username: 'skeleton',
@@ -65,9 +68,38 @@ const TeamTable = () => {
6568

6669
const navigate = useNavigate();
6770

71+
const reducedChoices = teamMembers?.reduce((acc, currentObject) => {
72+
const { roles } = currentObject;
73+
roles.forEach((role) => {
74+
if (role in acc) {
75+
acc[role].number += 1;
76+
} else {
77+
acc[role] = {
78+
name: role,
79+
number: 1,
80+
value: role,
81+
};
82+
}
83+
});
84+
return acc;
85+
}, {}) ?? {};
86+
87+
const handleFetchData = (querySettings) => {
88+
console.log('Filters', querySettings.filters);
89+
console.log('Sorting', querySettings.sortBy);
90+
};
91+
6892
return (
6993
<DataTable
7094
isPaginated
95+
isFilterable
96+
defaultColumnValues={{ Filter: TextFilter }}
97+
numBreakoutFilters={3}
98+
manualFilters
99+
manualPagination
100+
isSortable
101+
manualSortBy
102+
fetchData={handleFetchData}
71103
data={rows}
72104
itemCount={rows?.length}
73105
additionalColumns={[
@@ -91,27 +123,44 @@ const TeamTable = () => {
91123
]}
92124
initialState={{
93125
pageSize: 10,
126+
hiddenColumns: ['createdAt'],
94127
}}
95128
columns={
96129
[
97130
{
98131
Header: intl.formatMessage(messages['library.authz.team.table.username']),
99132
accessor: 'username',
100133
Cell: NameCell,
134+
disableSortBy: true,
101135
},
102136
{
103137
Header: intl.formatMessage(messages['library.authz.team.table.email']),
104138
accessor: 'email',
105139
Cell: EmailCell,
140+
disableFilters: true,
141+
disableSortBy: true,
106142
},
107143
{
108144
Header: intl.formatMessage(messages['library.authz.team.table.roles']),
109145
accessor: 'roles',
110146
Cell: RolesCell,
147+
Filter: CheckboxFilter,
148+
filter: 'includesValue',
149+
filterChoices: Object.values(reducedChoices),
150+
disableSortBy: true,
151+
},
152+
{
153+
accessor: 'createdAt',
154+
Filter: false,
155+
disableFilters: true,
156+
disableSortBy: true,
111157
},
112158
]
113159
}
114-
/>
160+
>
161+
<TableControlBar />
162+
<DataTable.Table />
163+
</DataTable>
115164
);
116165
};
117166

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface TeamMember {
1313
fullName: string;
1414
email: string;
1515
roles: string[];
16+
createdAt: string;
1617
}
1718

1819
export interface LibraryMetadata {

0 commit comments

Comments
 (0)