Skip to content

Commit

Permalink
feat: plugin API supports text component (#1415)
Browse files Browse the repository at this point in the history
  • Loading branch information
kswenson authored Aug 20, 2024
1 parent ef70ba2 commit bfb2421
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 15 deletions.
1 change: 1 addition & 0 deletions v3/src/components/text/text-defs.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const kTextTileType = "CodapText"
export const kV2TextType = "text"
export const kTextTileClass = "codap-text"
56 changes: 56 additions & 0 deletions v3/src/components/text/text-registration.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { V2Text } from "../../data-interactive/data-interactive-component-types"
import { diComponentHandler } from "../../data-interactive/handlers/component-handler"
import { testGetComponent, testUpdateComponent } from "../../data-interactive/handlers/component-handler-test-utils"
import { appState } from "../../models/app-state"
import { DocumentContentModel } from "../../models/document/document-content"
import { FreeTileRow } from "../../models/document/free-tile-row"
import { getTileComponentInfo } from "../../models/tiles/tile-component-info"
Expand Down Expand Up @@ -85,3 +89,55 @@ describe("Text registration", () => {
expect(tileWithInvalidComponent).toBeUndefined()
})
})

describe("text component handler", () => {
const documentContent = appState.document.content!
const diHandler = diComponentHandler

it("can create an empty text tile and retrieve its contents", () => {
expect(documentContent.tileMap.size).toBe(0)
const emptyTextTileResult = diHandler.create!({}, { type: "text" })
expect(emptyTextTileResult.success).toBe(true)
expect(documentContent.tileMap.size).toBe(1)
const [tileId, tile] = Array.from(documentContent.tileMap.entries())[0]
expect(isTextModel(tile.content)).toBe(true)
expect((tile.content as ITextModel).textContent).toBe("")

testGetComponent(tile, diHandler, (textTile, values) => {
const { text } = values as V2Text
expect(isTextModel(textTile.content)).toBe(true)
const textContent = textTile.content as ITextModel
expect(textContent.value).toBe(text)
})

documentContent.deleteTile(tileId)
expect(documentContent.tileMap.size).toBe(0)
})

it("can create a text tile with default text content and retrieve its contents", () => {
expect(documentContent.tileMap.size).toBe(0)
// creating empty text tile
const emptyTextTileResult = diHandler.create!({}, { type: "text", text: "To be, or not to be" })
expect(emptyTextTileResult.success).toBe(true)
expect(documentContent.tileMap.size).toBe(1)
const [tileId, tile] = Array.from(documentContent.tileMap.entries())[0]
expect(isTextModel(tile.content)).toBe(true)
expect((tile.content as ITextModel).textContent).toBe("To be, or not to be")

testGetComponent(tile, diHandler, (textTile, values) => {
const { text } = values as V2Text
expect(isTextModel(textTile.content)).toBe(true)
const textContent = textTile.content as ITextModel
expect(textContent.value).toBe(text)
})

const newValues: Partial<V2Text> = { text: "To be, or not to be, that is the question." }
testUpdateComponent(tile, diHandler, newValues, (textTile, values) => {
expect(isTextModel(tile.content)).toBe(true)
expect((tile.content as ITextModel).textContent).toBe(newValues.text)
})

documentContent.deleteTile(tileId)
expect(documentContent.tileMap.size).toBe(0)
})
})
59 changes: 48 additions & 11 deletions v3/src/components/text/text-registration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { serializeValue, textToSlate } from "@concord-consortium/slate-editor"
import { textToSlate } from "@concord-consortium/slate-editor"
import { SetRequired } from "type-fest"
import { ComponentTitleBar } from "../component-title-bar"
import { V2Text } from "../../data-interactive/data-interactive-component-types"
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"
Expand All @@ -9,8 +10,9 @@ import { safeJsonParse } from "../../utilities/js-utils"
import { t } from "../../utilities/translation/translate"
import { registerV2TileImporter } from "../../v2/codap-v2-tile-importers"
import { isV2TextComponent } from "../../v2/codap-v2-types"
import { kTextTileClass, kTextTileType } from "./text-defs"
import { ITextSnapshot, TextModel } from "./text-model"
import { ComponentTitleBar } from "../component-title-bar"
import { kTextTileClass, kTextTileType, kV2TextType } from "./text-defs"
import { editorValueToModelValue, isTextModel, ITextSnapshot, modelValueToEditorValue, TextModel } from "./text-model"
import { TextTile } from "./text-tile"
import TextIcon from "../../assets/icons/icon-text.svg"

Expand Down Expand Up @@ -43,23 +45,58 @@ registerTileComponentInfo({
defaultHeight: 100
})

