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

View File

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

View File

@@ -34,6 +34,13 @@
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-right-radius: 0;
}
}
.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 { IAppState } from "~redux/reducers";
import { DocsList } from "./DocsList";
import { DocumentsList } from "./DocumentsList";
export interface IOverviewComponentProps {
allDocs: { [key: number]: IDocumentJSON };
@@ -43,12 +43,12 @@ export class OverviewComponent extends React.PureComponent<
<div id="overview">
<div className="section">
<H3>Recent</H3>
<DocsList docs={recentCut} />
<DocumentsList docs={recentCut} />
</div>
<span className="separator" />
<div className="section">
<H3>All documents</H3>
<DocsList docs={docs} />
<DocumentsList docs={docs} newDocument={true} />
</div>
</div>
);

View File

@@ -16,7 +16,7 @@ export async function fetchAllDocs(): Promise<IAPIResponse<IDocumentJSON[]>> {
export async function fetchDoc(
id: number,
): Promise<IAPIResponse<IDocumentJSON>> {
return fetchJSONAuth(`docs/byID/${id}`, "GET");
return fetchJSONAuth(`/docs/byID/${id}`, "GET");
}
export async function patchDoc(
@@ -24,9 +24,13 @@ export async function patchDoc(
name?: string,
content?: string,
): 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>> {
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_FAIL = "DOCS_FETCH_FAIL",
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",
}
@@ -48,8 +53,41 @@ export function fetchDocsSuccess(
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 =
| IDocsFetchStartAction
| IDocsFetchFailAction
| IDocsFetchSuccessAction
| IDocsShowSpinnerAction;
| IDocsShowSpinnerAction
| IDocNewFailAction
| IDocNewStartAction
| IDocNewSuccessAction;

View File

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

View File

@@ -8,13 +8,16 @@ import {
race,
takeLatest,
} from "redux-saga/effects";
import { fetchAllDocs } from "~redux/api/docs";
import { createNewDoc, fetchAllDocs } from "~redux/api/docs";
import {
DocsTypes,
fetchDocsFail,
fetchDocsSuccess,
IDocNewStartAction,
IDocsFetchStartAction,
newDocFail,
newDocSuccess,
showDocsSpinner,
} from "./actions";
@@ -52,6 +55,38 @@ function* docsFetchStart(action: IDocsFetchStartAction) {
}
}
export function* docsSaga() {
yield all([takeLatest(DocsTypes.DOCS_FETCH_START, docsFetchStart)]);
function* docNewStart(action: IDocNewStartAction) {
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()
public name: string;
@Column({ type: "text" })
@Column({ type: "text", default: "" })
public content: string;
@Column({ type: "timestamp", default: null })

View File

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