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

Export game view #1690

Merged
merged 4 commits into from
Dec 20, 2024
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
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 { 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 @@
.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 @@
})
.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"

Check warning on line 42 in v3/src/components/web-view/web-view-model.ts

View check run for this annotation

Codecov / codecov/patch

v3/src/components/web-view/web-view-model.ts#L41-L42

Added lines #L41 - L42 were not covered by tests
},
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
Loading