registerV2TileImporter("DG.TextView", ({ v2Component, insertTile }) => {
if (!isV2TextComponent(v2Component)) return

const { guid, componentStorage: { title = "", text } } = v2Component

function importTextToModelValue(text?: string) {
// According to a comment in the v2 code: "Prior to build 0535 this was simple text.
// As of 0535 it is a JSON representation of the rich text content."
// For v3, we make sure we're always dealing with rich-text JSON.
const json = safeJsonParse(text)
const value = json != null && typeof json === "object" ? text : JSON.stringify(serializeValue(textToSlate(text)))
return text != null && json != null && typeof json === "object"
? text
: editorValueToModelValue(textToSlate(text ?? ""))
}

registerV2TileImporter("DG.TextView", ({ v2Component, insertTile }) => {
if (!isV2TextComponent(v2Component)) return

const { guid, componentStorage: { title = "", text } } = v2Component

const content: SetRequired<ITextSnapshot, "type"> = {
type: kTextTileType,
value
value: importTextToModelValue(text)
}
const textTileSnap: ITileModelSnapshotIn = { id: toV3Id(kTextIdPrefix, guid), _title: title, content }
const textTile = insertTile(textTileSnap)

return textTile
})

registerComponentHandler(kV2TextType, {
create({ values }) {
const { text } = values as V2Text
return {
content: {
type: kTextTileType,
value: importTextToModelValue(text)
}
}
},
get(content) {
if (isTextModel(content)) {
return {
type: kV2TextType,
text: content.value
}
}
},
update(content, values) {
if (isTextModel(content)) {
const { text } = values as V2Text
if (text) {
const modelValue = importTextToModelValue(text)
content.setValue(modelValueToEditorValue(modelValue))
}
}

return { success: true }
}
})
3 changes: 2 additions & 1 deletion v3/src/data-interactive/data-interactive-component-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { kCaseTableTileType, kV2CaseTableType } from "../components/case-table/c
import { kGraphTileType, kV2GraphType } from "../components/graph/graph-defs"
import { kMapTileType, kV2MapType } from "../components/map/map-defs"
import { kSliderTileType, kV2SliderType } from "../components/slider/slider-defs"
import { kTextTileType, kV2TextType } from "../components/text/text-defs"
import { kV2GameType, kV2WebViewType, kWebViewTileType } from "../components/web-view/web-view-defs"

// export const kV2ImageType = "image"
Expand All @@ -18,7 +19,7 @@ export const kComponentTypeV3ToV2Map: Record<string, string> = {
// kV2ImageType
[kMapTileType]: kV2MapType,
[kSliderTileType]: kV2SliderType,
// kV2TextType
[kTextTileType]: kV2TextType,
[kWebViewTileType]: kV2WebViewType
}

Expand Down
17 changes: 15 additions & 2 deletions v3/src/data-interactive/handlers/component-handler-test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { toV2Id } from "../../utilities/codap-utils"
import { kComponentTypeV3ToV2Map, V2Component } from "../data-interactive-component-types"
import { DIHandler } from "../data-interactive-types"

type specificTileTest = (tile: ITileModel, values: V2Component) => void
type specificTileGetTest = (tile: ITileModel, values: V2Component) => void
interface IOptions {
type?: string
}
export function testGetComponent(
tile: ITileModel, handler: DIHandler, extraTest?: specificTileTest, options?: IOptions
tile: ITileModel, handler: DIHandler, extraTest?: specificTileGetTest, options?: IOptions
) {
const getResult = handler.get!({ component: tile })
expect(getResult.success).toBe(true)
Expand All @@ -31,3 +31,16 @@ export function testGetComponent(

extraTest?.(tile, values)
}

type specificTileUpdateTest = (tile: ITileModel, values: Partial<V2Component>) => void

export function testUpdateComponent(
tile: ITileModel, handler: DIHandler, values: Partial<V2Component>, extraTest?: specificTileUpdateTest
) {
const updateResult = handler.update!({ component: tile }, values)
expect(updateResult.success).toBe(true)

// TODO: fill out generic tile update code

extraTest?.(tile, values)
}
2 changes: 1 addition & 1 deletion v3/src/data-interactive/handlers/component-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export const diComponentHandler: DIHandler = {
let result: DIHandlerFnResult | undefined
component.applyModelChange(() => {
// Handle updating generic component features
const { cannotClose, dimensions, position, title } = values as V2Component
const { cannotClose, dimensions, position, title } = values as Partial<V2Component>
if (cannotClose != null) component.setCannotClose(cannotClose)
if (title) component.setTitle(title)
// TODO Handle string positions?
Expand Down

0 comments on commit bfb2421

Please sign in to comment.