Skip to content

Commit fc1896f

Browse files
authored
Merge pull request #2 from dabapps/reduxify
Reduxify
2 parents 0ca2813 + 356c606 commit fc1896f

18 files changed

+835
-151
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/coverage/*
33
/build/*
44
/dist/*
5-
.vscode/*
5+
/.vscode/*
66

77
.DS_Store
88
npm-debug.log*

README.md

Lines changed: 98 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@
22

33
[![Build Status](https://travis-ci.com/dabapps/django-s3-file-upload-client.svg?token=k7ApnEQbpXLoWVm5Bc9o&branch=master)](https://travis-ci.com/dabapps/django-s3-file-upload-client)
44

5-
Upload files from the browser to S3 - client side implementation
5+
Upload files from the browser to S3 - client side implementation (with redux integration)
66

77
For the server side implementation see [github.com/dabapps/django-s3-file-upload-server](https://github.com/dabapps/django-s3-file-upload-server)
88

9-
## Getting Started
10-
11-
### Installation
9+
## Installation
1210

1311
To install the package run:
1412

@@ -18,33 +16,79 @@ npm i @dabapps/django-s3-file-upload -S
1816

1917
## Usage
2018

19+
### How it works
20+
2121
The flow to be able to upload files from the browser straight to AWS is as follows.
2222
![Flow S3 file uploads](images/flow-s3-file-uploads.png)
2323

24-
All you need to do is import and call the function `uploadFileToS3` with your file and it will chain the three requests that the frontend needs to make to upload a file from the browser to S3:
25-
1. request upload from server
26-
2. upload file to AWS S3
27-
3. mark upload as complete on server
24+
This library abstracts away the process of chaining the three requests that the frontend needs to make to upload a file from the browser to S3:
25+
1. Request upload from server
26+
2. Upload file to AWS S3
27+
3. Mark upload as complete on server
28+
29+
The implementation is specific to the endpoints setup in this repo [github.com/dabapps/django-s3-file-upload-server](https://github.com/dabapps/django-s3-file-upload-server) so be sure to have the backend configured accordingly.
30+
31+
### Redux integration
32+
33+
This library provides 3 functions that can be combined to handle uploading multiple files, and storing the results, errors, and loading states in redux.
2834

29-
The function `uploadFileToS3` returns a `Promise` of type `Promise<UploadData>` with:
35+
#### createActionSet
36+
37+
This function simply creates an object with some keys that will be used internally to allow the action and reducer to communicate.
3038

3139
```ts
32-
interface UploadData {
33-
id: string;
34-
created: string;
35-
modified: string;
36-
complete_url: string;
37-
file: string;
38-
file_key: string;
39-
file_path: string;
40-
filename: string;
41-
upload_form: UploadForm;
40+
const UPLOAD_PROFILE_PHOTO = createActionSet('UPLOAD_PROFILE_PHOTO');
41+
```
42+
43+
#### createFileUploadAction
44+
45+
This creates a [redux-thunk](https://github.com/reduxjs/redux-thunk) action that handles dispatching requests, and actions that track the loading states of the files, as well as the responses / errors.
46+
47+
```ts
48+
const uploadProfilePhoto = createFileUploadAction(UPLOAD_PROFILE_PHOTO);
49+
```
50+
51+
`createFileUploadAction` takes an action set, and an optional options object, and returns a `Promise<UploadData>` (see [Types](#types)).
52+
53+
Actions created with this function will not throw errors by default. This means that calling `.catch` on the returned promise will not be effective unless you utilize the following option...
54+
55+
Currently the options object only exists to allow you to provide a `shouldRethrow` function, which is called with any errors, and will cause the action to rethrow any errors if `true` is returned from `shouldRethrow`.
56+
57+
Once you've created your action you can then dispatch this from your store, or connected component e.g.
58+
59+
```ts
60+
class MyComponent extends PureComponent<Props> {
61+
// ...
62+
private onSubmit = (data: FormData) => {
63+
this.props.uploadProfilePhoto([data.picture]);
64+
}
65+
// ...
4266
}
67+
68+
export default connect(undefined, { uploadProfilePhoto })(MyComponent);
4369
```
4470

45-
The implementation is specific to the endpoints setup in this repo [github.com/dabapps/django-s3-file-upload-server](https://github.com/dabapps/django-s3-file-upload-server) so be sure to have the backend configured accordingly.
71+
This takes an array of files. If you only have a single file to upload just provide an array containing that file.
4672

47-
## Examples
73+
You should prevent the user from attempting to upload the same set of, or additional files while the requests are in progress (as this will cause issues with the loading states). You can check the current loading state from the [reducer](#createFileUploadReducer), and disable your submit button.
74+
75+
#### createFileUploadReducer
76+
77+
This function is used to create a reducer for a specific set of file uploads.
78+
79+
DO NOT use the same reducer for uploading multiple sets of files unless you really know what you are doing.
80+
81+
```ts
82+
const profilePhotoUpload = createFileUploadReducer(UPLOAD_PROFILE_PHOTO);
83+
```
84+
85+
This can then be added to your store, and connected to React components to provide you with the `UploadState` (see [Types](#types)).
86+
87+
### Basic usage
88+
89+
The function `uploadFileToS3` is used internally by the other functions that integrate with redux. If you're not using redux, you can use this to upload individual files, and manually store the loading state, responses, and errors.
90+
91+
`uploadFileToS3` returns a `Promise` of type `Promise<UploadData>` (see [Types](#types)).
4892

4993
Let's say we have a form which contains a `file`, which we want to upload to S3 on submit, we can do the following:
5094

@@ -88,6 +132,39 @@ const handleSubmit = (formData: FormData) => {
88132
};
89133
```
90134

135+
### Types
136+
137+
The response type for each file upload:
138+
139+
```ts
140+
interface UploadData {
141+
id: string;
142+
created: string;
143+
modified: string;
144+
complete_url: string;
145+
file: string;
146+
file_key: string;
147+
file_path: string;
148+
filename: string;
149+
upload_form: UploadForm;
150+
}
151+
```
152+
153+
Reducer state:
154+
155+
```ts
156+
interface UploadState {
157+
loading: boolean;
158+
fileCount: number; // Total number of files to be uploaded
159+
inFlightCount: number; // Number of files being uploaded (but have not finished)
160+
completeCount: number;
161+
successCount: number;
162+
failureCount: number;
163+
data: undefined | ReadonlyArray<UploadData>;
164+
error: undefined | ReadonlyArray<unknown>;
165+
}
166+
```
167+
91168
## Code of conduct
92169

93170
For guidelines regarding the code of conduct when contributing to this repository please review [https://www.dabapps.com/open-source/code-of-conduct/](https://www.dabapps.com/open-source/code-of-conduct/)

package-lock.json

Lines changed: 36 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
},
1010
"scripts": {
1111
"dist": "rm -rf dist/ && mkdir -p dist/ && tsc --project './tsconfig.dist.json'",
12+
"typecheck": "tsc --noEmit --project tsconfig.json",
1213
"lint": "tsc --noEmit --project tsconfig.json && npm run prettier-check && tslint --project tsconfig.json '{src,tests,types,examples,docs}/**/*.@(ts|tsx)'",
1314
"prettier-check": "prettier --check '**/*.{ts,tsx,js,jsx}'",
1415
"prettier": "prettier --write '**/*.{ts,tsx,js,jsx}'",
1516
"tests": "jest",
16-
"test": "npm run lint && npm run tests -- --runInBand --coverage"
17+
"test": "npm run lint && npm run typecheck && npm run tests -- --runInBand --coverage"
1718
},
1819
"repository": {
1920
"type": "git",
@@ -35,8 +36,12 @@
3536
},
3637
"homepage": "https://github.com/dabapps/django-s3-file-upload-client#readme",
3738
"dependencies": {
39+
"@dabapps/redux-create-reducer": "^1.0.3",
40+
"@types/redux-thunk": "^2.1.0",
3841
"axios": "^0.18.0",
39-
"js-cookie": "^2.2.0"
42+
"js-cookie": "^2.2.0",
43+
"redux": "*",
44+
"redux-thunk": "*"
4045
},
4146
"devDependencies": {
4247
"@types/jest": "^24.0.9",

src/actions.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { AnyAction } from 'redux';
2+
import { ThunkDispatch } from 'redux-thunk';
3+
import {
4+
ActionSet,
5+
BeginAction,
6+
FailureAction,
7+
FileUploadOptions,
8+
SuccessAction,
9+
} from './types';
10+
import { uploadFileToS3 } from './upload-file-to-s3';
11+
12+
export const uploadFileWithLoading = <S>(
13+
actionSet: ActionSet,
14+
file: File,
15+
dispatch: ThunkDispatch<S, unknown, AnyAction>
16+
) => {
17+
dispatch({ type: actionSet.REQUEST });
18+
19+
return uploadFileToS3(file)
20+
.then(data => {
21+
dispatch<SuccessAction>({ type: actionSet.SUCCESS, payload: data });
22+
return data;
23+
})
24+
.catch(error => {
25+
dispatch<FailureAction>({ type: actionSet.FAILURE, payload: error });
26+
throw error;
27+
});
28+
};
29+
30+
export const createFileUploadAction = <S>(
31+
actionSet: ActionSet,
32+
options?: FileUploadOptions
33+
) => (files: ReadonlyArray<File>) => (
34+
dispatch: ThunkDispatch<S, unknown, AnyAction>
35+
) => {
36+
dispatch<BeginAction>({
37+
type: actionSet.BEGIN,
38+
payload: files.length,
39+
});
40+
41+
const promises = files.map(file =>
42+
uploadFileWithLoading(actionSet, file, dispatch)
43+
);
44+
45+
return Promise.all(promises).catch(error => {
46+
if (options && options.shouldRethrow && options.shouldRethrow(error)) {
47+
throw error;
48+
}
49+
});
50+
};

src/constants.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { UploadState } from './types';
2+
3+
export const POST = 'POST';
4+
5+
export const INITIAL_REDUCER_STATE: UploadState = {
6+
loading: false,
7+
fileCount: 0,
8+
inFlightCount: 0,
9+
completeCount: 0,
10+
successCount: 0,
11+
failureCount: 0,
12+
data: undefined,
13+
error: undefined,
14+
};

0 commit comments

Comments
 (0)