From 20e3b499cfd155e1eadfe54367fa90f3d51cf61c Mon Sep 17 00:00:00 2001 From: TS Date: Mon, 11 May 2020 22:27:07 -0700 Subject: [PATCH] =?UTF-8?q?Refact:=20=E5=88=86=E7=A6=BBviewe=20update=20re?= =?UTF-8?q?ducer;=20=E6=B7=BB=E5=8A=A0typescript=20(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 分离vieweupdate reducer; 添加typescript * Fix: Dispatch navigator actions * fix: dispatch change sub title --- .gitignore | 2 + package.json | 5 + src/App.js | 2 +- src/actions/index.js | 35 ++- src/component/Admin/Dashboard.js | 2 +- src/component/FileManager/FileManager.js | 3 +- src/component/Share/SharePreload.js | 3 +- src/component/Viewer/Code.js | 3 +- src/component/Viewer/Doc.js | 3 +- src/component/Viewer/PDF.js | 3 +- src/component/Viewer/Text.js | 3 +- src/component/Viewer/Video.js | 2 +- src/middleware/{Auth.js => Auth.ts} | 16 +- src/middleware/Init.js | 2 - src/reducers/index.js | 323 +---------------------- src/reducers/index.test.js | 43 ++- src/reducers/view.js | 38 --- src/redux/combineReducers.ts | 16 ++ src/redux/viewUpdate/action.ts | 18 ++ src/redux/viewUpdate/index.ts | 7 + src/redux/viewUpdate/reducer.ts | 309 ++++++++++++++++++++++ tsconfig.json | 11 + yarn.lock | 29 ++ 23 files changed, 483 insertions(+), 395 deletions(-) rename src/middleware/{Auth.js => Auth.ts} (85%) delete mode 100644 src/reducers/view.js create mode 100644 src/redux/combineReducers.ts create mode 100644 src/redux/viewUpdate/action.ts create mode 100644 src/redux/viewUpdate/index.ts create mode 100644 src/redux/viewUpdate/reducer.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 4d29575..911e1ee 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +dist/ \ No newline at end of file diff --git a/package.json b/package.json index 0af0a16..a2321bb 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "@material-ui/icons": "^4.5.1", "@material-ui/lab": "^4.0.0-alpha.42", "@svgr/webpack": "4.3.2", + "@types/invariant": "^2.2.32", + "@types/react-dom": "^16.9.7", "@typescript-eslint/eslint-plugin": "^2.2.0", "@typescript-eslint/parser": "^2.2.0", "axios": "^0.19.0", @@ -37,6 +39,7 @@ "http-proxy-middleware": "^0.20.0", "husky": "^4.2.5", "identity-obj-proxy": "3.0.0", + "invariant": "^2.2.4", "is-wsl": "^1.1.0", "jest": "24.9.0", "jest-environment-jsdom-fourteen": "0.1.0", @@ -74,6 +77,7 @@ "react-router-dom": "^5.1.2", "recharts": "^2.0.0-beta.1", "redux": "^4.0.4", + "redux-mock-store": "^1.5.4", "redux-thunk": "^2.3.0", "resolve": "1.12.0", "resolve-url-loader": "3.1.0", @@ -83,6 +87,7 @@ "terser-webpack-plugin": "1.4.1", "timeago-react": "^3.0.0", "ts-pnp": "1.1.4", + "typescript": "^3.8.3", "url-loader": "2.1.0", "webpack": "4.41.0", "webpack-dev-server": "3.2.1", diff --git a/src/App.js b/src/App.js index ef1238b..3a5f5b0 100644 --- a/src/App.js +++ b/src/App.js @@ -71,7 +71,7 @@ export default function App() { }, content: { flexGrow: 1, - padding: theme.spacing.unit * 0, + padding: theme.spacing(0), minWidth: 0 }, toolbar: theme.mixins.toolbar diff --git a/src/actions/index.js b/src/actions/index.js index c910c1f..12e9b7d 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -1,16 +1,30 @@ export * from './explorer' export const navigateTo = path => { - return { - type: "NAVIGATOR_TO", - path: path - }; + return (dispatch, getState) => { + const state = getState() + const navigatorLoading = path !== state.navigator.path + return dispatch({ + type: "NAVIGATOR_TO", + path: path, + navigatorLoading, + }) + } }; export const navigateUp = () => { - return { - type: "NAVIGATOR_UP" - }; + return (dispatch, getState) => { + const state = getState() + const pathSplit = state.navigator.path.split("/"); + pathSplit.pop(); + const newPath = pathSplit.length===1? "/":pathSplit.join("/"); + const navigatorLoading = newPath !== state.navigator.path + return dispatch({ + type: "NAVIGATOR_UP", + path: newPath, + navigatorLoading + }) + } }; export const drawerToggleAction = open => { @@ -35,13 +49,6 @@ export const changeViewMethod = method => { }; }; -export const changeSubTitle = title =>{ - return { - type: "CHANGE_SUB_TITLE", - title: title - }; -}; - export const toggleDaylightMode = ()=>{ return { type: "TOGGLE_DAYLIGHT_MODE", diff --git a/src/component/Admin/Dashboard.js b/src/component/Admin/Dashboard.js index cc09320..3dc0a1a 100644 --- a/src/component/Admin/Dashboard.js +++ b/src/component/Admin/Dashboard.js @@ -22,7 +22,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { useDispatch } from "react-redux"; import { useHistory, useLocation } from "react-router"; import { useRouteMatch } from "react-router-dom"; -import { changeSubTitle } from "../../actions"; +import { changeSubTitle } from "../../redux/viewUpdate/action"; import pathHelper from "../../utils/page"; import UserAvatar from "../Navbar/UserAvatar"; diff --git a/src/component/FileManager/FileManager.js b/src/component/FileManager/FileManager.js index 312faff..18e1b97 100644 --- a/src/component/FileManager/FileManager.js +++ b/src/component/FileManager/FileManager.js @@ -3,7 +3,8 @@ import { DndProvider } from 'react-dnd' import HTML5Backend from 'react-dnd-html5-backend' import { connect } from "react-redux" import { withRouter } from "react-router-dom" -import { changeSubTitle, closeAllModals, navigateTo, setSelectedTarget, toggleSnackbar } from "../../actions" +import { closeAllModals, navigateTo, setSelectedTarget, toggleSnackbar } from "../../actions" +import { changeSubTitle } from "../../redux/viewUpdate/action"; import pathHelper from "../../utils/page" import DragLayer from "./DnD/DragLayer" import Explorer from "./Explorer" diff --git a/src/component/Share/SharePreload.js b/src/component/Share/SharePreload.js index 78b9db1..e28a787 100644 --- a/src/component/Share/SharePreload.js +++ b/src/component/Share/SharePreload.js @@ -2,7 +2,8 @@ import React, { Suspense, useCallback, useEffect, useState } from "react"; import PageLoading from "../Placeholder/PageLoading"; import { useParams } from "react-router"; import API from "../../middleware/Api"; -import { changeSubTitle, toggleSnackbar } from "../../actions"; +import { toggleSnackbar } from "../../actions"; +import { changeSubTitle } from "../../redux/viewUpdate/action"; import { useDispatch } from "react-redux"; import Notice from "./NotFound"; import LockedFile from "./LockedFile"; diff --git a/src/component/Viewer/Code.js b/src/component/Viewer/Code.js index 4575fda..0cfa2cc 100644 --- a/src/component/Viewer/Code.js +++ b/src/component/Viewer/Code.js @@ -4,7 +4,8 @@ import { makeStyles } from "@material-ui/core/styles"; import { useLocation, useParams, useRouteMatch } from "react-router"; import API from "../../middleware/Api"; import { useDispatch } from "react-redux"; -import { changeSubTitle, toggleSnackbar } from "../../actions"; +import { toggleSnackbar } from "../../actions"; +import { changeSubTitle } from "../../redux/viewUpdate/action"; import pathHelper from "../../utils/page"; import SaveButton from "../Dial/Save"; import { codePreviewSuffix } from "../../config"; diff --git a/src/component/Viewer/Doc.js b/src/component/Viewer/Doc.js index 3f381a4..7e2b119 100644 --- a/src/component/Viewer/Doc.js +++ b/src/component/Viewer/Doc.js @@ -3,7 +3,8 @@ import { makeStyles } from "@material-ui/core/styles"; import {useLocation, useParams, useRouteMatch} from "react-router"; import API from "../../middleware/Api"; import {useDispatch} from "react-redux"; -import {changeSubTitle, toggleSnackbar} from "../../actions"; +import {toggleSnackbar} from "../../actions"; +import { changeSubTitle } from "../../redux/viewUpdate/action"; import pathHelper from "../../utils/page"; const useStyles = makeStyles(() => ({ diff --git a/src/component/Viewer/PDF.js b/src/component/Viewer/PDF.js index 5bd280a..116003c 100644 --- a/src/component/Viewer/PDF.js +++ b/src/component/Viewer/PDF.js @@ -5,7 +5,8 @@ import { makeStyles } from "@material-ui/core/styles"; import { useLocation, useParams, useRouteMatch } from "react-router"; import { getBaseURL } from "../../middleware/Api"; import { useDispatch } from "react-redux"; -import { changeSubTitle, toggleSnackbar } from "../../actions"; +import { toggleSnackbar } from "../../actions"; +import { changeSubTitle } from "../../redux/viewUpdate/action"; import pathHelper from "../../utils/page"; import TextLoading from "../Placeholder/TextLoading"; pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`; diff --git a/src/component/Viewer/Text.js b/src/component/Viewer/Text.js index b8745fe..b6aaa55 100644 --- a/src/component/Viewer/Text.js +++ b/src/component/Viewer/Text.js @@ -4,7 +4,8 @@ import { makeStyles } from "@material-ui/core/styles"; import {useLocation, useParams, useRouteMatch} from "react-router"; import API from "../../middleware/Api"; import { useDispatch } from "react-redux"; -import { changeSubTitle, toggleSnackbar } from "../../actions"; +import { toggleSnackbar } from "../../actions"; +import { changeSubTitle } from "../../redux/viewUpdate/action"; import Editor from 'for-editor' import SaveButton from "../Dial/Save"; import pathHelper from "../../utils/page"; diff --git a/src/component/Viewer/Video.js b/src/component/Viewer/Video.js index e27816d..069c93e 100644 --- a/src/component/Viewer/Video.js +++ b/src/component/Viewer/Video.js @@ -5,7 +5,7 @@ import { makeStyles } from "@material-ui/core/styles"; import { useLocation, useParams, useRouteMatch } from "react-router"; import { getBaseURL } from "../../middleware/Api"; import { useDispatch } from "react-redux"; -import { changeSubTitle } from "../../actions"; +import { changeSubTitle } from "../../redux/viewUpdate/action"; import pathHelper from "../../utils/page"; const useStyles = makeStyles(theme => ({ diff --git a/src/middleware/Auth.js b/src/middleware/Auth.ts similarity index 85% rename from src/middleware/Auth.js rename to src/middleware/Auth.ts index b552b79..0659640 100644 --- a/src/middleware/Auth.js +++ b/src/middleware/Auth.ts @@ -1,19 +1,16 @@ const Auth = { isAuthenticated: false, - authenticate(cb) { + authenticate(cb: any) { Auth.SetUser(cb); Auth.isAuthenticated = true; }, GetUser(){ return JSON.parse(localStorage.getItem("user")) }, - SetUser(newUser){ + SetUser(newUser: any){ localStorage.setItem("user", JSON.stringify(newUser)); }, - /** - * @return {boolean} - */ - Check() { + Check(): boolean { if (Auth.isAuthenticated) { return true; } @@ -29,16 +26,13 @@ const Auth = { oldUser.id = 0; localStorage.setItem("user", JSON.stringify(oldUser)); }, - SetPreference(key,value){ + SetPreference(key: string,value: any){ let preference = JSON.parse(localStorage.getItem("user_preference")); preference = (preference == null) ? {} : preference; preference[key] = value; localStorage.setItem("user_preference", JSON.stringify(preference)); }, - /** - * @return {null} - */ - GetPreference(key){ + GetPreference(key: string): any | null{ let preference = JSON.parse(localStorage.getItem("user_preference")); if (preference && preference[key]){ return preference[key]; diff --git a/src/middleware/Init.js b/src/middleware/Init.js index c01da74..bf4957b 100644 --- a/src/middleware/Init.js +++ b/src/middleware/Init.js @@ -15,8 +15,6 @@ export var InitSiteConfig = (rawStore) => { rawStore.navigator.path = c===null?"/":c; // 初始化用户个性配置 rawStore.siteConfig = initUserConfig(rawStore.siteConfig); - // 是否登录 - rawStore.viewUpdate.isLogin = Auth.Check(); // 更改站点标题 document.title = rawStore.siteConfig.title; diff --git a/src/reducers/index.js b/src/reducers/index.js index 2a9c6e7..38687b2 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,5 +1,7 @@ /* eslint-disable no-case-declarations */ import { InitSiteConfig } from "../middleware/Init"; +import { combineReducers } from '../redux/combineReducers' +import viewUpdate from '../redux/viewUpdate/reducer' const checkSelectedProps = (state)=>{ let isMultiple,withFolder,withFile=false; @@ -15,17 +17,12 @@ const checkSelectedProps = (state)=>{ return [isMultiple,withFolder,withFile]; } -const doNavigate = (path,state)=>{ +const doNavigate = (path, state)=>{ window.currntPath = path; return Object.assign({}, state, { navigator:Object.assign({}, state.navigator, { path: path }), - viewUpdate:Object.assign({}, state.viewUpdate, { - contextOpen:false, - navigatorError:false, - navigatorLoading:path !== state.navigator.path, - }), explorer:Object.assign({}, state.explorer, { selected:[], selectProps: { @@ -92,46 +89,6 @@ export const initState = { path: "/", refresh: true }, - viewUpdate: { - isLogin:false, - loadUploader:false, - open: false, - explorerViewMethod: "icon", - sortMethod: "timePos", - subTitle:null, - contextType: "none", - menuOpen: false, - navigatorLoading: true, - navigatorError: false, - navigatorErrorMsg: null, - modalsLoading: false, - storageRefresh: false, - userPopoverAnchorEl: null, - shareUserPopoverAnchorEl: null, - modals: { - createNewFolder: false, - createNewFile: false, - rename: false, - move: false, - remove: false, - share: false, - music: false, - remoteDownload: false, - torrentDownload: false, - getSource: false, - copy:false, - resave: false, - compress:false, - decompress:false, - }, - snackbar: { - toggle: false, - vertical: "top", - horizontal: "center", - msg: "", - color: "" - } - }, explorer: { dndSignal:false, dndTarget:null, @@ -162,18 +119,6 @@ const defaultStatus = InitSiteConfig(initState); // TODO: 将cloureveApp切分成小的reducer const cloudreveApp = (state = defaultStatus, action) => { switch (action.type) { - case 'DRAWER_TOGGLE': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - open:action.open, - }), - }); - case 'CHANGE_VIEW_METHOD': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - explorerViewMethod:action.method, - }), - }); case 'CHANGE_SORT_METHOD': let list = [...state.explorer.fileList,...state.explorer.dirList]; // eslint-disable-next-line @@ -203,24 +148,11 @@ const cloudreveApp = (state = defaultStatus, action) => { }); return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - sortMethod:action.method, - }), explorer: Object.assign({}, state.explorer, { fileList: fileList, dirList: dirList, }), }); - case 'CHANGE_CONTEXT_MENU': - if(state.viewUpdate.contextOpen && action.open){ - return Object.assign({}, state); - } - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - contextOpen: action.open, - contextType:action.menuType, - }), - }); case 'DRAG_AND_DROP': return Object.assign({}, state, { explorer: Object.assign({}, state.explorer, { @@ -229,19 +161,6 @@ const cloudreveApp = (state = defaultStatus, action) => { dndSource:action.source, }), }); - case 'SET_NAVIGATOR_LOADING_STATUE': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - navigatorLoading:action.status, - }), - }); - case 'SET_NAVIGATOR_ERROR': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - navigatorError: action.status, - navigatorErrorMsg: action.msg, - }), - }); case 'UPDATE_FILE_LIST': // eslint-disable-next-line action.list.sort((a,b)=>{ @@ -379,224 +298,11 @@ const cloudreveApp = (state = defaultStatus, action) => { } break case 'NAVIGATOR_UP': - var pathSplit = state.navigator.path.split("/"); - pathSplit.pop(); - var newPath = pathSplit.length===1?"/":pathSplit.join("/"); - return doNavigate(newPath,state); - case 'OPEN_CREATE_FOLDER_DIALOG': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - createNewFolder:true, - }), - contextOpen:false, - }), - }); - case 'OPEN_CREATE_FILE_DIALOG': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - createNewFile:true, - }), - contextOpen:false, - }), - }); - case 'OPEN_RENAME_DIALOG': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - rename:true, - }), - contextOpen:false, - }), - }); - case 'OPEN_REMOVE_DIALOG': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - remove:true, - }), - contextOpen:false, - }), - }); - case 'OPEN_MOVE_DIALOG': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - move:true, - }), - contextOpen:false, - }), - }); - case 'OPEN_RESAVE_DIALOG': - window.shareKey = action.key; - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - resave:true, - }), - contextOpen:false, - }), - }); - case 'SET_USER_POPOVER': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - userPopoverAnchorEl:action.anchor, - }), - }); - case 'SET_SHARE_USER_POPOVER': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - shareUserPopoverAnchorEl:action.anchor, - }), - }); - case 'OPEN_SHARE_DIALOG': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - share:true, - }), - contextOpen:false, - }), - }); + return doNavigate(action.path, state); case 'SET_SITE_CONFIG': return Object.assign({}, state, { siteConfig: action.config, }); - case 'OPEN_MUSIC_DIALOG': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - music:true, - }), - contextOpen:false, - }), - }); - case 'OPEN_REMOTE_DOWNLOAD_DIALOG': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - remoteDownload:true, - }), - contextOpen:false, - }), - }); - case 'OPEN_TORRENT_DOWNLOAD_DIALOG': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - torrentDownload:true, - }), - contextOpen:false, - }), - }); - case 'OPEN_DECOMPRESS_DIALOG': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - decompress:true, - }), - contextOpen:false, - }), - }); - case 'OPEN_COMPRESS_DIALOG': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - compress:true, - }), - contextOpen:false, - }), - }); - case 'OPEN_GET_SOURCE_DIALOG': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - getSource:true, - }), - contextOpen:false, - }), - }); - case 'OPEN_COPY_DIALOG': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - copy:true, - }), - contextOpen:false, - }), - }); - case 'OPEN_LOADING_DIALOG': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - loading:true, - loadingText:action.text, - }), - contextOpen:false, - }), - }); - case 'CLOSE_ALL_MODALS': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modals: Object.assign({}, state.viewUpdate.modals, { - createNewFolder:false, - createNewFile:false, - rename:false, - move:false, - remove:false, - share:false, - music:false, - remoteDownload:false, - torrentDownload:false, - getSource:false, - resave:false, - copy:false, - loading:false, - compress:false, - decompress:false, - }), - }), - }); - case 'CHANGE_SUB_TITLE': - document.title = (action.title === null || action.title === undefined) ? state.siteConfig.title : (action.title + " - " +state.siteConfig.title); - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - subTitle: action.title, - }), - }); - case 'TOGGLE_SNACKBAR': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - snackbar:{ - toggle:!state.viewUpdate.snackbar.toggle, - vertical:action.vertical, - horizontal:action.horizontal, - msg:action.msg, - color:action.color, - }, - }), - }); - case 'SET_MODALS_LOADING': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - modalsLoading:action.status, - }), - }); - case 'SET_SESSION_STATUS': - return { - ...state, - viewUpdate: { - ...state.viewUpdate, - isLogin:action.status, - } - } - case 'ENABLE_LOAD_UPLOADER': - return Object.assign({}, state, { - viewUpdate: Object.assign({}, state.viewUpdate, { - loadUploader:true, - }), - }); case 'REFRESH_FILE_LIST': return Object.assign({}, state, { navigator: Object.assign({}, state.navigator, { @@ -617,11 +323,6 @@ const cloudreveApp = (state = defaultStatus, action) => { path: "/搜索结果", refresh:state.explorer.keywords === null? state.navigator.refresh:!state.navigator.refresh, }), - viewUpdate:Object.assign({}, state.viewUpdate, { - contextOpen:false, - navigatorError:false, - navigatorLoading:true, - }), explorer:Object.assign({}, state.explorer, { selected:[], selectProps: { @@ -641,12 +342,6 @@ const cloudreveApp = (state = defaultStatus, action) => { }, }), }); - case 'REFRESH_STORAGE': - return Object.assign({}, state, { - viewUpdate:Object.assign({}, state.viewUpdate, { - storageRefresh:!state.viewUpdate.storageRefresh, - }), - }); case 'SAVE_FILE': return Object.assign({}, state, { explorer:Object.assign({}, state.explorer, { @@ -679,4 +374,12 @@ const cloudreveApp = (state = defaultStatus, action) => { } } -export default cloudreveApp \ No newline at end of file +export default (state, action) => { + const { viewUpdate: viewUpdateState } = state || {} + const appState = cloudreveApp(state, action) + const combinedState = combineReducers({ viewUpdate })({ viewUpdate: viewUpdateState }, action) + return { + ...appState, + ...combinedState, + } +} \ No newline at end of file diff --git a/src/reducers/index.test.js b/src/reducers/index.test.js index dc7afd6..142a996 100644 --- a/src/reducers/index.test.js +++ b/src/reducers/index.test.js @@ -1,4 +1,8 @@ -import cloudreveApp, { initState } from './index' +import configureMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import cloudreveApp, { initState as cloudreveState } from './index' +import { initState as viewUpdateState } from '../redux/viewUpdate/reducer' + import { setModalsLoading, openLoadingDialog, openGetSourceDialog, openShareDialog, openMoveDialog, navigateUp, navigateTo, drawerToggleAction, changeViewMethod, changeSortMethod, @@ -10,13 +14,23 @@ import { setModalsLoading, openLoadingDialog, openGetSourceDialog, setShareUserPopover, setSiteConfig, openMusicDialog, openRemoteDownloadDialog, openTorrentDownloadDialog, openDecompressDialog, openCompressDialog, openCopyDialog, - closeAllModals, changeSubTitle, toggleSnackbar, setSessionStatus, - enableLoadUploader, refreshFileList, searchMyFile, showImgPreivew, refreshStorage, saveFile, setLastSelect, setShiftSelectedIds + closeAllModals, toggleSnackbar, setSessionStatus, + enableLoadUploader, refreshFileList, searchMyFile, showImgPreivew, + refreshStorage, saveFile, setLastSelect, setShiftSelectedIds } from '../actions/index' +import { changeSubTitle } from "../redux/viewUpdate/action" + + +const initState = { + ...cloudreveState, + viewUpdate: viewUpdateState, +} +const middlewares = [thunk] +const mockStore = configureMockStore(middlewares) describe('index reducer', () => { it('should return the initial state', () => { - expect(cloudreveApp(undefined, {})).toEqual(initState) + expect(cloudreveApp(undefined, { type: '@@INIT'})).toEqual(initState) }) it('should handle DRAWER_TOGGLE', () => { @@ -593,9 +607,11 @@ describe('index reducer', () => { }) }) - it('should handle NAVIGATOR_TO', () => { + it('should handle NAVIGATOR_TO', async () => { + const store = mockStore(initState) const action = navigateTo('/somewhere') - expect(cloudreveApp(initState, action)).toEqual({ + const navAction = await store.dispatch(action) + expect(cloudreveApp(initState, navAction)).toEqual({ ...initState, navigator: { ...initState.navigator, @@ -621,8 +637,7 @@ describe('index reducer', () => { expect(window.currntPath).toEqual('/somewhere') }) - it('should handle NAVIGATOR_UP', () => { - const action = navigateUp('somewhere') + it('should handle NAVIGATOR_UP', async () => { const navState = { ...initState, navigator: { @@ -630,7 +645,10 @@ describe('index reducer', () => { path: '/to/somewhere' } } - expect(cloudreveApp(navState, action)).toEqual({ + const store = mockStore(navState) + const action = navigateUp('somewhere') + const navAction = await store.dispatch(action) + expect(cloudreveApp(navState, navAction)).toEqual({ ...initState, navigator: { ...initState.navigator, @@ -993,15 +1011,18 @@ describe('index reducer', () => { }) }) - it('should handle CHANGE_SUB_TITLE', () => { + it('should handle CHANGE_SUB_TITLE', async () => { + const store = mockStore(initState) const action = changeSubTitle('test sub title') - expect(cloudreveApp(initState, action)).toEqual({ + const changeSubtitleAction = await store.dispatch(action) + expect(cloudreveApp(initState, changeSubtitleAction)).toEqual({ ...initState, viewUpdate: { ...initState.viewUpdate, subTitle: 'test sub title', } }) + expect(document.title).toEqual('test sub title - Cloudreve') }) it('should handle TOGGLE_SNACKBAR', () => { diff --git a/src/reducers/view.js b/src/reducers/view.js deleted file mode 100644 index a52c4fa..0000000 --- a/src/reducers/view.js +++ /dev/null @@ -1,38 +0,0 @@ -const viewUpdate = (state = [], action) => { - switch (action.type) { - case 'DRAWER_TOGGLE': - return Object.assign({}, state, { - open: action.open - }); - case 'CHANGE_VIEW_METHOD': - return Object.assign({}, state, { - explorerViewMethod: action.method - - }); - case 'CHANGE_SORT_METHOD': - return Object.assign({}, state, { - sortMethod: action.method - }); - case 'CHANGE_CONTEXT_MENU': - if(state.contextOpen && action.open){ - return Object.assign({}, state); - } - return Object.assign({}, state, { - contextType: action.menuType, - contextOpen: action.open, - }); - case 'SET_NAVIGATOR_LOADING_STATUE': - return Object.assign({}, state, { - navigatorLoading: action.status - }); - case 'SET_NAVIGATOR_ERROR': - return Object.assign({}, state, { - navigatorError: action.status, - navigatorErrorMsg: action.msg, - }); - default: - return state - } - } - - export default viewUpdate \ No newline at end of file diff --git a/src/redux/combineReducers.ts b/src/redux/combineReducers.ts new file mode 100644 index 0000000..e9db1d5 --- /dev/null +++ b/src/redux/combineReducers.ts @@ -0,0 +1,16 @@ +import { combineReducers as combine, ReducersMapObject, AnyAction } from 'redux' +import invariant from 'invariant' + +export const combineReducers = (reducers: ReducersMapObject) => { + const combinedReducer = combine(reducers) + // TODO: define state type + return (state: any, action: AnyAction) => { + if (action.type.split('/').length > 1) { + const namespace = action.type.split('/')[0] + const reducer = reducers[namespace] + invariant(!!reducer, `reducer ${namespace} doesn't exist`) + return reducer && reducer(state, action) + } + return combinedReducer(state, action) + } +} \ No newline at end of file diff --git a/src/redux/viewUpdate/action.ts b/src/redux/viewUpdate/action.ts new file mode 100644 index 0000000..8041b8e --- /dev/null +++ b/src/redux/viewUpdate/action.ts @@ -0,0 +1,18 @@ +import { ThunkAction } from 'redux-thunk' + +export interface ACTION_CHANGE_SUBTITLE { + type: "CHANGE_SUB_TITLE", + title: string, +} + +export const changeSubTitle = (title: string): +ThunkAction => { + return (dispatch, getState) => { + const state = getState() + document.title = (title === null || title === undefined) ? state.siteConfig.title : (title + " - " +state.siteConfig.title); + return dispatch({ + type: "CHANGE_SUB_TITLE", + title: title + }) + } +} diff --git a/src/redux/viewUpdate/index.ts b/src/redux/viewUpdate/index.ts new file mode 100644 index 0000000..2c2801b --- /dev/null +++ b/src/redux/viewUpdate/index.ts @@ -0,0 +1,7 @@ +import * as actions from './action' +import * as reducers from './reducer' + +export default { + actions, + reducers, +} \ No newline at end of file diff --git a/src/redux/viewUpdate/reducer.ts b/src/redux/viewUpdate/reducer.ts new file mode 100644 index 0000000..d2a0bcd --- /dev/null +++ b/src/redux/viewUpdate/reducer.ts @@ -0,0 +1,309 @@ +import { AnyAction } from "redux" +import Auth from "../../middleware/Auth" + +export interface ViewUpdateState { + isLogin: boolean, + loadUploader:boolean, + open: boolean, + explorerViewMethod: string, + sortMethod: string, + subTitle: string | null, + contextType: string, + contextOpen: boolean, + menuOpen: boolean, + navigatorLoading: boolean, + navigatorError: boolean, + navigatorErrorMsg: string | null, + modalsLoading: boolean, + storageRefresh: boolean, + userPopoverAnchorEl: any, + shareUserPopoverAnchorEl: any, + modals: { + createNewFolder: boolean, + createNewFile: boolean, + rename: boolean, + move: boolean, + remove: boolean, + share: boolean, + music: boolean, + remoteDownload: boolean, + torrentDownload: boolean, + getSource: boolean, + copy:boolean, + resave: boolean, + compress:boolean, + decompress:boolean, + }, + snackbar: { + toggle: boolean, + vertical: string, + horizontal: string, + msg: string, + color: string + } +} +export const initState: ViewUpdateState = { + // 是否登录 + isLogin: Auth.Check(), + loadUploader:false, + open: false, + explorerViewMethod: "icon", + sortMethod: "timePos", + subTitle: null, + contextType: "none", + contextOpen: false, + menuOpen: false, + navigatorLoading: true, + navigatorError: false, + navigatorErrorMsg: null, + modalsLoading: false, + storageRefresh: false, + userPopoverAnchorEl: null, + shareUserPopoverAnchorEl: null, + modals: { + createNewFolder: false, + createNewFile: false, + rename: false, + move: false, + remove: false, + share: false, + music: false, + remoteDownload: false, + torrentDownload: false, + getSource: false, + copy:false, + resave: false, + compress:false, + decompress:false, + }, + snackbar: { + toggle: false, + vertical: "top", + horizontal: "center", + msg: "", + color: "" + } +} +const viewUpdate = (state: ViewUpdateState = initState, action: AnyAction) => { + switch (action.type) { + case 'DRAWER_TOGGLE': + return Object.assign({}, state, { + open: action.open, + }); + case 'CHANGE_VIEW_METHOD': + return Object.assign({}, state, { + explorerViewMethod: action.method, + }); + case 'SET_NAVIGATOR_LOADING_STATUE': + return Object.assign({}, state, { + navigatorLoading:action.status, + }); + case 'SET_NAVIGATOR_ERROR': + return Object.assign({}, state, { + navigatorError: action.status, + navigatorErrorMsg: action.msg, + }) + case 'OPEN_CREATE_FOLDER_DIALOG': + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + createNewFolder:true, + }), + contextOpen:false, + }); + case 'OPEN_CREATE_FILE_DIALOG': + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + createNewFile:true, + }), + contextOpen:false, + }); + case 'OPEN_RENAME_DIALOG': + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + rename:true, + }), + contextOpen:false, + }); + case 'OPEN_REMOVE_DIALOG': + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + remove:true, + }), + contextOpen:false, + }); + case 'OPEN_MOVE_DIALOG': + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + move:true, + }), + contextOpen:false, + }); + case 'OPEN_RESAVE_DIALOG': + // window.shareKey = action.key; + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + resave:true, + }), + contextOpen:false, + }); + case 'SET_USER_POPOVER': + return Object.assign({}, state, { + userPopoverAnchorEl:action.anchor, + }); + case 'SET_SHARE_USER_POPOVER': + return Object.assign({}, state, { + shareUserPopoverAnchorEl:action.anchor, + }); + case 'OPEN_SHARE_DIALOG': + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + share:true, + }), + contextOpen:false, + }); + case 'OPEN_MUSIC_DIALOG': + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + music:true, + }), + contextOpen:false, + }); + case 'OPEN_REMOTE_DOWNLOAD_DIALOG': + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + remoteDownload:true, + }), + contextOpen:false, + }); + case 'OPEN_TORRENT_DOWNLOAD_DIALOG': + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + torrentDownload:true, + }), + contextOpen:false, + }); + case 'OPEN_DECOMPRESS_DIALOG': + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + decompress:true, + }), + contextOpen:false, + }); + case 'OPEN_COMPRESS_DIALOG': + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + compress:true, + }), + contextOpen:false, + }); + case 'OPEN_GET_SOURCE_DIALOG': + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + getSource:true, + }), + contextOpen:false, + }); + case 'OPEN_COPY_DIALOG': + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + copy:true, + }), + contextOpen:false, + }); + case 'OPEN_LOADING_DIALOG': + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + loading:true, + loadingText:action.text, + }), + contextOpen:false, + }); + case 'CLOSE_ALL_MODALS': + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + createNewFolder:false, + createNewFile:false, + rename:false, + move:false, + remove:false, + share:false, + music:false, + remoteDownload:false, + torrentDownload:false, + getSource:false, + resave:false, + copy:false, + loading:false, + compress:false, + decompress:false, + }), + }); + case 'TOGGLE_SNACKBAR': + return Object.assign({}, state, { + snackbar:{ + toggle:!state.snackbar.toggle, + vertical:action.vertical, + horizontal:action.horizontal, + msg:action.msg, + color:action.color, + }, + }); + case 'SET_MODALS_LOADING': + return Object.assign({}, state, { + modalsLoading:action.status, + }); + case 'SET_SESSION_STATUS': + return { + ...state, + isLogin: action.status, + } + case 'ENABLE_LOAD_UPLOADER': + return Object.assign({}, state, { + loadUploader:true, + }); + case 'REFRESH_STORAGE': + return Object.assign({}, state, { + storageRefresh:!state.storageRefresh, + }); + case 'SEARCH_MY_FILE': + return Object.assign({}, state, { + contextOpen:false, + navigatorError:false, + navigatorLoading:true, + }) + case 'CHANGE_CONTEXT_MENU': + if(state.contextOpen && action.open){ + return Object.assign({}, state); + } + return Object.assign({}, state, { + contextOpen: action.open, + contextType: action.menuType, + }); + case 'CHANGE_SUB_TITLE': + return Object.assign({}, state, { + subTitle: action.title, + }); + case 'CHANGE_SORT_METHOD': + return { + ...state, + sortMethod: action.method + } + case 'NAVIGATOR_TO': + return { + ...state, + contextOpen:false, + navigatorError:false, + navigatorLoading: action.navigatorLoading + } + // case 'NAVIGATOR_TO': + // return Object.assign({}, state, { + // contextOpen:false, + // navigatorError:false, + // navigatorLoading:true, + // }) + default: + return state + } + } + + export default viewUpdate \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..219cec0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "sourceMap": true, + "noImplicitAny": true, + "esModuleInterop": true, + "module": "commonjs", + "target": "es6", + "jsx": "react" + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index f2acd40..55cd803 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1381,6 +1381,11 @@ resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.30.tgz#20efa342807606ada5483731a8137cb1561e5fe9" integrity sha512-98fB+yo7imSD2F7PF7GIpELNgtLNgo5wjivu0W5V4jx+KVVJxo6p/qN4zdzSTBWy4/sN3pPyXwnhRSD28QX+ag== +"@types/invariant@^2.2.32": + version "2.2.32" + resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.32.tgz#cf523a609062564e36e7a7dadb5089ed87da6382" + integrity sha512-WjY4WVFaehHv+TOgm+dS3UI559NvsPGFz/C0nIo7KOOdC+HeC7Y3/yLzdJYQ3+oFQaTXrOVm7cNtIgMataIDVg== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -1421,6 +1426,13 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== +"@types/react-dom@^16.9.7": + version "16.9.7" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.7.tgz#60844d48ce252d7b2dccf0c7bb937130e27c0cd2" + integrity sha512-GHTYhM8/OwUCf254WO5xqR/aqD3gC9kSTLpopWGpQLpnw23jk44RvMHsyUSEplvRJZdHxhJGMMLF0kCPYHPhQA== + dependencies: + "@types/react" "*" + "@types/react-transition-group@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.2.3.tgz#4924133f7268694058e415bf7aea2d4c21131470" @@ -6764,6 +6776,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -9465,6 +9482,13 @@ reduce-function-call@^1.0.1: dependencies: balanced-match "^1.0.0" +redux-mock-store@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.4.tgz#90d02495fd918ddbaa96b83aef626287c9ab5872" + integrity sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA== + dependencies: + lodash.isplainobject "^4.0.6" + redux-thunk@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" @@ -10946,6 +10970,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" + integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== + ua-parser-js@^0.7.18: version "0.7.20" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.20.tgz#7527178b82f6a62a0f243d1f94fd30e3e3c21098"