Skip to content

Commit

Permalink
feat: v2 export infrastructure
Browse files Browse the repository at this point in the history
  • Loading branch information
kswenson committed Dec 3, 2024
1 parent 8319410 commit c157ea4
Show file tree
Hide file tree
Showing 15 changed files with 225 additions and 26 deletions.
3 changes: 2 additions & 1 deletion v3/src/components/calculator/calculator-defs.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const kCalculatorTileType = "Calculator"
export const kV2CalculatorType = "calculator"
export const kV2CalculatorDGType = "DG.Calculator" // component type in documents
export const kV2CalculatorDIType = "calculator" // component type in plugin API
export const kCalculatorTileClass = "calculator"
18 changes: 17 additions & 1 deletion v3/src/components/calculator/calculator-registration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { getTileComponentInfo } from "../../models/tiles/tile-component-info"
import { getTileContentInfo } from "../../models/tiles/tile-content-info"
import { ITileModelSnapshotIn } from "../../models/tiles/tile-model"
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 } from "../../v2/codap-v2-types"
import { kCalculatorTileType } from "./calculator-defs"
import { kCalculatorTileType, kV2CalculatorDGType } from "./calculator-defs"
import "./calculator-registration"

const fs = require("fs")
Expand All @@ -20,6 +21,7 @@ describe("Calculator registration", () => {
const calculator = calculatorContentInfo?.defaultContent()
expect(calculator).toBeDefined()
})

it("imports v2 calculator components", () => {
const file = path.join(__dirname, "../../test/v2", "calculator.codap")
const calculatorJson = fs.readFileSync(file, "utf8")
Expand Down Expand Up @@ -47,4 +49,18 @@ describe("Calculator registration", () => {
})
expect(tileWithInvalidComponent).toBeUndefined()
})

it("exports v2 calculator components", () => {
const calculatorContentInfo = getTileContentInfo(kCalculatorTileType)!
const docContent = DocumentContentModel.create()
const freeTileRow = FreeTileRow.create()
docContent.setRowCreator(() => freeTileRow)
const tile = docContent.insertTileSnapshotInDefaultRow({
name: calculatorContentInfo?.defaultName?.(),
content: calculatorContentInfo?.defaultContent()
})!
const output = exportV2Component({ tile, row: freeTileRow })
expect(output?.type).toBe(kV2CalculatorDGType)
expect(output?.componentStorage.name).toBe(calculatorContentInfo?.defaultName?.())
})
})
22 changes: 14 additions & 8 deletions v3/src/components/calculator/calculator-registration.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import CalcIcon from "../../assets/icons/icon-calc.svg"
import { registerComponentHandler } from "../../data-interactive/handlers/component-handler"
import { registerTileComponentInfo } from "../../models/tiles/tile-component-info"
import { ITileLikeModel, registerTileContentInfo } from "../../models/tiles/tile-content-info"
import { ITileModelSnapshotIn } from "../../models/tiles/tile-model"
import { CalculatorComponent } from "./calculator"
import { kCalculatorTileClass, kCalculatorTileType, kV2CalculatorType } from "./calculator-defs"
import { CalculatorModel, ICalculatorSnapshot } from "./calculator-model"
import { CalculatorTitleBar } from "./calculator-title-bar"
import CalcIcon from '../../assets/icons/icon-calc.svg'
import { toV3Id } from "../../utilities/codap-utils"
import { t } from "../../utilities/translation/translate"
import { registerV2TileExporter } from "../../v2/codap-v2-tile-exporters"
import { registerV2TileImporter } from "../../v2/codap-v2-tile-importers"
import { isV2CalculatorComponent } from "../../v2/codap-v2-types"
import { t } from "../../utilities/translation/translate"
import { CalculatorComponent } from "./calculator"
import { kCalculatorTileClass, kCalculatorTileType, kV2CalculatorDGType, kV2CalculatorDIType } from "./calculator-defs"
import { CalculatorModel, ICalculatorSnapshot } from "./calculator-model"
import { CalculatorTitleBar } from "./calculator-title-bar"

