mirror of
https://github.com/usatiuk/writer.git
synced 2025-10-29 00:17:48 +01:00
move local document state to redux
This commit is contained in:
@@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
const testDoc: IDocumentJSON = {
|
||||
name: "not changed",
|
||||
content: "not changed",
|
||||
id: 1,
|
||||
user: 1,
|
||||
createdAt: 0,
|
||||
editedAt: 0,
|
||||
};
|
||||
|
||||
const testDocsChanged: { [key: number]: IDocumentEntry } = {
|
||||
1: {
|
||||
name: "not changed",
|
||||
content: "not changed",
|
||||
id: 1,
|
||||
user: 1,
|
||||
createdAt: 0,
|
||||
editedAt: 0,
|
||||
...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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ const defaultHomeProps: IHomeProps = {
|
||||
|
||||
fetching: false,
|
||||
uploading: false,
|
||||
dirty: false,
|
||||
|
||||
darkMode: false,
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user