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

feat: v2 export infrastructure #1665

Merged
merged 3 commits into from
Dec 3, 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
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
29 changes: 17 additions & 12 deletions v3/src/models/app-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,21 @@
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 @@
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 @@ -93,14 +101,11 @@
}

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

let content: ISerializedDocumentModel
let content: ISerializedV3Document
kswenson marked this conversation as resolved.
Show resolved Hide resolved
if (isCodapV2Document(snap)) {
const v2Document = new CodapV2Document(snap, metadata)
const v3Document = importV2Document(v2Document)
Expand Down Expand Up @@ -136,11 +141,11 @@
}
})
}
if (content.revisionId && this.treeManager) {
if (snap.revisionId && this.treeManager) {
// Restore the revisionId from the stored document
// This will allow us to consistently compare the local document
// to the stored document.
this.treeManager.setRevisionId(content.revisionId)
this.treeManager.setRevisionId(snap.revisionId)

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L148 was not covered by tests
}

// monitor document changes for undo/redo
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 { toV2Id } from "../utilities/codap-utils"
import { CodapV2Component, CodapV2ComponentStorage } from "./codap-v2-types"

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 export 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 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
Loading
Loading