├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json └── src ├── Actions └── Actions.js ├── Api ├── Api.js └── ApiHandler.js ├── App.js ├── App.test.js ├── Components ├── Breadcrumb │ ├── Breadcrumb.css │ ├── Breadcrumb.jsx │ ├── BreadcrumbText.css │ └── BreadcrumbText.jsx ├── ContextMenu │ ├── ContextMenu.css │ ├── ContextMenu.jsx │ └── ContextMenuActions │ │ ├── CopyAction.jsx │ │ ├── CreateFolderAction.jsx │ │ ├── DownloadAction.jsx │ │ ├── EditAction.jsx │ │ ├── MoveAction.jsx │ │ ├── OpenAction.jsx │ │ ├── RemoveAction.jsx │ │ ├── RenameAction.jsx │ │ └── UploadFileAction.jsx ├── Dialogs │ ├── Content │ │ └── Content.jsx │ ├── Copy │ │ └── Copy.jsx │ ├── CreateFolder │ │ └── CreateFolder.jsx │ ├── Dialogs.jsx │ ├── Edit │ │ └── Edit.jsx │ ├── Move │ │ └── Move.jsx │ ├── Rename │ │ └── Rename.jsx │ └── UploadFile │ │ └── UploadFile.jsx ├── File │ ├── File.css │ ├── File.jsx │ └── FileSublist │ │ └── FileSublist.jsx ├── FileList │ ├── FileList.css │ ├── FileList.jsx │ ├── FileListEmptyMessage.css │ ├── FileListEmptyMessage.jsx │ └── FileListSublist │ │ ├── FileListSublist.css │ │ └── FileListSublist.jsx ├── FileUploader │ ├── FileUploader.jsx │ └── UploadFileList.jsx ├── Loader │ └── Loader.jsx ├── Navbar │ ├── Navbar.jsx │ └── ThreeDotsMenu.jsx └── Notification │ ├── DynamicSnackbar.jsx │ └── NotificationBar.jsx ├── Reducers └── MainReducer.js ├── config.js ├── index.css ├── index.js └── serviceWorker.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Filemanager 2 | 3 | Hello ex [angular-filemanager](https://github.com/joni2back/angular-filemanager/) user, this is the new version in React. 4 | 5 | I will try to make it clean and retro-compatible with the previous bridges/connectors 6 | 7 | It's very important for me your collaboration on my development tasks and time. 8 | Please help me to move forward with a donation by paypal :) [![Donate](https://www.paypal.com/en_GB/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=XRB7EW72PS982) 9 | 10 | --- 11 | 12 | ### Environment configuration 13 | **1) Install deps using NPM with** 14 | ```npm install``` 15 | 16 | **2) Start development environment** 17 | ```npm start``` 18 | 19 | **3) Run tests** 20 | ```npm run test``` 21 | 22 | **4) Compile for production** 23 | ```npm run build``` 24 | 25 | --- 26 | 27 | ## Connectors 28 | I am also developing a local file connector API in NodeJS in [filemanager-connector-node](https://github.com/joni2back/filemanager-connector-node) 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-filemanager", 3 | "version": "0.0.14", 4 | "private": true, 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/joni2back/react-filemanager.git" 8 | }, 9 | "keywords": [ 10 | "filemanager" 11 | ], 12 | "author": "Jonas Sciangula Street", 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/joni2back/react-filemanager/issues" 16 | }, 17 | "homepage": "https://joni2back.github.io/react-filemanager", 18 | "dependencies": { 19 | "@material-ui/core": "^4.11.0", 20 | "@material-ui/icons": "^4.9.1", 21 | "react": "^16.6.3", 22 | "react-dom": "^16.6.3", 23 | "react-redux": "~5.1.1", 24 | "react-scripts": "^4.0.0", 25 | "redux": "~4.0.1", 26 | "redux-thunk": "~2.3.0" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject" 33 | }, 34 | "eslintConfig": { 35 | "extends": "react-app" 36 | }, 37 | "browserslist": [ 38 | ">0.2%", 39 | "not dead", 40 | "not ie <= 11", 41 | "not op_mini all" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joni2back/react-filemanager/07d8f22325db7291033075bf4583142b9cb52354/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | 25 | React Filemanager 26 | 45 | 46 | 47 |
48 |
49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Filemanager", 3 | "name": "React Filemanager", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#fff", 14 | "background_color": "#2196f3" 15 | } 16 | -------------------------------------------------------------------------------- /src/Actions/Actions.js: -------------------------------------------------------------------------------- 1 | import * as APIHandler from '../Api/ApiHandler.js'; 2 | 3 | /** 4 | * Request API to get file list for the selected path then refresh UI 5 | * @returns {Function} 6 | */ 7 | export const uploadFiles = (fileList) => (dispatch, getState) => { 8 | const { path } = getState(); 9 | dispatch(setLoading(true)); 10 | dispatch(setSelectedFiles([])); 11 | dispatch(setFileUploadProgress(50)); 12 | 13 | APIHandler.uploadFiles(path.join('/'), fileList).then(r => { 14 | dispatch(setFileUploadProgress(100)); 15 | setTimeout(f => { 16 | dispatch(resetFileUploader()); 17 | }, 300); 18 | dispatch(refreshFileList()); 19 | }).catch(r => { 20 | dispatch({ 21 | type: 'SET_ERROR_MSG', 22 | value: r.toString() 23 | }); 24 | dispatch(setLoading(false)); 25 | }); 26 | }; 27 | 28 | /** 29 | * Request API to get file list for the selected path then refresh UI 30 | * @returns {Function} 31 | */ 32 | export const refreshFileList = () => (dispatch, getState) => { 33 | const { path } = getState(); 34 | dispatch(setLoading(true)); 35 | dispatch(setSelectedFiles([])); 36 | 37 | APIHandler.getFileList(path.join('/')).then(r => { 38 | dispatch(setLoading(false)); 39 | dispatch(setFileList(r)); 40 | }).catch(r => { 41 | dispatch(setFileList([])); 42 | dispatch({ 43 | type: 'SET_ERROR_MSG', 44 | value: r.toString() 45 | }); 46 | dispatch(setLoading(false)); 47 | }); 48 | }; 49 | 50 | 51 | /** 52 | * Request API to get file list for the selected path then refresh UI 53 | * @returns {Function} 54 | */ 55 | export const refreshFileListSublist = () => (dispatch, getState) => { 56 | const { pathSublist } = getState(); 57 | dispatch(setLoadingSublist(true)); 58 | dispatch(setSelectedFolderSublist(null)); 59 | 60 | APIHandler.getFileList(pathSublist.join('/')).then(r => { 61 | dispatch(setLoadingSublist(false)); 62 | dispatch(setFileListSublist(r)); 63 | }).catch(r => { 64 | dispatch(setFileListSublist([])); 65 | dispatch({ 66 | type: 'SET_ERROR_MSG', 67 | value: r.toString() 68 | }); 69 | dispatch(setLoadingSublist(false)); 70 | }); 71 | }; 72 | 73 | 74 | /** 75 | * Request API to get file content then dispatch defined events 76 | * @param {String} fileName 77 | * @returns {Function} 78 | */ 79 | export const getFileContent = (fileName) => (dispatch, getState) => { 80 | const { path } = getState(); 81 | 82 | dispatch(setLoading(true)); 83 | dispatch(setFileContent(null)); 84 | dispatch(setVisibleDialogContent(true)); 85 | APIHandler.getFileBody(path.join('/'), fileName).then(blob => { 86 | dispatch(setFileContent(blob)); 87 | dispatch(setLoading(false)); 88 | }).catch(r => { 89 | dispatch({ 90 | type: 'SET_ERROR_MSG', 91 | value: r.toString() 92 | }); 93 | dispatch(setLoading(false)); 94 | }); 95 | }; 96 | 97 | /** 98 | * Request API to rename file then dispatch defined events 99 | * @param {String} fileName 100 | * @returns {Function} 101 | */ 102 | export const renameItem = (fileName, newFileName) => (dispatch, getState) => { 103 | const { path } = getState(); 104 | dispatch(setLoading(true)); 105 | APIHandler.renameItem(path.join('/'), fileName, newFileName).then(blob => { 106 | dispatch(setVisibleDialogRename(false)); 107 | dispatch(setLoading(false)); 108 | dispatch(refreshFileList()); 109 | }).catch(r => { 110 | dispatch({ 111 | type: 'SET_ERROR_MSG', 112 | value: r.toString() 113 | }); 114 | dispatch(setLoading(false)); 115 | }); 116 | }; 117 | 118 | /** 119 | * Request API to get download file then dispatch defined events 120 | * @param {String} fileName 121 | * @returns {Function} 122 | */ 123 | export const downloadFile = (fileName) => (dispatch, getState) => { 124 | const { path } = getState(); 125 | dispatch(setLoading(true)); 126 | APIHandler.getFileBody(path.join('/'), fileName).then(blob => { 127 | // TODO workaround large files disables ui for long time 128 | const blobUrl = window.URL.createObjectURL(blob); 129 | let tempLink = window.document.createElement('a'); 130 | tempLink.href = blobUrl; 131 | tempLink.setAttribute('download', fileName); 132 | tempLink.click(); 133 | window.URL.revokeObjectURL(blobUrl); 134 | dispatch(setLoading(false)); 135 | }).catch(r => { 136 | dispatch({ 137 | type: 'SET_ERROR_MSG', 138 | value: r.toString() 139 | }); 140 | dispatch(setLoading(false)); 141 | }); 142 | }; 143 | 144 | /** 145 | * Request API to get file content then dispatch defined events 146 | * @param {String} fileName 147 | * @returns {Function} 148 | */ 149 | export const getFileContentForEdit = (fileName) => (dispatch, getState) => { 150 | const { path } = getState(); 151 | dispatch(setLoading(true)); 152 | dispatch(setFileContent(null)); 153 | dispatch(setVisibleDialogEdit(true)); 154 | APIHandler.getFileBody(path.join('/'), fileName).then(blob => { 155 | dispatch(setFileContent(blob)); 156 | dispatch(setLoading(false)); 157 | }).catch(r => { 158 | dispatch({ 159 | type: 'SET_ERROR_MSG', 160 | value: r.toString() 161 | }); 162 | dispatch(setLoading(false)); 163 | }); 164 | }; 165 | 166 | 167 | /** 168 | * Request API to create a folder then dispatch defined events 169 | * @param {String} createFolderName 170 | * @returns {Function} 171 | */ 172 | export const createNewFolder = (createFolderName) => (dispatch, getState) => { 173 | const { path } = getState(); 174 | dispatch(setLoading(true)); 175 | 176 | APIHandler.createFolder(path.join('/'), createFolderName).then(r => { 177 | dispatch(setVisibleDialogCreateFolder(false)); 178 | dispatch(setLoading(false)); 179 | dispatch(refreshFileList()); 180 | }).catch(r => { 181 | dispatch({ 182 | type: 'SET_ERROR_MSG', 183 | value: r.toString() 184 | }); 185 | dispatch(setLoading(false)); 186 | }); 187 | }; 188 | 189 | 190 | /** 191 | * Request API to remove an item then dispatch defined events 192 | * @param {Array} filenames 193 | * @returns {Function} 194 | */ 195 | export const removeItems = (files) => (dispatch, getState) => { 196 | const { path } = getState(); 197 | const filenames = files.map(f => f.name); 198 | 199 | dispatch(setLoading(true)); 200 | APIHandler.removeItems(path.join('/'), filenames).then(r => { 201 | dispatch(setLoading(false)); 202 | dispatch(refreshFileList()); 203 | }).catch(r => { 204 | dispatch({ 205 | type: 'SET_ERROR_MSG', 206 | value: r.toString() 207 | }); 208 | dispatch(setLoading(false)); 209 | }); 210 | }; 211 | 212 | 213 | /** 214 | * Request API to move an item then dispatch defined events 215 | * @param {Array} filenames 216 | * @returns {Function} 217 | */ 218 | export const moveItems = (files) => (dispatch, getState) => { 219 | const { path, pathSublist, selectedFolderSublist } = getState(); 220 | const destination = pathSublist.join('/') + '/' + selectedFolderSublist.name; 221 | const filenames = files.map(f => f.name); 222 | 223 | dispatch(setLoading(true)); 224 | APIHandler.moveItems(path.join('/'), destination, filenames).then(r => { 225 | dispatch(setLoading(false)); 226 | dispatch(setVisibleDialogMove(false)); 227 | dispatch(refreshFileList()); 228 | }).catch(r => { 229 | dispatch({ 230 | type: 'SET_ERROR_MSG', 231 | value: r.toString() 232 | }); 233 | dispatch(setLoading(false)); 234 | }); 235 | }; 236 | 237 | 238 | /** 239 | * Request API to copy an item then dispatch defined events 240 | * @param {Array} filenames 241 | * @returns {Function} 242 | */ 243 | export const copyItems = (files) => (dispatch, getState) => { 244 | const { path, pathSublist, selectedFolderSublist } = getState(); 245 | const destination = pathSublist.join('/') + '/' + selectedFolderSublist.name; 246 | const filenames = files.map(f => f.name); 247 | 248 | dispatch(setLoading(true)); 249 | APIHandler.copyItems(path.join('/'), destination, filenames).then(r => { 250 | dispatch(setLoading(false)); 251 | dispatch(setVisibleDialogCopy(false)); 252 | dispatch(refreshFileList()); 253 | }).catch(r => { 254 | dispatch({ 255 | type: 'SET_ERROR_MSG', 256 | value: r.toString() 257 | }); 258 | dispatch(setLoading(false)); 259 | }); 260 | }; 261 | 262 | /** 263 | * This handles multiple selection by using shift key 264 | * @param {Object} lastFile 265 | * @returns {Function} 266 | */ 267 | export const setSelectedFileFromLastTo = (lastFile) => (dispatch, getState) => { 268 | const { fileList, selectedFiles } = getState(); 269 | 270 | const lastPreviouslySelected = [...selectedFiles].pop(); 271 | const lastPreviouslySelectedIndex = fileList.indexOf(fileList.find(f => f.name === lastPreviouslySelected.name)) 272 | const lastSelectedIndex = fileList.indexOf(fileList.find(f => f.name === lastFile.name)) 273 | 274 | let toAdd = []; 275 | if (lastSelectedIndex > lastPreviouslySelectedIndex) { 276 | toAdd = fileList.filter((index, element) => { 277 | return fileList.indexOf(index) <= lastSelectedIndex && fileList.indexOf(index) >= lastPreviouslySelectedIndex 278 | }); 279 | } else { 280 | toAdd = fileList.filter((index, element) => { 281 | return fileList.indexOf(index) >= lastSelectedIndex && fileList.indexOf(index) <= lastPreviouslySelectedIndex 282 | }); 283 | } 284 | dispatch(setSelectedFiles([...selectedFiles, ...toAdd])); 285 | }; 286 | 287 | 288 | /** 289 | * @returns {Function} 290 | */ 291 | export const initSubList = () => (dispatch, getState) => { 292 | const { path } = getState(); 293 | dispatch(setSelectedFolderSublist(null)); 294 | dispatch(setFileListSublist([])); 295 | dispatch(setPathSublist([...path])); 296 | dispatch(refreshFileListSublist()); 297 | }; 298 | 299 | export const resetFileUploader = () => (dispatch, getState) => { 300 | dispatch(setFileUploadProgress(0)); 301 | dispatch(setVisibleDialogUploadFile(false)); 302 | dispatch(setFileUploadList([])); 303 | }; 304 | 305 | export const enterToPreviousDirectory = () => (dispatch, getState) => { 306 | const { path } = getState(); 307 | dispatch(setPath(path.slice(0, -1))); 308 | dispatch(setFileListFilter(null)); 309 | dispatch(refreshFileList()); 310 | }; 311 | 312 | export const enterToPreviousDirectoryByIndex = (index) => (dispatch, getState) => { 313 | const { path } = getState(); 314 | const newPath = [...path].slice(0, ++index); 315 | dispatch(setPath(newPath)); 316 | dispatch(refreshFileList()); 317 | dispatch(setFileListFilter(null)); 318 | }; 319 | 320 | export const enterToPreviousDirectorySublist = () => (dispatch, getState) => { 321 | const { pathSublist } = getState(); 322 | dispatch(setPathSublist(pathSublist.slice(0, -1))); 323 | dispatch(refreshFileListSublist()); 324 | }; 325 | 326 | export const setPath = (path) => { 327 | return { 328 | type: 'SET_PATH', 329 | value: path 330 | }; 331 | }; 332 | 333 | export const setPathSublist = (path) => { 334 | return { 335 | type: 'SET_PATH_SUB_LIST', 336 | value: path 337 | }; 338 | }; 339 | 340 | export const enterToDirectory = (directory) => (dispatch, getState) => { 341 | dispatch({ 342 | type: 'ENTER_TO_DIRECTORY', 343 | value: directory 344 | }); 345 | dispatch(setFileListFilter(null)); 346 | dispatch(refreshFileList()); 347 | }; 348 | 349 | export const enterToDirectorySublist = (directory) => (dispatch, getState) => { 350 | dispatch({ 351 | type: 'ENTER_TO_DIRECTORY_SUB_LIST', 352 | value: directory 353 | }); 354 | dispatch(refreshFileListSublist()); 355 | }; 356 | 357 | export const setFileList = (fileList) => { 358 | return { 359 | type: 'SET_FILE_LIST', 360 | value: fileList 361 | }; 362 | }; 363 | 364 | export const setFileListSublist = (fileList) => { 365 | return { 366 | type: 'SET_FILE_LIST_SUB_LIST', 367 | value: fileList 368 | }; 369 | }; 370 | 371 | export const setSelectedFiles = (files) => { 372 | return { 373 | type: 'SET_SELECTED_FILES', 374 | value: files 375 | }; 376 | }; 377 | 378 | export const setSelectedFolderSublist = (file) => { 379 | return { 380 | type: 'SET_SELECTED_FOLDER_SUB_LIST', 381 | value: file 382 | }; 383 | }; 384 | 385 | export const setFileListFilter = (search) => { 386 | return { 387 | type: 'SET_FILE_LIST_FILTER', 388 | value: search 389 | }; 390 | }; 391 | 392 | export const setContextMenuVisible = (visible) => { 393 | return { 394 | type: 'SET_CONTEXT_MENU_VISIBLE', 395 | value: !!visible 396 | }; 397 | }; 398 | 399 | export const setContextMenuPosition = (x, y) => { 400 | return { 401 | type: 'SET_CONTEXT_MENU_POSITION', 402 | value: [x, y] 403 | }; 404 | }; 405 | 406 | export const setContextMenuPositionElement = (element) => { 407 | return { 408 | type: 'SET_CONTEXT_MENU_POSITION_ELEMENT', 409 | value: element 410 | }; 411 | }; 412 | 413 | export const toggleSelectedFile = (file) => { 414 | return { 415 | type: 'TOGGLE_SELECTED_FILE', 416 | value: file 417 | }; 418 | }; 419 | 420 | export const rightClickOnFile = (file) => (dispatch, getState) => { 421 | const { selectedFiles } = getState(); 422 | const isSelected = selectedFiles.indexOf(selectedFiles.find(f => f.name === file.name)) !== -1; 423 | 424 | !isSelected && dispatch(setSelectedFiles([file])); 425 | }; 426 | 427 | export const setLoading = (value) => { 428 | return { 429 | type: 'SET_LOADING', 430 | value: value 431 | }; 432 | }; 433 | 434 | export const setLoadingSublist = (value) => { 435 | return { 436 | type: 'SET_LOADING_SUB_LIST', 437 | value: value 438 | }; 439 | }; 440 | 441 | export const setVisibleDialogCreateFolder = (visible) => { 442 | return { 443 | type: 'SET_VISIBLE_DIALOG_CREATE_FOLDER', 444 | value: !!visible 445 | }; 446 | }; 447 | 448 | export const setVisibleDialogUploadFile = (visible) => { 449 | return { 450 | type: 'SET_VISIBLE_DIALOG_UPLOAD_FILE', 451 | value: !!visible 452 | }; 453 | }; 454 | 455 | export const setVisibleDialogRename = (visible) => { 456 | return { 457 | type: 'SET_VISIBLE_DIALOG_RENAME', 458 | value: !!visible 459 | }; 460 | }; 461 | 462 | export const setVisibleDialogMove = (visible) => { 463 | return { 464 | type: 'SET_VISIBLE_DIALOG_MOVE', 465 | value: !!visible 466 | }; 467 | }; 468 | 469 | export const setVisibleDialogCopy = (visible) => { 470 | return { 471 | type: 'SET_VISIBLE_DIALOG_COPY', 472 | value: !!visible 473 | }; 474 | }; 475 | 476 | export const setVisibleDialogContent = (visible) => { 477 | return { 478 | type: 'SET_VISIBLE_DIALOG_CONTENT', 479 | value: !!visible 480 | }; 481 | }; 482 | 483 | export const setVisibleDialogEdit = (visible) => { 484 | return { 485 | type: 'SET_VISIBLE_DIALOG_EDIT', 486 | value: !!visible 487 | }; 488 | }; 489 | 490 | export const setFileContent = (blob) => { 491 | return { 492 | type: 'SET_FILE_CONTENT', 493 | value: blob 494 | }; 495 | }; 496 | 497 | export const setFileUploadProgress = (percentage) => { 498 | return { 499 | type: 'SET_FILE_UPLOAD_PROGRESS', 500 | value: percentage 501 | }; 502 | }; 503 | 504 | export const setFileUploadList = (files) => { 505 | return { 506 | type: 'SET_FILE_UPLOAD_LIST', 507 | value: files 508 | }; 509 | }; -------------------------------------------------------------------------------- /src/Api/Api.js: -------------------------------------------------------------------------------- 1 | import config from './../config.js'; 2 | 3 | /** 4 | * Fetch API to list files from directory 5 | * @param {String} path 6 | * @returns {Object} 7 | */ 8 | export function list(path) { 9 | return fetch(config.url_list + '?path=' + (encodeURIComponent(path) || '/')); 10 | }; 11 | 12 | 13 | /** 14 | * Fetch API to create a directory 15 | * @param {String} path 16 | * @param {String} directory 17 | * @returns {Object} 18 | */ 19 | export function createDirectory(path, directory) { 20 | return fetch(config.url_create_folder, { 21 | method: 'POST', 22 | headers: { 23 | 'Content-Type': 'application/json' 24 | }, 25 | body: JSON.stringify({ 26 | path, directory 27 | }) 28 | }); 29 | }; 30 | 31 | 32 | /** 33 | * Fetch API to get file body 34 | * @param {String} path 35 | * @returns {Object} 36 | */ 37 | export function getFileContent(path) { 38 | return fetch(config.url_get_content + '?path=' + (encodeURIComponent(path) || '/')); 39 | }; 40 | 41 | 42 | /** 43 | * Fetch API to remove a file or folder 44 | * @param {String} path 45 | * @param {Array} filenames 46 | * @param {Boolean} recursive 47 | * @returns {Object} 48 | */ 49 | export function remove(path, filenames, recursive = true) { 50 | return fetch(config.url_remove, { 51 | method: 'POST', 52 | headers: { 53 | 'Content-Type': 'application/json' 54 | }, 55 | body: JSON.stringify({ 56 | path, filenames, recursive 57 | }) 58 | }); 59 | }; 60 | 61 | /** 62 | * Fetch API to move files 63 | * @param {String} path 64 | * @param {Array} filenames 65 | * @param {Boolean} recursive 66 | * @returns {Object} 67 | */ 68 | export function move(path, destination, filenames) { 69 | return fetch(config.url_move, { 70 | method: 'POST', 71 | headers: { 72 | 'Content-Type': 'application/json' 73 | }, 74 | body: JSON.stringify({ 75 | path, destination, filenames 76 | }) 77 | }); 78 | }; 79 | 80 | /** 81 | * Fetch API to move files 82 | * @param {String} path 83 | * @param {Array} filenames 84 | * @param {Boolean} recursive 85 | * @returns {Object} 86 | */ 87 | export function rename(path, destination) { 88 | return fetch(config.url_rename, { 89 | method: 'POST', 90 | headers: { 91 | 'Content-Type': 'application/json' 92 | }, 93 | body: JSON.stringify({ 94 | path, destination 95 | }) 96 | }); 97 | }; 98 | 99 | /** 100 | * Fetch API to copy files 101 | * @param {String} path 102 | * @param {Array} filenames 103 | * @param {Boolean} recursive 104 | * @returns {Object} 105 | */ 106 | export function copy(path, destination, filenames) { 107 | return fetch(config.url_copy, { 108 | method: 'POST', 109 | headers: { 110 | 'Content-Type': 'application/json' 111 | }, 112 | body: JSON.stringify({ 113 | path, destination, filenames 114 | }) 115 | }); 116 | }; 117 | 118 | /** 119 | * Fetch API to copy files 120 | * @param {String} path 121 | * @param {Object} fileList 122 | * @returns {Object} 123 | */ 124 | export function upload(path, fileList, formData = new FormData()) { 125 | [...fileList].forEach(f => { 126 | formData.append('file[]', f); 127 | }); 128 | formData.append('path', path); 129 | 130 | return fetch(config.url_upload, { 131 | method: 'POST', 132 | body: formData, 133 | headers: { 134 | // a workaround for node connector, passing the path by header 135 | path: path 136 | } 137 | }); 138 | }; 139 | -------------------------------------------------------------------------------- /src/Api/ApiHandler.js: -------------------------------------------------------------------------------- 1 | import * as API from './Api.js'; 2 | import config from './../config.js'; 3 | 4 | const messageTranslation = { 5 | 'unknown_response': 'Unknown error response from connector', 6 | 'TypeError: Failed to fetch': 'Cannot get a response from connector.', 7 | }; 8 | 9 | /** 10 | * Response handler for fetch responses 11 | * @param {Function} resolve 12 | * @param {Function} reject 13 | * @returns {Object} 14 | */ 15 | const handleFetch = (resolve, reject) => { 16 | return { 17 | xthen: (response) => { 18 | const contentType = response.headers.get('content-type'); 19 | const contentDisp = response.headers.get('content-disposition'); 20 | const isJson = /(application|text)\/json/.test(contentType); 21 | const isAttachment = /attachment/.test(contentDisp); 22 | 23 | if (! response.ok) { 24 | if (isJson) { 25 | throw response.json(); 26 | } 27 | throw Error(messageTranslation['unknown_response']); 28 | } 29 | 30 | if (isAttachment) { 31 | response.blob().then(blob => { 32 | resolve(blob); 33 | }); 34 | return; 35 | } 36 | 37 | if (isJson) { 38 | response.json().then(json => { 39 | if (! json.success) { 40 | throw new Error(); 41 | } 42 | resolve(json.data); 43 | }); 44 | return; 45 | } 46 | }, 47 | xcatch: (errorResponse) => { 48 | // is thrown json 49 | if (errorResponse && errorResponse.then) { 50 | errorResponse.then(errJson => { 51 | return reject(errJson.errorMsg || JSON.stringify(errJson)); 52 | }); 53 | } else { 54 | return reject(messageTranslation[errorResponse] || errorResponse); 55 | } 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * Clean path string removing double slashes and prepending a slash 62 | * @param {String} path 63 | * @returns {String} 64 | */ 65 | const fixPath = (path) => { 66 | return ('/' + path).replace(/\/\//g, '/'); 67 | }; 68 | 69 | /** 70 | * Wrap API response for retrive file liest 71 | * @param {String} path 72 | * @returns {Object} 73 | */ 74 | export const getFileList = (path) => { 75 | path = fixPath(path); 76 | return new Promise((resolve, reject) => { 77 | return API.list(path) 78 | .then(handleFetch(resolve, reject).xthen) 79 | .catch(handleFetch(resolve, reject).xcatch) 80 | }) 81 | }; 82 | 83 | /** 84 | * Wrap API response for retrive file content 85 | * @param {String} path 86 | * @returns {Object} 87 | */ 88 | export const getFileBody = (path, filename) => { 89 | path = fixPath(path + '/' + filename); 90 | return new Promise((resolve, reject) => { 91 | return API.getFileContent(path) 92 | .then(handleFetch(resolve, reject).xthen) 93 | .catch(handleFetch(resolve, reject).xcatch) 94 | }) 95 | }; 96 | 97 | 98 | /** 99 | * Wrap API response for retrive file content 100 | * @param {String} path 101 | * @returns {Object} 102 | */ 103 | export const renameItem = (path, filename, newFileName) => { 104 | const oldPath = fixPath(path + '/' + filename); 105 | const newPath = fixPath(path + '/' + newFileName); 106 | 107 | return new Promise((resolve, reject) => { 108 | return API.rename(oldPath, newPath) 109 | .then(handleFetch(resolve, reject).xthen) 110 | .catch(handleFetch(resolve, reject).xcatch) 111 | }) 112 | }; 113 | 114 | /** 115 | * Wrap API response for create folder 116 | * @param {String} path 117 | * @param {String} folder 118 | * @returns {Object} 119 | */ 120 | export const createFolder = (path, folder) => { 121 | path = fixPath(path); 122 | return new Promise((resolve, reject) => { 123 | if (! (folder || '').trim()) { 124 | return reject('Invalid folder name'); 125 | } 126 | return API.createDirectory(path, folder) 127 | .then(handleFetch(resolve, reject).xthen) 128 | .catch(handleFetch(resolve, reject).xcatch) 129 | }) 130 | }; 131 | 132 | /** 133 | * Wrap API response for remove file or folder 134 | * @param {String} path 135 | * @param {Array} filenames 136 | * @param {Boolean} recursive 137 | * @returns {Object} 138 | */ 139 | export const removeItems = (path, filenames, recursive = true) => { 140 | path = fixPath(path); 141 | return new Promise((resolve, reject) => { 142 | if (! filenames.length) { 143 | return reject('No files to remove'); 144 | } 145 | return API.remove(path, filenames, recursive) 146 | .then(handleFetch(resolve, reject).xthen) 147 | .catch(handleFetch(resolve, reject).xcatch) 148 | }) 149 | }; 150 | 151 | /** 152 | * Wrap API response for move file or folder 153 | * @param {String} path 154 | * @param {Array} filenames 155 | * @param {Boolean} recursive 156 | * @returns {Object} 157 | */ 158 | export const moveItems = (path, destination, filenames) => { 159 | path = fixPath(path); 160 | destination = fixPath(destination); 161 | return new Promise((resolve, reject) => { 162 | if (! filenames.length) { 163 | return reject('No files to move'); 164 | } 165 | return API.move(path, destination, filenames) 166 | .then(handleFetch(resolve, reject).xthen) 167 | .catch(handleFetch(resolve, reject).xcatch) 168 | }) 169 | }; 170 | 171 | /** 172 | * Wrap API response for copy file or folder 173 | * @param {String} path 174 | * @param {Array} filenames 175 | * @param {Boolean} recursive 176 | * @returns {Object} 177 | */ 178 | export const copyItems = (path, destination, filenames) => { 179 | path = fixPath(path); 180 | destination = fixPath(destination); 181 | return new Promise((resolve, reject) => { 182 | if (! filenames.length) { 183 | return reject('No files to copy'); 184 | } 185 | return API.copy(path, destination, filenames) 186 | .then(handleFetch(resolve, reject).xthen) 187 | .catch(handleFetch(resolve, reject).xcatch) 188 | }) 189 | }; 190 | 191 | /** 192 | * Wrap API response for upload files 193 | * @param {String} path 194 | * @param {Object} fileList 195 | * @returns {Object} 196 | */ 197 | export const uploadFiles = (path, fileList) => { 198 | path = fixPath(path); 199 | 200 | return new Promise((resolve, reject) => { 201 | if (! fileList.length) { 202 | return reject('No files to upload'); 203 | } 204 | return API.upload(path, fileList) 205 | .then(handleFetch(resolve, reject).xthen) 206 | .catch(handleFetch(resolve, reject).xcatch) 207 | }) 208 | }; 209 | 210 | /** 211 | * Calculate available actions for a file 212 | * @param {Object} file 213 | * @returns {Array} 214 | */ 215 | export const getActionsByFile = (file, acts = []) => { 216 | if (file.type === 'dir') { 217 | acts.push('open'); 218 | 219 | typeof file.compressible !== 'undefined' ? 220 | file.compressible && acts.push('compress'): 221 | acts.push('compress'); 222 | } 223 | 224 | if (file.type === 'file') { 225 | acts.push('download'); 226 | config.isImageFilePattern.test(file.name) && acts.push('open'); 227 | 228 | typeof file.editable !== 'undefined' ? 229 | file.editable && acts.push('edit'): 230 | config.isEditableFilePattern.test(file.name) && acts.push('edit'); 231 | 232 | typeof file.extractable !== 'undefined' ? 233 | file.extractable && acts.push('extract'): 234 | config.isExtractableFilePattern.test(file.name) && acts.push('extract'); 235 | 236 | acts.push('copy'); 237 | } 238 | 239 | acts.push('move'); 240 | acts.push('rename'); 241 | acts.push('perms'); 242 | acts.push('remove'); 243 | 244 | return acts; 245 | } 246 | 247 | /** 248 | * Calculate available actions for selected files, excluding non coincidences 249 | * @param {Array} files 250 | * @returns {Array} 251 | */ 252 | export const getActionsByMultipleFiles = (files, acts = []) => { 253 | files.forEach(file => { 254 | const fileActs = getActionsByFile(file); 255 | // intersects previous actions with the following to leave only coincidences 256 | acts = acts.length ? acts.filter(value => -1 !== fileActs.indexOf(value)) : fileActs; 257 | }); 258 | 259 | if (files.length > 1) { 260 | acts.splice(acts.indexOf('open'), acts.indexOf('open') >= 0); 261 | acts.splice(acts.indexOf('edit'), acts.indexOf('edit') >= 0); 262 | acts.splice(acts.indexOf('compress'), acts.indexOf('compress') >= 0); 263 | acts.splice(acts.indexOf('download'), acts.indexOf('download') >= 0); 264 | acts.splice(acts.indexOf('rename'), acts.indexOf('rename') >= 0); 265 | acts.push('compress'); 266 | } 267 | return acts; 268 | } 269 | 270 | /** 271 | * Calculate file size by bytes in human readable format 272 | * @param {Number} bytes 273 | * @returns {String} 274 | */ 275 | export const getHumanFileSize = (bytes) => { 276 | const e = (Math.log(bytes) / Math.log(1e3)) | 0; 277 | return +(bytes / Math.pow(1e3, e)).toFixed(2) + ' ' + ('kMGTPEZY'[e - 1] || '') + 'B'; 278 | }; -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import FileList from './Components/FileList/FileList.jsx'; 3 | import Navbar from './Components/Navbar/Navbar.jsx'; 4 | import ContextMenu from './Components/ContextMenu/ContextMenu.jsx'; 5 | import Dialogs from './Components/Dialogs/Dialogs.jsx'; 6 | 7 | import { MuiThemeProvider as MaterialUI, createMuiTheme } from '@material-ui/core/styles'; 8 | import blue from '@material-ui/core/colors/blue'; 9 | import { connect } from 'react-redux'; 10 | import { setContextMenuVisible, refreshFileList } from './Actions/Actions.js'; 11 | import DynamicSnackbar from './Components/Notification/DynamicSnackbar.jsx'; 12 | 13 | const theme = createMuiTheme({ 14 | palette: { 15 | primary: blue, 16 | }, 17 | typography: { 18 | useNextVariants: true, 19 | } 20 | }); 21 | 22 | class App extends Component { 23 | 24 | componentDidMount() { 25 | this.props.init(); 26 | }; 27 | 28 | render() { 29 | return ( 30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 | ); 40 | } 41 | } 42 | 43 | const mapStateToProps = (state) => { 44 | return { 45 | }; 46 | }; 47 | 48 | const mapDispatchToProps = (dispatch) => { 49 | return { 50 | init: () => { 51 | dispatch(refreshFileList()); 52 | }, 53 | 54 | handleHideContextMenu: (event) => { 55 | if (! (event.target.tagName === 'INPUT' || /label/i.test(event.target.className))) { 56 | event.preventDefault(); 57 | } 58 | dispatch(setContextMenuVisible(false)); 59 | } 60 | }; 61 | }; 62 | 63 | export default connect(mapStateToProps, mapDispatchToProps)(App); 64 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/Components/Breadcrumb/Breadcrumb.css: -------------------------------------------------------------------------------- 1 | .Breadcrumb { 2 | background: gray; 3 | font-size: 13px; 4 | color: #fff; 5 | padding: 10px 25px; 6 | } 7 | 8 | .Breadcrumb > span { 9 | font-weight: bold; 10 | min-width: 20px; 11 | display: inline-block; 12 | text-align: center; 13 | cursor: pointer; 14 | } 15 | 16 | .Breadcrumb > span:hover { 17 | color: #eee; 18 | } -------------------------------------------------------------------------------- /src/Components/Breadcrumb/Breadcrumb.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import './Breadcrumb.css'; 4 | import BreadcrumbText from './BreadcrumbText.jsx'; 5 | 6 | class Breadcrumb extends Component { 7 | render() { 8 | return
9 | 10 |
11 | } 12 | } 13 | 14 | const mapDispatchToProps = (dispatch) => { 15 | return { 16 | }; 17 | }; 18 | 19 | const mapStateToProps = (state) => { 20 | return { 21 | }; 22 | }; 23 | export default connect(mapStateToProps, mapDispatchToProps)(Breadcrumb); 24 | -------------------------------------------------------------------------------- /src/Components/Breadcrumb/BreadcrumbText.css: -------------------------------------------------------------------------------- 1 | .BreadcrumbText { 2 | } 3 | 4 | .BreadcrumbText span { 5 | cursor: pointer; 6 | text-overflow: ellipsis; 7 | } 8 | 9 | .BreadcrumbText span:hover { 10 | color: #efefef; 11 | } -------------------------------------------------------------------------------- /src/Components/Breadcrumb/BreadcrumbText.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft'; 5 | import Button from '@material-ui/core/Button'; 6 | import './BreadcrumbText.css'; 7 | 8 | const styles = theme => ({ 9 | lastPath: { 10 | display: 'block', 11 | [theme.breakpoints.up('sm')]: { 12 | display: 'none' 13 | } 14 | }, 15 | paths: { 16 | display: 'none', 17 | [theme.breakpoints.up('sm')]: { 18 | display: 'block', 19 | } 20 | } 21 | }); 22 | 23 | class BreadcrumbText extends Component { 24 | 25 | render() { 26 | const { classes, handleClickPath, path, rootTitle, handleGoBack, canGoBack } = this.props; 27 | 28 | const separator = >; 29 | const rootPath = handleClickPath(e, -1, path)} data-index={0}> 30 | { rootTitle } { path.length ? separator : '' } 31 | ; 32 | const lastPath = [...path].pop() || rootTitle; 33 | 34 | const directories = path.map((dir, index) => { 35 | return handleClickPath(e, index, path)}> 36 | {dir} { path.length -1 !== index ? separator : '' }  37 | 38 | }); 39 | 40 | return ( 41 |
42 |
43 | 46 | {lastPath} 47 |
48 |
{rootPath} {directories}
49 |
50 | ); 51 | } 52 | } 53 | 54 | 55 | const mapDispatchToProps = (dispatch) => { 56 | return { 57 | }; 58 | }; 59 | 60 | const mapStateToProps = (state) => { 61 | return { 62 | }; 63 | }; 64 | 65 | export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(BreadcrumbText)); 66 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenu.css: -------------------------------------------------------------------------------- 1 | .XXXXXXXXXXXContextMenu { 2 | max-width: 200px; 3 | position: absolute; 4 | display: block; 5 | background: green; 6 | border: 1px solid black; 7 | padding: 10px; 8 | text-align: left; 9 | } 10 | 11 | .ContextMenu ul { 12 | margin: 0; 13 | padding: 0; 14 | list-style: none; 15 | } 16 | 17 | .ContextMenu ul li { 18 | padding: 10px 20px; 19 | border-bottom: 1px solid #000; 20 | } 21 | 22 | .ContextMenu ul li:last-child { 23 | border: none; 24 | } -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenu.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import './ContextMenu.css'; 4 | import Menu from '@material-ui/core/Menu'; 5 | import { getActionsByMultipleFiles } from '../../Api/ApiHandler.js'; 6 | import OpenAction from './ContextMenuActions/OpenAction.jsx'; 7 | import RemoveAction from './ContextMenuActions/RemoveAction.jsx'; 8 | import MoveAction from './ContextMenuActions/MoveAction.jsx'; 9 | import CopyAction from './ContextMenuActions/CopyAction.jsx'; 10 | import EditAction from './ContextMenuActions/EditAction.jsx'; 11 | import RenameAction from './ContextMenuActions/RenameAction.jsx'; 12 | import DownloadAction from './ContextMenuActions/DownloadAction.jsx'; 13 | 14 | class ContextMenu extends Component { 15 | 16 | render() { 17 | const { acts, visible, x, y } = this.props; 18 | const actionsComp = acts.map((act, key) => { 19 | let component; 20 | if (act === 'open') { 21 | component = ; 22 | } 23 | if (act === 'edit') { 24 | component = ; 25 | } 26 | if (act === 'copy') { 27 | component = ; 28 | } 29 | if (act === 'move') { 30 | component = ; 31 | } 32 | if (act === 'rename') { 33 | component = ; 34 | } 35 | if (act === 'download') { 36 | component = ; 37 | } 38 | if (act === 'remove') { 39 | component = ; 40 | } 41 | return component; 42 | }); 43 | 44 | return ( 45 |
46 | {} } 55 | PaperProps={{ style: {width: 170} }}> 56 | { actionsComp } 57 | 58 |
59 | ); 60 | } 61 | } 62 | 63 | const mapStateToProps = (state) => { 64 | return { 65 | x: state.contextMenuPosition[0] || 0, 66 | y: state.contextMenuPosition[1] || 0, 67 | visible: !!state.contextMenuVisible, 68 | acts: getActionsByMultipleFiles(state.selectedFiles), 69 | }; 70 | }; 71 | 72 | const mapDispatchToProps = (dispatch) => { 73 | return { 74 | }; 75 | }; 76 | 77 | export default connect(mapStateToProps, mapDispatchToProps)(ContextMenu); 78 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/CopyAction.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import FileCopyIcon from '@material-ui/icons/FileCopy'; 7 | import { initSubList, setVisibleDialogCopy } from '../../../Actions/Actions.js'; 8 | 9 | function CopyAction(props) { 10 | const {handleClick, selectedFiles} = props; 11 | 12 | return ( 13 | handleClick(e, selectedFiles)}> 14 | 15 | 16 | 17 | 18 | Copy 19 | 20 | 21 | ); 22 | } 23 | 24 | const mapStateToProps = (state) => { 25 | return { 26 | selectedFiles: state.selectedFiles 27 | }; 28 | }; 29 | 30 | const mapDispatchToProps = (dispatch, ownProps) => { 31 | return { 32 | handleClick: (event, selectedFiles) => { 33 | dispatch(initSubList()); 34 | dispatch(setVisibleDialogCopy(true)); 35 | } 36 | }; 37 | }; 38 | 39 | export default connect(mapStateToProps, mapDispatchToProps)(CopyAction); 40 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/CreateFolderAction.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import CreateNewFolderIcon from '@material-ui/icons/CreateNewFolder'; 7 | import { setVisibleDialogCreateFolder } from '../../../Actions/Actions.js'; 8 | 9 | function CreateFolderAction(props) { 10 | const {handleClick, handleClose} = props; 11 | 12 | const handleCloseAfter = (callback) => (event) => { 13 | callback(); 14 | handleClose(); 15 | }; 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | Create folder 24 | 25 | 26 | ); 27 | } 28 | 29 | const mapStateToProps = (state) => { 30 | return { 31 | }; 32 | }; 33 | 34 | const mapDispatchToProps = (dispatch, ownProps) => { 35 | return { 36 | handleClick: (event) => { 37 | dispatch(setVisibleDialogCreateFolder(true)); 38 | } 39 | }; 40 | }; 41 | 42 | export default connect(mapStateToProps, mapDispatchToProps)(CreateFolderAction); 43 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/DownloadAction.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import { downloadFile } from '../../../Actions/Actions.js'; 5 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import CloudDownloadIcon from '@material-ui/icons/CloudDownload'; 8 | 9 | function DownloadAction(props) { 10 | const {handleClick, selectedFiles} = props; 11 | return ( 12 | handleClick(e, selectedFiles)}> 13 | 14 | 15 | 16 | 17 | Download 18 | 19 | 20 | ); 21 | } 22 | 23 | const mapStateToProps = (state) => { 24 | return { 25 | selectedFiles: state.selectedFiles 26 | }; 27 | }; 28 | 29 | const mapDispatchToProps = (dispatch, ownProps) => { 30 | return { 31 | handleClick: (event, selectedFiles) => { 32 | dispatch(downloadFile(selectedFiles[0].name)); 33 | } 34 | }; 35 | }; 36 | 37 | export default connect(mapStateToProps, mapDispatchToProps)(DownloadAction); 38 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/EditAction.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import { getFileContentForEdit } from '../../../Actions/Actions.js'; 5 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; 8 | 9 | function OpenAction(props) { 10 | const {handleClick, selectedFiles} = props; 11 | return ( 12 | handleClick(e, selectedFiles)}> 13 | 14 | 15 | 16 | 17 | Edit 18 | 19 | 20 | ); 21 | } 22 | 23 | const mapStateToProps = (state) => { 24 | return { 25 | selectedFiles: state.selectedFiles 26 | }; 27 | }; 28 | 29 | const mapDispatchToProps = (dispatch, ownProps) => { 30 | return { 31 | handleClick: (event, selectedFiles) => { 32 | dispatch(getFileContentForEdit(selectedFiles[0].name)); 33 | } 34 | }; 35 | }; 36 | 37 | export default connect(mapStateToProps, mapDispatchToProps)(OpenAction); 38 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/MoveAction.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import HowToVoteIcon from '@material-ui/icons/HowToVote'; 7 | import { initSubList, setVisibleDialogMove } from '../../../Actions/Actions.js'; 8 | 9 | function MoveAction(props) { 10 | const {handleClick, selectedFiles} = props; 11 | 12 | return ( 13 | handleClick(e, selectedFiles)}> 14 | 15 | 16 | 17 | 18 | Move 19 | 20 | 21 | ); 22 | } 23 | 24 | const mapStateToProps = (state) => { 25 | return { 26 | selectedFiles: state.selectedFiles 27 | }; 28 | }; 29 | 30 | const mapDispatchToProps = (dispatch, ownProps) => { 31 | return { 32 | handleClick: (event, selectedFiles) => { 33 | dispatch(initSubList()); 34 | dispatch(setVisibleDialogMove(true)); 35 | } 36 | }; 37 | }; 38 | 39 | export default connect(mapStateToProps, mapDispatchToProps)(MoveAction); 40 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/OpenAction.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import { getFileContent, enterToDirectory } from '../../../Actions/Actions.js'; 5 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; 8 | 9 | function OpenAction(props) { 10 | const {handleClick, selectedFiles} = props; 11 | return ( 12 | handleClick(e, selectedFiles)}> 13 | 14 | 15 | 16 | 17 | Open 18 | 19 | 20 | ); 21 | } 22 | 23 | const mapStateToProps = (state) => { 24 | return { 25 | selectedFiles: state.selectedFiles 26 | }; 27 | }; 28 | 29 | const mapDispatchToProps = (dispatch, ownProps) => { 30 | return { 31 | handleClick: (event, selectedFiles) => { 32 | if (selectedFiles[0].type === 'dir') { 33 | dispatch(enterToDirectory(selectedFiles[0].name)); 34 | return; 35 | } 36 | dispatch(getFileContent(selectedFiles[0].name)); 37 | } 38 | }; 39 | }; 40 | 41 | export default connect(mapStateToProps, mapDispatchToProps)(OpenAction); 42 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/RemoveAction.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import { removeItems } from '../../../Actions/Actions.js'; 5 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import DeleteIcon from '@material-ui/icons/Delete'; 8 | 9 | function RemoveAction(props) { 10 | const {handleClick, selectedFiles} = props; 11 | return ( 12 | handleClick(e, selectedFiles)}> 13 | 14 | 15 | 16 | 17 | Remove 18 | 19 | 20 | ); 21 | } 22 | 23 | const mapStateToProps = (state) => { 24 | return { 25 | selectedFiles: state.selectedFiles 26 | }; 27 | }; 28 | 29 | const mapDispatchToProps = (dispatch, ownProps) => { 30 | return { 31 | handleClick: (event, selectedFiles) => { 32 | dispatch(removeItems(selectedFiles)); 33 | } 34 | }; 35 | }; 36 | 37 | export default connect(mapStateToProps, mapDispatchToProps)(RemoveAction); 38 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/RenameAction.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import WrapTextIcon from '@material-ui/icons/WrapText'; 7 | import { setVisibleDialogRename } from '../../../Actions/Actions.js'; 8 | 9 | function MoveAction(props) { 10 | const {handleClick, selectedFiles} = props; 11 | 12 | return ( 13 | handleClick(e, selectedFiles)}> 14 | 15 | 16 | 17 | 18 | Rename 19 | 20 | 21 | ); 22 | } 23 | 24 | const mapStateToProps = (state) => { 25 | return { 26 | selectedFiles: state.selectedFiles 27 | }; 28 | }; 29 | 30 | const mapDispatchToProps = (dispatch, ownProps) => { 31 | return { 32 | handleClick: (event, selectedFiles) => { 33 | dispatch(setVisibleDialogRename(true)); 34 | } 35 | }; 36 | }; 37 | 38 | export default connect(mapStateToProps, mapDispatchToProps)(MoveAction); 39 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/UploadFileAction.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import CloudUploadIcon from '@material-ui/icons/CloudUpload'; 7 | import { setVisibleDialogUploadFile } from '../../../Actions/Actions.js'; 8 | 9 | function UploadFileAction(props) { 10 | const {handleClick, handleClose} = props; 11 | 12 | const handleCloseAfter = (callback) => (event) => { 13 | callback(); 14 | handleClose(); 15 | }; 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | Upload files 24 | 25 | 26 | ); 27 | } 28 | 29 | const mapStateToProps = (state) => { 30 | return { 31 | }; 32 | }; 33 | 34 | const mapDispatchToProps = (dispatch, ownProps) => { 35 | return { 36 | handleClick: (event) => { 37 | dispatch(setVisibleDialogUploadFile(true)); 38 | } 39 | }; 40 | }; 41 | 42 | export default connect(mapStateToProps, mapDispatchToProps)(UploadFileAction); 43 | -------------------------------------------------------------------------------- /src/Components/Dialogs/Content/Content.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogTitle from '@material-ui/core/DialogTitle'; 7 | import { connect } from 'react-redux'; 8 | import { setVisibleDialogContent } from '../../../Actions/Actions.js'; 9 | 10 | class FormDialog extends Component { 11 | 12 | state = { 13 | lastBlobUrl: null, 14 | content: '...', 15 | loading: false 16 | }; 17 | 18 | componentDidUpdate() { 19 | if (this.props.blobUrl !== this.state.lastBlobUrl) { 20 | this.setState({ 21 | lastBlobUrl: this.props.blobUrl 22 | }); 23 | this.setState({ 24 | loading: true 25 | }); 26 | } 27 | } 28 | 29 | render() { 30 | const { handleClose, open } = this.props; 31 | return ( 32 |
33 | 34 | Viewing file 35 | 36 | 37 | 38 | 39 | 42 | 43 | 44 |
45 | ); 46 | } 47 | } 48 | 49 | const mapStateToProps = (state) => { 50 | return { 51 | open: state.visibleDialogContent, 52 | blobUrl: state.fileContentBlobUrl 53 | }; 54 | }; 55 | 56 | const mapDispatchToProps = (dispatch, ownProps) => { 57 | return { 58 | handleClose: (event) => { 59 | dispatch(setVisibleDialogContent(false)); 60 | }, 61 | handleOpen: (event) => { 62 | dispatch(setVisibleDialogContent(true)); 63 | }, 64 | }; 65 | }; 66 | 67 | export default connect(mapStateToProps, mapDispatchToProps)(FormDialog); 68 | -------------------------------------------------------------------------------- /src/Components/Dialogs/Copy/Copy.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogTitle from '@material-ui/core/DialogTitle'; 7 | import { connect } from 'react-redux'; 8 | import { setVisibleDialogCopy, setSelectedFolderSublist, enterToPreviousDirectorySublist, copyItems } from '../../../Actions/Actions.js'; 9 | import FileListSublist from '../../FileList/FileListSublist/FileListSublist.jsx'; 10 | import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft'; 11 | 12 | class FormDialog extends Component { 13 | 14 | render() { 15 | const { 16 | selectedPath, handleClose, handleSave, open, 17 | canGoBack, canCopy, selectedFiles, handleGoBack 18 | } = this.props; 19 | 20 | return ( 21 | 22 |
23 | 24 | Copy files to { selectedPath.join('/') } 25 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 37 | 40 | 41 |
42 |
43 | ); 44 | } 45 | } 46 | 47 | const mapStateToProps = (state) => { 48 | // prevent copying to same folder 49 | const canCopy = state.path.join('') !== state.pathSublist.join('') + (state.selectedFolderSublist ? state.selectedFolderSublist.name : ''); 50 | 51 | return { 52 | open: state.visibleDialogCopy, 53 | selectedFolderSublist: state.selectedFolderSublist, 54 | selectedPath: state.selectedFolderSublist ? [...state.pathSublist, state.selectedFolderSublist.name] : [], 55 | canGoBack: state.pathSublist.length, 56 | canCopy: state.selectedFolderSublist && canCopy, 57 | selectedFiles: state.selectedFiles 58 | }; 59 | }; 60 | 61 | const mapDispatchToProps = (dispatch, ownProps) => { 62 | return { 63 | handleClose: (event) => { 64 | dispatch(setSelectedFolderSublist(null)); 65 | dispatch(setVisibleDialogCopy(false)); 66 | }, 67 | handleSave: (event, selectedFiles) => { 68 | dispatch(copyItems(selectedFiles)); 69 | }, 70 | handleGoBack: (event) => { 71 | dispatch(setSelectedFolderSublist(null)); 72 | dispatch(enterToPreviousDirectorySublist()); 73 | } 74 | }; 75 | }; 76 | 77 | export default connect(mapStateToProps, mapDispatchToProps)(FormDialog); 78 | -------------------------------------------------------------------------------- /src/Components/Dialogs/CreateFolder/CreateFolder.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import TextField from '@material-ui/core/TextField'; 4 | import Dialog from '@material-ui/core/Dialog'; 5 | import DialogActions from '@material-ui/core/DialogActions'; 6 | import DialogContent from '@material-ui/core/DialogContent'; 7 | import DialogTitle from '@material-ui/core/DialogTitle'; 8 | import { connect } from 'react-redux'; 9 | import { createNewFolder, setVisibleDialogCreateFolder } from '../../../Actions/Actions.js'; 10 | 11 | class FormDialog extends Component { 12 | 13 | render() { 14 | const { handleClose, handleSave, value, open } = this.props; 15 | 16 | return ( 17 | 18 |
19 | Create folder 20 | 21 | 22 | 23 | 24 | 27 | 30 | 31 |
32 |
33 | ); 34 | } 35 | } 36 | 37 | const mapStateToProps = (state) => { 38 | return { 39 | createFolderName: state.createFolderName, 40 | open: state.visibleDialogCreateFolder 41 | }; 42 | }; 43 | 44 | const mapDispatchToProps = (dispatch, ownProps) => { 45 | return { 46 | handleClose: event => { 47 | dispatch(setVisibleDialogCreateFolder(false)); 48 | }, 49 | handleSave: event => { 50 | event.preventDefault(); 51 | const folderName = event.currentTarget.form.querySelector('input').value; 52 | dispatch(createNewFolder(folderName)); 53 | } 54 | }; 55 | }; 56 | 57 | export default connect(mapStateToProps, mapDispatchToProps)(FormDialog); 58 | -------------------------------------------------------------------------------- /src/Components/Dialogs/Dialogs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DialogContent from './Content/Content.jsx'; 3 | import DialogEdit from './Edit/Edit.jsx'; 4 | import DialogCreateFolder from './CreateFolder/CreateFolder.jsx'; 5 | import DialogRename from './Rename/Rename.jsx'; 6 | import DialogMove from './Move/Move.jsx'; 7 | import DialogCopy from './Copy/Copy.jsx'; 8 | import DialogUploadFile from './UploadFile/UploadFile.jsx'; 9 | 10 | function Dialogs(props) { 11 | return ( 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | ); 22 | } 23 | 24 | export default Dialogs; 25 | -------------------------------------------------------------------------------- /src/Components/Dialogs/Edit/Edit.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogContentText from '@material-ui/core/DialogContentText'; 7 | import DialogTitle from '@material-ui/core/DialogTitle'; 8 | import { connect } from 'react-redux'; 9 | import { setVisibleDialogEdit } from '../../../Actions/Actions.js'; 10 | 11 | class FormDialog extends Component { 12 | 13 | state = { 14 | lastBlobUrl: null, 15 | content: null, 16 | loading: false 17 | }; 18 | 19 | componentDidUpdate() { 20 | if (this.props.blobUrl !== this.state.lastBlobUrl) { 21 | this.setState({ 22 | lastBlobUrl: this.props.blobUrl 23 | }); 24 | this.setState({ 25 | loading: true 26 | }); 27 | 28 | this.props.blobUrl && fetch(this.props.blobUrl).then(r => { 29 | return r.text(); 30 | }).then(t => { 31 | this.setState({ 32 | content: t 33 | }); 34 | this.setState({ 35 | loading: false 36 | }); 37 | }); 38 | } 39 | } 40 | 41 | render() { 42 | const { handleClose, handleSave, open } = this.props; 43 | const textAreaStyle = { 44 | width: '100%', 45 | minHeight: '300px' 46 | }; 47 | const textArea =