Skip to content

Commit

Permalink
feat(schedule): add status column filter (#2284)
Browse files Browse the repository at this point in the history
* feat(schedule): add filter to status column at All tab

* feat(schedule): add filtering by status

* feat(schedule): clear status column filter when tab switched

* feat(schedule): refactor FilteredTags to handle multiple filter types, add handling to close status column filter

* refactor(mentor-registry): update FilteredTags

* Revert "refactor(mentor-registry): update FilteredTags"

This reverts commit 8359fc7.

* Revert "feat(schedule): refactor FilteredTags to handle multiple filter types, add handling to close status column filter"

This reverts commit 2bf62bf.

* feat(scheduler): implement combined filter functionality for status and type columns

* feat: replace deprecated onFilterDropdownVisibleChange

* refactor(schedule): rename property at getColumns

* refactor(schedule): rename variable at TableView

* feat(schedule): move tagFilters state to combinedFilter as filterTags

* refactor(schedule): delete unused LocalStorageKeys

* test(schedule): update tests for new useLocalStorage structure

* refactor(schedule): fix formatting

* fix(schedule): filterTags filtering condition when tab is switched

* feat(schedule): add FilterTag type for tags and update regarding functionality

* refactor(schedule): make CombinedFilter interface as type

* refactor(schedule): fix formatting

* tests(schedule): fix TableView tests
  • Loading branch information
ThorsAngerVaNeT authored Sep 11, 2023
1 parent 72cf45c commit 30b8478
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 37 deletions.
2 changes: 1 addition & 1 deletion client/src/components/Table/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function getColumnSearchProps(dataIndex: string | string[], label?: strin
);
return val;
},
onFilterDropdownVisibleChange: (visible: boolean) => {
onFilterDropdownOpenChange: (visible: boolean) => {
if (visible && searchInput) {
setTimeout(() => searchInput.select());
}
Expand Down
35 changes: 24 additions & 11 deletions client/src/modules/Schedule/components/TableView/TableView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ describe('TableView', () => {
it('by selected tag', () => {
jest
.spyOn(ReactUse, 'useLocalStorage')
// Mock useLocalStorage for tagFilter
.mockReturnValueOnce([[TagsEnum.Test], jest.fn(), jest.fn()]);
// Mock useLocalStorage for combinedFilter
.mockReturnValueOnce([{ types: [TagsEnum.Test], statuses: [], tags: [] }, jest.fn(), jest.fn()]);
const data = generateCourseData();

render(<TableView settings={PROPS_SETTINGS_MOCK} data={data} />);
Expand Down Expand Up @@ -170,11 +170,15 @@ describe('TableView', () => {
`('should check filters in dropdown when tag "$tag" was selected', async ({ tag }: { tag: string }) => {
jest
.spyOn(ReactUse, 'useLocalStorage')
// Mock useLocalStorage for tagFilter
.mockReturnValue([[TagsEnum.Coding, TagsEnum.Test, TagsEnum.Interview], jest.fn(), jest.fn()]);
// Mock useLocalStorage for combinedFilter
.mockReturnValueOnce([
{ types: [TagsEnum.Coding, TagsEnum.Test, TagsEnum.Interview], statuses: [], tags: [] },
jest.fn(),
jest.fn(),
]);
render(<TableView settings={PROPS_SETTINGS_MOCK} data={generateCourseData()} />);

const tagFilterBtn = screen.getByRole('button', { name: /filter/i });
const [, tagFilterBtn] = screen.getAllByRole('button', { name: /filter/i });
fireEvent.click(tagFilterBtn);

const filtersDropdown = await screen.findByRole('menu');
Expand All @@ -183,11 +187,11 @@ describe('TableView', () => {
expect(checkbox).toBeChecked();
});

it('should not render filtered tags when tagFilter is null', () => {
it('should not render filtered tags when tags is empty', () => {
jest
.spyOn(ReactUse, 'useLocalStorage')
// Mock useLocalStorage for tagFilter
.mockReturnValue([null, jest.fn(), jest.fn()]);
// Mock useLocalStorage for combinedFilter
.mockReturnValueOnce([{ tags: [] }, jest.fn(), jest.fn()]);
render(<TableView settings={PROPS_SETTINGS_MOCK} data={generateCourseData()} />);

const tag = screen.queryByText(/Type: /);
Expand All @@ -197,16 +201,25 @@ describe('TableView', () => {

it('should remove tags when "Clear all" button was clicked', async () => {
const setFilterMock = jest.fn();
const types = [TagsEnum.Coding, TagsEnum.Test, TagsEnum.Interview];
jest
.spyOn(ReactUse, 'useLocalStorage')
// Mock useLocalStorage for tagFilter
.mockReturnValue([[TagsEnum.Coding, TagsEnum.Test, TagsEnum.Interview], setFilterMock, jest.fn()]);
// Mock useLocalStorage for combinedFilter
.mockReturnValueOnce([
{
types,
statuses: [],
tags: types.map(t => ({ label: `${ColumnName.Type}: ${t}`, value: t, tagType: ColumnName.Type })),
},
setFilterMock,
jest.fn(),
]);
render(<TableView settings={PROPS_SETTINGS_MOCK} data={generateCourseData()} />);

const clearAllBtn = screen.getByText(/Clear all/);
fireEvent.click(clearAllBtn);

expect(setFilterMock).toHaveBeenCalledWith([]);
expect(setFilterMock).toHaveBeenCalledWith({ types: [], statuses: [], tags: [] });
});
});

Expand Down
131 changes: 108 additions & 23 deletions client/src/modules/Schedule/components/TableView/TableView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Col, Form, Row, Table } from 'antd';
import { Col, Form, Row, Table, message } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { CourseScheduleItemDto } from 'api';
import { GithubUserLink } from 'components/GithubUserLink';
Expand All @@ -17,38 +17,50 @@ import {
ColumnName,
CONFIGURABLE_COLUMNS,
LocalStorageKeys,
SCHEDULE_STATUSES,
TAG_NAME_MAP,
TAGS,
} from 'modules/Schedule/constants';
import { ScheduleSettings } from 'modules/Schedule/hooks/useScheduleSettings';
import { useMemo, useState } from 'react';
import { useMemo, useState, useEffect } from 'react';
import { useLocalStorage } from 'react-use';
import dayjs from 'dayjs';
import { statusRenderer, renderTagWithStyle } from './renderers';
import { statusRenderer, renderTagWithStyle, renderStatusWithStyle } from './renderers';
import { FilterValue } from 'antd/lib/table/interface';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import { capitalize } from 'lodash';

dayjs.extend(utc);
dayjs.extend(timezone);

const getColumns = ({
timezone,
tagColors,
tagFilter,
combinedFilter,
filteredInfo,
currentTabKey,
}: {
tagFilter: string[];
combinedFilter: CombinedFilter;
timezone: string;
tagColors: Record<string, string>;
filteredInfo: Record<string, FilterValue | null>;
currentTabKey: string;
}): ColumnsType<CourseScheduleItemDto> => {
const timezoneOffset = `(UTC ${dayjs().tz(timezone).format('Z')})`;
const { types, statuses } = combinedFilter;
return [
{
key: ColumnKey.Status,
title: ColumnName.Status,
dataIndex: 'status',
render: statusRenderer,
...(currentTabKey === ALL_TAB_KEY && {
filters: SCHEDULE_STATUSES.map(({ value }) => ({ text: renderStatusWithStyle(value), value })),
defaultFilteredValue: statuses,
filtered: statuses?.length > 0,
filteredValue: statuses || null,
}),
},
{
key: ColumnKey.Name,
Expand All @@ -64,9 +76,9 @@ const getColumns = ({
dataIndex: 'tag',
render: (tag: CourseScheduleItemDto['tag']) => renderTagWithStyle(tag, tagColors),
filters: TAGS.map(status => ({ text: renderTagWithStyle(status.value, tagColors), value: status.value })),
defaultFilteredValue: tagFilter,
filtered: tagFilter?.length > 0,
filteredValue: tagFilter || null,
defaultFilteredValue: types,
filtered: types?.length > 0,
filteredValue: types || null,
},
{
key: ColumnKey.StartDate,
Expand Down Expand Up @@ -122,39 +134,116 @@ export interface TableViewProps {
statusFilter?: string;
}

export type CombinedFilter = {
types: string[];
statuses: string[];

tags?: FilterTag[];
};

export type FilterTag = {
label: string;
value: string;
tagType: ColumnName.Type | ColumnName.Status;
};

const hasStatusFilter = (statusFilter?: string, itemStatus?: string) =>
Array.isArray(statusFilter) || statusFilter === ALL_TAB_KEY || itemStatus === statusFilter;

export function TableView({ data, settings, statusFilter = ALL_TAB_KEY }: TableViewProps) {
const [form] = Form.useForm();
const [tagFilter = [], setTagFilter] = useLocalStorage<string[]>(LocalStorageKeys.TagFilter);
const [filteredInfo, setFilteredInfo] = useState<Record<string, FilterValue | string[] | null>>({});
const [combinedFilter = { types: [], statuses: [], tags: [] }, setCombinedFilter] = useLocalStorage<CombinedFilter>(
LocalStorageKeys.Filters,
);

useEffect(() => {
if (statusFilter !== ALL_TAB_KEY && combinedFilter.statuses.length) {
const tags = combinedFilter.tags?.filter(({ tagType }) => tagType !== ColumnName.Status);
setCombinedFilter({ ...combinedFilter, statuses: [], tags });
}
}, [statusFilter]);

const filteredData = data
.filter(item => (hasStatusFilter(statusFilter, item.status) ? item : null))
.filter(event => (tagFilter?.length > 0 ? tagFilter.includes(event.tag) : event));
const filteredData = useMemo(() => {
return data
.filter(item => (hasStatusFilter(statusFilter, item.status) ? item : null))
.filter(
item =>
(combinedFilter?.types?.length ? combinedFilter.types.includes(item.tag) : true) &&
(combinedFilter?.statuses?.length ? combinedFilter.statuses.includes(item.status) : true),
);
}, [combinedFilter, data, statusFilter]);

const filteredColumns = useMemo(
() =>
getColumns({
tagColors: settings.tagColors,
timezone: settings.timezone,
tagFilter,
combinedFilter,
filteredInfo,
currentTabKey: statusFilter,
}).filter(column => {
const key = (column.key as ColumnKey) ?? ColumnKey.Name;
return CONFIGURABLE_COLUMNS.includes(key) ? !settings.columnsHidden.includes(key) : true;
}),
[settings.columnsHidden, settings.timezone, settings.tagColors, statusFilter, tagFilter],
[settings.columnsHidden, settings.timezone, settings.tagColors, statusFilter, combinedFilter],
);
const columns = filteredColumns as ColumnsType<CourseScheduleItemDto>;

const handleTagClose = (removedTag: string) => {
setTagFilter(tagFilter.filter(t => t !== removedTag));
const handleTagClose = (removedTagLabel: string) => {
const tags = combinedFilter.tags?.filter(({ label }) => label !== removedTagLabel);
const removedTag = combinedFilter.tags?.find(({ label }) => label === removedTagLabel);

switch (removedTag?.tagType) {
case ColumnName.Type:
setCombinedFilter({
...combinedFilter,
types: combinedFilter.types.filter(tag => tag !== removedTag.value),
tags,
});
break;
case ColumnName.Status:
setCombinedFilter({
...combinedFilter,
statuses: combinedFilter.statuses.filter(status => status !== removedTag.value),
tags,
});
break;
default:
message.error('Unknown tag');
break;
}
};

const handleClearAllButtonClick = () => {
setTagFilter([]);
setCombinedFilter({ types: [], statuses: [], tags: [] });
};

const handleTableChange = (_: unknown, filters: Record<ColumnKey, FilterValue | string[] | null>) => {
const combinedFilter: CombinedFilter = {
types: filters.type?.map(tag => tag.toString()) ?? [],
statuses: filters.status?.map(status => status.toString()) ?? [],
};

combinedFilter.tags = [
...combinedFilter.types.map(
(tag: string): FilterTag => ({
label: `${ColumnName.Type}: ${TAG_NAME_MAP[tag as CourseScheduleItemDto['tag']]}`,
value: tag,
tagType: ColumnName.Type,
}),
),
...combinedFilter.statuses.map(
(status: string): FilterTag => ({
label: `${ColumnName.Status}: ${capitalize(status)}`,
value: status,
tagType: ColumnName.Status,
}),
),
];

setCombinedFilter(combinedFilter);
setFilteredInfo(filters);
};

const generateUniqueRowKey = ({ id, name, tag }: CourseScheduleItemDto) => [id, name, tag].join('|');
Expand All @@ -164,8 +253,7 @@ export function TableView({ data, settings, statusFilter = ALL_TAB_KEY }: TableV
<Col span={24}>
<Form form={form} component={false}>
<FilteredTags
filterName={`${ColumnName.Type}: `}
tagFilters={tagFilter}
tagFilters={combinedFilter.tags?.map(({ label }) => label) ?? []}
onTagClose={handleTagClose}
onClearAllButtonClick={handleClearAllButtonClick}
/>
Expand All @@ -176,10 +264,7 @@ export function TableView({ data, settings, statusFilter = ALL_TAB_KEY }: TableV
triggerAsc: undefined,
cancelSort: undefined,
}}
onChange={(_, filters: Record<ColumnKey, FilterValue | string[] | null>) => {
setTagFilter(filters?.type as string[]);
setFilteredInfo(filters);
}}
onChange={handleTableChange}
pagination={false}
dataSource={filteredData}
rowKey={generateUniqueRowKey}
Expand Down
10 changes: 9 additions & 1 deletion client/src/modules/Schedule/components/TableView/renderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Badge, Tag } from 'antd';
import { CourseScheduleItemDto, CourseScheduleItemDtoStatusEnum } from 'api';
import capitalize from 'lodash/capitalize';
import { DEFAULT_TAG_COLOR_MAP, TAG_NAME_MAP } from 'modules/Schedule/constants';
import { getTagStyle, getTaskStatusColor } from 'modules/Schedule/utils';
import { getStatusStyle, getTagStyle, getTaskStatusColor } from 'modules/Schedule/utils';

export function statusRenderer(value: CourseScheduleItemDtoStatusEnum) {
const label = capitalize(value);
Expand All @@ -11,6 +11,14 @@ export function statusRenderer(value: CourseScheduleItemDtoStatusEnum) {
return <Badge color={color} text={label} />;
}

export function renderStatusWithStyle(statusName: CourseScheduleItemDtoStatusEnum) {
return (
<Tag style={getStatusStyle(statusName)} key={statusName}>
{capitalize(statusName)}
</Tag>
);
}

export function renderTagWithStyle(tagName: CourseScheduleItemDto['tag'], tagColors = DEFAULT_TAG_COLOR_MAP) {
return (
<Tag style={getTagStyle(tagName, tagColors)} key={tagName}>
Expand Down
2 changes: 1 addition & 1 deletion client/src/modules/Schedule/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export enum LocalStorageKeys {
ColumnsHidden = 'scheduleColumnsHidden',
EventTypesHidden = 'scheduleEventTypesHidden',
StatusFilter = 'scheduleStatusFilter',
TagFilter = 'scheduleTagFilter',
Filters = 'scheduleFilters',
}

export const TAG_NAME_MAP: Record<CourseScheduleItemDto['tag'], string> = {
Expand Down
10 changes: 10 additions & 0 deletions client/src/modules/Schedule/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,13 @@ export function getTaskStatusColor(value: CourseScheduleItemDtoStatusEnum) {
return '#d9d9d9';
}
}

export const getStatusStyle = (statusName: CourseScheduleItemDtoStatusEnum, styles?: CSSProperties) => {
const statusColor = getTaskStatusColor(statusName);
return {
...styles,
borderColor: statusColor,
color: statusColor,
backgroundColor: `${statusColor}10`,
};
};

0 comments on commit 30b8478

Please sign in to comment.