move local document state to redux

This commit is contained in:
2019-08-30 22:22:45 +02:00
parent 1b42e02768
commit 55c13270a2
6 changed files with 109 additions and 72 deletions

View File

@@ -12,12 +12,14 @@ import {
deleteDocCancel,
deleteDocStart,
fetchDocsStart,
updateDoc,
uploadDocStart,
} from "~redux/docs/actions";
import { IDocumentEntry } from "~redux/docs/reducer";
import { IAppState } from "~redux/reducers";
export interface IDocumentEditComponentProps extends RouteComponentProps {
allDocs: { [key: number]: IDocumentJSON };
allDocs: { [key: number]: IDocumentEntry };
fetching: boolean;
spinner: boolean;
@@ -26,32 +28,17 @@ export interface IDocumentEditComponentProps extends RouteComponentProps {
deleteDoc: (id: number) => void;
cancelDelete: () => void;
uploadDoc: (id: number, name: string, content: string) => void;
updateDoc: (id: number, name: string, content: string) => void;
}
export interface IDocumentEditComponentState {
loaded: boolean;
id: number | null;
name: string | null;
content: string | null;
savedName: string | null;
savedContent: string | null;
dirty: boolean;
}
const defaultDocumentEditComponentState: IDocumentEditComponentState = {
loaded: false,
id: null,
name: null,
content: null,
savedName: null,
savedContent: null,
dirty: false,
};
export class DocumentEditComponent extends React.PureComponent<
@@ -71,6 +58,8 @@ export class DocumentEditComponent extends React.PureComponent<
public render() {
if (this.state.loaded) {
const doc = this.props.allDocs[this.state.id];
return (
<div className="document">
<div className="documentHeader">
@@ -78,7 +67,7 @@ export class DocumentEditComponent extends React.PureComponent<
className={Classes.INPUT}
onChange={this.handleInputChange}
name="name"
value={this.state.name}
value={doc.name}
onKeyPress={this.handleNameKeyPress}
/>
<div className="buttons">
@@ -99,7 +88,7 @@ export class DocumentEditComponent extends React.PureComponent<
<TextArea
onChange={this.handleInputChange}
name="content"
value={this.state.content}
value={doc.content}
/>
</div>
);
@@ -109,16 +98,9 @@ export class DocumentEditComponent extends React.PureComponent<
}
public upload() {
this.props.uploadDoc(
this.state.id,
this.state.name,
this.state.content,
);
this.setState({
savedName: this.state.name,
savedContent: this.state.content,
dirty: false,
} as any);
const doc = this.props.allDocs[this.state.id];
console.log("upload");
this.props.uploadDoc(this.state.id, doc.name, doc.content);
}
public remove() {
@@ -147,22 +129,15 @@ export class DocumentEditComponent extends React.PureComponent<
const value = target.value;
const name = target.name;
const { savedName, savedContent } = this.state;
const doc = this.props.allDocs[this.state.id];
const updDoc: { [key: string]: string } = {
name: this.state.name,
content: this.state.content,
name: doc.name,
content: doc.content,
};
updDoc[name] = value;
const dirty =
savedName !== updDoc.name || savedContent !== updDoc.content;
this.setState({
[name]: value,
dirty,
} as any);
this.props.updateDoc(this.state.id, updDoc.name, updDoc.content);
}
public componentDidUpdate() {
@@ -179,7 +154,9 @@ export class DocumentEditComponent extends React.PureComponent<
}
public onUnload(e: BeforeUnloadEvent) {
if (this.state.dirty) {
const doc = this.props.allDocs[this.state.id];
if (doc.dirty) {
e.preventDefault();
e.returnValue = "";
}
@@ -201,10 +178,6 @@ export class DocumentEditComponent extends React.PureComponent<
...this.state,
loaded: true,
id: doc.id,
name: doc.name,
content: doc.content,
savedContent: doc.content,
savedName: doc.name,
});
}
}
@@ -226,6 +199,8 @@ function mapDispatchToProps(dispatch: Dispatch) {
deleteDoc: (id: number) => dispatch(deleteDocStart(id)),
uploadDoc: (id: number, name: string, content: string) =>
dispatch(uploadDocStart(id, name, content)),
updateDoc: (id: number, name: string, content: string) =>
dispatch(updateDoc(id, name, content)),
};
}

View File

