Skip to content

Commit

Permalink
Merge pull request #1645 from concord-consortium/188523684-handle-cfm…
Browse files Browse the repository at this point in the history
…-shared-data

handle CFM sharing data
  • Loading branch information
scytacki authored Nov 23, 2024
2 parents aacea38 + e46431c commit 4fb07b6
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 16 deletions.
1 change: 1 addition & 0 deletions v3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ Inside of your `package.json` file:

Various developer features can be enabled by adding a `debug` local storage key with one or more of the following flags separated by spaces. Local storage is specific to the domain that CODAP is running on. In Chrome local storage can be edited by opening the developer tools and going to the "Application" tab. Then find "Storage/Local storage" and the domain that CODAP is running on.

- `cfmEvents` console log all events received from the CFM
- `cfmLocalStorage` enable the CFM local storage provider so documents can be saved and loaded from the browser's local storage
- `document` this will add the active document as `window.currentDocument`, you can use `currentDocument.toJSON()` to views the current documents content. You can also use `currentDocument.treeManagerAPI.document.toJSON()` to inspect the history of the document.
- `formulas` print info about recalculating formulas
Expand Down
1 change: 1 addition & 0 deletions v3/src/lib/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ if (debug.length > 0) {

const debugContains = (key: string) => debug.indexOf(key) !== -1

export const DEBUG_CFM_EVENTS = debugContains("cfmEvents")
export const DEBUG_CFM_LOCAL_STORAGE = debugContains("cfmLocalStorage")
export const DEBUG_DOCUMENT = debugContains("document")
export const DEBUG_FORMULAS = debugContains("formulas")
Expand Down
86 changes: 83 additions & 3 deletions v3/src/lib/handle-cfm-event.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { CloudFileManagerClient, CloudFileManagerClientEvent } from "@concord-consortium/cloud-file-manager"
import { cloneDeep } from "lodash"
import {
ClientEventCallback, CloudFileManagerClient, CloudFileManagerClientEvent
} from "@concord-consortium/cloud-file-manager"
import { getSnapshot } from "mobx-state-tree"
import { appState } from "../models/app-state"
import { createCodapDocument, isCodapDocument } from "../models/codap/create-codap-document"
Expand Down Expand Up @@ -65,6 +68,51 @@ describe("handleCFMEvent", () => {
expect(isCodapDocument(contentArg)).toBe(true)
})

it("handles the `getContent` message with sharing info", async () => {
const mockCfmClient = {} as CloudFileManagerClient
// This is not real metadata for sharing. Our CFM handler should
// not care and just add it to the returned content whatever it is.
const mockSharingInfo = { sharingInfo: "value" }
const mockCfmEvent = {
type: "getContent",
callback: jest.fn(),
data: {
shared: mockSharingInfo
}
}
const mockCfmEventArg = mockCfmEvent as unknown as CloudFileManagerClientEvent
await handleCFMEvent(mockCfmClient, mockCfmEventArg)

const contentArg = mockCfmEvent.callback.mock.calls[0][0]
expect(isCodapDocument(contentArg)).toBe(true)

expect(contentArg.metadata.shared).toEqual(mockSharingInfo)
})

it("handles the sharedFile message", async () => {
const mockCfmClient = {
dirty: jest.fn() as CloudFileManagerClient["dirty"]
} as CloudFileManagerClient
const mockCfmEvent = {
type: "sharedFile"
}
const mockCfmEventArg = mockCfmEvent as unknown as CloudFileManagerClientEvent
await handleCFMEvent(mockCfmClient, mockCfmEventArg)
expect(mockCfmClient.dirty).toHaveBeenCalledWith(true)
})

it("handles the unsharedFile message", async () => {
const mockCfmClient = {
dirty: jest.fn() as CloudFileManagerClient["dirty"]
} as CloudFileManagerClient
const mockCfmEvent = {
type: "unsharedFile"
}
const mockCfmEventArg = mockCfmEvent as unknown as CloudFileManagerClientEvent
await handleCFMEvent(mockCfmClient, mockCfmEventArg)
expect(mockCfmClient.dirty).toHaveBeenCalledWith(true)
})

it("handles the willOpenFile message", async () => {
const mockCfmClient = {} as CloudFileManagerClient
const mockCfmEvent = {
Expand All @@ -89,11 +137,14 @@ describe("handleCFMEvent", () => {
type: "openedFile",
data: {
content: mockV2Document
}
},
callback: jest.fn() as ClientEventCallback
} as CloudFileManagerClientEvent
const spy = jest.spyOn(ImportV2Document, "importV2Document")
await handleCFMEvent(mockCfmClient, cfmEvent)
expect(ImportV2Document.importV2Document).toHaveBeenCalledTimes(1)
// No error and no shared data
expect(cfmEvent.callback).toHaveBeenCalledWith(null, {})
spy.mockRestore()
})

Expand All @@ -104,12 +155,41 @@ describe("handleCFMEvent", () => {
type: "openedFile",
data: {
content: getSnapshot(v3Document)
}
},
callback: jest.fn() as ClientEventCallback
} as CloudFileManagerClientEvent
const spy = jest.spyOn(appState, "setDocument")
await handleCFMEvent(mockCfmClient, cfmEvent)
expect(spy).toHaveBeenCalledTimes(1)
// No error and no shared data
expect(cfmEvent.callback).toHaveBeenCalledWith(null, {})
spy.mockRestore()
})

