Skip to content

Commit

Permalink
feat: Diff view
Browse files Browse the repository at this point in the history
  • Loading branch information
areknawo committed Jun 25, 2024
1 parent 4b325a5 commit e4ce5b5
Show file tree
Hide file tree
Showing 49 changed files with 1,270 additions and 158 deletions.
1 change: 1 addition & 0 deletions apps/backend/collaboration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@hocuspocus/server": "^2.11.3",
"@vrite/backend": "workspace:*",
"@vrite/sdk": "workspace:*",
"dayjs": "^1.11.10",
"fastify": "^4.26.0",
"mongodb": "^6.3.0",
"y-protocols": "^1.0.6",
Expand Down
84 changes: 49 additions & 35 deletions apps/backend/collaboration/src/extensions/version-history.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Extension, onChangePayload } from "@hocuspocus/server";
import { Extension, onChangePayload, onDisconnectPayload } from "@hocuspocus/server";
import {
docToJSON,
getContentVersionsCollection,
getVersionsCollection,
jsonToBuffer,
publishVersionEvent,
fetchEntryMembers
fetchEntryMembers,
DocJSON
} from "@vrite/backend";
import { FastifyInstance } from "fastify";
import { Binary, ObjectId } from "mongodb";
import dayjs from "dayjs";

