From 8b4ca09a1a01d2ea545b2a347a9113ee6754c354 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Tue, 24 Sep 2024 17:54:40 -0400 Subject: [PATCH] feat: case card summary view (PT-188001359) (#1508) * feat: case card summary view --- v3/cypress/e2e/case-card.spec.ts | 96 +++++++++++++++++-- v3/src/components/case-card/card-view.scss | 26 +++++ v3/src/components/case-card/card-view.tsx | 31 +++++- .../components/case-card/case-attr-view.scss | 7 ++ .../components/case-card/case-attr-view.tsx | 66 ++++++++----- .../components/case-card/case-attrs-view.tsx | 17 +++- .../components/case-card/case-card-model.ts | 72 +++++++++++++- v3/src/components/case-card/case-view.tsx | 22 +++-- .../case-tile-common/attribute-header.tsx | 5 +- v3/src/utilities/translation/lang/en-US.json5 | 2 + 10 files changed, 296 insertions(+), 48 deletions(-) diff --git a/v3/cypress/e2e/case-card.spec.ts b/v3/cypress/e2e/case-card.spec.ts index c10c89f30..eeea8291a 100644 --- a/v3/cypress/e2e/case-card.spec.ts +++ b/v3/cypress/e2e/case-card.spec.ts @@ -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() @@ -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") @@ -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() @@ -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}") @@ -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"]') @@ -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"]') diff --git a/v3/src/components/case-card/card-view.scss b/v3/src/components/case-card/card-view.scss index 5abb87ad7..712816018 100644 --- a/v3/src/components/case-card/card-view.scss +++ b/v3/src/components/case-card/card-view.scss @@ -3,6 +3,8 @@ display: inline-block; font-size: 11px; min-width: 350px; + padding-bottom: 50px; + position: relative; width: 100%; height: 100%; @@ -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; diff --git a/v3/src/components/case-card/card-view.tsx b/v3/src/components/case-card/card-view.tsx index 55a4b1c9a..e965c65e2 100644 --- a/v3/src/components/case-card/card-view.tsx +++ b/v3/src/components/case-card/card-view.tsx @@ -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" @@ -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(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 (
@@ -38,6 +53,18 @@ export const CardView = observer(function CardView({onNewCollectionDrop}: CardVi displayedCaseLineage={selectedItemLineage} onNewCollectionDrop={onNewCollectionDrop} /> +
+ +
}
diff --git a/v3/src/components/case-card/case-attr-view.scss b/v3/src/components/case-card/case-attr-view.scss index 10bddcc06..4bc9e7868 100644 --- a/v3/src/components/case-card/case-attr-view.scss +++ b/v3/src/components/case-card/case-attr-view.scss @@ -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; diff --git a/v3/src/components/case-card/case-attr-view.tsx b/v3/src/components/case-card/case-attr-view.tsx index 4b73c9eeb..a7f086c24 100644 --- a/v3/src/components/case-card/case-attr-view.tsx +++ b/v3/src/components/case-card/case-attr-view.tsx @@ -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) @@ -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 }] @@ -54,7 +58,37 @@ export const CaseAttrView = observer(function CaseAttrView (props: ICaseAttrView } else { setEditingValue(displayValue) } - setIsEditing(false) + } + + const renderEditableOrSummaryValue = () => { + if (isCollectionSummarized) { + return ( +
+ {displayValue} +
+ ) + } + + return ( + setIsEditing(true)} + onSubmit={handleSubmit} + submitOnBlur={true} + value={isEditing ? editingValue : `${displayValue}${showUnitWithValue ? ` ${unit}` : ""}`} + > + + + + ) } const customButtonStyle = { @@ -73,31 +107,17 @@ export const CaseAttrView = observer(function CaseAttrView (props: ICaseAttrView customButtonStyle={customButtonStyle} getDividerBounds={getDividerBounds} HeaderDivider={AttributeHeaderDivider} + showUnits={false} onSetHeaderContentElt={onSetContentElt} /> - setIsEditing(true)} - onSubmit={handleSubmit} - submitOnBlur={true} - value={isEditing ? editingValue : displayValue} - > - - - + {renderEditableOrSummaryValue()} ) diff --git a/v3/src/components/case-card/case-attrs-view.tsx b/v3/src/components/case-card/case-attrs-view.tsx index 6eec3311a..5292f6118 100644 --- a/v3/src/components/case-card/case-attrs-view.tsx +++ b/v3/src/components/case-card/case-attrs-view.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useRef, useState } from "react" import { observer } from "mobx-react-lite" +import { clsx } from "clsx" import { IGroupedCase } from "../../models/data/data-set-types" import { CaseAttrView } from "./case-attr-view" import { IValueType } from "../../models/data/attribute" @@ -18,9 +19,8 @@ interface ICaseAttrsViewProps { } function getDividerBounds(containerBounds: DOMRect, cellBounds: DOMRect) { - const kCardCellHeight = 25 return { - top: cellBounds.bottom - containerBounds.top + kCardCellHeight, + top: cellBounds.bottom - containerBounds.top, left: cellBounds.left - containerBounds.left, width: cellBounds.right - cellBounds.left, height: 6 @@ -28,12 +28,16 @@ function getDividerBounds(containerBounds: DOMRect, cellBounds: DOMRect) { } export const CaseAttrsView = observer(function CaseAttrsView({caseItem, collection}: ICaseAttrsViewProps) { - const data = useCaseCardModel()?.data + const cardModel = useCaseCardModel() + const data = cardModel?.data + const displayValues = useCaseCardModel()?.displayValues + const isCollectionSummarized = !!cardModel?.summarizedCollections.find(cid => cid === collection?.id) const contentRef = useRef(null) const [, setCellElt] = useState(null) const values: IValueType[] = collection?.attributes.map(attr => { return attr?.id && data?.getValue(caseItem?.__id__, attr.id) }) ?? [] + const summaryValues = displayValues && collection ? displayValues(collection, caseItem) : [] const handleSetHeaderContentElt = useCallback((contentElt: HTMLDivElement | null) => { contentRef.current = contentElt @@ -42,8 +46,10 @@ export const CaseAttrsView = observer(function CaseAttrsView({caseItem, collecti return _cellElt }, []) + const tableClassName = clsx("case-card-attrs", "fadeIn", {"summary-view": isCollectionSummarized}) + return ( - +
@@ -65,7 +72,7 @@ export const CaseAttrsView = observer(function CaseAttrsView({caseItem, collecti collection={collection} attrId={attr.id} name={attr.name} - value={values[index]} + value={isCollectionSummarized ? summaryValues[index] : values[index]} unit={attr.units} getDividerBounds={getDividerBounds} onSetContentElt={handleSetHeaderContentElt} diff --git a/v3/src/components/case-card/case-card-model.ts b/v3/src/components/case-card/case-card-model.ts index 31d65aa0b..3f2f252d5 100644 --- a/v3/src/components/case-card/case-card-model.ts +++ b/v3/src/components/case-card/case-card-model.ts @@ -5,6 +5,7 @@ import { ITileContentModel, TileContentModel } from "../../models/tiles/tile-con import { kCaseCardTileType } from "./case-card-defs" import { ICollectionModel } from "../../models/data/collection" import { ICaseCreation, IGroupedCase } from "../../models/data/data-set-types" +import { IValueType } from "../../models/data/attribute" export const CaseCardModel = TileContentModel .named("CaseCardModel") @@ -12,6 +13,7 @@ export const CaseCardModel = TileContentModel type: types.optional(types.literal(kCaseCardTileType), kCaseCardTileType), // key is collection id; value is width attributeColumnWidths: types.map(types.number), + summarizedCollections: types.optional(types.array(types.string), []) }) .views(self => ({ get data() { @@ -22,7 +24,7 @@ export const CaseCardModel = TileContentModel }, attributeColumnWidth(collectionId: string) { return self.attributeColumnWidths.get(collectionId) - }, + } })) .views(self => ({ caseLineage(itemId?: string) { @@ -35,6 +37,59 @@ export const CaseCardModel = TileContentModel return parentCaseInfo.childCaseIds .map(childCaseId => self.data?.caseInfoMap.get(childCaseId)?.groupedCase) .filter(groupedCase => !!groupedCase) + }, + displayValues(collection: ICollectionModel, caseItem: IGroupedCase) { + + const getNumericSummary = (numericValues: number[], attrUnits: string): string => { + const minValue = Math.min(...numericValues) + const maxValue = Math.max(...numericValues) + return minValue === maxValue + ? `${minValue}${attrUnits ? ` ${attrUnits}` : ""}` + : `${minValue}-${maxValue}${attrUnits ? ` ${attrUnits}` : ""}` + } + + const getCategoricalSummary = (uniqueValues: Set): string => { + const uniqueValuesArray = Array.from(uniqueValues) + if (uniqueValuesArray.length === 1) { + return `${uniqueValuesArray[0]}` + } else if (uniqueValuesArray.length === 2) { + return `${uniqueValuesArray[0]}, ${uniqueValuesArray[1]}` + } else { + return `${uniqueValuesArray.length} values` + } + } + + if (self.summarizedCollections.includes(collection.id)) { + const summaryMap = collection?.attributes.reduce((acc: Record, attr) => { + if (!attr || !attr.id) return acc + + const selectedCases = self.data?.selection + const casesToUse = selectedCases && selectedCases.size >= 1 + ? Array.from(selectedCases).map((id) => ({ __id__: id })) + : collection.cases + const allValues = casesToUse.map(c => self.data?.getValue(c.__id__, attr.id)) + const uniqueValues = new Set(allValues) + const isNumeric = attr.numValues?.some((v, i) => attr.isNumeric(i)) + let summary = "" + + if (isNumeric) { + const numericValues = attr.numValues?.filter((v, i) => attr.isNumeric(i)) + const attrUnits = attr.units ?? "" // self.data?.attrFromID(attr.id)?.units ?? "" + summary = getNumericSummary(numericValues, attrUnits) + } else { + summary = getCategoricalSummary(uniqueValues) + } + return { ...acc, [attr.id]: summary } + }, {}) + return collection?.attributes.map(attr => attr?.id && summaryMap[attr.id]) ?? [] + } else { + return collection?.attributes.map(attr => attr?.id && self.data?.getValue(caseItem?.__id__, attr.id)) ?? [] + } + } + })) + .actions(self => ({ + setSummarizedCollections(collections: string[]) { + self.summarizedCollections.replace(collections) } })) .actions(self => ({ @@ -64,6 +119,21 @@ export const CaseCardModel = TileContentModel return newCaseId }, + setShowSummary(show: boolean, collectionId?: string) { + if (show) { + self.data?.setSelectedCases([]) + } + + const updatedSummarizedCollections = show + ? collectionId + ? [...self.summarizedCollections, collectionId] + : self.data?.collections.map(c => c.id) ?? [] + : collectionId + ? self.summarizedCollections.filter(cid => cid !== collectionId) + : [] + + self.setSummarizedCollections(updatedSummarizedCollections) + }, updateAfterSharedModelChanges(sharedModel?: ISharedModel) { // TODO }, diff --git a/v3/src/components/case-card/case-view.tsx b/v3/src/components/case-card/case-view.tsx index 5ccc4cf59..2674392ae 100644 --- a/v3/src/components/case-card/case-view.tsx +++ b/v3/src/components/case-card/case-view.tsx @@ -22,7 +22,7 @@ import "./case-view.scss" interface ICaseViewProps { cases: IGroupedCase[] level: number - onSelectCases: (caseIds: string[]) => void + onSelectCases: (caseIds: string[], collection: string) => void displayedCaseLineage?: readonly string[] onNewCollectionDrop: (dataSet: IDataSet, attrId: string, beforeCollectionId: string) => void } @@ -37,8 +37,12 @@ export const CaseView = observer(function CaseView(props: ICaseViewProps) { const {cases, level, onSelectCases, onNewCollectionDrop, displayedCaseLineage = []} = props const cardModel = useCaseCardModel() const data = cardModel?.data + const selectedCases = data?.selection + const selectionContainsCollectionCases = selectedCases && cases.some(c => selectedCases.has(c.__id__)) + const summaryTotal = selectionContainsCollectionCases ? selectedCases.size : cases.length const collectionId = useCollectionContext() const collection = data?.getCollection(collectionId) + const isCollectionSummarized = !!cardModel?.summarizedCollections.includes(collectionId) const initialSelectedCase = collection?.cases.find(c => c.__id__ === displayedCaseLineage[level]) const displayedCase = initialSelectedCase ?? cases[0] const displayedCaseId = displayedCase?.__id__ @@ -48,6 +52,9 @@ export const CaseView = observer(function CaseView(props: ICaseViewProps) { return Math.max(0, cases.findIndex(c => c.__id__ === displayedCaseId)) }, [cases, displayedCaseId]) + const caseIndexText = isCollectionSummarized + ? `${summaryTotal} ${summaryTotal === 1 ? t("DG.DataContext.singleCaseName") : t("DG.DataContext.pluralCaseName")}` + : `${displayedCaseIndex + 1} of ${cases.length}` const prevButtonDisabled = displayedCaseIndex <= 0 const nextButtonDisabled = displayedCaseIndex >= cases.length - 1 @@ -57,11 +64,14 @@ export const CaseView = observer(function CaseView(props: ICaseViewProps) { }, [onNewCollectionDrop]) const handleSelectCase = useCallback((delta: number) => () => { - const newCase = cases[displayedCaseIndex + delta] + const selectedCaseIndex = isCollectionSummarized + ? delta < 0 ? cases.length - 1 : 0 + : displayedCaseIndex + delta + const newCase = cases[selectedCaseIndex] if (!newCase.__id__) return - onSelectCases([newCase.__id__]) - }, [displayedCaseIndex, cases, onSelectCases]) + onSelectCases([newCase.__id__], collectionId) + }, [isCollectionSummarized, cases, displayedCaseIndex, onSelectCases, collectionId]) const handleAddNewCase = () => { if (collection) { @@ -69,7 +79,7 @@ export const CaseView = observer(function CaseView(props: ICaseViewProps) { data?.applyModelChange(() => { const newItemId = cardModel?.addNewCase(cases, collection, displayedCaseId) newCaseId = newItemId && data?.getItemCaseIds(newItemId)[level] - newCaseId && onSelectCases([newCaseId]) + newCaseId && onSelectCases([newCaseId], collectionId) }, { notify: () => { if (newCaseId) { @@ -136,7 +146,7 @@ export const CaseView = observer(function CaseView(props: ICaseViewProps) { - {displayedCaseIndex + 1} of {cases.length} + {caseIndexText}
@@ -51,6 +57,7 @@ export const CaseAttrsView = observer(function CaseAttrsView({caseItem, collecti attributeId={kIndexColumnKey} getDividerBounds={getDividerBounds} HeaderDivider={AttributeHeaderDivider} + showUnits={false} onSetHeaderContentElt={handleSetHeaderContentElt} />