diff --git a/packages/core/components/FileDetails/FileDetails.module.css b/packages/core/components/FileDetails/FileDetails.module.css
index 85c245e8c..f891a330e 100644
--- a/packages/core/components/FileDetails/FileDetails.module.css
+++ b/packages/core/components/FileDetails/FileDetails.module.css
@@ -114,7 +114,7 @@
}
.font-size-button {
- background-color: darkgrey;
+ background-color: lightgrey;
border-radius: 0;
font-size: 10px;
font-weight: normal;
diff --git a/packages/core/components/FileDetails/index.tsx b/packages/core/components/FileDetails/index.tsx
index e4d0ac0bb..aa355c09c 100644
--- a/packages/core/components/FileDetails/index.tsx
+++ b/packages/core/components/FileDetails/index.tsx
@@ -1,4 +1,4 @@
-import { ActionButton, IButtonStyles } from "@fluentui/react";
+import { ActionButton, IButtonStyles, Icon } from "@fluentui/react";
import classNames from "classnames";
import * as React from "react";
import { useDispatch, useSelector } from "react-redux";
@@ -15,6 +15,7 @@ import { ROOT_ELEMENT_ID } from "../../App";
import { selection } from "../../state";
import SvgIcon from "../../components/SvgIcon";
import { NO_IMAGE_ICON_PATH_DATA } from "../../icons";
+import { RENDERABLE_IMAGE_FORMATS, THUMBNAIL_SIZE_TO_NUM_COLUMNS } from "../../constants";
import styles from "./FileDetails.module.css";
@@ -126,7 +127,11 @@ export default function FileDetails(props: FileDetails) {
const globalDispatch = useDispatch();
const [windowState, windowDispatch] = React.useReducer(windowStateReducer, INITIAL_STATE);
const [fileDetails, isLoading] = useFileDetails();
+ const fileGridColumnCount = useSelector(selection.selectors.getFileGridColumnCount);
const shouldDisplaySmallFont = useSelector(selection.selectors.getShouldDisplaySmallFont);
+ const shouldDisplayThumbnailView = useSelector(
+ selection.selectors.getShouldDisplayThumbnailView
+ );
// If FileDetails pane is minimized, set its width to the width of the WindowActionButtons. Else, let it be
// defined by whatever the CSS determines (setting an inline style to undefined will prompt ReactDOM to not apply
@@ -164,8 +169,7 @@ export default function FileDetails(props: FileDetails) {
);
} else if (fileDetails) {
- const renderableImageFormats = [".jpg", ".jpeg", ".png", ".gif"];
- const isFileRenderableImage = renderableImageFormats.some((format) =>
+ const isFileRenderableImage = RENDERABLE_IMAGE_FORMATS.some((format) =>
fileDetails?.name.toLowerCase().endsWith(format)
);
if (isFileRenderableImage) {
@@ -226,6 +230,64 @@ export default function FileDetails(props: FileDetails) {
[styles.hidden]: windowState.state === WindowState.MINIMIZED,
})}
/>
+ {
+ globalDispatch(selection.actions.setFileThumbnailView(true));
+ globalDispatch(
+ selection.actions.setFileGridColumnCount(
+ THUMBNAIL_SIZE_TO_NUM_COLUMNS.LARGE
+ )
+ );
+ }}
+ title="Large thumbnail view"
+ >
+
+
+ {
+ globalDispatch(selection.actions.setFileThumbnailView(true));
+ globalDispatch(
+ selection.actions.setFileGridColumnCount(
+ THUMBNAIL_SIZE_TO_NUM_COLUMNS.SMALL
+ )
+ );
+ }}
+ title="Small thumbnail view"
+ >
+
+
+
+ globalDispatch(
+ selection.actions.setFileThumbnailView(!shouldDisplayThumbnailView)
+ )
+ }
+ title="List view"
+ >
+
+
void;
+ onSelect: OnSelect;
+}
+
+interface LazilyRenderedThumbnailProps {
+ columnIndex: number; // injected by react-window
+ data: LazilyRenderedThumbnailContext; // injected by react-window
+ rowIndex: number; // injected by react-window
+ style: React.CSSProperties; // injected by react-window
+}
+
+const MARGIN = 20; // px;
+
+/**
+ * A single file in the listing of available files FMS.
+ * Follows the pattern set by LazilyRenderedRow
+ */
+export default function LazilyRenderedThumbnail(props: LazilyRenderedThumbnailProps) {
+ const {
+ data: { fileSet, itemCount, measuredWidth, onContextMenu, onSelect },
+ columnIndex,
+ rowIndex,
+ style,
+ } = props;
+
+ const shouldDisplaySmallFont = useSelector(selection.selectors.getShouldDisplaySmallFont);
+ const fileSelection = useSelector(selection.selectors.getFileSelection);
+ const fileGridColCount = useSelector(selection.selectors.getFileGridColumnCount);
+ const overallIndex = fileGridColCount * rowIndex + columnIndex;
+ const file = fileSet.getFileByIndex(overallIndex);
+ const thumbnailSize = measuredWidth / fileGridColCount - 2 * MARGIN;
+
+ const isSelected = React.useMemo(() => {
+ return fileSelection.isSelected(fileSet, overallIndex);
+ }, [fileSelection, fileSet, overallIndex]);
+
+ const isFocused = React.useMemo(() => {
+ return fileSelection.isFocused(fileSet, overallIndex);
+ }, [fileSelection, fileSet, overallIndex]);
+
+ const onClick = (evt: React.MouseEvent) => {
+ evt.preventDefault();
+ evt.stopPropagation();
+
+ if (onSelect && file !== undefined) {
+ onSelect(
+ { index: overallIndex, id: file.file_id },
+ {
+ // Details on different OS keybindings
+ // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent#Properties
+ ctrlKeyIsPressed: evt.ctrlKey || evt.metaKey,
+ shiftKeyIsPressed: evt.shiftKey,
+ }
+ );
+ }
+ };
+
+ // Display the start of the file name and at least part of the file type
+ const clipFileName = (filename: string) => {
+ if (fileGridColCount === THUMBNAIL_SIZE_TO_NUM_COLUMNS.SMALL && filename.length > 15) {
+ return filename.slice(0, 6) + "..." + filename.slice(-4);
+ } else if (filename.length > 20) {
+ return filename.slice(0, 9) + "..." + filename.slice(-8);
+ }
+ return filename;
+ };
+
+ // If the file has a thumbnail image specified, we want to display the specified thumbnail.
+ // Otherwise, we want to display the file itself as the thumbnail if possible.
+ // If there is no thumbnail and the file cannot be displayed as the thumbnail, show a no image icon
+ // TODO: Add custom icons per file type
+ let thumbnail = (
+
+ );
+ if (file?.thumbnail) {
+ // thumbnail exists
+ thumbnail = (
+
+
+
+ );
+ } else if (file) {
+ const isFileRenderableImage = RENDERABLE_IMAGE_FORMATS.some((format) =>
+ file?.file_name.toLowerCase().endsWith(format)
+ );
+ if (isFileRenderableImage) {
+ // render the image as the thumbnail
+ thumbnail = (
+
+
+
+ );
+ }
+ }
+
+ let content;
+ if (file) {
+ const filenameForRender = clipFileName(file?.file_name);
+ content = (
+
+ {thumbnail}
+
+ {filenameForRender}
+
+
+ );
+ } else if (overallIndex < itemCount) {
+ // Grid will attempt to render a cell even if we're past the total index
+ content = "Loading...";
+ } // No `else` since if past total index we stil want empty content to fill up the outer grid
+
+ return (
+
+ {content}
+
+ );
+}
diff --git a/packages/core/components/FileList/index.tsx b/packages/core/components/FileList/index.tsx
index 6bb07d8b5..b82b31cc4 100644
--- a/packages/core/components/FileList/index.tsx
+++ b/packages/core/components/FileList/index.tsx
@@ -3,12 +3,13 @@ import debouncePromise from "debounce-promise";
import { defaults, isFunction } from "lodash";
import * as React from "react";
import { useSelector } from "react-redux";
-import { FixedSizeList } from "react-window";
+import { FixedSizeGrid, FixedSizeList } from "react-window";
import InfiniteLoader from "react-window-infinite-loader";
import FileSet from "../../entity/FileSet";
import Header from "./Header";
import LazilyRenderedRow from "./LazilyRenderedRow";
+import LazilyRenderedThumbnail from "./LazilyRenderedThumbnail";
import { selection } from "../../state";
import useLayoutMeasurements from "../../hooks/useLayoutMeasurements";
import useFileSelector from "./useFileSelector";
@@ -36,6 +37,8 @@ const DEFAULTS = {
};
const MAX_NON_ROOT_HEIGHT = 300;
+const SMALL_ROW_HEIGHT = 18;
+const TALL_ROW_HEIGHT = 22;
/**
* Wrapper for react-window-infinite-loader and react-window that knows how to lazily fetch its own data. It will lay
@@ -45,8 +48,17 @@ export default function FileList(props: FileListProps) {
const [totalCount, setTotalCount] = React.useState(null);
const fileSelection = useSelector(selection.selectors.getFileSelection);
const isDisplayingSmallFont = useSelector(selection.selectors.getShouldDisplaySmallFont);
+ const shouldDisplayThumbnailView = useSelector(
+ selection.selectors.getShouldDisplayThumbnailView
+ );
+ const fileGridColumnCount = useSelector(selection.selectors.getFileGridColumnCount);
+ const [measuredNodeRef, measuredHeight, measuredWidth] = useLayoutMeasurements<
+ HTMLDivElement
+ >();
+ let defaultRowHeight = isDisplayingSmallFont ? SMALL_ROW_HEIGHT : TALL_ROW_HEIGHT;
+ if (shouldDisplayThumbnailView) defaultRowHeight = measuredWidth / fileGridColumnCount;
const { className, fileSet, isRoot, rowHeight, sortOrder } = defaults({}, props, DEFAULTS, {
- rowHeight: isDisplayingSmallFont ? 18 : 22,
+ rowHeight: defaultRowHeight,
});
const onSelect = useFileSelector(fileSet, sortOrder);
@@ -58,24 +70,29 @@ export default function FileList(props: FileListProps) {
// 100% of the height of its container.
// Otherwise, the height of the list should reflect the number of items it has to render, up to
// a certain maximum.
- const [measuredNodeRef, measuredHeight, measuredWidth] = useLayoutMeasurements<
- HTMLDivElement
- >();
const dataDrivenHeight = rowHeight * (totalCount || DEFAULT_TOTAL_COUNT) + 3 * rowHeight; // adding three additional rowHeights leaves room for the header + horz. scroll bar
const calculatedHeight = Math.min(MAX_NON_ROOT_HEIGHT, dataDrivenHeight);
const height = isRoot ? measuredHeight : calculatedHeight;
const listRef = React.useRef(null);
+ const gridRef = React.useRef(null);
const outerRef = React.useRef(null);
// This hook is responsible for ensuring that if the details pane is currently showing a file row
// within this FileList the file row shown in the details pane is scrolled into view.
React.useEffect(() => {
- if (listRef.current && outerRef.current && fileSelection.isFocused(fileSet)) {
+ if (
+ (listRef.current || gridRef.current) &&
+ outerRef.current &&
+ fileSelection.isFocused(fileSet)
+ ) {
const { indexWithinFileSet } = fileSelection.getFocusedItemIndices();
if (indexWithinFileSet !== undefined) {
const listScrollTop = outerRef.current.scrollTop;
- const focusedItemTop = indexWithinFileSet * rowHeight;
+ let focusedItemTop = indexWithinFileSet * rowHeight;
+ if (gridRef.current) {
+ focusedItemTop = (indexWithinFileSet / fileGridColumnCount) * rowHeight;
+ }
const focusedItemBottom = focusedItemTop + rowHeight;
const headerHeight = 40; // px; defined in Header.module.css; stickily sits on top of the list
const visibleArea = height - headerHeight;
@@ -88,11 +105,17 @@ export default function FileList(props: FileListProps) {
const centeredWithinVisibleArea = Math.floor(
centerOfFocusedItem - visibleArea / 2
);
- listRef.current.scrollTo(Math.max(0, centeredWithinVisibleArea));
+ if (listRef.current) {
+ listRef.current.scrollTo(Math.max(0, centeredWithinVisibleArea));
+ } else if (gridRef.current) {
+ gridRef.current.scrollTo({
+ scrollTop: Math.max(0, centeredWithinVisibleArea),
+ });
+ }
}
}
}
- }, [fileSelection, fileSet, height, rowHeight]);
+ }, [fileSelection, fileSet, height, fileGridColumnCount, rowHeight]);
// Get a count of all files in the FileList, but don't wait on it
React.useEffect(() => {
@@ -126,7 +149,7 @@ export default function FileList(props: FileListProps) {
itemCount={totalCount || DEFAULT_TOTAL_COUNT}
>
{({ onItemsRendered, ref: innerRef }) => {
- const callbackRef = (instance: FixedSizeList | null) => {
+ const callbackRefList = (instance: FixedSizeList | null) => {
listRef.current = instance;
// react-window-infinite-loader takes a reference to the List component instance:
@@ -135,8 +158,69 @@ export default function FileList(props: FileListProps) {
innerRef(instance);
}
};
+ const callbackRefGrid = (instance: FixedSizeGrid | null) => {
+ gridRef.current = instance;
+ if (isFunction(innerRef)) {
+ innerRef(instance);
+ }
+ };
+ // Custom onItemsRendered for grids
+ // The built-in onItemsRendered from InfiniteLoader only supports lists
+ const onGridItemsRendered = (gridData: any) => {
+ const {
+ visibleRowStartIndex,
+ visibleRowStopIndex,
+ visibleColumnStopIndex,
+ overscanRowStartIndex,
+ overscanRowStopIndex,
+ overscanColumnStopIndex,
+ } = gridData;
+
+ // Convert injected grid props to InfiniteLoader list props
+ const visibleStartIndex =
+ visibleRowStartIndex * (visibleColumnStopIndex + 1);
+ const visibleStopIndex =
+ visibleRowStopIndex * (visibleColumnStopIndex + 1);
+ const overscanStartIndex =
+ overscanRowStartIndex * (overscanColumnStopIndex + 1);
+ const overscanStopIndex =
+ overscanRowStopIndex * (overscanColumnStopIndex + 1);
- return (
+ onItemsRendered({
+ // call onItemsRendered from InfiniteLoader
+ visibleStartIndex,
+ visibleStopIndex,
+ overscanStartIndex,
+ overscanStopIndex,
+ });
+ };
+
+ const fixedSizeGrid = (
+
+ {LazilyRenderedThumbnail}
+
+ );
+
+ const fixedSizeList = (
{LazilyRenderedRow}
);
+
+ return shouldDisplayThumbnailView ? fixedSizeGrid : fixedSizeList;
}}
);
diff --git a/packages/core/components/FileList/test/LazilyRenderedThumbnail.test.tsx b/packages/core/components/FileList/test/LazilyRenderedThumbnail.test.tsx
new file mode 100644
index 000000000..e65917d40
--- /dev/null
+++ b/packages/core/components/FileList/test/LazilyRenderedThumbnail.test.tsx
@@ -0,0 +1,202 @@
+import { configureMockStore, mergeState } from "@aics/redux-utils";
+import { render } from "@testing-library/react";
+import { expect } from "chai";
+import * as React from "react";
+import { Provider } from "react-redux";
+import * as sinon from "sinon";
+
+import LazilyRenderedThumbnail from "../LazilyRenderedThumbnail";
+import { initialState } from "../../../state";
+import FileSet from "../../../entity/FileSet";
+
+describe("", () => {
+ function makeItemData() {
+ const fileSet = new FileSet();
+ sinon.stub(fileSet, "getFileByIndex").callsFake((index) => {
+ if (index === 0) {
+ return {
+ annotations: [],
+ file_id: "abc1230",
+ file_name: "my_image0.czi",
+ file_path: "some/path/to/my_image0.czi",
+ file_size: 1,
+ thumbnail: "some/path/to/my_image0.jpg",
+ uploaded: new Date().toISOString(),
+ };
+ }
+ if (index === 9) {
+ return {
+ annotations: [],
+ file_id: "abc1239",
+ file_name: "my_image9.jpg",
+ file_path: "some/path/to/my_image9.jpg",
+ file_size: 1,
+ thumbnail: "",
+ uploaded: new Date().toISOString(),
+ };
+ }
+ if (index === 25) {
+ return {
+ annotations: [],
+ file_id: "abc12325",
+ file_name: "my_image25.czi",
+ file_path: "some/path/to/my_image25.czi",
+ file_size: 1,
+ thumbnail: "",
+ uploaded: new Date().toISOString(),
+ };
+ }
+ });
+
+ return {
+ fileSet,
+ measuredWidth: 600,
+ itemCount: 100,
+ onContextMenu: sinon.spy(),
+ onSelect: sinon.spy(),
+ };
+ }
+
+ it("renders thumbnail when file has one specified", () => {
+ // Arrange
+ const state = mergeState(initialState, {});
+ const { store } = configureMockStore({ state });
+
+ // Act
+ const { getByText, getByRole } = render(
+
+
+
+ );
+
+ // Assert
+ // Also checking for proper row/col indexing
+ const thumbnail = getByRole("img");
+ expect(thumbnail.getAttribute("src")).to.include("some/path/to/my_image0.jpg");
+ expect(getByText("my_image0.czi")).to.not.equal(null);
+ });
+
+ it("renders file as thumbnail if file is renderable type", () => {
+ // Arrange
+ const { store } = configureMockStore({ state: initialState });
+
+ // Act
+ const { getByText, getByRole } = render(
+
+
+
+ );
+
+ // Assert
+ // Also confirms proper row/col indexing
+ const thumbnail = getByRole("img");
+ expect(thumbnail.getAttribute("src")).to.include("some/path/to/my_image9.jpg");
+ expect(getByText("my_image9.jpg")).to.not.equal(null);
+ });
+
+ it("renders svg as thumbnail if file has no renderable thumbnail", () => {
+ // Arrange
+ const { store } = configureMockStore({ state: initialState });
+
+ // Act
+ const { getByText, queryByRole } = render(
+
+
+
+ );
+
+ // Assert
+ // Also confirms proper row/col indexing
+ expect(".no-thumbnail").to.exist;
+ expect(".svg").to.exist;
+ expect(queryByRole("img")).not.to.exist;
+ expect(getByText("my_image25.czi")).to.not.equal(null);
+ });
+
+ it("renders a loading indicator when data is not available", () => {
+ // Arrange
+ const { store } = configureMockStore({ state: initialState });
+
+ // Act
+ const { queryByText } = render(
+
+
+
+ );
+
+ // Assert
+ expect(queryByText("my_image")).to.equal(null);
+ expect(queryByText("Loading...")).to.not.equal(null);
+ });
+
+ // We want to be able to render empty cells past the total item count in order to fill the grid
+ it("renders an empty cell if the index is past the total item count", () => {
+ // Arrange
+ const { store } = configureMockStore({ state: initialState });
+
+ // Act
+ const { queryByText } = render(
+
+
+
+ );
+
+ // Assert
+ expect(queryByText("my_image")).to.equal(null);
+ expect(queryByText("Loading...")).to.equal(null);
+ });
+
+ it("renders and indexes correctly with different number of columns", () => {
+ // Arrange
+ const state = {
+ ...initialState,
+ selection: {
+ ...initialState.selection,
+ fileGridColumnCount: 10,
+ },
+ };
+ const { store } = configureMockStore({ state });
+
+ // Act
+ const { getByText } = render(
+
+
+
+ );
+
+ // Assert
+ expect(".no-thumbnail").to.exist;
+ expect(".svg").to.exist;
+ expect(getByText("my_image25.czi")).to.not.equal(null);
+ });
+});
diff --git a/packages/core/constants/index.ts b/packages/core/constants/index.ts
index 40fb3bdaf..49929ea83 100644
--- a/packages/core/constants/index.ts
+++ b/packages/core/constants/index.ts
@@ -153,3 +153,10 @@ export const SEARCHABLE_TOP_LEVEL_FILE_ANNOTATIONS = TOP_LEVEL_FILE_ANNOTATIONS.
);
export const TOP_LEVEL_FILE_ANNOTATION_NAMES = TOP_LEVEL_FILE_ANNOTATIONS.map((a) => a.name);
+
+export const THUMBNAIL_SIZE_TO_NUM_COLUMNS = {
+ LARGE: 5,
+ SMALL: 10,
+};
+
+export const RENDERABLE_IMAGE_FORMATS = [".jpg", ".jpeg", ".png", ".gif"];
diff --git a/packages/core/state/selection/actions.ts b/packages/core/state/selection/actions.ts
index 24dbdef3c..1df59e240 100644
--- a/packages/core/state/selection/actions.ts
+++ b/packages/core/state/selection/actions.ts
@@ -546,3 +546,42 @@ export function adjustGlobalFontSize(shouldDisplaySmallFont: boolean): AdjustGlo
type: ADJUST_GLOBAL_FONT_SIZE,
};
}
+
+/**
+ * SET_FILE_VIEW_TYPE
+ *
+ * Intention to set the file view type to thumbnail or list
+ */
+export const SET_FILE_THUMBNAIL_VIEW = makeConstant(STATE_BRANCH_NAME, "set-file-thumbnail-view");
+
+export interface SetFileThumbnailView {
+ payload: boolean;
+ type: string;
+}
+
+export function setFileThumbnailView(shouldDisplayThumbnailView: boolean): SetFileThumbnailView {
+ return {
+ payload: shouldDisplayThumbnailView,
+ type: SET_FILE_THUMBNAIL_VIEW,
+ };
+}
+
+/**
+ * SET_FILE_GRID_COLUMN_COUNT
+ */
+export const SET_FILE_GRID_COLUMN_COUNT = makeConstant(
+ STATE_BRANCH_NAME,
+ "set-file-grid-column-count"
+);
+
+export interface SetFileGridColumnCount {
+ payload: number;
+ type: string;
+}
+
+export function setFileGridColumnCount(fileGridColumnCount: number): SetFileGridColumnCount {
+ return {
+ payload: fileGridColumnCount,
+ type: SET_FILE_GRID_COLUMN_COUNT,
+ };
+}
diff --git a/packages/core/state/selection/reducer.ts b/packages/core/state/selection/reducer.ts
index 7ff01b00e..4de8658d6 100644
--- a/packages/core/state/selection/reducer.ts
+++ b/packages/core/state/selection/reducer.ts
@@ -2,7 +2,7 @@ import { makeReducer } from "@aics/redux-utils";
import { castArray, difference, omit } from "lodash";
import interaction from "../interaction";
-import { AnnotationName, PAST_YEAR_FILTER } from "../../constants";
+import { AnnotationName, PAST_YEAR_FILTER, THUMBNAIL_SIZE_TO_NUM_COLUMNS } from "../../constants";
import Annotation from "../../entity/Annotation";
import FileFilter from "../../entity/FileFilter";
import FileFolder from "../../entity/FileFolder";
@@ -24,6 +24,8 @@ import {
CHANGE_VIEW,
SELECT_TUTORIAL,
ADJUST_GLOBAL_FONT_SIZE,
+ SET_FILE_THUMBNAIL_VIEW,
+ SET_FILE_GRID_COLUMN_COUNT,
} from "./actions";
import FileSort, { SortOrder } from "../../entity/FileSort";
import Tutorial from "../../entity/Tutorial";
@@ -38,10 +40,12 @@ export interface SelectionStateBranch {
[index: string]: number; // columnName to widthPercent mapping
};
displayAnnotations: Annotation[];
+ fileGridColumnCount: number;
fileSelection: FileSelection;
filters: FileFilter[];
openFileFolders: FileFolder[];
shouldDisplaySmallFont: boolean;
+ shouldDisplayThumbnailView: boolean;
sortColumn?: FileSort;
tutorial?: Tutorial;
}
@@ -57,10 +61,12 @@ export const initialState = {
[AnnotationName.FILE_SIZE]: 0.15,
},
displayAnnotations: [],
+ fileGridColumnCount: THUMBNAIL_SIZE_TO_NUM_COLUMNS.LARGE,
fileSelection: new FileSelection(),
filters: [PAST_YEAR_FILTER],
openFileFolders: [],
shouldDisplaySmallFont: false,
+ shouldDisplayThumbnailView: false,
sortColumn: new FileSort(AnnotationName.UPLOADED, SortOrder.DESC),
};
@@ -74,6 +80,14 @@ export default makeReducer(
...state,
shouldDisplaySmallFont: action.payload,
}),
+ [SET_FILE_THUMBNAIL_VIEW]: (state, action) => ({
+ ...state,
+ shouldDisplayThumbnailView: action.payload,
+ }),
+ [SET_FILE_GRID_COLUMN_COUNT]: (state, action) => ({
+ ...state,
+ fileGridColumnCount: action.payload,
+ }),
[SET_FILE_FILTERS]: (state, action) => ({
...state,
filters: action.payload,
diff --git a/packages/core/state/selection/selectors.ts b/packages/core/state/selection/selectors.ts
index 0cc560a85..e6b4ec88a 100644
--- a/packages/core/state/selection/selectors.ts
+++ b/packages/core/state/selection/selectors.ts
@@ -19,11 +19,14 @@ export const getAvailableAnnotationsForHierarchy = (state: State) =>
export const getAvailableAnnotationsForHierarchyLoading = (state: State) =>
state.selection.availableAnnotationsForHierarchyLoading;
export const getColumnWidths = (state: State) => state.selection.columnWidths;
+export const getFileGridColumnCount = (state: State) => state.selection.fileGridColumnCount;
export const getFileFilters = (state: State) => state.selection.filters;
export const getCollection = (state: State) => state.selection.collection;
export const getFileSelection = (state: State) => state.selection.fileSelection;
export const getOpenFileFolders = (state: State) => state.selection.openFileFolders;
export const getShouldDisplaySmallFont = (state: State) => state.selection.shouldDisplaySmallFont;
+export const getShouldDisplayThumbnailView = (state: State) =>
+ state.selection.shouldDisplayThumbnailView;
export const getSortColumn = (state: State) => state.selection.sortColumn;
export const getTutorial = (state: State) => state.selection.tutorial;