interface Configuration {
debounce: number | false | null;
Expand Down Expand Up @@ -38,13 +40,17 @@ class VersionHistory implements Extension {
this.contentVersionsCollection = getContentVersionsCollection(fastify.mongo.db!);
}

public async onChange({
documentName,
document,
context,
update,
...x
}: onChangePayload): Promise<void> {
public async onDisconnect({ documentName, clientsCount }: onDisconnectPayload): Promise<any> {
if (clientsCount === 0) {
const debounced = this.debounced.get(documentName);

if (debounced?.timeout) clearTimeout(debounced.timeout);

this.debounced.delete(documentName);
}
}

public async onChange({ documentName, document, context }: onChangePayload): Promise<void> {
return this.debounceUpdate({ documentName, document, context });
}

Expand All @@ -65,9 +71,13 @@ class VersionHistory implements Extension {
const update = (): void => {
const debouncedData = this.debounced.get(documentName);

this.createVersion(contentPieceId, variantId, debouncedData?.members || [], {
context,
document
this.createVersion({
contentPieceId,
variantId,
members: debouncedData?.members || [],
json: docToJSON(document),
userId: `${context.userId}`,
workspaceId: `${context.workspaceId}`
});
};

Expand All @@ -80,52 +90,56 @@ class VersionHistory implements Extension {
);
}

private async createVersion(
contentPieceId: string,
variantId: string | null,
members: string[],
details: Pick<onChangePayload, "context" | "document">
): Promise<void> {
if (variantId) return;
private async createVersion(details: {
contentPieceId: string;
workspaceId: string;
userId: string;
variantId: string | null;
members: string[];
json: DocJSON;
}): Promise<void> {
if (details.variantId) return;

const ctx = {
db: this.fastify.mongo.db!,
auth: {
workspaceId: new ObjectId(`${details.context.workspaceId}`),
userId: new ObjectId(`${details.context.userId}`)
workspaceId: new ObjectId(`${details.workspaceId}`),
userId: new ObjectId(`${details.userId}`)
}
};
const json = docToJSON(details.document);
const buffer = jsonToBuffer(json);
const buffer = jsonToBuffer(details.json);
const versionId = new ObjectId();
const date = new Date();
const version = {
_id: versionId,
date,
contentPieceId: new ObjectId(contentPieceId),
...(variantId ? { variantId: new ObjectId(variantId) } : {}),
members: members.map((id) => new ObjectId(id)),
workspaceId: ctx.auth.workspaceId
contentPieceId: new ObjectId(details.contentPieceId),
...(details.variantId ? { variantId: new ObjectId(details.variantId) } : {}),
members: details.members.map((id) => new ObjectId(id)),
workspaceId: ctx.auth.workspaceId,
expiresAt: dayjs(date).add(31, "days").toDate()
};

await this.versionsCollection.insertOne(version);
await this.contentVersionsCollection.insertOne({
_id: new ObjectId(),
contentPieceId: new ObjectId(contentPieceId),
contentPieceId: new ObjectId(details.contentPieceId),
versionId,
...(variantId ? { variantId: new ObjectId(variantId) } : {}),
content: new Binary(buffer)
...(details.variantId ? { variantId: new ObjectId(details.variantId) } : {}),
content: new Binary(buffer),
expiresAt: dayjs(date).add(31, "days").toDate()
});
publishVersionEvent({ fastify: this.fastify }, `${details.context.workspaceId}`, {
publishVersionEvent({ fastify: this.fastify }, `${details.workspaceId}`, {
action: "create",
userId: `${details.context.userId}`,
userId: `${details.userId}`,
data: {
id: `${versionId}`,
date: date.toISOString(),
contentPieceId: `${contentPieceId}`,
variantId: variantId ? `${variantId}` : null,
contentPieceId: `${details.contentPieceId}`,
variantId: details.variantId ? `${details.variantId}` : null,
members: await fetchEntryMembers(ctx.db, version),
workspaceId: `${ctx.auth.workspaceId}`
workspaceId: `${ctx.auth.workspaceId}`,
expiresAt: version.expiresAt?.toISOString()
}
});
}
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ const VersionEditorView = lazy(async () => {

return { default: VersionEditorView };
});
const DiffEditorView = lazy(async () => {
const { DiffEditorView } = await import("#views/editor");

return { default: DiffEditorView };
});
const DashboardView = lazy(async () => {
const { DashboardView } = await import("#views/dashboard");

Expand Down Expand Up @@ -75,6 +80,7 @@ const App: Component = () => {
<Route path="/editor/*contentPieceId" component={ContentPieceEditorView} />
<Route path="/snippet/*snippetId" component={SnippetEditorView} />
<Route path="/version/:contentPieceId/*versionId" component={VersionEditorView} />
<Route path="/diff/:diffAgainst/:contentPieceId/*versionId" component={DiffEditorView} />
<Show when={hostConfig.githubApp}>
<Route path="/conflict" component={ConflictView} />
</Show>
Expand Down
20 changes: 14 additions & 6 deletions apps/web/src/context/history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
useContext
} from "solid-js";
import { createStore, reconcile } from "solid-js/store";
import { useLocation, useParams } from "@solidjs/router";
import { useParams } from "@solidjs/router";
import { App, useClient, useContentData } from "#context";

interface HistoryActions {
Expand All @@ -26,13 +26,13 @@ interface HistoryDataContextData {
historyActions: HistoryActions;
loadMore(): Promise<void>;
activeVersionId(): string;
diffAgainst(): "latest" | "previous" | "";
}

const HistoryDataContext = createContext<HistoryDataContextData>();
const HistoryDataProvider: ParentComponent = (props) => {
const client = useClient();
const params = useParams();
const location = useLocation();
const { activeContentPieceId, activeVariantId } = useContentData();
const [versions, setVersions] = createStore<Record<string, App.VersionWithAdditionalData>>({});
const [entryIds, setEntryIds] = createSignal<string[]>([]);
Expand All @@ -59,10 +59,13 @@ const HistoryDataProvider: ParentComponent = (props) => {
setLoading(false);
setMoreToLoad(data.length === 100);
};
const activeVersionId = createMemo((): string => {
if (!location.pathname.startsWith("/version")) return "";
const activeVersionId = createMemo(() => {
return params.versionId || "";
});
const diffAgainst = createMemo(() => {
if (!params.diffAgainst) return "";

return params.versionId;
return params.diffAgainst === "latest" ? "latest" : "previous";
});
const historyActions: HistoryActions = {
updateVersion: (update) => {
Expand All @@ -75,7 +78,11 @@ const HistoryDataProvider: ParentComponent = (props) => {
onData({ action, data }) {
if (action === "update") {
historyActions.updateVersion(data);
} else if (action === "create") {
} else if (
action === "create" &&
data.contentPieceId === activeContentPieceId() &&
data.variantId === activeVariantId()
) {
setEntryIds((entries) => [data.id, ...entries]);
setVersions(data.id, data);
}
Expand Down Expand Up @@ -120,6 +127,7 @@ const HistoryDataProvider: ParentComponent = (props) => {
moreToLoad,
loadMore,
activeVersionId,
diffAgainst,
historyActions
}}
>
Expand Down
11 changes: 8 additions & 3 deletions apps/web/src/layout/side-panel-right.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
mdiHistory,
mdiShapeOutline
} from "@mdi/js";
import { useLocation } from "@solidjs/router";
import { useLocation, useParams } from "@solidjs/router";
import { useContentData, useHistoryData, useLocalStorage } from "#context";
import { createRef } from "#lib/utils";
import { ExplorerView } from "#views/explorer";
Expand Down Expand Up @@ -44,9 +44,9 @@ const sidePanelRightViews: Record<
},
history: {
show: () => {
const location = useLocation();
const params = useParams();

return showEditorSpecificView() || location.pathname.startsWith("/version");
return showEditorSpecificView() || Boolean(params.versionId);
},
view: HistoryView,
icon: mdiHistory,
Expand All @@ -67,6 +67,8 @@ const SidePanelRight: Component = () => {
const { show } = sidePanelRightViews[viewId];

if (show && !show()) {
setStorage({ sidePanelRightView: "explorer" });

return "explorer";
}

Expand Down Expand Up @@ -106,9 +108,11 @@ const SidePanelRight: Component = () => {
};
const onPointerUp = (): void => {
setDragging(false);
document.body.style.userSelect = "";
};
const onPointerLeave = (): void => {
setDragging(false);
document.body.style.userSelect = "";
};

setPreviousWidth(Number(localStorage.getItem("rightPanelWidth")));
Expand Down Expand Up @@ -176,6 +180,7 @@ const SidePanelRight: Component = () => {
setDragging(true);
setPreviousWidth(storage().rightPanelWidth || 0);
setPrevX(event.x);
document.body.style.userSelect = "none";
}}
onPointerEnter={() => {
triggerHandleHover();
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/layout/side-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,11 @@ const SidePanel: Component = () => {
};
const onPointerUp = (): void => {
setDragging(false);
document.body.style.userSelect = "";
};
const onPointerLeave = (): void => {
setDragging(false);
document.body.style.userSelect = "";
};

setPreviousWidth(Number(localStorage.getItem("sidePanelWidth")));
Expand Down Expand Up @@ -124,6 +126,7 @@ const SidePanel: Component = () => {
setDragging(true);
setPreviousWidth(storage().sidePanelWidth || 0);
setPrevX(event.x);
document.body.style.userSelect = "none";
}}
onPointerEnter={() => {
triggerHandleHover();
Expand Down
13 changes: 8 additions & 5 deletions apps/web/src/layout/toolbar/export-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,25 @@ import {
mdiLanguageHtml5,
mdiLanguageMarkdown
} from "@mdi/js";
import { Component, createSignal } from "solid-js";
import { Component, Show, createSignal } from "solid-js";
import { gfmOutputTransformer, htmlOutputTransformer } from "@vrite/sdk/transformers";
import { JSONContent } from "@vrite/sdk";
import { nanoid } from "nanoid";
import clsx from "clsx";
import { diff } from "lib0";
import { Card, Dropdown, Heading, IconButton, Overlay, Tooltip } from "#components/primitives";
import { MiniCodeEditor } from "#components/fragments";
import {
App,
useAuthenticatedUserData,
useClient,
useCommandPalette,
useContentData,
useHistoryData,
useNotifications,
useSharedState
} from "#context";
import { formatCode } from "#lib/code-editor";
import { escapeHTML } from "#lib/utils";
import { mdxIcon } from "#assets/icons";

interface ExportMenuProps {
Expand All @@ -38,7 +40,8 @@ type ExportType = "html" | "json" | "md" | "mdx";

const ExportMenu: Component<ExportMenuProps> = (props) => {
const { useSharedSignal } = useSharedState();
const client = useClient();
const { activeContentPieceId } = useContentData();
const { diffAgainst } = useHistoryData();
const [editor] = useSharedSignal("editor");
const { registerCommand } = useCommandPalette();
const { workspaceSettings } = useAuthenticatedUserData();
Expand Down Expand Up @@ -154,7 +157,7 @@ const ExportMenu: Component<ExportMenuProps> = (props) => {
]);

return (
<>
<Show when={activeContentPieceId() && !diffAgainst()}>
<Dropdown
activatorWrapperClass="w-full"
activatorButton={() => (
Expand Down Expand Up @@ -295,7 +298,7 @@ const ExportMenu: Component<ExportMenuProps> = (props) => {
</div>
</Card>
</Overlay>
</>
</Show>
);
};

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/layout/toolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const toolbarViews: Record<string, Component<Record<string, any>>> = {
const { useSharedSignal } = useSharedState();
const { registerCommand } = useCommandPalette();
const [sharedProvider] = useSharedSignal("provider");
const { storage, setStorage } = useLocalStorage();
const { setStorage } = useLocalStorage();
const [menuOpened, setMenuOpened] = createSignal(false);

createEffect(
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/editor/editing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ const createClipboardSerializer = (
const createExtensions = (
extensionsContext: ExtensionsContextData,
settings: App.WorkspaceSettings,
provider: HocuspocusProvider
provider?: HocuspocusProvider | null
): Array<MarkExtension | NodeExtension> => {
const resetExtensionConfig = {
addKeyboardShortcuts: () => ({}),
Expand Down
Loading

0 comments on commit e4ce5b5

Please sign in to comment.