Skip to content

Commit

Permalink
Merge pull request #71 from AllenInstitute/feature/standardize-proper…
Browse files Browse the repository at this point in the history
…ty-filters

Feature/standardize property filters
  • Loading branch information
aswallace authored Mar 26, 2024
2 parents 154b369 + b1588ac commit e343558
Show file tree
Hide file tree
Showing 34 changed files with 998 additions and 530 deletions.
120 changes: 99 additions & 21 deletions packages/core/components/AnnotationFilterForm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Spinner, SpinnerSize } from "@fluentui/react";
import { find, isNil } from "lodash";
import * as React from "react";
import { useDispatch, useSelector } from "react-redux";

import { AnnotationType } from "../../entity/AnnotationFormatter";
import FileFilter from "../../entity/FileFilter";
import ListPicker, { ListItem } from "../../components/ListPicker";
import ListPicker, { ListItem } from "../ListPicker";
import NumberRangePicker from "../NumberRangePicker";
import SearchBoxForm from "../SearchBoxForm";
import DateRangePicker from "../DateRangePicker";
import { interaction, metadata, selection } from "../../state";
import useAnnotationValues from "./useAnnotationValues";

Expand All @@ -20,11 +24,11 @@ interface AnnotationFilterFormProps {
*/
export default function AnnotationFilterForm(props: AnnotationFilterFormProps) {
const { annotationName } = props;

const dispatch = useDispatch();
const annotations = useSelector(metadata.selectors.getAnnotations);
const fileFilters = useSelector(selection.selectors.getAnnotationFilters);
const annotations = useSelector(metadata.selectors.getSupportedAnnotations);
const fileFilters = useSelector(selection.selectors.getFileFilters);
const annotationService = useSelector(interaction.selectors.getAnnotationService);
// TODO: annotationService throws an error for annotations that aren't in the API
const [annotationValues, isLoading, errorMessage] = useAnnotationValues(
annotationName,
annotationService
Expand All @@ -35,6 +39,11 @@ export default function AnnotationFilterForm(props: AnnotationFilterFormProps) {
[annotations, annotationName]
);

const currentValues = React.useMemo(
() => find(fileFilters, (annotation) => annotation.name === annotationName),
[annotationName, fileFilters]
);

const items = React.useMemo<ListItem[]>(() => {
const appliedFilters = fileFilters
.filter((filter) => filter.name === annotation?.name)
Expand Down Expand Up @@ -89,22 +98,91 @@ export default function AnnotationFilterForm(props: AnnotationFilterFormProps) {
dispatch(selection.actions.addFileFilter(filters));
};

// TODO, return different pickers based on annotation type
// e.g., a date picker, a range (numeric) picker, etc.
switch (annotation?.type) {
case AnnotationType.STRING:
// prettier-ignore
default: // FALL-THROUGH
return (
<ListPicker
items={items}
loading={isLoading}
errorMessage={errorMessage}
onDeselect={onDeselect}
onDeselectAll={onDeselectAll}
onSelect={onSelect}
onSelectAll={onSelectAll}
/>
);
function onSearch(filterValue: string) {
if (filterValue && filterValue.trim()) {
const fileFilter = new FileFilter(annotationName, filterValue);
if (currentValues) {
dispatch(selection.actions.removeFileFilter(currentValues));
}
dispatch(selection.actions.addFileFilter(fileFilter));
}
}

function onReset() {
if (currentValues) {
dispatch(selection.actions.removeFileFilter(currentValues));
}
}

const listPicker = () => {
return (
<ListPicker
items={items}
loading={isLoading}
errorMessage={errorMessage}
onDeselect={onDeselect}
onDeselectAll={onDeselectAll}
onSelect={onSelect}
onSelectAll={onSelectAll}
/>
);
};

if (isLoading) {
return (
<div>
<Spinner size={SpinnerSize.small} />
</div>
);
}

const customInput = () => {
switch (annotation?.type) {
case AnnotationType.DATE:
case AnnotationType.DATETIME:
return (
<DateRangePicker
onSearch={onSearch}
onReset={onReset}
currentRange={currentValues}
/>
);
case AnnotationType.NUMBER:
return (
<NumberRangePicker
items={items}
loading={isLoading}
errorMessage={errorMessage}
onSearch={onSearch}
onReset={onReset}
currentRange={currentValues}
units={annotation?.units}
/>
);
case AnnotationType.DURATION:
case AnnotationType.STRING:
// prettier-ignore
default: // FALL-THROUGH
return (
<> {listPicker()} </>
);
}
};
// Use the checkboxes if values exist and are few enough to reasonably scroll through
if (items.length > 0 && items.length <= 100) {
return <> {listPicker()} </>;
}
// Use a search box if the API does not return values to select
// (e.g., it's not an AICS annotation)
else if (items.length === 0 && annotation?.type === AnnotationType.STRING) {
return (
<SearchBoxForm
onSearch={onSearch}
onReset={onReset}
fieldName={annotation.name}
currentValue={currentValues}
/>
);
}
return <> {customInput()} </>;
}
4 changes: 2 additions & 2 deletions packages/core/components/AnnotationList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const SEARCH_ICON_PATH_DATA =
*/
export default function AnnotationList(props: AnnotationListProps) {
const dispatch = useDispatch();
const filters = useSelector(selection.selectors.getAnnotationFilters);
const filters = useSelector(selection.selectors.getFileFilters);
const annotationsLoading = useSelector(
selection.selectors.getAvailableAnnotationsForHierarchyLoading
);
Expand Down Expand Up @@ -94,7 +94,7 @@ export default function AnnotationList(props: AnnotationListProps) {
return (
<div className={classNames(styles.root, props.className)}>
<h3 className={styles.title}>Available Annotations</h3>
<h6 className={styles.description}>Drag any annotation to the box above</h6>
<h6 className={styles.description}>Drag annotations to the box above</h6>
<div className={styles.listContainer} id={Tutorial.ANNOTATION_LIST_ID}>
<div className={styles.searchBox}>
<SvgIcon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { annotationsJson } from "../../../entity/Annotation/mocks";
import FileFilter from "../../../entity/FileFilter";
import { initialState, reducer, reduxLogics, selection } from "../../../state";
import { DND_LIST_CONTAINER_ID } from "../../DnDList/DnDList";
import { SEARCHABLE_TOP_LEVEL_FILE_ANNOTATIONS } from "../../../constants";

import styles from "../AnnotationList.module.css";

Expand Down Expand Up @@ -55,7 +56,9 @@ describe("<AnnotationList />", () => {
const allAnnotationDisplayNames = annotationsJson.map(
(annotation) => annotation.annotationDisplayName
);
expect(queryNumberListItems()).to.equal(allAnnotationDisplayNames.length);
expect(queryNumberListItems()).to.equal(
allAnnotationDisplayNames.length + SEARCHABLE_TOP_LEVEL_FILE_ANNOTATIONS.length
);
allAnnotationDisplayNames.forEach((annotation) => {
expect(getByText(annotation)).to.exist;
});
Expand Down Expand Up @@ -187,8 +190,15 @@ describe("<AnnotationList />", () => {

it("does not exist when no annotations are filtered", () => {
// Arrange
const state = {
...initialState,
selection: {
...initialState.selection,
filters: [],
},
};
const { store } = configureMockStore({
state: mergeState(initialState, {
state: mergeState(state, {
metadata: {
annotations: annotationsJson.map(
(annotation) => new Annotation(annotation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ const FILTERS_APPLIED_COLOR_INDICATOR = "#0b9aab"; // style guide torquise
*/
export default function AnnotationFilter(props: FilterProps) {
const { annotationName, iconColor, styleOverrides } = props;

const fileFilters = useSelector(selection.selectors.getAnnotationFilters);
const fileFilters = useSelector(selection.selectors.getFileFilters);

const annotationIsFiltered = React.useMemo(
() => fileFilters.some((filter) => filter.name === annotationName),
Expand All @@ -38,7 +37,7 @@ export default function AnnotationFilter(props: FilterProps) {
return <AnnotationFilterForm annotationName={annotationName} />;
},
directionalHint: DirectionalHint.rightTopEdge,
title: "Exclusively Include",
title: `Filter by ${annotationName}`,
shouldFocusOnMount: true,
items: [{ key: "placeholder" }], // necessary to have a non-empty items list to have `onRenderMenuList` called
};
Expand Down
7 changes: 5 additions & 2 deletions packages/core/components/AnnotationSidebar/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { DnDItem } from "../../components/DnDList/DnDList";
import Annotation from "../../entity/Annotation";
import FileFilter from "../../entity/FileFilter";
import { metadata, selection } from "../../state";
import { TOP_LEVEL_FILE_ANNOTATION_NAMES } from "../../constants";

export const getAnnotationListItems = createSelector(
[
metadata.selectors.getSortedAnnotations,
metadata.selectors.getSupportedAnnotations,
selection.selectors.getAvailableAnnotationsForHierarchy,
selection.selectors.getAnnotationHierarchy,
selection.selectors.getAnnotationFilters,
selection.selectors.getFileFilters,
],
(
annotations: Annotation[],
Expand All @@ -37,6 +38,8 @@ export const getAnnotationListItems = createSelector(
filtered: filteredAnnotationNames.has(annotation.name),
id: annotation.name,
title: annotation.displayName,
type: annotation.type,
isFileProperty: TOP_LEVEL_FILE_ANNOTATION_NAMES.includes(annotation.name),
}))
// Sort the filtered annotations to the top
.sort((a, b) => (a.filtered && !b.filtered ? -1 : 1))
Expand Down
32 changes: 23 additions & 9 deletions packages/core/components/AnnotationSidebar/test/selectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { annotationsJson } from "../../../entity/Annotation/mocks";
import * as annotationSelectors from "../selectors";
import { initialState } from "../../../state";
import FileFilter from "../../../entity/FileFilter";
import { SEARCHABLE_TOP_LEVEL_FILE_ANNOTATIONS } from "../../../constants";

describe("<AnnotationSidebar /> selectors", () => {
describe("getAnnotationListItems", () => {
Expand All @@ -18,9 +19,12 @@ describe("<AnnotationSidebar /> selectors", () => {
});

const listItems = annotationSelectors.getAnnotationListItems(state);
expect(listItems.length).to.equal(annotationsJson.length);
expect(listItems.length).to.equal(
annotationsJson.length + SEARCHABLE_TOP_LEVEL_FILE_ANNOTATIONS.length
);

const first = listItems[0]; // items are sorted according to Annotation::sort
// items are sorted according to Annotation::sort but file properties go first
const first = listItems[SEARCHABLE_TOP_LEVEL_FILE_ANNOTATIONS.length];
expect(first).to.have.property("id");
expect(first).to.have.property("description", "AICS cell line");
expect(first).to.have.property("title", "Cell line");
Expand All @@ -31,17 +35,23 @@ describe("<AnnotationSidebar /> selectors", () => {
new FileFilter("Cell Line", "AICS-0"),
new FileFilter("Date Created", "01/10/15"),
];
const state = mergeState(initialState, {
metadata: {
annotations: map(annotationsJson, (annotation) => new Annotation(annotation)),
},
const filteredState = {
...initialState,
selection: {
...initialState.selection,
filters,
},
};
const state = mergeState(filteredState, {
metadata: {
annotations: map(annotationsJson, (annotation) => new Annotation(annotation)),
},
});

const listItems = annotationSelectors.getAnnotationListItems(state);
expect(listItems.length).to.equal(annotationsJson.length);
expect(listItems.length).to.equal(
annotationsJson.length + SEARCHABLE_TOP_LEVEL_FILE_ANNOTATIONS.length
);

listItems.forEach((item) => {
const filtered = filters.findIndex((f) => f.name === item.id) !== -1;
Expand All @@ -64,7 +74,9 @@ describe("<AnnotationSidebar /> selectors", () => {
});

const listItems = annotationSelectors.getAnnotationListItems(state);
expect(listItems.length).to.equal(annotationsJson.length);
expect(listItems.length).to.equal(
annotationsJson.length + SEARCHABLE_TOP_LEVEL_FILE_ANNOTATIONS.length
);

listItems.forEach((item) => {
const disabled = !availableAnnotationsForHierarchySet.has(item.id);
Expand All @@ -85,7 +97,9 @@ describe("<AnnotationSidebar /> selectors", () => {
});

const listItems = annotationSelectors.getAnnotationListItems(state);
expect(listItems.length).to.equal(annotationsJson.length);
expect(listItems.length).to.equal(
annotationsJson.length + SEARCHABLE_TOP_LEVEL_FILE_ANNOTATIONS.length
);

listItems.forEach((item) => {
expect(item).to.have.property("disabled", false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.filter-input, .date-range-box {
border: none;
flex: auto;
}

.date-range-box {
display: flex;
}

.date-range-box {
max-height: 32px;
overflow: hidden;
}

.date-range-separator {
display: flex;
font-size: small;
padding: 0 2px;
}

.date-range-separator i {
margin: auto;
}
Loading

0 comments on commit e343558

Please sign in to comment.