Skip to content

Commit

Permalink
feat: case card summary view (PT-188001359) (#1508)
Browse files Browse the repository at this point in the history
* feat: case card summary view
  • Loading branch information
emcelroy authored Sep 24, 2024
1 parent 91c61d0 commit 8b4ca09
Show file tree
Hide file tree
Showing 10 changed files with 296 additions and 48 deletions.
96 changes: 87 additions & 9 deletions v3/cypress/e2e/case-card.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,66 @@ context("case card", () => {
cy.get('[data-testid="codap-case-table"]').should("exist")
cy.get('[data-testid="case-card-view"]').should("not.exist")
})
it("initially displays a summary view of all cases and whenever 'Summarize Dataset' button is clicked", () => {
cy.get(tableHeaderLeftSelector).click()
cy.get(`${tableHeaderLeftSelector} .card-table-toggle-message`).click()
cy.wait(500)
cy.get('[data-testid="case-card-view-title"]').should("have.text", "Cases")
cy.get('[data-testid="case-card-view-index"]').should("have.text", "27 cases")
cy.get('[data-testid="case-card-attr-name"]').eq(0).should("contain.text", "Mammal")
cy.get('[data-testid="case-card-attr-value"]').eq(0).should("have.text", "27 values")
cy.get('[data-testid="case-card-attr-name"]').eq(1).should("contain.text", "Order")
cy.get('[data-testid="case-card-attr-value"]').eq(1).should("have.text", "12 values")
cy.get('[data-testid="case-card-attr-name"]').eq(2).should("contain.text", "LifeSpan")
cy.get('[data-testid="case-card-attr-value"]').eq(2).should("have.text", "3-80")
cy.get('[data-testid="case-card-attr-name"]').eq(3).should("contain.text", "Height")
cy.get('[data-testid="case-card-attr-value"]').eq(3).should("have.text", "0.1-6.5")
cy.get('[data-testid="case-card-attr-name"]').eq(4).should("contain.text", "Mass")
cy.get('[data-testid="case-card-attr-value"]').eq(4).should("have.text", "0.02-6400")
cy.get('[data-testid="case-card-attr-name"]').eq(5).should("contain.text", "Sleep")
cy.get('[data-testid="case-card-attr-value"]').eq(5).should("have.text", "2-20")
cy.get('[data-testid="case-card-attr-name"]').eq(6).should("contain.text", "Speed")
cy.get('[data-testid="case-card-attr-value"]').eq(6).should("have.text", "1-110")
cy.get('[data-testid="case-card-attr-name"]').eq(7).should("contain.text", "Habitat")
cy.get('[data-testid="case-card-attr-value"]').eq(7).should("have.text", "3 values")
cy.get('[data-testid="case-card-attr-name"]').eq(8).should("contain.text", "Diet")
cy.get('[data-testid="case-card-attr-value"]').eq(8).should("have.text", "3 values")
cy.log("Switch to individual cases view.")
cy.get('[data-testid="summary-view-toggle-button"]').should("have.text", "Browse Individual Cases")
cy.get('[data-testid="summary-view-toggle-button"]').click()
cy.get('[data-testid="case-card-attr-value"]').eq(0).should("have.text", "African Elephant")
cy.get('[data-testid="case-card-attr-value"]').eq(1).should("have.text", "Proboscidae")
cy.get('[data-testid="case-card-attr-value"]').eq(2).should("have.text", "70")
cy.get('[data-testid="case-card-attr-value"]').eq(3).should("have.text", "4")
cy.get('[data-testid="case-card-attr-value"]').eq(4).should("have.text", "6400")
cy.get('[data-testid="case-card-attr-value"]').eq(5).should("have.text", "3")
cy.get('[data-testid="case-card-attr-value"]').eq(6).should("have.text", "40")
cy.get('[data-testid="case-card-attr-value"]').eq(7).should("have.text", "land")
cy.get('[data-testid="case-card-attr-value"]').eq(8).should("have.text", "plants")
cy.get('[data-testid="summary-view-toggle-button"]').should("have.text", "Summarize Dataset")
cy.log("Switch back to summary view.")
cy.get('[data-testid="summary-view-toggle-button"]').click()
cy.get('[data-testid="case-card-attr-value"]').eq(0).should("have.text", "27 values")
cy.get('[data-testid="case-card-attr-value"]').eq(1).should("have.text", "12 values")
cy.get('[data-testid="case-card-attr-value"]').eq(2).should("have.text", "3-80")
cy.get('[data-testid="case-card-attr-value"]').eq(3).should("have.text", "0.1-6.5")
cy.get('[data-testid="case-card-attr-value"]').eq(4).should("have.text", "0.02-6400")
cy.get('[data-testid="case-card-attr-value"]').eq(5).should("have.text", "2-20")
cy.get('[data-testid="case-card-attr-value"]').eq(6).should("have.text", "1-110")
cy.get('[data-testid="case-card-attr-value"]').eq(7).should("have.text", "3 values")
cy.get('[data-testid="case-card-attr-value"]').eq(8).should("have.text", "3 values")
cy.get('[data-testid="summary-view-toggle-button"]').should("have.text", "Browse Individual Cases")
})
it("should not initially display a summary view of all cases if there is a selection", () => {
table.getGridCell(6, 2).should("contain", "Cheetah").click()
cy.get(tableHeaderLeftSelector).click()
cy.get(`${tableHeaderLeftSelector} .card-table-toggle-message`).click()
cy.wait(500)
cy.get('[data-testid="case-card-attr-name"]').eq(0).should("contain.text", "Mammal")
cy.get('[data-testid="case-card-attr-value"]').eq(0).should("have.text", "Cheetah")
cy.get('[data-testid="case-card-attr-value"]').eq(1).should("have.text", "Carnivora")
cy.get('[data-testid="case-card-attr-value"]').eq(2).should("have.text", "14")
})
it("displays cases and allows user to scroll through them", () => {
cy.get(tableHeaderLeftSelector).click()
cy.get(`${tableHeaderLeftSelector} .card-table-toggle-message`).click()
Expand All @@ -36,11 +96,14 @@ context("case card", () => {
cy.get('[data-testid="case-card-view-title"]').should("have.text", "Cases")
cy.get('[data-testid="case-card-view-previous-button"]').should("be.disabled")
cy.get('[data-testid="case-card-view-next-button"]').should("not.be.disabled")
cy.get('[data-testid="case-card-view-index"]').should("have.text", "1 of 27")
cy.get('[data-testid="case-card-view-index"]').should("have.text", "27 cases")
cy.get('[data-testid="case-card-attr-name"]').first().should("contain.text", "Mammal")
cy.get('[data-testid="case-card-attr-value"]').first().should("have.text", "27 values")
cy.get('[data-testid="case-card-attr"]').should("have.length", 9)
cy.get('[data-testid="case-card-attr-name"]').should("have.length", 9)
cy.get('[data-testid="case-card-attr-value"]').should("have.length", 9)
cy.get('[data-testid="case-card-attr-name"]').first().should("contain.text", "Mammal")
cy.get('[data-testid="case-card-view-next-button"]').click()
cy.get('[data-testid="case-card-view-index"]').should("have.text", "1 of 27")
cy.get('[data-testid="case-card-attr-value"]').first().should("have.text", "African Elephant")
cy.get('[data-testid="case-card-view-next-button"]').click()
cy.get('[data-testid="case-card-attr-value"]').first().should("have.text", "Asian Elephant")
Expand All @@ -63,17 +126,31 @@ context("case card", () => {
cy.get('[data-testid="case-card-view-previous-button"]').should("have.length", 2).and("be.disabled")
cy.get('[data-testid="case-card-view-next-button"]').should("have.length", 2).and("not.be.disabled")
cy.get('[data-testid="case-card-view-index"]').should("have.length", 2)
cy.get('[data-testid="case-card-view-index"]').eq(0).should("have.text", "1 of 12")
cy.get('[data-testid="case-card-view-index"]').eq(1).should("have.text", "1 of 2")
cy.get('[data-testid="case-card-view-index"]').eq(0).should("have.text", "12 cases")
cy.get('[data-testid="case-card-view-index"]').eq(1).should("have.text", "2 cases")
cy.get('[data-testid="case-card-attrs"]').should("have.length", 2)
cy.get('[data-testid="case-card-attrs"]').eq(0).find('[data-testid="case-card-attr"]').should("have.length", 1)
cy.get('[data-testid="case-card-attrs"]').eq(0).find('[data-testid="case-card-attr-name"]')
.eq(0).should("contain.text", "Order")
.eq(0).should("contain.text", "Order")
cy.get('[data-testid="case-card-attrs"]').eq(0).find('[data-testid="case-card-attr-value"]')
.eq(0).should("have.text", "Proboscidae")
.eq(0).should("have.text", "12 values")
cy.get('[data-testid="case-card-attrs"]').eq(1).find('[data-testid="case-card-attr"]').should("have.length", 8)
cy.get('[data-testid="case-card-attrs"]').eq(1).find('[data-testid="case-card-attr-value"]')
.eq(0).should("have.text", "African Elephant")
.eq(0).should("have.text", "27 values")
cy.get('[data-testid="case-card-view-next-button"]').eq(0).click()
cy.get('[data-testid="case-card-view-index"]').eq(0).should("have.text", "1 of 12")
cy.get('[data-testid="case-card-attrs"]').eq(0).find('[data-testid="case-card-attr-value"]')
.eq(0).should("have.text", "Proboscidae")
cy.get('[data-testid="case-card-attrs"]').eq(1).find('[data-testid="case-card-attr-value"]')
.eq(0).should("have.text", "African Elephant, Asian Elephant")
cy.get('[data-testid="case-card-view-index"]').eq(1).should("have.text", "2 cases")
cy.get('[data-testid="case-card-view-next-button"]').eq(1).click()
cy.get('[data-testid="case-card-view-index"]').eq(0).should("have.text", "1 of 12")
cy.get('[data-testid="case-card-attrs"]').eq(0).find('[data-testid="case-card-attr-value"]')
.eq(0).should("have.text", "Proboscidae")
cy.get('[data-testid="case-card-view-index"]').eq(1).should("have.text", "1 of 2")
cy.get('[data-testid="case-card-attrs"]').eq(1).find('[data-testid="case-card-attr-value"]')
.eq(0).should("have.text", "African Elephant")
})
it("allows the user to add, edit, and hide attributes with undo/redo", () => {
cy.get(tableHeaderLeftSelector).click()
Expand All @@ -84,6 +161,7 @@ context("case card", () => {
cy.get('[data-testid="case-card-attr-value"]').should("have.length", 9)

cy.log("Add new attribute with undo/redo.")
cy.get('[data-testid="case-card-view-next-button"]').click()
cy.get('[data-testid="add-attribute-button"]').click()
cy.wait(500)
cy.get('[data-testid="column-name-input"]').should("exist").type(" Memory{enter}")
Expand Down Expand Up @@ -164,7 +242,7 @@ context("case card", () => {
cy.get('[data-testid="case-card-view"]').should("have.length", 3)
cy.log("Add new case to 'middle' collection.")
cy.get('[data-testid="case-card-view"]').eq(1).find('[data-testid="case-card-view-index"]')
.eq(0).should("have.text", "1 of 4")
.eq(0).should("have.text", "4 cases")
cy.get('[data-testid="case-card-view"]').eq(1).find('[data-testid="add-case-button"]')
.eq(0).click()
cy.get('[data-testid="case-card-view"]').eq(1).find('[data-testid="case-card-view-index"]')
Expand All @@ -183,7 +261,7 @@ context("case card", () => {
toolbar.getUndoTool().click()
toolbar.getUndoTool().click()
cy.get('[data-testid="case-card-view"]').eq(1).find('[data-testid="case-card-view-index"]')
.eq(0).should("have.text", "1 of 4")
.eq(0).should("have.text", "4 cases")
toolbar.getRedoTool().click()
toolbar.getRedoTool().click()
cy.get('[data-testid="case-card-view"]').eq(1).find('[data-testid="case-card-view-index"]')
Expand Down
26 changes: 26 additions & 0 deletions v3/src/components/case-card/card-view.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
display: inline-block;
font-size: 11px;
min-width: 350px;
padding-bottom: 50px;
position: relative;
width: 100%;
height: 100%;

Expand All @@ -11,6 +13,30 @@
animation: fadeIn 0.25s ease-in forwards;
}

.summary-view-toggle-container {
background: white;
bottom: 0;
height: 50px;
left: 0;
padding: 10px 0;
position: sticky;
width: 100%;
}
.summary-view-toggle-button {
all: unset;
background: #f5fbfc;
border: solid 1.5px #979797;
border-radius: 5px;
color: #0592af;
cursor: pointer;
display: block;
font-size: 10px;
font-weight: bold;
margin: 0 auto;
padding: 6px 8px;
text-align: center;
}

@keyframes fadeIn {
100% {
opacity: 1;
Expand Down
31 changes: 29 additions & 2 deletions v3/src/components/case-card/card-view.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useRef } from "react"
import React, { useEffect, useRef } from "react"
import { observer } from "mobx-react-lite"
import { CollectionContext } from "../../hooks/use-collection-context"
import { AttributeHeaderDividerContext } from "../case-tile-common/use-attribute-header-divider-context"
import { CaseView } from "./case-view"
import { useCaseCardModel } from "./use-case-card-model"
import { IDataSet } from "../../models/data/data-set"
import { t } from "../../utilities/translation/translate"

import "./card-view.scss"

Expand All @@ -16,16 +17,30 @@ export const CardView = observer(function CardView({onNewCollectionDrop}: CardVi
const cardModel = useCaseCardModel()
const data = cardModel?.data
const collections = data?.collections
const areAllCollectionsSummarized = !!collections?.every(c => cardModel?.summarizedCollections.includes(c.id))
const rootCollection = collections?.[0]
const selectedItems = data?.selection
const selectedItemId = selectedItems && Array.from(selectedItems)[0]
const selectedItemLineage = cardModel?.caseLineage(selectedItemId)
const contentRef = useRef<HTMLDivElement>(null)

const handleSelectCases = (caseIds: string[]) => {
const handleSelectCases = (caseIds: string[], collectionId: string) => {
cardModel?.setShowSummary(caseIds.length > 1, collectionId)
data?.setSelectedCases(caseIds)
}

const handleSummaryButtonClick = () => {
cardModel?.setShowSummary(!areAllCollectionsSummarized)
}

// The first time the card is rendered, summarize all collections unless there is a selection.
useEffect(function startWithAllCollectionsSummarized() {
if (data?.selection.size === 0) {
const allCollections = data?.collections.map(c => c.id) ?? []
cardModel?.setSummarizedCollections(allCollections)
}
}, [cardModel, data])

return (
<div ref={contentRef} className="case-card-content" data-testid="case-card-content">
<AttributeHeaderDividerContext.Provider value={contentRef}>
Expand All @@ -38,6 +53,18 @@ export const CardView = observer(function CardView({onNewCollectionDrop}: CardVi
displayedCaseLineage={selectedItemLineage}
onNewCollectionDrop={onNewCollectionDrop}
/>
<div className="summary-view-toggle-container">
<button
className="summary-view-toggle-button"
data-testid="summary-view-toggle-button"
onClick={handleSummaryButtonClick}
>
{ areAllCollectionsSummarized
? t("V3.caseCard.summaryButton.showIndividualCases")
: t("V3.caseCard.summaryButton.showSummary")
}
</button>
</div>
</CollectionContext.Provider>
}
</AttributeHeaderDividerContext.Provider>
Expand Down
7 changes: 7 additions & 0 deletions v3/src/components/case-card/case-attr-view.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@
.case-card-attr-value-text {
white-space: nowrap;

&.static-summary {
font-style: italic;
height: 25px;
padding: 4px;
text-align: left;
}

span {
border-radius: 0;
display: inline-block;
Expand Down
66 changes: 43 additions & 23 deletions v3/src/components/case-card/case-attr-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ interface ICaseAttrViewProps {
}

export const CaseAttrView = observer(function CaseAttrView (props: ICaseAttrViewProps) {
const { caseId, attrId, value, getDividerBounds, onSetContentElt } = props
const data = useCaseCardModel()?.data
const { caseId, collection, attrId, unit, value, getDividerBounds, onSetContentElt } = props
const cardModel = useCaseCardModel()
const data = cardModel?.data
const isCollectionSummarized = !!cardModel?.summarizedCollections.includes(collection.id)
const displayValue = value ? String(value) : ""
const showUnitWithValue = isFiniteNumber(Number(value)) && unit
const [isEditing, setIsEditing] = useState(false)
const [editingValue, setEditingValue] = useState(displayValue)

Expand All @@ -37,11 +40,12 @@ export const CaseAttrView = observer(function CaseAttrView (props: ICaseAttrView
}

const handleCancel = (_previousName?: string) => {
setEditingValue(displayValue)
setIsEditing(false)
setEditingValue(displayValue)
}

const handleSubmit = (newValue?: string) => {
setIsEditing(false)
if (newValue) {
const casesToUpdate: ICase[] = [{ __id__: caseId, [attrId]: newValue }]

Expand All @@ -54,7 +58,37 @@ export const CaseAttrView = observer(function CaseAttrView (props: ICaseAttrView
} else {
setEditingValue(displayValue)
}
setIsEditing(false)
}

const renderEditableOrSummaryValue = () => {
if (isCollectionSummarized) {
return (
<div className="case-card-attr-value-text static-summary">
{displayValue}
</div>
)
}

return (
<Editable
className="case-card-attr-value-text"
isPreviewFocusable={true}
onCancel={handleCancel}
onChange={handleChangeValue}
onEdit={() => setIsEditing(true)}
onSubmit={handleSubmit}
submitOnBlur={true}
value={isEditing ? editingValue : `${displayValue}${showUnitWithValue ? ` ${unit}` : ""}`}
>
<EditablePreview paddingY={0} />
<EditableInput
className="case-card-attr-value-text-editor"
data-testid="case-card-attr-value-text-editor"
paddingY={0}
value={editingValue}
/>
</Editable>
)
}

const customButtonStyle = {
Expand All @@ -73,31 +107,17 @@ export const CaseAttrView = observer(function CaseAttrView (props: ICaseAttrView
customButtonStyle={customButtonStyle}
getDividerBounds={getDividerBounds}
HeaderDivider={AttributeHeaderDivider}
showUnits={false}
onSetHeaderContentElt={onSetContentElt}
/>
</td>
<td
className={clsx("case-card-attr-value", {editing: isEditing, numeric: isFiniteNumber(Number(value))})}
className={clsx("case-card-attr-value",
{editing: isEditing, numeric: !isCollectionSummarized && isFiniteNumber(Number(value))}
)}
data-testid="case-card-attr-value"
>
<Editable
className="case-card-attr-value-text"
isPreviewFocusable={true}
onCancel={handleCancel}
onChange={handleChangeValue}
onEdit={() => setIsEditing(true)}
onSubmit={handleSubmit}
submitOnBlur={true}
value={isEditing ? editingValue : displayValue}
>
<EditablePreview paddingY={0} />
<EditableInput
className="case-card-attr-value-text-editor"
data-testid="case-card-attr-value-text-editor"
paddingY={0}
value={editingValue}
/>
</Editable>
{renderEditableOrSummaryValue()}
</td>
</tr>
)
Expand Down
Loading

0 comments on commit 8b4ca09

Please sign in to comment.