Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON to client #5553

Merged
merged 32 commits into from May 24, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0a82c5d
wip
tommoor Jul 5, 2023
4cb2e0e
fix editor tasks parsing on server
tommoor Jul 5, 2023
fc2277d
Add getEmptyDocument helper
tommoor Jul 6, 2023
b001da6
Clone template instead of reference
tommoor Jul 6, 2023
6118545
Merge branch 'main' into tom/3000-json-to-client
tommoor Jul 8, 2023
59b7544
Need to include state by default now
tommoor Jul 9, 2023
db0f3c5
Merge branch 'main' into tom/3000-json-to-client
tommoor Jul 15, 2023
3f487c9
test
tommoor Jul 15, 2023
dd267da
Merge branch 'main' into tom/3000-json-to-client
tommoor Aug 20, 2023
94f7434
Merge branch 'main' into tom/3000-json-to-client
tommoor Sep 1, 2023
dda5c83
Merge branch 'main' into tom/3000-json-to-client
tommoor Nov 18, 2023
fe8b34c
test
tommoor Nov 18, 2023
f007111
Support public page url signing
tommoor Nov 18, 2023
4a85f45
Add API response backwards compat, move API version to header
tommoor Nov 18, 2023
1526a98
test
tommoor Nov 18, 2023
40e58db
fix response
tommoor Nov 18, 2023
c60de30
Can no longer share toMarkdown method
tommoor Dec 18, 2023
47f0787
test
tommoor Dec 18, 2023
541a989
wip
tommoor Dec 19, 2023
dfa6d63
Merge main
tommoor May 19, 2024
2c42cdc
More test support
tommoor May 19, 2024
4ebcb00
test
tommoor May 19, 2024
b189d33
Prefer content over state for performance if available
tommoor May 20, 2024
b2d4b8c
Move DocumentHelper to named export
tommoor May 21, 2024
13634b9
fix: Backwards compatability of text field
tommoor May 21, 2024
906dfd3
Named exports
tommoor May 21, 2024
1004f9b
docs
tommoor May 21, 2024
2827c49
Write .content on welcome docs
tommoor May 21, 2024
86ffae7
fix: Facepile style and count
tommoor May 21, 2024
31091c6
fix: Incorrect fetching into store
tommoor May 22, 2024
489d194
Remove comment marks from publicly shared documents
tommoor May 23, 2024
08f4dfe
lint
tommoor May 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/editor/index.tsx
Expand Up @@ -38,7 +38,7 @@ import { basicExtensions as extensions } from "@shared/editor/nodes";
import Node from "@shared/editor/nodes/Node";
import ReactNode from "@shared/editor/nodes/ReactNode";
import { EventType } from "@shared/editor/types";
import { UserPreferences } from "@shared/types";
import { ProsemirrorData, UserPreferences } from "@shared/types";
import ProsemirrorHelper from "@shared/utils/ProsemirrorHelper";
import EventEmitter from "@shared/utils/events";
import Flex from "~/components/Flex";
Expand All @@ -58,7 +58,7 @@ export type Props = {
/** The current userId, if any */
userId?: string;
/** The editor content, should only be changed if you wish to reset the content */
value?: string;
value?: string | ProsemirrorData;
/** The initial editor content as a markdown string or JSON object */
defaultValue: string | object;
/** Placeholder displayed when the editor is empty */
Expand Down
36 changes: 22 additions & 14 deletions app/models/Document.ts
Expand Up @@ -3,7 +3,7 @@ import i18n, { t } from "i18next";
import floor from "lodash/floor";
import { action, autorun, computed, observable, set } from "mobx";
import { ExportContentType } from "@shared/types";
import type { NavigationNode } from "@shared/types";
import type { NavigationNode, ProsemirrorData } from "@shared/types";
import Storage from "@shared/utils/Storage";
import { isRTL } from "@shared/utils/rtl";
import slugify from "@shared/utils/slugify";
Expand Down Expand Up @@ -50,19 +50,16 @@ export default class Document extends ParanoidModel {
@observable
id: string;

@observable.shallow
data: ProsemirrorData;

/**
* The id of the collection that this document belongs to, if any.
*/
@Field
@observable
collectionId?: string | null;

/**
* The text content of the document as Markdown.
*/
@observable
text: string;

/**
* The title of the document.
*/
Expand Down Expand Up @@ -402,10 +399,16 @@ export default class Document extends ParanoidModel {
duplicate = (options?: { title?: string; recursive?: boolean }) =>
this.store.duplicate(this, options);

getSummary = (paragraphs = 4) => {
const result = this.text.trim().split("\n").slice(0, paragraphs).join("\n");
return result;
};
/**
* Returns the first blocks of the document, useful for displaying a preview.
*
* @param blocks The number of blocks to return, defaults to 4.
* @returns A new ProseMirror document.
*/
getSummary = (blocks = 4) => ({
...this.data,
content: this.data.content.slice(0, blocks),
});

@computed
get pinned(): boolean {
Expand All @@ -427,14 +430,19 @@ export default class Document extends ParanoidModel {
return !this.isDeleted && !this.isTemplate && !this.isArchived;
}

@computed
get childDocuments() {
return this.store.orderedData.filter(
(doc) => doc.parentDocumentId === this.id
);
}

@computed
get asNavigationNode(): NavigationNode {
return {
id: this.id,
title: this.title,
children: this.store.orderedData
.filter((doc) => doc.parentDocumentId === this.id)
.map((doc) => doc.asNavigationNode),
children: this.childDocuments.map((doc) => doc.asNavigationNode),
url: this.url,
isDraft: this.isDraft,
};
Expand Down
5 changes: 3 additions & 2 deletions app/models/Revision.ts
@@ -1,4 +1,5 @@
import { computed } from "mobx";
import { ProsemirrorData } from "@shared/types";
import { isRTL } from "@shared/utils/rtl";
import Document from "./Document";
import User from "./User";
Expand All @@ -16,8 +17,8 @@ class Revision extends Model {
/** The document title when the revision was created */
title: string;

/** Markdown string of the content when revision was created */
text: string;
/** Prosemirror data of the content when revision was created */
data: ProsemirrorData;

/** The emoji of the document when the revision was created */
emoji: string | null;
Expand Down
5 changes: 3 additions & 2 deletions app/scenes/Document/components/DataLoader.tsx
Expand Up @@ -2,6 +2,7 @@ import { observer } from "mobx-react";
import * as React from "react";
import { useLocation, RouteComponentProps, StaticContext } from "react-router";
import { NavigationNode, TeamPreference } from "@shared/types";
import ProsemirrorHelper from "@shared/utils/ProsemirrorHelper";
import { RevisionHelper } from "@shared/utils/RevisionHelper";
import Document from "~/models/Document";
import Revision from "~/models/Revision";
Expand Down Expand Up @@ -92,7 +93,7 @@ function DataLoader({ match, children }: Props) {
}
}
void fetchDocument();
}, [ui, documents, document, shareId, documentSlug]);
}, [ui, documents, shareId, documentSlug]);

React.useEffect(() => {
async function fetchRevision() {
Expand Down Expand Up @@ -161,7 +162,7 @@ function DataLoader({ match, children }: Props) {
collectionId: document.collectionId,
parentDocumentId: document.parentDocumentId,
title,
text: "",
data: ProsemirrorHelper.getEmptyDocument(),
});

return newDocument.url;
Expand Down
46 changes: 23 additions & 23 deletions app/scenes/Document/components/Document.tsx
@@ -1,6 +1,9 @@
import cloneDeep from "lodash/cloneDeep";
import debounce from "lodash/debounce";
import isEqual from "lodash/isEqual";
import { action, observable } from "mobx";
import { observer } from "mobx-react";
import { Node } from "prosemirror-model";
import { AllSelection } from "prosemirror-state";
import * as React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
Expand All @@ -16,9 +19,8 @@ import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { s } from "@shared/styles";
import { NavigationNode } from "@shared/types";
import { Heading } from "@shared/utils/ProsemirrorHelper";
import ProsemirrorHelper, { Heading } from "@shared/utils/ProsemirrorHelper";
import { parseDomain } from "@shared/utils/domains";
import getTasks from "@shared/utils/getTasks";
import RootStore from "~/stores/RootStore";
import Document from "~/models/Document";
import Revision from "~/models/Revision";
Expand All @@ -34,6 +36,7 @@ import PlaceholderDocument from "~/components/PlaceholderDocument";
import RegisterKeyDown from "~/components/RegisterKeyDown";
import withStores from "~/components/withStores";
import type { Editor as TEditor } from "~/editor";
import { SearchResult } from "~/editor/components/LinkEditor";
import { client } from "~/utils/ApiClient";
import { replaceTitleVariables } from "~/utils/date";
import { emojiToUrl } from "~/utils/emoji";
Expand Down Expand Up @@ -73,13 +76,13 @@ type Props = WithTranslation &
RootStore &
RouteComponentProps<Params, StaticContext, LocationState> & {
sharedTree?: NavigationNode;
abilities: Record<string, any>;
abilities: Record<string, boolean>;
document: Document;
revision?: Revision;
readOnly: boolean;
shareId?: string;
onCreateLink?: (title: string) => Promise<string>;
onSearchLink?: (term: string) => any;
onSearchLink?: (term: string) => Promise<SearchResult[]>;
};

@observer
Expand Down Expand Up @@ -108,8 +111,6 @@ class DocumentScene extends React.Component<Props> {
@observable
headings: Heading[] = [];

getEditorText: () => string = () => this.props.document.text;

componentDidMount() {
this.updateIsDirty();
}
Expand Down Expand Up @@ -140,8 +141,8 @@ class DocumentScene extends React.Component<Props> {
return;
}

const { view, parser } = editorRef;
const doc = parser.parse(template.text);
const { view, schema } = editorRef;
const doc = Node.fromJSON(schema, template.data);

if (doc) {
view.dispatch(
Expand All @@ -168,10 +169,8 @@ class DocumentScene extends React.Component<Props> {
if (template.emoji) {
this.props.document.emoji = template.emoji;
}
if (template.text) {
this.props.document.text = template.text;
}

this.props.document.data = cloneDeep(template.data);
this.updateIsDirty();

return this.onSave({
Expand Down Expand Up @@ -292,15 +291,18 @@ class DocumentScene extends React.Component<Props> {
}

// get the latest version of the editor text value
const text = this.getEditorText ? this.getEditorText() : document.text;
const doc = this.editor.current?.view.state.doc;
if (!doc) {
return;
}

// prevent save before anything has been written (single hash is empty doc)
if (text.trim() === "" && document.title.trim() === "") {
if (ProsemirrorHelper.isEmpty(doc) && document.title.trim() === "") {
return;
}

document.text = text;
document.tasks = getTasks(document.text);
document.data = doc.toJSON();
document.tasks = ProsemirrorHelper.getTasksSummary(doc);

// prevent autosave if nothing has changed
if (options.autosave && !this.isEditorDirty && !document.isDirty()) {
Expand Down Expand Up @@ -340,12 +342,11 @@ class DocumentScene extends React.Component<Props> {

updateIsDirty = () => {
const { document } = this.props;
const editorText = this.getEditorText().trim();
this.isEditorDirty = editorText !== document.text.trim();
const doc = this.editor.current?.view.state.doc;
this.isEditorDirty = !isEqual(doc?.toJSON(), document.data);

// a single hash is a doc with just an empty title
this.isEmpty =
(!editorText || editorText === "#" || editorText === "\\") && !this.title;
this.isEmpty = (!doc || ProsemirrorHelper.isEmpty(doc)) && !this.title;
};

updateIsDirtyDebounced = debounce(this.updateIsDirty, 500);
Expand All @@ -358,9 +359,8 @@ class DocumentScene extends React.Component<Props> {
this.isUploading = false;
};

handleChange = (getEditorText: () => string) => {
handleChange = () => {
const { document } = this.props;
this.getEditorText = getEditorText;

// Keep derived task list in sync
const tasks = this.editor.current?.getTasks();
Expand Down Expand Up @@ -507,8 +507,8 @@ class DocumentScene extends React.Component<Props> {
isDraft={document.isDraft}
template={document.isTemplate}
document={document}
value={readOnly ? document.text : undefined}
defaultValue={document.text}
value={readOnly ? document.data : undefined}
defaultValue={document.data}
embedsDisabled={embedsDisabled}
onSynced={this.onSynced}
onFileUploadStart={this.onFileUploadStart}
Expand Down
3 changes: 2 additions & 1 deletion app/scenes/DocumentNew.tsx
Expand Up @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import { useHistory, useLocation, useRouteMatch } from "react-router-dom";
import { toast } from "sonner";
import { UserPreference } from "@shared/types";
import ProsemirrorHelper from "@shared/utils/ProsemirrorHelper";
import CenteredContent from "~/components/CenteredContent";
import Flex from "~/components/Flex";
import PlaceholderDocument from "~/components/PlaceholderDocument";
Expand Down Expand Up @@ -49,7 +50,7 @@ function DocumentNew({ template }: Props) {
templateId: query.get("templateId") ?? undefined,
template,
title: "",
text: "",
data: ProsemirrorHelper.getEmptyDocument(),
});
history.replace(
template || !user.separateEditMode
Expand Down
1 change: 0 additions & 1 deletion app/stores/DocumentsStore.ts
Expand Up @@ -501,7 +501,6 @@ export default class DocumentsStore extends Store<Document> {
const res = await client.post("/documents.info", {
id,
shareId: options.shareId,
apiVersion: 2,
});

invariant(res?.data, "Document not available");
Expand Down
1 change: 0 additions & 1 deletion app/stores/RevisionsStore.ts
Expand Up @@ -35,7 +35,6 @@ export default class RevisionsStore extends Store<Revision> {
id: "latest",
documentId: document.id,
title: document.title,
text: document.text,
createdAt: document.updatedAt,
createdBy: document.createdBy,
},
Expand Down
1 change: 1 addition & 0 deletions app/utils/ApiClient.ts
Expand Up @@ -82,6 +82,7 @@ class ApiClient {
Accept: "application/json",
"cache-control": "no-cache",
"x-editor-version": EDITOR_VERSION,
"x-api-version": "3",
pragma: "no-cache",
...options?.headers,
};
Expand Down
2 changes: 1 addition & 1 deletion plugins/webhooks/server/tasks/DeliverWebhookTask.ts
Expand Up @@ -507,7 +507,7 @@ export default class DeliverWebhookTask extends BaseTask<Props> {
subscription,
payload: {
id: event.documentId,
model: model && (await presentDocument(model)),
model: model && (await presentDocument(undefined, model)),
},
});
}
Expand Down
41 changes: 16 additions & 25 deletions server/models/Document.test.ts
Expand Up @@ -179,20 +179,7 @@ describe("#findByPk", () => {
});

describe("tasks", () => {
test("should consider all the possible checkTtems", async () => {
const document = await buildDocument({
text: `- [x] test
- [X] test
- [ ] test
- [-] test
- [_] test`,
});
const tasks = document.tasks;
expect(tasks.completed).toBe(4);
expect(tasks.total).toBe(5);
});

test("should return tasks keys set to 0 if checkItems isn't present", async () => {
test("should return tasks keys set to 0 if check items isn't present", async () => {
const document = await buildDocument({
text: `text`,
});
Expand All @@ -201,11 +188,12 @@ describe("tasks", () => {
expect(tasks.total).toBe(0);
});

test("should return tasks keys set to 0 if the text contains broken checkItems", async () => {
test("should return tasks keys set to 0 if the text contains broken check items", async () => {
const document = await buildDocument({
text: `- [x ] test
- [ x ] test
- [ ] test`,
text: `
- [x ] test
- [ x ] test
- [ ] test`,
});
const tasks = document.tasks;
expect(tasks.completed).toBe(0);
Expand All @@ -214,8 +202,9 @@ describe("tasks", () => {

test("should return tasks", async () => {
const document = await buildDocument({
text: `- [x] list item
- [ ] list item`,
text: `
- [x] list item
- [ ] list item`,
});
const tasks = document.tasks;
expect(tasks.completed).toBe(1);
Expand All @@ -224,15 +213,17 @@ describe("tasks", () => {

test("should update tasks on save", async () => {
const document = await buildDocument({
text: `- [x] list item
- [ ] list item`,
text: `
- [x] list item
- [ ] list item`,
});
const tasks = document.tasks;
expect(tasks.completed).toBe(1);
expect(tasks.total).toBe(2);
document.text = `- [x] list item
- [ ] list item
- [ ] list item`;
document.text = `
- [x] list item
- [ ] list item
- [ ] list item`;
await document.save();
const newTasks = document.tasks;
expect(newTasks.completed).toBe(1);
Expand Down