From 0afa4d54b38858e755f7a69c4fd2ae4bbe6687b6 Mon Sep 17 00:00:00 2001 From: Stepan Anokhin <38860530+stepan-anokhin@users.noreply.github.com> Date: Fri, 13 Nov 2020 22:59:21 +0700 Subject: [PATCH] Refactor ui state management (#185, #189) (#190) * Move helpers to separate modules * Move file cache to separate package * Move file matches state to separate package * Move cluster state to separate package * Move file-list state to separate package * Collect reusable prop-types in a public package * Extract entity fetching logic * Refactor file-cluster state Use generic approach to manage file-cluster state. * Refactor file-matches state Use generic approach to manage file-matches state. * Update server client * Update matches params (#189) * Fix file cluster update * Disable false-positive linting issue --- web/src/application/state/index.js | 4 +- web/src/application/state/initialState.js | 13 + web/src/application/state/reducers.js | 13 +- web/src/application/state/sagas.js | 2 +- .../CategorySelector/CategorySelector.js | 2 +- .../FileBrowserPage/CategorySelector/index.js | 2 +- .../FileBrowserActions/FileBrowserActions.js | 4 +- .../FileBrowserActions/SortSelector.js | 2 +- .../FileBrowserActions/ViewSelector.js | 2 +- .../FileBrowserPage/FileBrowserPage.js | 36 +- .../FileGridList/FileGridListItem.js | 2 +- .../FileLinearList/FileLinearListItem.js | 2 +- .../FilterPane/ContentFilters.js | 8 +- .../FilterPane/MetadataFilters.js | 8 +- .../FileBrowserPage/FilterPane/useFilters.js | 6 +- .../FileClusterPage/FileClusterPage.js | 2 +- .../FileDetails/FileDescriptionPane.js | 2 +- .../FileDetails/FileDetails.js | 2 +- .../MatchFiles/FileMatchHeader.js | 2 +- .../MatchFiles/MatchSelector.js | 2 +- .../MotherFile/FileDetailsHeader.js | 2 +- .../FileMatchesPage/FileMatchesPage.js | 23 +- .../MatchPreview/FileAttributes.js | 2 +- .../MatchPreview/MatchPreview.js | 2 +- .../components/FileSummary/CreationDate.js | 2 +- .../components/FileSummary/Duration.js | 2 +- .../components/FileSummary/FileSummary.js | 2 +- .../components/FileSummary/Fingerprint.js | 2 +- .../components/FileSummary/HasAudio.js | 2 +- .../components/FileSummary/HasExif.js | 2 +- .../collection/components/FileSummary/Name.js | 2 +- .../FileSummaryHeader/FileSummaryHeader.js | 2 +- .../components/MatchGraph/LinkTooltip.js | 2 +- .../components/MatchGraph/MatchGraph.js | 4 +- .../components/MatchGraph/NodeTooltip.js | 2 +- .../components/VideoDetailsPage/ExifPanel.js | 2 +- .../VideoDetailsPage/FileInfoPanel.js | 2 +- .../ObjectTimeLine/ObjectGroup.js | 2 +- .../ObjectTimeLine/ObjectGroupPopper.js | 2 +- .../ObjectTimeLine/ObjectPreview.js | 2 +- .../ObjectTimeLine/ObjectTimeLine.js | 2 +- .../ObjectsPanel/ObjectGroupListItem.js | 2 +- .../ObjectsPanel/ObjectsPanel.js | 2 +- .../VideoDetailsPage/SceneSelector/Scene.js | 2 +- .../SceneSelector/SceneSelector.js | 2 +- .../VideoDetailsPage/VideoInformation.js | 2 +- .../VideoDetailsPage/VideoInformationPane.js | 2 +- .../VideoDetailsPage/VideoPlayer.js | 2 +- .../VideoDetailsPage/VideoPlayerPane.js | 2 +- web/src/collection/hooks/useFile.js | 2 +- web/src/collection/hooks/useFileCluster.js | 76 ++-- web/src/collection/hooks/useFileMatches.js | 76 +--- .../FileType.js | 0 .../MatchType.js | 0 .../ObjectType.js | 0 .../SceneType.js | 0 web/src/collection/state/actions.js | 167 --------- .../state/fetchEntities/fetchEntitiesSaga.js | 61 ++++ .../state/fetchEntities/initialState.js | 54 +++ .../state/fetchEntities/makeEntityReducer.js | 59 +++ .../fetchEntities/makeFetchEntitiesHook.js | 78 ++++ web/src/collection/state/fileCache/actions.js | 20 + .../state/fileCache/initialState.js | 11 + web/src/collection/state/fileCache/reducer.js | 27 ++ .../collection/state/fileCluster/actions.js | 62 ++++ .../state/fileCluster/initialState.js | 23 ++ .../collection/state/fileCluster/reducer.js | 35 ++ web/src/collection/state/fileCluster/sagas.js | 33 ++ .../state/{ => fileList}/FileListType.js | 0 .../state/{ => fileList}/FileSort.js | 0 .../state/{ => fileList}/MatchCategory.js | 0 web/src/collection/state/fileList/actions.js | 49 +++ .../collection/state/fileList/initialState.js | 35 ++ web/src/collection/state/fileList/reducer.js | 70 ++++ web/src/collection/state/fileList/sagas.js | 67 ++++ .../collection/state/fileMatches/actions.js | 57 +++ .../state/fileMatches/initialState.js | 18 + .../collection/state/fileMatches/reducer.js | 19 + web/src/collection/state/fileMatches/sagas.js | 33 ++ .../state/helpers/extendEntityList.js | 22 ++ .../state/helpers/extendEntityMap.js | 11 + web/src/collection/state/index.js | 13 - web/src/collection/state/initialState.js | 29 ++ web/src/collection/state/reducers.js | 341 +----------------- web/src/collection/state/sagas.js | 196 +--------- web/src/collection/state/selectors.js | 12 +- web/src/server-api/Server/Server.js | 14 +- 87 files changed, 1061 insertions(+), 906 deletions(-) create mode 100644 web/src/application/state/initialState.js rename web/src/collection/{components/FileBrowserPage => prop-types}/FileType.js (100%) rename web/src/collection/{components/FileMatchesPage => prop-types}/MatchType.js (100%) rename web/src/collection/{components/VideoDetailsPage => prop-types}/ObjectType.js (100%) rename web/src/collection/{components/VideoDetailsPage => prop-types}/SceneType.js (100%) delete mode 100644 web/src/collection/state/actions.js create mode 100644 web/src/collection/state/fetchEntities/fetchEntitiesSaga.js create mode 100644 web/src/collection/state/fetchEntities/initialState.js create mode 100644 web/src/collection/state/fetchEntities/makeEntityReducer.js create mode 100644 web/src/collection/state/fetchEntities/makeFetchEntitiesHook.js create mode 100644 web/src/collection/state/fileCache/actions.js create mode 100644 web/src/collection/state/fileCache/initialState.js create mode 100644 web/src/collection/state/fileCache/reducer.js create mode 100644 web/src/collection/state/fileCluster/actions.js create mode 100644 web/src/collection/state/fileCluster/initialState.js create mode 100644 web/src/collection/state/fileCluster/reducer.js create mode 100644 web/src/collection/state/fileCluster/sagas.js rename web/src/collection/state/{ => fileList}/FileListType.js (100%) rename web/src/collection/state/{ => fileList}/FileSort.js (100%) rename web/src/collection/state/{ => fileList}/MatchCategory.js (100%) create mode 100644 web/src/collection/state/fileList/actions.js create mode 100644 web/src/collection/state/fileList/initialState.js create mode 100644 web/src/collection/state/fileList/reducer.js create mode 100644 web/src/collection/state/fileList/sagas.js create mode 100644 web/src/collection/state/fileMatches/actions.js create mode 100644 web/src/collection/state/fileMatches/initialState.js create mode 100644 web/src/collection/state/fileMatches/reducer.js create mode 100644 web/src/collection/state/fileMatches/sagas.js create mode 100644 web/src/collection/state/helpers/extendEntityList.js create mode 100644 web/src/collection/state/helpers/extendEntityMap.js delete mode 100644 web/src/collection/state/index.js create mode 100644 web/src/collection/state/initialState.js diff --git a/web/src/application/state/index.js b/web/src/application/state/index.js index 8dd1361c..ce421d13 100644 --- a/web/src/application/state/index.js +++ b/web/src/application/state/index.js @@ -1,3 +1,3 @@ -export {} from "./actions"; -export { initialState, appRootReducer } from "./reducers"; export { appRootSaga } from "./sagas"; +export { default as appRootReducer } from "./reducers"; +export { default as initialState } from "./initialState"; diff --git a/web/src/application/state/initialState.js b/web/src/application/state/initialState.js new file mode 100644 index 00000000..2d97b36d --- /dev/null +++ b/web/src/application/state/initialState.js @@ -0,0 +1,13 @@ +import collInitialState from "../../collection/state/initialState"; + +/** + * The entire application initial state. + */ +const initialState = { + /** + * File collection management state. + */ + coll: collInitialState, +}; + +export default initialState; diff --git a/web/src/application/state/reducers.js b/web/src/application/state/reducers.js index 975736b3..0fad13bc 100644 --- a/web/src/application/state/reducers.js +++ b/web/src/application/state/reducers.js @@ -1,13 +1,8 @@ -import { - collRootReducer, - initialState as collInitialState, -} from "../../collection/state"; import { combineReducers } from "redux"; +import collRootReducer from "../../collection/state/reducers"; -export const initialState = { - coll: collInitialState, -}; - -export const appRootReducer = combineReducers({ +const appRootReducer = combineReducers({ coll: collRootReducer, }); + +export default appRootReducer; diff --git a/web/src/application/state/sagas.js b/web/src/application/state/sagas.js index d302ae3c..a370a470 100644 --- a/web/src/application/state/sagas.js +++ b/web/src/application/state/sagas.js @@ -1,5 +1,5 @@ import { all } from "redux-saga/effects"; -import { collRootSaga } from "../../collection/state"; +import collRootSaga from "../../collection/state/sagas"; /** * Application root saga. Initializes all other sagas. diff --git a/web/src/collection/components/FileBrowserPage/CategorySelector/CategorySelector.js b/web/src/collection/components/FileBrowserPage/CategorySelector/CategorySelector.js index a8799fc0..c2353822 100644 --- a/web/src/collection/components/FileBrowserPage/CategorySelector/CategorySelector.js +++ b/web/src/collection/components/FileBrowserPage/CategorySelector/CategorySelector.js @@ -2,7 +2,7 @@ import React from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { useIntl } from "react-intl"; -import { MatchCategory } from "../../../state/MatchCategory"; +import { MatchCategory } from "../../../state/fileList/MatchCategory"; import CategoryButton from "./CategoryButton"; import AllInclusiveOutlinedIcon from "@material-ui/icons/AllInclusiveOutlined"; import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined"; diff --git a/web/src/collection/components/FileBrowserPage/CategorySelector/index.js b/web/src/collection/components/FileBrowserPage/CategorySelector/index.js index 470ee435..2c928b21 100644 --- a/web/src/collection/components/FileBrowserPage/CategorySelector/index.js +++ b/web/src/collection/components/FileBrowserPage/CategorySelector/index.js @@ -1,2 +1,2 @@ export { default } from "./CategorySelector"; -export { MatchCategory } from "../../../state/MatchCategory"; +export { MatchCategory } from "../../../state/fileList/MatchCategory"; diff --git a/web/src/collection/components/FileBrowserPage/FileBrowserActions/FileBrowserActions.js b/web/src/collection/components/FileBrowserPage/FileBrowserActions/FileBrowserActions.js index ce15fe91..7c4dc91e 100644 --- a/web/src/collection/components/FileBrowserPage/FileBrowserActions/FileBrowserActions.js +++ b/web/src/collection/components/FileBrowserPage/FileBrowserActions/FileBrowserActions.js @@ -8,9 +8,9 @@ import ViewSelector from "./ViewSelector"; import SortSelector from "./SortSelector"; import SquaredIconButton from "../../../../common/components/SquaredIconButton"; import { useIntl } from "react-intl"; -import { FileSort } from "../../../state/FileSort"; +import { FileSort } from "../../../state/fileList/FileSort"; import { Badge } from "@material-ui/core"; -import FileListType from "../../../state/FileListType"; +import FileListType from "../../../state/fileList/FileListType"; const useStyles = makeStyles((theme) => ({ actions: { diff --git a/web/src/collection/components/FileBrowserPage/FileBrowserActions/SortSelector.js b/web/src/collection/components/FileBrowserPage/FileBrowserActions/SortSelector.js index c95fec86..0e725830 100644 --- a/web/src/collection/components/FileBrowserPage/FileBrowserActions/SortSelector.js +++ b/web/src/collection/components/FileBrowserPage/FileBrowserActions/SortSelector.js @@ -7,7 +7,7 @@ import InputLabel from "@material-ui/core/InputLabel"; import Select from "@material-ui/core/Select"; import MenuItem from "@material-ui/core/MenuItem"; import { useIntl } from "react-intl"; -import { FileSort } from "../../../state/FileSort"; +import { FileSort } from "../../../state/fileList/FileSort"; const useStyles = makeStyles(() => ({ select: { diff --git a/web/src/collection/components/FileBrowserPage/FileBrowserActions/ViewSelector.js b/web/src/collection/components/FileBrowserPage/FileBrowserActions/ViewSelector.js index 8e4f7139..6030221a 100644 --- a/web/src/collection/components/FileBrowserPage/FileBrowserActions/ViewSelector.js +++ b/web/src/collection/components/FileBrowserPage/FileBrowserActions/ViewSelector.js @@ -4,7 +4,7 @@ import ListIcon from "@material-ui/icons/ViewStream"; import GridIcon from "@material-ui/icons/ViewModule"; import { useIntl } from "react-intl"; import IconSelect from "../../../../common/components/IconSelect"; -import FileListType from "../../../state/FileListType"; +import FileListType from "../../../state/fileList/FileListType"; function useMessages() { const intl = useIntl(); diff --git a/web/src/collection/components/FileBrowserPage/FileBrowserPage.js b/web/src/collection/components/FileBrowserPage/FileBrowserPage.js index 462e6f88..aa030427 100644 --- a/web/src/collection/components/FileBrowserPage/FileBrowserPage.js +++ b/web/src/collection/components/FileBrowserPage/FileBrowserPage.js @@ -11,13 +11,13 @@ import FileLinearList from "./FileLinearList/FileLinearList"; import FileGridList from "./FileGridList"; import { useDispatch, useSelector } from "react-redux"; import { - selectCounts, - selectError, + selectFileList, + selectFileCounts, + selectFileError, selectFiles, - selectFilters, - selectLoading, + selectFileFilters, + selectFileLoading, } from "../../state/selectors"; -import { fetchFiles, selectColl, updateFilters } from "../../state"; import Fab from "@material-ui/core/Fab"; import Zoom from "@material-ui/core/Zoom"; import VisibilitySensor from "react-visibility-sensor"; @@ -25,9 +25,13 @@ import { scrollIntoView } from "../../../common/helpers/scroll"; import { useHistory, useLocation } from "react-router-dom"; import { routes } from "../../../routing/routes"; import { useIntl } from "react-intl"; -import { defaultFilters } from "../../state/reducers"; -import FileListType from "../../state/FileListType"; -import { changeFileListView } from "../../state/actions"; +import FileListType from "../../state/fileList/FileListType"; +import { + changeFileListView, + fetchFiles, + updateFilters, +} from "../../state/fileList/actions"; +import { defaultFilters } from "../../state/fileList/initialState"; const useStyles = makeStyles((theme) => ({ container: { @@ -127,17 +131,17 @@ function FileBrowserPage(props) { const { className } = props; const classes = useStyles(); const [showFilters, setShowFilters] = useState(false); - const collState = useSelector(selectColl); - const error = useSelector(selectError); - const loading = useSelector(selectLoading); + const fileListState = useSelector(selectFileList); + const error = useSelector(selectFileError); + const loading = useSelector(selectFileLoading); const files = useSelector(selectFiles); - const filters = useSelector(selectFilters); - const counts = useSelector(selectCounts); + const filters = useSelector(selectFileFilters); + const counts = useSelector(selectFileCounts); const dispatch = useDispatch(); const [top, setTop] = useState(true); const topRef = useRef(null); const history = useHistory(); - const view = collState.fileListType; + const view = fileListState.fileListType; const List = listComponent(view); const intl = useIntl(); const showFiltersRef = useRef(); @@ -146,10 +150,10 @@ function FileBrowserPage(props) { const activeFilters = FilterPane.useActiveFilters(); useEffect(() => { - if (!keepFilters || collState.neverLoaded) { + if (!keepFilters || fileListState.neverLoaded) { dispatch(updateFilters(defaultFilters)); } - }, [keepFilters, collState.neverLoaded]); + }, [keepFilters, fileListState.neverLoaded]); const handleFetchPage = useCallback(() => dispatch(fetchFiles()), []); diff --git a/web/src/collection/components/FileBrowserPage/FileGridList/FileGridListItem.js b/web/src/collection/components/FileBrowserPage/FileGridList/FileGridListItem.js index 882ef31f..8e652a35 100644 --- a/web/src/collection/components/FileBrowserPage/FileGridList/FileGridListItem.js +++ b/web/src/collection/components/FileBrowserPage/FileGridList/FileGridListItem.js @@ -2,7 +2,7 @@ import React, { useCallback } from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import { FileType } from "../FileType"; +import { FileType } from "../../../prop-types/FileType"; import MediaPreview from "../../../../common/components/MediaPreview"; import VideocamOutlinedIcon from "@material-ui/icons/VideocamOutlined"; import MoreHorizOutlinedIcon from "@material-ui/icons/MoreHorizOutlined"; diff --git a/web/src/collection/components/FileBrowserPage/FileLinearList/FileLinearListItem.js b/web/src/collection/components/FileBrowserPage/FileLinearList/FileLinearListItem.js index ab4b64c1..4ecf2511 100644 --- a/web/src/collection/components/FileBrowserPage/FileLinearList/FileLinearListItem.js +++ b/web/src/collection/components/FileBrowserPage/FileLinearList/FileLinearListItem.js @@ -2,7 +2,7 @@ import React, { useCallback } from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import { FileType } from "../FileType"; +import { FileType } from "../../../prop-types/FileType"; import MoreHorizOutlinedIcon from "@material-ui/icons/MoreHorizOutlined"; import IconButton from "@material-ui/core/IconButton"; import { useIntl } from "react-intl"; diff --git a/web/src/collection/components/FileBrowserPage/FilterPane/ContentFilters.js b/web/src/collection/components/FileBrowserPage/FilterPane/ContentFilters.js index 56504b01..dd79eb79 100644 --- a/web/src/collection/components/FileBrowserPage/FilterPane/ContentFilters.js +++ b/web/src/collection/components/FileBrowserPage/FilterPane/ContentFilters.js @@ -5,9 +5,9 @@ import { useFilters } from "./useFilters"; import { useIntl } from "react-intl"; import RangeFilter from "./RangeFilter"; import { useSelector } from "react-redux"; -import { selectFilters } from "../../../state/selectors"; +import { selectFileFilters } from "../../../state/selectors"; import objectDiff from "../../../../common/helpers/objectDiff"; -import { initialState } from "../../../state"; +import { defaultFilters } from "../../../state/fileList/initialState"; /** * Get i18n text @@ -25,8 +25,8 @@ function useMessages() { * Get count of active filters. */ function useActiveFilters() { - const filters = useSelector(selectFilters); - const diff = objectDiff(filters, initialState.filters); + const filters = useSelector(selectFileFilters); + const diff = objectDiff(filters, defaultFilters); return Number(diff.length); } diff --git a/web/src/collection/components/FileBrowserPage/FilterPane/MetadataFilters.js b/web/src/collection/components/FileBrowserPage/FilterPane/MetadataFilters.js index efa8dc0d..f61a8480 100644 --- a/web/src/collection/components/FileBrowserPage/FilterPane/MetadataFilters.js +++ b/web/src/collection/components/FileBrowserPage/FilterPane/MetadataFilters.js @@ -7,10 +7,10 @@ import FilterList from "./FilterList"; import DateRangeFilter from "./DateRangeFilter"; import BoolFilter from "./BoolFilter"; import { useIntl } from "react-intl"; -import { initialState } from "../../../state"; +import { defaultFilters } from "../../../state/fileList/initialState"; import objectDiff from "../../../../common/helpers/objectDiff"; import { useSelector } from "react-redux"; -import { selectFilters } from "../../../state/selectors"; +import { selectFileFilters } from "../../../state/selectors"; /** * Get i18n text. @@ -31,8 +31,8 @@ function useMessages() { * Get count of active filters. */ function useActiveFilters() { - const filters = useSelector(selectFilters); - const diff = objectDiff(filters, initialState.filters); + const filters = useSelector(selectFileFilters); + const diff = objectDiff(filters, defaultFilters); return diff.extensions + diff.date + diff.audio + diff.exif; } diff --git a/web/src/collection/components/FileBrowserPage/FilterPane/useFilters.js b/web/src/collection/components/FileBrowserPage/FilterPane/useFilters.js index ca1ee643..78f4c112 100644 --- a/web/src/collection/components/FileBrowserPage/FilterPane/useFilters.js +++ b/web/src/collection/components/FileBrowserPage/FilterPane/useFilters.js @@ -1,15 +1,15 @@ import { isEqual, merge } from "lodash"; import { useDispatch, useSelector } from "react-redux"; -import { selectFilters } from "../../../state/selectors"; +import { selectFileFilters } from "../../../state/selectors"; import { useCallback, useEffect, useState } from "react"; -import { updateFilters } from "../../../state"; +import { updateFilters } from "../../../state/fileList/actions"; /** * Hook to smoothly update hooks */ export function useFilters() { // Access current redux state - const filters = useSelector(selectFilters); + const filters = useSelector(selectFileFilters); const dispatch = useDispatch(); const [changes, setChanges] = useState({}); // unsaved changes diff --git a/web/src/collection/components/FileClusterPage/FileClusterPage.js b/web/src/collection/components/FileClusterPage/FileClusterPage.js index a59655f6..06252a26 100644 --- a/web/src/collection/components/FileClusterPage/FileClusterPage.js +++ b/web/src/collection/components/FileClusterPage/FileClusterPage.js @@ -56,7 +56,7 @@ function FileClusterPage(props) { resumeLoading: loadCluster, hasMore, total, - } = useFileCluster({ fileId: id, hops: 2 }); + } = useFileCluster({ fileId: id, filters: { hops: 2 } }); const handleLoadFile = useCallback(() => { loadFile(); diff --git a/web/src/collection/components/FileComparisonPage/FileDetails/FileDescriptionPane.js b/web/src/collection/components/FileComparisonPage/FileDetails/FileDescriptionPane.js index 145fb9e7..34911a89 100644 --- a/web/src/collection/components/FileComparisonPage/FileDetails/FileDescriptionPane.js +++ b/web/src/collection/components/FileComparisonPage/FileDetails/FileDescriptionPane.js @@ -2,7 +2,7 @@ import React, { useCallback, useState } from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import { FileType } from "../../FileBrowserPage/FileType"; +import { FileType } from "../../../prop-types/FileType"; import Paper from "@material-ui/core/Paper"; import CollapseButton from "../../../../common/components/CollapseButton"; import { useIntl } from "react-intl"; diff --git a/web/src/collection/components/FileComparisonPage/FileDetails/FileDetails.js b/web/src/collection/components/FileComparisonPage/FileDetails/FileDetails.js index 8d226631..7d93fb28 100644 --- a/web/src/collection/components/FileComparisonPage/FileDetails/FileDetails.js +++ b/web/src/collection/components/FileComparisonPage/FileDetails/FileDetails.js @@ -2,7 +2,7 @@ import React, { useCallback, useState } from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import { FileType } from "../../FileBrowserPage/FileType"; +import { FileType } from "../../../prop-types/FileType"; import VideoPlayerPane from "../../VideoDetailsPage/VideoPlayerPane"; import { seekTo } from "../../VideoDetailsPage/seekTo"; import FileDescriptionPane from "./FileDescriptionPane"; diff --git a/web/src/collection/components/FileComparisonPage/MatchFiles/FileMatchHeader.js b/web/src/collection/components/FileComparisonPage/MatchFiles/FileMatchHeader.js index 4eb08ebf..ed3417fb 100644 --- a/web/src/collection/components/FileComparisonPage/MatchFiles/FileMatchHeader.js +++ b/web/src/collection/components/FileComparisonPage/MatchFiles/FileMatchHeader.js @@ -3,7 +3,7 @@ import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; import FileSummary from "../../FileSummary/FileSummary"; -import { FileType } from "../../FileBrowserPage/FileType"; +import { FileType } from "../../../prop-types/FileType"; import Distance from "../../../../common/components/Distance"; const useStyles = makeStyles((theme) => ({ diff --git a/web/src/collection/components/FileComparisonPage/MatchFiles/MatchSelector.js b/web/src/collection/components/FileComparisonPage/MatchFiles/MatchSelector.js index c438b009..4d1c1f53 100644 --- a/web/src/collection/components/FileComparisonPage/MatchFiles/MatchSelector.js +++ b/web/src/collection/components/FileComparisonPage/MatchFiles/MatchSelector.js @@ -2,7 +2,7 @@ import React, { useCallback } from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import FileType from "../../FileBrowserPage/FileType"; +import FileType from "../../../prop-types/FileType"; import FormControl from "@material-ui/core/FormControl"; import InputLabel from "@material-ui/core/InputLabel"; import Select from "@material-ui/core/Select"; diff --git a/web/src/collection/components/FileComparisonPage/MotherFile/FileDetailsHeader.js b/web/src/collection/components/FileComparisonPage/MotherFile/FileDetailsHeader.js index 46114f3e..ab2d0e83 100644 --- a/web/src/collection/components/FileComparisonPage/MotherFile/FileDetailsHeader.js +++ b/web/src/collection/components/FileComparisonPage/MotherFile/FileDetailsHeader.js @@ -3,7 +3,7 @@ import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; import FileSummary from "../../FileSummary/FileSummary"; -import { FileType } from "../../FileBrowserPage/FileType"; +import { FileType } from "../../../prop-types/FileType"; const useStyles = makeStyles((theme) => ({ header: { diff --git a/web/src/collection/components/FileMatchesPage/FileMatchesPage.js b/web/src/collection/components/FileMatchesPage/FileMatchesPage.js index edb057fd..81fd2a47 100644 --- a/web/src/collection/components/FileMatchesPage/FileMatchesPage.js +++ b/web/src/collection/components/FileMatchesPage/FileMatchesPage.js @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; @@ -18,9 +18,12 @@ import useFile from "../../hooks/useFile"; import FileLoadingHeader from "../FileLoadingHeader"; import { useDispatch, useSelector } from "react-redux"; import { selectFileMatches } from "../../state/selectors"; -import { fetchFileMatches, updateFileMatchFilters } from "../../state/actions"; import LoadTrigger from "../../../common/components/LoadingTrigger/LoadTrigger"; import { routes } from "../../../routing/routes"; +import { + fetchFileMatchesSlice, + updateFileMatchesParams, +} from "../../state/fileMatches/actions"; const useStyles = makeStyles((theme) => ({ root: { @@ -74,18 +77,18 @@ function FileMatchesPage(props) { const dispatch = useDispatch(); const history = useHistory(); + useEffect(() => { + if (fileMatches.params.fileId !== id) { + dispatch(updateFileMatchesParams({ fileId: id })); + } + }, [id, fileMatches]); + const handleCompare = useCallback( (file) => history.push(routes.collection.fileComparisonURL(id, file?.id)), [id] ); - const handleLoad = useCallback(() => { - if (fileMatches.total == null || fileMatches.filters.fileId !== id) { - dispatch(updateFileMatchFilters({ fileId: id, hops: 1 })); - } else { - dispatch(fetchFileMatches()); - } - }, [id, fileMatches]); + const handleLoad = useCallback(() => dispatch(fetchFileMatchesSlice()), []); if (file == null) { return ( @@ -151,7 +154,7 @@ function FileMatchesPage(props) { hasMore={ fileMatches.total == null || fileMatches.matches.length < fileMatches.total || - fileMatches.filters.fileId !== id + fileMatches.params.fileId !== id } container={MatchPreview.Container} errorMessage={messages.loadError} diff --git a/web/src/collection/components/FileMatchesPage/MatchPreview/FileAttributes.js b/web/src/collection/components/FileMatchesPage/MatchPreview/FileAttributes.js index 1e4f9c3d..ddfec341 100644 --- a/web/src/collection/components/FileMatchesPage/MatchPreview/FileAttributes.js +++ b/web/src/collection/components/FileMatchesPage/MatchPreview/FileAttributes.js @@ -2,7 +2,7 @@ import React from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import FileType from "../../FileBrowserPage/FileType"; +import FileType from "../../../prop-types/FileType"; import TableBody from "@material-ui/core/TableBody"; import Table from "@material-ui/core/Table"; import { useIntl } from "react-intl"; diff --git a/web/src/collection/components/FileMatchesPage/MatchPreview/MatchPreview.js b/web/src/collection/components/FileMatchesPage/MatchPreview/MatchPreview.js index d8ec2338..2faacbad 100644 --- a/web/src/collection/components/FileMatchesPage/MatchPreview/MatchPreview.js +++ b/web/src/collection/components/FileMatchesPage/MatchPreview/MatchPreview.js @@ -9,7 +9,7 @@ import MoreHorizOutlinedIcon from "@material-ui/icons/MoreHorizOutlined"; import FileAttributes from "./FileAttributes"; import { useIntl } from "react-intl"; import ButtonBase from "@material-ui/core/ButtonBase"; -import FileType from "../../FileBrowserPage/FileType"; +import FileType from "../../../prop-types/FileType"; import Container from "./Container"; import Distance from "../../../../common/components/Distance"; diff --git a/web/src/collection/components/FileSummary/CreationDate.js b/web/src/collection/components/FileSummary/CreationDate.js index 389647f5..a16c2190 100644 --- a/web/src/collection/components/FileSummary/CreationDate.js +++ b/web/src/collection/components/FileSummary/CreationDate.js @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import { formatDate } from "../../../common/helpers/format"; import EventAvailableOutlinedIcon from "@material-ui/icons/EventAvailableOutlined"; import AttributeText from "../../../common/components/AttributeText"; -import { FileType } from "../FileBrowserPage/FileType"; +import { FileType } from "../../prop-types/FileType"; import { useIntl } from "react-intl"; function CreationDate(props) { diff --git a/web/src/collection/components/FileSummary/Duration.js b/web/src/collection/components/FileSummary/Duration.js index 9ded30ae..29701e31 100644 --- a/web/src/collection/components/FileSummary/Duration.js +++ b/web/src/collection/components/FileSummary/Duration.js @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import { formatDuration } from "../../../common/helpers/format"; import ScheduleOutlinedIcon from "@material-ui/icons/ScheduleOutlined"; import AttributeText from "../../../common/components/AttributeText"; -import { FileType } from "../FileBrowserPage/FileType"; +import { FileType } from "../../prop-types/FileType"; function Duration(props) { const { file, className, ...other } = props; diff --git a/web/src/collection/components/FileSummary/FileSummary.js b/web/src/collection/components/FileSummary/FileSummary.js index 9ae37ffc..82986715 100644 --- a/web/src/collection/components/FileSummary/FileSummary.js +++ b/web/src/collection/components/FileSummary/FileSummary.js @@ -3,7 +3,7 @@ import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; import Name from "./Name"; -import { FileType } from "../FileBrowserPage/FileType"; +import { FileType } from "../../prop-types/FileType"; import Divider from "./Divider"; import Spacer from "./Spacer"; import Fingerprint from "./Fingerprint"; diff --git a/web/src/collection/components/FileSummary/Fingerprint.js b/web/src/collection/components/FileSummary/Fingerprint.js index db3a988c..1b0c83e1 100644 --- a/web/src/collection/components/FileSummary/Fingerprint.js +++ b/web/src/collection/components/FileSummary/Fingerprint.js @@ -1,7 +1,7 @@ import React from "react"; import PropTypes from "prop-types"; import AttributeText from "../../../common/components/AttributeText"; -import { FileType } from "../FileBrowserPage/FileType"; +import { FileType } from "../../prop-types/FileType"; import { useIntl } from "react-intl"; /** diff --git a/web/src/collection/components/FileSummary/HasAudio.js b/web/src/collection/components/FileSummary/HasAudio.js index 3a9d2af7..69d93b3e 100644 --- a/web/src/collection/components/FileSummary/HasAudio.js +++ b/web/src/collection/components/FileSummary/HasAudio.js @@ -1,7 +1,7 @@ import React from "react"; import PropTypes from "prop-types"; import VolumeOffOutlinedIcon from "@material-ui/icons/VolumeOffOutlined"; -import { FileType } from "../FileBrowserPage/FileType"; +import { FileType } from "../../prop-types/FileType"; function HasAudio(props) { const { className, ...other } = props; diff --git a/web/src/collection/components/FileSummary/HasExif.js b/web/src/collection/components/FileSummary/HasExif.js index 0c7c8faa..880b336d 100644 --- a/web/src/collection/components/FileSummary/HasExif.js +++ b/web/src/collection/components/FileSummary/HasExif.js @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import { formatBool } from "../../../common/helpers/format"; import ExifIcon from "../../../common/components/icons/ExifIcon"; import AttributeText from "../../../common/components/AttributeText"; -import { FileType } from "../FileBrowserPage/FileType"; +import { FileType } from "../../prop-types/FileType"; import { useIntl } from "react-intl"; function HasExif(props) { diff --git a/web/src/collection/components/FileSummary/Name.js b/web/src/collection/components/FileSummary/Name.js index 397d3a98..cc9fa966 100644 --- a/web/src/collection/components/FileSummary/Name.js +++ b/web/src/collection/components/FileSummary/Name.js @@ -2,7 +2,7 @@ import React from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import { FileType } from "../FileBrowserPage/FileType"; +import { FileType } from "../../prop-types/FileType"; import VideocamOutlinedIcon from "@material-ui/icons/VideocamOutlined"; import AttributeText from "../../../common/components/AttributeText"; import { useIntl } from "react-intl"; diff --git a/web/src/collection/components/FileSummaryHeader/FileSummaryHeader.js b/web/src/collection/components/FileSummaryHeader/FileSummaryHeader.js index 90e940df..b3fcb558 100644 --- a/web/src/collection/components/FileSummaryHeader/FileSummaryHeader.js +++ b/web/src/collection/components/FileSummaryHeader/FileSummaryHeader.js @@ -3,7 +3,7 @@ import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; import Paper from "@material-ui/core/Paper"; -import { FileType } from "../FileBrowserPage/FileType"; +import { FileType } from "../../prop-types/FileType"; import IconButton from "@material-ui/core/IconButton"; import ArrowBackOutlinedIcon from "@material-ui/icons/ArrowBackOutlined"; import { useIntl } from "react-intl"; diff --git a/web/src/collection/components/MatchGraph/LinkTooltip.js b/web/src/collection/components/MatchGraph/LinkTooltip.js index e1b394a8..36c12f80 100644 --- a/web/src/collection/components/MatchGraph/LinkTooltip.js +++ b/web/src/collection/components/MatchGraph/LinkTooltip.js @@ -8,7 +8,7 @@ import VideocamOutlinedIcon from "@material-ui/icons/VideocamOutlined"; import { basename } from "../../../common/helpers/paths"; import { formatDuration } from "../../../common/helpers/format"; import { useIntl } from "react-intl"; -import FileType from "../FileBrowserPage/FileType"; +import FileType from "../../prop-types/FileType"; const useStyles = makeStyles((theme) => ({ root: { diff --git a/web/src/collection/components/MatchGraph/MatchGraph.js b/web/src/collection/components/MatchGraph/MatchGraph.js index f80e2a80..84d8b7ab 100644 --- a/web/src/collection/components/MatchGraph/MatchGraph.js +++ b/web/src/collection/components/MatchGraph/MatchGraph.js @@ -3,8 +3,8 @@ import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; import D3Graph from "./D3Graph"; -import MatchType from "../FileMatchesPage/MatchType"; -import FileType from "../FileBrowserPage/FileType"; +import MatchType from "../../prop-types/MatchType"; +import FileType from "../../prop-types/FileType"; import { useHistory } from "react-router-dom"; import { routes } from "../../../routing/routes"; import prepareGraph from "./prepareGraph"; diff --git a/web/src/collection/components/MatchGraph/NodeTooltip.js b/web/src/collection/components/MatchGraph/NodeTooltip.js index e7a4d08e..043dbb4c 100644 --- a/web/src/collection/components/MatchGraph/NodeTooltip.js +++ b/web/src/collection/components/MatchGraph/NodeTooltip.js @@ -2,7 +2,7 @@ import React from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import FileType from "../FileBrowserPage/FileType"; +import FileType from "../../prop-types/FileType"; import Paper from "@material-ui/core/Paper"; import VideocamOutlinedIcon from "@material-ui/icons/VideocamOutlined"; import { basename } from "../../../common/helpers/paths"; diff --git a/web/src/collection/components/VideoDetailsPage/ExifPanel.js b/web/src/collection/components/VideoDetailsPage/ExifPanel.js index a485383d..8c22b539 100644 --- a/web/src/collection/components/VideoDetailsPage/ExifPanel.js +++ b/web/src/collection/components/VideoDetailsPage/ExifPanel.js @@ -2,7 +2,7 @@ import React, { useState } from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import { FileType } from "../FileBrowserPage/FileType"; +import { FileType } from "../../prop-types/FileType"; import { SelectableTab, SelectableTabs, diff --git a/web/src/collection/components/VideoDetailsPage/FileInfoPanel.js b/web/src/collection/components/VideoDetailsPage/FileInfoPanel.js index 8479bf77..91798d88 100644 --- a/web/src/collection/components/VideoDetailsPage/FileInfoPanel.js +++ b/web/src/collection/components/VideoDetailsPage/FileInfoPanel.js @@ -2,7 +2,7 @@ import React from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import { FileType } from "../FileBrowserPage/FileType"; +import { FileType } from "../../prop-types/FileType"; import Table from "@material-ui/core/Table"; import TableBody from "@material-ui/core/TableBody"; import TableRow from "@material-ui/core/TableRow"; diff --git a/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectGroup.js b/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectGroup.js index 8494220d..6f940e51 100644 --- a/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectGroup.js +++ b/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectGroup.js @@ -2,7 +2,7 @@ import React, { useCallback } from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import ObjectType from "../ObjectType"; +import ObjectType from "../../../prop-types/ObjectType"; import usePopup from "../../../../common/hooks/usePopup"; import ObjectGroupPopper from "./ObjectGroupPopper"; import { ButtonBase } from "@material-ui/core"; diff --git a/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectGroupPopper.js b/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectGroupPopper.js index cc53f965..8134a152 100644 --- a/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectGroupPopper.js +++ b/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectGroupPopper.js @@ -2,7 +2,7 @@ import React, { useCallback, useState } from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import ObjectType from "../ObjectType"; +import ObjectType from "../../../prop-types/ObjectType"; import Popper from "@material-ui/core/Popper"; import Paper from "@material-ui/core/Paper"; import ClickAwayListener from "@material-ui/core/ClickAwayListener"; diff --git a/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectPreview.js b/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectPreview.js index 5a47a530..48ab17a0 100644 --- a/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectPreview.js +++ b/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectPreview.js @@ -2,7 +2,7 @@ import React, { useEffect, useRef } from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import ObjectType from "../ObjectType"; +import ObjectType from "../../../prop-types/ObjectType"; import ButtonBase from "@material-ui/core/ButtonBase"; import { useIntl } from "react-intl"; import { objectKind, objectTime } from "./helpers"; diff --git a/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectTimeLine.js b/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectTimeLine.js index c97be6be..9c89e02a 100644 --- a/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectTimeLine.js +++ b/web/src/collection/components/VideoDetailsPage/ObjectTimeLine/ObjectTimeLine.js @@ -2,7 +2,7 @@ import React from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import { FileType } from "../../FileBrowserPage/FileType"; +import { FileType } from "../../../prop-types/FileType"; import ObjectGroup from "./ObjectGroup"; import { groupObjects } from "../groupObjects"; import { useIntl } from "react-intl"; diff --git a/web/src/collection/components/VideoDetailsPage/ObjectsPanel/ObjectGroupListItem.js b/web/src/collection/components/VideoDetailsPage/ObjectsPanel/ObjectGroupListItem.js index 319fe08a..03adade5 100644 --- a/web/src/collection/components/VideoDetailsPage/ObjectsPanel/ObjectGroupListItem.js +++ b/web/src/collection/components/VideoDetailsPage/ObjectsPanel/ObjectGroupListItem.js @@ -2,7 +2,7 @@ import React from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles, withStyles } from "@material-ui/styles"; -import ObjectType from "../ObjectType"; +import ObjectType from "../../../prop-types/ObjectType"; import TimeCaption from "../TimeCaption"; import SquaredIconButton from "../../../../common/components/SquaredIconButton"; import ObjectKinds from "../ObjectKinds"; diff --git a/web/src/collection/components/VideoDetailsPage/ObjectsPanel/ObjectsPanel.js b/web/src/collection/components/VideoDetailsPage/ObjectsPanel/ObjectsPanel.js index a0e19622..8403ebbd 100644 --- a/web/src/collection/components/VideoDetailsPage/ObjectsPanel/ObjectsPanel.js +++ b/web/src/collection/components/VideoDetailsPage/ObjectsPanel/ObjectsPanel.js @@ -2,7 +2,7 @@ import React from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import { FileType } from "../../FileBrowserPage/FileType"; +import { FileType } from "../../../prop-types/FileType"; import ObjectGroupList from "./ObjectGroupList"; import { groupObjects } from "../groupObjects"; import ObjectGroupListItem from "./ObjectGroupListItem"; diff --git a/web/src/collection/components/VideoDetailsPage/SceneSelector/Scene.js b/web/src/collection/components/VideoDetailsPage/SceneSelector/Scene.js index e3a6b552..ef0c1848 100644 --- a/web/src/collection/components/VideoDetailsPage/SceneSelector/Scene.js +++ b/web/src/collection/components/VideoDetailsPage/SceneSelector/Scene.js @@ -2,7 +2,7 @@ import React, { useCallback } from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import SceneType from "../SceneType"; +import SceneType from "../../../prop-types/SceneType"; import MediaPreview from "../../../../common/components/MediaPreview"; import TimeCaption from "../TimeCaption"; import { useIntl } from "react-intl"; diff --git a/web/src/collection/components/VideoDetailsPage/SceneSelector/SceneSelector.js b/web/src/collection/components/VideoDetailsPage/SceneSelector/SceneSelector.js index 5d3cf3a8..d6f0fc23 100644 --- a/web/src/collection/components/VideoDetailsPage/SceneSelector/SceneSelector.js +++ b/web/src/collection/components/VideoDetailsPage/SceneSelector/SceneSelector.js @@ -2,7 +2,7 @@ import React, { useCallback, useState } from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import SceneType from "../SceneType"; +import SceneType from "../../../prop-types/SceneType"; import SceneList from "./SceneList"; import Scene from "./Scene"; import { useIntl } from "react-intl"; diff --git a/web/src/collection/components/VideoDetailsPage/VideoInformation.js b/web/src/collection/components/VideoDetailsPage/VideoInformation.js index 69d3977a..aa3fede9 100644 --- a/web/src/collection/components/VideoDetailsPage/VideoInformation.js +++ b/web/src/collection/components/VideoDetailsPage/VideoInformation.js @@ -2,7 +2,7 @@ import React, { useState } from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import { FileType } from "../FileBrowserPage/FileType"; +import { FileType } from "../../prop-types/FileType"; import FileInfoPanel from "./FileInfoPanel"; import ObjectsPanel from "./ObjectsPanel"; import ExifPanel from "./ExifPanel"; diff --git a/web/src/collection/components/VideoDetailsPage/VideoInformationPane.js b/web/src/collection/components/VideoDetailsPage/VideoInformationPane.js index db212000..b06231d3 100644 --- a/web/src/collection/components/VideoDetailsPage/VideoInformationPane.js +++ b/web/src/collection/components/VideoDetailsPage/VideoInformationPane.js @@ -2,7 +2,7 @@ import React from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import { FileType } from "../FileBrowserPage/FileType"; +import { FileType } from "../../prop-types/FileType"; import Paper from "@material-ui/core/Paper"; import { useIntl } from "react-intl"; import VideoInformation from "./VideoInformation"; diff --git a/web/src/collection/components/VideoDetailsPage/VideoPlayer.js b/web/src/collection/components/VideoDetailsPage/VideoPlayer.js index 318f7e8b..3eecf64e 100644 --- a/web/src/collection/components/VideoDetailsPage/VideoPlayer.js +++ b/web/src/collection/components/VideoDetailsPage/VideoPlayer.js @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; -import { FileType } from "../FileBrowserPage/FileType"; +import { FileType } from "../../prop-types/FileType"; import MediaPreview from "../../../common/components/MediaPreview"; import ReactPlayer from "react-player"; import { FLV_GLOBAL } from "react-player/lib/players/FilePlayer"; diff --git a/web/src/collection/components/VideoDetailsPage/VideoPlayerPane.js b/web/src/collection/components/VideoDetailsPage/VideoPlayerPane.js index 4a786be2..a355ef9e 100644 --- a/web/src/collection/components/VideoDetailsPage/VideoPlayerPane.js +++ b/web/src/collection/components/VideoDetailsPage/VideoPlayerPane.js @@ -3,7 +3,7 @@ import clsx from "clsx"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/styles"; import Paper from "@material-ui/core/Paper"; -import { FileType } from "../FileBrowserPage/FileType"; +import { FileType } from "../../prop-types/FileType"; import VideoPlayer from "./VideoPlayer"; import SceneSelector from "./SceneSelector"; import ObjectTimeLine from "./ObjectTimeLine"; diff --git a/web/src/collection/hooks/useFile.js b/web/src/collection/hooks/useFile.js index c41ec6a2..bd26d3e8 100644 --- a/web/src/collection/hooks/useFile.js +++ b/web/src/collection/hooks/useFile.js @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { selectCachedFile } from "../state/selectors"; import { useServer } from "../../server-api/context"; -import { cacheFile } from "../state/actions"; +import { cacheFile } from "../state/fileCache/actions"; import { Status } from "../../server-api/Response"; /** diff --git a/web/src/collection/hooks/useFileCluster.js b/web/src/collection/hooks/useFileCluster.js index df4e78d3..5e3264f2 100644 --- a/web/src/collection/hooks/useFileCluster.js +++ b/web/src/collection/hooks/useFileCluster.js @@ -1,67 +1,33 @@ -import { useCallback } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useSelector } from "react-redux"; import { selectFileCluster } from "../state/selectors"; -import { fetchFileCluster, updateFileClusterFilters } from "../state/actions"; -import useLoadAll from "./useLoadAll"; -import { initialState } from "../state"; +import { + fetchFileClusterSlice, + updateFileClusterParams, +} from "../state/fileCluster/actions"; +import initialState from "../state/fileCluster/initialState"; +import makeFetchEntitiesHook from "../state/fetchEntities/makeFetchEntitiesHook"; -/** - * Check if auto-loading may continue. - */ -function mayContinue(fileClusterState, fileId) { - return !( - fileClusterState.loading || - fileClusterState.error || - fileClusterState.matches.length >= fileClusterState.total || - fileClusterState.total == null || - fileClusterState.filters.fileId !== fileId - ); -} +const useFetchFileCluster = makeFetchEntitiesHook({ + stateSelector: selectFileCluster, + defaultParams: initialState.params, + updateParams: updateFileClusterParams, + fetchNextSlice: fetchFileClusterSlice, + resourceName: "matches", +}); /** - * Check if there are remaining cluster items. + * Fetch all file cluster elements satisfying the query params. + * @param params - The cluster query params. */ -function hasMore(fileClusterState, fileId) { - return ( - fileClusterState.total == null || - fileClusterState.matches.length < fileClusterState.total || - fileClusterState.filters.fileId !== fileId - ); -} - -/** - * Fetch all file cluster elements satisfying filter criteria. - * @param filters cluster loading filters - */ -export default function useFileCluster(filters) { - if (filters.fileId == null) { +export default function useFileCluster(params) { + if (params.fileId == null) { throw new Error("File id cannot be null."); } - const dispatch = useDispatch(); - const fileCluster = useSelector(selectFileCluster); - - const handleStart = useCallback( - (mergedFilters) => dispatch(updateFileClusterFilters(mergedFilters)), - [] - ); - const handleContinue = useCallback(() => dispatch(fetchFileCluster()), []); - - const resumeLoading = useLoadAll({ - requestedFilters: filters, - defaultFilters: initialState.fileCluster.filters, - savedFilters: fileCluster.filters, - mayContinue: mayContinue(fileCluster, filters.fileId), - startFetching: handleStart, - continueFetching: handleContinue, - }); + const state = useSelector(selectFileCluster); return { - matches: fileCluster.matches, - files: fileCluster.files, - total: fileCluster.total, - error: fileCluster.error, - resumeLoading, - hasMore: hasMore(fileCluster, filters.fileId), + ...useFetchFileCluster(params), + files: state.files, }; } diff --git a/web/src/collection/hooks/useFileMatches.js b/web/src/collection/hooks/useFileMatches.js index c96feec7..6852e392 100644 --- a/web/src/collection/hooks/useFileMatches.js +++ b/web/src/collection/hooks/useFileMatches.js @@ -1,66 +1,26 @@ -import { useCallback } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import makeFetchEntitiesHook from "../state/fetchEntities/makeFetchEntitiesHook"; import { selectFileMatches } from "../state/selectors"; -import { fetchFileMatches, updateFileMatchFilters } from "../state/actions"; -import useLoadAll from "./useLoadAll"; -import { initialState } from "../state"; +import { + fetchFileMatchesSlice, + updateFileMatchesParams, +} from "../state/fileMatches/actions"; +import initialState from "../state/fileMatches/initialState"; -/** - * Check if auto-loading may continue. - */ -function mayContinue(fileMatchesState, fileId) { - return !( - fileMatchesState.loading || - fileMatchesState.error || - fileMatchesState.matches.length >= fileMatchesState.total || - fileMatchesState.total == null || - fileMatchesState.filters.fileId !== fileId - ); -} +const useFetchFileMatches = makeFetchEntitiesHook({ + stateSelector: selectFileMatches, + defaultParams: initialState.params, + updateParams: updateFileMatchesParams, + fetchNextSlice: fetchFileMatchesSlice, + resourceName: "matches", +}); /** - * Check if there are remaining matches. + * Fetch all file matches satisfying the query params. + * @param params - The matches query params. */ -function hasMore(fileMatchesState, fileId) { - return ( - fileMatchesState.total == null || - fileMatchesState.matches.length < fileMatchesState.total || - fileMatchesState.filters.fileId !== fileId - ); -} - -/** - * Fetch all immediate file matches filtering criteria. - * @param filters match loading filters - */ -export default function useFileMatches(filters) { - if (filters.fileId == null) { +export default function useFileMatches(params) { + if (params.fileId == null) { throw new Error("File id cannot be null."); } - - const dispatch = useDispatch(); - const fileMatches = useSelector(selectFileMatches); - - const handleStart = useCallback( - (mergedFilters) => dispatch(updateFileMatchFilters(mergedFilters)), - [] - ); - const handleContinue = useCallback(() => dispatch(fetchFileMatches()), []); - - const resumeLoading = useLoadAll({ - requestedFilters: filters, - defaultFilters: initialState.fileMatches.filters, - savedFilters: fileMatches.filters, - mayContinue: mayContinue(fileMatches, filters.fileId), - startFetching: handleStart, - continueFetching: handleContinue, - }); - - return { - matches: fileMatches.matches, - total: fileMatches.total, - error: fileMatches.error, - resumeLoading, - hasMore: hasMore(fileMatches, filters.fileId), - }; + return useFetchFileMatches(params); } diff --git a/web/src/collection/components/FileBrowserPage/FileType.js b/web/src/collection/prop-types/FileType.js similarity index 100% rename from web/src/collection/components/FileBrowserPage/FileType.js rename to web/src/collection/prop-types/FileType.js diff --git a/web/src/collection/components/FileMatchesPage/MatchType.js b/web/src/collection/prop-types/MatchType.js similarity index 100% rename from web/src/collection/components/FileMatchesPage/MatchType.js rename to web/src/collection/prop-types/MatchType.js diff --git a/web/src/collection/components/VideoDetailsPage/ObjectType.js b/web/src/collection/prop-types/ObjectType.js similarity index 100% rename from web/src/collection/components/VideoDetailsPage/ObjectType.js rename to web/src/collection/prop-types/ObjectType.js diff --git a/web/src/collection/components/VideoDetailsPage/SceneType.js b/web/src/collection/prop-types/SceneType.js similarity index 100% rename from web/src/collection/components/VideoDetailsPage/SceneType.js rename to web/src/collection/prop-types/SceneType.js diff --git a/web/src/collection/state/actions.js b/web/src/collection/state/actions.js deleted file mode 100644 index 719041a3..00000000 --- a/web/src/collection/state/actions.js +++ /dev/null @@ -1,167 +0,0 @@ -import FileListType from "./FileListType"; - -export const ACTION_CHANGE_FILE_LIST_VIEW = "coll.CHANGE_FILE_LIST_VIEW"; - -export function changeFileListView(view) { - if (FileListType.values().indexOf(view) === -1) { - throw new Error(`Unknown file list type: ${view}`); - } - return { type: ACTION_CHANGE_FILE_LIST_VIEW, view }; -} - -export const ACTION_UPDATE_FILTERS = "coll.UPDATE_FILTERS"; - -export function updateFilters(filters) { - return { type: ACTION_UPDATE_FILTERS, filters }; -} - -export const ACTION_UPDATE_FILTERS_SUCCESS = "coll.UPDATE_FILTERS_SUCCESS"; - -export function updateFiltersSuccess(files, counts) { - return { type: ACTION_UPDATE_FILTERS_SUCCESS, files, counts }; -} - -export const ACTION_UPDATE_FILTERS_FAILURE = "coll.UPDATE_FILTERS_FAILURE"; - -export function updateFiltersFailure(error) { - return { type: ACTION_UPDATE_FILTERS_FAILURE, error }; -} - -/** - * Fetch next files page. - */ -export const ACTION_FETCH_FILES = "coll.FETCH_FILES"; - -export function fetchFiles() { - return { type: ACTION_FETCH_FILES }; -} - -export const ACTION_FETCH_FILES_SUCCESS = "coll.FETCH_FILES_SUCCESS"; - -export function fetchFilesSuccess(files, counts) { - return { type: ACTION_FETCH_FILES_SUCCESS, files, counts }; -} - -export const ACTION_FETCH_FILES_FAILURE = "coll.FETCH_FILES_FAILURE"; - -export function fetchFilesFailure(error) { - return { type: ACTION_FETCH_FILES_FAILURE, error }; -} - -/** - * Add file to cache. - */ - -export const ACTION_CACHE_FILE = "coll.CACHE_FILE"; - -export function cacheFile(file) { - return { file, type: ACTION_CACHE_FILE }; -} - -/** - * Single file matches actions - */ - -export const ACTION_UPDATE_FILE_MATCH_FILTERS = - "coll.UPDATE_FILE_MATCH_FILTERS"; - -export function updateFileMatchFilters(filters) { - return { filters, type: ACTION_UPDATE_FILE_MATCH_FILTERS }; -} - -export const ACTION_UPDATE_FILE_MATCH_FILTERS_SUCCESS = - "coll.UPDATE_FILE_MATCH_FILTERS_SUCCESS"; - -export function updateFileMatchFiltersSuccess(matches, files, total) { - return { - matches, - files, - total, - type: ACTION_UPDATE_FILE_MATCH_FILTERS_SUCCESS, - }; -} - -export const ACTION_UPDATE_FILE_MATCH_FILTERS_FAILURE = - "coll.UPDATE_FILE_MATCH_FILTERS_FAILURE"; - -export function updateFileMatchFiltersFailure(error) { - return { type: ACTION_UPDATE_FILE_MATCH_FILTERS_FAILURE, error }; -} - -/** - * Fetch next matches page - */ - -export const ACTION_FETCH_FILE_MATCHES = "coll.FETCH_FILE_MATCHES"; - -export function fetchFileMatches() { - return { type: ACTION_FETCH_FILE_MATCHES }; -} - -export const ACTION_FETCH_FILE_MATCHES_SUCCESS = - "coll.FETCH_FILE_MATCHES_SUCCESS"; - -export function fetchFileMatchesSuccess(matches, files, total) { - return { matches, files, total, type: ACTION_FETCH_FILE_MATCHES_SUCCESS }; -} - -export const ACTION_FETCH_FILE_MATCHES_FAILURE = - "coll.FETCH_FILE_MATCHES_FAILURE"; - -export function fetchFileMatchesFailure(error) { - return { error, type: ACTION_FETCH_FILE_MATCHES_FAILURE }; -} - -/** - * File cluster actions - */ - -export const ACTION_UPDATE_FILE_CLUSTER_FILTERS = - "coll.UPDATE_FILE_CLUSTER_FILTERS"; - -export function updateFileClusterFilters(filters) { - return { filters, type: ACTION_UPDATE_FILE_CLUSTER_FILTERS }; -} - -export const ACTION_UPDATE_FILE_CLUSTER_FILTERS_SUCCESS = - "coll.UPDATE_FILE_CLUSTER_FILTERS_SUCCESS"; - -export function updateFileClusterFiltersSuccess(matches, files, total) { - return { - matches, - files, - total, - type: ACTION_UPDATE_FILE_CLUSTER_FILTERS_SUCCESS, - }; -} - -export const ACTION_UPDATE_FILE_CLUSTER_FILTERS_FAILURE = - "coll.UPDATE_FILE_CLUSTER_FILTERS_FAILURE"; - -export function updateFileClusterFiltersFailure(error) { - return { type: ACTION_UPDATE_FILE_CLUSTER_FILTERS_FAILURE, error }; -} - -/** - * Fetch next cluster items page - */ - -export const ACTION_FETCH_FILE_CLUSTER = "coll.FETCH_FILE_CLUSTER"; - -export function fetchFileCluster() { - return { type: ACTION_FETCH_FILE_CLUSTER }; -} - -export const ACTION_FETCH_FILE_CLUSTER_SUCCESS = - "coll.FETCH_FILE_CLUSTER_SUCCESS"; - -export function fetchFileClusterSuccess(matches, files, total) { - return { matches, files, total, type: ACTION_FETCH_FILE_CLUSTER_SUCCESS }; -} - -export const ACTION_FETCH_FILE_CLUSTER_FAILURE = - "coll.FETCH_FILE_CLUSTER_FAILURE"; - -export function fetchFileClusterFailure(error) { - return { error, type: ACTION_FETCH_FILE_CLUSTER_FAILURE }; -} diff --git a/web/src/collection/state/fetchEntities/fetchEntitiesSaga.js b/web/src/collection/state/fetchEntities/fetchEntitiesSaga.js new file mode 100644 index 00000000..b1b5e46d --- /dev/null +++ b/web/src/collection/state/fetchEntities/fetchEntitiesSaga.js @@ -0,0 +1,61 @@ +import { call, put, select } from "redux-saga/effects"; + +/** + * Default function to determine request params from the current collection + * state. The `defaultParams` assumes the state has the same structure as + * the example from the "./initialState.js" module. + * + * @param state - The state that contains params, limit and items. + * @param resourceName - The state property name to hold entities. + * @return {{offset: *, limit: number}} + */ +function defaultParams(state, resourceName = "items") { + const { params, limit = 20 } = state; + return { offset: state[resourceName].length, limit, ...params }; +} + +/** + * Generic saga to fetch a slice from some entity collection. + * + * @param requestResource - The request function that will be used in {@link call} effect. + * @param {function} stateSelector - The selector for the current entity collection state. + * @param {function} success - The factory of the success action (must accept response data). + * @param {function} failure - The factory for the failure action (must accept response error). + * @param {function} getParams - The function to get request arguments. + * @param {string} resourceName - The name of the state property that holds entities array. + */ +export default function* fetchEntitiesSaga({ + requestResource, + stateSelector, + success, + failure, + getParams = defaultParams, + resourceName = "items", +}) { + try { + // Determine current query params + const state = yield select(stateSelector); + const args = getParams(state, resourceName); + + // Send request to permanent storage + const resp = yield call(requestResource, args); + + // Handle error + if (resp.failure) { + console.error(`Error fetching ${resourceName}`, { + args, + state, + requestResource, + error: resp.error, + }); + yield put(failure(resp.error)); + return; + } + + // Update state + yield put(success(resp.data)); + } catch (error) { + console.error(error); + yield put(failure(error)); + } +} diff --git a/web/src/collection/state/fetchEntities/initialState.js b/web/src/collection/state/fetchEntities/initialState.js new file mode 100644 index 00000000..d5698e6b --- /dev/null +++ b/web/src/collection/state/fetchEntities/initialState.js @@ -0,0 +1,54 @@ +/** + * Example initial state for loadable + * entity collection. + * + * @type {Object} + */ +// eslint-disable-next-line +const initialState = { + /** + * Request params. + */ + params: { + /** + * Whatever request parameters (like + * filters, fileId, include-fields, etc.) + * that may affect the actual data that + * will be fetched. + * + * If one of these parameters changes then + * all the fetched data must be discarded + * and a new data should be requested according + * to the new parameters. There must be all + * such parameters and only them. + */ + }, + /** + * Total count of entities that + * may be fetched with the given + * parameters. The value must be + * `undefined` when no data were + * fetched since the last params + * change. + */ + total: undefined, + /** + * True iff the previous request + * finished and was unsuccessful. + */ + error: false, + /** + * True iff the last request + * is still in progress. + */ + loading: false, + /** + * Maximal number of entities that + * should be fetched with one request. + */ + limit: 100, + /** + * Fetched entities. + */ + items: [], +}; diff --git a/web/src/collection/state/fetchEntities/makeEntityReducer.js b/web/src/collection/state/fetchEntities/makeEntityReducer.js new file mode 100644 index 00000000..491324ae --- /dev/null +++ b/web/src/collection/state/fetchEntities/makeEntityReducer.js @@ -0,0 +1,59 @@ +import lodash from "lodash"; +import extendEntityList from "../helpers/extendEntityList"; + +export default function makeEntityReducer({ + updateParams, + fetchSlice, + fetchSliceSuccess, + fetchSliceFailure, + initialState, + resourceName = "items", +}) { + return function fetchEntityReducer(state = initialState, action) { + switch (action.type) { + case updateParams: { + const params = lodash.merge({}, state.params, action.params); + return { + ...state, + params, + loading: false, + error: false, + total: undefined, + [resourceName]: [], + }; + } + case fetchSlice: + return { + ...state, + loading: true, + error: false, + }; + case fetchSliceSuccess: + if (!state.loading) { + const warning = + `Unexpected state when handling ${fetchSliceSuccess}: ` + + `state.loading must be true. Skipping...`; + console.warn(warning); + return state; + } + return { + ...state, + loading: false, + error: false, + [resourceName]: extendEntityList( + state[resourceName], + action[resourceName] + ), + total: action.total, + }; + case fetchSliceFailure: + return { + ...state, + loading: false, + error: true, + }; + default: + return state; + } + }; +} diff --git a/web/src/collection/state/fetchEntities/makeFetchEntitiesHook.js b/web/src/collection/state/fetchEntities/makeFetchEntitiesHook.js new file mode 100644 index 00000000..6ab8e085 --- /dev/null +++ b/web/src/collection/state/fetchEntities/makeFetchEntitiesHook.js @@ -0,0 +1,78 @@ +import { useDispatch, useSelector } from "react-redux"; +import { useCallback, useEffect } from "react"; +import useValue from "../../hooks/useValue"; +import lodash from "lodash"; + +/** + * Check if auto-loading may continue. + */ +function getMayContinue(state, mergedParams, resourceName) { + return !( + state.loading || + state.error || + state[resourceName].length >= state.total || + !lodash.isEqual(state.params, mergedParams) + ); +} + +/** + * Check if there are remaining cluster items. + */ +function hasMore(state, mergedParams, resourceName) { + return ( + state.total == null || + state[resourceName].length < state.total || + !lodash.isEqual(state.params, mergedParams) + ); +} + +/** + * Make a hook to load all available entities assuming the application state + * obeys convention of the fetchEntity framework. + */ +export default function makeFetchEntitiesHook({ + updateParams, + fetchNextSlice, + stateSelector, + defaultParams, + resourceName, +}) { + return function useFetchEntities(desiredParams) { + const dispatch = useDispatch(); + const state = useSelector(stateSelector); + const savedParams = state.params; + const mergedParams = useValue( + lodash.merge({}, defaultParams, desiredParams) + ); + const mayContinue = getMayContinue(state, mergedParams, resourceName); + + // Update filters and fetch the first slice when filters are changed + useEffect(() => { + dispatch(updateParams(mergedParams)); + }, [mergedParams]); + + // Fetch the next slice when ready. + useEffect(() => { + if (mayContinue) { + dispatch(fetchNextSlice()); + } + }, [mayContinue, mergedParams]); + + // Provide callback to resume loading on error. + const resumeLoading = useCallback(() => { + if (!lodash.isEqual(mergedParams, savedParams)) { + dispatch(updateParams(mergedParams)); + } else { + dispatch(fetchNextSlice(mergedParams)); + } + }, [mergedParams, savedParams]); + + return { + [resourceName]: state[resourceName], + total: state.total, + error: state.error, + resumeLoading, + hasMore: hasMore(state, mergedParams, resourceName), + }; + }; +} diff --git a/web/src/collection/state/fileCache/actions.js b/web/src/collection/state/fileCache/actions.js new file mode 100644 index 00000000..fd470a89 --- /dev/null +++ b/web/src/collection/state/fileCache/actions.js @@ -0,0 +1,20 @@ +/** + * "Add file to cache" action type. + */ +export const ACTION_CACHE_FILE = "coll.CACHE_FILE"; + +/** + * @typedef CacheFileAction + * @type Object + * @property {string} type - action type. + * @property {{id: any}} file - the file that should be cached. + */ + +/** + * Create "Add file to cache" action. + * @param {{id: any}} file - The file that should be cached. + * @return {CacheFileAction} A new action instance. + */ +export function cacheFile(file) { + return { file, type: ACTION_CACHE_FILE }; +} diff --git a/web/src/collection/state/fileCache/initialState.js b/web/src/collection/state/fileCache/initialState.js new file mode 100644 index 00000000..a1024b9f --- /dev/null +++ b/web/src/collection/state/fileCache/initialState.js @@ -0,0 +1,11 @@ +/** + * Initial state of cache of fully-fetched individual files. + * @type {{files: {}, maxSize: number, history: [number]}} + */ +const initialState = { + maxSize: 1000, + files: {}, + history: [], +}; + +export default initialState; diff --git a/web/src/collection/state/fileCache/reducer.js b/web/src/collection/state/fileCache/reducer.js new file mode 100644 index 00000000..757a4466 --- /dev/null +++ b/web/src/collection/state/fileCache/reducer.js @@ -0,0 +1,27 @@ +import initialState from "./initialState"; +import { ACTION_CACHE_FILE } from "./actions"; + +/** + * Root reducer for file cache. + * @param {Object} state - The initial state that will be modified. + * @param {CacheFileAction|Object} action - Action that must be executed. + * @return {Object} The new state. + */ +export default function fileCacheReducer(state = initialState, action) { + switch (action.type) { + case ACTION_CACHE_FILE: { + const files = { ...state.files, [action.file.id]: action.file }; + const history = [ + action.file.id, + ...state.history.filter((id) => id !== action.file.id), + ]; + if (history.length > state.maxSize) { + const evicted = history.pop(); + delete files[evicted]; + } + return { ...state, history, files }; + } + default: + return state; + } +} diff --git a/web/src/collection/state/fileCluster/actions.js b/web/src/collection/state/fileCluster/actions.js new file mode 100644 index 00000000..2ef97d1d --- /dev/null +++ b/web/src/collection/state/fileCluster/actions.js @@ -0,0 +1,62 @@ +/** + * "Update cluster params" action type. + * @type {string} + */ +export const ACTION_UPDATE_FILE_CLUSTER_PARAMS = + "coll.UPDATE_FILE_CLUSTER_PARAMS"; + +/** + * Create new update-cluster-params action. + */ +export function updateFileClusterParams(params) { + return { params, type: ACTION_UPDATE_FILE_CLUSTER_PARAMS }; +} + +/** + * "Fetch the next cluster slice" action type. + * @type {string} + */ +export const ACTION_FETCH_FILE_CLUSTER_SLICE = "coll.FETCH_FILE_CLUSTER_SLICE"; + +/** + * Create new "Fetch the next cluster slice" action. + * @return {{type: string}} + */ +export function fetchFileClusterSlice() { + return { type: ACTION_FETCH_FILE_CLUSTER_SLICE }; +} + +/** + * "Success of cluster slice fetching" action type. + * @type {string} + */ +export const ACTION_FETCH_FILE_CLUSTER_SLICE_SUCCESS = + "coll.FETCH_FILE_CLUSTER_SLICE_SUCCESS"; + +/** + * Create new "Success of cluster slice fetching" action. + */ +export function fetchFileClusterSliceSuccess({ matches, files, total }) { + return { + matches, + files, + total, + type: ACTION_FETCH_FILE_CLUSTER_SLICE_SUCCESS, + }; +} + +/** + * "Failure of cluster slice fetching" action type. + * @type {string} + */ +export const ACTION_FETCH_FILE_CLUSTER_SLICE_FAILURE = + "coll.FETCH_FILE_CLUSTER_SLICE_FAILURE"; + +/** + * Create new "Failure of cluster slice fetching" action. + * @param error + * @return {{error: *, type: string}} + */ +export function fetchFileClusterSliceFailure(error) { + return { error, type: ACTION_FETCH_FILE_CLUSTER_SLICE_FAILURE }; +} diff --git a/web/src/collection/state/fileCluster/initialState.js b/web/src/collection/state/fileCluster/initialState.js new file mode 100644 index 00000000..070df79d --- /dev/null +++ b/web/src/collection/state/fileCluster/initialState.js @@ -0,0 +1,23 @@ +/** + * Initial state of the fetched cluster items. + * @type {Object} + */ +const initialState = { + params: { + fileId: undefined, + filters: { + hops: 2, + minDistance: 0.0, + maxDistance: 1.0, + }, + fields: ["meta", "exif"], + }, + total: undefined, + error: false, + loading: false, + limit: 100, + matches: [], + files: {}, +}; + +export default initialState; diff --git a/web/src/collection/state/fileCluster/reducer.js b/web/src/collection/state/fileCluster/reducer.js new file mode 100644 index 00000000..76766add --- /dev/null +++ b/web/src/collection/state/fileCluster/reducer.js @@ -0,0 +1,35 @@ +import initialState from "./initialState"; +import { + ACTION_FETCH_FILE_CLUSTER_SLICE, + ACTION_FETCH_FILE_CLUSTER_SLICE_FAILURE, + ACTION_FETCH_FILE_CLUSTER_SLICE_SUCCESS, + ACTION_UPDATE_FILE_CLUSTER_PARAMS, +} from "./actions"; +import makeEntityReducer from "../fetchEntities/makeEntityReducer"; +import extendEntityMap from "../helpers/extendEntityMap"; + +const defaultReducer = makeEntityReducer({ + updateParams: ACTION_UPDATE_FILE_CLUSTER_PARAMS, + fetchSlice: ACTION_FETCH_FILE_CLUSTER_SLICE, + fetchSliceSuccess: ACTION_FETCH_FILE_CLUSTER_SLICE_SUCCESS, + fetchSliceFailure: ACTION_FETCH_FILE_CLUSTER_SLICE_FAILURE, + initialState: initialState, + resourceName: "matches", +}); + +export default function fileClusterReducer(state = initialState, action) { + switch (action.type) { + case ACTION_UPDATE_FILE_CLUSTER_PARAMS: + return { + ...defaultReducer(state, action), + files: {}, + }; + case ACTION_FETCH_FILE_CLUSTER_SLICE_SUCCESS: + return { + ...defaultReducer(state, action), + files: extendEntityMap(state.files, action.files), + }; + default: + return defaultReducer(state, action); + } +} diff --git a/web/src/collection/state/fileCluster/sagas.js b/web/src/collection/state/fileCluster/sagas.js new file mode 100644 index 00000000..fffe96aa --- /dev/null +++ b/web/src/collection/state/fileCluster/sagas.js @@ -0,0 +1,33 @@ +import { + ACTION_FETCH_FILE_CLUSTER_SLICE, + fetchFileClusterSliceFailure, + fetchFileClusterSliceSuccess, +} from "./actions"; +import { takeLatest } from "redux-saga/effects"; +import fetchEntitiesSaga from "../fetchEntities/fetchEntitiesSaga"; + +/** + * Fetch the next slice of file cluster items. + */ +function* fetchFileClusterSliceSaga(server, selectFileCluster) { + yield* fetchEntitiesSaga({ + requestResource: [server, server.fetchFileCluster], + stateSelector: selectFileCluster, + success: fetchFileClusterSliceSuccess, + failure: fetchFileClusterSliceFailure, + resourceName: "matches", + }); +} + +/** + * Initialize collection-related sagas... + */ +export default function* fileClusterRootSaga(server, selectFileCluster) { + // Handle every slice fetch. + yield takeLatest( + ACTION_FETCH_FILE_CLUSTER_SLICE, + fetchFileClusterSliceSaga, + server, + selectFileCluster + ); +} diff --git a/web/src/collection/state/FileListType.js b/web/src/collection/state/fileList/FileListType.js similarity index 100% rename from web/src/collection/state/FileListType.js rename to web/src/collection/state/fileList/FileListType.js diff --git a/web/src/collection/state/FileSort.js b/web/src/collection/state/fileList/FileSort.js similarity index 100% rename from web/src/collection/state/FileSort.js rename to web/src/collection/state/fileList/FileSort.js diff --git a/web/src/collection/state/MatchCategory.js b/web/src/collection/state/fileList/MatchCategory.js similarity index 100% rename from web/src/collection/state/MatchCategory.js rename to web/src/collection/state/fileList/MatchCategory.js diff --git a/web/src/collection/state/fileList/actions.js b/web/src/collection/state/fileList/actions.js new file mode 100644 index 00000000..f6bccfdb --- /dev/null +++ b/web/src/collection/state/fileList/actions.js @@ -0,0 +1,49 @@ +import FileListType from "./FileListType"; + +export const ACTION_CHANGE_FILE_LIST_VIEW = "coll.CHANGE_FILE_LIST_VIEW"; + +export function changeFileListView(view) { + if (FileListType.values().indexOf(view) === -1) { + throw new Error(`Unknown file list type: ${view}`); + } + return { type: ACTION_CHANGE_FILE_LIST_VIEW, view }; +} + +export const ACTION_UPDATE_FILTERS = "coll.UPDATE_FILTERS"; + +export function updateFilters(filters) { + return { type: ACTION_UPDATE_FILTERS, filters }; +} + +export const ACTION_UPDATE_FILTERS_SUCCESS = "coll.UPDATE_FILTERS_SUCCESS"; + +export function updateFiltersSuccess(files, counts) { + return { type: ACTION_UPDATE_FILTERS_SUCCESS, files, counts }; +} + +export const ACTION_UPDATE_FILTERS_FAILURE = "coll.UPDATE_FILTERS_FAILURE"; + +export function updateFiltersFailure(error) { + return { type: ACTION_UPDATE_FILTERS_FAILURE, error }; +} + +/** + * Fetch next files page. + */ +export const ACTION_FETCH_FILES = "coll.FETCH_FILES"; + +export function fetchFiles() { + return { type: ACTION_FETCH_FILES }; +} + +export const ACTION_FETCH_FILES_SUCCESS = "coll.FETCH_FILES_SUCCESS"; + +export function fetchFilesSuccess(files, counts) { + return { type: ACTION_FETCH_FILES_SUCCESS, files, counts }; +} + +export const ACTION_FETCH_FILES_FAILURE = "coll.FETCH_FILES_FAILURE"; + +export function fetchFilesFailure(error) { + return { type: ACTION_FETCH_FILES_FAILURE, error }; +} diff --git a/web/src/collection/state/fileList/initialState.js b/web/src/collection/state/fileList/initialState.js new file mode 100644 index 00000000..5f388348 --- /dev/null +++ b/web/src/collection/state/fileList/initialState.js @@ -0,0 +1,35 @@ +import { MatchCategory } from "./MatchCategory"; +import { FileSort } from "./FileSort"; +import FileListType from "./FileListType"; + +const initialState = { + neverLoaded: true, + error: false, + loading: false, + files: [], + filters: { + query: "", + extensions: [], + length: { lower: null, upper: null }, + date: { lower: null, upper: null }, + audio: null, + exif: null, + matches: MatchCategory.all, + sort: FileSort.date, + }, + fileListType: FileListType.grid, + limit: 20, + counts: { + all: 0, + related: 0, + duplicates: 0, + unique: 0, + }, +}; + +/** + * Default file list filters. + */ +export const defaultFilters = initialState.filters; + +export default initialState; diff --git a/web/src/collection/state/fileList/reducer.js b/web/src/collection/state/fileList/reducer.js new file mode 100644 index 00000000..e81bde23 --- /dev/null +++ b/web/src/collection/state/fileList/reducer.js @@ -0,0 +1,70 @@ +import { + ACTION_CHANGE_FILE_LIST_VIEW, + ACTION_FETCH_FILES, + ACTION_FETCH_FILES_FAILURE, + ACTION_FETCH_FILES_SUCCESS, + ACTION_UPDATE_FILTERS, + ACTION_UPDATE_FILTERS_FAILURE, + ACTION_UPDATE_FILTERS_SUCCESS, +} from "./actions"; +import extendEntityList from "../helpers/extendEntityList"; +import FileListType from "./FileListType"; +import initialState from "./initialState"; + +export default function fileListReducer(state = initialState, action) { + switch (action.type) { + case ACTION_UPDATE_FILTERS: + return { + ...state, + filters: { ...state.filters, ...action.filters }, + files: [], + loading: true, + neverLoaded: false, + }; + case ACTION_UPDATE_FILTERS_SUCCESS: + return { + ...state, + files: [...action.files], + counts: { ...action.counts }, + error: false, + loading: false, + }; + case ACTION_UPDATE_FILTERS_FAILURE: + return { + ...state, + files: [], + error: true, + loading: false, + }; + case ACTION_FETCH_FILES: + return { + ...state, + loading: true, + neverLoaded: false, + }; + case ACTION_FETCH_FILES_SUCCESS: + return { + ...state, + error: false, + files: extendEntityList(state.files, action.files), + counts: { ...action.counts }, + loading: false, + }; + case ACTION_FETCH_FILES_FAILURE: + return { + ...state, + error: true, + loading: false, + }; + case ACTION_CHANGE_FILE_LIST_VIEW: + if (FileListType.values().indexOf(action.view) === -1) { + throw new Error(`Unknown file list type: ${action.view}`); + } + return { + ...state, + fileListType: action.view, + }; + default: + return state; + } +} diff --git a/web/src/collection/state/fileList/sagas.js b/web/src/collection/state/fileList/sagas.js new file mode 100644 index 00000000..8e90d16b --- /dev/null +++ b/web/src/collection/state/fileList/sagas.js @@ -0,0 +1,67 @@ +import { call, put, select, takeLatest } from "redux-saga/effects"; +import { + ACTION_FETCH_FILES, + ACTION_UPDATE_FILTERS, + fetchFilesFailure, + fetchFilesSuccess, + updateFiltersFailure, + updateFiltersSuccess, +} from "./actions"; + +function resolveReportActions(fetchAction) { + switch (fetchAction.type) { + case ACTION_UPDATE_FILTERS: + return [updateFiltersSuccess, updateFiltersFailure]; + case ACTION_FETCH_FILES: + return [fetchFilesSuccess, fetchFilesFailure]; + default: + throw new Error(`Unsupported fetch action type: ${fetchAction.type}`); + } +} + +/** + * Fetch next page of files. + */ +function* fetchFilesSaga(server, selectFileList, action) { + // Determine report-result actions + const [success, failure] = resolveReportActions(action); + + try { + // Determine current limit, offset and filters from the state + const { limit, files: loadedFiles, filters } = yield select(selectFileList); + const offset = loadedFiles.length; + + // Send request to the server + const resp = yield call([server, server.fetchFiles], { + limit, + offset, + filters, + }); + + // Handle error + if (resp.failure) { + console.error("Fetch files error", resp.error); + yield put(failure(resp.error)); + return; + } + + // Update state + const { counts, files } = resp.data; + yield put(success(files, counts)); + } catch (error) { + console.error(error); + yield put(failure(error)); + } +} + +/** + * Initialize collection-related sagas... + */ +export default function* fileListRootSaga(server, selectFileList) { + yield takeLatest( + [ACTION_UPDATE_FILTERS, ACTION_FETCH_FILES], + fetchFilesSaga, + server, + selectFileList + ); +} diff --git a/web/src/collection/state/fileMatches/actions.js b/web/src/collection/state/fileMatches/actions.js new file mode 100644 index 00000000..403ab343 --- /dev/null +++ b/web/src/collection/state/fileMatches/actions.js @@ -0,0 +1,57 @@ +/** + * "Update matches params" action type. + * @type {string} + */ +export const ACTION_UPDATE_FILE_MATCHES_PARAMS = + "coll.UPDATE_FILE_MATCHES_PARAMS"; + +/** + * Create new "Update matches params" action. + */ +export function updateFileMatchesParams(params) { + return { params, type: ACTION_UPDATE_FILE_MATCHES_PARAMS }; +} + +/** + * "Fetch the next matches slice" action type. + * @type {string} + */ +export const ACTION_FETCH_FILE_MATCHES_SLICE = "coll.FETCH_FILE_MATCHES_SLICE"; + +/** + * Create new "Fetch the next matches slice" action. + * @return {{type: string}} + */ +export function fetchFileMatchesSlice() { + return { type: ACTION_FETCH_FILE_MATCHES_SLICE }; +} + +/** + * "Success of matches slice fetching" action type. + * @type {string} + */ +export const ACTION_FETCH_FILE_MATCHES_SLICE_SUCCESS = + "coll.FETCH_FILE_MATCHES_SLICE_SUCCESS"; + +/** + * Create new "Success of matches slice fetching" action. + */ +export function fetchFileMatchesSliceSuccess({ matches, total }) { + return { matches, total, type: ACTION_FETCH_FILE_MATCHES_SLICE_SUCCESS }; +} + +/** + * "Failure of matches slice fetching" action type. + * @type {string} + */ +export const ACTION_FETCH_FILE_MATCHES_SLICE_FAILURE = + "coll.FETCH_FILE_MATCHES_SLICE_FAILURE"; + +/** + * Create new "Failure of matches slice fetching" action. + * @param error + * @return {{error: *, type: string}} + */ +export function fetchFileMatchesSliceFailure(error) { + return { error, type: ACTION_FETCH_FILE_MATCHES_SLICE_FAILURE }; +} diff --git a/web/src/collection/state/fileMatches/initialState.js b/web/src/collection/state/fileMatches/initialState.js new file mode 100644 index 00000000..aa969e49 --- /dev/null +++ b/web/src/collection/state/fileMatches/initialState.js @@ -0,0 +1,18 @@ +/** + * Initial state of the fetched matches collection. + * @type {Object} + */ +const initialState = { + params: { + fileId: undefined, + filters: {}, + fields: ["meta", "exif"], + }, + total: undefined, + error: false, + loading: false, + limit: 100, + matches: [], +}; + +export default initialState; diff --git a/web/src/collection/state/fileMatches/reducer.js b/web/src/collection/state/fileMatches/reducer.js new file mode 100644 index 00000000..ed3f983d --- /dev/null +++ b/web/src/collection/state/fileMatches/reducer.js @@ -0,0 +1,19 @@ +import { + ACTION_FETCH_FILE_MATCHES_SLICE, + ACTION_FETCH_FILE_MATCHES_SLICE_FAILURE, + ACTION_FETCH_FILE_MATCHES_SLICE_SUCCESS, + ACTION_UPDATE_FILE_MATCHES_PARAMS, +} from "./actions"; +import initialState from "./initialState"; +import makeEntityReducer from "../fetchEntities/makeEntityReducer"; + +const fileMatchesReducer = makeEntityReducer({ + updateParams: ACTION_UPDATE_FILE_MATCHES_PARAMS, + fetchSlice: ACTION_FETCH_FILE_MATCHES_SLICE, + fetchSliceSuccess: ACTION_FETCH_FILE_MATCHES_SLICE_SUCCESS, + fetchSliceFailure: ACTION_FETCH_FILE_MATCHES_SLICE_FAILURE, + initialState: initialState, + resourceName: "matches", +}); + +export default fileMatchesReducer; diff --git a/web/src/collection/state/fileMatches/sagas.js b/web/src/collection/state/fileMatches/sagas.js new file mode 100644 index 00000000..6eacd1bc --- /dev/null +++ b/web/src/collection/state/fileMatches/sagas.js @@ -0,0 +1,33 @@ +import { takeLatest } from "redux-saga/effects"; +import { + ACTION_FETCH_FILE_MATCHES_SLICE, + fetchFileMatchesSliceFailure, + fetchFileMatchesSliceSuccess, +} from "./actions"; +import fetchEntitiesSaga from "../fetchEntities/fetchEntitiesSaga"; + +/** + * Fetch the next slice of file matches. + */ +function* fetchFileMatchesSliceSaga(server, selectFileMatches) { + yield* fetchEntitiesSaga({ + requestResource: [server, server.fetchFileMatches], + stateSelector: selectFileMatches, + success: fetchFileMatchesSliceSuccess, + failure: fetchFileMatchesSliceFailure, + resourceName: "matches", + }); +} + +/** + * Initialize collection-related sagas... + */ +export default function* fileMatchRootSaga(server, selectFileMatches) { + // Handle every slice fetch. + yield takeLatest( + ACTION_FETCH_FILE_MATCHES_SLICE, + fetchFileMatchesSliceSaga, + server, + selectFileMatches + ); +} diff --git a/web/src/collection/state/helpers/extendEntityList.js b/web/src/collection/state/helpers/extendEntityList.js new file mode 100644 index 00000000..cd48c7d4 --- /dev/null +++ b/web/src/collection/state/helpers/extendEntityList.js @@ -0,0 +1,22 @@ +/** + * Get set of entity ids. + */ +function ids(entities) { + const result = new Set(); + for (let entity of entities) { + result.add(entity.id); + } + return result; +} + +/** + * Add new entities to the entity list. + * @param {Object} existing - The existing mapping that should be updated. + * @param {[{id: any}]} loaded _ The new entities that should be added. + * @returns {Object} The updated mapping. + */ +export default function extendEntityList(existing, loaded) { + const existingIds = ids(existing); + const newEntities = loaded.filter((item) => !existingIds.has(item.id)); + return [...existing, ...newEntities]; +} diff --git a/web/src/collection/state/helpers/extendEntityMap.js b/web/src/collection/state/helpers/extendEntityMap.js new file mode 100644 index 00000000..7496cb74 --- /dev/null +++ b/web/src/collection/state/helpers/extendEntityMap.js @@ -0,0 +1,11 @@ +/** + * Add new entities to the `id=>entity` mapping object. + * @param {Object} existing - The existing mapping that should be updated. + * @param {[{id: any}]} loaded _ The new entities that should be added. + * @returns {Object} The updated mapping. + */ +export default function extendEntityMap(existing, loaded) { + const result = { ...existing }; + loaded.forEach((entity) => (result[entity.id] = entity)); + return result; +} diff --git a/web/src/collection/state/index.js b/web/src/collection/state/index.js deleted file mode 100644 index d4389a93..00000000 --- a/web/src/collection/state/index.js +++ /dev/null @@ -1,13 +0,0 @@ -export { - ACTION_FETCH_FILES_FAILURE, - ACTION_FETCH_FILES, - ACTION_FETCH_FILES_SUCCESS, - ACTION_UPDATE_FILTERS, - fetchFiles, - fetchFilesFailure, - fetchFilesSuccess, - updateFilters, -} from "./actions"; -export { initialState, collRootReducer } from "./reducers"; -export { default as collRootSaga } from "./sagas"; -export { selectColl } from "./selectors"; diff --git a/web/src/collection/state/initialState.js b/web/src/collection/state/initialState.js new file mode 100644 index 00000000..70c21072 --- /dev/null +++ b/web/src/collection/state/initialState.js @@ -0,0 +1,29 @@ +import fileCacheInitialState from "./fileCache/initialState"; +import fileClusterInitialState from "./fileCluster/initialState"; +import fileMatchesInitialState from "./fileMatches/initialState"; +import fileListInitialState from "./fileList/initialState"; + +/** + * Initial State for file collection management. + */ +const initialState = { + /** + * Files loaded and displayed on the file browser page ('My Collection'). + */ + fileList: fileListInitialState, + /** + * Cached individual files with fully-loaded data. + */ + fileCache: fileCacheInitialState, + /** + * Single file neighboring cluster (closely-connected files). + */ + fileCluster: fileClusterInitialState, + /** + * Single-file's immediate matches (used in 'NN Files Matched' and 'Compare' + * pages). + */ + fileMatches: fileMatchesInitialState, +}; + +export default initialState; diff --git a/web/src/collection/state/reducers.js b/web/src/collection/state/reducers.js index 6a2f6fc7..4dcf0f00 100644 --- a/web/src/collection/state/reducers.js +++ b/web/src/collection/state/reducers.js @@ -1,327 +1,14 @@ -import { - ACTION_CACHE_FILE, - ACTION_CHANGE_FILE_LIST_VIEW, - ACTION_FETCH_FILE_CLUSTER, - ACTION_FETCH_FILE_CLUSTER_FAILURE, - ACTION_FETCH_FILE_CLUSTER_SUCCESS, - ACTION_FETCH_FILE_MATCHES, - ACTION_FETCH_FILE_MATCHES_FAILURE, - ACTION_FETCH_FILE_MATCHES_SUCCESS, - ACTION_FETCH_FILES, - ACTION_FETCH_FILES_FAILURE, - ACTION_FETCH_FILES_SUCCESS, - ACTION_UPDATE_FILE_CLUSTER_FILTERS, - ACTION_UPDATE_FILE_CLUSTER_FILTERS_FAILURE, - ACTION_UPDATE_FILE_CLUSTER_FILTERS_SUCCESS, - ACTION_UPDATE_FILE_MATCH_FILTERS, - ACTION_UPDATE_FILE_MATCH_FILTERS_FAILURE, - ACTION_UPDATE_FILE_MATCH_FILTERS_SUCCESS, - ACTION_UPDATE_FILTERS, - ACTION_UPDATE_FILTERS_FAILURE, - ACTION_UPDATE_FILTERS_SUCCESS, -} from "./actions"; -import { MatchCategory } from "./MatchCategory"; -import { FileSort } from "./FileSort"; -import FileListType from "./FileListType"; - -export const initialState = { - neverLoaded: true, - error: false, - loading: false, - files: [], - filters: { - query: "", - extensions: [], - length: { lower: null, upper: null }, - date: { lower: null, upper: null }, - audio: null, - exif: null, - matches: MatchCategory.all, - sort: FileSort.date, - }, - fileListType: FileListType.grid, - limit: 20, - counts: { - all: 0, - related: 0, - duplicates: 0, - unique: 0, - }, - /** - * File id=>file LRU cache - */ - fileCache: { - maxSize: 1000, - files: {}, - history: [], - }, - /** - * File cluster - */ - fileCluster: { - filters: { - fileId: undefined, - hops: 2, - minDistance: 0.0, - maxDistance: 1.0, - fields: ["meta", "exif"], - }, - total: undefined, - error: false, - loading: false, - limit: 100, - matches: [], - files: {}, - }, - /** - * Immediate file matches - */ - fileMatches: { - fileId: undefined, - filters: { - fields: ["meta", "exif"], - }, - total: undefined, - error: false, - loading: false, - limit: 100, - offset: 0, - matches: [], - }, -}; - -/** - * Default collection filters. - */ -export const defaultFilters = initialState.filters; - -function ids(entities) { - const result = new Set(); - for (let entity of entities) { - result.add(entity.id); - } - return result; -} - -function extendEntityList(existing, loaded) { - const existingIds = ids(existing); - const newEntities = loaded.filter((item) => !existingIds.has(item.id)); - return [...existing, ...newEntities]; -} - -function extendEntityMap(existing, loaded) { - const result = { ...existing }; - loaded.forEach((entity) => (result[entity.id] = entity)); - return result; -} - -function fileCacheReducer(state = initialState.fileCache, action) { - switch (action.type) { - case ACTION_CACHE_FILE: { - const files = { ...state.files, [action.file.id]: action.file }; - const history = [ - action.file.id, - ...state.history.filter((id) => id !== action.file.id), - ]; - if (history.length > state.maxSize) { - const evicted = history.pop(); - delete files[evicted]; - } - return { ...state, history, files }; - } - default: - return state; - } -} - -function fileMatchesReducer(state = initialState.fileMatches, action) { - switch (action.type) { - case ACTION_UPDATE_FILE_MATCH_FILTERS: - return { - ...state, - fileId: action.fileId, - filters: { ...state.filters, ...action.filters }, - matches: [], - loading: true, - error: false, - total: undefined, - }; - case ACTION_UPDATE_FILE_MATCH_FILTERS_SUCCESS: - return { - ...state, - total: action.total, - matches: [...action.matches], - error: false, - loading: false, - }; - case ACTION_UPDATE_FILE_MATCH_FILTERS_FAILURE: - return { - ...state, - matches: [], - total: undefined, - error: true, - loading: false, - }; - case ACTION_FETCH_FILE_MATCHES: - return { - ...state, - error: false, - loading: true, - }; - case ACTION_FETCH_FILE_MATCHES_SUCCESS: - return { - ...state, - total: action.total, - matches: extendEntityList(state.matches, action.matches), - error: false, - loading: false, - }; - case ACTION_FETCH_FILE_MATCHES_FAILURE: - return { - ...state, - error: true, - loading: false, - }; - default: - return state; - } -} - -function fileClusterReducer(state = initialState.fileCluster, action) { - switch (action.type) { - case ACTION_UPDATE_FILE_CLUSTER_FILTERS: - return { - ...state, - filters: { ...state.filters, ...action.filters }, - matches: [], - files: {}, - loading: true, - error: false, - total: undefined, - }; - case ACTION_UPDATE_FILE_CLUSTER_FILTERS_SUCCESS: - return { - ...state, - total: action.total, - matches: [...action.matches], - files: extendEntityMap({}, action.files), - error: false, - loading: false, - }; - case ACTION_UPDATE_FILE_CLUSTER_FILTERS_FAILURE: - return { - ...state, - matches: [], - files: {}, - total: undefined, - error: true, - loading: false, - }; - case ACTION_FETCH_FILE_CLUSTER: - return { - ...state, - error: false, - loading: true, - }; - case ACTION_FETCH_FILE_CLUSTER_SUCCESS: - return { - ...state, - total: action.total, - matches: extendEntityList(state.matches, action.matches), - files: extendEntityMap(state.files, action.files), - error: false, - loading: false, - }; - case ACTION_FETCH_FILE_CLUSTER_FAILURE: - return { - ...state, - error: true, - loading: false, - }; - default: - return state; - } -} - -export function collRootReducer(state = initialState, action) { - switch (action.type) { - case ACTION_UPDATE_FILTERS: - return { - ...state, - filters: { ...state.filters, ...action.filters }, - files: [], - loading: true, - neverLoaded: false, - }; - case ACTION_UPDATE_FILTERS_SUCCESS: - return { - ...state, - files: [...action.files], - counts: { ...action.counts }, - error: false, - loading: false, - }; - case ACTION_UPDATE_FILTERS_FAILURE: - return { - ...state, - files: [], - error: true, - loading: false, - }; - case ACTION_FETCH_FILES: - return { - ...state, - loading: true, - neverLoaded: false, - }; - case ACTION_FETCH_FILES_SUCCESS: - return { - ...state, - error: false, - files: extendEntityList(state.files, action.files), - counts: { ...action.counts }, - loading: false, - }; - case ACTION_FETCH_FILES_FAILURE: - return { - ...state, - error: true, - loading: false, - }; - case ACTION_CACHE_FILE: - return { - ...state, - fileCache: fileCacheReducer(state.fileCache, action), - }; - case ACTION_CHANGE_FILE_LIST_VIEW: - if (FileListType.values().indexOf(action.view) === -1) { - throw new Error(`Unknown file list type: ${action.view}`); - } - return { - ...state, - fileListType: action.view, - }; - case ACTION_UPDATE_FILE_MATCH_FILTERS: - case ACTION_UPDATE_FILE_MATCH_FILTERS_SUCCESS: - case ACTION_UPDATE_FILE_MATCH_FILTERS_FAILURE: - case ACTION_FETCH_FILE_MATCHES: - case ACTION_FETCH_FILE_MATCHES_SUCCESS: - case ACTION_FETCH_FILE_MATCHES_FAILURE: - return { - ...state, - fileMatches: fileMatchesReducer(state.fileMatches, action), - }; - case ACTION_UPDATE_FILE_CLUSTER_FILTERS: - case ACTION_UPDATE_FILE_CLUSTER_FILTERS_SUCCESS: - case ACTION_UPDATE_FILE_CLUSTER_FILTERS_FAILURE: - case ACTION_FETCH_FILE_CLUSTER: - case ACTION_FETCH_FILE_CLUSTER_SUCCESS: - case ACTION_FETCH_FILE_CLUSTER_FAILURE: - return { - ...state, - fileCluster: fileClusterReducer(state.fileCluster, action), - }; - default: - return state; - } -} +import fileCacheReducer from "./fileCache/reducer"; +import fileMatchesReducer from "./fileMatches/reducer"; +import fileClusterReducer from "./fileCluster/reducer"; +import { combineReducers } from "redux"; +import fileListReducer from "./fileList/reducer"; + +const collRootReducer = combineReducers({ + fileList: fileListReducer, + fileCache: fileCacheReducer, + fileCluster: fileClusterReducer, + fileMatches: fileMatchesReducer, +}); + +export default collRootReducer; diff --git a/web/src/collection/state/sagas.js b/web/src/collection/state/sagas.js index 9361e045..949a3ae2 100644 --- a/web/src/collection/state/sagas.js +++ b/web/src/collection/state/sagas.js @@ -1,192 +1,18 @@ -import { call, put, select, takeLatest } from "redux-saga/effects"; +import { fork } from "redux-saga/effects"; import { - ACTION_FETCH_FILE_CLUSTER, - ACTION_FETCH_FILE_MATCHES, - ACTION_FETCH_FILES, - ACTION_UPDATE_FILE_CLUSTER_FILTERS, - ACTION_UPDATE_FILE_MATCH_FILTERS, - ACTION_UPDATE_FILTERS, - fetchFileClusterFailure, - fetchFileClusterSuccess, - fetchFileMatchesFailure, - fetchFileMatchesSuccess, - fetchFilesFailure, - fetchFilesSuccess, - updateFileClusterFiltersFailure, - updateFileClusterFiltersSuccess, - updateFileMatchFiltersFailure, - updateFileMatchFiltersSuccess, - updateFiltersFailure, - updateFiltersSuccess, -} from "./actions"; -import { selectColl, selectFileCluster, selectFileMatches } from "./selectors"; - -function* updateFileMatchesFiltersSaga(server, action) { - yield* fetchFileMatchesSaga( - server, - action, - updateFileMatchFiltersSuccess, - updateFileMatchFiltersFailure - ); -} - -function* fetchFileMatchesPageSaga(server, action) { - yield* fetchFileMatchesSaga( - server, - action, - fetchFileMatchesSuccess, - fetchFileMatchesFailure - ); -} - -function* fetchFileMatchesSaga(server, action, success, failure) { - try { - // Determine current query params - const { limit, filters, matches: current } = yield select( - selectFileMatches - ); - - // Send request to the server - const resp = yield call([server, server.fetchFileMatches], { - limit, - offset: current.length, - id: filters.fileId, - filters, - }); - - // Handle error - if (resp.failure) { - console.error("Fetch file matches error", resp.error); - yield put(failure(resp.error)); - return; - } - - // Update state - const { total, matches, files } = resp.data; - yield put(success(matches, files, total)); - } catch (error) { - console.error(error); - yield put(failure(error)); - } -} - -function* updateFileClusterFiltersSaga(server, action) { - yield* fetchFileClusterSaga( - server, - action, - updateFileClusterFiltersSuccess, - updateFileClusterFiltersFailure - ); -} - -function* fetchFileClusterPageSaga(server, action) { - yield* fetchFileClusterSaga( - server, - action, - fetchFileClusterSuccess, - fetchFileClusterFailure - ); -} - -function* fetchFileClusterSaga(server, action, success, failure) { - try { - // Determine current query params - const { limit, filters, matches: current } = yield select( - selectFileCluster - ); - - // Send request to the server - const resp = yield call([server, server.fetchFileCluster], { - limit, - offset: current.length, - id: filters.fileId, - fields: filters.fields, - filters, - }); - - // Handle error - if (resp.failure) { - console.error("Fetch file matches error", resp.error); - yield put(failure(resp.error)); - return; - } - - // Update state - const { total, matches, files } = resp.data; - yield put(success(matches, files, total)); - } catch (error) { - console.error(error); - yield put(failure(error)); - } -} - -function resolveReportActions(fetchAction) { - switch (fetchAction.type) { - case ACTION_UPDATE_FILTERS: - return [updateFiltersSuccess, updateFiltersFailure]; - case ACTION_FETCH_FILES: - return [fetchFilesSuccess, fetchFilesFailure]; - default: - throw new Error(`Unsupported fetch action type: ${fetchAction.type}`); - } -} - -/** - * Fetch next page of files. - */ -function* fetchFilesSaga(server, action) { - // Determine report-result actions - const [success, failure] = resolveReportActions(action); - - try { - // Determine current limit, offset and filters from the state - const { limit, files: loadedFiles, filters } = yield select(selectColl); - const offset = loadedFiles.length; - - // Send request to the server - const resp = yield call([server, server.fetchFiles], { - limit, - offset, - filters, - }); - - // Handle error - if (resp.failure) { - console.error("Fetch files error", resp.error); - yield put(failure(resp.error)); - return; - } - - // Update state - const { counts, files } = resp.data; - yield put(success(files, counts)); - } catch (error) { - console.error(error); - yield put(failure(error)); - } -} + selectFileCluster, + selectFileList, + selectFileMatches, +} from "./selectors"; +import fileMatchRootSaga from "./fileMatches/sagas"; +import fileClusterRootSaga from "./fileCluster/sagas"; +import fileListRootSaga from "./fileList/sagas"; /** * Initialize collection-related sagas... */ export default function* collRootSaga(server) { - console.log("coll root saga"); - yield takeLatest( - [ACTION_UPDATE_FILTERS, ACTION_FETCH_FILES], - fetchFilesSaga, - server - ); - yield takeLatest( - ACTION_UPDATE_FILE_CLUSTER_FILTERS, - updateFileClusterFiltersSaga, - server - ); - yield takeLatest(ACTION_FETCH_FILE_CLUSTER, fetchFileClusterPageSaga, server); - - yield takeLatest( - ACTION_UPDATE_FILE_MATCH_FILTERS, - updateFileMatchesFiltersSaga, - server - ); - yield takeLatest(ACTION_FETCH_FILE_MATCHES, fetchFileMatchesPageSaga, server); + yield fork(fileListRootSaga, server, selectFileList); + yield fork(fileMatchRootSaga, server, selectFileMatches); + yield fork(fileClusterRootSaga, server, selectFileCluster); } diff --git a/web/src/collection/state/selectors.js b/web/src/collection/state/selectors.js index ed9a2882..652ec379 100644 --- a/web/src/collection/state/selectors.js +++ b/web/src/collection/state/selectors.js @@ -3,15 +3,17 @@ */ export const selectColl = (state) => state.coll; -export const selectFiles = (state) => selectColl(state).files; +export const selectFileList = (state) => selectColl(state).fileList; -export const selectFilters = (state) => selectColl(state).filters; +export const selectFiles = (state) => selectFileList(state).files; -export const selectCounts = (state) => selectColl(state).counts; +export const selectFileFilters = (state) => selectFileList(state).filters; -export const selectLoading = (state) => selectColl(state).loading; +export const selectFileCounts = (state) => selectFileList(state).counts; -export const selectError = (state) => selectColl(state).error; +export const selectFileLoading = (state) => selectFileList(state).loading; + +export const selectFileError = (state) => selectFileList(state).error; /** * Select cached file by id. diff --git a/web/src/server-api/Server/Server.js b/web/src/server-api/Server/Server.js index 8da4efbd..7056bf3f 100644 --- a/web/src/server-api/Server/Server.js +++ b/web/src/server-api/Server/Server.js @@ -47,9 +47,15 @@ export default class Server { } } - async fetchFileCluster({ id, limit = 20, offset = 0, fields = [], filters }) { + async fetchFileCluster({ + fileId, + limit = 20, + offset = 0, + fields = [], + filters, + }) { try { - const response = await this.axios.get(`/files/${id}/cluster`, { + const response = await this.axios.get(`/files/${fileId}/cluster`, { params: { limit, offset, @@ -64,14 +70,14 @@ export default class Server { } async fetchFileMatches({ - id, + fileId, limit = 20, offset = 0, fields = ["meta", "exif", "scenes"], filters = {}, }) { try { - const response = await this.axios.get(`/files/${id}/matches`, { + const response = await this.axios.get(`/files/${fileId}/matches`, { params: { limit, offset,