Merge pull request #14 from usaatyuk/sharing

Sharing
This commit is contained in:
2019-09-13 15:20:20 +03:00
committed by GitHub
27 changed files with 10935 additions and 10643 deletions

21030
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -16,3 +16,11 @@ export function showDeletionToast(cancelFn: () => void) {
},
});
}
export function showSharedToast() {
AppToaster.show({
message: "Link copied to clipboard!",
intent: "success",
timeout: 2000,
});
}

View File

@@ -38,4 +38,4 @@
}
}
}
}
}

View File

@@ -2,4 +2,4 @@
.bp3-dark {
@import "../../node_modules/highlight.js/styles/a11y-dark";
}
}

View File

@@ -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%;
}
}
}
}
}

View File

@@ -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)),
};
}

View File

@@ -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">

View 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%;
}

View 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;

View File

@@ -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}
/>,
);

View File

@@ -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;
}
}
}

View File

@@ -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 (

View File

@@ -0,0 +1,5 @@
import * as React from "react";
export function NotFound() {
return <div>404</div>;
}

View File

@@ -1,2 +1 @@
module.exports = "test-file-stub";

View File

@@ -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>

View File

@@ -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>> {

View File

@@ -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 =

View File

@@ -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);

View File

@@ -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),
});

View File

@@ -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/*"]
}
}
}
}

View File

@@ -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,
};
}
}

View File

@@ -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);

View File

@@ -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}`)

View File

@@ -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 };
}