Skip to content

Commit

Permalink
Merge pull request #110 from AllenInstitute/feature/annotation-persis…
Browse files Browse the repository at this point in the history
…tance

Feature/annotation persistance
  • Loading branch information
BrianWhitneyAI authored May 22, 2024
2 parents ab23c6b + 5ee02de commit 73e0bf7
Show file tree
Hide file tree
Showing 13 changed files with 112 additions and 35 deletions.
69 changes: 51 additions & 18 deletions packages/core/components/AnnotationPicker/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from "react";
import { uniqBy } from "lodash";
import { useSelector } from "react-redux";

import ListPicker from "../ListPicker";
Expand Down Expand Up @@ -27,31 +28,63 @@ interface Props {
* downloading a manifest.
*/
export default function AnnotationPicker(props: Props) {
const annotations = useSelector(metadata.selectors.getSortedAnnotations);
const annotations = useSelector(metadata.selectors.getSortedAnnotations).filter(
(annotation) =>
!props.disabledTopLevelAnnotations ||
!TOP_LEVEL_FILE_ANNOTATION_NAMES.includes(annotation.name)
);
const unavailableAnnotations = useSelector(
selection.selectors.getUnavailableAnnotationsForHierarchy
);
const areAvailableAnnotationLoading = useSelector(
selection.selectors.getAvailableAnnotationsForHierarchyLoading
);

const items = annotations
.filter(
(annotation) =>
!props.disabledTopLevelAnnotations ||
!TOP_LEVEL_FILE_ANNOTATION_NAMES.includes(annotation.name)
)
.map((annotation) => ({
selected: props.selections.some((selected) => selected.name === annotation.name),
disabled:
!props.enableAllAnnotations &&
unavailableAnnotations.some((unavailable) => unavailable.name === annotation.name),
loading: !props.enableAllAnnotations && areAvailableAnnotationLoading,
description: annotation.description,
data: annotation,
value: annotation.name,
displayValue: annotation.displayName,
}));
const recentAnnotationNames = useSelector(selection.selectors.getRecentAnnotations);
const recentAnnotations = recentAnnotationNames.flatMap((name) =>
annotations.filter((annotation) => annotation.name === name)
);

// Define buffer item
const bufferBar = {
selected: false,
disabled: false,
isBuffer: true,
value: "recent buffer",
displayValue: "",
};

// combine all annotation lists and buffer item objects
const rawItems = [...recentAnnotations, bufferBar, ...annotations];

const items = uniqBy(
rawItems.flatMap((annotation) => {
if (annotation instanceof Annotation) {
return {
selected: props.selections.some(
(selected) => selected.name === annotation.name
),
disabled:
!props.enableAllAnnotations &&
unavailableAnnotations.some(
(unavailable) => unavailable.name === annotation.name
),
recent:
recentAnnotationNames.includes(annotation.name) &&
!props.selections.some((selected) => selected.name === annotation.name),
loading: !props.enableAllAnnotations && areAvailableAnnotationLoading,
description: annotation.description,
data: annotation,
value: annotation.name,
displayValue: annotation.displayName,
};
} else {
// This is reached if the 'annotation' is a spacer.
return annotation;
}
}),
"value"
);

const removeSelection = (item: ListItem<Annotation>) => {
props.setSelections(
Expand Down
7 changes: 7 additions & 0 deletions packages/core/components/ListPicker/ListRow.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@
.selected {
background-color: var(--primary-background-color);
color: var(--primary-text-color);
margin: calc(var(--spacing) / 4) 0
}

.isBuffer {
background-color: var(--secondary-text-color);
height: 3px;
pointer-events: none;
}

.item-container, .item-container label {
Expand Down
6 changes: 5 additions & 1 deletion packages/core/components/ListPicker/ListRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import styles from "./ListRow.module.css";
export interface ListItem<T = any> {
disabled?: boolean;
loading?: boolean;
recent?: boolean;
isBuffer?: boolean;
selected: boolean;
displayValue: AnnotationValue;
value: AnnotationValue;
Expand Down Expand Up @@ -37,9 +39,10 @@ export default function ListRow(props: Props) {
className={classNames(styles.itemContainer, {
[styles.selected]: item.selected,
[styles.disabled]: item.disabled,
[styles.isBuffer]: item.isBuffer,
})}
menuIconProps={{
iconName: props.subMenuRenderer ? "ChevronRight" : undefined,
iconName: props.subMenuRenderer && !item.isBuffer ? "ChevronRight" : undefined,
}}
menuProps={
props.subMenuRenderer
Expand All @@ -59,6 +62,7 @@ export default function ListRow(props: Props) {
<div>{item.selected && <Icon iconName="CheckMark" />}</div>
{item.displayValue}
</label>
{item.recent && <Icon iconName="Redo" />}
{item.loading && <Spinner className={styles.spinner} size={SpinnerSize.small} />}
</DefaultButton>
);
Expand Down
3 changes: 2 additions & 1 deletion packages/core/components/ListPicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ export default function ListPicker(props: ListPickerProps) {
</div>
<div className={styles.footer}>
<h6>
Displaying {filteredItems.length} of {items.length} Options
{/* (item.length -1) to account for buffer in item list. */}
Displaying {filteredItems.length - 1} of {items.length - 1} Options
</h6>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,6 @@ describe("<ListPicker />", () => {
);

// Act / Assert
expect(getByText(`Displaying ${items.length} of ${items.length} Options`)).to.exist;
expect(getByText(`Displaying ${items.length - 1} of ${items.length - 1} Options`)).to.exist;
});
});
2 changes: 2 additions & 0 deletions packages/core/services/PersistentConfigService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum PersistedConfigKeys {
HasUsedApplicationBefore = "HAS_USED_APPLICATION_BEFORE",
UserSelectedApplications = "USER_SELECTED_APPLICATIONS",
Queries = "QUERIES",
RecentAnnotations = "RECENT_ANNOTATIONS",
}

export interface UserSelectedApplication {
Expand All @@ -26,6 +27,7 @@ export interface PersistedConfig {
[PersistedConfigKeys.ImageJExecutable]?: string; // Deprecated
[PersistedConfigKeys.HasUsedApplicationBefore]?: boolean;
[PersistedConfigKeys.Queries]?: Query[];
[PersistedConfigKeys.RecentAnnotations]?: string[];
[PersistedConfigKeys.UserSelectedApplications]?: UserSelectedApplication[];
}

Expand Down
4 changes: 4 additions & 0 deletions packages/core/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export function createReduxStore(options: CreateStoreOptions = {}) {
const displayAnnotations = rawDisplayAnnotations
? rawDisplayAnnotations.map((annotation) => new Annotation(annotation))
: [];
const recentAnnotations = persistedConfig?.[PersistedConfigKeys.RecentAnnotations]?.length
? persistedConfig?.[PersistedConfigKeys.RecentAnnotations]
: [];
const preloadedState: State = mergeState(initialState, {
interaction: {
isOnWeb: !!options.isOnWeb,
Expand Down Expand Up @@ -107,6 +110,7 @@ export function createReduxStore(options: CreateStoreOptions = {}) {
),
},
})),
recentAnnotations,
},
});
return configureStore<State>({
Expand Down
19 changes: 16 additions & 3 deletions packages/core/state/selection/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { makeReducer } from "@aics/redux-utils";
import { omit } from "lodash";
import { castArray, omit, uniq } from "lodash";

import interaction from "../interaction";
import { THUMBNAIL_SIZE_TO_NUM_COLUMNS } from "../../constants";
Expand Down Expand Up @@ -32,6 +32,8 @@ import {
SET_FILE_GRID_COLUMN_COUNT,
REMOVE_QUERY,
RemoveQuery,
SetSortColumnAction,
SetFileFiltersAction,
} from "./actions";
import FileSort, { SortOrder } from "../../entity/FileSort";
import Tutorial from "../../entity/Tutorial";
Expand All @@ -51,6 +53,7 @@ export interface SelectionStateBranch {
filters: FileFilter[];
isDarkTheme: boolean;
openFileFolders: FileFolder[];
recentAnnotations: string[];
selectedQuery?: string;
shouldDisplaySmallFont: boolean;
shouldDisplayThumbnailView: boolean;
Expand All @@ -75,6 +78,7 @@ export const initialState = {
fileSelection: new FileSelection(),
filters: [],
openFileFolders: [],
recentAnnotations: [],
shouldDisplaySmallFont: false,
queries: [],
shouldDisplayThumbnailView: false,
Expand All @@ -98,9 +102,13 @@ export default makeReducer<SelectionStateBranch>(
...state,
fileGridColumnCount: action.payload,
}),
[SET_FILE_FILTERS]: (state, action) => ({
[SET_FILE_FILTERS]: (state, action: SetFileFiltersAction) => ({
...state,
filters: action.payload,
recentAnnotations: uniq([
...action.payload.map((filter) => filter.name),
...state.recentAnnotations,
]).slice(0, 5),

// Reset file selections when file filters change
fileSelection: new FileSelection(),
Expand Down Expand Up @@ -153,8 +161,12 @@ export default makeReducer<SelectionStateBranch>(
...state,
queries: action.payload,
}),
[SET_SORT_COLUMN]: (state, action) => ({
[SET_SORT_COLUMN]: (state, action: SetSortColumnAction) => ({
...state,
recentAnnotations: uniq([
...castArray(action.payload?.annotationName ?? []),
...state.recentAnnotations,
]).slice(0, 5),
sortColumn: action.payload,
}),
[interaction.actions.REFRESH]: (state) => ({
Expand Down Expand Up @@ -185,6 +197,7 @@ export default makeReducer<SelectionStateBranch>(
...state,
annotationHierarchy: action.payload,
availableAnnotationsForHierarchyLoading: true,
recentAnnotations: uniq([...action.payload, ...state.recentAnnotations]).slice(0, 5),

// Reset file selections when annotation hierarchy changes
fileSelection: new FileSelection(),
Expand Down
1 change: 1 addition & 0 deletions packages/core/state/selection/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const getFileFilters = (state: State) => state.selection.filters;
export const getFileSelection = (state: State) => state.selection.fileSelection;
export const getIsDarkTheme = (state: State) => state.selection.isDarkTheme;
export const getOpenFileFolders = (state: State) => state.selection.openFileFolders;
export const getRecentAnnotations = (state: State) => state.selection.recentAnnotations;
export const getSelectedQuery = (state: State) => state.selection.selectedQuery;
export const getShouldDisplaySmallFont = (state: State) => state.selection.shouldDisplaySmallFont;
export const getShouldDisplayThumbnailView = (state: State) =>
Expand Down
22 changes: 11 additions & 11 deletions packages/core/state/selection/test/reducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@ import { DataSource } from "../../../services/DataSourceService";

describe("Selection reducer", () => {
[
selection.actions.SET_ANNOTATION_HIERARCHY,
interaction.actions.SET_FILE_EXPLORER_SERVICE_BASE_URL,
].forEach((actionConstant) =>
{
actionConstant: selection.actions.SET_ANNOTATION_HIERARCHY,
expectedAction: selection.actions.setAnnotationHierarchy([]),
},
{
actionConstant: interaction.actions.SET_FILE_EXPLORER_SERVICE_BASE_URL,
expectedAction: interaction.actions.setFileExplorerServiceBaseUrl("base"),
},
].forEach(({ actionConstant, expectedAction }) =>
it(`clears selected file state when ${actionConstant} is fired`, () => {
// arrange
const prevSelection = new FileSelection().select({
Expand All @@ -29,20 +35,14 @@ describe("Selection reducer", () => {
...selection.initialState,
fileSelection: prevSelection,
};

const action = {
type: actionConstant,
};

// act
const nextSelectionState = selection.reducer(initialSelectionState, action);
const nextSelectionState = selection.reducer(initialSelectionState, expectedAction);
const nextSelection = selection.selectors.getFileSelection({
...initialState,
selection: nextSelectionState,
});

// assert
expect(prevSelection.count()).to.equal(3); // sanity-check
expect(prevSelection.count()).to.equal(3); // consistency check
expect(nextSelection.count()).to.equal(0);
})
);
Expand Down
2 changes: 2 additions & 0 deletions packages/desktop/src/renderer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ store.subscribe(() => {
const csvColumns = interaction.selectors.getCsvColumns(state);
const displayAnnotations = selection.selectors.getAnnotationsToDisplay(state);
const hasUsedApplicationBefore = interaction.selectors.hasUsedApplicationBefore(state);
const recentAnnotations = selection.selectors.getRecentAnnotations(state);
const userSelectedApplications = interaction.selectors.getUserSelectedApplications(state);

const appState = {
Expand All @@ -86,6 +87,7 @@ store.subscribe(() => {
})),
[PersistedConfigKeys.HasUsedApplicationBefore]: hasUsedApplicationBefore,
[PersistedConfigKeys.Queries]: queries,
[PersistedConfigKeys.RecentAnnotations]: recentAnnotations,
[PersistedConfigKeys.UserSelectedApplications]: userSelectedApplications,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ const OPTIONS: Options<Record<string, unknown>> = {
[PersistedConfigKeys.HasUsedApplicationBefore]: {
type: "boolean",
},
[PersistedConfigKeys.RecentAnnotations]: {
type: "array",
items: {
type: "string",
},
},
[PersistedConfigKeys.UserSelectedApplications]: {
type: "array",
items: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe(`${RUN_IN_RENDERER} PersistentConfigServiceElectron`, () => {
name: "ZEN",
},
];
const expectedRecentAnnotations = ["column"];
const expectedQueries = [
{
name: "foo",
Expand All @@ -71,6 +72,7 @@ describe(`${RUN_IN_RENDERER} PersistentConfigServiceElectron`, () => {
);
service.persist(PersistedConfigKeys.UserSelectedApplications, expectedUserSelectedApps);
service.persist(PersistedConfigKeys.DisplayAnnotations, expectedDisplayAnnotations);
service.persist(PersistedConfigKeys.RecentAnnotations, expectedRecentAnnotations);

const expectedConfig = {
[PersistedConfigKeys.AllenMountPoint]: expectedAllenMountPoint,
Expand All @@ -80,6 +82,7 @@ describe(`${RUN_IN_RENDERER} PersistentConfigServiceElectron`, () => {
[PersistedConfigKeys.HasUsedApplicationBefore]: expectedHasUsedApplicationBefore,
[PersistedConfigKeys.UserSelectedApplications]: expectedUserSelectedApps,
[PersistedConfigKeys.DisplayAnnotations]: expectedDisplayAnnotations,
[PersistedConfigKeys.RecentAnnotations]: expectedRecentAnnotations,
};

// Act
Expand All @@ -100,6 +103,7 @@ describe(`${RUN_IN_RENDERER} PersistentConfigServiceElectron`, () => {
[PersistedConfigKeys.ImageJExecutable]: "/my/imagej",
[PersistedConfigKeys.Queries]: [],
[PersistedConfigKeys.HasUsedApplicationBefore]: undefined,
[PersistedConfigKeys.RecentAnnotations]: ["column"],
[PersistedConfigKeys.UserSelectedApplications]: [
{
filePath: "/some/path/to/ImageJ",
Expand Down

0 comments on commit 73e0bf7

Please sign in to comment.