├── .eslintrc
├── .gitignore
├── .npmignore
├── .storybook
├── main.js
└── preview.js
├── .travis.yml
├── .vscode
└── settings.json
├── LICENSE
├── PULL_REQUEST_FORMAT.md
├── README.md
├── demo-site
├── README.md
├── _config.yml
├── _includes
│ ├── head.html
│ └── required_static.html
├── demos.js
├── demos.sass
├── flat-simple.js
├── grouped-thumbnails.js
├── index.html
├── nested-editable.js
├── package.json
├── test.sass
├── webpack.config.js
└── yarn.lock
├── index.js
├── package.json
├── src
├── actions
│ ├── default.js
│ └── index.js
├── base-file.js
├── base-folder.js
├── browser.js
├── browser.sass
├── confirmations
│ ├── default.js
│ ├── index.js
│ └── multiple.js
├── constants.js
├── details
│ ├── default.js
│ └── index.js
├── files
│ ├── index.js
│ ├── list-thumbnail.js
│ ├── simple-list-thumbnail.js
│ ├── table.js
│ └── utils.js
├── filters
│ ├── default.js
│ └── index.js
├── folders
│ ├── index.js
│ ├── list-thumbnail.js
│ └── table.js
├── groupers
│ ├── by-folder.js
│ ├── by-modified.js
│ ├── index.js
│ └── utils.js
├── headers
│ ├── index.js
│ └── table.js
├── icons
│ ├── FontAwesome.js
│ └── index.js
├── index.js
├── list-style.sass
├── sorters
│ ├── by-modified.js
│ ├── by-name.js
│ ├── index.js
│ └── utils.js
├── table-style.sass
└── utils.js
├── stories
├── index.js
└── stories.sass
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "react",
4 | "@typescript-eslint"
5 | ],
6 | "parser": "@typescript-eslint/parser",
7 | "rules": {
8 |
9 | // stops the 'React' was used before it was defined no-use-before-define error for .js files.
10 | "no-use-before-define": "off",
11 | "@typescript-eslint/no-use-before-define": ["error"],
12 |
13 | "react/prop-types": 1,
14 | "react/jsx-handler-names": "off",
15 | "react/jsx-uses-react": "error",
16 | "react/jsx-uses-vars": "error",
17 |
18 | "comma-dangle": [2, {
19 | "arrays": "always-multiline",
20 | "exports": "always-multiline",
21 | "functions": "never",
22 | "imports": "always-multiline",
23 | "objects": "always-multiline"
24 | }],
25 |
26 | "space-before-function-paren": [2, {
27 | "anonymous": "never",
28 | "named": "never",
29 | "asyncArrow": "always"
30 | }],
31 |
32 | "jsx-quotes": [2, "prefer-double"],
33 | "no-var": 2,
34 |
35 | "semi": [2, "never"],
36 | "lines-between-class-members": "off"
37 | },
38 |
39 | "extends": ["standard", "standard-react"],
40 |
41 | "env": {
42 | "browser": true,
43 | "jest": true,
44 | "jasmine": true
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | node_modules
31 | jspm_packages
32 |
33 | # Optional npm cache directory
34 | .npm
35 |
36 | # Optional REPL history
37 | .node_repl_history
38 |
39 | # Built files
40 | dist/
41 |
42 | # Jekyll
43 | demo-site/_site/
44 | demo-site/dist/
45 |
46 | .yalc
47 | *yalc.lock
48 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src/**/*.jsx
2 | demo-site/
3 | webpack.config.js
4 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const path = require("path")
2 |
3 | module.exports = {
4 | stories: ['../stories/index.js'],
5 | webpackFinal: (config) => {
6 | config.module.rules.push({
7 | test: /\.(sass|scss)$/,
8 | use: ['style-loader', 'css-loader', 'sass-loader'],
9 | include: [
10 | path.resolve(__dirname, '../stories')
11 | ]
12 | })
13 | config.resolve.extensions.push('.sass')
14 | return config
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import "../dist/react-keyed-file-browser.css"
3 |
4 | export const decorators = [(Story) => ];
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | dist: bionic
3 | node_js: node
4 | cache: yarn
5 |
6 | branches:
7 | only:
8 | - master
9 |
10 | script:
11 | - yarn lint
12 |
13 | notifications:
14 | email: false
15 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "javascript.implicitProjectConfig.checkJs": true
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Uptick Pty Ltd
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/PULL_REQUEST_FORMAT.md:
--------------------------------------------------------------------------------
1 | # Pull Request Format
2 |
3 | ## Description
4 |
5 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
6 |
7 |
8 | ## Changelog
9 |
10 | Please delete options that are not relevant.
11 |
12 | - [ ] Bug fix (non-breaking change which fixes an issue)
13 | - [ ] New feature (non-breaking change which adds functionality)
14 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
15 | - [ ] This change requires a documentation update
16 |
17 | ## How Has This Been Tested?
18 |
19 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
20 |
21 | - [ ] Test A
22 | - [ ] Test B
23 |
24 |
25 | ## Checklist:
26 |
27 | - [ ] My code follows the style guidelines of this project
28 | - [ ] I have performed a self-review of my own code
29 | - [ ] I have commented my code, particularly in hard-to-understand areas
30 | - [ ] I have made corresponding changes to the documentation
31 | - [ ] My changes generate no new warnings
32 | - [ ] I have added tests that prove my fix is effective or that my feature works
33 | - [ ] New and existing unit tests pass locally with my changes
34 | - [ ] Any dependent changes have been merged and published in downstream modules
35 | - [ ] I have checked my code and corrected any misspellings
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-keyed-file-browser
2 |
3 | [](https://travis-ci.org/uptick/react-keyed-file-browser)
4 | [](http://badge.fury.io/js/react-keyed-file-browser)
5 | 
6 |
7 | Folder based file browser given a flat keyed list of objects, powered by React.
8 |
9 | ## Live Demo
10 |
11 | Check out the live demo here: http://uptick.github.io/react-keyed-file-browser/
12 |
13 | ## Installation
14 |
15 | Install the package with npm:
16 |
17 | ```bash
18 | # NPM
19 | npm install react-keyed-file-browser
20 |
21 | # Yarn
22 | yarn add react-keyed-file-browser
23 | ```
24 |
25 |
26 | ```javascript
27 | import React from 'react'
28 | import ReactDOM from 'react-dom'
29 |
30 | import FileBrowser from 'react-keyed-file-browser'
31 |
32 | ReactDOM.render(
33 | ,
36 | document.getElementById('root')
37 | );
38 | ```
39 |
40 | Include icons from FontAwesome 4:
41 |
42 | ```javascript
43 | import React from 'react'
44 | import ReactDOM from 'react-dom'
45 |
46 | import FileBrowser, { Icons } from 'react-keyed-file-browser'
47 |
48 | // this imports the FontAwesome Icon Styles
49 | import 'font-awesome/css/font-awesome.min.css'
50 |
51 | var mount = document.querySelectorAll('div.browser-mount');
52 | ReactDOM.render(
53 | ,
57 | mount[0]
58 | );
59 | ```
60 |
61 | or your own icons by specifying as so:
62 | ```javascript
63 | ,
67 | Image: ,
68 | PDF: ,
69 | Rename: ,
70 | Folder: ,
71 | FolderOpen: ,
72 | Delete: ,
73 | Loading: ,
74 | }}
75 | />
76 | ```
77 |
78 | Optionally, include the built css with an import:
79 |
80 | ```scss
81 | @import 'node_modules/react-keyed-file-browser/dist/react-keyed-file-browser.css';
82 | ```
83 | or tag:
84 |
85 | ```html
86 |
90 | ```
91 | ## Examples
92 |
93 | Using a custom drag and drop provider.
94 | ```javascript
95 | import { RawFileBrowser } from 'react-keyed-file-browser'
96 |
97 | import { DndProvider } from 'react-dnd'
98 | import { HTML5Backend } from 'react-dnd-html5-backend'
99 |
100 |
101 |
102 |
103 | ```
104 |
105 |
106 | Full reference documentation coming soon. For now, take a look at the reference implementation with
107 | event handlers on the live demo at http://uptick.github.io/react-keyed-file-browser/.
108 |
--------------------------------------------------------------------------------
/demo-site/README.md:
--------------------------------------------------------------------------------
1 | ```
2 | rm yarn.lock && rm -rf node_modules
3 | yarn
4 | yarn build
5 | jekyll build
6 | jekyll serve
7 | ```
8 |
--------------------------------------------------------------------------------
/demo-site/_config.yml:
--------------------------------------------------------------------------------
1 | exclude: []
2 |
3 | layouts_dir: 'node_modules/uptick-demo-site/dist'
4 |
5 | package_name: React Keyed File Browser
6 | package_github_url: https://github.com/uptick/react-keyed-file-browser
7 | package_npm_url: https://www.npmjs.com/package/react-keyed-file-browser
8 |
--------------------------------------------------------------------------------
/demo-site/_includes/head.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/demo-site/_includes/required_static.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/demo-site/demos.js:
--------------------------------------------------------------------------------
1 | import { init } from 'uptick-demo-site'
2 |
3 | import FlatSimpleDemo from './flat-simple.js'
4 | import NestedEditableDemo from './nested-editable.js'
5 | import GroupedThumbnailsDemo from './grouped-thumbnails.js'
6 |
7 | init();
8 |
--------------------------------------------------------------------------------
/demo-site/demos.sass:
--------------------------------------------------------------------------------
1 | @import 'node_modules/uptick-demo-site/dist/uptick-demo-site'
2 |
3 | @import 'node_modules/react-keyed-file-browser/src/browser'
4 |
5 | $fa-font-path: "https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/fonts" !default
6 | @import 'node_modules/font-awesome/scss/font-awesome'
7 |
--------------------------------------------------------------------------------
/demo-site/flat-simple.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import Moment from 'moment'
4 |
5 | import FileBrowser from 'react-keyed-file-browser'
6 |
7 | const mount = document.querySelectorAll('div.demo-mount-flat-simple')
8 | ReactDOM.render(
9 | ,
28 | mount[0]
29 | )
30 |
--------------------------------------------------------------------------------
/demo-site/grouped-thumbnails.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import Moment from 'moment'
4 |
5 | import FileBrowser, { FileRenderers, FolderRenderers, Groupers, Icons } from 'react-keyed-file-browser'
6 |
7 | const mount = document.querySelectorAll('div.demo-mount-grouped-thumbnails')
8 | ReactDOM.render(
9 | ,
59 | mount[0]
60 | )
61 |
--------------------------------------------------------------------------------
/demo-site/index.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: base
3 | ---
4 |
5 |
8 |
To participate in the demonstration you will need:
9 |
10 | Javascript enabled
11 | A modern browser
12 |
13 |
All examples given are written with babel loaders to support es6 (stage-0) and JSX syntax.
14 |
15 |
Simple Flat & Read-Only Example
16 |
This example demonstrates read-only use of the browser, with a flat list of files (the keys do
17 | not contain any forward slashes so no folders are drawn).
18 |
19 |
Loading ...
23 |
24 |
Nested Table with Event Handlers
25 |
In this example, the files are contained within common folders as indicated by the forward
26 | slash in the file keys.
27 |
Simple event handlers are also provided as props to the browser, which allow it to respond to
28 | actions on the files. The presence of these handlers enables the buttons and/or the drag &
29 | drop responsiveness.
30 |
This example is not connected to any file storage backend; it simply takes all changes made by
31 | the user and assumes that they would have been successful.
32 |
33 |
Loading ...
37 |
38 |
Different Renderers and Groupers
39 |
In this example, the files and folder renderers have been replaced with Thumbnail renderers.
40 | Files are grouped by their month modified rather than their keyed folder structure.
41 |
42 |
Loading ...
46 |
47 |
--------------------------------------------------------------------------------
/demo-site/nested-editable.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import Moment from 'moment'
4 |
5 | import FileBrowser, {Icons} from 'react-keyed-file-browser'
6 |
7 | class NestedEditableDemo extends React.Component {
8 | state = {
9 | files: [
10 | {
11 | key: 'photos/animals/cat in a hat.png',
12 | modified: +Moment().subtract(1, 'hours'),
13 | size: 1.5 * 1024 * 1024,
14 | },
15 | {
16 | key: 'photos/animals/kitten_ball.png',
17 | modified: +Moment().subtract(3, 'days'),
18 | size: 545 * 1024,
19 | },
20 | {
21 | key: 'photos/animals/elephants.png',
22 | modified: +Moment().subtract(3, 'days'),
23 | size: 52 * 1024,
24 | },
25 | {
26 | key: 'photos/funny fall.gif',
27 | modified: +Moment().subtract(2, 'months'),
28 | size: 13.2 * 1024 * 1024,
29 | },
30 | {
31 | key: 'photos/holiday.jpg',
32 | modified: +Moment().subtract(25, 'days'),
33 | size: 85 * 1024,
34 | },
35 | {
36 | key: 'documents/letter chunks.doc',
37 | modified: +Moment().subtract(15, 'days'),
38 | size: 480 * 1024,
39 | },
40 | {
41 | key: 'documents/export.pdf',
42 | modified: +Moment().subtract(15, 'days'),
43 | size: 4.2 * 1024 * 1024,
44 | },
45 | ],
46 | }
47 |
48 | handleCreateFolder = (key) => {
49 | this.setState(state => {
50 | state.files = state.files.concat([{
51 | key: key,
52 | }])
53 | return state
54 | })
55 | }
56 | handleCreateFiles = (files, prefix) => {
57 | this.setState(prevState => {
58 | const newFiles = files.map((file) => {
59 | let newKey = prefix
60 | if (prefix !== '' && prefix.substring(prefix.length - 1, prefix.length) !== '/') {
61 | newKey += '/'
62 | }
63 | const invalidChar = ['/', '\\']
64 | if (invalidChar.some(char => file.name.indexOf(char) !== -1)) return
65 | newKey += file.name
66 | return {
67 | key: newKey,
68 | size: file.size,
69 | modified: +Moment(),
70 | }
71 | })
72 | const uniqueNewFiles = []
73 | newFiles.forEach((newFile) => {
74 | const exists = prevState.files.some(existingFile => (existingFile.key === newFile.key))
75 | if (!exists) uniqueNewFiles.push(newFile)
76 | })
77 | const updatedFiles = [...prevState.files, uniqueNewFiles]
78 | return { files: updatedFiles }
79 | })
80 | }
81 | handleRenameFolder = (oldKey, newKey) => {
82 | this.setState(state => {
83 | const newFiles = []
84 | state.files.map((file) => {
85 | if (file.key.substr(0, oldKey.length) === oldKey) {
86 | newFiles.push({
87 | ...file,
88 | key: file.key.replace(oldKey, newKey),
89 | modified: +Moment(),
90 | })
91 | } else {
92 | newFiles.push(file)
93 | }
94 | })
95 | state.files = newFiles
96 | return state
97 | })
98 | }
99 | handleRenameFile = (oldKey, newKey) => {
100 | this.setState(state => {
101 | const newFiles = []
102 | state.files.map((file) => {
103 | if (file.key === oldKey) {
104 | newFiles.push({
105 | ...file,
106 | key: newKey,
107 | modified: +Moment(),
108 | })
109 | } else {
110 | newFiles.push(file)
111 | }
112 | })
113 | state.files = newFiles
114 | return state
115 | })
116 | }
117 | handleDeleteFolder = (folderKey) => {
118 | this.setState(state => {
119 | const newFiles = []
120 | state.files.map((file) => {
121 | if (file.key.substr(0, folderKey.length) !== folderKey) {
122 | newFiles.push(file)
123 | }
124 | })
125 | state.files = newFiles
126 | return state
127 | })
128 | }
129 | handleDeleteFile = (fileKey) => {
130 | this.setState(state => {
131 | const newFiles = []
132 | state.files.map((file) => {
133 | if (file.key !== fileKey) {
134 | newFiles.push(file)
135 | }
136 | })
137 | state.files = newFiles
138 | return state
139 | })
140 | }
141 |
142 | render() {
143 | return (
144 |
157 | )
158 | }
159 | }
160 |
161 | const mount = document.querySelectorAll('div.demo-mount-nested-editable')
162 | ReactDOM.render(
163 | ,
164 | mount[0]
165 | )
166 |
--------------------------------------------------------------------------------
/demo-site/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-keyed-file-browser-demo-site",
3 | "version": "1.0.0",
4 | "description": "Demo site for react-keyed-file-browser package",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "collect-static": "cp -a node_modules/font-awesome/fonts/. dist/",
9 | "build-css": "./node_modules/node-sass/bin/node-sass demos.sass dist/demos.css; npm run collect-static",
10 | "watch-css": "npm run build-css; ./node_modules/node-sass/bin/node-sass demos.sass dist/demos.css --watch",
11 | "build-js": "./node_modules/webpack/bin/webpack.js",
12 | "watch-js": "./node_modules/webpack/bin/webpack.js --watch",
13 | "build": "npm run build-js; npm run build-css",
14 | "watch": "parallel --ungroup ::: \"npm run watch-js\" \"npm run watch-css\""
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "react-keyed-file-browser"
19 | },
20 | "author": "ABA Systems Pty Ltd",
21 | "license": "MIT",
22 | "dependencies": {
23 | "uptick-demo-site": "latest",
24 | "font-awesome": "^4.7.0",
25 | "moment": "^2.16.0",
26 | "react": "^0.14.8",
27 | "react-dom": "^0.14.8",
28 | "react-keyed-file-browser": "^1.4.0"
29 | },
30 | "devDependencies": {
31 | "@babel/core": "^7.2.2",
32 | "@babel/plugin-proposal-class-properties": "^7.3.0",
33 | "@babel/plugin-proposal-decorators": "^7.3.0",
34 | "@babel/plugin-proposal-export-default-from": "^7.2.0",
35 | "@babel/preset-env": "^7.3.1",
36 | "@babel/preset-react": "^7.0.0",
37 | "babel-cli": "^6.6.4",
38 | "babel-loader": "^8.0.5",
39 | "node-sass": "^4.13.1",
40 | "sass-loader": "^7.1.0",
41 | "style-loader": "^0.23.1",
42 | "webpack": "^4.29.1",
43 | "webpack-cli": "^3.2.3",
44 | "webpack-node-externals": "^1.5.4"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/demo-site/test.sass:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uptick/react-keyed-file-browser/0d4370243a48079c4fe6f26a82ab692a4b4b57bd/demo-site/test.sass
--------------------------------------------------------------------------------
/demo-site/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: './demos.js',
3 | output: {
4 | path: __dirname + '/dist',
5 | filename: 'demos.js',
6 | },
7 | module: {
8 | rules: [
9 | {
10 | test: /\.(js|jsx)$/,
11 | exclude: /node_modules/,
12 | loader: 'babel-loader',
13 | options: {
14 | rootMode: 'upward',
15 | },
16 | },
17 | ],
18 | },
19 | }
20 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./dist/react-keyed-file-browser')
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-keyed-file-browser",
3 | "version": "1.14.0",
4 | "description": "Folder based file browser given a flat keyed list of objects, powered by React.",
5 | "main": "index.js",
6 | "scripts": {
7 | "publish-demo": "git branch -D gh-pages; git push origin --delete gh-pages; git checkout -b gh-pages; cd demo-site; yarn; npm run build; cd ..; git add .; git add -f demo-site/dist; git add -f demo-site/node_modules/uptick-demo-site/dist; git commit -m \"Demo site build\"; git push origin gh-pages; git checkout master; git push origin `git subtree split --prefix demo-site gh-pages`:gh-pages --force;",
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "build-js": "./node_modules/.bin/webpack --mode=production",
10 | "build-css": "./node_modules/.bin/node-sass src/browser.sass dist/react-keyed-file-browser.css",
11 | "build": "npm run build-js && npm run build-css",
12 | "prepublish": "npm run build",
13 | "storybook": "start-storybook -p 9001",
14 | "lint": "eslint src"
15 | },
16 | "files": [
17 | "dist/**"
18 | ],
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/uptick/react-keyed-file-browser.git"
22 | },
23 | "keywords": [
24 | "react",
25 | "keyed",
26 | "file",
27 | "document",
28 | "browser",
29 | "s3"
30 | ],
31 | "author": "Uptick Pty Ltd",
32 | "license": "MIT",
33 | "bugs": {
34 | "url": "https://github.com/uptick/react-keyed-file-browser/issues"
35 | },
36 | "homepage": "https://github.com/uptick/react-keyed-file-browser#readme",
37 | "dependencies": {
38 | "classnames": "^2.2.6",
39 | "date-fns": "^2.22.1",
40 | "lodash.flow": "^3.5.0",
41 | "prop-types": "^15.7.2",
42 | "react-dnd": "^14.0.2",
43 | "react-dnd-html5-backend": "^14.0"
44 | },
45 | "peerDependencies": {
46 | "react": "0.14.x || 15.x || 16.x || 17.x"
47 | },
48 | "devDependencies": {
49 | "@sambego/storybook-state": "^2.0.1",
50 | "@storybook/react": "^6.2.9",
51 | "@typescript-eslint/eslint-plugin": "^4.26.0",
52 | "@typescript-eslint/parser": "^4.26.0",
53 | "css-loader": "^6.2.0",
54 | "esbuild": "^0.12.5",
55 | "esbuild-loader": "^2.13.1",
56 | "eslint": "^7.27.0",
57 | "eslint-config-standard": "*",
58 | "eslint-config-standard-react": "*",
59 | "eslint-plugin-import": "*",
60 | "eslint-plugin-node": "*",
61 | "eslint-plugin-promise": "*",
62 | "eslint-plugin-react": "*",
63 | "eslint-plugin-standard": "*",
64 | "font-awesome": "^4.7.0",
65 | "node-sass": "^6.0.0",
66 | "react": "^17.0.2",
67 | "react-dom": "^17.0.2",
68 | "sass-loader": "^12.1.0",
69 | "style-loader": "^3.1.0",
70 | "typescript": "^4.3.2",
71 | "webpack": "^4.29.1",
72 | "webpack-cli": "^4.7.0",
73 | "webpack-node-externals": "^1.5.4"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/actions/default.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Actions = (props) => {
5 | const {
6 | selectedItems,
7 | isFolder,
8 | icons,
9 | nameFilter,
10 |
11 | canCreateFolder,
12 | onCreateFolder,
13 |
14 | canRenameFile,
15 | onRenameFile,
16 |
17 | canRenameFolder,
18 | onRenameFolder,
19 |
20 | canDeleteFile,
21 | onDeleteFile,
22 |
23 | canDeleteFolder,
24 | onDeleteFolder,
25 |
26 | canDownloadFile,
27 | onDownloadFile,
28 |
29 | canDownloadFolder,
30 | onDownloadFolder,
31 |
32 | } = props
33 |
34 | /** @type any */
35 | let actions = []
36 |
37 | if (selectedItems.length) {
38 | // Something is selected. Build custom actions depending on what it is.
39 | const selectedItemsAction = selectedItems.filter(item => item.action)
40 | if (selectedItemsAction.length === selectedItems.length && [...new Set(selectedItemsAction)].length === 1) {
41 | // Selected item has an active action against it. Disable all other actions.
42 | let actionText
43 | switch (selectedItemsAction[0].action) {
44 | case 'delete':
45 | actionText = 'Deleting ...'
46 | break
47 |
48 | case 'rename':
49 | actionText = 'Renaming ...'
50 | break
51 |
52 | default:
53 | actionText = 'Moving ...'
54 | break
55 | }
56 |
57 | actions = (
58 | // TODO: Enable plugging in custom spinner.
59 |
60 | {icons.Loading} {actionText}
61 |
62 | )
63 | } else {
64 | if (isFolder && canCreateFolder && !nameFilter) {
65 | actions.push(
66 |
67 |
72 | {icons.Folder}
73 | Add Subfolder
74 |
75 |
76 | )
77 | }
78 |
79 | const itemsWithoutKeyDerived = selectedItems.find(item => !item.keyDerived)
80 | if (!itemsWithoutKeyDerived && !isFolder && canRenameFile && selectedItems.length === 1) {
81 | actions.push(
82 |
83 |
88 | {icons.Rename}
89 | Rename
90 |
91 |
92 | )
93 | } else if (!itemsWithoutKeyDerived && isFolder && canRenameFolder) {
94 | actions.push(
95 |
96 |
101 | {icons.Rename}
102 | Rename
103 |
104 |
105 | )
106 | }
107 |
108 | if (!itemsWithoutKeyDerived && !isFolder && canDeleteFile) {
109 | actions.push(
110 |
111 |
116 | {icons.Delete}
117 | Delete
118 |
119 |
120 | )
121 | } else if (!itemsWithoutKeyDerived && isFolder && canDeleteFolder) {
122 | actions.push(
123 |
124 |
129 | {icons.Delete}
130 | Delete
131 |
132 |
133 | )
134 | }
135 |
136 | if ((!isFolder && canDownloadFile) || (isFolder && canDownloadFolder)) {
137 | actions.push(
138 |
139 |
144 | {icons.Download}
145 | Download
146 |
147 |
148 | )
149 | }
150 |
151 | if (actions.length) {
152 | actions = ()
153 | } else {
154 | actions = (
)
155 | }
156 | }
157 | } else {
158 | // Nothing selected: We're in the 'root' folder. Only allowed action is adding a folder.
159 | if (canCreateFolder && !nameFilter) {
160 | actions.push(
161 |
162 |
167 | {icons.Folder}
168 | Add Folder
169 |
170 |
171 | )
172 | }
173 |
174 | if (actions.length) {
175 | actions = ()
176 | } else {
177 | actions = (
)
178 | }
179 | }
180 |
181 | return actions
182 | }
183 |
184 | Actions.propTypes = {
185 | selectedItems: PropTypes.arrayOf(PropTypes.object),
186 | isFolder: PropTypes.bool,
187 | icons: PropTypes.object,
188 | nameFilter: PropTypes.string,
189 |
190 | canCreateFolder: PropTypes.bool,
191 | onCreateFolder: PropTypes.func,
192 |
193 | canRenameFile: PropTypes.bool,
194 | onRenameFile: PropTypes.func,
195 |
196 | canRenameFolder: PropTypes.bool,
197 | onRenameFolder: PropTypes.func,
198 |
199 | canDeleteFile: PropTypes.bool,
200 | onDeleteFile: PropTypes.func,
201 |
202 | canDeleteFolder: PropTypes.bool,
203 | onDeleteFolder: PropTypes.func,
204 |
205 | canDownloadFile: PropTypes.bool,
206 | onDownloadFile: PropTypes.func,
207 |
208 | canDownloadFolder: PropTypes.bool,
209 | onDownloadFolder: PropTypes.func,
210 | }
211 |
212 | Actions.defaultProps = {
213 | selectedItems: [],
214 | isFolder: false,
215 | icons: {},
216 | nameFilter: '',
217 |
218 | canCreateFolder: false,
219 | onCreateFolder: null,
220 |
221 | canRenameFile: false,
222 | onRenameFile: null,
223 |
224 | canRenameFolder: false,
225 | onRenameFolder: null,
226 |
227 | canDeleteFile: false,
228 | onDeleteFile: null,
229 |
230 | canDeleteFolder: false,
231 | onDeleteFolder: null,
232 |
233 | canDownloadFile: false,
234 | onDownloadFile: null,
235 |
236 | canDownloadFolder: false,
237 | onDownloadFolder: null,
238 | }
239 |
240 | export default Actions
241 |
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import DefaultAction from './default'
2 |
3 | export {
4 | DefaultAction,
5 | }
6 |
--------------------------------------------------------------------------------
/src/base-file.js:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import PropTypes from 'prop-types'
3 | import React from 'react'
4 | import { moveFilesAndFolders } from './utils'
5 | import { extensionMapping } from './constants.js'
6 |
7 | class BaseFile extends React.Component {
8 | static propTypes = {
9 | fileKey: PropTypes.string,
10 | url: PropTypes.string,
11 |
12 | newKey: PropTypes.string,
13 | isRenaming: PropTypes.bool,
14 |
15 | connectDragSource: PropTypes.func,
16 | connectDropTarget: PropTypes.func,
17 | isDragging: PropTypes.bool,
18 | action: PropTypes.string,
19 |
20 | browserProps: PropTypes.shape({
21 | icons: PropTypes.object,
22 | select: PropTypes.func,
23 | beginAction: PropTypes.func,
24 | endAction: PropTypes.func,
25 | preview: PropTypes.func,
26 |
27 | createFiles: PropTypes.func,
28 | moveFile: PropTypes.func,
29 | moveFolder: PropTypes.func,
30 | renameFile: PropTypes.func,
31 | deleteFile: PropTypes.func,
32 | }),
33 | }
34 |
35 | state = {
36 | newName: this.getName(),
37 | }
38 |
39 | selectFileNameFromRef(element) {
40 | if (element) {
41 | const currentName = element.value
42 | const pointIndex = currentName.lastIndexOf('.')
43 | element.setSelectionRange(0, pointIndex || currentName.length)
44 | element.focus()
45 | }
46 | }
47 |
48 | getName() {
49 | let name = this.props.newKey || this.props.fileKey
50 | const slashIndex = name.lastIndexOf('/')
51 | if (slashIndex !== -1) {
52 | name = name.substr(slashIndex + 1)
53 | }
54 | return name
55 | }
56 | getExtension() {
57 | const blobs = this.props.fileKey.split('.')
58 | return blobs[blobs.length - 1].toLowerCase().trim()
59 | }
60 |
61 | getFileType() {
62 | return extensionMapping[this.getExtension()] || 'File'
63 | }
64 |
65 | handleFileClick = (event) => {
66 | event && event.preventDefault()
67 | this.props.browserProps.preview({
68 | url: this.props.url,
69 | name: this.getName(),
70 | key: this.props.fileKey,
71 | extension: this.getExtension(),
72 | })
73 | }
74 | handleItemClick = (event) => {
75 | event.stopPropagation()
76 | this.props.browserProps.select(this.props.fileKey, 'file', event.ctrlKey || event.metaKey, event.shiftKey)
77 | }
78 | handleItemDoubleClick = (event) => {
79 | event.stopPropagation()
80 | this.handleFileClick()
81 | }
82 |
83 | handleRenameClick = (event) => {
84 | if (!this.props.browserProps.renameFile) {
85 | return
86 | }
87 | this.props.browserProps.beginAction('rename', this.props.fileKey)
88 | }
89 | handleNewNameChange = (event) => {
90 | const newName = event.target.value
91 | this.setState({ newName: newName })
92 | }
93 | handleRenameSubmit = (event) => {
94 | if (event) {
95 | event.preventDefault()
96 | }
97 | if (!this.props.browserProps.renameFile) {
98 | return
99 | }
100 | const newName = this.state.newName.trim()
101 | if (newName.length === 0) {
102 | // todo: move to props handler
103 | // window.notify({
104 | // style: 'error',
105 | // title: 'Invalid new file name',
106 | // body: 'File name cannot be blank',
107 | // })
108 | return
109 | }
110 | const invalidChar = ['/', '\\']
111 | if (invalidChar.some(char => newName.indexOf(char) !== -1)) return
112 | // todo: move to props handler
113 | // window.notify({
114 | // style: 'error',
115 | // title: 'Invalid new file name',
116 | // body: 'File names cannot contain forward slashes.',
117 | // })
118 | let newKey = newName
119 | const slashIndex = this.props.fileKey.lastIndexOf('/')
120 | if (slashIndex !== -1) {
121 | newKey = `${this.props.fileKey.substr(0, slashIndex)}/${newName}`
122 | }
123 | this.props.browserProps.renameFile(this.props.fileKey, newKey)
124 | }
125 |
126 | handleDeleteClick = (event) => {
127 | if (!this.props.browserProps.deleteFile) {
128 | return
129 | }
130 | this.props.browserProps.beginAction('delete', this.props.fileKey)
131 | }
132 | handleDeleteSubmit = (event) => {
133 | event.preventDefault()
134 | if (!this.props.browserProps.deleteFile) {
135 | return
136 | }
137 |
138 | this.props.browserProps.deleteFile(this.props.browserProps.actionTargets)
139 | }
140 |
141 | handleCancelEdit = (event) => {
142 | this.props.browserProps.endAction()
143 | }
144 |
145 | connectDND(render) {
146 | const inAction = (this.props.isDragging || this.props.action)
147 | if (
148 | typeof this.props.browserProps.moveFile === 'function' &&
149 | !inAction &&
150 | !this.props.isRenaming
151 | ) {
152 | render = this.props.connectDragSource(render)
153 | }
154 | if (
155 | typeof this.props.browserProps.createFiles === 'function' ||
156 | typeof this.props.browserProps.moveFile === 'function' ||
157 | typeof this.props.browserProps.moveFolder === 'function'
158 | ) {
159 | render = this.props.connectDropTarget(render)
160 | }
161 | return render
162 | }
163 | }
164 |
165 | const dragSource = {
166 | beginDrag(props) {
167 | if (
168 | !props.browserProps.selection.length ||
169 | !props.browserProps.selection.includes(props.fileKey)
170 | ) {
171 | props.browserProps.select(props.fileKey, 'file')
172 | }
173 | return {
174 | key: props.fileKey,
175 | }
176 | },
177 |
178 | endDrag(props, monitor, component) {
179 | moveFilesAndFolders(props, monitor, component)
180 | },
181 | }
182 |
183 | function dragCollect(connect, monitor) {
184 | return {
185 | connectDragPreview: connect.dragPreview(),
186 | connectDragSource: connect.dragSource(),
187 | isDragging: monitor.isDragging(),
188 | }
189 | }
190 |
191 | const targetSource = {
192 | drop(props, monitor) {
193 | if (monitor.didDrop()) {
194 | return
195 | }
196 | const key = props.newKey || props.fileKey
197 | const slashIndex = key.lastIndexOf('/')
198 | const path = (slashIndex !== -1) ? key.substr(0, slashIndex + 1) : ''
199 | const item = monitor.getItem()
200 | if (item.files && props.browserProps.createFiles) {
201 | props.browserProps.createFiles(item.files, path)
202 | }
203 | return {
204 | path: path,
205 | }
206 | },
207 | }
208 |
209 | function targetCollect(connect, monitor) {
210 | return {
211 | connectDropTarget: connect.dropTarget(),
212 | isOver: monitor.isOver({ shallow: true }),
213 | }
214 | }
215 |
216 | const BaseFileConnectors = {
217 | dragSource,
218 | dragCollect,
219 | targetSource,
220 | targetCollect,
221 | }
222 |
223 | export default BaseFile
224 | export {
225 | BaseFileConnectors,
226 | }
227 |
--------------------------------------------------------------------------------
/src/base-folder.js:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import PropTypes from 'prop-types'
3 | import React from 'react'
4 | import { moveFilesAndFolders } from './utils'
5 |
6 | class BaseFolder extends React.Component {
7 | static propTypes = {
8 | name: PropTypes.string,
9 | fileKey: PropTypes.string,
10 |
11 | newName: PropTypes.string,
12 | keyDerived: PropTypes.bool,
13 | isDraft: PropTypes.bool,
14 | isRenaming: PropTypes.bool,
15 | isDeleting: PropTypes.bool,
16 |
17 | connectDragSource: PropTypes.func,
18 | connectDropTarget: PropTypes.func,
19 | isDragging: PropTypes.bool,
20 | action: PropTypes.string,
21 |
22 | browserProps: PropTypes.shape({
23 | select: PropTypes.func,
24 | toggleFolder: PropTypes.func,
25 | beginAction: PropTypes.func,
26 | endAction: PropTypes.func,
27 | preview: PropTypes.func,
28 |
29 | createFiles: PropTypes.func,
30 | createFolder: PropTypes.func,
31 | moveFile: PropTypes.func,
32 | moveFolder: PropTypes.func,
33 | renameFolder: PropTypes.func,
34 | deleteFolder: PropTypes.func,
35 | }),
36 | }
37 |
38 | state = {
39 | newName: this.props.isDraft ? 'New folder' : this.getName(),
40 | }
41 |
42 | selectFolderNameFromRef(element) {
43 | if (element) {
44 | const currentName = element.value
45 | element.setSelectionRange(0, currentName.length)
46 | element.focus()
47 | }
48 | }
49 |
50 | getName() {
51 | if (this.props.name) {
52 | return this.props.name
53 | }
54 | const folders = this.props.fileKey.split('/')
55 | return this.props.newName || folders[folders.length - 2]
56 | }
57 |
58 | handleFolderClick = (event) => {
59 | event.stopPropagation()
60 | this.props.browserProps.select(this.props.fileKey, 'folder', event.ctrlKey || event.metaKey, event.shiftKey)
61 | }
62 | handleFolderDoubleClick = (event) => {
63 | event.stopPropagation()
64 | this.toggleFolder()
65 | }
66 |
67 | handleRenameClick = (event) => {
68 | if (!this.props.browserProps.renameFolder) {
69 | return
70 | }
71 | this.props.browserProps.beginAction('rename', this.props.fileKey)
72 | }
73 | handleNewNameChange = (event) => {
74 | const newName = event.target.value
75 | this.setState({ newName: newName })
76 | }
77 | handleRenameSubmit = (event) => {
78 | event.preventDefault()
79 | event.stopPropagation()
80 | if (!this.props.browserProps.renameFolder && !this.props.isDraft) {
81 | return
82 | }
83 | const newName = this.state.newName.trim()
84 | if (newName.length === 0) {
85 | // todo: move to props handler
86 | // window.notify({
87 | // style: 'error',
88 | // title: 'Invalid new folder name',
89 | // body: 'Folder name cannot be blank',
90 | // })
91 | return
92 | }
93 | const invalidChar = ['/', '\\']
94 | if (invalidChar.some(char => newName.indexOf(char) !== -1)) return
95 | // todo: move to props handler
96 | // window.notify({
97 | // style: 'error',
98 | // title: 'Invalid new folder name',
99 | // body: 'Folder names cannot contain forward slashes.',
100 | // })
101 |
102 | let newKey = this.props.fileKey.substr(0, this.props.fileKey.substr(0, this.props.fileKey.length - 1).lastIndexOf('/'))
103 | if (newKey.length) {
104 | newKey += '/'
105 | }
106 | newKey += newName
107 | newKey += '/'
108 | if (this.props.isDraft) {
109 | this.props.browserProps.createFolder(newKey)
110 | } else {
111 | this.props.browserProps.renameFolder(this.props.fileKey, newKey)
112 | }
113 | }
114 |
115 | handleDeleteClick = (event) => {
116 | if (!this.props.browserProps.deleteFolder) {
117 | return
118 | }
119 | this.props.browserProps.beginAction('delete', this.props.fileKey)
120 | }
121 | handleDeleteSubmit = (event) => {
122 | event.preventDefault()
123 | event.stopPropagation()
124 | if (!this.props.browserProps.deleteFolder) {
125 | return
126 | }
127 | this.props.browserProps.deleteFolder(this.props.browserProps.actionTargets)
128 | }
129 |
130 | handleCancelEdit = (event) => {
131 | this.props.browserProps.endAction()
132 | }
133 |
134 | toggleFolder = () => {
135 | this.props.browserProps.toggleFolder(this.props.fileKey)
136 | }
137 |
138 | connectDND(render) {
139 | const inAction = (this.props.isDragging || this.props.action)
140 | if (this.props.keyDerived) {
141 | if (
142 | typeof this.props.browserProps.moveFolder === 'function' &&
143 | !inAction &&
144 | !this.props.isRenaming &&
145 | !this.props.isDeleting
146 | ) {
147 | render = this.props.connectDragSource(render)
148 | }
149 | if (
150 | typeof this.props.browserProps.createFiles === 'function' ||
151 | typeof this.props.browserProps.moveFolder === 'function' ||
152 | typeof this.props.browserProps.moveFile === 'function'
153 | ) {
154 | render = this.props.connectDropTarget(render)
155 | }
156 | }
157 | return render
158 | }
159 | }
160 |
161 | const dragSource = {
162 | beginDrag(props) {
163 | if (!props.browserProps.selection.length) {
164 | props.browserProps.select(props.fileKey, 'folder')
165 | }
166 | return {
167 | key: props.fileKey,
168 | }
169 | },
170 |
171 | endDrag(props, monitor, component) {
172 | moveFilesAndFolders(props, monitor, component)
173 | },
174 | }
175 |
176 | function dragCollect(connect, monitor) {
177 | return {
178 | connectDragPreview: connect.dragPreview(),
179 | connectDragSource: connect.dragSource(),
180 | isDragging: monitor.isDragging(),
181 | }
182 | }
183 |
184 | const BaseFolderConnectors = {
185 | dragSource,
186 | dragCollect,
187 | }
188 |
189 | export default BaseFolder
190 | export {
191 | BaseFolderConnectors,
192 | }
193 |
--------------------------------------------------------------------------------
/src/browser.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { Component } from 'react'
3 | // drag and drop
4 | import { DndProvider } from 'react-dnd'
5 | import { HTML5Backend } from 'react-dnd-html5-backend'
6 |
7 | // default components (most overridable)
8 | import { DefaultDetail } from './details'
9 | import { DefaultFilter } from './filters'
10 |
11 | // default renderers
12 | import { TableHeader } from './headers'
13 | import { TableFile } from './files'
14 | import { TableFolder } from './folders'
15 | import { DefaultConfirmDeletion, MultipleConfirmDeletion } from './confirmations'
16 |
17 | // default processors
18 | import { GroupByFolder } from './groupers'
19 | import { SortByName } from './sorters'
20 |
21 | import { isFolder } from './utils'
22 | import { DefaultAction } from './actions'
23 |
24 | const SEARCH_RESULTS_PER_PAGE = 20
25 | const regexForNewFolderOrFileSelection = /.*\/__new__[/]?$/gm
26 |
27 | function getItemProps(file, browserProps) {
28 | return {
29 | key: `file-${file.key}`,
30 | fileKey: file.key,
31 | isSelected: (browserProps.selection.includes(file.key)),
32 | isOpen: file.key in browserProps.openFolders || browserProps.nameFilter,
33 | isRenaming: browserProps.activeAction === 'rename' && browserProps.actionTargets.includes(file.key),
34 | isDeleting: browserProps.activeAction === 'delete' && browserProps.actionTargets.includes(file.key),
35 | isDraft: !!file.draft,
36 | }
37 | }
38 |
39 | class RawFileBrowser extends React.Component {
40 | static propTypes = {
41 | files: PropTypes.arrayOf(PropTypes.shape({
42 | key: PropTypes.string.isRequired,
43 | modified: PropTypes.number,
44 | size: PropTypes.number,
45 | })).isRequired,
46 | actions: PropTypes.node,
47 | showActionBar: PropTypes.bool.isRequired,
48 | canFilter: PropTypes.bool.isRequired,
49 | showFoldersOnFilter: PropTypes.bool,
50 | noFilesMessage: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
51 | noMatchingFilesMessage: PropTypes.func,
52 | showMoreResults: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
53 |
54 | group: PropTypes.func.isRequired,
55 | sort: PropTypes.func.isRequired,
56 |
57 | icons: PropTypes.shape({
58 | Folder: PropTypes.element,
59 | FolderOpen: PropTypes.element,
60 | File: PropTypes.element,
61 | PDF: PropTypes.element,
62 | Image: PropTypes.element,
63 | Delete: PropTypes.element,
64 | Rename: PropTypes.element,
65 | Loading: PropTypes.element,
66 | Download: PropTypes.element,
67 | }),
68 |
69 | nestChildren: PropTypes.bool.isRequired,
70 | renderStyle: PropTypes.oneOf([
71 | 'list',
72 | 'table',
73 | ]).isRequired,
74 |
75 | startOpen: PropTypes.bool.isRequired, // TODO: remove?
76 |
77 | headerRenderer: PropTypes.func,
78 | headerRendererProps: PropTypes.object,
79 | filterRenderer: PropTypes.func,
80 | filterRendererProps: PropTypes.object,
81 | fileRenderer: PropTypes.func,
82 | fileRendererProps: PropTypes.object,
83 | folderRenderer: PropTypes.func,
84 | folderRendererProps: PropTypes.object,
85 | detailRenderer: PropTypes.func,
86 | detailRendererProps: PropTypes.object,
87 | actionRenderer: PropTypes.func,
88 | confirmDeletionRenderer: PropTypes.func,
89 | confirmMultipleDeletionRenderer: PropTypes.func,
90 |
91 | onCreateFiles: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
92 | onCreateFolder: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
93 | onMoveFile: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
94 | onMoveFolder: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
95 | onRenameFile: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
96 | onRenameFolder: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
97 | onDeleteFile: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
98 | onDeleteFolder: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
99 | onDownloadFile: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
100 | onDownloadFolder: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
101 |
102 | onSelect: PropTypes.func,
103 | onSelectFile: PropTypes.func,
104 | onSelectFolder: PropTypes.func,
105 |
106 | onPreviewOpen: PropTypes.func,
107 | onPreviewClose: PropTypes.func,
108 |
109 | onFolderOpen: PropTypes.func,
110 | onFolderClose: PropTypes.func,
111 | }
112 |
113 | static defaultProps = {
114 | showActionBar: true,
115 | canFilter: true,
116 | showFoldersOnFilter: false,
117 | noFilesMessage: 'No files.',
118 | noMatchingFilesMessage: (filter) => `No files matching "${filter}".`,
119 | showMoreResults: 'Show more results',
120 |
121 | group: GroupByFolder,
122 | sort: SortByName,
123 |
124 | nestChildren: false,
125 | renderStyle: 'table',
126 |
127 | startOpen: false,
128 |
129 | headerRenderer: TableHeader,
130 | headerRendererProps: {},
131 | filterRenderer: DefaultFilter,
132 | filterRendererProps: {},
133 | fileRenderer: TableFile,
134 | fileRendererProps: {},
135 | folderRenderer: TableFolder,
136 | folderRendererProps: {},
137 | detailRenderer: DefaultDetail,
138 | detailRendererProps: {},
139 | actionRenderer: DefaultAction,
140 | confirmDeletionRenderer: DefaultConfirmDeletion,
141 | confirmMultipleDeletionRenderer: MultipleConfirmDeletion,
142 |
143 | icons: {},
144 |
145 | onSelect: (fileOrFolder) => { }, // Always called when a file or folder is selected
146 | onSelectFile: (file) => { }, // Called after onSelect, only on file selection
147 | onSelectFolder: (folder) => { }, // Called after onSelect, only on folder selection
148 |
149 | onPreviewOpen: (file) => { }, // File opened
150 | onPreviewClose: (file) => { }, // File closed
151 |
152 | onFolderOpen: (folder) => { }, // Folder opened
153 | onFolderClose: (folder) => { }, // Folder closed
154 | }
155 |
156 | state = {
157 | openFolders: {},
158 | selection: [],
159 | activeAction: null,
160 | actionTargets: [],
161 |
162 | nameFilter: '',
163 | searchResultsShown: SEARCH_RESULTS_PER_PAGE,
164 |
165 | previewFile: null,
166 |
167 | addFolder: null,
168 | }
169 |
170 | componentDidMount() {
171 | if (this.props.renderStyle === 'table' && this.props.nestChildren) {
172 | console.warn('Invalid settings: Cannot nest table children in file browser')
173 | }
174 |
175 | window.addEventListener('click', this.handleGlobalClick)
176 | }
177 |
178 | componentWillUnmount() {
179 | window.removeEventListener('click', this.handleGlobalClick)
180 | }
181 |
182 | getFile = (key) => {
183 | let hasPrefix = false
184 | const exactFolder = this.props.files.find((f) => {
185 | if (f.key.startsWith(key)) {
186 | hasPrefix = true
187 | }
188 | return f.key === key
189 | })
190 | if (exactFolder) {
191 | return exactFolder
192 | }
193 | if (hasPrefix) {
194 | return { key, modified: 0, size: 0, relativeKey: key }
195 | }
196 | }
197 |
198 | // item manipulation
199 | createFiles = (files, prefix) => {
200 | this.setState(prevState => {
201 | const stateChanges = { selection: [] }
202 | if (prefix) {
203 | stateChanges.openFolders = {
204 | ...prevState.openFolders,
205 | [prefix]: true,
206 | }
207 | }
208 | return stateChanges
209 | }, () => {
210 | this.props.onCreateFiles(files, prefix)
211 | })
212 | }
213 |
214 | createFolder = (key) => {
215 | this.setState({
216 | activeAction: null,
217 | actionTargets: [],
218 | selection: [key],
219 | }, () => {
220 | this.props.onCreateFolder(key)
221 | })
222 | }
223 |
224 | moveFile = (oldKey, newKey) => {
225 | this.setState({
226 | activeAction: null,
227 | actionTargets: [],
228 | selection: [newKey],
229 | }, () => {
230 | this.props.onMoveFile(oldKey, newKey)
231 | })
232 | }
233 |
234 | moveFolder = (oldKey, newKey) => {
235 | this.setState(prevState => {
236 | const stateChanges = {
237 | activeAction: null,
238 | actionTargets: [],
239 | selection: [newKey],
240 | }
241 | if (oldKey in prevState.openFolders) {
242 | stateChanges.openFolders = {
243 | ...prevState.openFolders,
244 | [newKey]: true,
245 | }
246 | }
247 | return stateChanges
248 | }, () => {
249 | this.props.onMoveFolder(oldKey, newKey)
250 | })
251 | }
252 |
253 | renameFile = (oldKey, newKey) => {
254 | this.setState({
255 | activeAction: null,
256 | actionTargets: [],
257 | selection: [newKey],
258 | }, () => {
259 | this.props.onRenameFile(oldKey, newKey)
260 | })
261 | }
262 |
263 | renameFolder = (oldKey, newKey) => {
264 | this.setState(prevState => {
265 | const stateChanges = {
266 | activeAction: null,
267 | actionTargets: [],
268 | }
269 | if (prevState.selection[0].substr(0, oldKey.length) === oldKey) {
270 | stateChanges.selection = [prevState.selection[0].replace(oldKey, newKey)]
271 | }
272 | if (oldKey in prevState.openFolders) {
273 | stateChanges.openFolders = {
274 | ...prevState.openFolders,
275 | [newKey]: true,
276 | }
277 | }
278 | return stateChanges
279 | }, () => {
280 | this.props.onRenameFolder(oldKey, newKey)
281 | })
282 | }
283 |
284 | deleteFile = (keys) => {
285 | this.setState({
286 | activeAction: null,
287 | actionTargets: [],
288 | selection: [],
289 | }, () => {
290 | this.props.onDeleteFile(keys)
291 | })
292 | }
293 |
294 | deleteFolder = (key) => {
295 | this.setState(prevState => {
296 | const stateChanges = {
297 | activeAction: null,
298 | actionTargets: [],
299 | selection: [],
300 | }
301 | if (key in prevState.openFolders) {
302 | stateChanges.openFolders = { ...prevState.openFolders }
303 | delete stateChanges.openFolders[key]
304 | }
305 | return stateChanges
306 | }, () => {
307 | this.props.onDeleteFolder(key)
308 | })
309 | }
310 |
311 | downloadFile = (keys) => {
312 | this.setState({
313 | activeAction: null,
314 | actionTargets: [],
315 | }, () => {
316 | this.props.onDownloadFile(keys)
317 | })
318 | }
319 |
320 | downloadFolder = (keys) => {
321 | this.setState({
322 | activeAction: null,
323 | actionTargets: [],
324 | }, () => {
325 | this.props.onDownloadFolder(keys)
326 | })
327 | }
328 |
329 | // browser manipulation
330 | beginAction = (action, keys) => {
331 | this.setState({
332 | activeAction: action,
333 | actionTargets: keys || [],
334 | })
335 | }
336 |
337 | endAction = () => {
338 | if (this.state.selection && this.state.selection.length > 0 && (
339 | this.state.selection.filter((selection) => selection.match(regexForNewFolderOrFileSelection)).length > 0
340 | )) {
341 | this.setState({ selection: [] })
342 | }
343 | this.beginAction(null, null)
344 | }
345 |
346 | select = (key, selectedType, ctrlKey, shiftKey) => {
347 | const { actionTargets } = this.state
348 | const shouldClearState = actionTargets.length && !actionTargets.includes(key)
349 | const selected = this.getFile(key)
350 |
351 | let newSelection = [key]
352 | if (ctrlKey || shiftKey) {
353 | const indexOfKey = this.state.selection.indexOf(key)
354 | if (indexOfKey !== -1) {
355 | newSelection = [...this.state.selection.slice(0, indexOfKey), ...this.state.selection.slice(indexOfKey + 1)]
356 | } else {
357 | newSelection = [...this.state.selection, key]
358 | }
359 | }
360 |
361 | this.setState(prevState => ({
362 | selection: newSelection,
363 | actionTargets: shouldClearState ? [] : actionTargets,
364 | activeAction: shouldClearState ? null : prevState.activeAction,
365 | }), () => {
366 | this.props.onSelect(selected)
367 |
368 | if (selectedType === 'file') this.props.onSelectFile(selected)
369 | if (selectedType === 'folder') this.props.onSelectFolder(selected)
370 | })
371 | }
372 |
373 | preview = (file) => {
374 | if (this.state.previewFile && this.state.previewFile.key !== file.key) this.closeDetail()
375 |
376 | this.setState({
377 | previewFile: file,
378 | }, () => {
379 | this.props.onPreviewOpen(file)
380 | })
381 | }
382 |
383 | closeDetail = () => {
384 | this.setState({
385 | previewFile: null,
386 | }, () => {
387 | this.props.onPreviewClose(this.state.previewFile)
388 | })
389 | }
390 |
391 | handleShowMoreClick = (event) => {
392 | event.preventDefault()
393 | this.setState(prevState => ({
394 | searchResultsShown: prevState.searchResultsShown + SEARCH_RESULTS_PER_PAGE,
395 | }))
396 | }
397 |
398 | toggleFolder = (folderKey) => {
399 | const isOpen = folderKey in this.state.openFolders
400 | this.setState(prevState => {
401 | const stateChanges = {
402 | openFolders: { ...prevState.openFolders },
403 | }
404 | if (isOpen) {
405 | delete stateChanges.openFolders[folderKey]
406 | } else {
407 | stateChanges.openFolders[folderKey] = true
408 | }
409 | return stateChanges
410 | }, () => {
411 | const callback = isOpen ? 'onFolderClose' : 'onFolderOpen'
412 | this.props[callback](this.getFile(folderKey), this.getBrowserProps())
413 | })
414 | }
415 |
416 | openFolder = (folderKey) => {
417 | this.setState(prevState => ({
418 | openFolders: {
419 | ...prevState.openFolders,
420 | [folderKey]: true,
421 | },
422 | }), () => {
423 | this.props.onFolderOpen(this.getFile(folderKey), this.getBrowserProps())
424 | })
425 | }
426 |
427 | // event handlers
428 | handleGlobalClick = (event) => {
429 | const inBrowser = !!(this.browserRef && this.browserRef.contains(event.target))
430 |
431 | if (!inBrowser) {
432 | this.setState({
433 | selection: [],
434 | actionTargets: [],
435 | activeAction: null,
436 | })
437 | }
438 | }
439 | handleActionBarRenameClick = (event) => {
440 | event.preventDefault()
441 | this.beginAction('rename', this.state.selection)
442 | }
443 | handleActionBarDeleteClick = (event) => {
444 | event.preventDefault()
445 | this.beginAction('delete', this.state.selection)
446 | }
447 | handleActionBarAddFolderClick = (event) => {
448 | event.preventDefault()
449 | if (this.state.activeAction === 'createFolder') {
450 | return
451 | }
452 | this.setState(prevState => {
453 | let addKey = ''
454 | if (prevState.selection && prevState.selection.length > 0) {
455 | addKey += prevState.selection
456 | if (addKey.substr(addKey.length - 1, addKey.length) !== '/') {
457 | addKey += '/'
458 | }
459 | }
460 |
461 | if (addKey !== '__new__/' && !addKey.endsWith('/__new__/')) addKey += '__new__/'
462 | const stateChanges = {
463 | actionTargets: [addKey],
464 | activeAction: 'createFolder',
465 | selection: [addKey],
466 | }
467 | if (prevState.selection && prevState.selection.length > 0) {
468 | stateChanges.openFolders = {
469 | ...prevState.openFolders,
470 | [this.state.selection]: true,
471 | }
472 | }
473 | return stateChanges
474 | })
475 | }
476 | handleActionBarDownloadClick = (event) => {
477 | event.preventDefault()
478 |
479 | const files = this.getFiles()
480 | const selectedItems = this.getSelectedItems(files)
481 |
482 | const selectionIsFolder = (selectedItems.length === 1 && isFolder(selectedItems[0]))
483 | if (selectionIsFolder) {
484 | this.downloadFolder(this.state.selection)
485 | return
486 | }
487 |
488 | this.downloadFile(this.state.selection)
489 | }
490 |
491 | updateFilter = (newValue) => {
492 | this.setState({
493 | nameFilter: newValue,
494 | searchResultsShown: SEARCH_RESULTS_PER_PAGE,
495 | })
496 | }
497 |
498 | getBrowserProps() {
499 | return {
500 | // browser config
501 | nestChildren: this.props.nestChildren,
502 | fileRenderer: this.props.fileRenderer,
503 | fileRendererProps: this.props.fileRendererProps,
504 | folderRenderer: this.props.folderRenderer,
505 | folderRendererProps: this.props.folderRendererProps,
506 | confirmDeletionRenderer: this.props.confirmDeletionRenderer,
507 | confirmMultipleDeletionRenderer: this.props.confirmMultipleDeletionRenderer,
508 | icons: this.props.icons,
509 |
510 | // browser state
511 | openFolders: this.state.openFolders,
512 | nameFilter: this.state.nameFilter,
513 | selection: this.state.selection,
514 | activeAction: this.state.activeAction,
515 | actionTargets: this.state.actionTargets,
516 |
517 | // browser manipulation
518 | select: this.select,
519 | openFolder: this.openFolder,
520 | toggleFolder: this.toggleFolder,
521 | beginAction: this.beginAction,
522 | endAction: this.endAction,
523 | preview: this.preview,
524 |
525 | // item manipulation
526 | createFiles: this.props.onCreateFiles ? this.createFiles : undefined,
527 | createFolder: this.props.onCreateFolder ? this.createFolder : undefined,
528 | renameFile: this.props.onRenameFile ? this.renameFile : undefined,
529 | renameFolder: this.props.onRenameFolder ? this.renameFolder : undefined,
530 | moveFile: this.props.onMoveFile ? this.moveFile : undefined,
531 | moveFolder: this.props.onMoveFolder ? this.moveFolder : undefined,
532 | deleteFile: this.props.onDeleteFile ? this.deleteFile : undefined,
533 | deleteFolder: this.props.onDeleteFolder ? this.deleteFolder : undefined,
534 |
535 | getItemProps: getItemProps,
536 | }
537 | }
538 |
539 | renderActionBar(selectedItems) {
540 | const {
541 | icons, canFilter,
542 | filterRendererProps, filterRenderer: FilterRenderer,
543 | actionRenderer: ActionRenderer,
544 | onCreateFolder, onRenameFile, onRenameFolder,
545 | onDeleteFile, onDeleteFolder, onDownloadFile,
546 | onDownloadFolder,
547 | } = this.props
548 | const browserProps = this.getBrowserProps()
549 | const selectionIsFolder = (selectedItems.length === 1 && isFolder(selectedItems[0]))
550 | let filter
551 | if (canFilter) {
552 | filter = (
553 |
558 | )
559 | }
560 |
561 | const actions = (
562 |
592 | )
593 |
594 | return (
595 |
596 | {filter}
597 | {actions}
598 |
599 | )
600 | }
601 |
602 | renderFiles(files, depth) {
603 | const {
604 | fileRenderer: FileRenderer, fileRendererProps,
605 | folderRenderer: FolderRenderer, folderRendererProps,
606 | } = this.props
607 | const browserProps = this.getBrowserProps()
608 | let renderedFiles = []
609 |
610 | files.map((file) => {
611 | const thisItemProps = {
612 | ...browserProps.getItemProps(file, browserProps),
613 | depth: this.state.nameFilter ? 0 : depth,
614 | }
615 |
616 | if (!isFolder(file)) {
617 | renderedFiles.push(
618 |
624 | )
625 | } else {
626 | if (this.props.showFoldersOnFilter || !this.state.nameFilter) {
627 | renderedFiles.push(
628 |
634 | )
635 | }
636 | if (this.state.nameFilter || (thisItemProps.isOpen && !browserProps.nestChildren)) {
637 | renderedFiles = renderedFiles.concat(this.renderFiles(file.children, depth + 1))
638 | }
639 | }
640 | })
641 | return renderedFiles
642 | }
643 |
644 | handleMultipleDeleteSubmit = () => {
645 | console.log(this)
646 | this.deleteFolder(this.state.selection.filter(selection => selection[selection.length - 1] === '/'))
647 | this.deleteFile(this.state.selection.filter(selection => selection[selection.length - 1] !== '/'))
648 | }
649 |
650 | getFiles() {
651 | let files = this.props.files.concat([])
652 | if (this.state.activeAction === 'createFolder') {
653 | files.push({
654 | key: this.state.actionTargets[0],
655 | size: 0,
656 | draft: true,
657 | })
658 | }
659 | if (this.state.nameFilter) {
660 | const filteredFiles = []
661 | const terms = this.state.nameFilter.toLowerCase().split(' ')
662 | files.map((file) => {
663 | let skip = false
664 | terms.map((term) => {
665 | if (file.key.toLowerCase().trim().indexOf(term) === -1) {
666 | skip = true
667 | }
668 | })
669 | if (skip) {
670 | return
671 | }
672 | filteredFiles.push(file)
673 | })
674 | files = filteredFiles
675 | }
676 | if (typeof this.props.group === 'function') {
677 | files = this.props.group(files, '')
678 | } else {
679 | const newFiles = []
680 | files.map((file) => {
681 | if (!isFolder(file)) {
682 | newFiles.push(file)
683 | }
684 | })
685 | files = newFiles
686 | }
687 | if (typeof this.props.sort === 'function') {
688 | files = this.props.sort(files)
689 | }
690 | return files
691 | }
692 |
693 | getSelectedItems(files) {
694 | const { selection } = this.state
695 | const selectedItems = []
696 | const findSelected = (item) => {
697 | if (selection.includes(item.key)) {
698 | selectedItems.push(item)
699 | }
700 | if (item.children) {
701 | item.children.map(findSelected)
702 | }
703 | }
704 | files.map(findSelected)
705 | return selectedItems
706 | }
707 |
708 | render() {
709 | const browserProps = this.getBrowserProps()
710 | const headerProps = {
711 | browserProps,
712 | fileKey: '',
713 | fileCount: this.props.files.length,
714 | }
715 | let renderedFiles
716 |
717 | const files = this.getFiles()
718 | const selectedItems = this.getSelectedItems(files)
719 |
720 | let header
721 | /** @type any */
722 | let contents = this.renderFiles(files, 0)
723 | switch (this.props.renderStyle) {
724 | case 'table':
725 | if (!contents.length) {
726 | if (this.state.nameFilter) {
727 | contents = (
728 |
729 |
730 | {this.props.noMatchingFilesMessage(this.state.nameFilter)}
731 |
732 |
733 | )
734 | } else {
735 | contents = (
736 |
737 |
738 | {this.props.noFilesMessage}
739 |
740 |
741 | )
742 | }
743 | } else {
744 | if (this.state.nameFilter) {
745 | const numFiles = contents.length
746 | contents = contents.slice(0, this.state.searchResultsShown)
747 | if (numFiles > contents.length) {
748 | contents.push(
749 |
750 |
751 |
755 | {this.props.showMoreResults}
756 |
757 |
758 |
759 | )
760 | }
761 | }
762 | }
763 |
764 | if (this.props.headerRenderer) {
765 | header = (
766 |
767 |
771 |
772 | )
773 | }
774 |
775 | renderedFiles = (
776 |
777 | {header}
778 |
779 | {contents}
780 |
781 |
782 | )
783 | break
784 |
785 | case 'list':
786 | if (!contents.length) {
787 | if (this.state.nameFilter) {
788 | contents = ({this.props.noMatchingFilesMessage(this.state.nameFilter)}
)
789 | } else {
790 | contents = ({this.props.noFilesMessage}
)
791 | }
792 | } else {
793 | let more
794 | if (this.state.nameFilter) {
795 | const numFiles = contents.length
796 | contents = contents.slice(0, this.state.searchResultsShown)
797 | if (numFiles > contents.length) {
798 | more = (
799 |
803 | {this.props.showMoreResults}
804 |
805 | )
806 | }
807 | }
808 | contents = (
809 |
813 | )
814 | }
815 |
816 | if (this.props.headerRenderer) {
817 | header = (
818 |
822 | )
823 | }
824 |
825 | renderedFiles = (
826 |
827 | {header}
828 | {contents}
829 |
830 | )
831 | break
832 | }
833 |
834 | const ConfirmMultipleDeletionRenderer = this.props.confirmMultipleDeletionRenderer
835 |
836 | return (
837 |
838 | {this.props.actions}
839 |
{ this.browserRef = el }}>
840 | {this.props.showActionBar && this.renderActionBar(selectedItems)}
841 | {this.state.activeAction === 'delete' && this.state.selection.length > 1 &&
842 |
}
845 |
846 | {renderedFiles}
847 |
848 |
849 | {this.state.previewFile !== null && (
850 |
855 | )}
856 |
857 | )
858 | }
859 | }
860 |
861 | class FileBrowser extends Component {
862 | render() {
863 | return (
864 |
865 |
866 |
867 | )
868 | }
869 | }
870 |
871 | export default FileBrowser
872 |
873 | export { RawFileBrowser }
874 |
--------------------------------------------------------------------------------
/src/browser.sass:
--------------------------------------------------------------------------------
1 | $main-padding: 0.5rem !default
2 | $theme-colour-text: #337ab7 !default
3 |
4 | div.rendered-react-keyed-file-browser
5 | div.action-bar
6 | margin-bottom: $main-padding
7 | flex-wrap: wrap
8 | display: flex
9 | align-items: flex-start
10 |
11 | input[type="search"]
12 | display: block
13 | flex-grow: 2
14 | min-width: 300px
15 | padding: calc($main-padding / 2) $main-padding
16 | line-height: 1em
17 | margin-bottom: $main-padding
18 | border: 0.1rem solid #ddd
19 |
20 | .item-actions
21 | text-align: right
22 | margin: 0
23 | padding: 0
24 |
25 | ul.item-actions
26 | display: block
27 | flex-grow: 1
28 | min-width: 300px
29 |
30 | margin-left: 10px
31 | white-space: nowrap
32 | li
33 | display: inline-block
34 | margin: 0
35 | &:not(:last-child)
36 | margin-right: $main-padding
37 |
38 | @import './table-style'
39 | @import './list-style'
40 |
--------------------------------------------------------------------------------
/src/confirmations/default.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const ConfirmDeletion = (props) => {
5 | const {
6 | children,
7 | handleDeleteSubmit,
8 | handleFileClick,
9 | url,
10 | } = props
11 |
12 | return (
13 |
27 | )
28 | }
29 |
30 | ConfirmDeletion.propTypes = {
31 | children: PropTypes.node,
32 | handleDeleteSubmit: PropTypes.func,
33 | handleFileClick: PropTypes.func,
34 | url: PropTypes.string,
35 | }
36 |
37 | ConfirmDeletion.defaultProps = {
38 | url: '#',
39 | }
40 |
41 | export default ConfirmDeletion
42 |
--------------------------------------------------------------------------------
/src/confirmations/index.js:
--------------------------------------------------------------------------------
1 | import DefaultConfirmDeletion from './default'
2 | import MultipleConfirmDeletion from './multiple'
3 |
4 | export {
5 | DefaultConfirmDeletion,
6 | MultipleConfirmDeletion,
7 | }
8 |
--------------------------------------------------------------------------------
/src/confirmations/multiple.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const MultipleConfirmDeletion = (props) => {
5 | const {
6 | handleDeleteSubmit,
7 | } = props
8 |
9 | return (
10 |
11 | Confirm Deletion
12 |
13 | )
14 | }
15 |
16 | MultipleConfirmDeletion.propTypes = {
17 | handleDeleteSubmit: PropTypes.func,
18 | }
19 |
20 | export default MultipleConfirmDeletion
21 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | const FILETYPE_EXTENSIONS = {
2 | Archive: [
3 | 'zip',
4 | 'rar',
5 | '7z',
6 | ],
7 | Audio: [
8 | 'mp3',
9 | 'ogg',
10 | 'wav',
11 | 'aac',
12 | ],
13 | Excel: [
14 | 'xls',
15 | 'xlsx',
16 | ],
17 | Image: [
18 | 'jpg',
19 | 'jpeg',
20 | 'png',
21 | 'bmp',
22 | ],
23 | PDF: [
24 | 'pdf',
25 | ],
26 | PowerPoint: [
27 | 'ppt',
28 | 'pptx',
29 | ],
30 | Text: [
31 | 'txt',
32 | ],
33 | Video: [
34 | 'mp4',
35 | 'flv',
36 | 'avi',
37 | 'wmv',
38 | 'mov',
39 | ],
40 | Word: [
41 | 'doc',
42 | 'docx',
43 | ],
44 | Code: [
45 | 'js',
46 | ],
47 | }
48 |
49 | const extensionMapping = {}
50 |
51 | for (const [type, extensions] of Object.entries(FILETYPE_EXTENSIONS)) {
52 | for (const extension of extensions) {
53 | extensionMapping[extension] = type
54 | }
55 | }
56 |
57 | export { extensionMapping }
58 |
--------------------------------------------------------------------------------
/src/details/default.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React from 'react'
3 |
4 | class Detail extends React.Component {
5 | static propTypes = {
6 | file: PropTypes.shape({
7 | key: PropTypes.string.isRequired,
8 | name: PropTypes.string.isRequired,
9 | extension: PropTypes.string.isRequired,
10 | url: PropTypes.string,
11 | }).isRequired,
12 | close: PropTypes.func,
13 | }
14 |
15 | handleCloseClick = (event) => {
16 | if (event) {
17 | event.preventDefault()
18 | }
19 | this.props.close()
20 | }
21 |
22 | render() {
23 | let name = this.props.file.key.split('/')
24 | name = name.length ? name[name.length - 1] : ''
25 |
26 | return (
27 |
28 |
Item Detail
29 |
30 | Key
31 | {this.props.file.key}
32 |
33 | Name
34 | {name}
35 |
36 |
Close
37 |
38 | )
39 | }
40 | }
41 |
42 | export default Detail
43 |
--------------------------------------------------------------------------------
/src/details/index.js:
--------------------------------------------------------------------------------
1 | import DefaultDetail from './default.js'
2 |
3 | export {
4 | DefaultDetail,
5 | }
6 |
--------------------------------------------------------------------------------
/src/files/index.js:
--------------------------------------------------------------------------------
1 | import ListThumbnailFile, { RawListThumbnailFile } from './list-thumbnail.js'
2 | import SimpleListThumbnailFile from './simple-list-thumbnail.js'
3 | import TableFile, { RawTableFile } from './table.js'
4 |
5 | export {
6 | ListThumbnailFile,
7 | SimpleListThumbnailFile,
8 | TableFile,
9 |
10 | RawListThumbnailFile,
11 | RawTableFile,
12 | }
13 |
--------------------------------------------------------------------------------
/src/files/list-thumbnail.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ClassNames from 'classnames'
3 | import { DragSource, DropTarget } from 'react-dnd'
4 | import { NativeTypes } from 'react-dnd-html5-backend'
5 | import { formatDistanceToNow } from 'date-fns'
6 | import flow from 'lodash.flow'
7 |
8 | import BaseFile, { BaseFileConnectors } from './../base-file.js'
9 | import { fileSize } from './utils.js'
10 |
11 | class RawListThumbnailFile extends BaseFile {
12 | static defaultProps = {
13 | showName: true,
14 | showSize: true,
15 | showModified: true,
16 | isSelectable: true,
17 | }
18 |
19 | render() {
20 | const {
21 | thumbnail_url: thumbnailUrl, action, url,
22 | isDragging, isRenaming, isSelected, isSelectable, isOver, isDeleting,
23 | showName, showSize, showModified, browserProps, connectDragPreview,
24 | } = this.props
25 |
26 | let icon
27 | if (thumbnailUrl) {
28 | icon = (
29 |
35 | )
36 | } else {
37 | icon = browserProps.icons[this.getFileType()] || browserProps.icons.File
38 | }
39 |
40 | const inAction = (isDragging || action)
41 |
42 | const ConfirmDeletionRenderer = browserProps.confirmDeletionRenderer
43 |
44 | let name
45 | if (showName) {
46 | if (!inAction && isDeleting && browserProps.selection.length === 1) {
47 | name = (
48 |
53 | {this.getName()}
54 |
55 | )
56 | } else if (!inAction && isRenaming) {
57 | name = (
58 |
68 | )
69 | } else {
70 | name = (
71 |
72 | {this.getName()}
73 |
74 | )
75 | }
76 | }
77 |
78 | let size
79 | if (showSize) {
80 | if (!isRenaming && !isDeleting) {
81 | size = (
82 | {fileSize(this.props.size)}
83 | )
84 | }
85 | }
86 | let modified
87 | if (showModified) {
88 | if (!isRenaming && !isDeleting) {
89 | modified = (
90 |
91 | Last modified: {formatDistanceToNow(this.props.modified, { addSuffix: true })}
92 |
93 | )
94 | }
95 | }
96 |
97 | let rowProps = {}
98 | if (isSelectable) {
99 | rowProps = {
100 | onClick: this.handleItemClick,
101 | }
102 | }
103 |
104 | let row = (
105 |
115 |
116 | {icon}
117 | {name}
118 | {size}
119 | {modified}
120 |
121 |
122 | )
123 | if (typeof browserProps.moveFile === 'function') {
124 | row = connectDragPreview(row)
125 | }
126 |
127 | return this.connectDND(row)
128 | }
129 | }
130 |
131 | const ListThumbnailFile = flow(
132 | DragSource('file', BaseFileConnectors.dragSource, BaseFileConnectors.dragCollect),
133 | DropTarget(['file', 'folder', NativeTypes.FILE], BaseFileConnectors.targetSource, BaseFileConnectors.targetCollect)
134 | )(RawListThumbnailFile)
135 |
136 | export default ListThumbnailFile
137 | export { RawListThumbnailFile }
138 |
--------------------------------------------------------------------------------
/src/files/simple-list-thumbnail.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import ListThumbnailFile from './list-thumbnail.js'
4 |
5 | class SimpleListThumbnailFile extends React.Component {
6 | render() {
7 | return (
8 |
15 | )
16 | }
17 | }
18 |
19 | export default SimpleListThumbnailFile
20 |
--------------------------------------------------------------------------------
/src/files/table.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ClassNames from 'classnames'
3 | import { DragSource, DropTarget } from 'react-dnd'
4 | import { NativeTypes } from 'react-dnd-html5-backend'
5 | import { formatDistanceToNow } from 'date-fns'
6 | import flow from 'lodash.flow'
7 |
8 | import BaseFile, { BaseFileConnectors } from './../base-file.js'
9 | import { fileSize } from './utils.js'
10 |
11 | class RawTableFile extends BaseFile {
12 | render() {
13 | const {
14 | isDragging, isDeleting, isRenaming, isOver, isSelected,
15 | action, url, browserProps, connectDragPreview,
16 | depth, size, modified,
17 | } = this.props
18 |
19 | const icon = browserProps.icons[this.getFileType()] || browserProps.icons.File
20 | const inAction = (isDragging || action)
21 |
22 | const ConfirmDeletionRenderer = browserProps.confirmDeletionRenderer
23 |
24 | let name
25 | if (!inAction && isDeleting && browserProps.selection.length === 1) {
26 | name = (
27 |
32 | {icon}
33 | {this.getName()}
34 |
35 | )
36 | } else if (!inAction && isRenaming) {
37 | name = (
38 |
49 | )
50 | } else {
51 | name = (
52 |
57 | {icon}
58 | {this.getName()}
59 |
60 | )
61 | }
62 |
63 | let draggable = (
64 |
65 | {name}
66 |
67 | )
68 | if (typeof browserProps.moveFile === 'function') {
69 | draggable = connectDragPreview(draggable)
70 | }
71 |
72 | const row = (
73 |
83 |
84 |
85 | {draggable}
86 |
87 |
88 | {fileSize(size)}
89 |
90 | {typeof modified === 'undefined' ? '-' : formatDistanceToNow(modified, { addSuffix: true })}
91 |
92 |
93 | )
94 |
95 | return this.connectDND(row)
96 | }
97 | }
98 |
99 | const TableFile = flow(
100 | DragSource('file', BaseFileConnectors.dragSource, BaseFileConnectors.dragCollect),
101 | DropTarget(['file', 'folder', NativeTypes.FILE], BaseFileConnectors.targetSource, BaseFileConnectors.targetCollect)
102 | )(RawTableFile)
103 |
104 | export default TableFile
105 | export { RawTableFile }
106 |
--------------------------------------------------------------------------------
/src/files/utils.js:
--------------------------------------------------------------------------------
1 | function floatPrecision(floatValue, precision) {
2 | floatValue = parseFloat(floatValue)
3 | if (isNaN(floatValue)) { return parseFloat('0').toFixed(precision) } else {
4 | const power = Math.pow(10, precision)
5 | floatValue = (Math.round(floatValue * power) / power).toFixed(precision)
6 | return floatValue.toString()
7 | }
8 | }
9 |
10 | function fileSize(size) {
11 | if (size > 1024) {
12 | const kbSize = size / 1024
13 | if (kbSize > 1024) {
14 | const mbSize = kbSize / 1024
15 | return `${floatPrecision(mbSize, 2)} MB`
16 | }
17 | return `${Math.round(kbSize)} kB`
18 | }
19 | return `${size} B`
20 | }
21 |
22 | export { floatPrecision, fileSize }
23 |
--------------------------------------------------------------------------------
/src/filters/default.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React from 'react'
3 |
4 | class Filter extends React.Component {
5 | static propTypes = {
6 | value: PropTypes.string.isRequired,
7 | updateFilter: PropTypes.func,
8 | }
9 |
10 | handleFilterChange = (event) => {
11 | const newValue = event.target.value
12 | this.props.updateFilter(newValue)
13 | }
14 |
15 | render() {
16 | return (
17 |
23 | )
24 | }
25 | }
26 |
27 | export default Filter
28 |
--------------------------------------------------------------------------------
/src/filters/index.js:
--------------------------------------------------------------------------------
1 | import DefaultFilter from './default.js'
2 |
3 | export {
4 | DefaultFilter,
5 | }
6 |
--------------------------------------------------------------------------------
/src/folders/index.js:
--------------------------------------------------------------------------------
1 | import ListThumbnailFolder, { RawListThumbnailFolder } from './list-thumbnail.js'
2 | import TableFolder, { RawTableFolder } from './table.js'
3 |
4 | export {
5 | ListThumbnailFolder,
6 | TableFolder,
7 |
8 | RawListThumbnailFolder,
9 | RawTableFolder,
10 | }
11 |
--------------------------------------------------------------------------------
/src/folders/list-thumbnail.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ClassNames from 'classnames'
3 | import { DragSource, DropTarget } from 'react-dnd'
4 | import { NativeTypes } from 'react-dnd-html5-backend'
5 | import flow from 'lodash.flow'
6 |
7 | import BaseFolder, { BaseFolderConnectors } from './../base-folder.js'
8 | import { BaseFileConnectors } from './../base-file.js'
9 |
10 | import { isFolder } from '../utils'
11 |
12 | class RawListThumbnailFolder extends BaseFolder {
13 | render() {
14 | const {
15 | isOpen, isDragging, isDeleting, isRenaming, isDraft, isOver, isSelected,
16 | url, action, browserProps, depth, keyDerived, connectDragPreview,
17 | } = this.props
18 |
19 | const icon = browserProps.icons[isOpen ? 'FolderOpen' : 'Folder']
20 |
21 | const inAction = (isDragging || action)
22 |
23 | const ConfirmDeletionRenderer = browserProps.confirmDeletionRenderer
24 |
25 | let name
26 | if (!inAction && isDeleting && browserProps.selection.length === 1) {
27 | name = (
28 |
33 | {this.getName()}
34 |
35 | )
36 | } else if ((!inAction && isRenaming) || isDraft) {
37 | name = (
38 |
39 |
49 |
50 | )
51 | } else {
52 | name = (
53 |
58 | )
59 | }
60 |
61 | let children
62 | if (isOpen && browserProps.nestChildren) {
63 | children = []
64 | for (let childIndex = 0; childIndex < children.length; childIndex++) {
65 | const file = children[childIndex]
66 |
67 | const thisItemProps = {
68 | ...browserProps.getItemProps(file, browserProps),
69 | depth: depth + 1,
70 | }
71 |
72 | if (!isFolder(file)) {
73 | children.push(
74 |
80 | )
81 | } else {
82 | children.push(
83 |
89 | )
90 | }
91 | }
92 | if (children.length) {
93 | children = ()
94 | } else {
95 | children = (No items in this folder
)
96 | }
97 | }
98 |
99 | let folder = (
100 |
111 |
112 | {icon}
113 | {name}
114 |
115 | {children}
116 |
117 | )
118 | if (typeof browserProps.moveFolder === 'function' && keyDerived) {
119 | folder = connectDragPreview(folder)
120 | }
121 |
122 | return this.connectDND(folder)
123 | }
124 | }
125 |
126 | const ListThumbnailFolder = flow(
127 | DragSource('folder', BaseFolderConnectors.dragSource, BaseFolderConnectors.dragCollect),
128 | DropTarget(['file', 'folder', NativeTypes.FILE], BaseFileConnectors.targetSource, BaseFileConnectors.targetCollect)
129 | )(RawListThumbnailFolder)
130 |
131 | export default ListThumbnailFolder
132 | export { RawListThumbnailFolder }
133 |
--------------------------------------------------------------------------------
/src/folders/table.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ClassNames from 'classnames'
3 | import { DragSource, DropTarget } from 'react-dnd'
4 | import { NativeTypes } from 'react-dnd-html5-backend'
5 | import flow from 'lodash.flow'
6 |
7 | import BaseFolder, { BaseFolderConnectors } from './../base-folder.js'
8 | import { BaseFileConnectors } from './../base-file.js'
9 |
10 | class RawTableFolder extends BaseFolder {
11 | render() {
12 | const {
13 | isOpen, isDragging, isDeleting, isRenaming, isDraft, isOver, isSelected,
14 | action, url, browserProps, connectDragPreview, depth,
15 | } = this.props
16 |
17 | const icon = browserProps.icons[isOpen ? 'FolderOpen' : 'Folder']
18 | const inAction = (isDragging || action)
19 |
20 | const ConfirmDeletionRenderer = browserProps.confirmDeletionRenderer
21 |
22 | let name
23 | if (!inAction && isDeleting && browserProps.selection.length === 1) {
24 | name = (
25 |
30 | {icon}
31 | {this.getName()}
32 |
33 | )
34 | } else if ((!inAction && isRenaming) || isDraft) {
35 | name = (
36 |
37 |
48 |
49 | )
50 | } else {
51 | name = (
52 |
58 | )
59 | }
60 |
61 | let draggable = (
62 |
63 | {name}
64 |
65 | )
66 | if (typeof browserProps.moveFile === 'function') {
67 | draggable = connectDragPreview(draggable)
68 | }
69 |
70 | const folder = (
71 |
81 |
82 |
83 | {draggable}
84 |
85 |
86 |
87 |
88 |
89 | )
90 |
91 | return this.connectDND(folder)
92 | }
93 | }
94 |
95 | const TableFolder = flow(
96 | DragSource('folder', BaseFolderConnectors.dragSource, BaseFolderConnectors.dragCollect),
97 | DropTarget(['file', 'folder', NativeTypes.FILE], BaseFileConnectors.targetSource, BaseFileConnectors.targetCollect)
98 | )(RawTableFolder)
99 |
100 | export default TableFolder
101 | export { RawTableFolder }
102 |
--------------------------------------------------------------------------------
/src/groupers/by-folder.js:
--------------------------------------------------------------------------------
1 | import { isFolder } from '../utils'
2 |
3 | export default function(files, root) {
4 | const fileTree = {
5 | contents: [],
6 | children: {},
7 | }
8 |
9 | files.map((file) => {
10 | file.relativeKey = (file.newKey || file.key).substr(root.length)
11 | let currentFolder = fileTree
12 | const folders = file.relativeKey.split('/')
13 | folders.map((folder, folderIndex) => {
14 | if (folderIndex === folders.length - 1 && isFolder(file)) {
15 | for (const key in file) {
16 | currentFolder[key] = file[key]
17 | }
18 | }
19 | if (folder === '') {
20 | return
21 | }
22 | const isAFile = (!isFolder(file) && (folderIndex === folders.length - 1))
23 | if (isAFile) {
24 | currentFolder.contents.push({
25 | ...file,
26 | keyDerived: true,
27 | })
28 | } else {
29 | if (folder in currentFolder.children === false) {
30 | currentFolder.children[folder] = {
31 | contents: [],
32 | children: {},
33 | }
34 | }
35 | currentFolder = currentFolder.children[folder]
36 | }
37 | })
38 | })
39 |
40 | function addAllChildren(level, prefix) {
41 | if (prefix !== '') {
42 | prefix += '/'
43 | }
44 | let files = []
45 | for (const folder in level.children) {
46 | files.push({
47 | ...level.children[folder],
48 | contents: undefined,
49 | keyDerived: true,
50 | key: root + prefix + folder + '/',
51 | relativeKey: prefix + folder + '/',
52 | children: addAllChildren(level.children[folder], prefix + folder),
53 | size: 0,
54 | })
55 | }
56 | files = files.concat(level.contents)
57 | return files
58 | }
59 |
60 | files = addAllChildren(fileTree, '')
61 | return files
62 | }
63 |
--------------------------------------------------------------------------------
/src/groupers/by-modified.js:
--------------------------------------------------------------------------------
1 | import { format, startOfMonth, endOfMonth } from 'date-fns'
2 | import { relativeTimeWindows } from './utils'
3 | import { isFolder } from '../utils'
4 |
5 | export default function(files, root) {
6 | const timeWindows = relativeTimeWindows()
7 |
8 | for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
9 | const file = files[fileIndex]
10 | if (isFolder(file)) { continue }
11 | const newFile = {
12 | ...file,
13 | keyDerived: true,
14 | }
15 |
16 | let allocated = false
17 | const fileModified = +newFile.modified
18 | for (let windex = 0; windex < timeWindows.length; windex++) {
19 | const timeWindow = timeWindows[windex]
20 | if (fileModified > +timeWindow.begins && fileModified <= +timeWindow.ends) {
21 | timeWindow.items.push(newFile)
22 | allocated = true
23 | break
24 | }
25 | }
26 | if (!allocated) {
27 | const newWindow = {
28 | name: format(newFile.modified, 'MMMM yyyy'),
29 | begins: startOfMonth(newFile.modified),
30 | ends: endOfMonth(newFile.modified),
31 | items: [],
32 | }
33 | newWindow.items.push(newFile)
34 | timeWindows.push(newWindow)
35 | }
36 | }
37 |
38 | const grouped = []
39 | for (let windex = 0; windex < timeWindows.length; windex++) {
40 | const timeWindow = timeWindows[windex]
41 | if (!timeWindow.items.length) { continue }
42 | grouped.push({
43 | key: `${timeWindow.name.toLowerCase().replace(' ', '_')}/`,
44 | name: timeWindow.name,
45 | children: timeWindow.items,
46 | size: 0,
47 | })
48 | }
49 |
50 | return grouped
51 | }
52 |
--------------------------------------------------------------------------------
/src/groupers/index.js:
--------------------------------------------------------------------------------
1 | import GroupByFolder from './by-folder.js'
2 | import GroupByModifiedRelative from './by-modified.js'
3 |
4 | export {
5 | GroupByFolder,
6 | GroupByModifiedRelative,
7 | }
8 |
--------------------------------------------------------------------------------
/src/groupers/utils.js:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import {
3 | startOfToday,
4 | endOfToday,
5 | startOfYesterday,
6 | endOfYesterday,
7 | startOfWeek,
8 | endOfWeek,
9 | addWeeks,
10 | startOfMonth,
11 | endOfMonth,
12 | getMonth,
13 | } from 'date-fns'
14 |
15 | function relativeTimeWindows() {
16 | const windows = []
17 | const now = new Date()
18 | windows.push({
19 | name: 'Today',
20 | begins: startOfToday(),
21 | ends: endOfToday(),
22 | items: [],
23 | })
24 | windows.push({
25 | name: 'Yesterday',
26 | begins: startOfYesterday(),
27 | ends: endOfYesterday(),
28 | items: [],
29 | })
30 | windows.push({
31 | name: 'Earlier this Week',
32 | begins: startOfWeek(now),
33 | ends: endOfWeek(now),
34 | items: [],
35 | })
36 | windows.push({
37 | name: 'Last Week',
38 | begins: startOfWeek(addWeeks(now, -1)),
39 | ends: endOfWeek(addWeeks(now, -1)),
40 | items: [],
41 | })
42 | if (getMonth(windows[windows.length - 1].begins) === getMonth(now)) {
43 | windows.push({
44 | name: 'Earlier this Month',
45 | begins: startOfMonth(now),
46 | ends: endOfMonth(now),
47 | items: [],
48 | })
49 | }
50 | return windows
51 | }
52 |
53 | export { relativeTimeWindows }
54 |
--------------------------------------------------------------------------------
/src/headers/index.js:
--------------------------------------------------------------------------------
1 | import TableHeader from './table.js'
2 |
3 | export {
4 | TableHeader,
5 | }
6 |
--------------------------------------------------------------------------------
/src/headers/table.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React from 'react'
3 | import ClassNames from 'classnames'
4 |
5 | import { DropTarget } from 'react-dnd'
6 | import { NativeTypes } from 'react-dnd-html5-backend'
7 |
8 | import { BaseFileConnectors } from './../base-file.js'
9 |
10 | class RawTableHeader extends React.Component {
11 | static propTypes = {
12 | select: PropTypes.func,
13 | fileKey: PropTypes.string,
14 |
15 | connectDropTarget: PropTypes.func,
16 | isOver: PropTypes.bool,
17 | isSelected: PropTypes.func,
18 |
19 | browserProps: PropTypes.shape({
20 | createFiles: PropTypes.func,
21 | moveFolder: PropTypes.func,
22 | moveFile: PropTypes.func,
23 | }),
24 | }
25 |
26 | handleHeaderClick(event) {
27 | this.props.select(this.props.fileKey)
28 | }
29 |
30 | render() {
31 | const header = (
32 |
38 | File
39 | Size
40 | Last Modified
41 |
42 | )
43 |
44 | if (
45 | typeof this.props.browserProps.createFiles === 'function' ||
46 | typeof this.props.browserProps.moveFile === 'function' ||
47 | typeof this.props.browserProps.moveFolder === 'function'
48 | ) {
49 | return this.props.connectDropTarget(header)
50 | } else {
51 | return header
52 | }
53 | }
54 | }
55 |
56 | const TableHeader = DropTarget(
57 | ['file', 'folder', NativeTypes.FILE],
58 | BaseFileConnectors.targetSource,
59 | BaseFileConnectors.targetCollect
60 | )(RawTableHeader)
61 |
62 | export default TableHeader
63 | export { RawTableHeader }
64 |
--------------------------------------------------------------------------------
/src/icons/FontAwesome.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | // See https://allthingssmitty.com/2016/09/12/checking-if-font-awesome-loaded/
4 | const IsFontAwesomeLoaded = (version) => {
5 | const prefix = version === 4 ? 'fa' : 'fas'
6 | const fontNames = version === 4
7 | ? ['FontAwesome', '"FontAwesome"']
8 | : ['"Font Awesome 5 Free"', '"Font Awesome 5 Pro"']
9 | let FontAwesomeLoaded = false
10 | const span = document.createElement('span')
11 |
12 | span.className = prefix
13 | span.style.display = 'none'
14 | document.body.insertBefore(span, document.body.firstChild)
15 |
16 | const css = (element, property) => window.getComputedStyle(element, null).getPropertyValue(property)
17 |
18 | if (!fontNames.includes(css(span, 'font-family'))) {
19 | console.warn(
20 | `Font Awesome ${version} was not detected but Font Awesome ${version} icons have been requested for \`react-object-list\``
21 | )
22 | } else {
23 | FontAwesomeLoaded = true
24 | }
25 | document.body.removeChild(span)
26 | return FontAwesomeLoaded
27 | }
28 |
29 | const FontAwesomeIcons = (majorVersion = 4) => {
30 | switch (majorVersion) {
31 | case 4:
32 | IsFontAwesomeLoaded(4)
33 | return {
34 | File: ,
35 | Image: ,
36 | Video: ,
37 | Audio: ,
38 | Archive: ,
39 | Word: ,
40 | Excel: ,
41 | PowerPoint: ,
42 | Text: ,
43 | PDF: ,
44 | Rename: ,
45 | Folder: ,
46 | FolderOpen: ,
47 | Delete: ,
48 | Loading: ,
49 | Download: ,
50 | }
51 | case 5:
52 | IsFontAwesomeLoaded(5)
53 | return {
54 | File: ,
55 | Image: ,
56 | Video: ,
57 | Audio: ,
58 | Archive: ,
59 | Word: ,
60 | Excel: ,
61 | PowerPoint: ,
62 | Text: ,
63 | PDF: ,
64 | Rename: ,
65 | Folder: ,
66 | FolderOpen: ,
67 | Delete: ,
68 | Loading: ,
69 | Download: ,
70 | }
71 | default:
72 | console.warn(
73 | `Could not find config for version ${majorVersion}`,
74 | 'Accepted versions are: 4, 5',
75 | 'Please make an issue in `react-object-list` to fix this.'
76 | )
77 | }
78 | }
79 |
80 | export default FontAwesomeIcons
81 |
--------------------------------------------------------------------------------
/src/icons/index.js:
--------------------------------------------------------------------------------
1 | export { default as FontAwesome } from './FontAwesome'
2 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import FileBrowser, { RawFileBrowser } from './browser'
2 | import BaseFile, { BaseFileConnectors } from './base-file'
3 | import BaseFolder, { BaseFolderConnectors } from './base-folder'
4 |
5 | import * as Headers from './headers'
6 | import * as FileRenderers from './files'
7 | import * as FolderRenderers from './folders'
8 |
9 | import * as Details from './details'
10 | import * as Filters from './filters'
11 | import * as Groupers from './groupers'
12 | import * as Sorters from './sorters'
13 | import * as Icons from './icons'
14 | import * as Utils from './utils'
15 |
16 | export default FileBrowser
17 | export {
18 | RawFileBrowser, // Use this one if you want to wrap with dragdrop context yourself.
19 |
20 | BaseFile,
21 | BaseFileConnectors,
22 | BaseFolder,
23 | BaseFolderConnectors,
24 |
25 | Headers,
26 | FileRenderers,
27 | FolderRenderers,
28 |
29 | Details,
30 | Filters,
31 | Groupers,
32 | Sorters,
33 | Icons,
34 | Utils,
35 | }
36 |
--------------------------------------------------------------------------------
/src/list-style.sass:
--------------------------------------------------------------------------------
1 | $action-size: 24px
2 | $folder-colour: #333
3 | $file-colour: #ccc
4 | $selection-colour: #ccc
5 | $selection-text-colour: #fff
6 | $dragover-colour: #eee
7 |
8 | div.rendered-file-browser
9 | div.files
10 | // list styles
11 | li.file, li.folder
12 | &.pending, &.dragging
13 | opacity: 0.4
14 | &.dragover
15 | background: $dragover-colour
16 | &.selected > div.item
17 | color: $selection-text-colour
18 | .text-muted
19 | color: darken($selection-text-colour, 10%)
20 | a:not(.btn), &.folder a:not(.btn), i
21 | color: $selection-text-colour
22 | background: $selection-colour
23 |
24 | $file-spacing: 4px
25 | ul
26 | list-style: none
27 | padding: 0
28 | display: grid
29 | gap: $file-spacing
30 | grid-template-columns: repeat(3, 1fr)
31 |
32 | $thumb-size: 120px
33 | $folder-size: 45px
34 | li
35 | &.folder
36 | grid-column: 1/4
37 | > div.item
38 | display: flex
39 | flex-direction: row
40 | align-items: center
41 |
42 | padding-left: 4px
43 |
44 | border: 1px solid #eee
45 | span.name
46 | flex: 1
47 | line-height: folder-size
48 | span.thumb
49 | flex-basis: $folder-size
50 | border: none
51 | text-align: center
52 | > i
53 | line-height: $folder-size
54 | font-size: calc($folder-size / 2.5)
55 | form.renaming
56 | margin-top: 8px
57 | margin-right: 8px
58 |
59 | > p
60 | margin: 8px
61 | margin-bottom: 0
62 | padding: 0
63 |
64 | &.expanded
65 | > div.item
66 | padding-left: 0px
67 | margin-right: -1px
68 | margin-left: -1px
69 | padding-bottom: 8px
70 | border-bottom: 1px solid #eee
71 | border-left: 4px solid #eee
72 | border-right: 1px solid #eee
73 | &.selected
74 | border-bottom: 1px solid $selection-colour
75 | border-left: 4px solid $selection-colour
76 | border-right: 1px solid $selection-colour
77 | &.selected.folder > div.item
78 | border: 1px solid darken($selection-colour, 5%)
79 | span.thumb
80 | border: none
81 |
82 | &.file
83 | margin: $file-spacing
84 | padding: 0
85 | > div.item
86 | display: flex
87 | flex-direction: column
88 | padding: 4px
89 | margin: 0
90 |
91 | span.thumb
92 | flex-basis: $thumb-size
93 | text-align: center
94 | position: relative
95 | border: 1px solid #eee
96 | margin-bottom: 10px
97 | > i
98 | font-size: 40px
99 | line-height: $thumb-size
100 | div.image
101 | position: absolute
102 | top: 0
103 | left: 0
104 | width: 100%
105 | height: 100%
106 | background-size: cover
107 | background-position: 50% 50%
108 | background-repeat: no-repeat
109 | &.selected.file > div.item
110 | span.thumb
111 | border: 1px solid darken($selection-colour, 5%)
112 | span.thumb div.image
113 | opacity: 0.8
114 | p.loading, p.empty
115 | margin: 16px 0
116 |
--------------------------------------------------------------------------------
/src/sorters/by-modified.js:
--------------------------------------------------------------------------------
1 | import { compareAsc } from 'date-fns'
2 |
3 | function lastModifiedSort(allFiles) {
4 | const folders = []
5 | let files = []
6 | for (let fileIndex = 0; fileIndex < allFiles.length; fileIndex++) {
7 | const file = allFiles[fileIndex]
8 | const keyFolders = (file.newKey || file.key).split('/')
9 | if (file.children) {
10 | // file.name = keyFolders[keyFolders.length - 2]
11 | folders.push(file)
12 | } else {
13 | file.name = keyFolders[keyFolders.length - 1]
14 | files.push(file)
15 | }
16 | }
17 |
18 | files = files.sort(compareAsc)
19 |
20 | for (let folderIndex = 0; folderIndex < folders.length; folderIndex++) {
21 | const folder = folders[folderIndex]
22 | folder.children = lastModifiedSort(folder.children)
23 | }
24 |
25 | let sortedFiles = []
26 | sortedFiles = sortedFiles.concat(folders)
27 | sortedFiles = sortedFiles.concat(files)
28 | return sortedFiles
29 | }
30 |
31 | export default function(files) {
32 | return lastModifiedSort(files)
33 | }
34 |
--------------------------------------------------------------------------------
/src/sorters/by-name.js:
--------------------------------------------------------------------------------
1 | import { naturalSortComparer } from './utils.js'
2 |
3 | function naturalDraftComparer(a, b) {
4 | if (a.draft && !b.draft) {
5 | return 1
6 | } else if (b.draft && !a.draft) {
7 | return -1
8 | }
9 | return naturalSortComparer(a, b)
10 | }
11 |
12 | function naturalSort(allFiles) {
13 | let folders = []
14 | let files = []
15 |
16 | for (let fileIndex = 0; fileIndex < allFiles.length; fileIndex++) {
17 | const file = allFiles[fileIndex]
18 | const keyFolders = (file.newKey || file.key).split('/')
19 | if (file.children) {
20 | if (!file.name) {
21 | file.name = keyFolders[keyFolders.length - 2]
22 | }
23 | folders.push(file)
24 | } else {
25 | if (!file.name) {
26 | file.name = keyFolders[keyFolders.length - 1]
27 | }
28 | files.push(file)
29 | }
30 | }
31 |
32 | files = files.sort(naturalSortComparer)
33 | folders = folders.sort(naturalDraftComparer)
34 |
35 | for (let folderIndex = 0; folderIndex < folders.length; folderIndex++) {
36 | const folder = folders[folderIndex]
37 | folder.children = naturalSort(folder.children)
38 | }
39 |
40 | let sortedFiles = []
41 | sortedFiles = sortedFiles.concat(folders)
42 | sortedFiles = sortedFiles.concat(files)
43 | return sortedFiles
44 | }
45 |
46 | export default function(files) {
47 | return naturalSort(files)
48 | }
49 |
--------------------------------------------------------------------------------
/src/sorters/index.js:
--------------------------------------------------------------------------------
1 | import SortByName from './by-name.js'
2 | import SortByModified from './by-modified.js'
3 |
4 | export {
5 | SortByName,
6 | SortByModified,
7 | }
8 |
--------------------------------------------------------------------------------
/src/sorters/utils.js:
--------------------------------------------------------------------------------
1 | const NUMBER_GROUPS = /(-?\d*\.?\d+)/g
2 |
3 | function naturalSortComparer(a, b) {
4 | const aa = String(a.name).split(NUMBER_GROUPS)
5 | const bb = String(b.name).split(NUMBER_GROUPS)
6 | const min = Math.min(aa.length, bb.length)
7 |
8 | for (let i = 0; i < min; i++) {
9 | const x = parseFloat(aa[i]) || aa[i].toLowerCase()
10 | const y = parseFloat(bb[i]) || bb[i].toLowerCase()
11 | if (x < y) return -1
12 | else if (x > y) return 1
13 | }
14 |
15 | return 0
16 | }
17 |
18 | export { naturalSortComparer }
19 |
--------------------------------------------------------------------------------
/src/table-style.sass:
--------------------------------------------------------------------------------
1 | $selection-bar-size: 0.3rem
2 |
3 | div.rendered-react-keyed-file-browser
4 | div.files table
5 | width: 100%
6 | margin-bottom: 2rem
7 |
8 | -webkit-touch-callout: none
9 | -webkit-user-select: none
10 | -khtml-user-select: none
11 | -moz-user-select: none
12 | -ms-user-select: none
13 | user-select: none
14 |
15 | th, td
16 | font-weight: normal
17 | text-align: left
18 | margin: 0
19 | padding: $main-padding
20 | th
21 | font-weight: bold
22 |
23 | th, td
24 | &.size, &.modified
25 | text-align: right
26 |
27 | &.name
28 | i
29 | padding-right: $main-padding
30 |
31 | thead th
32 | border-bottom: 0.1rem solid #ddd
33 | tr:not(:last-child) td
34 | border-bottom: 0.1rem solid #eee
35 |
36 | td.name
37 | padding-left: calc($main-padding + $selection-bar-size)
38 |
39 | form.renaming
40 | display: flex
41 | align-items: center
42 | i
43 | flex-grow: 0
44 | flex-shrink: 0
45 | input[type="text"]
46 | flex: 1
47 |
48 | tr
49 | td
50 | cursor: pointer
51 | &.selected
52 | td
53 | font-weight: bold
54 | input, button
55 | font-weight: normal
56 | &.name
57 | position: relative
58 | &:after
59 | content: ' '
60 | position: absolute
61 | left: 0
62 | top: 0
63 | width: $selection-bar-size
64 | height: 100%
65 | background: darken($theme-colour-text, 20%)
66 | &.dragover
67 | td, th
68 | background: #eee
69 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | function isFolder(file) {
2 | return file.key.endsWith('/')
3 | }
4 |
5 | function moveFilesAndFolders(props, monitor, component) {
6 | if (!monitor.didDrop()) {
7 | return
8 | }
9 |
10 | const dropResult = monitor.getDropResult()
11 |
12 | const folders = []
13 | const files = []
14 |
15 | props.browserProps.selection.forEach(selection => {
16 | selection[selection.length - 1] === '/' ? folders.push(selection) : files.push(selection)
17 | })
18 |
19 | props.browserProps.openFolder(dropResult.path)
20 |
21 | folders
22 | .forEach(selection => {
23 | const fileKey = selection
24 | const fileNameParts = fileKey.split('/')
25 | const folderName = fileNameParts[fileNameParts.length - 2]
26 |
27 | const newKey = `${dropResult.path}${folderName}/`
28 | // abort if the new folder name contains itself
29 | if (newKey.substr(0, fileKey.length) === fileKey) return
30 |
31 | if (newKey !== fileKey && props.browserProps.moveFolder) {
32 | props.browserProps.moveFolder(fileKey, newKey)
33 | }
34 | })
35 |
36 | files
37 | .forEach(selection => {
38 | const fileKey = selection
39 | const fileNameParts = fileKey.split('/')
40 | const fileName = fileNameParts[fileNameParts.length - 1]
41 | const newKey = `${dropResult.path}${fileName}`
42 | if (newKey !== fileKey && props.browserProps.moveFile) {
43 | props.browserProps.moveFile(fileKey, newKey)
44 | }
45 | })
46 | }
47 |
48 | export { isFolder, moveFilesAndFolders }
49 |
--------------------------------------------------------------------------------
/stories/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { addHours, subHours, subDays, subMonths } from 'date-fns'
3 | import { State, Store } from '@sambego/storybook-state'
4 | import { HTML5Backend } from 'react-dnd-html5-backend'
5 | import { DndProvider } from 'react-dnd'
6 |
7 | import FileBrowser, { FileRenderers, FolderRenderers, Groupers, Icons, RawFileBrowser } from '../src'
8 | import './stories'
9 |
10 | export default {
11 | title: 'File Browser',
12 | component: FileBrowser,
13 | }
14 |
15 | const files = [
16 | {
17 | key: 'animals/',
18 | modified: addHours(new Date(), 1).getTime(),
19 | size: 0,
20 | },
21 | {
22 | key: 'animals/dog.png',
23 | modified: subHours(new Date(), 1).getTime(),
24 | size: 0,
25 | },
26 | {
27 | key: 'cat.png',
28 | modified: subHours(new Date(), 1).getTime(),
29 | size: 1.5 * 1024 * 1024,
30 | },
31 | {
32 | key: 'kitten.png',
33 | modified: subDays(new Date(), 3).getTime(),
34 | size: 545 * 1024,
35 | },
36 | {
37 | key: 'elephant.png',
38 | modified: subDays(new Date(), 3).getTime(),
39 | size: 52 * 1024,
40 | },
41 | ]
42 |
43 | const moreThan20Files = Array(21).fill(
44 | {
45 | key: 'cat.png',
46 | modified: subHours(new Date(), 1).getTime(),
47 | size: 1.5 * 1024 * 1024,
48 | },
49 | )
50 |
51 | const store = new Store({ files })
52 | const dndStore = new Store({ files })
53 |
54 | export const simpleFlatAndReadOnlyExample = () =>
55 |
56 | export const differentRendersAndGroupers = () => (
57 |
107 | )
108 |
109 | export const customerRenderAndCustomNoMatchingFilesMessage = () => (
110 | <>
111 | Search for a string not contained in the file names in order to see the custom No Results message.
112 | (
115 |
125 | )}
126 | noMatchingFilesMessage={(filter) => `There are no files that match "${filter}".`}
127 | />
128 | >
129 | )
130 |
131 | export const moreThan20FilesWithCustomShowMore = () => (
132 | <>
133 | Search for "cat" to see the Show More Results message at the bottom of the table.
134 |
138 | >
139 | )
140 |
141 | export const emptyFilesListWithCustomNoFilesMessage = () => (
142 |
146 | )
147 |
148 | export const groupByFolder = () => (
149 | {}}
200 | headerRenderer={null}
201 | group={Groupers.GroupByFolder}
202 | fileRenderer={FileRenderers.ListThumbnailFile}
203 | folderRenderer={FolderRenderers.ListThumbnailFolder}
204 | />
205 | )
206 |
207 | export const simpleFlatAndReadOnlyExampleWithBulkActions = () => (
208 |
209 | {(state) => (
210 | {
213 | store.set({
214 | files: store.get('files').concat([
215 | {
216 | key: key,
217 | modified: Date.now(),
218 | size: (Math.floor(Math.random() * 100) + 1) * 1024,
219 | },
220 | ]),
221 | })
222 | }}
223 | onCreateFiles={(files, prefix) => {
224 | const newFiles = store.get('files').map((file) => {
225 | let newKey = prefix
226 | if (
227 | prefix !== '' &&
228 | prefix.substring(prefix.length - 1, prefix.length) !== '/'
229 | ) {
230 | newKey += '/'
231 | }
232 | newKey += file.name
233 | return {
234 | key: newKey,
235 | size: file.size,
236 | modified: Date.now(),
237 | }
238 | })
239 |
240 | const uniqueNewFiles = []
241 | newFiles.map((newFile) => {
242 | let exists = false
243 | state.files.map((existingFile) => {
244 | if (existingFile.key === newFile.key) {
245 | exists = true
246 | }
247 | })
248 | if (!exists) {
249 | uniqueNewFiles.push(newFile)
250 | }
251 | })
252 | store.set({
253 | files: store.get('files').concat(uniqueNewFiles),
254 | })
255 | }}
256 | onMoveFolder={(oldKey, newKey) => {
257 | const newFiles = []
258 | store.get('files').map((file) => {
259 | if (file.key.substr(0, oldKey.length) === oldKey) {
260 | newFiles.push({
261 | ...file,
262 | key: file.key.replace(oldKey, newKey),
263 | modified: Date.now(),
264 | })
265 | } else {
266 | newFiles.push(file)
267 | }
268 | })
269 | store.set({
270 | files: newFiles,
271 | })
272 | }}
273 | onMoveFile={(oldKey, newKey) => {
274 | const newFiles = []
275 | store.get('files').map((file) => {
276 | if (file.key === oldKey) {
277 | newFiles.push({
278 | ...file,
279 | key: newKey,
280 | modified: Date.now(),
281 | })
282 | } else {
283 | newFiles.push(file)
284 | }
285 | })
286 | store.set({
287 | files: newFiles,
288 | })
289 | }}
290 | onRenameFolder={(oldKey, newKey) => {
291 | const newFiles = []
292 | store.get('files').map((file) => {
293 | if (file.key.substr(0, oldKey.length) === oldKey) {
294 | newFiles.push({
295 | ...file,
296 | key: file.key.replace(oldKey, newKey),
297 | modified: Date.now(),
298 | })
299 | } else {
300 | newFiles.push(file)
301 | }
302 | })
303 | store.set({
304 | files: newFiles,
305 | })
306 | }}
307 | onRenameFile={(oldKey, newKey) => {
308 | const newFiles = []
309 | store.get('files').map((file) => {
310 | if (file.key === oldKey) {
311 | newFiles.push({
312 | ...file,
313 | key: newKey,
314 | modified: Date.now(),
315 | })
316 | } else {
317 | newFiles.push(file)
318 | }
319 | })
320 | store.set({
321 | files: newFiles,
322 | })
323 | }}
324 | onDeleteFolder={(folderKeys) => {
325 | const newFiles = []
326 | store.get('files').map((file) => {
327 | if (
328 | !folderKeys.find(
329 | (folderKey) =>
330 | file.key.substr(0, folderKey.length) === folderKey
331 | )
332 | ) {
333 | newFiles.push(file)
334 | }
335 | })
336 | store.set({
337 | files: newFiles,
338 | })
339 | }}
340 | onDeleteFile={(fileKeys) => {
341 | store.set({
342 | files: store
343 | .get('files')
344 | .filter((file) => !fileKeys.includes(file.key)),
345 | })
346 | }}
347 | onDownloadFile={(fileKeys) => {
348 | console.log('Downloading files: ', fileKeys)
349 | }}
350 | files={state.files}
351 | />
352 | )}
353 |
354 | )
355 |
356 | export const withCustomDNDProvider = () => (
357 |
358 | {(state) => (
359 |
360 | {
363 | dndStore.set({
364 | files: dndStore.get('files').concat([
365 | {
366 | key: key,
367 | modified: Date.now(),
368 | size: (Math.floor(Math.random() * 100) + 1) * 1024,
369 | },
370 | ]),
371 | })
372 | }}
373 | onCreateFiles={(files, prefix) => {
374 | const newFiles = dndStore.get('files').map((file) => {
375 | let newKey = prefix
376 | if (
377 | prefix !== '' &&
378 | prefix.substring(prefix.length - 1, prefix.length) !== '/'
379 | ) {
380 | newKey += '/'
381 | }
382 | newKey += file.name
383 | return {
384 | key: newKey,
385 | size: file.size,
386 | modified: Date.now(),
387 | }
388 | })
389 |
390 | const uniqueNewFiles = []
391 | newFiles.map((newFile) => {
392 | let exists = false
393 | state.files.map((existingFile) => {
394 | if (existingFile.key === newFile.key) {
395 | exists = true
396 | }
397 | })
398 | if (!exists) {
399 | uniqueNewFiles.push(newFile)
400 | }
401 | })
402 | dndStore.set({
403 | files: dndStore.get('files').concat(uniqueNewFiles),
404 | })
405 | }}
406 | onMoveFolder={(oldKey, newKey) => {
407 | const newFiles = []
408 | dndStore.get('files').map((file) => {
409 | if (file.key.substr(0, oldKey.length) === oldKey) {
410 | newFiles.push({
411 | ...file,
412 | key: file.key.replace(oldKey, newKey),
413 | modified: Date.now(),
414 | })
415 | } else {
416 | newFiles.push(file)
417 | }
418 | })
419 | dndStore.set({
420 | files: newFiles,
421 | })
422 | }}
423 | onMoveFile={(oldKey, newKey) => {
424 | const newFiles = []
425 | dndStore.get('files').map((file) => {
426 | if (file.key === oldKey) {
427 | newFiles.push({
428 | ...file,
429 | key: newKey,
430 | modified: Date.now(),
431 | })
432 | } else {
433 | newFiles.push(file)
434 | }
435 | })
436 | dndStore.set({
437 | files: newFiles,
438 | })
439 | }}
440 | onRenameFolder={(oldKey, newKey) => {
441 | const newFiles = []
442 | dndStore.get('files').map((file) => {
443 | if (file.key.substr(0, oldKey.length) === oldKey) {
444 | newFiles.push({
445 | ...file,
446 | key: file.key.replace(oldKey, newKey),
447 | modified: Date.now(),
448 | })
449 | } else {
450 | newFiles.push(file)
451 | }
452 | })
453 | dndStore.set({
454 | files: newFiles,
455 | })
456 | }}
457 | onRenameFile={(oldKey, newKey) => {
458 | const newFiles = []
459 | dndStore.get('files').map((file) => {
460 | if (file.key === oldKey) {
461 | newFiles.push({
462 | ...file,
463 | key: newKey,
464 | modified: Date.now(),
465 | })
466 | } else {
467 | newFiles.push(file)
468 | }
469 | })
470 | dndStore.set({
471 | files: newFiles,
472 | })
473 | }}
474 | onDeleteFolder={(folderKeys) => {
475 | const newFiles = []
476 | dndStore.get('files').map((file) => {
477 | if (
478 | !folderKeys.find(
479 | (folderKey) =>
480 | file.key.substr(0, folderKey.length) === folderKey
481 | )
482 | ) {
483 | newFiles.push(file)
484 | }
485 | })
486 | dndStore.set({
487 | files: newFiles,
488 | })
489 | }}
490 | onDeleteFile={(fileKeys) => {
491 | dndStore.set({
492 | files: dndStore
493 | .get('files')
494 | .filter((file) => !fileKeys.includes(file.key)),
495 | })
496 | }}
497 | onDownloadFile={(fileKeys) => {
498 | console.log('Downloading files: ', fileKeys)
499 | }}
500 | files={state.files}
501 | />
502 |
503 | )}
504 |
505 | )
506 |
--------------------------------------------------------------------------------
/stories/stories.sass:
--------------------------------------------------------------------------------
1 | $fa-font-path: "https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/fonts" !default
2 |
3 | @import '../src/browser'
4 | @import '../node_modules/font-awesome/scss/font-awesome'
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "target": "esnext",
5 | "lib": ["esnext", "dom"],
6 | "strict": true,
7 | "noImplicitAny": false,
8 | "esModuleInterop": true,
9 | "moduleResolution": "node",
10 | "outDir": "dist",
11 | },
12 |
13 | "include": ["src/**/*"],
14 |
15 | "exclude": [
16 | "node_modules",
17 | "dist",
18 | "src/**/*.stories.tsx",
19 | "src/**/*.test.tsx"
20 | ]
21 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const NodeExternals = require('webpack-node-externals')
3 |
4 | module.exports = {
5 | entry: './src/index.js',
6 | output: {
7 | path: path.join(__dirname, '/dist'),
8 | filename: 'react-keyed-file-browser.js',
9 | library: 'react-keyed-file-browser',
10 | libraryTarget: 'umd',
11 | },
12 | module: {
13 | rules: [
14 | {
15 | test: /\.(js|jsx)$/,
16 | loader: 'esbuild-loader',
17 | include: path.join(__dirname, './src/'),
18 | options: {
19 | loader: 'jsx',
20 | target: 'es2015',
21 | },
22 | },
23 | {
24 | test: /.*\.sass*/,
25 | loader: ['style-loader', 'css-loader', 'sass-loader'],
26 | include: path.join(__dirname, '/src'),
27 | },
28 | ],
29 | },
30 | externals: NodeExternals(),
31 | }
32 |
--------------------------------------------------------------------------------