Skip to content

Commit

Permalink
feat: Add filter views to save frequently used filters in data browser (
Browse files Browse the repository at this point in the history
  • Loading branch information
dblythy authored Jun 9, 2023
1 parent 27cdaf1 commit a9ec3a9
Show file tree
Hide file tree
Showing 8 changed files with 661 additions and 529 deletions.
8 changes: 4 additions & 4 deletions src/components/Autocomplete/Autocomplete.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export default class Autocomplete extends Component {
// Tab
// do not type it
e.preventDefault();

e.stopPropagation();
// move focus to input
this.inputRef.current.focus();
Expand Down Expand Up @@ -318,7 +318,7 @@ export default class Autocomplete extends Component {
onClick={onClick}
/>
);
}
}

return (
<React.Fragment>
Expand Down Expand Up @@ -372,5 +372,5 @@ Autocomplete.propTypes = {
),
error: PropTypes.string.describe(
'Error to be rendered in place of label if defined'
)
}
)
}
103 changes: 58 additions & 45 deletions src/components/BrowserFilter/BrowserFilter.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
import * as Filters from 'lib/Filters';
import Button from 'components/Button/Button.react';
import Filter from 'components/Filter/Filter.react';
import FilterRow from 'components/BrowserFilter/FilterRow.react';
import Icon from 'components/Icon/Icon.react';
import Popover from 'components/Popover/Popover.react';
import Position from 'lib/Position';
import React from 'react';
import styles from 'components/BrowserFilter/BrowserFilter.scss';
import * as Filters from 'lib/Filters';
import Button from 'components/Button/Button.react';
import Filter from 'components/Filter/Filter.react';
import FilterRow from 'components/BrowserFilter/FilterRow.react';
import Icon from 'components/Icon/Icon.react';
import Popover from 'components/Popover/Popover.react';
import Field from 'components/Field/Field.react';
import TextInput from 'components/TextInput/TextInput.react';
import Label from 'components/Label/Label.react';
import Position from 'lib/Position';
import React from 'react';
import styles from 'components/BrowserFilter/BrowserFilter.scss';
import { List, Map } from 'immutable';