@@ -1,20 +1,42 @@
import { mount } from "enzyme";
import * as React from "react";
import { IDocumentEntry } from "~redux/docs/reducer";
import { IDocumentJSON } from "../../../../src/entity/Document";
import { DocumentEditComponent } from "../DocumentEdit";
const mock: any = jest.fn();
const onUnloadSpy = jest.spyOn(DocumentEditComponent.prototype, "onUnload");
const testDoc = {
1: {
const testDoc: IDocumentJSON = {
name: "not changed",
content: "not changed",
id: 1,
user: 1,
createdAt: 0,
editedAt: 0,
};
const testDocsChanged: { [key: number]: IDocumentEntry } = {
1: {
...testDoc,
name: "changed",
content: "changed",
remote: {
...testDoc,
},
dirty: true,
},
};
const testDocsNotChanged: { [key: number]: IDocumentEntry } = {
1: {
...testDoc,
remote: {
...testDoc,
},
dirty: false,
},
};
@@ -32,25 +54,20 @@ describe("<DocumentEdit />", () => {
const wrapper = mount(
<DocumentEditComponent
allDocs={testDoc}
allDocs={testDocsChanged}
fetching={false}
spinner={false}
fetchDocs={mock}
cancelDelete={mock}
deleteDoc={mock}
uploadDoc={mock}
updateDoc={mock}
history={mock}
location={mock}
match={{ params: { id: 1 } } as any}
/>,
);
const content = wrapper.find("textarea");
expect(content).toHaveLength(1);
(content.instance() as any).value = "changed";
content.simulate("change");
const preventDefault = jest.fn();
const event = { preventDefault };
map.beforeunload(event);
@@ -67,28 +84,20 @@ describe("<DocumentEdit />", () => {
const wrapper = mount(
<DocumentEditComponent
allDocs={testDoc}
allDocs={testDocsNotChanged}
fetching={false}
spinner={false}
fetchDocs={mock}
cancelDelete={mock}
deleteDoc={mock}
uploadDoc={mock}
updateDoc={mock}
history={mock}
location={mock}
match={{ params: { id: 1 } } as any}
/>,
);
const content = wrapper.find("textarea");
expect(content).toHaveLength(1);
(content.instance() as any).value = "changed";
content.simulate("change");
(content.instance() as any).value = "not changed";
content.simulate("change");
const preventDefault = jest.fn();
const event = { preventDefault };
map.beforeunload(event);

View File

@@ -32,6 +32,7 @@ export interface IHomeProps extends RouteComponentProps {
fetching: boolean;
uploading: boolean;
dirty: boolean;
darkMode: boolean;
@@ -78,7 +79,7 @@ export class HomeComponent extends React.PureComponent<IHomeProps> {
</Navbar.Group>
<Navbar.Group align={Alignment.RIGHT}>
<Button id="uploadingStatusButton">
{this.props.uploading ? (
{this.props.uploading || this.props.dirty ? (
<Spinner size={20} />
) : (
<Icon icon="saved" />
@@ -175,6 +176,7 @@ function mapStateToProps(state: IAppState) {
darkMode: state.localSettings.darkMode,
fetching: state.docs.fetching,
uploading: state.docs.uploading,
dirty: state.docs.dirty,
};
}

View File

@@ -10,6 +10,7 @@ const defaultHomeProps: IHomeProps = {
fetching: false,
uploading: false,
dirty: false,
darkMode: false,

View File

@@ -21,6 +21,8 @@ export enum DocsTypes {
DOC_UPLOAD_SUCCESS = "DOC_UPLOAD_SUCCESS",
DOCS_SHOW_SPINNER = "DOCS_SHOW_SPINNER",
DOC_UPDATE = "DOC_UPDATE",
}
export interface IDocsShowSpinnerAction extends Action {
@@ -177,6 +179,23 @@ export function uploadDocSuccess(doc: IDocumentJSON): IDocUploadSuccessAction {
return { type: DocsTypes.DOC_UPLOAD_SUCCESS, payload: { doc } };
}
export interface IDocUpdateAction extends Action {
type: DocsTypes.DOC_UPDATE;
payload: {
id: number;
name: string;
content: string;
};
}
export function updateDoc(
id: number,
name: string,
content: string,
): IDocUpdateAction {
return { type: DocsTypes.DOC_UPDATE, payload: { id, name, content } };
}
export type DocsAction =
| IDocsFetchStartAction
| IDocsFetchFailAction
@@ -192,4 +211,5 @@ export type DocsAction =
| IDocDeleteCancelAction
| IDocUploadFailAction
| IDocUploadStartAction
| IDocUploadSuccessAction;
| IDocUploadSuccessAction
| IDocUpdateAction;

View File

@@ -11,6 +11,8 @@ export interface IDocumentEntry extends IDocumentJSON {
export interface IDocsState {
all: { [key: number]: IDocumentEntry };
dirty: boolean;
fetching: boolean;
uploading: boolean;
error: string | null;
@@ -23,6 +25,7 @@ export interface IDocsState {
const defaultDocsState: IDocsState = {
all: null,
dirty: false,
fetching: false,
uploading: false,
error: null,
@@ -81,12 +84,39 @@ export const docsReducer: Reducer<IDocsState, DocsAction> = (
const all = { ...state.all };
const doc = action.payload.doc;
all[doc.id] = { ...doc, remote: doc, dirty: false };
return { ...state, all, uploading: false };
return { ...state, all, uploading: false, dirty: false };
}
case DocsTypes.DOCS_FETCH_FAIL:
return { ...defaultDocsState, ...action.payload };
case UserTypes.USER_LOGOUT:
return defaultDocsState;
case DocsTypes.DOC_UPDATE: {
const all = { ...state.all };
let dirty = state.dirty;
const { payload } = action;
const doc = all[payload.id];
const remote = doc.remote;
if (
payload.content !== remote.content ||
payload.name !== remote.name
) {
all[payload.id] = {
...doc,
content: payload.content,
name: payload.name,
dirty: true,
};
dirty = true;
} else {
all[payload.id] = {
...doc,
content: payload.content,
name: payload.name,
dirty: false,
};
}
return { ...state, all, dirty };
}
default:
return state;
break;