Skip to content

Commit

Permalink
feat: v2 slider export (#1695)
Browse files Browse the repository at this point in the history
  • Loading branch information
kswenson authored Dec 20, 2024
1 parent 4236c25 commit d705d34
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 31 deletions.
11 changes: 7 additions & 4 deletions v3/src/components/slider/slider-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { getGlobalValueManager, getSharedModelManager } from "../../models/tiles
import { ITileContentModel, TileContentModel } from "../../models/tiles/tile-content"
import { DateUnit, dateUnits, unitsStringToMilliseconds } from "../../utilities/date-utils"
import { kSliderTileType } from "./slider-defs"
import {AnimationDirection, AnimationDirections, AnimationMode, AnimationModes, FixValueFn, ISliderScaleType,
import {
AnimationDirection, AnimationDirections, AnimationMode, AnimationModes, FixValueFn, ISliderScaleType,
kDefaultAnimationDirection, kDefaultAnimationMode, kDefaultAnimationRate, kDefaultDateMultipleOfUnit,
kDefaultSliderScaleType, SliderScaleTypes} from "./slider-types"
kDefaultSliderAxisMax, kDefaultSliderAxisMin, kDefaultSliderScaleType, SliderScaleTypes
} from "./slider-types"

export const SliderModel = TileContentModel
.named("SliderModel")
Expand All @@ -25,7 +27,7 @@ export const SliderModel = TileContentModel
_animationRate: types.maybe(types.number), // frames per second
scaleType: types.optional(types.enumeration([...SliderScaleTypes]), kDefaultSliderScaleType),
axis: types.optional(types.union(NumericAxisModel, DateAxisModel),
() => NumericAxisModel.create({ place: 'bottom', min: -0.5, max: 11.5 }))
() => NumericAxisModel.create({ place: 'bottom', min: kDefaultSliderAxisMin, max: kDefaultSliderAxisMax }))
})
.views(self => ({
get name() {
Expand Down Expand Up @@ -169,7 +171,8 @@ export const SliderModel = TileContentModel
if (scaleType !== self.scaleType) {
switch (scaleType) {
case "numeric":
self.axis = NumericAxisModel.create({ place: 'bottom', min: -0.5, max: 11.5 })
self.axis = NumericAxisModel.create({
place: 'bottom', min: kDefaultSliderAxisMin, max: kDefaultSliderAxisMax })
self.setValue(0.5)
break
case "date": {
Expand Down
70 changes: 51 additions & 19 deletions v3/src/components/slider/slider-registration.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { DocumentContentModel } from "../../models/document/document-content"
import { FreeTileRow } from "../../models/document/free-tile-row"
import { createCodapDocument } from "../../models/codap/create-codap-document"
import { FreeTileRow, IFreeTileRow } from "../../models/document/free-tile-row"
import { GlobalValue, IGlobalValueSnapshot } from "../../models/global/global-value"
import { getTileComponentInfo } from "../../models/tiles/tile-component-info"
import { getTileContentInfo } from "../../models/tiles/tile-content-info"
import { getGlobalValueManager, getSharedModelManager } from "../../models/tiles/tile-environment"
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 { ICodapV2DocumentJson, ICodapV2SliderStorage } from "../../v2/codap-v2-types"
import { kSliderTileType } from "./slider-defs"
import { isSliderModel } from "./slider-model"
import "./slider-registration"

const fs = require("fs")
Expand All @@ -34,35 +37,39 @@ describe("Slider registration", () => {
expect(slider).toBeDefined()
expect(mockGlobalValueManager.addValueSnapshot).toHaveBeenCalledTimes(1)
})
it("imports v2 slider components", () => {
it("imports/exports v2 slider components", () => {
const file = path.join(__dirname, "../../test/v2", "slider.codap")
const sliderJson = fs.readFileSync(file, "utf8")
const sliderDoc = JSON.parse(sliderJson) as ICodapV2DocumentJson
const v2Document = new CodapV2Document(sliderDoc)
const mockGlobalValueManager = {
addValueSnapshot: jest.fn((snap: IGlobalValueSnapshot) => GlobalValue.create(snap))
}
const mockSharedModelManager = {
addTileSharedModel: jest.fn(),
getSharedModelsByType: () => [mockGlobalValueManager]
}

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

const [sliderComponent] = v2Document.components
expect(sliderComponent.type).toBe("DG.SliderView")

const tile = importV2Component({
v2Component: v2Document.components[0],
v2Component: sliderComponent,
v2Document,
sharedModelManager: mockSharedModelManager as any,
sharedModelManager,
insertTile: mockInsertTile
})
})!
expect(tile).toBeDefined()
expect(mockGlobalValueManager.addValueSnapshot).toHaveBeenCalledTimes(1)
expect(mockSharedModelManager.addTileSharedModel).toHaveBeenCalledTimes(1)
expect(mockInsertTile).toHaveBeenCalledTimes(1)
expect(globalValueManager?.globals.size).toBe(1)

const sliderModel = isSliderModel(tile.content) ? tile.content : undefined
expect(sliderModel).toBeDefined()
expect(sliderModel?.animationDirection).toBe("lowToHigh")
expect(sliderModel?.animationMode).toBe("onceOnly")
expect(sliderModel?._animationRate).toBeUndefined()
expect(sliderModel?.multipleOf).toBeUndefined()

const tileWithInvalidDocument = importV2Component({
v2Component: {} as any,
Expand All @@ -72,10 +79,35 @@ describe("Slider registration", () => {
expect(tileWithInvalidDocument).toBeUndefined()

const tileWithNoSharedModel = importV2Component({
v2Component: v2Document.components[0],
v2Component: sliderComponent,
v2Document,
insertTile: mockInsertTile
})
expect(tileWithNoSharedModel).toBeUndefined()

// export numeric slider in v2 format
const row = docContent.getRowByIndex(0) as IFreeTileRow
const sliderExport = exportV2Component({ tile, row, sharedModelManager })
expect(sliderExport?.type).toBe("DG.SliderView")
const sliderStorage = sliderExport!.componentStorage as ICodapV2SliderStorage
expect(sliderStorage._links_?.model).toBeDefined()
expect(sliderStorage.animationDirection).toBe(1)
expect(sliderStorage.animationMode).toBe(1)
expect(sliderStorage.maxPerSecond).toBeNull()
expect(sliderStorage.restrictToMultiplesOf).toBeNull()

// change to date-time slider and export in v2 format
sliderModel!.setScaleType("date")
sliderModel!.setDateMultipleOfUnit("day")
const dateSliderExport = exportV2Component({ tile, row, sharedModelManager })
expect(dateSliderExport?.type).toBe("DG.SliderView")
const dateSliderStorage = dateSliderExport!.componentStorage as ICodapV2SliderStorage
expect(dateSliderStorage._links_?.model).toBeDefined()
expect(dateSliderStorage.animationDirection).toBe(1)
expect(dateSliderStorage.animationMode).toBe(1)
expect(dateSliderStorage.maxPerSecond).toBeNull()
expect(dateSliderStorage.restrictToMultiplesOf).toBeNull()
expect(dateSliderStorage.v3?.scaleType).toBe("date")
expect(dateSliderStorage.v3?.dateMultipleOfUnit).toBe("day")
})
})
68 changes: 60 additions & 8 deletions v3/src/components/slider/slider-registration.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SetRequired } from "type-fest"
import { SetOptional, SetRequired } from "type-fest"
import SliderIcon from "../../assets/icons/icon-slider.svg"
import { V2Slider } from "../../data-interactive/data-interactive-component-types"
import { registerComponentHandler } from "../../data-interactive/handlers/component-handler"
import { errorResult } from "../../data-interactive/handlers/di-results"
Expand All @@ -8,18 +9,25 @@ import { registerTileComponentInfo } from "../../models/tiles/tile-component-inf
import { ITileLikeModel, registerTileContentInfo } from "../../models/tiles/tile-content-info"
import { getGlobalValueManager } from "../../models/tiles/tile-environment"
import { ITileModelSnapshotIn } from "../../models/tiles/tile-model"
import { toV3GlobalId, toV3Id } from "../../utilities/codap-utils"
import { toV2Id, toV3GlobalId, toV3Id } from "../../utilities/codap-utils"
import { DateUnit, unitsStringToMilliseconds } from "../../utilities/date-utils"
import { isFiniteNumber } from "../../utilities/math-utils"
import { isAliveSafe } from "../../utilities/mst-utils"
import { t } from "../../utilities/translation/translate"
import { registerV2TileExporter } from "../../v2/codap-v2-tile-exporters"
import { registerV2TileImporter } from "../../v2/codap-v2-tile-importers"
import { isV2SliderComponent } from "../../v2/codap-v2-types"
import {
guidLink, ICodapV2BaseComponentStorage, ICodapV2SliderStorage, isV2SliderComponent
} from "../../v2/codap-v2-types"
import { SliderComponent } from "./slider-component"
import { SliderInspector } from "./slider-inspector"
import { kSliderTileType, kSliderTileClass, kV2SliderType } from "./slider-defs"
import { ISliderSnapshot, SliderModel, isSliderModel } from "./slider-model"
import { SliderTitleBar } from "./slider-title-bar"
import { AnimationDirections, AnimationModes, kDefaultAnimationDirection, kDefaultAnimationMode } from "./slider-types"
import SliderIcon from '../../assets/icons/icon-slider.svg'
import {
AnimationDirection, AnimationDirections, AnimationMode, AnimationModes,
kDefaultAnimationDirection, kDefaultAnimationMode, kDefaultSliderAxisMax, kDefaultSliderAxisMin
} from "./slider-types"
import { kDefaultSliderName, kDefaultSliderValue } from "./slider-utils"

export const kSliderIdPrefix = "SLID"
Expand Down Expand Up @@ -65,7 +73,46 @@ registerTileComponentInfo({
isFixedHeight: true,
// must be in sync with rendered size for auto placement code
defaultHeight: 73
})

registerV2TileExporter(kSliderTileType, ({ tile }) => {
const sliderModel = isSliderModel(tile.content) ? tile.content : undefined
if (!sliderModel) return
const {
domain: [lowerBound, upperBound],
animationDirection,
animationMode,
_animationRate,
multipleOf,
dateMultipleOfUnit,
scaleType
} = sliderModel

const domain = isFiniteNumber(lowerBound) && isFiniteNumber(upperBound) ? { lowerBound, upperBound } : undefined

const getAnimationDirectionIndex = (direction?: AnimationDirection) => {
return direction != null ? AnimationDirections.findIndex(_direction => _direction === direction) : 1
}
const getAnimationModeIndex = (mode?: AnimationMode) => {
return mode != null ? AnimationModes.findIndex(_mode => _mode === mode) : 1
}
// v2 doesn't support date-time sliders; convert to seconds instead
const restrictToMultiplesOf = scaleType === "date" && multipleOf != null
? multipleOf * unitsStringToMilliseconds(dateMultipleOfUnit) / 1000
: multipleOf
// v3 extensions: ignored by v2, but allows full round-trip for v3 save/restore
const v3: ICodapV2SliderStorage["v3"] = { scaleType, multipleOf, dateMultipleOfUnit }

const componentStorage: SetOptional<ICodapV2SliderStorage, keyof ICodapV2BaseComponentStorage> = {
_links_: { model: guidLink("DG.GlobalValue", toV2Id(tile.id)) },
...domain,
animationDirection: getAnimationDirectionIndex(animationDirection),
animationMode: getAnimationModeIndex(animationMode),
maxPerSecond: _animationRate ?? null,
restrictToMultiplesOf: restrictToMultiplesOf ?? null,
v3
}
return { type: "DG.SliderView", componentStorage }
})

registerV2TileImporter("DG.SliderView", ({ v2Component, v2Document, sharedModelManager, insertTile }) => {
Expand All @@ -79,7 +126,7 @@ registerV2TileImporter("DG.SliderView", ({ v2Component, v2Document, sharedModelM
guid: componentGuid,
componentStorage: {
name, title: v2Title = "", _links_, lowerBound, upperBound, animationDirection, animationMode,
restrictToMultiplesOf, maxPerSecond, userTitle, userSetTitle
restrictToMultiplesOf, maxPerSecond, userTitle, userSetTitle, v3
}
} = v2Component
const globalId = _links_.model.id
Expand All @@ -100,15 +147,20 @@ registerV2TileImporter("DG.SliderView", ({ v2Component, v2Document, sharedModelM
return AnimationModes[mode] || kDefaultAnimationMode
}

const axisType = v3?.scaleType ?? "numeric"
const axisMin = lowerBound ?? kDefaultSliderAxisMin
const axisMax = upperBound ?? kDefaultSliderAxisMax

// create slider model
const content: ISliderSnapshot = {
type: kSliderTileType,
globalValue: globalValue.id,
multipleOf: restrictToMultiplesOf ?? undefined,
multipleOf: v3?.multipleOf ?? restrictToMultiplesOf ?? undefined,
dateMultipleOfUnit: v3?.dateMultipleOfUnit as DateUnit ?? undefined,
animationDirection: getAnimationDirectionStr(animationDirection),
animationMode: getAnimationModeStr(animationMode),
_animationRate: maxPerSecond ?? undefined,
axis: { type: "numeric", place: "bottom", min: lowerBound ?? 0, max: upperBound ?? 12 }
axis: { type: axisType, place: "bottom", min: axisMin, max: axisMax }
}
const title = v2Title && (userTitle || userSetTitle) ? v2Title : undefined
const sliderTileSnap: ITileModelSnapshotIn = {
Expand Down
3 changes: 3 additions & 0 deletions v3/src/components/slider/slider-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export const kDefaultSliderAxisTop = 0
export const kDefaultSliderAxisHeight = 24
export const kDefaultSliderPadding = 10

export const kDefaultSliderAxisMin = -0.5
export const kDefaultSliderAxisMax = 11.5

// values are translation string keys; indices are v2 values
export const AnimationDirections = ["backAndForth", "lowToHigh", "highToLow"] as const
export type AnimationDirection = typeof AnimationDirections[number]
Expand Down
10 changes: 10 additions & 0 deletions v3/src/v2/codap-v2-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ export interface ICodapV2BaseComponentStorage {
// In the CFM shared files there are more than 20,000 examples of cannotClose: true
// and more 20,000 examples cannotClose: false
cannotClose?: boolean
// allows v2 documents saved by v3 to contain v3-specific enhancements
v3?: object
}

export interface ICodapV2CalculatorStorage extends ICodapV2BaseComponentStorage {
Expand All @@ -261,7 +263,15 @@ export interface ICodapV2SliderStorage extends ICodapV2BaseComponentStorage {
animationMode?: number
restrictToMultiplesOf?: number | null
maxPerSecond?: number | null
// NOTE: v2 writes out the `userTitle` property, but reads in property `userChangedTitle`.
// It is also redundant with the `userSetTitle` property shared by all components. ¯\_(ツ)_/¯
userTitle?: boolean
// v3 enhancements
v3?: {
scaleType: "numeric" | "date",
multipleOf?: number
dateMultipleOfUnit?: string
}
}

export interface ICodapV2RowHeight {
Expand Down

0 comments on commit d705d34

Please sign in to comment.