diff --git a/cypress/e2e/functional/document_tests/tiles_copy_test_spec.js b/cypress/e2e/functional/document_tests/tiles_copy_test_spec.js index fab8cbdd32..8b28aafbe8 100644 --- a/cypress/e2e/functional/document_tests/tiles_copy_test_spec.js +++ b/cypress/e2e/functional/document_tests/tiles_copy_test_spec.js @@ -10,6 +10,7 @@ import DataCardToolTile from '../../../support/elements/tile/DataCardToolTile'; import DataflowToolTile from '../../../support/elements/tile/DataflowToolTile'; import SimulatorTile from '../../../support/elements/tile/SimulatorTile'; import DiagramToolTile from '../../../support/elements/tile/DiagramToolTile'; +import XYPlotToolTile from "../../../support/elements/tile/XYPlotToolTile"; const student5 = `${Cypress.config("qaUnitStudent5")}`; const student6 = `${Cypress.config("qaUnitStudent6")}`; @@ -24,22 +25,28 @@ let clueCanvas = new ClueCanvas, dc = new DataCardToolTile, dataflowToolTile = new DataflowToolTile, simulatorTile = new SimulatorTile, - diagramTile = new DiagramToolTile; + diagramTile = new DiagramToolTile, + graphTile = new XYPlotToolTile; let canvas = new Canvas; const imageName = "Image Tile"; const simName = "Test Simulation"; const diagramName = "Test Diagram"; +const categoricalGraphName = "Categorical Graph Test"; +const categoricalGraphCopyName = "Categorical Graph Test 1"; + const studentWorkspace = 'QA 1.1 Solving a Mystery with Proportional Reasoning'; const studentWorkspaceCopyTiles = 'Test Workspace Copy Tiles'; const studentClassWorkCopyTiles = 'Test Class Work Copy Tiles'; -const tiles1 = [{ "name": "table" }, -{ "name": "geometry" }, -{ "name": "drawing" }, -{ "name": "expression" }, -{ "name": "numberline" }, -{ "name": "image" }]; +const tiles1 = [ + { "name": "table" }, + { "name": "geometry" }, + { "name": "drawing" }, + { "name": "expression" }, + { "name": "numberline" }, + { "name": "image" } +]; const tiles2 = [ { "name": "data-card" }, { "name": "dataflow" }, @@ -302,3 +309,46 @@ context('Test copy tiles from one document to other document', function () { }); }); + +context("Test copy tile within a document", function () { + it("Copies a graph tile within a document", function () { + beforeTest(student5); + + // Add table tile and populate it with categorical data. + cy.log("Add table tile with categorical data"); + clueCanvas.addTile("table"); + cy.get(".primary-workspace").within((workspace) => { + tableToolTile.typeInTableCellXY(0, 0, "small"); + tableToolTile.getTableCellXY(0, 0).should("contain", "small"); + tableToolTile.typeInTableCellXY(1, 0, "medium"); + tableToolTile.getTableCellXY(1, 0).should("contain", "medium"); + tableToolTile.typeInTableCellXY(0, 1, "red"); + tableToolTile.getTableCellXY(0, 1).should("contain", "red"); + tableToolTile.typeInTableCellXY(1, 1, "green"); + tableToolTile.getTableCellXY(1, 1).should("contain", "green"); + }); + + // Graph the table data in a new graph tile + cy.get("[data-original-title='Graph It!']").click(); + cy.get("[data-test=link-tile-select]").select("New Graph"); + cy.get(".modal-button").contains("Graph It").click(); + graphTile.getTile().should("have.length", 1); + graphTile.getXYPlotTitle().first().should("contain", "Graph 1"); + graphTile.getXYPlotTitle().first().click(); + cy.get(".primary-workspace .graph-wrapper .editable-tile-title").first().type(categoricalGraphName + "{enter}"); + graphTile.getXYPlotTitle().first().should("contain", categoricalGraphName); + graphTile.getGraphDot().should("have.length", 2).each(($g) => { + cy.wrap($g).should("have.attr", "transform").should("not.be.empty"); + }); + + // Click on new graph tile to select it, then copy it + graphTile.getTile().first().click(); + cy.get("[data-testid=tool-duplicate]").click(); + graphTile.getTile().should("have.length", 2); + graphTile.getXYPlotTitle().eq(1).should("contain", categoricalGraphCopyName); + graphTile.getTile().eq(1).find("g.graph-dot").should("have.length", 2).each(($g) => { + cy.wrap($g).should("have.attr", "transform").should("not.be.empty"); + }); + + }); +}); diff --git a/src/plugins/graph/models/graph-model.ts b/src/plugins/graph/models/graph-model.ts index 411c137f0f..4f09a0f70e 100644 --- a/src/plugins/graph/models/graph-model.ts +++ b/src/plugins/graph/models/graph-model.ts @@ -735,19 +735,18 @@ export const GraphModel = TileContentModel } }, afterAttach() { - if (self.layers.length === 1 && !self.layers[0].config.dataset && !self.layers[0].config.isEmpty) { - // Non-empty DataConfiguration lacking a dataset reference = legacy data needing a one-time fix. - // We can't do that fix until the SharedModelManager is ready, though. - addDisposer(self, reaction( - () => { - return self.tileEnv?.sharedModelManager?.isReady; - }, - (ready) => { - if (!ready) return; - this.setDataConfigurationReferences(); - } - )); - } + // Some shared model references may need to be updated. We can't update them until the SharedModelManager + // is ready, though. + addDisposer(self, reaction( + () => { + return self.tileEnv?.sharedModelManager?.isReady; + }, + (ready) => { + if (!ready) return; + this.initializeSharedModelReferences(); + + }, { fireImmediately: true } + )); // Automatically asign colors to anything that might need them. addDisposer(self, reaction( @@ -765,13 +764,40 @@ export const GraphModel = TileContentModel } )); }, - setDataConfigurationReferences() { - // Updates pre-existing DataConfiguration objects that don't have the now-required references - // for dataset and metadata. We can determine these from the unique shared models these - // legacy tile models should have. + initializeSharedModelReferences() { const smm = getSharedModelManager(self); if (smm && smm.isReady) { const sharedDataSets = smm.getTileSharedModelsByType(self, SharedDataSet); + let sharedMetadata = smm.getTileSharedModelsByType(self, SharedCaseMetadata); + + // If there's a shared dataset without corresponding shared case metadata, create a new shared case + // metadata instance, link it to the dataset, and add it to the tile. This is needed when graph tiles are + // copied since the original graph tile's case metadata is not copied along with the shared dataset. + sharedDataSets.forEach((sds) => { + if (!isSharedDataSet(sds)) return; + const hasLinkedCaseMetadata = sharedMetadata.some((smd) => { + if (isSharedCaseMetadata(smd)) { + return smd.data === sds.dataSet; + } + }); + if (!hasLinkedCaseMetadata) { + const smd = SharedCaseMetadata.create(); + smd.setData(sds.dataSet); + smm.addTileSharedModel(self, smd); + const datasetLayer = self.layers.find((layer) => layer.config.dataset === sds.dataSet); + if (datasetLayer) { + datasetLayer.config.metadata = smd; + } + } + }); + + // Update pre-existing, legacy DataConfiguration objects that don't have the now-required references + // for dataset and metadata. We can determine these from the unique shared models these + // legacy tile models should have. + const legacyGraph = self.layers.length === 1 && !self.layers[0].config.dataset && + !self.layers[0].config.isEmpty; + if (!legacyGraph) return; + if (sharedDataSets.length === 1) { const sds = sharedDataSets[0]; if (isSharedDataSet(sds)) { @@ -779,7 +805,7 @@ export const GraphModel = TileContentModel console.log('Updated legacy document - set dataset reference'); } } - const sharedMetadata = smm.getTileSharedModelsByType(self, SharedCaseMetadata); + sharedMetadata = smm.getTileSharedModelsByType(self, SharedCaseMetadata); if (sharedMetadata.length === 1) { const smd = sharedMetadata[0]; if (isSharedCaseMetadata(smd)) {