Skip to content

Commit a9ec3a9

Browse files
authored
feat: Add filter views to save frequently used filters in data browser (#2404)
1 parent 27cdaf1 commit a9ec3a9

File tree

8 files changed

+661
-529
lines changed

8 files changed

+661
-529
lines changed

src/components/Autocomplete/Autocomplete.react.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ export default class Autocomplete extends Component {
235235
// Tab
236236
// do not type it
237237
e.preventDefault();
238-
238+
239239
e.stopPropagation();
240240
// move focus to input
241241
this.inputRef.current.focus();
@@ -318,7 +318,7 @@ export default class Autocomplete extends Component {
318318
onClick={onClick}
319319
/>
320320
);
321-
}
321+
}
322322

323323
return (
324324
<React.Fragment>
@@ -372,5 +372,5 @@ Autocomplete.propTypes = {
372372
),
373373
error: PropTypes.string.describe(
374374
'Error to be rendered in place of label if defined'
375-
)
376-
}
375+
)
376+
}

src/components/BrowserFilter/BrowserFilter.react.js

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@
55
* This source code is licensed under the license found in the LICENSE file in
66
* the root directory of this source tree.
77
*/
8-
import * as Filters from 'lib/Filters';
9-
import Button from 'components/Button/Button.react';
10-
import Filter from 'components/Filter/Filter.react';
11-
import FilterRow from 'components/BrowserFilter/FilterRow.react';
12-
import Icon from 'components/Icon/Icon.react';
13-
import Popover from 'components/Popover/Popover.react';
14-
import Position from 'lib/Position';
15-
import React from 'react';
16-
import styles from 'components/BrowserFilter/BrowserFilter.scss';
8+
import * as Filters from 'lib/Filters';
9+
import Button from 'components/Button/Button.react';
10+
import Filter from 'components/Filter/Filter.react';
11+
import FilterRow from 'components/BrowserFilter/FilterRow.react';
12+
import Icon from 'components/Icon/Icon.react';
13+
import Popover from 'components/Popover/Popover.react';
14+
import Field from 'components/Field/Field.react';
15+
import TextInput from 'components/TextInput/TextInput.react';
16+
import Label from 'components/Label/Label.react';
17+
import Position from 'lib/Position';
18+
import React from 'react';
19+
import styles from 'components/BrowserFilter/BrowserFilter.scss';
1720
import { List, Map } from 'immutable';
1821

