Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion DocmostlyMac/DocmostlyMacCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ struct DocmostlyMacCommands: Commands {
.disabled(appState.phase != .authenticated)

Button("Space Settings", systemImage: "gearshape") {
select(.settings)
presentSpaceSettings()
}
.keyboardShortcut("4", modifiers: .command)
.disabled(appState.phase != .authenticated)
Expand Down Expand Up @@ -117,6 +117,16 @@ struct DocmostlyMacCommands: Commands {
openWindow(id: "main")
}

private func presentSpaceSettings() {
if let focusedActions {
focusedActions.presentSpaceSettings()
return
}

commandController.requestSpaceSettingsPresentation()
openWindow(id: "main")
}

private func showSettings() {
MacSettingsWindowController.show(
appState: appState,
Expand Down
10 changes: 10 additions & 0 deletions DocmostlyMac/MacDesktopCommandController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,27 @@ import SwiftUI
@Observable
final class MacDesktopCommandController {
var sidebarReloadRequestID = UUID()
var spaceSettingsPresentationRequestID: UUID?

func requestSidebarReload() {
sidebarReloadRequestID = UUID()
}

func requestSpaceSettingsPresentation() {
spaceSettingsPresentationRequestID = UUID()
}

func clearSpaceSettingsPresentationRequest() {
spaceSettingsPresentationRequestID = nil
}
}

struct MacDesktopCommandActions {
let canCreatePage: () -> Bool
let selectedPageRoute: () -> MacPageWindowRoute?
let presentCommandPalette: () -> Void
let presentPageCreation: () -> Void
let presentSpaceSettings: () -> Void
let selectSidebarDestination: (SidebarDestination) -> Void
let openSelectedPageInNewWindow: () -> Void
}
Expand Down
8 changes: 7 additions & 1 deletion DocmostlyMac/MacPageWindowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ struct MacPageWindowView: View {
keywords: ["permissions", "members"],
isEnabled: appState.phase == .authenticated
) {
selectSidebarDestination(.settings)
presentSpaceSettings()
},
MacCommandPaletteItem(
title: "Refresh Spaces",
Expand Down Expand Up @@ -156,6 +156,7 @@ struct MacPageWindowView: View {
selectedPageRoute: { selectedPageRoute },
presentCommandPalette: presentCommandPalette,
presentPageCreation: presentPageCreation,
presentSpaceSettings: presentSpaceSettings,
selectSidebarDestination: selectSidebarDestination,
openSelectedPageInNewWindow: openSelectedPageInNewWindow
)
Expand All @@ -179,6 +180,11 @@ struct MacPageWindowView: View {
isPageCreationPresented = true
}

private func presentSpaceSettings() {
commandController.requestSpaceSettingsPresentation()
openWindow(id: "main")
}

private func updateLoadedPageContext(pageID: String, spaceID: String, title: String) {
loadedPageID = pageID
loadedPageSpaceID = spaceID
Expand Down
7 changes: 6 additions & 1 deletion DocmostlyMac/MacRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ struct MacRootView: View {
keywords: ["permissions", "members"],
isEnabled: appState.phase == .authenticated
) {
appState.selectSidebarUtilityDestination(.settings)
presentSpaceSettings()
},
MacCommandPaletteItem(
title: "Refresh Spaces",
Expand Down Expand Up @@ -162,6 +162,7 @@ struct MacRootView: View {
selectedPageRoute: { selectedPageRoute },
presentCommandPalette: presentCommandPalette,
presentPageCreation: presentPageCreation,
presentSpaceSettings: presentSpaceSettings,
selectSidebarDestination: selectSidebarDestination,
openSelectedPageInNewWindow: openSelectedPageInNewWindow
)
Expand All @@ -175,6 +176,10 @@ struct MacRootView: View {
isPageCreationPresented = true
}

private func presentSpaceSettings() {
commandController.requestSpaceSettingsPresentation()
}

private func createRootPage(title: String) async -> String? {
guard let selectedSpace else {
return "No space selected."
Expand Down
21 changes: 21 additions & 0 deletions DocmostlyMacUITests/DocmostlyMacUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,27 @@ final class DocmostlyMacUITests: XCTestCase {
XCTAssertTrue(app.staticTexts["Roadmap native preview content"].waitForExistence(timeout: 5))
}

func testPreviewShellCanSwitchSpacesAfterOpeningOverviewPage() throws {
let app = launchMainShellPreview()

XCTAssertTrue(app.buttons["PageOpenLink.roadmap"].waitForExistence(timeout: 5))
app.buttons["PageOpenLink.roadmap"].click()
XCTAssertTrue(app.textFields["Page title"].waitForExistence(timeout: 5))
XCTAssertEqual(app.textFields["Page title"].value as? String, "Roadmap")

app.buttons["Product"].click()
XCTAssertTrue(app.menuItems["Engineering"].waitForExistence(timeout: 5))
app.menuItems["Engineering"].click()

XCTAssertTrue(app.buttons["PageOpenLink.architecture"].waitForExistence(timeout: 5))
XCTAssertFalse(app.images["Warning"].exists)

app.buttons["PageOpenLink.architecture"].click()
XCTAssertTrue(app.textFields["Page title"].waitForExistence(timeout: 5))
XCTAssertEqual(app.textFields["Page title"].value as? String, "Architecture")
XCTAssertTrue(app.staticTexts["Architecture native preview content"].waitForExistence(timeout: 5))
}

func testLaunchPerformance() throws {
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
Expand Down
15 changes: 0 additions & 15 deletions Tools/CRDTRuntime/src/docmostly-crdt-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import { Schema } from "@tiptap/pm/model";

const fragmentName = "default";
const seedClientID = 1;

const schema = new Schema({
nodes: {
Expand Down Expand Up @@ -223,7 +222,6 @@ class DocmostlyCRDTDocument {
this.snapshots = [];
this.ydoc = new Y.Doc();
this.fragment = this.ydoc.getXmlFragment(fragmentName);
this.applySeedDocument(seed.title, seed.document);
this.ydoc.on("update", (update, origin) => {
if (origin === this.localOrigin) {
this.localUpdates.push(base64FromBytes(update));
Expand Down Expand Up @@ -315,19 +313,6 @@ class DocmostlyCRDTDocument {
return snapshots;
}

applySeedDocument(title, document) {
this.title = title;
const seedDoc = new Y.Doc();
seedDoc.clientID = seedClientID;
const seedFragment = seedDoc.getXmlFragment(fragmentName);
const nextDoc = schema.nodeFromJSON(normalizedDocument(document));
const transactionTarget = {
transact: (operation) => seedDoc.transact(operation, this.remoteOrigin)
};
updateYFragment(transactionTarget, seedFragment, nextDoc, mappingStateFor(seedFragment));
Y.applyUpdate(this.ydoc, Y.encodeStateAsUpdate(seedDoc), this.remoteOrigin);
}

applyDocument(title, document, origin) {
this.title = title;
const nextDoc = schema.nodeFromJSON(normalizedDocument(document));
Expand Down
71 changes: 52 additions & 19 deletions Tools/CRDTRuntime/test/docmostly-crdt-runtime.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import assert from "node:assert/strict";
import test from "node:test";
import * as Y from "yjs";
import { yDocToProsemirrorJSON } from "y-prosemirror";
import {
initProseMirrorDoc,
updateYFragment,
yDocToProsemirrorJSON
} from "y-prosemirror";
import { Schema } from "@tiptap/pm/model";
import "../src/docmostly-crdt-runtime.js";

const fragmentName = "default";
const schema = new Schema({
nodes: {
doc: { content: "block+" },
text: { group: "inline" },
paragraph: {
group: "block",
content: "inline*",
attrs: { textAlign: { default: null } }
}
},
marks: {}
});

test("seeds the initial document into Yjs state without broadcasting it as a local update", () => {
test("starts with empty Yjs state without broadcasting fetched page content", () => {
const document = globalThis.docmostlyCRDT.createDocument({
pageID: "page-1",
title: "Page",
Expand All @@ -15,15 +32,13 @@ test("seeds the initial document into Yjs state without broadcasting it as a loc

assert.deepEqual(document.drainLocalUpdates(), []);

const emptyStateVector = base64FromBytes(Y.encodeStateVector(new Y.Doc()));
const update = document.encodeStateAsUpdate(emptyStateVector);
const ydoc = new Y.Doc();
Y.applyUpdate(ydoc, bytesFromBase64(update));

assert.deepEqual(yDocToProsemirrorJSON(ydoc, fragmentName), paragraphDocument("Seed"));
assert.equal(
document.encodeStateVector(),
base64FromBytes(Y.encodeStateVector(new Y.Doc()))
);
});

test("seeds independent documents with shared Yjs state so later updates replace the seed", () => {
test("applies remote document update to empty native state", () => {
const firstDocument = globalThis.docmostlyCRDT.createDocument({
pageID: "page-1",
title: "Page",
Expand Down Expand Up @@ -52,22 +67,21 @@ test("seeds independent documents with shared Yjs state so later updates replace
}]);
});

test("does not duplicate seed content when another seeded document syncs initial state", () => {
const firstDocument = globalThis.docmostlyCRDT.createDocument({
pageID: "page-1",
title: "Page",
document: paragraphDocument("Seed")
});
const secondDocument = globalThis.docmostlyCRDT.createDocument({
test("does not duplicate content when syncing with server-converted ydoc", () => {
const nativeDocument = globalThis.docmostlyCRDT.createDocument({
pageID: "page-1",
title: "Page",
document: paragraphDocument("Seed")
});
const serverDocument = serverYDocFromJSON(paragraphDocument("Seed"));

const emptyStateVector = base64FromBytes(Y.encodeStateVector(new Y.Doc()));
secondDocument.applyRemoteUpdate(firstDocument.encodeStateAsUpdate(emptyStateVector));
const serverUpdate = base64FromBytes(Y.encodeStateAsUpdate(
serverDocument,
bytesFromBase64(nativeDocument.encodeStateVector())
));
nativeDocument.applyRemoteUpdate(serverUpdate);

assert.deepEqual(secondDocument.drainDocumentSnapshots(), [{
assert.deepEqual(nativeDocument.drainDocumentSnapshots(), [{
title: "Page",
document: paragraphDocument("Seed"),
updatedAt: null
Expand All @@ -84,6 +98,25 @@ function paragraphDocument(text) {
};
}

function serverYDocFromJSON(document) {
const ydoc = new Y.Doc();
const fragment = ydoc.getXmlFragment(fragmentName);
const nextDoc = schema.nodeFromJSON(document);
const transactionTarget = {
transact: (operation) => ydoc.transact(operation)
};
updateYFragment(transactionTarget, fragment, nextDoc, mappingStateFor(fragment));
return ydoc;
}

function mappingStateFor(fragment) {
const state = initProseMirrorDoc(fragment, schema);
return {
mapping: state.mapping,
isOMark: state.meta.isOMark
};
}

function bytesFromBase64(base64) {
if (!base64) return new Uint8Array();
return Uint8Array.from(Buffer.from(base64, "base64"));
Expand Down
Loading
Loading