From 119b070fe3e2c2e1f1d698b7ffcc18f25eb57534 Mon Sep 17 00:00:00 2001 From: Evangeline Ireland Date: Wed, 24 Jul 2024 07:43:40 -0700 Subject: [PATCH] Adds tile list menu to tile list toolshelf button (#1346) * Adds tile list menu to tile list toolshelf button * Fixes bug where all tile show Web Page when there is no data context PR Fixes * Changes Tiles toolshelf button data-testid * Adds cypress test for Tile toolshelf button * toolshelf test linting * Fixes jest tests because of the change in tile content info model * Adds the functions to return the component title from the component registration * Update case card and case table getTitle returned value * Fixes webview icon size in the tiles list menu * Gets getTitle function from component content registration instead of individual components * Removes graph title when there is no data context Fixes broken cypress test * Clean up Case table gets title for tile or from dataset Changing case table title changes title in both dataset and tile model. * Clean up linting errors * More cleanup * Fixes case table/case card title in table menu. Removes code that accesses table tile title directly * Fixes broken Cypress test * chore: code review tweaks * PR Fixes * PR Fix Adds a box shadow on the component when it is focused, or when it is hovered over in the tile list menu * Creates a utitlity function that returns the function for getting the componenet title. PR Fixes * Fix double declaration * Fixes failing cypress test --------- Co-authored-by: Kirk Swenson --- v3/cypress/e2e/graph.spec.ts | 8 +- v3/cypress/e2e/plugin.spec.ts | 2 +- v3/cypress/e2e/toolbar.spec.ts | 49 ++++++++++++- .../support/elements/toolbar-elements.ts | 12 +++ v3/cypress/support/elements/web-view-tile.ts | 1 + .../calculator/calculator-registration.ts | 8 +- .../calculator/calculator-title-bar.tsx | 5 +- .../case-card/case-card-registration.ts | 6 ++ .../case-table-card-title-bar.tsx | 5 +- .../case-table/case-table-registration.ts | 6 ++ v3/src/components/codap-component.scss | 3 + v3/src/components/codap-component.tsx | 3 +- v3/src/components/component-title-bar.tsx | 2 +- .../components/graph-component-title-bar.tsx | 6 +- v3/src/components/graph/graph-registration.ts | 8 +- .../components/map-component-title-bar.tsx | 5 +- v3/src/components/map/map-registration.ts | 8 +- .../components/slider/slider-registration.ts | 12 ++- v3/src/components/slider/slider-title-bar.tsx | 11 +-- v3/src/components/tiles/tile-base-props.ts | 2 +- .../tool-shelf/tiles-list-button.tsx | 73 +++++++++++++++++++ v3/src/components/tool-shelf/tool-shelf.scss | 6 ++ v3/src/components/tool-shelf/tool-shelf.tsx | 4 +- .../web-view/web-view-registration.ts | 6 +- .../web-view/web-view-title-bar.tsx | 5 +- v3/src/index.scss | 2 + .../shared-model-document-manager.test.ts | 3 + v3/src/models/history/tree-manager.test.ts | 3 + v3/src/models/history/undo-store.test.ts | 3 + .../models/shared/shared-data-utils.test.ts | 3 + .../placeholder/placeholder-registration.ts | 3 +- v3/src/models/tiles/tile-content-info.ts | 14 +++- .../tiles/unknown-content-registration.ts | 3 +- v3/src/models/ui-state.ts | 15 ++++ v3/src/test/test-tile-content.ts | 3 +- v3/src/v2/codap-v2-document.ts | 4 +- 36 files changed, 258 insertions(+), 54 deletions(-) create mode 100644 v3/src/components/tool-shelf/tiles-list-button.tsx diff --git a/v3/cypress/e2e/graph.spec.ts b/v3/cypress/e2e/graph.spec.ts index de7904e12d..6c51a83f3a 100644 --- a/v3/cypress/e2e/graph.spec.ts +++ b/v3/cypress/e2e/graph.spec.ts @@ -147,14 +147,14 @@ context("Graph UI", () => { table.getGridCell(2, 2).should("contain", "African Elephant") cy.log("double-clicking the cell") // double-click to initiate editing cell - table.getGridCell(3, 4).dblclick() - table.getGridCell(3, 4).find("input").type("700{enter}") + table.getGridCell(3, 4).click().dblclick() + cy.get("[data-testid=cell-text-editor]").type("700{enter}") table.getGridCell(2, 2).should("contain", "African Elephant") cy.log("double-clicking the cell") // double-click to initiate editing cell - table.getGridCell(3, 5).dblclick() - table.getGridCell(3, 5).find("input").type("300{enter}") + table.getGridCell(3, 5).click().dblclick() + cy.get("[data-testid=cell-text-editor]").type("300{enter}") // get the rescale button c.getComponentTitle("graph").should("have.text", collectionName).click() diff --git a/v3/cypress/e2e/plugin.spec.ts b/v3/cypress/e2e/plugin.spec.ts index 2de57a3b38..92327eac84 100644 --- a/v3/cypress/e2e/plugin.spec.ts +++ b/v3/cypress/e2e/plugin.spec.ts @@ -318,7 +318,7 @@ context("codap plugins", () => { cy.log("Broadcast deleteCollection notifications when deleting the final attribute") cfm.openExampleDocument("Four Seals") cy.wait(2000) - table.getTableTile().should("contain.text", "Data_Set_1") + table.getTableTile().should("contain.text", "Tracks/Measurements") table.deleteAttrbute("species") openAPITester() webView.toggleAPITesterFilter() diff --git a/v3/cypress/e2e/toolbar.spec.ts b/v3/cypress/e2e/toolbar.spec.ts index 79e2406b4c..a18c922cf3 100644 --- a/v3/cypress/e2e/toolbar.spec.ts +++ b/v3/cypress/e2e/toolbar.spec.ts @@ -23,7 +23,8 @@ context("codap toolbar", () => { it("will open a graph", () => { c.getIconFromToolshelf("graph").click() graph.getGraphTile().should("be.visible") - c.getComponentTitle("graph").should("have.text", "New Dataset") + // graphs with no associated data set should not have a title + c.getComponentTitle("graph").should("have.text", "") }) it("will open a map", () => { c.getIconFromToolshelf("map").click() @@ -50,9 +51,7 @@ context("codap toolbar", () => { }) it('will display a webpage', ()=>{ const url='https://www.wikipedia.org' - let deleteUrl = "" - for (let i = 0; i < url.length; i++) deleteUrl += "{backspace}" - const url2=`${deleteUrl}https://en.wikipedia.org/wiki/Concord_Consortium` + const url2='https://en.wikipedia.org/wiki/Concord_Consortium' toolbar.getOptionsButton().click() toolbar.getWebViewButton().click() webView.getUrlModal().should("exist") @@ -66,4 +65,46 @@ context("codap toolbar", () => { cy.wait(1000) webView.getIFrame().find(`.mw-page-title-main`).should("contain.text", "Concord Consortium") }) + it('will show a list of open tiles when there is no data context', ()=>{ + // Don't open a table as this automatically creates a data context + c.getIconFromToolshelf("graph").click() + c.getIconFromToolshelf("map").click() + c.getIconFromToolshelf("slider").click() + c.getIconFromToolshelf("calc").click() + c.getIconFromToolshelf("plugins").click() + toolbar.getPluginSelection().eq(0).click() + //TODO need to add check for Text component + toolbar.getTilesButton().click() + toolbar.getTilesListMenu().should("be.visible") + toolbar.getTilesListMenuItem().should("have.length", 5) + + toolbar.getTilesListMenuItem().eq(0).should("have.text", "") + toolbar.getTilesListMenuIcon().eq(0).should("have.class", "Graph") + toolbar.getTilesListMenuItem().eq(1).should("have.text", "Map") + toolbar.getTilesListMenuIcon().eq(1).should("have.class", "Map") + toolbar.getTilesListMenuItem().eq(2).should("have.text", "v1") + toolbar.getTilesListMenuIcon().eq(2).should("have.class", "CodapSlider") + toolbar.getTilesListMenuItem().eq(3).should("have.text", "Calculator") + toolbar.getTilesListMenuIcon().eq(3).should("have.class", "Calculator") + toolbar.getTilesListMenuItem().eq(4).should("have.text", "Sampler") + toolbar.getTilesListMenuIcon().eq(4).should("have.class", "WebView") + }) + it('will show correct title for a new table', ()=>{ + c.getIconFromToolshelf("table").click() + toolbar.getNewCaseTable().click() + toolbar.getTilesButton().click() + toolbar.getTilesListMenu().should("be.visible") + toolbar.getTilesListMenuItem().should("have.length", 1) + toolbar.getTilesListMenuItem().eq(0).should("have.text", "New Dataset") + toolbar.getTilesListMenuIcon().eq(0).should("have.class", "CaseTable") + }) + it('will show a list of open tiles when there is a data context', ()=>{ + cy.visit("#file=examples:Four%20Seals") + toolbar.getTilesButton().click() + toolbar.getTilesListMenu().should("be.visible") + toolbar.getTilesListMenuItem().should("have.length", 3) + toolbar.getTilesListMenuItem().eq(0).should("have.text", "Tracks/Measurements") + toolbar.getTilesListMenuItem().eq(1).should("have.text", "Measurements") + toolbar.getTilesListMenuItem().eq(2).should("have.text", "Measurements") + }) }) diff --git a/v3/cypress/support/elements/toolbar-elements.ts b/v3/cypress/support/elements/toolbar-elements.ts index 9be07f6dbc..c6fd63d7e1 100644 --- a/v3/cypress/support/elements/toolbar-elements.ts +++ b/v3/cypress/support/elements/toolbar-elements.ts @@ -23,6 +23,18 @@ export const ToolbarElements = { getConfirmDeleteDatasetModal() { return cy.get(`[data-testid=delete-data-set-button-delete]`) }, + getTilesButton() { + return cy.get(`[data-testid=tool-shelf-button-tiles]`) + }, + getTilesListMenu() { + return cy.get(`[data-testid=tiles-list-menu]`) + }, + getTilesListMenuItem() { + return cy.get(`[data-testid=tiles-list-menu-item]`) + }, + getTilesListMenuIcon() { + return cy.get(`[data-testid=tile-list-menu-icon]`) + }, getOptionsButton() { return cy.get(`[data-testid=tool-shelf-button-options]`) }, diff --git a/v3/cypress/support/elements/web-view-tile.ts b/v3/cypress/support/elements/web-view-tile.ts index 0b436664f5..618f31d782 100644 --- a/v3/cypress/support/elements/web-view-tile.ts +++ b/v3/cypress/support/elements/web-view-tile.ts @@ -3,6 +3,7 @@ export const WebViewTileElements = { return cy.get(`.chakra-modal__content`) }, enterUrl(url: string) { + cy.get(`[data-testid=web-view-url-input]`).clear() cy.get(`[data-testid=web-view-url-input]`).type(url) cy.get(`[data-testid=OK-button]`).click() }, diff --git a/v3/src/components/calculator/calculator-registration.ts b/v3/src/components/calculator/calculator-registration.ts index e4946465eb..aac7fdd842 100644 --- a/v3/src/components/calculator/calculator-registration.ts +++ b/v3/src/components/calculator/calculator-registration.ts @@ -1,5 +1,5 @@ import { registerTileComponentInfo } from "../../models/tiles/tile-component-info" -import { registerTileContentInfo } from "../../models/tiles/tile-content-info" +import { ITileLikeModel, registerTileContentInfo } from "../../models/tiles/tile-content-info" import { ITileModelSnapshotIn } from "../../models/tiles/tile-model" import { CalculatorComponent } from "./calculator" import { kCalculatorTileClass, kCalculatorTileType } from "./calculator-defs" @@ -9,6 +9,7 @@ import CalcIcon from '../../assets/icons/icon-calc.svg' import { toV3Id } from "../../utilities/codap-utils" import { registerV2TileImporter } from "../../v2/codap-v2-tile-importers" import { isV2CalculatorComponent } from "../../v2/codap-v2-types" +import { t } from "../../utilities/translation/translate" export const kCalculatorIdPrefix = "CALC" @@ -17,7 +18,10 @@ registerTileContentInfo({ prefix: kCalculatorIdPrefix, modelClass: CalculatorModel, defaultContent: () => ({ type: kCalculatorTileType }), - isSingleton: true + isSingleton: true, + getTitle: (tile: ITileLikeModel) => { + return tile.title || t("DG.DocumentController.calculatorTitle") + } }) registerTileComponentInfo({ diff --git a/v3/src/components/calculator/calculator-title-bar.tsx b/v3/src/components/calculator/calculator-title-bar.tsx index 3e1e6418aa..aa84fe743f 100644 --- a/v3/src/components/calculator/calculator-title-bar.tsx +++ b/v3/src/components/calculator/calculator-title-bar.tsx @@ -2,13 +2,12 @@ import React, { useCallback } from "react" import { observer } from "mobx-react-lite" import { ComponentTitleBar } from "../component-title-bar" import { useDocumentContent } from "../../hooks/use-document-content" -import { t } from "../../utilities/translation/translate" import { ITileTitleBarProps } from "../tiles/tile-base-props" import { kCalculatorTileType } from "./calculator-defs" +import { getTitle } from "../../models/tiles/tile-content-info" export const CalculatorTitleBar = observer(function CalculatorTitleBar({ tile, onCloseTile, ...others }: ITileTitleBarProps) { - const getTitle = () => tile?.title || t("DG.DocumentController.calculatorTitle") const documentContent = useDocumentContent() const closeCalculator = useCallback(() => { documentContent?.applyModelChange(() => { @@ -19,6 +18,6 @@ export const CalculatorTitleBar = }) }, [documentContent]) return ( - + ) }) diff --git a/v3/src/components/case-card/case-card-registration.ts b/v3/src/components/case-card/case-card-registration.ts index 58cb8986ec..bff65051a9 100644 --- a/v3/src/components/case-card/case-card-registration.ts +++ b/v3/src/components/case-card/case-card-registration.ts @@ -5,6 +5,8 @@ import { kCaseCardTileType } from "./case-card-defs" import { CaseCardModel } from "./case-card-model" import { CaseTableCardTitleBar } from "../case-table-card-common/case-table-card-title-bar" import CardIcon from '../../assets/icons/icon-case-card.svg' +import { t } from "../../utilities/translation/translate" +import { getTileDataSet } from "../../models/shared/shared-data-utils" /* import { CaseCardInspector } from "./case-card-inspector" */ @@ -16,6 +18,10 @@ registerTileContentInfo({ prefix: kCaseCardIdPrefix, modelClass: CaseCardModel, defaultContent: () => ({ type: kCaseCardTileType }), + getTitle: (tile) => { + const data = tile.content && getTileDataSet(tile.content) + return data?.title || t("DG.DocumentController.caseTableTitle") + }, hideOnClose: true }) diff --git a/v3/src/components/case-table-card-common/case-table-card-title-bar.tsx b/v3/src/components/case-table-card-common/case-table-card-title-bar.tsx index 3f008242bf..091fa4b61f 100644 --- a/v3/src/components/case-table-card-common/case-table-card-title-bar.tsx +++ b/v3/src/components/case-table-card-common/case-table-card-title-bar.tsx @@ -13,6 +13,7 @@ import { kCaseTableTileType } from "../case-table/case-table-defs" import { ComponentTitleBar } from "../component-title-bar" import { ITileTitleBarProps } from "../tiles/tile-base-props" import { toggleCardTable } from "./case-table-card-utils" +import { getTitle } from "../../models/tiles/tile-content-info" import "./case-table-card-title-bar.scss" @@ -56,8 +57,6 @@ export const CaseTableCardTitleBar = observer(function CaseTableTitleBar({tile, onCloseTile, ...others}: ITileTitleBarProps) { const tileInfo = getTileInfo(tile?.content.type) const data = tile?.content && getTileDataSet(tile?.content) - // title reflects DataSet title - const getTitle = () => data?.title ?? "" const [showSwitchMessage, setShowSwitchMessage] = useState(false) const cardTableToggleRef = useRef(null) const documentContent = useDocumentContent() @@ -112,7 +111,7 @@ export const CaseTableCardTitleBar = const cardOrTableIconClass = tileInfo.iconClass return ( -
({ type: kCaseTableTileType }), + getTitle: (tile) => { + const data = tile.content && getTileDataSet(tile.content) + return data?.title || t("DG.DocumentController.caseTableTitle") + }, hideOnClose: true }) diff --git a/v3/src/components/codap-component.scss b/v3/src/components/codap-component.scss index f7efe038f0..f10be4b415 100644 --- a/v3/src/components/codap-component.scss +++ b/v3/src/components/codap-component.scss @@ -8,6 +8,9 @@ $corner-drag-size: calc($border-drag-width * 2); height: 100%; border-radius: vars.$border-radius-four-corners; // z-index: 20; + &.shadowed { + box-shadow: 0 1px 20px 0 rgba(0, 0, 0, 0.4); + } input { &:focus { diff --git a/v3/src/components/codap-component.tsx b/v3/src/components/codap-component.tsx index d929d5b64f..1b433fa5cb 100644 --- a/v3/src/components/codap-component.tsx +++ b/v3/src/components/codap-component.tsx @@ -42,7 +42,8 @@ export const CodapComponent = observer(function CodapComponent({ if (!info) return null const { TitleBar, Component, tileEltClass, isFixedWidth, isFixedHeight } = info - const classes = clsx("codap-component", tileEltClass, { minimized: isMinimized }) + const classes = clsx("codap-component", tileEltClass, { minimized: isMinimized }, + { shadowed: uiState.isFocusedTile(tile.id) || uiState.isHoveredTile(tile.id) }) return (
tile?.title || data?.name return ( - + ) }) diff --git a/v3/src/components/graph/graph-registration.ts b/v3/src/components/graph/graph-registration.ts index dc7e814866..673baaa6bd 100644 --- a/v3/src/components/graph/graph-registration.ts +++ b/v3/src/components/graph/graph-registration.ts @@ -1,10 +1,10 @@ import { SetRequired } from "type-fest" import { registerTileComponentInfo } from "../../models/tiles/tile-component-info" -import { registerTileContentInfo } from "../../models/tiles/tile-content-info" +import { ITileLikeModel, registerTileContentInfo } from "../../models/tiles/tile-content-info" import { kGraphIdPrefix, kGraphTileClass, kGraphTileType } from "./graph-defs" import { SharedDataSet } from "../../models/shared/shared-data-set" import { getSharedCaseMetadataFromDataset } from "../../models/shared/shared-data-utils" -import { GraphContentModel, IGraphContentModelSnapshot } from "./models/graph-content-model" +import { GraphContentModel, IGraphContentModelSnapshot, isGraphContentModel } from "./models/graph-content-model" import { kGraphDataConfigurationType } from "./models/graph-data-configuration-model" import { kGraphPointLayerType } from "./models/graph-point-layer-model" import { GraphComponentTitleBar } from "./components/graph-component-title-bar" @@ -36,6 +36,10 @@ registerTileContentInfo({ }] } return graphTileSnapshot + }, + getTitle: (tile: ITileLikeModel) => { + const data = isGraphContentModel(tile?.content) ? tile.content.dataset : undefined + return tile.title || data?.title || "" } }) diff --git a/v3/src/components/map/components/map-component-title-bar.tsx b/v3/src/components/map/components/map-component-title-bar.tsx index f7f0d3604c..d5ff58be96 100644 --- a/v3/src/components/map/components/map-component-title-bar.tsx +++ b/v3/src/components/map/components/map-component-title-bar.tsx @@ -1,14 +1,13 @@ import React from "react" -import { t } from "../../../utilities/translation/translate" import { ComponentTitleBar } from "../../component-title-bar" import { observer } from "mobx-react-lite" import { ITileTitleBarProps } from "../../tiles/tile-base-props" +import { getTitle } from "../../../models/tiles/tile-content-info" export const MapComponentTitleBar = observer(function MapComponentTitleBar(props: ITileTitleBarProps) { const {tile, ...others} = props - const getTitle = () => tile?.title || t("DG.DocumentController.mapTitle") return ( - + ) }) diff --git a/v3/src/components/map/map-registration.ts b/v3/src/components/map/map-registration.ts index 8b2ae9c9f4..180303cc46 100644 --- a/v3/src/components/map/map-registration.ts +++ b/v3/src/components/map/map-registration.ts @@ -1,5 +1,5 @@ import { registerTileComponentInfo } from "../../models/tiles/tile-component-info" -import { registerTileContentInfo } from "../../models/tiles/tile-content-info" +import { ITileLikeModel, registerTileContentInfo } from "../../models/tiles/tile-content-info" import {kMapIdPrefix, kMapTileClass, kMapTileType} from "./map-defs" import {kDefaultMapHeight, kDefaultMapWidth} from "./map-types" import MapIcon from "../../assets/icons/icon-map.svg" @@ -9,13 +9,17 @@ import {MapComponent} from "./components/map-component" import {MapInspector} from "./components/map-inspector" import {registerV2TileImporter} from "../../v2/codap-v2-tile-importers" import {v2MapImporter} from "./v2-map-importer" +import { t } from "../../utilities/translation/translate" registerTileContentInfo({ type: kMapTileType, prefix: kMapIdPrefix, modelClass: MapContentModel, - defaultContent: () => createMapContentModel() + defaultContent: () => createMapContentModel(), + getTitle: (tile: ITileLikeModel) => { + return tile.title || t("DG.DocumentController.mapTitle") + } }) registerTileComponentInfo({ diff --git a/v3/src/components/slider/slider-registration.ts b/v3/src/components/slider/slider-registration.ts index 3b02d594f0..27d1201667 100644 --- a/v3/src/components/slider/slider-registration.ts +++ b/v3/src/components/slider/slider-registration.ts @@ -1,6 +1,6 @@ import { SetRequired } from "type-fest" import { registerTileComponentInfo } from "../../models/tiles/tile-component-info" -import { registerTileContentInfo } from "../../models/tiles/tile-content-info" +import { ITileLikeModel, registerTileContentInfo } from "../../models/tiles/tile-content-info" import { getGlobalValueManager } from "../../models/tiles/tile-environment" import { ITileModelSnapshotIn } from "../../models/tiles/tile-model" import { toV3GlobalId, toV3Id } from "../../utilities/codap-utils" @@ -9,11 +9,13 @@ import { isV2SliderComponent } from "../../v2/codap-v2-types" import { SliderComponent } from "./slider-component" import { SliderInspector } from "./slider-inspector" import { kSliderTileType, kSliderTileClass } from "./slider-defs" -import { ISliderSnapshot, SliderModel } from "./slider-model" +import { ISliderSnapshot, SliderModel, isSliderModel } from "./slider-model" import { SliderTitleBar } from "./slider-title-bar" import { AnimationDirections, AnimationModes, kDefaultAnimationDirection, kDefaultAnimationMode } from "./slider-types" import SliderIcon from '../../assets/icons/icon-slider.svg' import { kDefaultSliderName, kDefaultSliderValue } from "./slider-utils" +import { t } from "../../utilities/translation/translate" +import { isAliveSafe } from "../../utilities/mst-utils" export const kSliderIdPrefix = "SLID" @@ -31,6 +33,12 @@ registerTileContentInfo({ type: kSliderTileType, globalValue: globalValue?.id ?? "" } return sliderTileSnap + }, + getTitle: (tile: ITileLikeModel) => { + const { title, content } = tile || {} + const sliderModel = isAliveSafe(content) && isSliderModel(content) ? content : undefined + const { name } = sliderModel || {} + return title || name || t("DG.DocumentController.sliderTitle") } }) diff --git a/v3/src/components/slider/slider-title-bar.tsx b/v3/src/components/slider/slider-title-bar.tsx index 60c43e829a..974f27a01b 100644 --- a/v3/src/components/slider/slider-title-bar.tsx +++ b/v3/src/components/slider/slider-title-bar.tsx @@ -3,20 +3,13 @@ import React, { useCallback } from "react" import { observer } from "mobx-react-lite" import { ComponentTitleBar } from "../component-title-bar" import { isAliveSafe } from "../../utilities/mst-utils" -import { t } from "../../utilities/translation/translate" import { ITileTitleBarProps } from "../tiles/tile-base-props" import { isSliderModel } from "./slider-model" +import { getTitle } from "../../models/tiles/tile-content-info" export const SliderTitleBar = observer(function SliderTitleBar({ tile, onCloseTile, ...others }: ITileTitleBarProps) { const { content } = tile || {} - const getTitle = useCallback(() => { - const { title } = tile || {} - const sliderModel = isAliveSafe(content) && isSliderModel(content) ? content : undefined - const { name } = sliderModel || {} - return title || name || t("DG.DocumentController.sliderTitle") - }, [content, tile]) - const handleCloseTile = useCallback((tileId: string) => { const sliderModel = isAliveSafe(content) && isSliderModel(content) ? content : undefined // when tile is closed by user, destroy the underlying global value as well @@ -25,6 +18,6 @@ export const SliderTitleBar = observer(function SliderTitleBar({ tile, onCloseTi }, [content, onCloseTile]) return ( - + ) }) diff --git a/v3/src/components/tiles/tile-base-props.ts b/v3/src/components/tiles/tile-base-props.ts index af5ea89731..7f916da576 100644 --- a/v3/src/components/tiles/tile-base-props.ts +++ b/v3/src/components/tiles/tile-base-props.ts @@ -8,7 +8,7 @@ export interface ITileBaseProps { export interface ITileTitleBarProps extends ITileBaseProps { // pass accessor function so that only title bar is re-rendered when title changes - getTitle?: () => string | undefined + getTitle?: (tile: ITileModel) => string | undefined children?: ReactNode onHandleTitleBarClick?: (e: React.MouseEvent) => void onHandleTitleChange?: (newValue?: string) => void diff --git a/v3/src/components/tool-shelf/tiles-list-button.tsx b/v3/src/components/tool-shelf/tiles-list-button.tsx new file mode 100644 index 0000000000..d2c9de45c0 --- /dev/null +++ b/v3/src/components/tool-shelf/tiles-list-button.tsx @@ -0,0 +1,73 @@ +import React from "react" +import { observer } from "mobx-react-lite" +import { Menu, MenuButton, MenuItem, MenuList } from "@chakra-ui/react" +import { useDocumentContent } from "../../hooks/use-document-content" +import { uiState } from "../../models/ui-state" +import { isFreeTileLayout } from "../../models/document/free-tile-row" +import { t } from "../../utilities/translation/translate" +import { kRightButtonBackground, ToolShelfButtonTag } from "./tool-shelf-button" +import { getTileComponentIcon } from "../../models/tiles/tile-component-info" +import { getTileContentInfo } from "../../models/tiles/tile-content-info" +import WebViewIcon from "../../assets/icons/icon-media-tool.svg" +import OptionsIcon from "../../assets/icons/icon-options.svg" + +import "./tool-shelf.scss" + +export const TilesListShelfButton = observer(function TilesListShelfButton() { + const documentContent = useDocumentContent() + const tilesArr = documentContent?.tileMap ? Array.from(documentContent.tileMap.values()) : [] + + const handleSelectTile = (tileId: string) => { + uiState.setFocusedTile(tileId) + const tileRow = documentContent?.findRowContainingTile(tileId) + const tileLayout = tileRow?.getTileLayout(tileId) + isFreeTileLayout(tileLayout) && tileLayout.setMinimized(false) + } + + const handleFocus = (tileId: string) => { + uiState.setHoveredTile(tileId) + } + + const handleBlur = (tileId: string) => { + uiState.setHoveredTile("") + } + + return ( + <> + + + + + + + {tilesArr?.map((tile) => { + const tileType = tile.content.type + const _Icon = getTileComponentIcon(tileType) + const Icon = _Icon ?? WebViewIcon + const iconClass = _Icon ? tileType : "WebView" + const tileInfo = getTileContentInfo(tileType) + const title = tileInfo?.getTitle(tile) + return ( + handleSelectTile(tile.id) } + onFocus={()=>handleFocus(tile.id)} // Handle focus similar to pointer over + onBlur={()=>handleBlur(tile.id)} // Handle blur similar to pointer leave + > + + {title} + + ) + })} + + + + ) +}) diff --git a/v3/src/components/tool-shelf/tool-shelf.scss b/v3/src/components/tool-shelf/tool-shelf.scss index 7808b510d6..02bb8c7181 100644 --- a/v3/src/components/tool-shelf/tool-shelf.scss +++ b/v3/src/components/tool-shelf/tool-shelf.scss @@ -151,3 +151,9 @@ $deleteButton: #4B8ADD; } } } + +.tile-list-menu-icon { + height: 15px; + max-width: 20px; + margin-right: 5px; +} diff --git a/v3/src/components/tool-shelf/tool-shelf.tsx b/v3/src/components/tool-shelf/tool-shelf.tsx index e674cf3cb5..f5df41e182 100644 --- a/v3/src/components/tool-shelf/tool-shelf.tsx +++ b/v3/src/components/tool-shelf/tool-shelf.tsx @@ -16,6 +16,7 @@ import { DEBUG_UNDO } from "../../lib/debug" import { IDocumentModel } from "../../models/document/document" import { t } from "../../utilities/translation/translate" import { OptionsShelfButton } from "./options-button" +import { TilesListShelfButton } from "./tiles-list-button" import { PluginsButton } from "./plugins-button" import { kRightButtonBackground, ToolShelfButton, ToolShelfTileButton } from "./tool-shelf-button" @@ -75,7 +76,8 @@ export const ToolShelf = observer(function ToolShelf({ document }: IProps) { { icon: , label: t("DG.ToolButtonData.tileListMenu.title"), - hint: t("DG.ToolButtonData.tileListMenu.toolTip") + hint: t("DG.ToolButtonData.tileListMenu.toolTip"), + button: }, { icon: , diff --git a/v3/src/components/web-view/web-view-registration.ts b/v3/src/components/web-view/web-view-registration.ts index 464ed80bbd..6ac40917c8 100644 --- a/v3/src/components/web-view/web-view-registration.ts +++ b/v3/src/components/web-view/web-view-registration.ts @@ -1,5 +1,5 @@ import { registerTileComponentInfo } from "../../models/tiles/tile-component-info" -import { registerTileContentInfo } from "../../models/tiles/tile-content-info" +import { ITileLikeModel, registerTileContentInfo } from "../../models/tiles/tile-content-info" import { ITileModelSnapshotIn } from "../../models/tiles/tile-model" import { toV3Id } from "../../utilities/codap-utils" import { registerV2TileImporter, V2TileImportArgs } from "../../v2/codap-v2-tile-importers" @@ -10,6 +10,7 @@ import { WebViewComponent } from "./web-view" import { WebViewInspector } from "./web-view-inspector" import { WebViewTitleBar } from "./web-view-title-bar" import { processPluginUrl } from "./web-view-utils" +import { t } from "../../utilities/translation/translate" export const kWebViewIdPrefix = "WEBV" @@ -20,7 +21,8 @@ registerTileContentInfo({ type: kWebViewTileType, prefix: kWebViewIdPrefix, modelClass: WebViewModel, - defaultContent: () => ({ type: kWebViewTileType }) + defaultContent: () => ({ type: kWebViewTileType }), + getTitle: (tile: ITileLikeModel) => tile.title || t("DG.WebView.defaultTitle") }) registerTileComponentInfo({ diff --git a/v3/src/components/web-view/web-view-title-bar.tsx b/v3/src/components/web-view/web-view-title-bar.tsx index 28693d8c4f..9bae0a08b5 100644 --- a/v3/src/components/web-view/web-view-title-bar.tsx +++ b/v3/src/components/web-view/web-view-title-bar.tsx @@ -1,18 +1,17 @@ import { observer } from "mobx-react-lite" import React from "react" -import { t } from "../../utilities/translation/translate" import { ComponentTitleBar } from "../component-title-bar" import { ITileTitleBarProps } from "../tiles/tile-base-props" +import { getTitle } from "../../models/tiles/tile-content-info" import { isWebViewModel } from "./web-view-model" import "./web-view-title-bar.scss" export const WebViewTitleBar = observer(function WebViewTitleBar({ tile, ...others }: ITileTitleBarProps) { - const getTitle = () => tile?.title || t("DG.WebView.defaultTitle") const webView = isWebViewModel(tile?.content) ? tile.content : undefined const children = webView?.isPlugin ?
{webView.version}
: null return ( - + {children} ) diff --git a/v3/src/index.scss b/v3/src/index.scss index b7cb205b12..af80df7216 100644 --- a/v3/src/index.scss +++ b/v3/src/index.scss @@ -14,6 +14,8 @@ body { /* Show outline for keyboard :focus-visible */ .chakra-button { + user-select: none; + &:focus-visible { // chakra uses box-shadow for focus highlight rather than outline box-shadow: var(--chakra-shadows-outline) !important; diff --git a/v3/src/models/document/shared-model-document-manager.test.ts b/v3/src/models/document/shared-model-document-manager.test.ts index dcc3d6fa7d..5cd7aaf60e 100644 --- a/v3/src/models/document/shared-model-document-manager.test.ts +++ b/v3/src/models/document/shared-model-document-manager.test.ts @@ -105,6 +105,9 @@ registerTileContentInfo({ modelClass: TestTile, defaultContent(options) { throw new Error("Function not implemented.") + }, + getTitle() { + return "Test" } }) registerTileComponentInfo({ diff --git a/v3/src/models/history/tree-manager.test.ts b/v3/src/models/history/tree-manager.test.ts index d99fb7fdb2..4b77d96494 100644 --- a/v3/src/models/history/tree-manager.test.ts +++ b/v3/src/models/history/tree-manager.test.ts @@ -80,6 +80,9 @@ registerTileContentInfo({ modelClass: TestTile, defaultContent(options) { return TestTile.create() + }, + getTitle() { + return "Test" } }) registerTileComponentInfo({ diff --git a/v3/src/models/history/undo-store.test.ts b/v3/src/models/history/undo-store.test.ts index ff8574635f..68dd05c33f 100644 --- a/v3/src/models/history/undo-store.test.ts +++ b/v3/src/models/history/undo-store.test.ts @@ -144,6 +144,9 @@ registerTileContentInfo({ modelClass: TestTile, defaultContent(options) { return TestTile.create() + }, + getTitle() { + return "Test" } }) registerTileComponentInfo({ diff --git a/v3/src/models/shared/shared-data-utils.test.ts b/v3/src/models/shared/shared-data-utils.test.ts index 5a4cba86af..5e6c92a368 100644 --- a/v3/src/models/shared/shared-data-utils.test.ts +++ b/v3/src/models/shared/shared-data-utils.test.ts @@ -30,6 +30,9 @@ registerTileContentInfo({ modelClass: TestTileContent, defaultContent(options) { return TestTileContent.create() + }, + getTitle() { + return "Test" } }) diff --git a/v3/src/models/tiles/placeholder/placeholder-registration.ts b/v3/src/models/tiles/placeholder/placeholder-registration.ts index 7eb2616504..105cc26fd2 100644 --- a/v3/src/models/tiles/placeholder/placeholder-registration.ts +++ b/v3/src/models/tiles/placeholder/placeholder-registration.ts @@ -12,7 +12,8 @@ registerTileContentInfo({ type: kPlaceholderTileType, prefix: "PLAC", modelClass: PlaceholderContentModel, - defaultContent: defaultPlaceholderContent + defaultContent: defaultPlaceholderContent, + getTitle: () => "Placeholder" }) registerTileComponentInfo({ diff --git a/v3/src/models/tiles/tile-content-info.ts b/v3/src/models/tiles/tile-content-info.ts index 7e6f8b2f95..4169824336 100644 --- a/v3/src/models/tiles/tile-content-info.ts +++ b/v3/src/models/tiles/tile-content-info.ts @@ -1,8 +1,14 @@ import { ITileMetadataModel, TileMetadataModel } from "./tile-metadata" -import { TileContentModel, ITileContentSnapshotWithType } from "./tile-content" +import { TileContentModel, ITileContentSnapshotWithType, ITileContentModel } from "./tile-content" import { AppConfigModelType } from "../stores/app-config-model" import { ITileEnvironment } from "./tile-environment" +// avoids circular dependency on ITileModel +export interface ITileLikeModel { + title?: string + content: ITileContentModel +} + export interface IDefaultContentOptions { // environment in which the tile will be created env?: ITileEnvironment; @@ -26,6 +32,7 @@ export interface ITileContentInfo { modelClass: typeof TileContentModel; defaultContent: (options?: IDefaultContentOptions) => ITileContentSnapshotWithType; titleBase?: string; + getTitle: (tile: ITileLikeModel) => string | undefined; metadataClass?: typeof TileMetadataModel; isSingleton?: boolean; // Only one instance of a tile is open per document (calculator and guide) hideOnClose?: boolean; @@ -64,6 +71,11 @@ export function getTilePrefixes() { return Object.values(gTileContentInfoMap).map(info => info.prefix) } +export function getTitle(tile?: ITileLikeModel) { + const tileContentInfo = getTileContentInfo(tile?.content.type) + return () => tile ? tileContentInfo?.getTitle?.(tile) : undefined +} + export interface ITileExportOptions { json?: boolean; // default true, but some tiles (e.g. geometry) use their export code to produce other formats includeId?: boolean; diff --git a/v3/src/models/tiles/unknown-content-registration.ts b/v3/src/models/tiles/unknown-content-registration.ts index 63837cb70a..ff1b1af9ba 100644 --- a/v3/src/models/tiles/unknown-content-registration.ts +++ b/v3/src/models/tiles/unknown-content-registration.ts @@ -13,7 +13,8 @@ registerTileContentInfo({ type: kUnknownTileType, prefix: "UNKN", modelClass: UnknownContentModel, - defaultContent + defaultContent, + getTitle: () => "Unknown" }) registerTileComponentInfo({ diff --git a/v3/src/models/ui-state.ts b/v3/src/models/ui-state.ts index 1a649fe764..27eb5ab587 100644 --- a/v3/src/models/ui-state.ts +++ b/v3/src/models/ui-state.ts @@ -10,6 +10,8 @@ export class UIState { // the focused tile is a singleton; in theory there can be multiple selected tiles @observable private focusTileId = "" + @observable + private hoverTileId = "" // rulerState is used by graph inspector to manage the visibility univariate measure groups @observable rulerState: RulerState = { @@ -27,15 +29,28 @@ export class UIState { return this.focusTileId } + get hoveredTile() { + return this.hoverTileId + } + isFocusedTile(tileId?: string) { return this.focusTileId === tileId } + isHoveredTile(tileId?: string) { + return this.hoverTileId === tileId + } + @action setFocusedTile(tileId = "") { this.focusTileId = tileId } + @action + setHoveredTile(tileId = "") { + this.hoverTileId = tileId + } + getRulerStateVisibility(key: RulerStateKey) { return this.rulerState[key] } diff --git a/v3/src/test/test-tile-content.ts b/v3/src/test/test-tile-content.ts index e8d696030b..ae572091bd 100644 --- a/v3/src/test/test-tile-content.ts +++ b/v3/src/test/test-tile-content.ts @@ -19,7 +19,8 @@ registerTileContentInfo({ type: "Test", prefix: "TEST", modelClass: TestTileContent, - defaultContent: () => ({ type: "Test" }) + defaultContent: () => ({ type: "Test" }), + getTitle: () => "Test" }) registerTileComponentInfo({ diff --git a/v3/src/v2/codap-v2-document.ts b/v3/src/v2/codap-v2-document.ts index 77e7be47f7..87ecb3566a 100644 --- a/v3/src/v2/codap-v2-document.ts +++ b/v3/src/v2/codap-v2-document.ts @@ -73,13 +73,13 @@ export class CodapV2Document { registerContexts(contexts?: ICodapV2DataContext[]) { contexts?.forEach(context => { - const { guid, type = "DG.DataContext", document, name = "", collections = [] } = context + const { guid, type = "DG.DataContext", document, name = "", title, collections = [] } = context if (document && this.guidMap.get(document)?.type !== "DG.Document") { console.warn("CodapV2Document.registerContexts: context with invalid document guid:", context.document) } this.guidMap.set(guid, { type, object: context }) const dataSetId = toV3DataSetId(guid) - const sharedDataSet = SharedDataSet.create({ dataSet: { id: dataSetId, name } }) + const sharedDataSet = SharedDataSet.create({ dataSet: { id: dataSetId, name, _title: title } }) this.dataMap.set(guid, sharedDataSet) const metadata = SharedCaseMetadata.create({ data: dataSetId }) this.metadataMap.set(guid, metadata)