export const kCalculatorIdPrefix = "CALC"

Expand Down Expand Up @@ -46,7 +47,7 @@ registerTileComponentInfo({
defaultWidth: 137
})

registerV2TileImporter("DG.Calculator", ({ v2Component, insertTile }) => {
registerV2TileImporter(kV2CalculatorDGType, ({ v2Component, insertTile }) => {
if (!isV2CalculatorComponent(v2Component)) return

const { guid, componentStorage: { name = "", title = "" } } = v2Component
Expand All @@ -63,7 +64,12 @@ registerV2TileImporter("DG.Calculator", ({ v2Component, insertTile }) => {
return calculatorTile
})

registerComponentHandler(kV2CalculatorType, {
registerV2TileExporter(kCalculatorTileType, () => {
// Calculator doesn't have calculator-specific storage
return { type: kV2CalculatorDGType }
})

registerComponentHandler(kV2CalculatorDIType, {
create() {
return { content: { type: kCalculatorTileType } }
},
Expand Down
4 changes: 2 additions & 2 deletions v3/src/data-interactive/data-interactive-component-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SlateExchangeValue } from "@concord-consortium/slate-editor"
import { kCalculatorTileType, kV2CalculatorType } from "../components/calculator/calculator-defs"
import { kCalculatorTileType, kV2CalculatorDIType } from "../components/calculator/calculator-defs"
import { kCaseCardTileType, kV2CaseCardType } from "../components/case-card/case-card-defs"
import { kCaseTableTileType, kV2CaseTableType } from "../components/case-table/case-table-defs"
import { kGraphTileType, kV2GraphType } from "../components/graph/graph-defs"
Expand All @@ -12,7 +12,7 @@ import { kV2GameType, kV2WebViewType, kWebViewTileType } from "../components/web
// export const kV2TextType = "text"

export const kComponentTypeV3ToV2Map: Record<string, string> = {
[kCalculatorTileType]: kV2CalculatorType,
[kCalculatorTileType]: kV2CalculatorDIType,
[kCaseTableTileType]: kV2CaseTableType,
[kCaseCardTileType]: kV2CaseCardType,
[kGraphTileType]: kV2GraphType,
Expand Down
5 changes: 5 additions & 0 deletions v3/src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { DEBUG_SAVE_AS_V2 } from "./debug"

// For now, this is determined by DEBUG flag, but it may be configured by url parameter
// or some other means eventually.
export const CONFIG_SAVE_AS_V2 = DEBUG_SAVE_AS_V2
1 change: 1 addition & 0 deletions v3/src/lib/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const DEBUG_HISTORY = debugContains("history")
export const DEBUG_LOGGER = debugContains("logger")
export const DEBUG_MAP = debugContains("map")
export const DEBUG_PLUGINS = debugContains("plugins")
export const DEBUG_SAVE_AS_V2 = debugContains("saveAsV2")
export const DEBUG_UNDO = debugContains("undo")

export function debugLog(debugFlag: boolean, ...args: any[]) {
Expand Down
7 changes: 4 additions & 3 deletions v3/src/lib/use-cloud-file-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { gLocale } from "../utilities/translation/locale"
import { t } from "../utilities/translation/translate"
import { removeDevUrlParams, urlParams } from "../utilities/url-params"
import { clientConnect, createCloudFileManager, renderRoot } from "./cfm-utils"
import { DEBUG_CFM_LOCAL_STORAGE, DEBUG_CFM_NO_AUTO_SAVE } from "./debug"
import { CONFIG_SAVE_AS_V2 } from "./config"
import { DEBUG_CFM_LOCAL_STORAGE, DEBUG_CFM_NO_AUTO_SAVE, DEBUG_SAVE_AS_V2 } from "./debug"
import { handleCFMEvent } from "./handle-cfm-event"

const locales = [
Expand Down Expand Up @@ -147,7 +148,7 @@ export function useCloudFileManager(optionsArg: CFMAppOptions) {

useEffect(function initCfm() {

const autoSaveInterval = DEBUG_CFM_NO_AUTO_SAVE ? undefined : 5
const autoSaveInterval = DEBUG_CFM_NO_AUTO_SAVE || DEBUG_SAVE_AS_V2 ? undefined : 5
const _options: CFMAppOptions = {
autoSaveInterval,
// When running in the Activity Player, hide the hamburger menu
Expand Down Expand Up @@ -189,7 +190,7 @@ export function useCloudFileManager(optionsArg: CFMAppOptions) {
},
mimeType: 'application/json',
readableMimeTypes: ['application/x-codap-document'],
extension: "codap3",
extension: CONFIG_SAVE_AS_V2 ? "codap" : "codap3",
readableExtensions: ["json", "", "codap", "codap3"],
enableLaraSharing: true,
log(event, eventData) {
Expand Down
3 changes: 2 additions & 1 deletion v3/src/models/app-state.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { reaction } from "mobx"
import { getSnapshot } from "mobx-state-tree"
import { appState } from "./app-state"
import { isCodapDocument } from "./codap/create-codap-document"
import { DocumentModel } from "./document/document"
import { serializeDocument } from "./document/serialize-document"

Expand Down Expand Up @@ -33,7 +34,7 @@ describe("AppState", () => {

it("returns de-serializable document snapshots", async () => {
const snap = await appState.getDocumentSnapshot()
const docModel = DocumentModel.create(snap)
const docModel = DocumentModel.create(isCodapDocument(snap) ? snap : { type: "CODAP" })
const docSnap = await serializeDocument(docModel, doc => getSnapshot(doc))
expect(docSnap).toEqual(snap)
})
Expand Down
22 changes: 15 additions & 7 deletions v3/src/models/app-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,21 @@ import { ISharedDataSet, kSharedDataSetType, SharedDataSet } from "./shared/shar
import { getSharedModelManager } from "./tiles/tile-environment"
import { Logger } from "../lib/logger"
import { t } from "../utilities/translation/translate"
import { CONFIG_SAVE_AS_V2 } from "../lib/config"
import { DEBUG_DOCUMENT } from "../lib/debug"
import { TreeManagerType } from "./history/tree-manager"
import { ICodapV2DocumentJson, isCodapV2Document } from "../v2/codap-v2-types"
import { CodapV2Document } from "../v2/codap-v2-document"
import { exportV2Document } from "../v2/export-v2-document"
import { importV2Document } from "../v2/import-v2-document"

const kAppName = "CODAP"

type AppMode = "normal" | "performance"

type ISerializedDocumentModel = IDocumentModelSnapshot & {revisionId?: string}
type ISerializedV3Document = IDocumentModelSnapshot & {revisionId?: string}
type ISerializedV2Document = ICodapV2DocumentJson & {revisionId?: string}
type ISerializedDocument = ISerializedV3Document | ISerializedV2Document

class AppState {
@observable
Expand Down Expand Up @@ -77,12 +81,16 @@ class AppState {
return revisionId === this.treeManager?.revisionId
}

async getDocumentSnapshot() {
// use cloneDeep because MST snapshots are immutable
const snapshot = await serializeDocument(this.currentDocument, doc => cloneDeep(getSnapshot(doc)))
async getDocumentSnapshot(): Promise<ISerializedDocument> {
const serializeFn = CONFIG_SAVE_AS_V2
// export as v2 if configured to do so
? (doc: IDocumentModel) => exportV2Document(doc) as ISerializedDocument

Check warning on line 87 in v3/src/models/app-state.ts

View check run for this annotation

Codecov / codecov/patch

v3/src/models/app-state.ts#L87

Added line #L87 was not covered by tests
// use cloneDeep because MST snapshots are immutable
: (doc: IDocumentModel) => cloneDeep(getSnapshot(doc)) as ISerializedDocument
const snapshot = await serializeDocument(this.currentDocument, serializeFn)
const revisionId = this.treeManager?.revisionId
if (revisionId) {
return { revisionId, ...snapshot }
snapshot.revisionId = revisionId

Check warning on line 93 in v3/src/models/app-state.ts

View check run for this annotation

Codecov / codecov/patch

v3/src/models/app-state.ts#L93

Added line #L93 was not covered by tests
}

return snapshot
Expand All @@ -94,13 +102,13 @@ class AppState {

@flow
*setDocument(
snap: ISerializedDocumentModel | ICodapV2DocumentJson,
snap: ISerializedV3Document | ICodapV2DocumentJson,
metadata?: Record<string, any>
) {
// stop monitoring changes for undo/redo on the existing document
this.disableDocumentMonitoring()

let content: ISerializedDocumentModel
let content: ISerializedV3Document
if (isCodapV2Document(snap)) {
const v2Document = new CodapV2Document(snap, metadata)
const v3Document = importV2Document(v2Document)
Expand Down
2 changes: 1 addition & 1 deletion v3/src/models/codap/create-codap-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const { buildNumber } = build

type ICodapDocumentModelSnapshot = SetOptional<IDocumentModelSnapshot, "type">

export function isCodapDocument(doc: unknown) {
export function isCodapDocument(doc: unknown): doc is ICodapDocumentModelSnapshot {
if (!doc || typeof doc !== "object") return false
if (!("content" in doc) || !doc.content || typeof doc.content !== "object") return false
return "rowMap" in doc.content && !!doc.content.rowMap &&
Expand Down
3 changes: 2 additions & 1 deletion v3/src/v2/codap-v2-import.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { kV2CalculatorDGType } from "../components/calculator/calculator-defs"
import { CodapV2Document } from "./codap-v2-document"
import { ICodapV2DocumentJson, isCodapV2Document } from "./codap-v2-types"

Expand Down Expand Up @@ -40,7 +41,7 @@ describe(`V2 "calculator.codap"`, () => {
expect(calculator.globalValues.length).toBe(0)
expect(calculator.dataSets.length).toBe(0)

expect(calculator.components.map(c => c.type)).toEqual(["DG.Calculator"])
expect(calculator.components.map(c => c.type)).toEqual([kV2CalculatorDGType])
})
})

Expand Down
65 changes: 65 additions & 0 deletions v3/src/v2/codap-v2-tile-exporters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { SetOptional } from "type-fest"
import { kDefaultTileHeight, kDefaultTileWidth, kTitleBarHeight } from "../components/constants"
import { IFreeTileRow, isFreeTileLayout } from "../models/document/free-tile-row"
import { ISharedModelManager } from "../models/shared/shared-model-manager"
import { ITileModel } from "../models/tiles/tile-model"
import { CodapV2Component, CodapV2ComponentStorage } from "./codap-v2-types"
import { toV2Id } from "../utilities/codap-utils"

export interface V2ExporterOutput {
type: CodapV2Component["type"]
storage?: SetOptional<CodapV2ComponentStorage, "cannotClose" | "userSetTitle">
}

export interface V2TileExportArgs {
tile: ITileModel
row?: IFreeTileRow
sharedModelManager?: ISharedModelManager
}
export type V2TileExportFn = (args: V2TileExportArgs) => Maybe<V2ExporterOutput>

// map from v2 component type to import function
const gV2TileExporters = new Map<string, V2TileExportFn>()

// register a v2 exporter for the specified tile type
export function registerV2TileExporter(tileType: string, exportFn: V2TileExportFn) {
gV2TileExporters.set(tileType, exportFn)
}

// export the specified v2 component using the appropriate registered exporter
export function exportV2Component(args: V2TileExportArgs): Maybe<CodapV2Component> {
const output = gV2TileExporters.get(args.tile.content.type)?.(args)
if (!output) return

const layout = args.row?.getTileLayout(args.tile.id)
if (!isFreeTileLayout(layout)) return

const id = toV2Id(args.tile.id)

const tileWidth = layout.width ?? kDefaultTileWidth
const tileHeight = layout.height ?? kDefaultTileHeight

return {
type: output.type,
guid: id,
id,
componentStorage: {
name: args.tile.name,
title: args.tile._title,
cannotClose: args.tile.cannotClose,
// TODO_V2_EXPORT check this logic
userSetTitle: !!args.tile._title && args.tile._title !== args.tile.name,
// include the component-specific storage
...output.storage
},
layout: {
width: tileWidth,
height: layout.isMinimized ? kTitleBarHeight : tileHeight,
left: layout.position.x,
top: layout.position.y,
isVisible: !layout.isHidden,
zIndex: layout.zIndex
},
savedHeight: layout.isMinimized ? tileHeight : null
} as Maybe<CodapV2Component>
}
6 changes: 5 additions & 1 deletion v3/src/v2/codap-v2-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,18 +506,21 @@ export interface ICodapV2SliderComponent extends ICodapV2BaseComponent {
}
export const isV2SliderComponent = (component: ICodapV2BaseComponent): component is ICodapV2SliderComponent =>
component.type === "DG.SliderView"

export interface ICodapV2TableComponent extends ICodapV2BaseComponent {
type: "DG.TableView"
componentStorage: ICodapV2TableStorage
}
export const isV2TableComponent = (component: ICodapV2BaseComponent): component is ICodapV2TableComponent =>
component.type === "DG.TableView"
component.type === "DG.TableView"

export interface ICodapV2WebViewComponent extends ICodapV2BaseComponent {
type: "DG.WebView"
componentStorage: ICodapV2WebViewStorage
}
export const isV2WebViewComponent =
(component: ICodapV2BaseComponent): component is ICodapV2WebViewComponent => component.type === "DG.WebView"

export interface ICodapGameViewComponent extends ICodapV2BaseComponent {
type: "DG.GameView"
componentStorage: ICodapV2GameViewStorage
Expand Down Expand Up @@ -556,6 +559,7 @@ export const isV2TextComponent = (component: ICodapV2BaseComponent): component i
export type CodapV2Component = ICodapV2CalculatorComponent | ICodapGameViewComponent | ICodapV2GraphComponent |
ICodapV2GuideComponent | ICodapV2MapComponent | ICodapV2SliderComponent |
ICodapV2TableComponent | ICodapV2TextComponent | ICodapV2WebViewComponent
export type CodapV2ComponentStorage = CodapV2Component["componentStorage"]

export interface ICodapV2DocumentJson {
type?: string // "DG.Document"
Expand Down
52 changes: 52 additions & 0 deletions v3/src/v2/export-v2-document.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { kCalculatorTileType, kV2CalculatorDGType } from "../components/calculator/calculator-defs"
import { createCodapDocument } from "../models/codap/create-codap-document"
import { exportV2Document } from "./export-v2-document"
import "../components/calculator/calculator-registration"

jest.mock("../../build_number.json", () => ({
buildNumber: 1234
}))

jest.mock("../../package.json", () => ({
version: "3.0.0-test"
}))

describe("exportV2Document", () => {
it("exports an empty document", () => {
const doc = createCodapDocument()
const output = exportV2Document(doc)
expect(output).toEqual({
type: "DG.Document",
id: 1,
guid: 1,
name: doc.title,
appName: "DG",
appVersion: "3.0.0-test",
appBuildNum: `${1234}`,
metadata: {},
components: [],
contexts: [],
globalValues: []
})
})

it("exports a document with a single component", () => {
const doc = createCodapDocument()
doc.content?.insertTileSnapshotInDefaultRow({ content: { type: kCalculatorTileType }})
const { components, ...others } = exportV2Document(doc)
expect(others).toEqual({
type: "DG.Document",
id: 1,
guid: 1,
name: doc.title,
appName: "DG",
appVersion: "3.0.0-test",
appBuildNum: `${1234}`,
metadata: {},
contexts: [],
globalValues: []
})
expect(components.length).toBe(1)
expect(components[0].type).toBe(kV2CalculatorDGType)
})
})
Loading

0 comments on commit c157ea4

Please sign in to comment.