mirror of
https://github.com/usatiuk/writer.git
synced 2025-10-28 16:07:49 +01:00
create new documents
This commit is contained in:
33
frontend/package-lock.json
generated
33
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
25
frontend/src/Documents/DocumentsList.tsx
Normal file
25
frontend/src/Documents/DocumentsList.tsx
Normal 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>;
|
||||
}
|
||||
35
frontend/src/Documents/NewDocumentCard.tsx
Normal file
35
frontend/src/Documents/NewDocumentCard.tsx
Normal 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),
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -15,7 +15,7 @@ docsRouter.post("/docs/new", async ctx => {
|
||||
content: string | undefined;
|
||||
};
|
||||
|
||||
if (!(name && content)) {
|
||||
if (!name) {
|
||||
ctx.throw(400);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user