Skip to content

Commit 55ab3c0

Browse files
authored
Merge pull request #56 from AllenInstitute/feature/allow-annotation-interactions-while-loading
Feature/allow annotation interactions while loading
2 parents 8298499 + a37eb41 commit 55ab3c0

File tree

16 files changed

+243
-55
lines changed

16 files changed

+243
-55
lines changed

dev-docs/02-setup-and-workflow.md

+6
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ rendered application, like `desktop` or `web`.
4141
To make that happen:
4242
1. Navigate to either `packages/desktop` or `packages/web` and run `npm run start`.
4343

44+
### Testing
45+
Most components in the project have associated unit tests; to run the full suite, run `npm run test`.
46+
The unit tests for each of the packages can also be run independently, using `npm run test:core`, `npm run test:desktop`,
47+
or `npm run test:web`, respectively.
48+
49+
To run the linter, use `npm run lint`, and for typechecking, use `npm run typeCheck`.
4450

4551
### Adding a dependency to to either the `desktop` or `web` subpackages
4652
In the case that you need to add either a dependency or devDependency to either the `desktop` or `web` subpackages within this

packages/core/components/AnnotationList/AnnotationListItem.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export default React.memo(function AnnotationListItem(props: AnnotationListItemP
4949
data-testid="annotation-list-item"
5050
className={classNames(
5151
{
52-
[styles.disabled]: disabled || loading,
52+
[styles.disabled]: disabled,
5353
},
5454
styles.title
5555
)}

packages/core/components/DirectoryTree/index.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Tutorial from "../../entity/Tutorial";
1010
import RootLoadingIndicator from "./RootLoadingIndicator";
1111
import useDirectoryHierarchy from "./useDirectoryHierarchy";
1212
import FileMetadataSearchBar from "../FileMetadataSearchBar";
13+
import EmptyFileListMessage from "../EmptyFileListMessage";
1314

1415
import styles from "./DirectoryTree.module.css";
1516

@@ -84,6 +85,9 @@ export default function DirectoryTree(props: FileListProps) {
8485
aria-multiselectable="true"
8586
id={Tutorial.FILE_LIST_ID}
8687
>
88+
{!error && (!content || (Array.isArray(content) && !content.length)) && (
89+
<EmptyFileListMessage />
90+
)}
8791
{!error && content}
8892
{error && (
8993
<aside className={styles.errorMessage}>

packages/core/components/DirectoryTree/test/DirectoryTree.test.tsx

+26
Original file line numberDiff line numberDiff line change
@@ -474,4 +474,30 @@ describe("<DirectoryTree />", () => {
474474
header = await findByTestId(directoryTreeNodes[0], "treeitemheader"); // refresh node
475475
expect(header.classList.contains(styles.focused)).to.be.true;
476476
});
477+
478+
it("displays 'No files found' when no files found", async () => {
479+
sandbox.restore();
480+
const emptyFileService = new FileService();
481+
sandbox.stub(interaction.selectors, "getFileService").returns(emptyFileService);
482+
483+
const { store } = configureMockStore({
484+
state,
485+
responseStubs,
486+
reducer,
487+
logics: reduxLogics,
488+
});
489+
490+
const { queryAllByRole, findByText } = render(
491+
<Provider store={store}>
492+
<DirectoryTree />
493+
</Provider>
494+
);
495+
496+
// No tree items should load
497+
const directoryTreeNodes = queryAllByRole("treeitem");
498+
expect(directoryTreeNodes.length).to.equal(0);
499+
500+
// Wait for the fileService call to return, then check for empty list message
501+
await findByText("Sorry! No files found");
502+
});
477503
});

packages/core/components/DnDList/DnDList.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export default function DnDList(props: DnDListProps) {
5757
data-testid={DND_LIST_CONTAINER_ID}
5858
>
5959
{items.reduce((accum, item, index) => {
60-
const disabled = loading || item.disabled;
60+
const disabled = item.disabled;
6161
return [
6262
...accum,
6363
...(dividers && dividers[index] ? [dividers[index]] : []),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.empty-file-list-container {
2+
display: flex;
3+
height: 100%;
4+
}
5+
6+
.empty-file-list-message {
7+
margin: auto;
8+
text-align: center;
9+
}
10+
11+
.empty-search-icon {
12+
font-size: 7em;
13+
}
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import * as React from "react";
2+
import styles from "./EmptyFileListMessage.module.css";
3+
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";
9+
10+
export default function EmptyFileListMessage() {
11+
const annotationHierarchyListItems = useSelector(annotationSelectors.getHierarchyListItems);
12+
const groupedByFilterName = useSelector(selection.selectors.getGroupedByFilterName);
13+
14+
return (
15+
<div className={styles.emptyFileListContainer}>
16+
<div className={styles.emptyFileListMessage}>
17+
<Icon className={styles.emptySearchIcon} iconName="SearchIssue" />
18+
<h2>Sorry! No files found</h2>
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>
60+
Double check your filters for any issues and then contact the software team if
61+
you still expect there to be matches present.
62+
</div>
63+
</div>
64+
</div>
65+
);
66+
}

packages/core/components/FileList/FileList.module.css

-14
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,6 @@
88
height: 100%;
99
}
1010

11-
.empty-file-list-container {
12-
display: flex;
13-
height: 100%;
14-
}
15-
16-
.empty-file-list-message {
17-
margin: auto;
18-
text-align: center;
19-
}
20-
21-
.empty-search-icon {
22-
font-size: 7em;
23-
}
24-
2511
.list {
2612
height: calc(100% - var(--row-count-height));
2713
}

packages/core/components/FileList/index.tsx

+2-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Icon } from "@fluentui/react";
21
import classNames from "classnames";
32
import debouncePromise from "debounce-promise";
43
import { defaults, isFunction } from "lodash";
@@ -14,6 +13,7 @@ import { selection } from "../../state";
1413
import useLayoutMeasurements from "../../hooks/useLayoutMeasurements";
1514
import useFileSelector from "./useFileSelector";
1615
import useFileAccessContextMenu from "./useFileAccessContextMenu";
16+
import EmptyFileListMessage from "../EmptyFileListMessage";
1717

1818
import styles from "./FileList.module.css";
1919

@@ -160,18 +160,7 @@ export default function FileList(props: FileListProps) {
160160
);
161161
}
162162
} else {
163-
content = (
164-
<div className={styles.emptyFileListContainer}>
165-
<div className={styles.emptyFileListMessage}>
166-
<Icon className={styles.emptySearchIcon} iconName="SearchIssue" />
167-
<h2>Sorry! No files found :(</h2>
168-
<h3>
169-
Double check your filters for any issues and then contact the Software team
170-
if you still expect there to be matches present.
171-
</h3>
172-
</div>
173-
</div>
174-
);
163+
content = <EmptyFileListMessage />;
175164
}
176165

177166
return (

packages/core/components/FileList/test/FileList.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe("<FileList />", () => {
5353
expect(queryByText("Counting files...")).to.exist;
5454

5555
// Wait for the fileService call to return, then check for updated list length display
56-
await findByText("Sorry! No files found :(");
56+
await findByText("Sorry! No files found");
5757

5858
// Assert
5959
expect(queryByText("Counting files...")).to.not.exist;

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.

0 commit comments

Comments
 (0)