create new documents

This commit is contained in:
2019-02-10 15:13:55 +03:00
parent e786fbf988
commit 556c945001
13 changed files with 177 additions and 44 deletions

View File

@@ -3315,14 +3315,12 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -3337,20 +3335,17 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@@ -3467,8 +3462,7 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@@ -3480,7 +3474,6 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@@ -3495,7 +3488,6 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@@ -3503,14 +3495,12 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.2.4", "version": "2.2.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.1", "safe-buffer": "^5.1.1",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@@ -3529,7 +3519,6 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@@ -3610,8 +3599,7 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@@ -3623,7 +3611,6 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@@ -3745,7 +3732,6 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@@ -4638,6 +4624,11 @@
"sshpk": "^1.7.0" "sshpk": "^1.7.0"
} }
}, },
"http2": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/http2/-/http2-3.3.7.tgz",
"integrity": "sha512-puSi8M8WNlFJm9Pk4c/Mbz9Gwparuj3gO9/RRO5zv6piQ0FY+9Qywp0PdWshYgsMJSalixFY7eC6oPu0zRxLAQ=="
},
"https-browserify": { "https-browserify": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",

View File

@@ -20,6 +20,7 @@
"dependencies": { "dependencies": {
"@blueprintjs/core": "^3.13.0", "@blueprintjs/core": "^3.13.0",
"@blueprintjs/icons": "^3.6.0", "@blueprintjs/icons": "^3.6.0",
"http2": "^3.3.7",
"rea": "0.0.1", "rea": "0.0.1",
"react": "^16.8.1", "react": "^16.8.1",
"react-dom": "^16.8.1", "react-dom": "^16.8.1",

View File

@@ -34,6 +34,13 @@
color: $dark-gray5; color: $dark-gray5;
} }
} }
.card.newDocumentCard {
background-color: $light-gray5;
color: $dark-gray2;
display: flex;
justify-content: center;
align-items: center;
}
} }
} }
@@ -64,7 +71,6 @@
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
} }
.bp3-dark { .bp3-dark {

View File

@@ -1,9 +0,0 @@
import * as React from "react";
import { IDocumentJSON } from "~../../src/entity/Document";
import { DocumentCard } from "./DocumentCard";
export function DocsList({ docs }: { docs: IDocumentJSON[] }) {
const cards = docs.map(doc => <DocumentCard key={doc.id} doc={doc} />);
return <div className="list">{cards}</div>;
}

View File

@@ -0,0 +1,25 @@
import * as React from "react";
import { IDocumentJSON } from "~../../src/entity/Document";
import { DocumentCard } from "./DocumentCard";
import { NewDocumentCard } from "./NewDocumentCard";
export interface IDocumentListProps {
docs: IDocumentJSON[];
newDocument?: boolean;
}
export function DocumentsList(props: IDocumentListProps) {
const cards = props.docs.map(doc => (
<DocumentCard key={doc.id} doc={doc} />
));
if (props.newDocument) {
return (
<div className="list">
{<NewDocumentCard />}
{cards}
</div>
);
}
return <div className="list">{cards}</div>;
}

View File

@@ -0,0 +1,35 @@
import { Card, Icon } from "@blueprintjs/core";
import * as React from "react";
import { connect } from "react-redux";
import { RouteChildrenProps, withRouter } from "react-router";
import { Dispatch } from "redux";
import { newDocStart } from "~redux/docs/actions";
export interface INewDocumentCardComponentProps extends RouteChildrenProps {
createNewDoc: () => void;
}
export function NewDocumentCardComponent(
props: INewDocumentCardComponentProps,
) {
return (
<Card
className="card newDocumentCard"
interactive={true}
onClick={() => props.createNewDoc()}
>
<Icon icon="add" iconSize={40} className="newDocumentIcon" />
</Card>
);
}
function mapDispatchToProps(dispatch: Dispatch) {
return { createNewDoc: () => dispatch(newDocStart()) };
}
export const NewDocumentCard = withRouter(
connect(
null,
mapDispatchToProps,
)(NewDocumentCardComponent),
);

View File

@@ -9,7 +9,7 @@ import { LoadingStub } from "~LoadingStub";
import { fetchDocsStart } from "~redux/docs/actions"; import { fetchDocsStart } from "~redux/docs/actions";
import { IAppState } from "~redux/reducers"; import { IAppState } from "~redux/reducers";
import { DocsList } from "./DocsList"; import { DocumentsList } from "./DocumentsList";
export interface IOverviewComponentProps { export interface IOverviewComponentProps {
allDocs: { [key: number]: IDocumentJSON }; allDocs: { [key: number]: IDocumentJSON };
@@ -43,12 +43,12 @@ export class OverviewComponent extends React.PureComponent<
<div id="overview"> <div id="overview">
<div className="section"> <div className="section">
<H3>Recent</H3> <H3>Recent</H3>
<DocsList docs={recentCut} /> <DocumentsList docs={recentCut} />
</div> </div>
<span className="separator" /> <span className="separator" />
<div className="section"> <div className="section">
<H3>All documents</H3> <H3>All documents</H3>
<DocsList docs={docs} /> <DocumentsList docs={docs} newDocument={true} />
</div> </div>
</div> </div>
); );

View File

@@ -16,7 +16,7 @@ export async function fetchAllDocs(): Promise<IAPIResponse<IDocumentJSON[]>> {
export async function fetchDoc( export async function fetchDoc(
id: number, id: number,
): Promise<IAPIResponse<IDocumentJSON>> { ): Promise<IAPIResponse<IDocumentJSON>> {
return fetchJSONAuth(`docs/byID/${id}`, "GET"); return fetchJSONAuth(`/docs/byID/${id}`, "GET");
} }
export async function patchDoc( export async function patchDoc(
@@ -24,9 +24,13 @@ export async function patchDoc(
name?: string, name?: string,
content?: string, content?: string,
): Promise<IAPIResponse<IDocumentJSON>> { ): Promise<IAPIResponse<IDocumentJSON>> {
return fetchJSONAuth(`docs/byID/${id}`, "PATCH", { name, content }); return fetchJSONAuth(`/docs/byID/${id}`, "PATCH", { name, content });
} }
export async function deleteDoc(id: number): Promise<IAPIResponse<boolean>> { export async function deleteDoc(id: number): Promise<IAPIResponse<boolean>> {
return fetchJSONAuth(`docs/byID/${id}`, "DELETE"); return fetchJSONAuth(`/docs/byID/${id}`, "DELETE");
}
export async function createNewDoc(): Promise<IAPIResponse<IDocumentJSON>> {
return fetchJSONAuth(`/docs/new`, "POST", { name: "New Document" });
} }

View File

@@ -5,6 +5,11 @@ export enum DocsTypes {
DOCS_FETCH_START = "DOCS_FETCH_START", DOCS_FETCH_START = "DOCS_FETCH_START",
DOCS_FETCH_FAIL = "DOCS_FETCH_FAIL", DOCS_FETCH_FAIL = "DOCS_FETCH_FAIL",
DOCS_FETCH_SUCCESS = "DOCS_FETCH_SUCCESS", DOCS_FETCH_SUCCESS = "DOCS_FETCH_SUCCESS",
DOC_NEW_START = "DOC_NEW_START",
DOC_NEW_FAIL = "DOC_NEW_FAIL",
DOC_NEW_SUCCESS = "DOC_NEW_SUCCESS",
DOCS_SHOW_SPINNER = "DOCS_SHOW_SPINNER", DOCS_SHOW_SPINNER = "DOCS_SHOW_SPINNER",
} }
@@ -48,8 +53,41 @@ export function fetchDocsSuccess(
return { type: DocsTypes.DOCS_FETCH_SUCCESS, payload: { all } }; return { type: DocsTypes.DOCS_FETCH_SUCCESS, payload: { all } };
} }
export interface IDocNewStartAction extends Action {
type: DocsTypes.DOC_NEW_START;
}
export interface IDocNewFailAction extends Action {
type: DocsTypes.DOC_NEW_FAIL;
payload: {
error: string;
};
}
export interface IDocNewSuccessAction extends Action {
type: DocsTypes.DOC_NEW_SUCCESS;
payload: {
doc: IDocumentJSON;
};
}
export function newDocStart(): IDocNewStartAction {
return { type: DocsTypes.DOC_NEW_START };
}
export function newDocFail(error: string): IDocNewFailAction {
return { type: DocsTypes.DOC_NEW_FAIL, payload: { error } };
}
export function newDocSuccess(doc: IDocumentJSON): IDocNewSuccessAction {
return { type: DocsTypes.DOC_NEW_SUCCESS, payload: { doc } };
}
export type DocsAction = export type DocsAction =
| IDocsFetchStartAction | IDocsFetchStartAction
| IDocsFetchFailAction | IDocsFetchFailAction
| IDocsFetchSuccessAction | IDocsFetchSuccessAction
| IDocsShowSpinnerAction; | IDocsShowSpinnerAction
| IDocNewFailAction
| IDocNewStartAction
| IDocNewSuccessAction;

View File

@@ -26,12 +26,19 @@ export const docsReducer: Reducer<IDocsState, DocsAction> = (
return { ...state, spinner: true }; return { ...state, spinner: true };
case DocsTypes.DOCS_FETCH_START: case DocsTypes.DOCS_FETCH_START:
return { ...defaultDocsState, fetching: true }; return { ...defaultDocsState, fetching: true };
case DocsTypes.DOCS_FETCH_SUCCESS: case DocsTypes.DOCS_FETCH_SUCCESS: {
const all: { [key: number]: IDocumentJSON } = {}; const all: { [key: number]: IDocumentJSON } = {};
action.payload.all.forEach(doc => { action.payload.all.forEach(doc => {
all[doc.id] = doc; all[doc.id] = doc;
}); });
return { ...defaultDocsState, all }; return { ...defaultDocsState, all };
}
case DocsTypes.DOC_NEW_SUCCESS: {
const all = { ...state.all };
const doc = action.payload.doc;
all[doc.id] = doc;
return { ...state, all };
}
case DocsTypes.DOCS_FETCH_FAIL: case DocsTypes.DOCS_FETCH_FAIL:
return { ...defaultDocsState, ...action.payload }; return { ...defaultDocsState, ...action.payload };
default: default:

View File

@@ -8,13 +8,16 @@ import {
race, race,
takeLatest, takeLatest,
} from "redux-saga/effects"; } from "redux-saga/effects";
import { fetchAllDocs } from "~redux/api/docs"; import { createNewDoc, fetchAllDocs } from "~redux/api/docs";
import { import {
DocsTypes, DocsTypes,
fetchDocsFail, fetchDocsFail,
fetchDocsSuccess, fetchDocsSuccess,
IDocNewStartAction,
IDocsFetchStartAction, IDocsFetchStartAction,
newDocFail,
newDocSuccess,
showDocsSpinner, showDocsSpinner,
} from "./actions"; } from "./actions";
@@ -52,6 +55,38 @@ function* docsFetchStart(action: IDocsFetchStartAction) {
} }
} }
export function* docsSaga() { function* docNewStart(action: IDocNewStartAction) {
yield all([takeLatest(DocsTypes.DOCS_FETCH_START, docsFetchStart)]); try {
const spinner = yield fork(startSpinner);
const { response, timeout } = yield race({
response: call(createNewDoc),
timeout: delay(10000),
});
yield cancel(spinner);
if (timeout) {
yield put(newDocFail("Timeout"));
return;
}
if (response) {
if (response.data == null) {
yield put(newDocFail(response.error));
} else {
const newDoc = response.data;
yield put(newDocSuccess(newDoc));
}
}
} catch (e) {
yield put(newDocFail("Internal error"));
}
}
export function* docsSaga() {
yield all([
takeLatest(DocsTypes.DOCS_FETCH_START, docsFetchStart),
takeLatest(DocsTypes.DOC_NEW_START, docNewStart),
]);
} }

View File

@@ -28,7 +28,7 @@ export class Document extends BaseEntity {
@Column() @Column()
public name: string; public name: string;
@Column({ type: "text" }) @Column({ type: "text", default: "" })
public content: string; public content: string;
@Column({ type: "timestamp", default: null }) @Column({ type: "timestamp", default: null })

View File

@@ -15,7 +15,7 @@ docsRouter.post("/docs/new", async ctx => {
content: string | undefined; content: string | undefined;
}; };
if (!(name && content)) { if (!name) {
ctx.throw(400); ctx.throw(400);
} }