diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9094339..95c46a9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index d964871..70f974a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/Documents/Docs.scss b/frontend/src/Documents/Docs.scss index b10d22c..6974e05 100644 --- a/frontend/src/Documents/Docs.scss +++ b/frontend/src/Documents/Docs.scss @@ -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 { diff --git a/frontend/src/Documents/DocsList.tsx b/frontend/src/Documents/DocsList.tsx deleted file mode 100644 index a5758d5..0000000 --- a/frontend/src/Documents/DocsList.tsx +++ /dev/null @@ -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 => ); - return
{cards}
; -} diff --git a/frontend/src/Documents/DocumentsList.tsx b/frontend/src/Documents/DocumentsList.tsx new file mode 100644 index 0000000..c6f7399 --- /dev/null +++ b/frontend/src/Documents/DocumentsList.tsx @@ -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 => ( + + )); + if (props.newDocument) { + return ( +
+ {} + {cards} +
+ ); + } + return
{cards}
; +} diff --git a/frontend/src/Documents/NewDocumentCard.tsx b/frontend/src/Documents/NewDocumentCard.tsx new file mode 100644 index 0000000..bc837f1 --- /dev/null +++ b/frontend/src/Documents/NewDocumentCard.tsx @@ -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 ( + props.createNewDoc()} + > + + + ); +} + +function mapDispatchToProps(dispatch: Dispatch) { + return { createNewDoc: () => dispatch(newDocStart()) }; +} + +export const NewDocumentCard = withRouter( + connect( + null, + mapDispatchToProps, + )(NewDocumentCardComponent), +); diff --git a/frontend/src/Documents/Overview.tsx b/frontend/src/Documents/Overview.tsx index 5f9d4cf..6aed2d1 100644 --- a/frontend/src/Documents/Overview.tsx +++ b/frontend/src/Documents/Overview.tsx @@ -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<

Recent

- +

All documents

- +
); diff --git a/frontend/src/redux/api/docs/index.ts b/frontend/src/redux/api/docs/index.ts index 7937f07..f297834 100644 --- a/frontend/src/redux/api/docs/index.ts +++ b/frontend/src/redux/api/docs/index.ts @@ -16,7 +16,7 @@ export async function fetchAllDocs(): Promise> { export async function fetchDoc( id: number, ): Promise> { - 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> { - return fetchJSONAuth(`docs/byID/${id}`, "PATCH", { name, content }); + return fetchJSONAuth(`/docs/byID/${id}`, "PATCH", { name, content }); } export async function deleteDoc(id: number): Promise> { - return fetchJSONAuth(`docs/byID/${id}`, "DELETE"); + return fetchJSONAuth(`/docs/byID/${id}`, "DELETE"); +} + +export async function createNewDoc(): Promise> { + return fetchJSONAuth(`/docs/new`, "POST", { name: "New Document" }); } diff --git a/frontend/src/redux/docs/actions.ts b/frontend/src/redux/docs/actions.ts index a8f5261..de8cb66 100644 --- a/frontend/src/redux/docs/actions.ts +++ b/frontend/src/redux/docs/actions.ts @@ -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; diff --git a/frontend/src/redux/docs/reducer.ts b/frontend/src/redux/docs/reducer.ts index 1a78c7f..af6d741 100644 --- a/frontend/src/redux/docs/reducer.ts +++ b/frontend/src/redux/docs/reducer.ts @@ -26,12 +26,19 @@ export const docsReducer: Reducer = ( 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: diff --git a/frontend/src/redux/docs/sagas.ts b/frontend/src/redux/docs/sagas.ts index e4071a2..a45740b 100644 --- a/frontend/src/redux/docs/sagas.ts +++ b/frontend/src/redux/docs/sagas.ts @@ -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), + ]); } diff --git a/src/entity/Document.ts b/src/entity/Document.ts index 0ba71f5..be884f0 100644 --- a/src/entity/Document.ts +++ b/src/entity/Document.ts @@ -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 }) diff --git a/src/routes/docs.ts b/src/routes/docs.ts index f3a7393..8e2d163 100644 --- a/src/routes/docs.ts +++ b/src/routes/docs.ts @@ -15,7 +15,7 @@ docsRouter.post("/docs/new", async ctx => { content: string | undefined; }; - if (!(name && content)) { + if (!name) { ctx.throw(400); }