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, deleteDocCancel,
deleteDocStart, deleteDocStart,
fetchDocsStart, fetchDocsStart,
updateDoc,
uploadDocStart, uploadDocStart,
} from "~redux/docs/actions"; } from "~redux/docs/actions";
import { IDocumentEntry } from "~redux/docs/reducer";
import { IAppState } from "~redux/reducers"; import { IAppState } from "~redux/reducers";
export interface IDocumentEditComponentProps extends RouteComponentProps { export interface IDocumentEditComponentProps extends RouteComponentProps {
allDocs: { [key: number]: IDocumentJSON }; allDocs: { [key: number]: IDocumentEntry };
fetching: boolean; fetching: boolean;
spinner: boolean; spinner: boolean;
@@ -26,32 +28,17 @@ export interface IDocumentEditComponentProps extends RouteComponentProps {
deleteDoc: (id: number) => void; deleteDoc: (id: number) => void;
cancelDelete: () => void; cancelDelete: () => void;
uploadDoc: (id: number, name: string, content: string) => void; uploadDoc: (id: number, name: string, content: string) => void;
updateDoc: (id: number, name: string, content: string) => void;
} }
export interface IDocumentEditComponentState { export interface IDocumentEditComponentState {
loaded: boolean; loaded: boolean;
id: number | null; id: number | null;
name: string | null;
content: string | null;
savedName: string | null;
savedContent: string | null;
dirty: boolean;
} }
const defaultDocumentEditComponentState: IDocumentEditComponentState = { const defaultDocumentEditComponentState: IDocumentEditComponentState = {
loaded: false, loaded: false,
id: null, id: null,
name: null,
content: null,
savedName: null,
savedContent: null,
dirty: false,
}; };
export class DocumentEditComponent extends React.PureComponent< export class DocumentEditComponent extends React.PureComponent<
@@ -71,6 +58,8 @@ export class DocumentEditComponent extends React.PureComponent<
public render() { public render() {
if (this.state.loaded) { if (this.state.loaded) {
const doc = this.props.allDocs[this.state.id];
return ( return (
<div className="document"> <div className="document">
<div className="documentHeader"> <div className="documentHeader">
@@ -78,7 +67,7 @@ export class DocumentEditComponent extends React.PureComponent<
className={Classes.INPUT} className={Classes.INPUT}
onChange={this.handleInputChange} onChange={this.handleInputChange}
name="name" name="name"
value={this.state.name} value={doc.name}
onKeyPress={this.handleNameKeyPress} onKeyPress={this.handleNameKeyPress}
/> />
<div className="buttons"> <div className="buttons">
@@ -99,7 +88,7 @@ export class DocumentEditComponent extends React.PureComponent<
<TextArea <TextArea
onChange={this.handleInputChange} onChange={this.handleInputChange}
name="content" name="content"
value={this.state.content} value={doc.content}
/> />
</div> </div>
); );
@@ -109,16 +98,9 @@ export class DocumentEditComponent extends React.PureComponent<
} }
public upload() { public upload() {
this.props.uploadDoc( const doc = this.props.allDocs[this.state.id];
this.state.id, console.log("upload");
this.state.name, this.props.uploadDoc(this.state.id, doc.name, doc.content);
this.state.content,
);
this.setState({
savedName: this.state.name,
savedContent: this.state.content,
dirty: false,
} as any);
} }
public remove() { public remove() {
@@ -147,22 +129,15 @@ export class DocumentEditComponent extends React.PureComponent<
const value = target.value; const value = target.value;
const name = target.name; const name = target.name;
const { savedName, savedContent } = this.state; const doc = this.props.allDocs[this.state.id];
const updDoc: { [key: string]: string } = { const updDoc: { [key: string]: string } = {
name: this.state.name, name: doc.name,
content: this.state.content, content: doc.content,
}; };
updDoc[name] = value; updDoc[name] = value;
const dirty = this.props.updateDoc(this.state.id, updDoc.name, updDoc.content);
savedName !== updDoc.name || savedContent !== updDoc.content;
this.setState({
[name]: value,
dirty,
} as any);
} }
public componentDidUpdate() { public componentDidUpdate() {
@@ -179,7 +154,9 @@ export class DocumentEditComponent extends React.PureComponent<
} }
public onUnload(e: BeforeUnloadEvent) { public onUnload(e: BeforeUnloadEvent) {
if (this.state.dirty) { const doc = this.props.allDocs[this.state.id];
if (doc.dirty) {
e.preventDefault(); e.preventDefault();
e.returnValue = ""; e.returnValue = "";
} }
@@ -201,10 +178,6 @@ export class DocumentEditComponent extends React.PureComponent<
...this.state, ...this.state,
loaded: true, loaded: true,
id: doc.id, 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)), deleteDoc: (id: number) => dispatch(deleteDocStart(id)),
uploadDoc: (id: number, name: string, content: string) => uploadDoc: (id: number, name: string, content: string) =>
dispatch(uploadDocStart(id, name, content)), 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 { mount } from "enzyme";
import * as React from "react"; import * as React from "react";
import { IDocumentEntry } from "~redux/docs/reducer";
import { IDocumentJSON } from "../../../../src/entity/Document";
import { DocumentEditComponent } from "../DocumentEdit"; import { DocumentEditComponent } from "../DocumentEdit";
const mock: any = jest.fn(); const mock: any = jest.fn();
const onUnloadSpy = jest.spyOn(DocumentEditComponent.prototype, "onUnload"); const onUnloadSpy = jest.spyOn(DocumentEditComponent.prototype, "onUnload");
const testDoc = { const testDoc: IDocumentJSON = {
name: "not changed",
content: "not changed",
id: 1,
user: 1,
createdAt: 0,
editedAt: 0,
};
const testDocsChanged: { [key: number]: IDocumentEntry } = {
1: { 1: {
name: "not changed", ...testDoc,
content: "not changed", name: "changed",
id: 1, content: "changed",
user: 1, remote: {
createdAt: 0, ...testDoc,
editedAt: 0, },
dirty: true,
},
};
const testDocsNotChanged: { [key: number]: IDocumentEntry } = {
1: {
...testDoc,
remote: {
...testDoc,
},
dirty: false,
}, },
}; };
@@ -32,25 +54,20 @@ describe("<DocumentEdit />", () => {
const wrapper = mount( const wrapper = mount(
<DocumentEditComponent <DocumentEditComponent
allDocs={testDoc} allDocs={testDocsChanged}
fetching={false} fetching={false}
spinner={false} spinner={false}
fetchDocs={mock} fetchDocs={mock}
cancelDelete={mock} cancelDelete={mock}
deleteDoc={mock} deleteDoc={mock}
uploadDoc={mock} uploadDoc={mock}
updateDoc={mock}
history={mock} history={mock}
location={mock} location={mock}
match={{ params: { id: 1 } } as any} 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 preventDefault = jest.fn();
const event = { preventDefault }; const event = { preventDefault };
map.beforeunload(event); map.beforeunload(event);
@@ -67,28 +84,20 @@ describe("<DocumentEdit />", () => {
const wrapper = mount( const wrapper = mount(
<DocumentEditComponent <DocumentEditComponent
allDocs={testDoc} allDocs={testDocsNotChanged}
fetching={false} fetching={false}
spinner={false} spinner={false}
fetchDocs={mock} fetchDocs={mock}
cancelDelete={mock} cancelDelete={mock}
deleteDoc={mock} deleteDoc={mock}
uploadDoc={mock} uploadDoc={mock}
updateDoc={mock}
history={mock} history={mock}
location={mock} location={mock}
match={{ params: { id: 1 } } as any} 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 preventDefault = jest.fn();
const event = { preventDefault }; const event = { preventDefault };
map.beforeunload(event); map.beforeunload(event);

View File

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

View File

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

View File

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

View File

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