Skip to content

Commit a37eb41

Browse files
committed
display applied filters in empty file message
1 parent 75f51a5 commit a37eb41

File tree

7 files changed

+173
-27
lines changed

7 files changed

+173
-27
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
.filter {
2+
margin: 4px 4px 4px 8px;
3+
/* flex parent */
4+
display: flex;
5+
flex-direction: row;
6+
justify-content: center;
7+
width: 100%;
8+
}
9+
10+
.filter-text {
11+
text-overflow: ellipsis;
12+
white-space: nowrap;
13+
overflow: hidden;
14+
text-align: center;
15+
max-width: 500px;
16+
}
17+
18+
.expandable {
19+
cursor: pointer;
20+
}
21+
22+
.filter-text[title] {
23+
text-decoration: none;
24+
}
25+
26+
.filter:not(:last-child):after {
27+
content: "; "
28+
}
29+
30+
.expanded {
31+
max-width: 100%;
32+
white-space: normal;
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import classNames from "classnames";
2+
import { map } from "lodash";
3+
import * as React from "react";
4+
import styles from "./FilterList.module.css";
5+
import { Filter } from "../../entity/FileFilter";
6+
7+
interface Props {
8+
name: string;
9+
filters: Filter[];
10+
}
11+
12+
/**
13+
* UI for displaying the annotation values applied as file filters. Each `FileFilter` within `props.filters`
14+
* must relate to the same annotation (e.g. Each `FileFilter::name` should be equal).
15+
* Logic is based on the FilterMedallion component
16+
*/
17+
export default function FilterList(props: Props) {
18+
const { filters, name } = props;
19+
20+
const [expanded, setExpanded] = React.useState(false);
21+
22+
// Determine if filter has reached its max-width and text is overflowing
23+
// If a filter is no longer overflowing but its "expanded" state is truthy, reset.
24+
const [overflowing, setOverflowing] = React.useState(false);
25+
const textRef = React.useRef<HTMLElement | null>(null);
26+
React.useEffect(() => {
27+
if (textRef.current) {
28+
const width = textRef.current.clientWidth;
29+
const scrollWidth = textRef.current.scrollWidth;
30+
const isOverflowing = scrollWidth > width;
31+
setOverflowing(isOverflowing);
32+
if (!isOverflowing) {
33+
setExpanded(false);
34+
}
35+
} else {
36+
setOverflowing(false);
37+
}
38+
}, [textRef, filters]);
39+
40+
const operator = filters.length > 1 ? "for values of" : "equal to";
41+
const valueDisplay = map(filters, (filter) => filter.displayValue).join(", ");
42+
const display = ` ${operator} ${valueDisplay}`;
43+
44+
return (
45+
<span className={styles.filter}>
46+
<span
47+
className={classNames(styles.filterText, {
48+
[styles.expandable]: overflowing,
49+
[styles.expanded]: expanded,
50+
})}
51+
onClick={() => overflowing && setExpanded((prev) => !prev)}
52+
ref={textRef}
53+
title={name + display}
54+
>
55+
<b>{name}</b> {display}
56+
</span>
57+
</span>
58+
);
59+
}

packages/core/components/EmptyFileListMessage/index.tsx

+50-2
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,65 @@
11
import * as React from "react";
22
import styles from "./EmptyFileListMessage.module.css";
33
import { Icon } from "@fluentui/react";
4+
import { useSelector } from "react-redux";
5+
import { map, isEmpty } from "lodash";
6+
import { selection } from "../../state";
7+
import * as annotationSelectors from "../AnnotationSidebar/selectors";
8+
import FilterList from "./FilterList";
49

510
export default function EmptyFileListMessage() {
11+
const annotationHierarchyListItems = useSelector(annotationSelectors.getHierarchyListItems);
12+
const groupedByFilterName = useSelector(selection.selectors.getGroupedByFilterName);
13+
614
return (
715
<div className={styles.emptyFileListContainer}>
816
<div className={styles.emptyFileListMessage}>
917
<Icon className={styles.emptySearchIcon} iconName="SearchIssue" />
1018
<h2>Sorry! No files found</h2>
11-
<h3>
19+
<div>
20+
We couldn&apos;t find any files
21+
{isEmpty(groupedByFilterName) && annotationHierarchyListItems.length === 0 ? (
22+
<>matching your request.</>
23+
) : (
24+
<span>
25+
{!isEmpty(groupedByFilterName) && (
26+
<span>
27+
{" "}
28+
matching
29+
{map(groupedByFilterName, (filters, filterName) => (
30+
<FilterList
31+
key={filterName}
32+
filters={filters}
33+
name={filterName}
34+
/>
35+
))}
36+
</span>
37+
)}
38+
{annotationHierarchyListItems.length > 0 && (
39+
<span>
40+
{" "}
41+
with annotation
42+
{annotationHierarchyListItems.length === 1 ? "" : "s "}
43+
{map(annotationHierarchyListItems, (annotation, index) => (
44+
<span key={annotation.id} title={annotation.description}>
45+
{index > 0
46+
? index === annotationHierarchyListItems.length - 1
47+
? " and "
48+
: ", "
49+
: " "}
50+
<b>{annotation.title}</b>
51+
</span>
52+
))}
53+
</span>
54+
)}{" "}
55+
</span>
56+
)}
57+
</div>
58+
<br />
59+
<div>
1260
Double check your filters for any issues and then contact the software team if
1361
you still expect there to be matches present.
14-
</h3>
62+
</div>
1563
</div>
1664
</div>
1765
);

packages/core/components/FilterDisplayBar/FilterMedallion.tsx

+1-7
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,11 @@ import * as React from "react";
55
import { useDispatch } from "react-redux";
66

77
import { selection } from "../../state";
8-
import FileFilter from "../../entity/FileFilter";
8+
import FileFilter, { Filter } from "../../entity/FileFilter";
99
import AnnotationFilter from "../AnnotationSidebar/AnnotationFilter";
1010

1111
import styles from "./FilterMedallion.module.css";
1212

13-
export interface Filter {
14-
name: string;
15-
value: any;
16-
displayValue: string;
17-
}
18-
1913
interface Props {
2014
name: string;
2115
filters: Filter[];

packages/core/components/FilterDisplayBar/index.tsx

+4-17
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import classNames from "classnames";
2-
import { groupBy, keyBy, map } from "lodash";
2+
import { map } from "lodash";
33
import * as React from "react";
44
import { useSelector } from "react-redux";
5-
import FileFilter from "../../entity/FileFilter";
65

7-
import { metadata, selection } from "../../state";
8-
import FilterMedallion, { Filter } from "./FilterMedallion";
6+
import { selection } from "../../state";
7+
import FilterMedallion from "./FilterMedallion";
98

109
import styles from "./FilterDisplayBar.module.css";
1110

@@ -23,19 +22,7 @@ export default function FilterDisplayBar(props: Props) {
2322
const { className, classNameHidden } = props;
2423

2524
const globalFilters = useSelector(selection.selectors.getAnnotationFilters);
26-
const annotations = useSelector(metadata.selectors.getAnnotations);
27-
const groupedByFilterName = React.useMemo(() => {
28-
const annotationNameToInstanceMap = keyBy(annotations, "name");
29-
const filters: Filter[] = map(globalFilters, (filter: FileFilter) => {
30-
const annotation = annotationNameToInstanceMap[filter.name];
31-
return {
32-
name: filter.name,
33-
value: filter.value,
34-
displayValue: annotation?.getDisplayValue(filter.value),
35-
};
36-
}).filter((filter) => filter.displayValue !== undefined);
37-
return groupBy(filters, (filter) => filter.name);
38-
}, [globalFilters, annotations]);
25+
const groupedByFilterName = useSelector(selection.selectors.getGroupedByFilterName);
3926

4027
return (
4128
<div

packages/core/entity/FileFilter/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ export interface FileFilterJson {
33
value: any;
44
}
55

6+
// Filter with formatted value
7+
export interface Filter {
8+
name: string;
9+
value: any;
10+
displayValue: string;
11+
}
12+
613
/**
714
* Stub for a filter used to constrain a listing of files to those that match a particular condition. Should be
815
* serializable to a URL query string-friendly format.

packages/core/state/selection/selectors.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { State } from "../";
44
import { TOP_LEVEL_FILE_ANNOTATION_NAMES } from "../../constants";
55
import Annotation from "../../entity/Annotation";
66
import FileExplorerURL from "../../entity/FileExplorerURL";
7-
import FileFilter from "../../entity/FileFilter";
7+
import FileFilter, { Filter } from "../../entity/FileFilter";
88
import FileFolder from "../../entity/FileFolder";
99
import FileSort from "../../entity/FileSort";
1010
import { Dataset } from "../../services/DatasetService";
11+
import { groupBy, keyBy, map } from "lodash";
12+
import { getAnnotations } from "../metadata/selectors";
1113

1214
// BASIC SELECTORS
1315
export const getAnnotationHierarchy = (state: State) => state.selection.annotationHierarchy;
@@ -59,3 +61,19 @@ export const getAnnotationFilters = createSelector([getFileFilters], (fileFilter
5961
export const getFileAttributeFilter = createSelector([getFileFilters], (fileFilters):
6062
| FileFilter
6163
| undefined => fileFilters.find((f) => TOP_LEVEL_FILE_ANNOTATION_NAMES.includes(f.name)));
64+
65+
export const getGroupedByFilterName = createSelector(
66+
[getAnnotationFilters, getAnnotations],
67+
(globalFilters: FileFilter[], annotations: Annotation[]) => {
68+
const annotationNameToInstanceMap = keyBy(annotations, "name");
69+
const filters: Filter[] = map(globalFilters, (filter: FileFilter) => {
70+
const annotation = annotationNameToInstanceMap[filter.name];
71+
return {
72+
name: filter.name,
73+
value: filter.value,
74+
displayValue: annotation?.getDisplayValue(filter.value),
75+
};
76+
}).filter((filter) => filter.displayValue !== undefined);
77+
return groupBy(filters, (filter) => filter.name);
78+
}
79+
);

0 commit comments

Comments
 (0)