Skip to content

Commit

Permalink
187651946 v3 DI Get Multidata Plugin Running (#1277)
Browse files Browse the repository at this point in the history
* Handle get collectionList requests.

* Support adding attributes to collections when handling create attribute API requests.

* Handle update attributeLocation requests.

* Handle create collection requests.
  • Loading branch information
tealefristoe authored May 25, 2024
1 parent 0e63cf2 commit 294daf1
Show file tree
Hide file tree
Showing 16 changed files with 440 additions and 83 deletions.
69 changes: 14 additions & 55 deletions v3/src/components/case-table/column-header-divider.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import React, { CSSProperties, useEffect, useState } from "react"
import { createPortal } from "react-dom"
import { IAttribute } from "../../models/data/attribute"
import { isCollectionModel } from "../../models/data/collection"
import { IAttributeChangeResult, IMoveAttributeOptions } from "../../models/data/data-set-types"
import { deleteCollectionNotification, moveAttributeNotification } from "../../models/data/data-set-notifications"
import { getCollectionAttrs } from "../../models/data/data-set-utils"
import { moveAttribute } from "../../models/data/data-set-utils"
import { useCollectionContext } from "../../hooks/use-collection-context"
import { useDataSetContext } from "../../hooks/use-data-set-context"
import { getDragAttributeInfo, useTileDroppable } from "../../hooks/use-drag-drop"
import { kAttributeDividerDropZoneBaseId } from "./case-table-drag-drop"
import { kIndexColumnKey } from "./case-table-types"

interface IProps {
columnKey: string
Expand All @@ -18,62 +13,26 @@ interface IProps {
export const ColumnHeaderDivider = ({ columnKey, cellElt }: IProps) => {
const collectionId = useCollectionContext()
const droppableId = `${kAttributeDividerDropZoneBaseId}:${collectionId}:${columnKey}`
const data = useDataSetContext()
const dataset = useDataSetContext()
const [tableElt, setTableElt] = useState<HTMLElement | null>(null)
const tableBounds = tableElt?.getBoundingClientRect()
const cellBounds = cellElt?.getBoundingClientRect()

const { isOver, setNodeRef: setDropRef } = useTileDroppable(droppableId, active => {
const { dataSet, attributeId: dragAttrId } = getDragAttributeInfo(active) || {}
const collection = data?.getCollection(collectionId)
if (!collection || !dataSet || (dataSet !== data) || !dragAttrId) return
const targetCollection = dataset?.getCollection(collectionId)
if (!targetCollection || !dataSet || (dataSet !== dataset) || !dragAttrId) return

const srcCollection = dataSet.getCollectionForAttribute(dragAttrId)
const firstAttr: IAttribute | undefined = getCollectionAttrs(collection, data)[0]
const options: IMoveAttributeOptions = columnKey === kIndexColumnKey
? { before: firstAttr?.id }
: { after: columnKey }
const notifications = moveAttributeNotification(data)
if (collection === srcCollection) {
if (isCollectionModel(collection)) {
// move the attribute within a collection
data.applyModelChange(
() => collection.moveAttribute(dragAttrId, options),
{
notifications,
undoStringKey: "DG.Undo.dataContext.moveAttribute",
redoStringKey: "DG.Redo.dataContext.moveAttribute"
}
)
}
else {
// move an ungrouped attribute within the DataSet
data.applyModelChange(
() => data.moveAttribute(dragAttrId, options),
{
notifications,
undoStringKey: "DG.Undo.dataContext.moveAttribute",
redoStringKey: "DG.Redo.dataContext.moveAttribute"
}
)
}
}
else {
// move the attribute to a new collection
let result: IAttributeChangeResult | undefined
data.applyModelChange(
() => {
result = data.setCollectionForAttribute(dragAttrId, { collection: collection?.id, ...options })
},
{
notifications: () => result?.removedCollectionId
? [deleteCollectionNotification(data), notifications]
: notifications,
undoStringKey: "DG.Undo.dataContext.moveAttribute",
redoStringKey: "DG.Redo.dataContext.moveAttribute"
}
)
}
const sourceCollection = dataSet.getCollectionForAttribute(dragAttrId)
moveAttribute({
afterAttrId: columnKey,
attrId: dragAttrId,
dataset,
includeNotifications: true,
sourceCollection,
targetCollection,
undoable: true
})
})

// find the `case-table-content` DOM element; divider must be drawn relative
Expand Down
8 changes: 7 additions & 1 deletion v3/src/data-interactive/data-interactive-type-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IAttribute, IAttributeSnapshot } from "../models/data/attribute"
import { ICollectionModel } from "../models/data/collection"
import { ICollectionModel, ICollectionPropsModel } from "../models/data/collection"
import { IDataSet } from "../models/data/data-set"
import { ICase } from "../models/data/data-set-types"
import { v2ModelSnapshotFromV2ModelStorage } from "../models/data/v2-model"
Expand Down Expand Up @@ -219,6 +219,12 @@ export function basicAttributeInfo(attribute: IAttribute) {
return { name, id: toV2Id(id), title }
}

export function basicCollectionInfo(collection: ICollectionPropsModel) {
const { name, id, title } = collection
const v2Id = toV2Id(id)
return { name, guid: v2Id, title, id: v2Id }
}

export function valuesFromGlobal(global: IGlobalValue) {
return {
name: global.name,
Expand Down
16 changes: 14 additions & 2 deletions v3/src/data-interactive/data-interactive-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export interface DIAllCases {
}
}
export type DIAttribute = Partial<ICodapV2Attribute>
export interface DIAttributeLocationValues {
collection?: string | number
position?: number
}
export interface DICase {
collectionID?: number
collectionName?: string
Expand Down Expand Up @@ -89,6 +93,13 @@ export interface DIInteractiveFrame {
version?: string
}
export type DIItem = DICaseValues
export interface DICreateCollection {
name?: string
title?: string
parent?: string
attributes?: DIAttribute[]
attrs?: DIAttribute[]
}
export interface DINewCase {
id?: number
itemID?: number
Expand All @@ -109,6 +120,7 @@ export interface DIResources {
caseFormulaSearch?: DICase[]
caseSearch?: DICase[]
collection?: ICollectionPropsModel
collectionList?: ICollectionPropsModel[]
component?: DIComponent
dataContext?: IDataSet
dataContextList?: IDataSet[]
Expand All @@ -123,8 +135,8 @@ export interface DIResources {
}

// types for values accepted as inputs by the API
export type DISingleValues = DIAttribute | DICase | DIDataContext |
DIGlobal | DIInteractiveFrame | DINewCase | DIUpdateCase | DINotification | V2Component
export type DISingleValues = DIAttribute | DIAttributeLocationValues | DICase | DIDataContext |
DIGlobal | DIInteractiveFrame | DICreateCollection | DINewCase | DIUpdateCase | DINotification | V2Component
export type DIValues = DISingleValues | DISingleValues[] | number | string[]

// types returned as outputs by the API
Expand Down
9 changes: 8 additions & 1 deletion v3/src/data-interactive/data-interactive-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IDataSet } from "../models/data/data-set"
import { ICaseCreation } from "../models/data/data-set-types"
import { toV2Id } from "../utilities/codap-utils"
import { toV2Id, toV3CollectionId } from "../utilities/codap-utils"
import { DICaseValues } from "./data-interactive-types"

export function canonicalizeAttributeName(name: string, iCanonicalize = true) {
Expand Down Expand Up @@ -47,3 +47,10 @@ export function attrNamesToIds(values: DICaseValues, dataSet: IDataSet, v2Ids?:
})
return caseValues
}

export function getCollection(dataContext?: IDataSet, nameOrId?: string) {
if (!dataContext || !nameOrId) return

return dataContext?.getCollectionByName(nameOrId) ||
dataContext?.getCollection(toV3CollectionId(nameOrId))
}
10 changes: 10 additions & 0 deletions v3/src/data-interactive/handlers/attribute-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { Attribute } from "../../models/data/attribute"
import { diAttributeHandler } from "./attribute-handler"
import { DIResultAttributes } from "../data-interactive-types"
import { DataSet } from "../../models/data/data-set"
import { setupTestDataset } from "./handler-test-utils"

describe("DataInteractive AttributeHandler", () => {
const handler = diAttributeHandler

it("get works as expected", () => {
expect(handler.get?.({}).success).toBe(false)

Expand All @@ -13,6 +15,7 @@ describe("DataInteractive AttributeHandler", () => {
expect(result?.success).toBe(true)
expect((result?.values as any)?.name).toBe("test")
})

it("create works as expected", () => {
const dataContext = DataSet.create({})
const resources = { dataContext }
Expand All @@ -32,7 +35,13 @@ describe("DataInteractive AttributeHandler", () => {
expect(dataContext.attributes.length).toBe(3)
expect(dataContext.attributes[1].name).toBe(name2)
expect(dataContext.attributes[2].name).toBe(name3)

const { dataset, c1 } = setupTestDataset()
expect(c1.attributes.length).toBe(1)
expect(handler.create?.({ dataContext: dataset, collection: c1 }, [{ name: name1 }]).success).toBe(true)
expect(c1.attributes.length).toBe(2)
})

it("update works as expected", () => {
const attribute = Attribute.create({ name: "test" })
const name = "new name"
Expand Down Expand Up @@ -62,6 +71,7 @@ describe("DataInteractive AttributeHandler", () => {
const resultAttr2 = values2.attrs?.[0]
expect(resultAttr2?.type).toBe(type)
})

it("delete works as expected", () => {
const attribute = Attribute.create({ name: "name" })
const dataContext = DataSet.create({})
Expand Down
4 changes: 2 additions & 2 deletions v3/src/data-interactive/handlers/attribute-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { attributeNotFoundResult, dataContextNotFoundResult } from "./di-results

export const diAttributeHandler: DIHandler = {
create(resources: DIResources, _values?: DIValues) {
const { dataContext } = resources
const { dataContext, collection } = resources
if (!dataContext) return dataContextNotFoundResult
const metadata = getSharedCaseMetadataFromDataset(dataContext)
const values = _values as DIAttribute | DIAttribute[]
Expand All @@ -34,7 +34,7 @@ export const diAttributeHandler: DIHandler = {
dataContext.applyModelChange(() => {
attributeValues.forEach(attributeValue => {
if (attributeValue) {
const attribute = createAttribute(attributeValue, dataContext, metadata)
const attribute = createAttribute(attributeValue, dataContext, metadata, collection)
if (attribute) attributes.push(attribute)
}
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ICollectionPropsModel } from "../../models/data/collection"
import { diAttributeLocationHandler } from "./attribute-location-handler"
import { setupTestDataset } from "./handler-test-utils"

describe("DataInteractive AttributeLocationHandler", () => {
const handler = diAttributeLocationHandler

it("update works as expected", () => {
const { dataset, c1, c2, a1, a2 } = setupTestDataset()
const a4 = dataset.addAttribute({ name: "a4" }, { collection: c1.id })
const a5 = dataset.addAttribute({ name: "a5" }, { collection: c2.id })
const a6 = dataset.addAttribute({ name: "a6" })
const dataContext = dataset
const resources = { attributeLocation: a1, dataContext }

const collectionAttributes = (collection: ICollectionPropsModel) =>
dataset.getGroupedCollection(collection.id)?.attributes

expect(handler.update?.({})?.success).toBe(false)
// Missing dataContext
expect(handler.update?.(
{ attributeLocation: a2 }, { collection: c1.name, position: 0 }
).success).toBe(false)
// Missing attributeLocation
expect(handler.update?.({ dataContext }, { collection: c1.name, position: 0 }).success).toBe(false)
// Parent of leftmost collection
expect(handler.update?.(
resources, { collection: "parent", position: 0 }
).success).toBe(false)

// Move attribute within the ungrouped collection
// Indexes snap to the end of the array
expect(dataset.ungroupedAttributes[1].id).toBe(a6.id)
expect(handler.update?.({ attributeLocation: a6, dataContext }, { position: -1 }).success).toBe(true)
expect(dataset.ungroupedAttributes[0].id).toBe(a6.id)
expect(handler.update?.({ attributeLocation: a6, dataContext }, { position: 10 }).success).toBe(true)
expect(dataset.ungroupedAttributes[1].id).toBe(a6.id)

// Move attribute within a grouped collection
// If not specified, move the attribute to the far right
expect(collectionAttributes(c2)?.[1]?.id).toBe(a5.id)
expect(handler.update?.({ attributeLocation: a5, dataContext }, { position: 0 }).success).toBe(true)
expect(collectionAttributes(c2)?.[0]?.id).toBe(a5.id)
expect(handler.update?.({ attributeLocation: a5, dataContext }).success).toBe(true)
expect(collectionAttributes(c2)?.[1]?.id).toBe(a5.id)

// Move attribute from ungrouped collection to middle of grouped collection
expect(collectionAttributes(c1)?.[1]?.id).toBe(a4.id)
expect(collectionAttributes(c1)?.length).toBe(2)
expect(handler.update?.({ attributeLocation: a6, dataContext }, { collection: c1.name, position: 1 }).success)
.toBe(true)
expect(collectionAttributes(c1)?.length).toBe(3)
expect(dataset.ungroupedAttributes.length).toBe(1)
expect(collectionAttributes(c1)?.[1]?.id).toBe(a6.id)
expect(collectionAttributes(c1)?.[2]?.id).toBe(a4.id)

// Move attribute from grouped collection to middle of its parent collection
// Round the position
expect(handler.update?.({ attributeLocation: a5, dataContext }, { collection: "parent", position: 1.2 }).success)
.toBe(true)
expect(collectionAttributes(c1)?.length).toBe(4)
expect(collectionAttributes(c2)?.length).toBe(1)
expect(collectionAttributes(c1)?.[1]?.id).toBe(a5.id)
})
})
40 changes: 38 additions & 2 deletions v3/src/data-interactive/handlers/attribute-location-handler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,44 @@
import { moveAttribute } from "../../models/data/data-set-utils"
import { registerDIHandler } from "../data-interactive-handler"
import { DIHandler, diNotImplementedYet } from "../data-interactive-types"
import { DIAttributeLocationValues, DIHandler, DIResources, DIValues } from "../data-interactive-types"
import { getCollection } from "../data-interactive-utils"
import { attributeNotFoundResult, collectionNotFoundResult, dataContextNotFoundResult } from "./di-results"

export const diAttributeLocationHandler: DIHandler = {
update: diNotImplementedYet
update(resources: DIResources, values?: DIValues) {
const { attributeLocation, dataContext } = resources
if (!dataContext) return dataContextNotFoundResult
if (!attributeLocation) return attributeNotFoundResult
const sourceCollection = dataContext.getCollectionForAttribute(attributeLocation.id)

const { collection, position } = (values ?? {}) as DIAttributeLocationValues
const targetCollection = collection === "parent"
? dataContext.getParentCollectionGroup(sourceCollection?.id)?.collection
: getCollection(dataContext, collection ? `${collection}` : undefined) ?? sourceCollection
if (!targetCollection) return collectionNotFoundResult

const targetAttrs =
dataContext.getGroupedCollection(targetCollection.id)?.attributes ?? dataContext.ungroupedAttributes
const numPos = Number(position)

// If the position isn't specified or isn't a number, make the attribute the right-most
// Otherwise, round the position to an integer
const pos = isNaN(numPos) ? targetAttrs.length : Math.round(numPos)

// Snap the position to the left or right if it is negative or very large
const _position = pos < 0 ? 0 : pos > targetAttrs.length ? targetAttrs.length : pos
const afterAttrId = targetAttrs[_position - 1]?.id

moveAttribute({
afterAttrId,
attrId: attributeLocation.id,
dataset: dataContext,
sourceCollection,
targetCollection
})

return { success: true }
}
}

registerDIHandler("attributeLocation", diAttributeLocationHandler)
Loading

0 comments on commit 294daf1

Please sign in to comment.