const POPOVER_CONTENT_ID = 'browserFilterPopover';
Expand All @@ -26,7 +29,9 @@ export default class BrowserFilter extends React.Component {
open: false,
editMode: true,
filters: new List(),
blacklistedFilters: Filters.BLACKLISTED_FILTERS.concat(props.blacklistedFilters)
confirmName: false,
name: '',
blacklistedFilters: Filters.BLACKLISTED_FILTERS.concat(props.blacklistedFilters),
};
this.toggle = this.toggle.bind(this);
this.wrapRef = React.createRef();
Expand All @@ -43,13 +48,13 @@ export default class BrowserFilter extends React.Component {
if (this.props.filters.size === 0) {
let available = Filters.availableFilters(this.props.schema, null, this.state.blacklistedFilters);
let field = Object.keys(available)[0];
filters = new List([
new Map({ field: field, constraint: available[field][0] })
]);
filters = new List([new Map({ field: field, constraint: available[field][0] })]);
}
this.setState(prevState => ({
this.setState((prevState) => ({
open: !prevState.open,
filters: filters,
name: '',
confirmName: false,
editMode: this.props.filters.size === 0
}));
this.props.setCurrent(null);
Expand All @@ -71,7 +76,7 @@ export default class BrowserFilter extends React.Component {
}

apply() {
let formatted = this.state.filters.map(filter => {
let formatted = this.state.filters.map((filter) => {
// TODO: type is unused?
/*let type = this.props.schema[filter.get('field')].type;
if (Filters.Constraints[filter.get('constraint')].hasOwnProperty('field')) {
Expand All @@ -82,13 +87,25 @@ export default class BrowserFilter extends React.Component {
// remove compareTo for constraints which are not comparable
let isComparable = Filters.Constraints[filter.get('constraint')].comparable;
if (!isComparable) {
return filter.delete('compareTo')
return filter.delete('compareTo');
}
return filter;
});
this.props.onChange(formatted);
}

save() {
let formatted = this.state.filters.map((filter) => {
let isComparable = Filters.Constraints[filter.get('constraint')].comparable;
if (!isComparable) {
return filter.delete('compareTo');
}
return filter;
});
this.props.onSaveFilter(formatted, this.state.name);
this.toggle();
}

render() {
let popover = null;
let buttonStyle = [styles.entry];
Expand All @@ -102,49 +119,45 @@ export default class BrowserFilter extends React.Component {
if (this.props.filters.size) {
popoverStyle.push(styles.active);
}
let available = Filters.availableFilters(
this.props.schema,
this.state.filters
);
let available = Filters.availableFilters(this.props.schema, this.state.filters);
popover = (
<Popover fixed={true} position={position} onExternalClick={this.toggle} contentId={POPOVER_CONTENT_ID}>
<div className={popoverStyle.join(' ')} onClick={() => this.props.setCurrent(null)} id={POPOVER_CONTENT_ID}>
<div onClick={this.toggle} style={{ cursor: 'pointer', width: node.clientWidth, height: node.clientHeight }}></div>
<div
onClick={this.toggle}
style={{
cursor: 'pointer',
width: node.clientWidth,
height: node.clientHeight,
}}
></div>
<div className={styles.body}>
<Filter
className={this.props.className}
blacklist={this.state.blacklistedFilters}
schema={this.props.schema}
filters={this.state.filters}
onChange={filters => this.setState({ filters: filters })}
onChange={(filters) => this.setState({ filters: filters })}
onSearch={this.apply.bind(this)}
renderRow={props => (
<FilterRow {...props} active={this.props.filters.size > 0} editMode={this.state.editMode} parentContentId={POPOVER_CONTENT_ID} />
)}
/>
<div className={styles.footer}>
<Button
color="white"
value="Clear all"
disabled={this.state.filters.size === 0}
width="120px"
onClick={this.clear.bind(this)}
/>
<Button
color="white"
value="Add filter"
disabled={Object.keys(available).length === 0}
width="120px"
onClick={this.addRow.bind(this)}
/>
<Button
color="white"
primary={true}
value="Apply these filters"
width="245px"
onClick={this.apply.bind(this)}
/>
</div>
{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 })} />} />}
{this.state.confirmName && (
<div className={styles.footer}>
<Button color="white" value="Back" width="120px" onClick={() => this.setState({ confirmName: false })} />
<Button color="white" value="Confirm" primary={true} width="120px" onClick={() => this.save()} />
</div>
)}
{!this.state.confirmName && (
<div className={styles.footer}>
<Button color="white" value="Save" width="120px" onClick={() => this.setState({ confirmName: true })} />
<Button color="white" value="Clear" disabled={this.state.filters.size === 0} width="120px" onClick={() => this.clear()} />
<Button color="white" value="Add" disabled={Object.keys(available).length === 0} width="120px" onClick={() => this.addRow()} />
<Button color="white" primary={true} value="Apply" width="120px" onClick={() => this.apply()} />
</div>
)}
</div>
</div>
</Popover>
Expand Down
115 changes: 101 additions & 14 deletions src/components/CategoryList/CategoryList.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
import PropTypes from 'lib/PropTypes';
import React from 'react';
import styles from 'components/CategoryList/CategoryList.scss';
import { Link } from 'react-router-dom';
import generatePath from 'lib/generatePath';
import PropTypes from 'lib/PropTypes';
import React from 'react';
import styles from 'components/CategoryList/CategoryList.scss';
import { Link } from 'react-router-dom';
import generatePath from 'lib/generatePath';
import { CurrentApp } from 'context/currentApp';

export default class CategoryList extends React.Component {
static contextType = CurrentApp;
constructor() {
super();
this.listWrapperRef = React.createRef();
this.state = {
openClasses: [],
};
}

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

_updateHighlight() {
if (this.highlight) {
let height = 0;
for (let i = 0; i < this.props.categories.length; i++) {
let c = this.props.categories[i];
let id = c.id || c.name;
if (id === this.props.current) {
if (this.state.openClasses.includes(id)) {
const query = new URLSearchParams(this.props.params);
if (query.has('filters')) {
const queryFilter = query.get('filters')
for (let i = 0; i < c.filters?.length; i++) {
const filter = c.filters[i];
if (queryFilter === filter.filter) {
height += (i + 1) * 20
break;
}
}
}
}
this.highlight.style.display = 'block';
this.highlight.style.top = (i * 20) + 'px';
this.highlight.style.top = height + 'px';
return;
}
if (this.state.openClasses.includes(id)) {
height = height + (20 * (c.filters.length + 1))
} else {
height += 20;
}
}
this.highlight.style.display = 'none';
}
}

toggleDropdown(e, id) {
e.preventDefault();
const openClasses = [...this.state.openClasses];
const index = openClasses.indexOf(id);
if (openClasses.includes(id)) {
openClasses.splice(index, 1);
} else {
openClasses.push(id);
}
this.setState({ openClasses });
}

render() {
if (this.props.categories.length === 0) {
return null;
Expand All @@ -67,15 +101,68 @@ export default class CategoryList extends React.Component {
}
let count = c.count;
let className = id === this.props.current ? styles.active : '';
let link = generatePath(
this.context,
(this.props.linkPrefix || '') + (c.link || id)
);
let selectedFilter = null;
if (this.state.openClasses.includes(id)) {
const query = new URLSearchParams(this.props.params);
if (query.has('filters')) {
const queryFilter = query.get('filters')
for (let i = 0; i < c.filters?.length; i++) {
const filter = c.filters[i];
if (queryFilter === filter.filter) {
selectedFilter = i;
className = '';
break;
}
}
}
}
let link = generatePath(this.context, (this.props.linkPrefix || '') + (c.link || id));
return (
<Link title={c.name} to={{ pathname: link }} className={className} key={id} >
<span>{count}</span>
<span>{c.name}</span>
</Link>
<div>
<div className={styles.link}>
<Link title={c.name} to={{ pathname: link }} className={className} key={id}>
<span>{count}</span>
<span>{c.name}</span>
</Link>
{(c.filters || []).length !== 0 && (
<a
className={styles.expand}
onClick={(e) => this.toggleDropdown(e, id)}
style={{
transform: this.state.openClasses.includes(id) ? 'scaleY(-1)' : 'scaleY(1)',
}}
></a>
)}
</div>
{this.state.openClasses.includes(id) &&
c.filters.map((filterData, index) => {
const { name, filter } = filterData;
const url = `${this.props.linkPrefix}${c.name}?filters=${encodeURIComponent(filter)}`;
return (
<div className={styles.childLink}>
<Link
className={selectedFilter === index ? styles.active : ''}
onClick={(e) => {
e.preventDefault();
this.props.filterClicked(url);
}}
key={name + index}
>
<span>{name}</span>
</Link>
<a
className={styles.close}
onClick={(e) => {
e.preventDefault();
this.props.removeFilter(filterData);
}}
>
×
</a>
</div>
);
})}
</div>
);
})}
</div>
Expand Down
Loading

0 comments on commit a9ec3a9

Please sign in to comment.