Skip to content

Commit

Permalink
Merge pull request #1690 from concord-consortium/188616810-export-gam…
Browse files Browse the repository at this point in the history
…eview

Export game view
  • Loading branch information
kswenson authored Dec 20, 2024
2 parents c135520 + ad52a2d commit 35b7376
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 39 deletions.
8 changes: 4 additions & 4 deletions v3/src/components/web-view/component-handler-web-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,19 @@ describe("DataInteractive ComponentHandler WebView and Game", () => {
expect(tileLayout?.width).toBe(newValue)

// Create game with url
const multidataUrl = "https://codap.concord.org/multidata-plugin/"
const result3 = handler.create!({}, { type: "game", URL: multidataUrl })
const multiDataUrl = "https://codap.concord.org/multidata-plugin/"
const result3 = handler.create!({}, { type: "game", URL: multiDataUrl })
expect(result3.success).toBe(true)
expect(documentContent.tileMap.size).toBe(2)
const result3Values = result3.values as DIComponentInfo
const tile3 = documentContent.tileMap.get(toV3Id(kWebViewIdPrefix, result3Values.id!))!
expect(tile3).toBeDefined()
expect(isWebViewModel(tile3.content)).toBe(true)
const gameModel = tile3.content as IWebViewModel
expect(gameModel.url).toBe(multidataUrl)
expect(gameModel.url).toBe(multiDataUrl)

// Get game
gameModel.setIsPlugin(true) // This would normally be set automatically when the plugin connects to codap
gameModel.setSubType("plugin") // This would normally be set automatically when the plugin connects to codap
testGetComponent(tile3, handler, (gameTile, values) => {
const { URL } = values as V2Game
expect(URL).toBe((gameTile.content as IWebViewModel).url)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function useDataInteractiveController(iframeRef: React.RefObject<HTMLIFra
const originUrl = extractOrigin(url) ?? ""
const phone = new iframePhone.ParentEndpoint(iframeRef.current, originUrl,
() => {
webViewModel?.setIsPlugin(true)
webViewModel?.setSubType("plugin")
debugLog(DEBUG_PLUGINS, "connection with iframe established")
})
const handler: iframePhone.IframePhoneRpcEndpointHandlerFn =
Expand Down
3 changes: 3 additions & 0 deletions v3/src/components/web-view/web-view-defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ export const kV2GuideViewType = "guideView"
export const kV2WebViewType = "webView"
export const kWebViewTileClass = "codap-web-view"
export const kWebViewBodyClass = "codap-web-view-body"

export const webViewSubTypes = ["guide", "plugin"] as const
export type WebViewSubType = typeof webViewSubTypes[number]
16 changes: 12 additions & 4 deletions v3/src/components/web-view/web-view-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import iframePhone from "iframe-phone"
import { Instance, SnapshotIn, types } from "mobx-state-tree"
import { DIMessage } from "../../data-interactive/iframe-phone-types"
import { ITileContentModel, TileContentModel } from "../../models/tiles/tile-content"
import { kWebViewTileType } from "./web-view-defs"
import { withoutUndo } from "../../models/history/without-undo"
import { kWebViewTileType, WebViewSubType, webViewSubTypes } from "./web-view-defs"

export const kDefaultAllowEmptyAttributeDeletion = true
export const kDefaultBlockAPIRequestsWhileEditing = false
Expand All @@ -17,6 +18,7 @@ export const WebViewModel = TileContentModel
.named("WebViewModel")
.props({
type: types.optional(types.literal(kWebViewTileType), kWebViewTileType),
subType: types.maybe(types.enumeration(webViewSubTypes)),
url: "",
state: types.frozen<unknown>(),
// fields controlled by plugins (like Collaborative) via interactiveFrame requests
Expand All @@ -30,20 +32,26 @@ export const WebViewModel = TileContentModel
})
.volatile(self => ({
dataInteractiveController: undefined as iframePhone.IframePhoneRpcEndpoint | undefined,
isPlugin: false,
version: kDefaultWebViewVersion
}))
.views(self => ({
get allowBringToFront() {
return !self.preventBringToFront
},
get isGuide() {
return self.subType === "guide"
},
get isPlugin() {
return self.subType === "plugin"
}
}))
.actions(self => ({
setDataInteractiveController(controller?: iframePhone.IframePhoneRpcEndpoint) {
self.dataInteractiveController = controller
},
setIsPlugin(isPlugin: boolean) {
self.isPlugin = isPlugin
setSubType(subType?: WebViewSubType) {
withoutUndo()
self.subType = subType
},
setSavedState(state: unknown) {
self.state = state
Expand Down
61 changes: 58 additions & 3 deletions v3/src/components/web-view/web-view-registration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { getTileComponentInfo } from "../../models/tiles/tile-component-info"
import { getTileContentInfo } from "../../models/tiles/tile-content-info"
import { getSharedModelManager } from "../../models/tiles/tile-environment"
import { ITileModelSnapshotIn } from "../../models/tiles/tile-model"
import { safeJsonParse } from "../../utilities/js-utils"
import { hasOwnProperty, safeJsonParse } from "../../utilities/js-utils"
import { CodapV2Document } from "../../v2/codap-v2-document"
import { exportV2Component } from "../../v2/codap-v2-tile-exporters"
import { importV2Component } from "../../v2/codap-v2-tile-importers"
import { ICodapV2DocumentJson, ICodapV2WebViewStorage } from "../../v2/codap-v2-types"
import { ICodapV2DocumentJson, ICodapV2GameViewStorage, ICodapV2WebViewStorage } from "../../v2/codap-v2-types"
import { kWebViewTileType } from "./web-view-defs"
import { isWebViewModel } from "./web-view-model"
import "./web-view-registration"
Expand All @@ -22,7 +22,7 @@ describe("WebView registration", () => {
expect(contentInfo).toBeDefined()
expect(getTileComponentInfo(kWebViewTileType)).toBeDefined()
const defaultContent = contentInfo?.defaultContent()
expect(defaultContent).toBeDefined();
expect(defaultContent).toBeDefined()
})

it("imports/exports v2 web view components", () => {
Expand Down Expand Up @@ -50,13 +50,68 @@ describe("WebView registration", () => {
expect(tile).toBeDefined()
expect(mockInsertTile).toHaveBeenCalledTimes(1)
const content = isWebViewModel(tile?.content) ? tile?.content : undefined
expect(tile.name).toBe("https://codap-resources.concord.org/images/walkingrates-50-percent.png")
expect(tile._title).toBe("Walking Rates")
expect(getTileContentInfo(kWebViewTileType)!.getTitle(tile)).toBe("Walking Rates")
expect(content?.url).toBe("https://codap-resources.concord.org/images/walkingrates-50-percent.png")
expect(content?.isPlugin).toBe(false)

const row = docContent.getRowByIndex(0) as IFreeTileRow
const componentExport = exportV2Component({ tile, row, sharedModelManager })
expect(componentExport?.type).toBe("DG.WebView")
const contentStorage = componentExport?.componentStorage as ICodapV2WebViewStorage
expect(contentStorage.URL).toBe("https://codap-resources.concord.org/images/walkingrates-50-percent.png")
expect(contentStorage.name).toBe("https://codap-resources.concord.org/images/walkingrates-50-percent.png")
expect(contentStorage.title).toBe("Walking Rates")
expect(contentStorage.userSetTitle).toBe(true)
})

it("imports/exports v2 game view components", () => {
const file = path.join(__dirname, "../../test/v2", "game-view-microdata.codap")
const json = fs.readFileSync(file, "utf8")
const doc = safeJsonParse<ICodapV2DocumentJson>(json)!
const v2Document = new CodapV2Document(doc)

const codapDoc = createCodapDocument()
const docContent = codapDoc.content!
docContent.setRowCreator(() => FreeTileRow.create())
const sharedModelManager = getSharedModelManager(docContent)
const mockInsertTile = jest.fn((tileSnap: ITileModelSnapshotIn) => {
return docContent?.insertTileSnapshotInDefaultRow(tileSnap)
})

const tile = importV2Component({
v2Component: v2Document.components[0],
v2Document,
sharedModelManager,
insertTile: mockInsertTile
})!
expect(tile).toBeDefined()
expect(mockInsertTile).toHaveBeenCalledTimes(1)
const content = isWebViewModel(tile?.content) ? tile?.content : undefined
// Note: a component with a userSetTitle false (like in this case) does not import the
// title into _title. However the name is imported.
// When the _title is undefined the name is used as the title
expect(tile._title).toBeUndefined()
expect(tile.name).toBe("Microdata Portal")
expect(getTileContentInfo(kWebViewTileType)!.getTitle(tile)).toBe("Microdata Portal")
expect(content?.url).toBe("https://codap-resources.concord.org/plugins/sdlc/plugin/index.html")
expect(content?.isPlugin).toBe(true)

const row = docContent.getRowByIndex(0) as IFreeTileRow
const componentExport = exportV2Component({ tile, row, sharedModelManager })
expect(componentExport?.type).toBe("DG.GameView")
const contentStorage = componentExport?.componentStorage as ICodapV2GameViewStorage
// shouldn't write out `name` property for GameView components
expect(hasOwnProperty(contentStorage, "name")).toBe(false)
expect(contentStorage.currentGameName).toBe("Microdata Portal")
// Note: the value of the exported title can probably be anything here, but undefined seems
// to be a safe value to make it clear to CODAPv2 that user hasn't set the title
expect(contentStorage.title).toBeUndefined()
expect(contentStorage.userSetTitle).toBe(false)

// Note: we do not convert the URL back to the relative one that is used by CODAPv2
// this seems OK to do.
expect(contentStorage.currentGameUrl).toBe("https://codap-resources.concord.org/plugins/sdlc/plugin/index.html")
})
})
71 changes: 55 additions & 16 deletions v3/src/components/web-view/web-view-registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { ITileModelSnapshotIn } from "../../models/tiles/tile-model"
import { toV3Id } from "../../utilities/codap-utils"
import { t } from "../../utilities/translation/translate"
import { registerV2TileImporter, V2TileImportArgs } from "../../v2/codap-v2-tile-importers"
import { registerV2TileExporter, V2ExportedComponent } from "../../v2/codap-v2-tile-exporters"
import { registerV2TileExporter, V2ExportedComponent, V2TileExportFn } from "../../v2/codap-v2-tile-exporters"
import {
isV2WebViewComponent, isV2GameViewComponent, ICodapV2WebViewComponent
isV2WebViewComponent, isV2GameViewComponent, ICodapV2WebViewComponent, ICodapV2GameViewComponent
} from "../../v2/codap-v2-types"
import { kV2GameType, kV2WebViewType, kWebViewTileType } from "./web-view-defs"
import { kV2GameType, kV2WebViewType, kWebViewTileType, WebViewSubType } from "./web-view-defs"
import { isWebViewModel, IWebViewSnapshot, WebViewModel } from "./web-view-model"
import { WebViewComponent } from "./web-view"
import { WebViewInspector } from "./web-view-inspector"
Expand Down Expand Up @@ -43,32 +43,65 @@ registerTileComponentInfo({
defaultHeight: kDefaultWebViewHeight
})

registerV2TileExporter(kWebViewTileType, ({ tile }) => {
const exportFn: V2TileExportFn = ({ tile }) => {
// This really should be a WebView Model. We shouldn't be called unless
// the tile type is kWebViewTileType which is what isWebViewModel is using.
const webViewContent = isWebViewModel(tile.content) ? tile.content : undefined
const url = webViewContent?.url ?? ""
const v2WebView: V2ExportedComponent<ICodapV2WebViewComponent> = {
type: "DG.WebView",
componentStorage: {
URL: url
if (webViewContent?.isPlugin) {
const v2GameView: V2ExportedComponent<ICodapV2GameViewComponent> = {
type: "DG.GameView",
componentStorage: {
currentGameUrl: url,
currentGameName: tile.name,
savedGameState: webViewContent?.state ?? undefined
// TODO_V2_EXPORT add the rest of the game properties
}
}
return v2GameView
} else {
const v2WebView: V2ExportedComponent<ICodapV2WebViewComponent> = {
type: "DG.WebView",
componentStorage: {
URL: url
}
}
return v2WebView
}
return v2WebView
})
}
exportFn.options = ({ tile }) => {
const webViewContent = isWebViewModel(tile.content) ? tile.content : undefined
// v2 doesn't write out a `name` property for game view components
return { suppressName: webViewContent?.isPlugin }
}
registerV2TileExporter(kWebViewTileType, exportFn)

function addWebViewSnapshot(args: V2TileImportArgs, guid: number, url?: string, state?: unknown) {
function addWebViewSnapshot(args: V2TileImportArgs, name?: string, url?: string, state?: unknown) {
const { v2Component, insertTile } = args
const { name, title, userSetTitle } = v2Component.componentStorage || {}
const { guid } = v2Component
const { title, userSetTitle } = v2Component.componentStorage || {}
const subTypeMap: Record<string, WebViewSubType> = {
"DG.GameView": "plugin",
"DG.GuideView": "guide"
}

const content: IWebViewSnapshot = {
type: kWebViewTileType,
subType: subTypeMap[v2Component.type],
state,
url
}
const webViewTileSnap: ITileModelSnapshotIn = {
id: toV3Id(kWebViewIdPrefix, guid),
name,
// Note: when a game view is imported the userSetTitle is often
// false, and often the title property is set to title which the plugin provided
// to CODAPv2. This value is also set in componentStorage.currentGameName. This
// currentGameName value is imported as the name field above.
// If round tripping a v2 document through v3 the title will be lost because
// of this. The name will be preserved though. Even when the name was not preserved
// CODAPv2 seemed to handle this correctly and updated the the title when the document
// is loaded.
_title: (userSetTitle && title) || undefined,
content
}
Expand All @@ -81,10 +114,12 @@ function importWebView(args: V2TileImportArgs) {
if (!isV2WebViewComponent(v2Component)) return

// parse the v2 content
const { guid, componentStorage: { URL } } = v2Component
const { componentStorage: { name, URL } } = v2Component

// create webView model
return addWebViewSnapshot(args, guid, URL)
// Note: a renamed WebView has the componentStorage.name set to the URL,
// only the componentStorage.title is updated
return addWebViewSnapshot(args, name, URL)
}
registerV2TileImporter("DG.WebView", importWebView)

Expand All @@ -93,13 +128,17 @@ function importGameView(args: V2TileImportArgs) {
if (!isV2GameViewComponent(v2Component)) return

// parse the v2 content
const { guid, componentStorage: { currentGameUrl, savedGameState} } = v2Component
const { componentStorage: { currentGameUrl, currentGameName, savedGameState} } = v2Component

// create webView model
return addWebViewSnapshot(args, guid, processPluginUrl(currentGameUrl), savedGameState)
// Note: a renamed GameView has the componentStorage.currentGameName set to the value
// provided by the plugin, only the componentStorage.title is updated
return addWebViewSnapshot(args, currentGameName, processPluginUrl(currentGameUrl), savedGameState)
}
registerV2TileImporter("DG.GameView", importGameView)

// TODO add importer for DG.GuideView

const webViewComponentHandler: DIComponentHandler = {
create({ values }) {
const { URL } = values as V2WebView
Expand Down
7 changes: 4 additions & 3 deletions v3/src/data-interactive/data-interactive-type-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,11 @@ export function convertDataSetToV2(dataSet: IDataSet, exportCases = false): ICod
description,
// metadata,
// preventReorg,
// TODO_V2_EXPORT
// TODO_V2_EXPORT setAsideItems
setAsideItems: [],
// TODO_V2_IMPORT, TODO_V2_EXPORT
contextStorage: { _links_: { selectedCases: [] } }
// TODO_V2_EXPORT contextStorage
// providing an empty object makes it possible for CODAPv2 to load more exported documents
contextStorage: {}
}
}

Expand Down
61 changes: 61 additions & 0 deletions v3/src/test/v2/game-view-microdata.codap
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "Untitled Document",
"guid": 1,
"id": 1,
"components": [
{
"type": "DG.GameView",
"guid": 2,
"id": 2,
"componentStorage": {
"currentGameName": "Microdata Portal",
"currentGameUrl": "../../../../extn/plugins/sdlc/plugin/index.html",
"allowInitGameOverride": true,
"savedGameState": {
"sampleNumber": 1,
"sampleSize": 16,
"selectedYears": [
2020
],
"selectedStates": [],
"selectedAttributes": [
"Sex",
"Age",
"Year",
"State",
"Boundaries"
],
"keepExistingData": false,
"activityLog": [
{
"time": "12/17/2024, 9:08:16 AM",
"message": "Connection: /4g/false/50/10/"
}
]
},
"title": "Microdata Portal",
"userSetTitle": false,
"cannotClose": false
},
"layout": {
"width": 380,
"height": 520,
"left": 5,
"top": 5,
"zIndex": 101,
"isVisible": true,
"right": 385,
"bottom": 525
},
"savedHeight": null
}
],
"contexts": [],
"globalValues": [],
"appName": "DG",
"appVersion": "2.0",
"appBuildNum": "0730",
"lang": "en",
"idCount": 2,
"metadata": {}
}
Loading

0 comments on commit 35b7376

Please sign in to comment.