mirror of
https://github.com/usatiuk/writer.git
synced 2025-10-29 00:17:48 +01:00
21030
frontend/package-lock.json
generated
21030
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,51 +1,51 @@
|
||||
{
|
||||
"name": "writer-frontend",
|
||||
"scripts": {
|
||||
"start": "parcel src/index.html",
|
||||
"build": "parcel build src/index.html",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/autoprefixer": "^9.6.1",
|
||||
"@types/enzyme": "^3.10.3",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.5",
|
||||
"@types/highlight.js": "^9.12.3",
|
||||
"@types/jest": "^24.0.18",
|
||||
"@types/parcel-bundler": "^1.12.1",
|
||||
"@types/react": "^16.9.2",
|
||||
"@types/react-dom": "^16.9.0",
|
||||
"@types/react-redux": "^7.1.2",
|
||||
"@types/react-router": "^5.0.3",
|
||||
"@types/react-router-dom": "^4.3.5",
|
||||
"@types/sass": "^1.16.0",
|
||||
"autoprefixer": "^9.6.1",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-adapter-react-16": "^1.14.0",
|
||||
"jest": "^24.9.0",
|
||||
"parcel-bundler": "^1.12.3",
|
||||
"postcss-modules": "^1.4.1",
|
||||
"redux-devtools-extension": "^2.13.8",
|
||||
"sass": "^1.22.12",
|
||||
"ts-jest": "^24.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "^3.18.1",
|
||||
"@blueprintjs/icons": "^3.10.0",
|
||||
"highlight.js": "^9.15.10",
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0",
|
||||
"react-markdown": "^4.2.2",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router": "^5.0.1",
|
||||
"react-router-dom": "^5.0.1",
|
||||
"react-spring": "^8.0.27",
|
||||
"redux": "^4.0.4",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.0.5"
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": true
|
||||
"name": "writer-frontend",
|
||||
"scripts": {
|
||||
"start": "parcel src/index.html",
|
||||
"build": "parcel build src/index.html",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/autoprefixer": "^9.6.1",
|
||||
"@types/enzyme": "^3.10.3",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.5",
|
||||
"@types/highlight.js": "^9.12.3",
|
||||
"@types/jest": "^24.0.18",
|
||||
"@types/parcel-bundler": "^1.12.1",
|
||||
"@types/react": "^16.9.2",
|
||||
"@types/react-dom": "^16.9.0",
|
||||
"@types/react-redux": "^7.1.2",
|
||||
"@types/react-router": "^5.0.3",
|
||||
"@types/react-router-dom": "^4.3.5",
|
||||
"@types/sass": "^1.16.0",
|
||||
"autoprefixer": "^9.6.1",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-adapter-react-16": "^1.14.0",
|
||||
"jest": "^24.9.0",
|
||||
"parcel-bundler": "^1.12.3",
|
||||
"postcss-modules": "^1.4.1",
|
||||
"redux-devtools-extension": "^2.13.8",
|
||||
"sass": "^1.22.12",
|
||||
"ts-jest": "^24.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "^3.18.1",
|
||||
"@blueprintjs/icons": "^3.10.0",
|
||||
"highlight.js": "^9.15.10",
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0",
|
||||
"react-markdown": "^4.2.2",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router": "^5.0.1",
|
||||
"react-router-dom": "^5.0.1",
|
||||
"react-spring": "^8.0.27",
|
||||
"redux": "^4.0.4",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.0.5"
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import "~@blueprintjs/core/lib/scss/variables";
|
||||
@import url('https://fonts.googleapis.com/css?family=PT+Sans:400,400i,700,700i&display=swap&subset=cyrillic,cyrillic-ext,latin-ext');
|
||||
@import url("https://fonts.googleapis.com/css?family=PT+Sans:400,400i,700,700i&display=swap&subset=cyrillic,cyrillic-ext,latin-ext");
|
||||
|
||||
.animationWrapper {
|
||||
position: absolute;
|
||||
@@ -28,4 +28,4 @@
|
||||
|
||||
body {
|
||||
font-family: "PT Sans", "emoji", sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
withRouter,
|
||||
} from "react-router";
|
||||
import { AuthScreen } from "~Auth/AuthScreen";
|
||||
import { SharedView } from "~Documents/SharedView";
|
||||
import { Home } from "~Home/Home";
|
||||
import { Landing } from "~Landing/Landing";
|
||||
import { IAppState } from "~redux/reducers";
|
||||
@@ -23,12 +24,14 @@ export function AppComponent(props: IAppComponentProps) {
|
||||
<Route path="/signup" component={AuthScreen} />,
|
||||
<Route path="/login" component={AuthScreen} />,
|
||||
<Route path="/docs/:id" component={Home} />,
|
||||
<Route path="/shared/:username/:id" component={SharedView} />,
|
||||
<Route path="/" component={Home} />,
|
||||
</Switch>
|
||||
) : (
|
||||
<Switch>
|
||||
<Route path="/signup" component={AuthScreen} />
|
||||
<Route path="/login" component={AuthScreen} />
|
||||
<Route path="/shared/:username/:id" component={SharedView} />,
|
||||
<Route exact={true} path="/" component={Landing} />
|
||||
<Route path="/" component={() => <Redirect to="/login" />} />
|
||||
</Switch>
|
||||
|
||||
@@ -16,3 +16,11 @@ export function showDeletionToast(cancelFn: () => void) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function showSharedToast() {
|
||||
AppToaster.show({
|
||||
message: "Link copied to clipboard!",
|
||||
intent: "success",
|
||||
timeout: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,4 +38,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
.bp3-dark {
|
||||
@import "../../node_modules/highlight.js/styles/a11y-dark";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,8 +97,22 @@
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
button {
|
||||
div {
|
||||
height: 100%;
|
||||
width: 3rem;
|
||||
|
||||
>* {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.bp3-popover-target {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import "./Docs.scss";
|
||||
|
||||
import { Button, Classes, TextArea } from "@blueprintjs/core";
|
||||
import {
|
||||
Button,
|
||||
Classes,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Popover,
|
||||
TextArea,
|
||||
} from "@blueprintjs/core";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { RouteComponentProps, withRouter } from "react-router";
|
||||
import { Dispatch } from "redux";
|
||||
import { IDocumentJSON } from "~../../src/entity/Document";
|
||||
import { showDeletionToast } from "~AppToaster";
|
||||
import { showDeletionToast, showSharedToast } from "~AppToaster";
|
||||
import { LoadingStub } from "~LoadingStub";
|
||||
import { NotFound } from "~NotFound";
|
||||
import {
|
||||
deleteDocCancel,
|
||||
deleteDocStart,
|
||||
@@ -23,12 +31,18 @@ export interface IDocumentEditComponentProps extends RouteComponentProps {
|
||||
|
||||
fetching: boolean;
|
||||
spinner: boolean;
|
||||
username: string;
|
||||
|
||||
fetchDocs: () => void;
|
||||
deleteDoc: (id: number) => void;
|
||||
cancelDelete: () => void;
|
||||
uploadDocs: () => void;
|
||||
updateDoc: (id: number, name: string, content: string) => void;
|
||||
updateDoc: (
|
||||
id: number,
|
||||
name: string,
|
||||
content: string,
|
||||
shared: boolean,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface IDocumentEditComponentState {
|
||||
@@ -51,6 +65,7 @@ export class DocumentEditComponent extends React.PureComponent<
|
||||
this.state = defaultDocumentEditComponentState;
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.handleNameKeyPress = this.handleNameKeyPress.bind(this);
|
||||
this.share = this.share.bind(this);
|
||||
this.remove = this.remove.bind(this);
|
||||
this.save = this.save.bind(this);
|
||||
this.onUnload = this.onUnload.bind(this);
|
||||
@@ -59,7 +74,9 @@ export class DocumentEditComponent extends React.PureComponent<
|
||||
public render() {
|
||||
if (this.state.loaded) {
|
||||
const doc = this.props.allDocs[this.state.id];
|
||||
|
||||
if (!doc) {
|
||||
return <NotFound />;
|
||||
}
|
||||
return (
|
||||
<div className="document">
|
||||
<div className="documentHeader">
|
||||
@@ -71,18 +88,48 @@ export class DocumentEditComponent extends React.PureComponent<
|
||||
onKeyPress={this.handleNameKeyPress}
|
||||
/>
|
||||
<div className="buttons">
|
||||
<Button
|
||||
icon="trash"
|
||||
minimal={true}
|
||||
intent="danger"
|
||||
onClick={this.remove}
|
||||
/>
|
||||
<Button
|
||||
icon="tick"
|
||||
intent="success"
|
||||
minimal={true}
|
||||
onClick={this.save}
|
||||
/>
|
||||
<div>
|
||||
<Popover
|
||||
target={
|
||||
<Button
|
||||
icon="document-share"
|
||||
minimal={true}
|
||||
intent={
|
||||
doc.shared ? "success" : "none"
|
||||
}
|
||||
/>
|
||||
}
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
icon="globe"
|
||||
text={
|
||||
doc.shared
|
||||
? "Remove access"
|
||||
: "Make public"
|
||||
}
|
||||
onClick={this.share}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
icon="trash"
|
||||
minimal={true}
|
||||
intent="danger"
|
||||
onClick={this.remove}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
icon="tick"
|
||||
intent="success"
|
||||
minimal={true}
|
||||
onClick={this.save}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TextArea
|
||||
@@ -118,6 +165,22 @@ export class DocumentEditComponent extends React.PureComponent<
|
||||
}
|
||||
}
|
||||
|
||||
public share() {
|
||||
const doc = this.props.allDocs[this.state.id];
|
||||
|
||||
const updShared = !doc.shared;
|
||||
|
||||
if (updShared) {
|
||||
navigator.clipboard.writeText(
|
||||
`http://localhost:1234/shared/${this.props.username}/${doc.id}`,
|
||||
);
|
||||
showSharedToast();
|
||||
}
|
||||
|
||||
this.props.updateDoc(this.state.id, doc.name, doc.content, updShared);
|
||||
this.upload();
|
||||
}
|
||||
|
||||
public handleInputChange(
|
||||
event:
|
||||
| React.FormEvent<HTMLInputElement>
|
||||
@@ -135,7 +198,12 @@ export class DocumentEditComponent extends React.PureComponent<
|
||||
};
|
||||
updDoc[name] = value;
|
||||
|
||||
this.props.updateDoc(this.state.id, updDoc.name, updDoc.content);
|
||||
this.props.updateDoc(
|
||||
this.state.id,
|
||||
updDoc.name,
|
||||
updDoc.content,
|
||||
doc.shared,
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
@@ -165,17 +233,11 @@ export class DocumentEditComponent extends React.PureComponent<
|
||||
this.props.fetchDocs();
|
||||
} else {
|
||||
const { id } = this.props.match.params as any;
|
||||
if (
|
||||
!this.state.loaded &&
|
||||
this.props.allDocs &&
|
||||
this.props.allDocs[id]
|
||||
) {
|
||||
const doc = this.props.allDocs[id];
|
||||
|
||||
if (!this.state.loaded && this.props.allDocs) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
loaded: true,
|
||||
id: doc.id,
|
||||
id,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -187,6 +249,7 @@ function mapStateToProps(state: IAppState) {
|
||||
allDocs: state.docs.all,
|
||||
fetching: state.docs.fetching,
|
||||
spinner: state.docs.spinner,
|
||||
username: state.user.user.username,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,8 +259,12 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
cancelDelete: () => dispatch(deleteDocCancel()),
|
||||
deleteDoc: (id: number) => dispatch(deleteDocStart(id)),
|
||||
uploadDocs: () => dispatch(uploadDocsStart()),
|
||||
updateDoc: (id: number, name: string, content: string) =>
|
||||
dispatch(updateDoc(id, name, content)),
|
||||
updateDoc: (
|
||||
id: number,
|
||||
name: string,
|
||||
content: string,
|
||||
shared: boolean,
|
||||
) => dispatch(updateDoc(id, name, content, shared)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { RouteComponentProps, withRouter } from "react-router";
|
||||
import { Dispatch } from "redux";
|
||||
import { IDocumentJSON } from "~../../src/entity/Document";
|
||||
import { LoadingStub } from "~LoadingStub";
|
||||
import { NotFound } from "~NotFound";
|
||||
import { fetchDocsStart } from "~redux/docs/actions";
|
||||
import { IAppState } from "~redux/reducers";
|
||||
import { CodeBlock } from "./CodeBlock";
|
||||
@@ -27,8 +28,12 @@ export class DocumentViewComponent extends React.PureComponent<
|
||||
> {
|
||||
public render() {
|
||||
const { id } = this.props.match.params as any;
|
||||
if (this.props.allDocs && this.props.allDocs[id]) {
|
||||
if (this.props.allDocs) {
|
||||
const doc = this.props.allDocs[id];
|
||||
if (!doc) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="document">
|
||||
<div className="documentHeader">
|
||||
|
||||
13
frontend/src/Documents/Shared.scss
Normal file
13
frontend/src/Documents/Shared.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
.viewComponent {
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
width: 50rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -$pt-navbar-height;
|
||||
bottom: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-top: 2 * $pt-navbar-height + 20px;
|
||||
max-height: 100%;
|
||||
}
|
||||
95
frontend/src/Documents/SharedView.tsx
Normal file
95
frontend/src/Documents/SharedView.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import "./Docs.scss";
|
||||
|
||||
import { Button, H1 } from "@blueprintjs/core";
|
||||
import * as React from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import { connect } from "react-redux";
|
||||
import { RouteComponentProps, withRouter } from "react-router";
|
||||
import { Dispatch } from "redux";
|
||||
import { IDocumentJSON } from "~../../src/entity/Document";
|
||||
import { LoadingStub } from "~LoadingStub";
|
||||
import { NotFound } from "~NotFound";
|
||||
import { fetchSharedDoc } from "~redux/api/docs";
|
||||
import { fetchDocsStart } from "~redux/docs/actions";
|
||||
import { IAppState } from "~redux/reducers";
|
||||
import { CodeBlock } from "./CodeBlock";
|
||||
|
||||
export interface ISharedViewComponentProps extends RouteComponentProps {
|
||||
loggedIn: boolean;
|
||||
username: string | undefined;
|
||||
}
|
||||
|
||||
export interface ISharedViewComponentState {
|
||||
loaded: boolean;
|
||||
doc: IDocumentJSON | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const defaultState: ISharedViewComponentState = {
|
||||
loaded: false,
|
||||
doc: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export class SharedViewComponent extends React.PureComponent<
|
||||
ISharedViewComponentProps,
|
||||
ISharedViewComponentState
|
||||
> {
|
||||
constructor(props: ISharedViewComponentProps) {
|
||||
super(props);
|
||||
this.state = defaultState;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.loaded) {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<div className="viewComponent">
|
||||
<div>{this.state.error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { doc } = this.state;
|
||||
return (
|
||||
<div className="viewComponent">
|
||||
<div className="document">
|
||||
<div className="documentHeader">
|
||||
<H1>{doc.name}</H1>
|
||||
</div>
|
||||
<div className="documentContents">
|
||||
<Markdown
|
||||
source={doc.content}
|
||||
renderers={{
|
||||
code: CodeBlock,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <LoadingStub />;
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
const { username, id } = this.props.match.params as any;
|
||||
const res = await fetchSharedDoc(username, id);
|
||||
if (!res.error) {
|
||||
this.setState({ loaded: true, doc: res.data });
|
||||
} else {
|
||||
this.setState({ loaded: true, error: res.error });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: IAppState) {
|
||||
return {
|
||||
loggedIn: state.user.user !== null,
|
||||
username: state.user.user.username,
|
||||
};
|
||||
}
|
||||
|
||||
export const SharedView = SharedViewComponent;
|
||||
@@ -16,6 +16,7 @@ const testDoc: IDocumentJSON = {
|
||||
user: 1,
|
||||
createdAt: 0,
|
||||
editedAt: 0,
|
||||
shared: false,
|
||||
};
|
||||
|
||||
const testDocsChanged: { [key: number]: IDocumentEntry } = {
|
||||
@@ -64,6 +65,7 @@ describe("<DocumentEdit />", () => {
|
||||
updateDoc={mock}
|
||||
history={mock}
|
||||
location={mock}
|
||||
username="asdf"
|
||||
match={{ params: { id: 1 } } as any}
|
||||
/>,
|
||||
);
|
||||
@@ -94,6 +96,7 @@ describe("<DocumentEdit />", () => {
|
||||
updateDoc={mock}
|
||||
history={mock}
|
||||
location={mock}
|
||||
username="asdf"
|
||||
match={{ params: { id: 1 } } as any}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
bottom: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-top: 2*$pt-navbar-height + 20px;
|
||||
padding-top: 2 * $pt-navbar-height + 20px;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
.bp3-navbar {
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
|
||||
#uploadingStatusButton {
|
||||
width: 40px;
|
||||
}
|
||||
@@ -41,4 +41,4 @@
|
||||
.bp3-navbar {
|
||||
transition: 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,11 +58,13 @@ export class HomeComponent extends React.PureComponent<IHomeProps> {
|
||||
|
||||
if ((this.props.match.params as any).id && this.props.allDocs) {
|
||||
const { id } = this.props.match.params as any;
|
||||
breadcrumbs.push({
|
||||
icon: "document",
|
||||
text: this.props.allDocs[id].name,
|
||||
onClick: () => this.props.history.push(`/docs/${id}`),
|
||||
});
|
||||
if (this.props.allDocs[id]) {
|
||||
breadcrumbs.push({
|
||||
icon: "document",
|
||||
text: this.props.allDocs[id].name,
|
||||
onClick: () => this.props.history.push(`/docs/${id}`),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
5
frontend/src/NotFound.tsx
Normal file
5
frontend/src/NotFound.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as React from "react";
|
||||
|
||||
export function NotFound() {
|
||||
return <div>404</div>;
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
module.exports = "test-file-stub";
|
||||
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Writer</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="body">
|
||||
|
||||
</div>
|
||||
<script src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>Writer</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="body"></div>
|
||||
<script src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IDocumentJSON } from "~../../src/entity/Document";
|
||||
import { IAPIResponse } from "~../../src/types";
|
||||
|
||||
import { fetchJSONAuth } from "../utils";
|
||||
import { fetchJSONAuth, fetchJSON } from "../utils";
|
||||
|
||||
export async function fetchRecentDocs(): Promise<
|
||||
IAPIResponse<IDocumentJSON[]>
|
||||
@@ -19,12 +19,24 @@ export async function fetchDoc(
|
||||
return fetchJSONAuth(`/docs/byID/${id}`, "GET");
|
||||
}
|
||||
|
||||
export async function fetchSharedDoc(
|
||||
username: string,
|
||||
id: number,
|
||||
): Promise<IAPIResponse<IDocumentJSON>> {
|
||||
return fetchJSON(`/docs/shared/${username}/${id}`, "GET");
|
||||
}
|
||||
|
||||
export async function patchDoc(
|
||||
id: number,
|
||||
name?: string,
|
||||
content?: string,
|
||||
shared?: boolean,
|
||||
): Promise<IAPIResponse<IDocumentJSON>> {
|
||||
return fetchJSONAuth(`/docs/byID/${id}`, "PATCH", { name, content });
|
||||
return fetchJSONAuth(`/docs/byID/${id}`, "PATCH", {
|
||||
name,
|
||||
content,
|
||||
shared,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteDoc(id: number): Promise<IAPIResponse<boolean>> {
|
||||
|
||||
@@ -180,6 +180,7 @@ export interface IDocUpdateAction extends Action {
|
||||
id: number;
|
||||
name: string;
|
||||
content: string;
|
||||
shared: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -187,8 +188,12 @@ export function updateDoc(
|
||||
id: number,
|
||||
name: string,
|
||||
content: string,
|
||||
shared: boolean,
|
||||
): IDocUpdateAction {
|
||||
return { type: DocsTypes.DOC_UPDATE, payload: { id, name, content } };
|
||||
return {
|
||||
type: DocsTypes.DOC_UPDATE,
|
||||
payload: { id, name, content, shared },
|
||||
};
|
||||
}
|
||||
|
||||
export type DocsAction =
|
||||
|
||||
@@ -99,12 +99,14 @@ export const docsReducer: Reducer<IDocsState, DocsAction> = (
|
||||
const remote = doc.remote;
|
||||
if (
|
||||
payload.content !== remote.content ||
|
||||
payload.name !== remote.name
|
||||
payload.name !== remote.name ||
|
||||
payload.shared !== remote.shared
|
||||
) {
|
||||
all[payload.id] = {
|
||||
...doc,
|
||||
content: payload.content,
|
||||
name: payload.name,
|
||||
shared: payload.shared,
|
||||
dirty: true,
|
||||
};
|
||||
dirty = true;
|
||||
@@ -113,6 +115,7 @@ export const docsReducer: Reducer<IDocsState, DocsAction> = (
|
||||
...doc,
|
||||
content: payload.content,
|
||||
name: payload.name,
|
||||
shared: payload.shared,
|
||||
dirty: false,
|
||||
};
|
||||
const dirtyDocs = Object.values(all).filter(e => e.dirty);
|
||||
|
||||
@@ -146,7 +146,7 @@ function* docsUploadStart(action: IDocsUploadStartAction) {
|
||||
|
||||
for (const doc of changedDocs) {
|
||||
const { response, timeout } = yield race({
|
||||
response: call(patchDoc, doc.id, doc.name, doc.content),
|
||||
response: call(patchDoc, doc.id, doc.name, doc.content, doc.shared),
|
||||
timeout: delay(10000),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
],
|
||||
"lib": ["es2017", "dom"],
|
||||
"jsx": "react",
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
@@ -16,9 +13,7 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"~*": [
|
||||
"./src/*"
|
||||
]
|
||||
"~*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface IDocumentJSON {
|
||||
content: string;
|
||||
createdAt: number;
|
||||
editedAt: number;
|
||||
shared: boolean;
|
||||
}
|
||||
|
||||
@Entity()
|
||||
@@ -37,13 +38,17 @@ export class Document extends BaseEntity {
|
||||
@Column({ type: "timestamp", default: null })
|
||||
public editedAt: Date;
|
||||
|
||||
constructor(user: User, name: string, content: string) {
|
||||
@Column({ type: "boolean", default: false })
|
||||
public shared: boolean;
|
||||
|
||||
constructor(user: User, name: string, content: string, shared: boolean) {
|
||||
super();
|
||||
this.createdAt = new Date();
|
||||
this.editedAt = this.createdAt;
|
||||
this.user = user;
|
||||
this.name = name;
|
||||
this.content = content;
|
||||
this.shared = shared;
|
||||
}
|
||||
|
||||
public toJSON(user: number): IDocumentJSON {
|
||||
@@ -54,6 +59,7 @@ export class Document extends BaseEntity {
|
||||
content: this.content,
|
||||
createdAt: this.createdAt.getTime(),
|
||||
editedAt: this.editedAt.getTime(),
|
||||
shared: this.shared,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Router from "koa-router";
|
||||
import { Document } from "~entity/Document";
|
||||
import { User } from "~entity/User";
|
||||
|
||||
export const docsRouter = new Router();
|
||||
|
||||
@@ -10,16 +11,17 @@ docsRouter.post("/docs/new", async ctx => {
|
||||
|
||||
const { user } = ctx.state;
|
||||
|
||||
const { name, content } = (ctx.request as any).body as {
|
||||
const { name, content, shared } = (ctx.request as any).body as {
|
||||
name: string | undefined;
|
||||
content: string | undefined;
|
||||
shared: boolean | undefined;
|
||||
};
|
||||
|
||||
if (!name) {
|
||||
ctx.throw(400);
|
||||
}
|
||||
|
||||
const document = new Document(user.id, name, content);
|
||||
const document = new Document(user.id, name, content, shared);
|
||||
|
||||
try {
|
||||
await document.save();
|
||||
@@ -48,12 +50,13 @@ docsRouter.patch("/docs/byID/:id", async ctx => {
|
||||
ctx.throw(400);
|
||||
}
|
||||
|
||||
const { name, content } = (ctx.request as any).body as {
|
||||
const { name, content, shared } = (ctx.request as any).body as {
|
||||
name: string | undefined;
|
||||
content: string | undefined;
|
||||
shared: boolean | undefined;
|
||||
};
|
||||
|
||||
const document = await Document.findOne({ id, user: user.id });
|
||||
const document = await Document.findOne({ id, user });
|
||||
|
||||
if (!document) {
|
||||
ctx.throw(404);
|
||||
@@ -65,6 +68,9 @@ docsRouter.patch("/docs/byID/:id", async ctx => {
|
||||
if (content) {
|
||||
document.content = content;
|
||||
}
|
||||
if (shared !== undefined) {
|
||||
document.shared = shared;
|
||||
}
|
||||
|
||||
try {
|
||||
document.editedAt = new Date();
|
||||
@@ -86,7 +92,7 @@ docsRouter.get("/docs/list", async ctx => {
|
||||
|
||||
const { user } = ctx.state;
|
||||
|
||||
const documents = await Document.find({ user: user.id });
|
||||
const documents = await Document.find({ user });
|
||||
|
||||
ctx.body = {
|
||||
error: false,
|
||||
@@ -109,7 +115,7 @@ docsRouter.get("/docs/byID/:id", async ctx => {
|
||||
|
||||
const { user } = ctx.state;
|
||||
|
||||
const document = await Document.findOne({ id, user: user.id });
|
||||
const document = await Document.findOne({ id, user });
|
||||
|
||||
if (!document) {
|
||||
ctx.throw(404);
|
||||
@@ -121,6 +127,38 @@ docsRouter.get("/docs/byID/:id", async ctx => {
|
||||
};
|
||||
});
|
||||
|
||||
docsRouter.get("/docs/shared/:username/:id", async ctx => {
|
||||
const { id, username } = ctx.params as {
|
||||
id: number | undefined;
|
||||
username: string | undefined;
|
||||
};
|
||||
|
||||
if (!id || !username) {
|
||||
ctx.throw(400);
|
||||
}
|
||||
|
||||
const user = await User.findOne({ username });
|
||||
|
||||
if (!user) {
|
||||
ctx.throw(404);
|
||||
}
|
||||
|
||||
const document = await Document.findOne({ id, user });
|
||||
|
||||
if (!document) {
|
||||
ctx.throw(404);
|
||||
}
|
||||
|
||||
if (!document.shared) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
error: false,
|
||||
data: document.toJSON(user.id),
|
||||
};
|
||||
});
|
||||
|
||||
docsRouter.delete("/docs/byID/:id", async ctx => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
@@ -136,7 +174,7 @@ docsRouter.delete("/docs/byID/:id", async ctx => {
|
||||
|
||||
const { user } = ctx.state;
|
||||
|
||||
const document = await Document.findOne({ id, user: user.id });
|
||||
const document = await Document.findOne({ id, user });
|
||||
|
||||
if (!document) {
|
||||
ctx.throw(404);
|
||||
|
||||
@@ -88,11 +88,28 @@ describe("docs", () => {
|
||||
|
||||
const documents = response.body.data as IDocumentJSON[];
|
||||
|
||||
const userDocs = [seed.doc1.toJSON(seed.user1.id)];
|
||||
const userDocs = [
|
||||
seed.doc1.toJSON(seed.user1.id),
|
||||
seed.doc2p.toJSON(seed.user1.id),
|
||||
];
|
||||
|
||||
expect(documents).to.deep.equal(userDocs);
|
||||
});
|
||||
|
||||
it("should get a shared document", async () => {
|
||||
const response = await request(callback)
|
||||
.get(`/docs/shared/${seed.user1.username}/${seed.doc2p.id}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.error).to.be.false;
|
||||
|
||||
const document = response.body.data as IDocumentJSON;
|
||||
|
||||
const usedDoc = seed.doc2p.toJSON(seed.user1.id);
|
||||
|
||||
expect(document).to.deep.equal(usedDoc);
|
||||
});
|
||||
|
||||
it("should get a document", async () => {
|
||||
const response = await request(callback)
|
||||
.get(`/docs/byID/${seed.doc1.id}`)
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface ISeed {
|
||||
user1: User;
|
||||
user2: User;
|
||||
doc1: Document;
|
||||
doc2p: Document;
|
||||
}
|
||||
|
||||
export async function seedDB(): Promise<ISeed> {
|
||||
@@ -19,8 +20,11 @@ export async function seedDB(): Promise<ISeed> {
|
||||
await user2.setPassword("User2");
|
||||
await user2.save();
|
||||
|
||||
const doc1 = new Document(user1, "Doc1", "Doc1");
|
||||
await doc1.save();
|
||||
const doc1 = new Document(user1, "Doc1", "Doc1", false);
|
||||
const doc2p = new Document(user1, "Doc2", "Doc2", true);
|
||||
|
||||
return { user1, user2, doc1 };
|
||||
await doc1.save();
|
||||
await doc2p.save();
|
||||
|
||||
return { user1, user2, doc1, doc2p };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user