1922
const POPOVER_CONTENT_ID = 'browserFilterPopover';
@@ -26,7 +29,9 @@ export default class BrowserFilter extends React.Component {
2629
open: false,
2730
editMode: true,
2831
filters: new List(),
29-
blacklistedFilters: Filters.BLACKLISTED_FILTERS.concat(props.blacklistedFilters)
32+
confirmName: false,
33+
name: '',
34+
blacklistedFilters: Filters.BLACKLISTED_FILTERS.concat(props.blacklistedFilters),
3035
};
3136
this.toggle = this.toggle.bind(this);
3237
this.wrapRef = React.createRef();
@@ -43,13 +48,13 @@ export default class BrowserFilter extends React.Component {
4348
if (this.props.filters.size === 0) {
4449
let available = Filters.availableFilters(this.props.schema, null, this.state.blacklistedFilters);
4550
let field = Object.keys(available)[0];
46-
filters = new List([
47-
new Map({ field: field, constraint: available[field][0] })
48-
]);
51+
filters = new List([new Map({ field: field, constraint: available[field][0] })]);
4952
}
50-
this.setState(prevState => ({
53+
this.setState((prevState) => ({
5154
open: !prevState.open,
5255
filters: filters,
56+
name: '',
57+
confirmName: false,
5358
editMode: this.props.filters.size === 0
5459
}));
5560
this.props.setCurrent(null);
@@ -71,7 +76,7 @@ export default class BrowserFilter extends React.Component {
7176
}
7277

7378
apply() {
74-
let formatted = this.state.filters.map(filter => {
79+
let formatted = this.state.filters.map((filter) => {
7580
// TODO: type is unused?
7681
/*let type = this.props.schema[filter.get('field')].type;
7782
if (Filters.Constraints[filter.get('constraint')].hasOwnProperty('field')) {
@@ -82,13 +87,25 @@ export default class BrowserFilter extends React.Component {
8287
// remove compareTo for constraints which are not comparable
8388
let isComparable = Filters.Constraints[filter.get('constraint')].comparable;
8489
if (!isComparable) {
85-
return filter.delete('compareTo')
90+
return filter.delete('compareTo');
8691
}
8792
return filter;
8893
});
8994
this.props.onChange(formatted);
9095
}
9196

97+
save() {
98+
let formatted = this.state.filters.map((filter) => {
99+
let isComparable = Filters.Constraints[filter.get('constraint')].comparable;
100+
if (!isComparable) {
101+
return filter.delete('compareTo');
102+
}
103+
return filter;
104+
});
105+
this.props.onSaveFilter(formatted, this.state.name);
106+
this.toggle();
107+
}
108+
92109
render() {
93110
let popover = null;
94111
let buttonStyle = [styles.entry];
@@ -102,49 +119,45 @@ export default class BrowserFilter extends React.Component {
102119
if (this.props.filters.size) {
103120
popoverStyle.push(styles.active);
104121
}
105-
let available = Filters.availableFilters(
106-
this.props.schema,
107-
this.state.filters
108-
);
122+
let available = Filters.availableFilters(this.props.schema, this.state.filters);
109123
popover = (
110124
<Popover fixed={true} position={position} onExternalClick={this.toggle} contentId={POPOVER_CONTENT_ID}>
111125
<div className={popoverStyle.join(' ')} onClick={() => this.props.setCurrent(null)} id={POPOVER_CONTENT_ID}>
112-
<div onClick={this.toggle} style={{ cursor: 'pointer', width: node.clientWidth, height: node.clientHeight }}></div>
126+
<div
127+
onClick={this.toggle}
128+
style={{
129+
cursor: 'pointer',
130+
width: node.clientWidth,
131+
height: node.clientHeight,
132+
}}
133+
></div>
113134
<div className={styles.body}>
114135
<Filter
115136
className={this.props.className}
116137
blacklist={this.state.blacklistedFilters}
117138
schema={this.props.schema}
118139
filters={this.state.filters}
119-
onChange={filters => this.setState({ filters: filters })}
140+
onChange={(filters) => this.setState({ filters: filters })}
120141
onSearch={this.apply.bind(this)}
121142
renderRow={props => (
122143
<FilterRow {...props} active={this.props.filters.size > 0} editMode={this.state.editMode} parentContentId={POPOVER_CONTENT_ID} />
123144
)}
124145
/>
125-
<div className={styles.footer}>
126-
<Button
127-
color="white"
128-
value="Clear all"
129-
disabled={this.state.filters.size === 0}
130-
width="120px"
131-
onClick={this.clear.bind(this)}
132-
/>
133-
<Button
134-
color="white"
135-
value="Add filter"
136-
disabled={Object.keys(available).length === 0}
137-
width="120px"
138-
onClick={this.addRow.bind(this)}
139-
/>
140-
<Button
141-
color="white"
142-
primary={true}
143-
value="Apply these filters"
144-
width="245px"
145-
onClick={this.apply.bind(this)}
146-
/>
147-
</div>
146+
{this.state.confirmName && <Field label={<Label text="Filter view name" />} input={<TextInput placeholder="Give it a good name..." value={this.state.name} onChange={(name) => this.setState({ name })} />} />}
147+
{this.state.confirmName && (
148+
<div className={styles.footer}>
149+
<Button color="white" value="Back" width="120px" onClick={() => this.setState({ confirmName: false })} />
150+
<Button color="white" value="Confirm" primary={true} width="120px" onClick={() => this.save()} />
151+
</div>
152+
)}
153+
{!this.state.confirmName && (
154+
<div className={styles.footer}>
155+
<Button color="white" value="Save" width="120px" onClick={() => this.setState({ confirmName: true })} />
156+
<Button color="white" value="Clear" disabled={this.state.filters.size === 0} width="120px" onClick={() => this.clear()} />
157+
<Button color="white" value="Add" disabled={Object.keys(available).length === 0} width="120px" onClick={() => this.addRow()} />
158+
<Button color="white" primary={true} value="Apply" width="120px" onClick={() => this.apply()} />
159+
</div>
160+
)}
148161
</div>
149162
</div>
150163
</Popover>

src/components/CategoryList/CategoryList.react.js

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,21 @@
55
* This source code is licensed under the license found in the LICENSE file in
66
* the root directory of this source tree.
77
*/
8-
import PropTypes from 'lib/PropTypes';
9-
import React from 'react';
10-
import styles from 'components/CategoryList/CategoryList.scss';
11-
import { Link } from 'react-router-dom';
12-
import generatePath from 'lib/generatePath';
8+
import PropTypes from 'lib/PropTypes';
9+
import React from 'react';
10+
import styles from 'components/CategoryList/CategoryList.scss';
11+
import { Link } from 'react-router-dom';
12+
import generatePath from 'lib/generatePath';
1313
import { CurrentApp } from 'context/currentApp';
1414

1515
export default class CategoryList extends React.Component {
1616
static contextType = CurrentApp;
1717
constructor() {
1818
super();
1919
this.listWrapperRef = React.createRef();
20+
this.state = {
21+
openClasses: [],
22+
};
2023
}
2124

2225
componentDidMount() {
@@ -41,19 +44,50 @@ export default class CategoryList extends React.Component {
4144

4245
_updateHighlight() {
4346
if (this.highlight) {
47+
let height = 0;
4448
for (let i = 0; i < this.props.categories.length; i++) {
4549
let c = this.props.categories[i];
4650
let id = c.id || c.name;
4751
if (id === this.props.current) {
52+
if (this.state.openClasses.includes(id)) {
53+
const query = new URLSearchParams(this.props.params);
54+
if (query.has('filters')) {
55+
const queryFilter = query.get('filters')
56+
for (let i = 0; i < c.filters?.length; i++) {
57+
const filter = c.filters[i];
58+
if (queryFilter === filter.filter) {
59+
height += (i + 1) * 20
60+
break;
61+
}
62+
}
63+
}
64+
}
4865
this.highlight.style.display = 'block';
49-
this.highlight.style.top = (i * 20) + 'px';
66+
this.highlight.style.top = height + 'px';
5067
return;
5168
}
69+
if (this.state.openClasses.includes(id)) {
70+
height = height + (20 * (c.filters.length + 1))
71+
} else {
72+
height += 20;
73+
}
5274
}
5375
this.highlight.style.display = 'none';
5476
}
5577
}
5678

79+
toggleDropdown(e, id) {
80+
e.preventDefault();
81+
const openClasses = [...this.state.openClasses];
82+
const index = openClasses.indexOf(id);
83+
if (openClasses.includes(id)) {
84+
openClasses.splice(index, 1);
85+
} else {
86+
openClasses.push(id);
87+
}
88+
this.setState({ openClasses });
89+
}
90+
5791
render() {
5892
if (this.props.categories.length === 0) {
5993
return null;
@@ -67,15 +101,68 @@ export default class CategoryList extends React.Component {
67101
}
68102
let count = c.count;
69103
let className = id === this.props.current ? styles.active : '';
70-
let link = generatePath(
71-
this.context,
72-
(this.props.linkPrefix || '') + (c.link || id)
73-
);
104+
let selectedFilter = null;
105+
if (this.state.openClasses.includes(id)) {
106+
const query = new URLSearchParams(this.props.params);
107+
if (query.has('filters')) {
108+
const queryFilter = query.get('filters')
109+
for (let i = 0; i < c.filters?.length; i++) {
110+
const filter = c.filters[i];
111+
if (queryFilter === filter.filter) {
112+
selectedFilter = i;
113+
className = '';
114+
break;
115+
}
116+
}
117+
}
118+
}
119+
let link = generatePath(this.context, (this.props.linkPrefix || '') + (c.link || id));
74120
return (
75-
<Link title={c.name} to={{ pathname: link }} className={className} key={id} >
76-
<span>{count}</span>
77-
<span>{c.name}</span>
78-
</Link>
121+
<div>
122+
<div className={styles.link}>
123+
<Link title={c.name} to={{ pathname: link }} className={className} key={id}>
124+
<span>{count}</span>
125+
<span>{c.name}</span>
126+
</Link>
127+
{(c.filters || []).length !== 0 && (
128+
<a
129+
className={styles.expand}
130+
onClick={(e) => this.toggleDropdown(e, id)}
131+
style={{
132+
transform: this.state.openClasses.includes(id) ? 'scaleY(-1)' : 'scaleY(1)',
133+
}}
134+
></a>
135+
)}
136+
</div>
137+
{this.state.openClasses.includes(id) &&
138+
c.filters.map((filterData, index) => {
139+
const { name, filter } = filterData;
140+
const url = `${this.props.linkPrefix}${c.name}?filters=${encodeURIComponent(filter)}`;
141+
return (
142+
<div className={styles.childLink}>
143+
<Link
144+
className={selectedFilter === index ? styles.active : ''}
145+
onClick={(e) => {
146+
e.preventDefault();
147+
this.props.filterClicked(url);
148+
}}
149+
key={name + index}
150+
>
151+
<span>{name}</span>
152+
</Link>
153+
<a
154+
className={styles.close}
155+
onClick={(e) => {
156+
e.preventDefault();
157+
this.props.removeFilter(filterData);
158+
}}
159+
>
160+
×
161+
</a>
162+
</div>
163+
);
164+
})}
165+
</div>
79166
);
80167
})}
81168
</div>

0 commit comments

Comments
 (0)