it("handles the `openedFile` message with sharing info", async () => {
const mockCfmClient = {} as CloudFileManagerClient
const v3Document = createCodapDocument()
// This is not real metadata for sharing. Our CFM handler should
// not care and just return it whatever it is.
const mockSharingInfo = { sharingInfo: "value" }
const snapshot = getSnapshot(v3Document)
const content = cloneDeep(snapshot) as any
content.metadata = {
shared: mockSharingInfo
}

const cfmEvent = {
type: "openedFile",
data: {
content
},
callback: jest.fn() as ClientEventCallback
} as CloudFileManagerClientEvent
const spy = jest.spyOn(appState, "setDocument")
await handleCFMEvent(mockCfmClient, cfmEvent)
expect(spy).toHaveBeenCalledTimes(1)
// No error and the sharing info is returned
expect(cfmEvent.callback).toHaveBeenCalledWith(null, mockSharingInfo)
spy.mockRestore()
})
})
63 changes: 50 additions & 13 deletions v3/src/lib/handle-cfm-event.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { cloneDeep } from "lodash"
import { CloudFileManagerClient, CloudFileManagerClientEvent } from "@concord-consortium/cloud-file-manager"
import { appState } from "../models/app-state"
import { removeDevUrlParams, urlParams } from "../utilities/url-params"
import { wrapCfmCallback } from "./cfm-utils"
import { DEBUG_CFM_EVENTS } from "./debug"

import build from "../../build_number.json"
import pkg from "../../package.json"

export function handleCFMEvent(cfmClient: CloudFileManagerClient, event: CloudFileManagerClientEvent) {
// const { data, state, ...restEvent } = event
// console.log("cfmEventCallback", JSON.stringify({ ...restEvent }))
export async function handleCFMEvent(cfmClient: CloudFileManagerClient, event: CloudFileManagerClientEvent) {
if (DEBUG_CFM_EVENTS) {
// We clone the event because the CFM reuses the same objects across multiple events
// eslint-disable-next-line no-console
console.log("cfmEvent", event.type, cloneDeep(event))
}

switch (event.type) {
case "connected":
Expand All @@ -33,10 +38,21 @@ export function handleCFMEvent(cfmClient: CloudFileManagerClient, event: CloudFi
// case "closedFile":
// break
case "getContent": {
// return the promise so tests can make sure it is complete
return appState.getDocumentSnapshot().then(content => {
event.callback(content)
})
const content = await appState.getDocumentSnapshot()
// getDocumentSnapshot makes a clone of the snapshot so it is safe to mutate in place.
const cfmContent = content as any

// Add 'metadata.shared' property based on the CFM event shared data
// The CFM assumes this is where the shared metadata is when it tries
// to strip it out in `getDownloadBlob`
const cfmSharedMetadata = event.data?.shared
if (cfmSharedMetadata) {
// In CODAPv2 the CFM metadata is cloned, so we do the same here to be safe
cfmContent.metadata = { shared: cloneDeep(cfmSharedMetadata) }
}
event.callback(cfmContent)

break
}
case "willOpenFile":
removeDevUrlParams()
Expand All @@ -46,8 +62,21 @@ export function handleCFMEvent(cfmClient: CloudFileManagerClient, event: CloudFi
case "openedFile": {
const content = event.data.content
const metadata = event.data.metadata
// return the promise so tests can make sure it is complete
return appState.setDocument(content, metadata)

// Pull the shared metadata out of the content if it exists
// Otherwise use the shared metadata passed from the CFM
const cfmSharedMetadata = content?.metadata?.shared || metadata?.shared || {}

// Clone this metadata because that is what CODAPv2 did so we do the
// same to be safe
const clonedCfmSharedMetadata = cloneDeep(cfmSharedMetadata)

await appState.setDocument(content, metadata)

// acknowledge a successful open and return shared metadata
event.callback(null, clonedCfmSharedMetadata)

break
}
case "savedFile": {
const { content } = event.data
Expand All @@ -66,10 +95,18 @@ export function handleCFMEvent(cfmClient: CloudFileManagerClient, event: CloudFi
}
break
}
// case "sharedFile":
// break
// case "unsharedFile":
// break
case "sharedFile":
case "unsharedFile":
// Make the document dirty to trigger a save with the updated sharing info
// If the file is already shared, and the user updates the shared document, the
// "sharedFile" event will happen again.
// Currently it isn't necessary to update the sharing info in this case, but
// perhaps in the future the sharing info will include properties that change
// each time, such as a timestamp for when the document was shared.
// Due to the design of the CFM event system we need to do this in the next time slice
await new Promise(resolve => setTimeout(resolve, 0))
cfmClient.dirty(true)
break
// case "importedData":
// break
case "renamedFile": {
Expand Down

0 comments on commit 4fb07b6

Please sign in to comment.