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,
|
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)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const defaultHomeProps: IHomeProps = {
|
|||||||
|
|
||||||
fetching: false,
|
fetching: false,
|
||||||
uploading: false,
|
uploading: false,
|
||||||
|
dirty: false,
|
||||||
|
|
||||||
darkMode: false,
|
darkMode: false,
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user