Skip to content

Commit

Permalink
187485557 v3 DI Attribute Notifications (#1236)
Browse files Browse the repository at this point in the history
* Broadcast hideAttributes notification.

* Broadcast unhideAttributes/showAttributes notification.

* Broadcast moveAttribute notification.

* Broadcast deleteAttributes notification.

* Broadcast createAttributes notification.

* Allow multiple notifications from a single model change.

* Broadcast updateAttributes notification on attribute rename, Edit Attribute Properties and Edit Formula.

---------

Co-authored-by: Kirk Swenson <[email protected]>
  • Loading branch information
tealefristoe and kswenson authored Apr 30, 2024
1 parent 0bcd3d9 commit 5fa9c1c
Show file tree
Hide file tree
Showing 25 changed files with 240 additions and 55 deletions.
70 changes: 69 additions & 1 deletion v3/cypress/e2e/plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { WebViewTileElements as webView } from "../support/elements/web-view-til

context("codap plugins", () => {
beforeEach(function () {
const url = `${Cypress.config("index")}?mouseSensor&sample=mammals&dashboard`
const url = `${Cypress.config("index")}?sample=mammals&dashboard`
cy.visit(url)
})
const openAPITester = () => {
Expand Down Expand Up @@ -145,6 +145,51 @@ context("codap plugins", () => {
openAPITester()
webView.toggleAPITesterFilter()

cy.log("Broadcast attribute notifications")

cy.log("Broadcast hideAttributes notifications")
c.selectTile("table", 0)
table.openAttributeMenu("Mammal")
table.selectMenuItemFromAttributeMenu("Hide Attribute")
webView.confirmAPITesterResponseContains(/"operation":\s"hideAttributes/)
webView.clearAPITesterResponses()

cy.log("Broadcast unhideAttributes notifications")
table.showAllAttributes()
webView.confirmAPITesterResponseContains(/"operation":\s"unhideAttributes/)
webView.clearAPITesterResponses()

cy.log("Broadcast createAttributes notifications")
// + button in collection header
table.addNewAttribute()
webView.confirmAPITesterResponseContains(/"operation":\s"createAttributes/)
webView.clearAPITesterResponses()
// New Attribute button in ruler menu
table.getRulerButton().click()
table.selectItemFromRulerMenu("New Attribute")
webView.confirmAPITesterResponseContains(/"operation":\s"createAttributes/)
webView.clearAPITesterResponses()

cy.log("Broadcast deleteAttributes notifications")
table.deleteAttrbute("newAttr2")
webView.confirmAPITesterResponseContains(/"operation":\s"deleteAttributes/)
webView.clearAPITesterResponses()

cy.log("Broadcast updateAttributes notifications")
// Rename attribute
const newName = "newerAttr"
table.renameAttribute("newAttr", newName)
webView.confirmAPITesterResponseContains(/"operation":\s"updateAttributes/)
webView.clearAPITesterResponses()
// Edit attribute properties
table.editAttributeProperties(newName, "", null, null, null, null, null)
webView.confirmAPITesterResponseContains(/"operation":\s"updateAttributes/)
webView.clearAPITesterResponses()
// Edit formula
table.editFormula(newName, "Mass * 2")
webView.confirmAPITesterResponseContains(/"operation":\s"updateAttributes/)
webView.clearAPITesterResponses()

cy.log("Broadcast global value change notifications")
slider.changeVariableValue(8)
webView.confirmAPITesterResponseContains(/"action":\s"notify",\s"resource":\s"global/)
Expand All @@ -155,4 +200,27 @@ context("codap plugins", () => {
slider.pauseSliderButton()
webView.clearAPITesterResponses()
})

it('will broadcast notifications involving dragging', () => {
const url = `${Cypress.config("index")}?mouseSensor&sample=mammals&dashboard`
cy.visit(url)

openAPITester()
webView.toggleAPITesterFilter()

cy.log("Broadcast moveAttribute notifications")
table.moveAttributeToParent("Sleep", "newCollection")
// Move attribute within the ungrouped collection
table.moveAttributeToParent("Diet", "headerDivider", 3)
webView.confirmAPITesterResponseContains(/"operation":\s"moveAttribute/)
webView.clearAPITesterResponses()
// Move attribute to a different collection
table.moveAttributeToParent("Diet", "prevCollection")
webView.confirmAPITesterResponseContains(/"operation":\s"moveAttribute/)
webView.clearAPITesterResponses()
// Move attribute within a true collection
table.moveAttributeToParent("Diet", "headerDivider", 3)
webView.confirmAPITesterResponseContains(/"operation":\s"moveAttribute/)
webView.clearAPITesterResponses()
})
})
4 changes: 2 additions & 2 deletions v3/cypress/e2e/table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,8 +663,8 @@ context("case table ui", () => {
table.getHideShowButton().then($element => {
c.checkToolTip($element, c.tooltips.tableHideShowButton)
})
table.getAttributesButton().then($element => {
c.checkToolTip($element, c.tooltips.tableAttributesButton)
table.getRulerButton().then($element => {
c.checkToolTip($element, c.tooltips.tableRulerButton)
})
})
})
Expand Down
8 changes: 6 additions & 2 deletions v3/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ Cypress.Commands.add("clickMenuItem", text => {
cy.get("button[role=menuitem]").contains(text).click()
})

Cypress.Commands.add("dragAttributeToTarget", (source, attribute, target) => {
Cypress.Commands.add("dragAttributeToTarget", (source, attribute, target, targetNumber) => {
const el = {
tableColumnHeader:
`.codap-case-table [data-testid="codap-attribute-button ${attribute}"]`,
headerDivider: `.codap-column-header-divider`,
caseCardHeader: ".react-data-card-attribute",
caseCardHeaderDropZone: ".react-data-card .data-cell-lower",
caseCardCollectionDropZone: ".react-data-card .collection-header-row",
Expand Down Expand Up @@ -107,6 +108,9 @@ Cypress.Commands.add("dragAttributeToTarget", (source, attribute, target) => {
case ("prevCollection"):
target_el = el.prevCollection
break
case ("headerDivider"):
target_el = el.headerDivider
break
default:
target_el = el.tableColumnHeader
break
Expand All @@ -116,7 +120,7 @@ Cypress.Commands.add("dragAttributeToTarget", (source, attribute, target) => {
cy.get(source_el).contains(attribute)
.trigger("mousedown", { force: true })
.then(() => {
cy.get(target_el).then($target => {
cy.get(target_el).eq(targetNumber ?? 0).then($target => {
return $target[0].getBoundingClientRect()
})
.then($targetRect => {
Expand Down
2 changes: 1 addition & 1 deletion v3/cypress/support/elements/component-elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const ComponentElements = {
tableResizeButton: "Resize all columns to fit data",
tableDeleteCasesButton: "Delete selected or unselected cases",
tableHideShowButton: "Show all cases or hide selected/unselected cases",
tableAttributesButton: "Make new attributes. Export case data."
tableRulerButton: "Make new attributes. Export case data."
},
getComponentSelector(component) {
return cy.get(`.codap-component[data-testid$=${component}]`)
Expand Down
14 changes: 7 additions & 7 deletions v3/cypress/support/elements/map-tile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const MapTileElements = {
},
selectHideShowButton() {
c.getInspectorPanel().find("[data-testid=map-hide-show-button]").click()
c.getInspectorPanel().find("[data-testid=trash-menu-list]").should("be.visible")
c.getInspectorPanel().find("[data-testid=hide-show-menu-list]").should("be.visible")
},
getDisplayValuesButton() {
return c.getInspectorPanel().find("[data-testid=map-display-values-button]")
Expand All @@ -36,30 +36,30 @@ export const MapTileElements = {
return c.getInspectorPanel().find("[data-testid=hide-selected-cases]")
},
selectHideSelectedCases() {
c.getInspectorPanel().find("[data-testid=trash-menu-list]").should("be.visible").then(() => {
c.getInspectorPanel().find("[data-testid=hide-show-menu-list]").should("be.visible").then(() => {
c.getInspectorPanel().find("[data-testid=hide-selected-cases]").click()
c.getInspectorPanel().find("[data-testid=hide-selected-cases]").should("not.exist")
c.getInspectorPanel().find("[data-testid=trash-menu-list]").should("not.be.visible")
c.getInspectorPanel().find("[data-testid=hide-show-menu-list]").should("not.be.visible")
})
},
getHideUnselectedCases() {
return c.getInspectorPanel().find("[data-testid=hide-unselected-cases]")
},
selectHideUnselectedCases() {
c.getInspectorPanel().find("[data-testid=trash-menu-list]").should("be.visible").then(() => {
c.getInspectorPanel().find("[data-testid=hide-show-menu-list]").should("be.visible").then(() => {
c.getInspectorPanel().find("[data-testid=hide-unselected-cases]").click()
c.getInspectorPanel().find("[data-testid=hide-unselected-cases]").should("not.exist")
c.getInspectorPanel().find("[data-testid=trash-menu-list]").should("not.be.visible")
c.getInspectorPanel().find("[data-testid=hide-show-menu-list]").should("not.be.visible")
})
},
getShowAllCases() {
return c.getInspectorPanel().find("[data-testid=show-all-cases]")
},
selectShowAllCases() {
c.getInspectorPanel().find("[data-testid=trash-menu-list]").should("be.visible").then(() => {
c.getInspectorPanel().find("[data-testid=hide-show-menu-list]").should("be.visible").then(() => {
c.getInspectorPanel().find("[data-testid=show-all-cases]").click()
c.getInspectorPanel().find("[data-testid=show-all-cases]").should("not.exist")
c.getInspectorPanel().find("[data-testid=trash-menu-list]").should("not.be.visible")
c.getInspectorPanel().find("[data-testid=hide-show-menu-list]").should("not.be.visible")
})
}
}
14 changes: 10 additions & 4 deletions v3/cypress/support/elements/table-tile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,14 @@ export const TableTileElements = {
getHideShowButton() {
return c.getInspectorPanel().find("[data-testid=hide-show-button]")
},
getAttributesButton() {
return c.getInspectorPanel().find("[data-testid=table-attributes-button]")
getRulerButton() {
return c.getInspectorPanel().find("[data-testid=ruler-button]")
},
getRulerMenuItem(item: string) {
return cy.get("[data-testid=ruler-menu-list] button").contains(item)
},
selectItemFromRulerMenu(item: string) {
this.getRulerMenuItem(item).click({ force: true })
},
verifyAttributeValues(attributes, values, collectionIndex = 1) {
attributes.forEach(a => {
Expand All @@ -216,8 +222,8 @@ export const TableTileElements = {
})
})
},
moveAttributeToParent(name, moveType) {
cy.dragAttributeToTarget("table", name, moveType)
moveAttributeToParent(name: string, moveType: string, targetNumber?: number) {
cy.dragAttributeToTarget("table", name, moveType, targetNumber)
},
getExpandAllGroupsButton(collectionIndex = 1) {
return this.getCollection(collectionIndex).find("[title=\"expand all groups\"]")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { TCalculatedColumn } from "../case-table-types"
import { EditAttributePropertiesModal } from "./edit-attribute-properties-modal"
import { t } from "../../../utilities/translation/translate"
import { EditFormulaModal } from "./edit-formula-modal"
import { hideAttributeNotification, removeAttributesNotification } from "../../../models/data/data-set-utils"

interface IProps {
column: TCalculatedColumn
Expand All @@ -28,6 +29,7 @@ const AttributeMenuListComp = forwardRef<HTMLDivElement, IProps>(
const rerandomizeDisabled = !attribute?.formula?.isRandomFunctionPresent

const handleMenuItemClick = (menuItem: string) => {
// TODO Don't forget to broadcast notifications as these menu items are implemented!
toast({
title: 'Menu item clicked',
description: `You clicked on ${menuItem} on ${columnName}`,
Expand All @@ -40,6 +42,7 @@ const AttributeMenuListComp = forwardRef<HTMLDivElement, IProps>(
caseMetadata?.applyModelChange(
() => caseMetadata?.setIsHidden(column.key, true),
{
notifications: hideAttributeNotification([column.key], data),
undoStringKey: "DG.Undo.caseTable.hideAttribute",
redoStringKey: "DG.Redo.caseTable.hideAttribute"
}
Expand All @@ -56,6 +59,7 @@ const AttributeMenuListComp = forwardRef<HTMLDivElement, IProps>(
data.applyModelChange(() => {
data.removeAttribute(attrId)
}, {
notifications: removeAttributesNotification([attrId], data),
undoStringKey: "DG.Undo.caseTable.deleteAttribute",
redoStringKey: "DG.Redo.caseTable.deleteAttribute"
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useDataSetContext } from "../../../hooks/use-data-set-context"
import { uniqueName } from "../../../utilities/js-utils"
import { CodapModal } from "../../codap-modal"
import { t } from "../../../utilities/translation/translate"
import { updateAttributesNotification } from "../../../models/data/data-set-utils"

// for use in menus of attribute types
const selectableAttributeTypes = ["none", ...attributeTypes] as const
Expand Down Expand Up @@ -65,6 +66,7 @@ export const EditAttributePropertiesModal = ({ attributeId, isOpen, onClose }: I
attribute.setEditable(editable === "yes")
}
}, {
notifications: updateAttributesNotification([attribute], data),
undoStringKey: "DG.Undo.caseTable.editAttribute",
redoStringKey: "DG.Redo.caseTable.editAttribute"
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { observer } from "mobx-react-lite"
import { CodapModal } from "../../codap-modal"
import { t } from "../../../utilities/translation/translate"
import { useDataSetContext } from "../../../hooks/use-data-set-context"
import { updateAttributesNotification } from "../../../models/data/data-set-utils"

interface IProps {
attributeId: string
Expand All @@ -24,7 +25,16 @@ export const EditFormulaModal = observer(function EditFormulaModal({ attributeId
}, [attribute?.formula?.display])

const applyFormula = () => {
attribute?.setDisplayExpression(formula)
if (attribute) {
dataSet?.applyModelChange(() => {
attribute.setDisplayExpression(formula)
}, {
// TODO Should also broadcast notify component edit formula and notify updateCases notifications
notifications: updateAttributesNotification([attribute], dataSet),
undoStringKey: "DG.Undo.caseTable.editAttributeFormula",
redoStringKey: "DG.Redo.caseTable.createAttribute"
})
}
closeModal()
}

Expand Down
2 changes: 1 addition & 1 deletion v3/src/components/case-table/case-table-inspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const CaseTableInspector = ({ tile, show }: ITileInspectorPanelProps) =>
<HideShowMenuList />
</InspectorMenu>
<InspectorMenu tooltip={t("DG.Inspector.attributes.toolTip")}
icon={<ValuesIcon className="inspector-menu-icon"/>} testId="table-attributes-button">
icon={<ValuesIcon className="inspector-menu-icon"/>} testId="ruler-button">
<RulerMenuList />
</InspectorMenu>
{showInfoModal && <DatasetInfoModal showInfoModal={showInfoModal} setShowInfoModal={setShowInfoModal}/>}
Expand Down
25 changes: 17 additions & 8 deletions v3/src/components/case-table/collection-title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import throttle from "lodash/throttle"
import {useResizeDetector} from "react-resize-detector"
import { observer } from "mobx-react-lite"
import { clsx } from "clsx"
import { uniqueName } from "../../utilities/js-utils"
import { useDataSetContext } from "../../hooks/use-data-set-context"
import { useCollectionContext } from "../../hooks/use-collection-context"
import { t } from "../../utilities/translation/translate"
import AddIcon from "../../assets/icons/icon-add-circle.svg"
import { useCollectionContext } from "../../hooks/use-collection-context"
import { useDataSetContext } from "../../hooks/use-data-set-context"
import { useTileModelContext } from "../../hooks/use-tile-model-context"
import { IAttribute } from "../../models/data/attribute"
import { createAttributesNotification } from "../../models/data/data-set-utils"
import { uniqueName } from "../../utilities/js-utils"
import { t } from "../../utilities/translation/translate"

export const CollectionTitle = observer(function CollectionTitle() {
const data = useDataSetContext()
Expand Down Expand Up @@ -75,10 +77,17 @@ export const CollectionTitle = observer(function CollectionTitle() {
}

const handleAddNewAttribute = () => {
const newAttrName = uniqueName(t("DG.CaseTable.defaultAttrName"),
(aName: string) => !data?.attributes.find(attr => aName === attr.name)
)
data?.addAttribute({ name: newAttrName }, { collection: collectionId })
let attribute: IAttribute | undefined
data?.applyModelChange(() => {
const newAttrName = uniqueName(t("DG.CaseTable.defaultAttrName"),
(aName: string) => !data.attributes.find(attr => aName === attr.name)
)
attribute = data.addAttribute({ name: newAttrName }, { collection: collectionId })
}, {
notifications: () => createAttributesNotification(attribute ? [attribute] : [], data),
undoStringKey: "DG.Undo.caseTable.createAttribute",
redoStringKey: "DG.Redo.caseTable.createAttribute"
})
}

const casesStr = t(caseCount === 1 ? "DG.DataContext.singleCaseName" : "DG.DataContext.pluralCaseName")
Expand Down
6 changes: 5 additions & 1 deletion v3/src/components/case-table/column-header-divider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createPortal } from "react-dom"
import { IAttribute } from "../../models/data/attribute"
import { isCollectionModel } from "../../models/data/collection"
import { IMoveAttributeOptions } from "../../models/data/data-set-types"
import { getCollectionAttrs } from "../../models/data/data-set-utils"
import { getCollectionAttrs, moveAttributeNotification } 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"
Expand Down Expand Up @@ -32,12 +32,14 @@ export const ColumnHeaderDivider = ({ columnKey, cellElt }: IProps) => {
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"
}
Expand All @@ -48,6 +50,7 @@ export const ColumnHeaderDivider = ({ columnKey, cellElt }: IProps) => {
data.applyModelChange(
() => data.moveAttribute(dragAttrId, options),
{
notifications,
undoStringKey: "DG.Undo.dataContext.moveAttribute",
redoStringKey: "DG.Redo.dataContext.moveAttribute"
}
Expand All @@ -59,6 +62,7 @@ export const ColumnHeaderDivider = ({ columnKey, cellElt }: IProps) => {
data.applyModelChange(
() => data.setCollectionForAttribute(dragAttrId, { collection: collection?.id, ...options }),
{
notifications,
undoStringKey: "DG.Undo.dataContext.moveAttribute",
redoStringKey: "DG.Redo.dataContext.moveAttribute"
}
Expand Down
Loading

0 comments on commit 5fa9c1c

Please sign in to comment.