Skip to content

Commit 39f8821

Browse files
authored
feat(web): filter the schedule by track (#3792)
- closes #687
1 parent 77bb5b5 commit 39f8821

12 files changed

Lines changed: 204 additions & 38 deletions

File tree

‎packages/translations/source/en.xlf‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,9 @@
299299
<trans-unit id="common.sign-in">
300300
<source>Sign in</source>
301301
</trans-unit>
302+
<trans-unit id="shared.filter-menu.tracks">
303+
<source>Tracks</source>
304+
</trans-unit>
302305
<trans-unit id="shared.filter-menu.complexity">
303306
<source>Complexity</source>
304307
</trans-unit>

‎packages/web/src/components/shared/filter-menu.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,32 @@ describe('filter-menu', () => {
122122
});
123123
});
124124

125+
it('names tracks by their titles, in the panel and once selected', async () => {
126+
const { element, shadowRoot } = await render({
127+
filterGroups: [
128+
{
129+
key: FilterGroupKey.track,
130+
filters: [{ group: FilterGroupKey.track, tag: 'expo-hall', label: 'Expo hall' }],
131+
},
132+
],
133+
selectedFilters: [{ group: FilterGroupKey.track, tag: 'expo-hall' }],
134+
});
135+
shadowRoot.querySelector('hb-button')!.click();
136+
await element.updateComplete;
137+
138+
expect(shadowRoot.querySelector('.group-title')).toHaveTextContent('Tracks');
139+
expect(shadowRoot.querySelector('.panel hb-chip')).toHaveTextContent('Expo hall');
140+
expect(shadowRoot.querySelector<HbChip>('.panel hb-chip')!.selected).toBe(true);
141+
expect(shadowRoot.querySelector('ul.selected hb-chip')).toHaveTextContent('Expo hall');
142+
143+
shadowRoot.querySelector<HTMLElement>('.panel hb-chip')!.click();
144+
145+
expect(filterUtils.toggleFilter).toHaveBeenCalledWith({
146+
group: FilterGroupKey.track,
147+
tag: 'expo-hall',
148+
});
149+
});
150+
125151
it('clears every filter', async () => {
126152
const { shadowRoot } = await render({
127153
selectedFilters: [{ group: FilterGroupKey.tags, tag: 'android' }],

‎packages/web/src/components/shared/filter-menu.ts‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import '../ui/hb-chip';
1414
import { ThemedElement } from '../themed-element';
1515

1616
/**
17-
* A Filters button that shows the tag and complexity chips under it, and the selected filters as
17+
* A Filters button that shows the track, tag and complexity chips under it, and the selected filters
1818
* chips that remove themselves. Filters live in the URL, so they survive reloads and links.
1919
*/
2020
@customElement('filter-menu')
@@ -102,9 +102,22 @@ export class FilterMenu extends ThemedElement {
102102
accessor opened = false;
103103

104104
private groupTitle(key: FilterGroupKey) {
105-
return key === FilterGroupKey.tags
106-
? msg('Tags', { id: 'shared.filter-menu.tags' })
107-
: msg('Complexity', { id: 'shared.filter-menu.complexity' });
105+
switch (key) {
106+
case FilterGroupKey.tags:
107+
return msg('Tags', { id: 'shared.filter-menu.tags' });
108+
case FilterGroupKey.track:
109+
return msg('Tracks', { id: 'shared.filter-menu.tracks' });
110+
default:
111+
return msg('Complexity', { id: 'shared.filter-menu.complexity' });
112+
}
113+
}
114+
115+
// Selected filters come from the URL, which has the tag but not the label.
116+
private label(selected: Filter) {
117+
const filter = this.filterGroups
118+
.find((group) => group.key === selected.group)
119+
?.filters.find((item) => generateClassName(item.tag) === selected.tag);
120+
return filter?.label ?? selected.tag;
108121
}
109122

110123
private resultsLabel(count: number) {
@@ -140,7 +153,7 @@ export class FilterMenu extends ThemedElement {
140153
(filter) => html`
141154
<li>
142155
<hb-chip filter selected @click="${() => this.toggle(filter)}">
143-
${filter.tag}
156+
${this.label(filter)}
144157
</hb-chip>
145158
</li>
146159
`,
@@ -180,7 +193,7 @@ export class FilterMenu extends ThemedElement {
180193
style="${styleMap(group.key === FilterGroupKey.tags ? tagChipStyle(filter.tag) : {})}"
181194
@click="${() => this.toggle(filter)}"
182195
>
183-
${filter.tag}
196+
${filter.label ?? filter.tag}
184197
</hb-chip>
185198
</li>
186199
`,

‎packages/web/src/models/filter-group.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Filter } from './filter';
33
export enum FilterGroupKey {
44
tags = 'tags',
55
complexity = 'complexity',
6+
track = 'track',
67
}
78

89
export interface FilterGroup {

‎packages/web/src/models/filter.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ import { FilterGroupKey } from './filter-group';
33
export interface Filter {
44
group: FilterGroupKey;
55
tag: string;
6+
/** What the chip shows, when it isn't the tag, such as a track's title for its ID. */
7+
label?: string;
68
}

‎packages/web/src/store/sessions/selectors.test.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Success } from '@abraham/remotedata';
22
import { describe, expect, it } from 'vitest';
33
import { selectFilterGroups, selectSession } from './selectors';
4+
import { scheduleTracks } from '../../config/site';
45
import { FilterGroupKey } from '../../models/filter-group';
56
import type { Session } from '../../models/session';
67
import type { RootState } from '..';
@@ -31,19 +32,22 @@ describe('selectSession', () => {
3132
});
3233

3334
describe('selectFilterGroups', () => {
34-
it('builds filter groups from the unique tags and complexity values, for the requested groups', () => {
35+
it('builds filter groups from the tracks and the unique tags and complexity values', () => {
3536
const state = { sessions: new Success(sessions) } as unknown as RootState;
3637

3738
const groups = selectFilterGroups(state, undefined);
3839

39-
expect(groups.map((group) => group.key)).toStrictEqual(['tags', 'complexity']);
40+
expect(groups.map((group) => group.key)).toStrictEqual(['track', 'tags', 'complexity']);
4041
expect(groups[0]!.filters).toStrictEqual(
42+
scheduleTracks.map(({ id, title }) => ({ group: 'track', tag: id, label: title })),
43+
);
44+
expect(groups[1]!.filters).toStrictEqual(
4145
expect.arrayContaining([
4246
{ group: 'tags', tag: 'a11y' },
4347
{ group: 'tags', tag: 'web' },
4448
]),
4549
);
46-
expect(groups[1]!.filters).toStrictEqual(
50+
expect(groups[2]!.filters).toStrictEqual(
4751
expect.arrayContaining([
4852
{ group: 'complexity', tag: 'Beginner' },
4953
{ group: 'complexity', tag: 'Advanced' },

‎packages/web/src/store/sessions/selectors.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Success } from '@abraham/remotedata';
22
import { createSelector } from '@reduxjs/toolkit';
33
import type { RootState } from '..';
4+
import { scheduleTracks } from '../../config/site';
45
import type { Filter } from '../../models/filter';
56
import { type FilterGroup, FilterGroupKey } from '../../models/filter-group';
67
import type { Session } from '../../models/session';
@@ -50,7 +51,11 @@ export const selectSession = createSelector(
5051
// Kept as a stable module-level reference (rather than a default parameter
5152
// literal) so repeated calls with no explicit `groups` argument pass the
5253
// same array instance, preserving `createSelector`'s memoization.
53-
const DEFAULT_FILTER_GROUPS: FilterGroupKey[] = [FilterGroupKey.tags, FilterGroupKey.complexity];
54+
const DEFAULT_FILTER_GROUPS: FilterGroupKey[] = [
55+
FilterGroupKey.track,
56+
FilterGroupKey.tags,
57+
FilterGroupKey.complexity,
58+
];
5459

5560
const selectGroups = (_state: RootState, groups: FilterGroupKey[] = DEFAULT_FILTER_GROUPS) =>
5661
groups;
@@ -66,6 +71,18 @@ export const selectFilterGroups = createSelector(
6671
selectGroups,
6772
(sessions: Session[], groups: FilterGroupKey[]): FilterGroup[] => {
6873
return [
74+
{
75+
key: FilterGroupKey.track,
76+
// With one track, there's nothing to pick.
77+
filters:
78+
scheduleTracks.length > 1
79+
? scheduleTracks.map(({ id, title }) => ({
80+
group: FilterGroupKey.track,
81+
tag: id,
82+
label: title,
83+
}))
84+
: [],
85+
},
6986
{
7087
key: FilterGroupKey.tags,
7188
filters: buildFilters(sessions, FilterGroupKey.tags),

‎packages/web/src/utils/filters.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ describe('parseFilters', () => {
1818
expect(parseFilters()).toStrictEqual([]);
1919
});
2020

21-
it('parses tags and complexity query params into filters', () => {
22-
setUrl('/?tags=a11y&tags=web&complexity=beginner');
21+
it('parses tags, complexity and track query params into filters', () => {
22+
setUrl('/?tags=a11y&tags=web&complexity=beginner&track=expo-hall');
2323

2424
expect(parseFilters()).toStrictEqual([
2525
{ group: FilterGroupKey.tags, tag: 'a11y' },
2626
{ group: FilterGroupKey.tags, tag: 'web' },
2727
{ group: FilterGroupKey.complexity, tag: 'beginner' },
28+
{ group: FilterGroupKey.track, tag: 'expo-hall' },
2829
]);
2930
});
3031
});

‎packages/web/src/utils/filters.ts‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,10 @@ export const clearFilters = () => setQueryString('');
1717
export const parseFilters = (): Filter[] => {
1818
// Pages are built without a query string.
1919
if (isServer) return [];
20-
const { search } = window.location;
21-
const searchParams = new URLSearchParams(search);
22-
const tags = searchParams.getAll(FilterGroupKey.tags).map((tag) => {
23-
return { group: FilterGroupKey.tags, tag };
24-
});
25-
const complexities = searchParams.getAll(FilterGroupKey.complexity).map((tag) => {
26-
return { group: FilterGroupKey.complexity, tag };
27-
});
28-
return [...tags, ...complexities];
20+
const searchParams = new URLSearchParams(window.location.search);
21+
return [FilterGroupKey.tags, FilterGroupKey.complexity, FilterGroupKey.track].flatMap((group) =>
22+
searchParams.getAll(group).map((tag) => ({ group, tag })),
23+
);
2924
};
3025

3126
const matchingFilter = (filterA: Filter, filterB: Filter) => {

‎packages/web/src/views/schedule/my-schedule.test.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import './my-schedule';
88
const day = (date: string, items: unknown[] = []): BuiltDay => ({
99
date,
1010
tracks: [{ id: 'track-1', title: 'Track 1' }],
11-
timeslots: [{ startTime: '10:00', endTime: '11:00', sessions: [{ items } as never] }],
11+
timeslots: [
12+
{
13+
startTime: '10:00',
14+
endTime: '11:00',
15+
sessions: [{ gridArea: '1 / 1 / 2 / 2', items } as never],
16+
},
17+
],
1218
tags: [],
1319
});
1420

0 commit comments

Comments
 (0)