Skip to content

Commit

Permalink
188497515 v3 Plotted Function/Value Formula Editor (#1574)
Browse files Browse the repository at this point in the history
* Separate attribute logic from formula editor.

* Use EditFormulaModal for plotted function and plotted value.
  • Loading branch information
tealefristoe authored and nstclair-cc committed Nov 8, 2024
1 parent 09bf101 commit 9e67584
Show file tree
Hide file tree
Showing 11 changed files with 195 additions and 284 deletions.
21 changes: 14 additions & 7 deletions v3/cypress/e2e/adornments.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,8 @@ context("Graph adornments", () => {
cy.get("[data-testid=adornment-wrapper]").should("have.length", 1)
cy.get("[data-testid=adornment-wrapper]").should("have.class", "visible")
cy.get("[data-testid=plotted-value-control-value]").click()
cy.get("[data-testid=edit-formula-value-form]").find("[data-testid=formula-value-input]").type("10")
cy.get("[data-testid=formula-editor-input] .cm-content").should("be.visible").and("have.focus")
cy.get("[data-testid=formula-editor-input] .cm-content").realType("10")
cy.get("[data-testid=Apply-button]").click()
cy.get("[data-testid=graph-adornments-grid]").find("*[data-testid^=plotted-value]").should("exist")
cy.get("*[data-testid^=plotted-value-line]").should("exist")
Expand All @@ -562,13 +563,17 @@ context("Graph adornments", () => {
cy.get("[data-testid=adornment-wrapper]").should("have.length", 1)
cy.get("[data-testid=adornment-wrapper]").should("have.class", "visible")
cy.get("[data-testid=plotted-value-control-value]").click()
cy.get("[data-testid=edit-formula-value-form]").find("[data-testid=formula-value-input]").type("mean(Sl")
cy.get("[data-testid=formula-editor-input] .cm-content").should("be.visible").and("have.focus")
cy.get("[data-testid=formula-editor-input] .cm-content").realType("mean(Sl")
cy.get("[data-testid=Apply-button]").click()
cy.get("[data-testid=plotted-value-error]").should("exist").should("include.text", "Syntax error")
cy.get("[data-testid=plotted-value-error]").should("exist").should("include.text", "Undefined symbol")
// Error should be cleaned up after the formula is fixed
cy.get("[data-testid=plotted-value-control-value]").click()
cy.get("[data-testid=edit-formula-value-form]").find("[data-testid=formula-value-input]")
.clear().type("mean(Sleep)")
cy.get("[data-testid=formula-editor-input] .cm-content").should("be.visible").and("have.focus")
for (let i = 0; i < 8; i++) {
cy.get("[data-testid=formula-editor-input] .cm-content").clear()
}
cy.get("[data-testid=formula-editor-input] .cm-content").realType("mean(Sleep)")
cy.get("[data-testid=Apply-button]").click()
cy.get("[data-testid=plotted-value-error]").should("not.exist")
})
Expand All @@ -579,14 +584,16 @@ context("Graph adornments", () => {
graph.getInspectorPalette().find("[data-testid=adornment-toggle-otherValues]").click()
graph.getInspectorPalette().find("[data-testid=adornment-checkbox-plotted-value]").click()
cy.get("[data-testid=plotted-value-control-value]").click()
cy.get("[data-testid=edit-formula-value-form]").find("[data-testid=formula-value-input]").type("10")
cy.get("[data-testid=formula-editor-input] .cm-content").should("be.visible").and("have.focus")
cy.get("[data-testid=formula-editor-input] .cm-content").realType("10")
cy.get("[data-testid=Apply-button]").click()
cy.get("[data-testid=graph-adornments-grid]").find("*[data-testid^=plotted-value]").should("exist")
cy.get("*[data-testid^=plotted-value-line]").should("exist")
cy.get("*[data-testid^=plotted-value-cover]").should("exist")
cy.get("*[data-testid^=plotted-value-tip]").should("exist").should("have.text", "value=10")
cy.get("[data-testid=plotted-value-control-value]").click()
cy.get("[data-testid=edit-formula-value-form]").find("[data-testid=formula-value-input]").clear()
cy.get("[data-testid=formula-editor-input] .cm-content").should("be.visible").and("have.focus")
cy.get("[data-testid=formula-editor-input] .cm-content").clear().clear()
cy.get("[data-testid=Apply-button]").click()
cy.get("*[data-testid^=plotted-value-line]").should("not.exist")
cy.get("*[data-testid^=plotted-value-cover]").should("not.exist")
Expand Down
3 changes: 2 additions & 1 deletion v3/cypress/e2e/bivariate-adornments.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ context("Graph adornments", () => {
cy.get("[data-testid=adornment-wrapper]").should("have.length", 1)
cy.get("[data-testid=adornment-wrapper]").should("have.class", "visible")
cy.get("[data-testid=plotted-function-control-value]").click()
cy.get("[data-testid=edit-formula-value-form]").find("[data-testid=formula-value-input]").type("10")
cy.get("[data-testid=formula-editor-input] .cm-content").should("be.visible").and("have.focus")
cy.get("[data-testid=formula-editor-input] .cm-content").realType("10")
cy.get("[data-testid=Apply-button]").click()
cy.get("[data-testid=graph-adornments-grid]").find("*[data-testid^=plotted-function]").should("exist")
cy.get("*[data-testid^=plotted-function-path]").should("exist")
Expand Down
51 changes: 51 additions & 0 deletions v3/src/components/common/edit-attribute-formula-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from "react"
import { observer } from "mobx-react-lite"
import { DataSetContext } from "../../hooks/use-data-set-context"
import { logStringifiedObjectMessage } from "../../lib/log-message"
import { appState } from "../../models/app-state"
import { updateAttributesNotification, updateCasesNotification } from "../../models/data/data-set-notifications"
import { getSharedDataSets } from "../../models/shared/shared-data-utils"
import { uiState } from "../../models/ui-state"
import { t } from "../../utilities/translation/translate"
import { EditFormulaModal } from "./edit-formula-modal"

export const EditAttributeFormulaModal = observer(function EditAttributeFormulaModal() {
const attributeId = uiState.editFormulaAttributeId
const dataSet = getSharedDataSets(appState.document).find(ds => ds.dataSet.attrFromID(attributeId))?.dataSet
const attribute = dataSet?.attrFromID(attributeId)
const value = attribute?.formula?.display

const applyFormula = (formula: string) => {
if (attribute) {
dataSet?.applyModelChange(() => {
attribute.setDisplayExpression(formula)
}, {
// TODO Should also broadcast notify component edit formula notification
notify: [
updateCasesNotification(dataSet),
updateAttributesNotification([attribute], dataSet)
],
undoStringKey: "DG.Undo.caseTable.editAttributeFormula",
redoStringKey: "DG.Redo.caseTable.editAttributeFormula",
log: logStringifiedObjectMessage("Edit attribute formula: %@",
{name: attribute.name, collection: dataSet?.getCollectionForAttribute(attributeId)?.name, formula},
"data")
})
}
}

return (
<DataSetContext.Provider value={dataSet}>
<EditFormulaModal
applyFormula={applyFormula}
formulaPrompt={t("DG.AttrFormView.formulaPrompt")}
isOpen={!!uiState.editFormulaAttributeId}
onClose={() => uiState.setEditFormulaAttributeId()}
titleInput={attribute?.name}
titleLabel={t("DG.AttrFormView.attrNamePrompt")}
titlePlaceholder={t("V3.AttrFormView.attrPlaceholder")}
value={value}
/>
</DataSetContext.Provider>
)
})
4 changes: 4 additions & 0 deletions v3/src/components/common/edit-formula-modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
cursor: default;

.formula-form-control {
.title-label {
width: 140px;
}

input {
max-width: 270px;
}
Expand Down
193 changes: 89 additions & 104 deletions v3/src/components/common/edit-formula-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ import {
import React, { useEffect, useState } from "react"
import { observer } from "mobx-react-lite"
import { clsx } from "clsx"
import { DataSetContext } from "../../hooks/use-data-set-context"
import { logStringifiedObjectMessage } from "../../lib/log-message"
import { appState } from "../../models/app-state"
import { updateAttributesNotification, updateCasesNotification } from "../../models/data/data-set-notifications"
import { getSharedDataSets } from "../../models/shared/shared-data-utils"
import { t } from "../../utilities/translation/translate"
import { FormulaEditor } from "./formula-editor"
import { FormulaEditorContext, useFormulaEditorState } from "./formula-editor-context"
Expand All @@ -19,49 +14,39 @@ import { InsertValuesMenu } from "./formula-insert-values-menu"
import "./edit-formula-modal.scss"

interface IProps {
attributeId: string
applyFormula: (formula: string) => void
formulaPrompt?: string
isOpen: boolean
onClose: () => void
onClose?: () => void
titleInput?: string
titleLabel: string
titlePlaceholder?: string
value?: string
}

export const EditFormulaModal = observer(function EditFormulaModal({ attributeId, isOpen, onClose }: IProps) {
const dataSet = getSharedDataSets(appState.document).find(ds => ds.dataSet.attrFromID(attributeId))?.dataSet
const attribute = dataSet?.attrFromID(attributeId)
export const EditFormulaModal = observer(function EditFormulaModal({
applyFormula, formulaPrompt, isOpen, onClose, titleInput, titleLabel, titlePlaceholder, value
}: IProps) {
const [showValuesMenu, setShowValuesMenu] = useState(false)
const [showFunctionMenu, setShowFunctionMenu] = useState(false)

const formulaEditorState = useFormulaEditorState(attribute?.formula?.display ?? "")
const formulaEditorState = useFormulaEditorState(value ?? "")
const { formula, setFormula } = formulaEditorState


useEffect(() => {
setFormula(attribute?.formula?.display || "")
}, [attribute, attribute?.formula?.display, setFormula])
setFormula(value || "")
}, [value, setFormula])

const applyFormula = () => {
if (attribute) {
dataSet?.applyModelChange(() => {
attribute.setDisplayExpression(formula)
}, {
// TODO Should also broadcast notify component edit formula notification
notify: [
updateCasesNotification(dataSet),
updateAttributesNotification([attribute], dataSet)
],
undoStringKey: "DG.Undo.caseTable.editAttributeFormula",
redoStringKey: "DG.Redo.caseTable.editAttributeFormula",
log: logStringifiedObjectMessage("Edit attribute formula: %@",
{name: attribute.name, collection: dataSet?.getCollectionForAttribute(attributeId)?.name, formula},
"data")
})
}
const handleApplyClick = () => {
applyFormula(formula)
closeModal()
}

const closeModal = () => {
setShowValuesMenu(false)
setShowFunctionMenu(false)
onClose()
setFormula("")
onClose?.()
}

const handleModalWhitespaceClick = () => {
Expand All @@ -87,82 +72,82 @@ export const EditFormulaModal = observer(function EditFormulaModal({ attributeId
onClick: closeModal
}, {
label: t("DG.AttrFormView.applyBtnTitle"),
onClick: applyFormula,
onClick: handleApplyClick,
default: true
}]

return (
<DataSetContext.Provider value={dataSet}>
<FormulaEditorContext.Provider value={formulaEditorState}>
<CodapModal
isOpen={isOpen}
onClose={closeModal}
modalWidth={"400px"}
modalHeight={"180px"}
onClick={handleModalWhitespaceClick}
>
<ModalHeader h="30" className="codap-modal-header" fontSize="md" data-testid="codap-modal-header">
<div className="codap-modal-icon-container" />
<div className="codap-header-title" />
<ModalCloseButton onClick={closeModal} data-testid="modal-close-button" />
</ModalHeader>
<ModalBody className="formula-modal-body" onKeyDown={e => e.stopPropagation()}>
<FormControl display="flex" flexDirection="column" className="formula-form-control">
<FormLabel display="flex" flexDirection="row">{t("DG.AttrFormView.attrNamePrompt")}
<Input
size="xs"
ml={5}
placeholder="attribute"
value={attribute?.name}
data-testid="attr-name-input"
disabled
/>
</FormLabel>
<FormLabel>{t("DG.AttrFormView.formulaPrompt")}
<FormulaEditor />
</FormLabel>
</FormControl>
<Flex flexDirection="row" justifyContent="flex-start">
<Box position="relative">
<Button className={clsx("formula-editor-button", "insert-value", {"menu-open": showValuesMenu})}
size="xs" ml="5" onClick={handleInsertValuesOpen} data-testid="formula-insert-value-button">
{t("DG.AttrFormView.operandMenuTitle")}
</Button>
{showValuesMenu &&
<InsertValuesMenu setShowValuesMenu={setShowValuesMenu} />
}
</Box>
<Box position="relative">
<Button
className={clsx("formula-editor-button", "insert-function", {"menu-open": showFunctionMenu})}
size="xs" ml="5" onClick={handleInsertFunctionsOpen} data-testid="formula-insert-function-button"
<FormulaEditorContext.Provider value={formulaEditorState}>
<CodapModal
isOpen={isOpen}
onClose={closeModal}
modalWidth={"400px"}
modalHeight={"180px"}
onClick={handleModalWhitespaceClick}
>
<ModalHeader h="30" className="codap-modal-header" fontSize="md" data-testid="codap-modal-header">
<div className="codap-modal-icon-container" />
<div className="codap-header-title" />
<ModalCloseButton onClick={closeModal} data-testid="modal-close-button" />
</ModalHeader>
<ModalBody className="formula-modal-body" onKeyDown={e => e.stopPropagation()}>
<FormControl display="flex" flexDirection="column" className="formula-form-control">
<FormLabel display="flex" flexDirection="row">
<span className="title-label">{titleLabel}</span>
<Input
size="xs"
ml={5}
placeholder={titlePlaceholder ?? ""}
value={titleInput ?? ""}
data-testid="attr-name-input"
disabled
/>
</FormLabel>
<FormLabel>
{formulaPrompt ?? t("DG.AttrFormView.formulaPrompt")}
<FormulaEditor />
</FormLabel>
</FormControl>
<Flex flexDirection="row" justifyContent="flex-start">
<Box position="relative">
<Button className={clsx("formula-editor-button", "insert-value", {"menu-open": showValuesMenu})}
size="xs" ml="5" onClick={handleInsertValuesOpen} data-testid="formula-insert-value-button">
{t("DG.AttrFormView.operandMenuTitle")}
</Button>
{showValuesMenu &&
<InsertValuesMenu setShowValuesMenu={setShowValuesMenu} />
}
</Box>
<Box position="relative">
<Button
className={clsx("formula-editor-button", "insert-function", {"menu-open": showFunctionMenu})}
size="xs" ml="5" onClick={handleInsertFunctionsOpen} data-testid="formula-insert-function-button"
>
{t("DG.AttrFormView.functionMenuTitle")}
</Button>
{showFunctionMenu &&
<InsertFunctionMenu setShowFunctionMenu={setShowFunctionMenu} />
}
</Box>
</Flex>
</ModalBody>
<ModalFooter mt="-5" className="formula-modal-footer">
{ footerButtons.map((b, idx) => {
const key = `${idx}-${b.label}`
return (
<Tooltip key={idx} label={b.tooltip} h="20px" fontSize="12px" color="white" openDelay={1000}
placement="bottom" bottom="15px" left="15px" data-testid="modal-tooltip"
>
{t("DG.AttrFormView.functionMenuTitle")}
</Button>
{showFunctionMenu &&
<InsertFunctionMenu setShowFunctionMenu={setShowFunctionMenu} />
}
</Box>
</Flex>
</ModalBody>
<ModalFooter mt="-5" className="formula-modal-footer">
{ footerButtons.map((b, idx) => {
const key = `${idx}-${b.label}`
return (
<Tooltip key={idx} label={b.tooltip} h="20px" fontSize="12px" color="white" openDelay={1000}
placement="bottom" bottom="15px" left="15px" data-testid="modal-tooltip"
>
<Button key={key} size="xs" variant={`${b.default ? "default" : ""}`} ml="5" onClick={b.onClick}
_hover={{backgroundColor: "#72bfca", color: "white"}} data-testid={`${b.label}-button`}>
{b.label}
</Button>
</Tooltip>
)
})
}
</ModalFooter>
</CodapModal>
</FormulaEditorContext.Provider>
</DataSetContext.Provider>
<Button key={key} size="xs" variant={`${b.default ? "default" : ""}`} ml="5" onClick={b.onClick}
_hover={{backgroundColor: "#72bfca", color: "white"}} data-testid={`${b.label}-button`}>
{b.label}
</Button>
</Tooltip>
)
})
}
</ModalFooter>
</CodapModal>
</FormulaEditorContext.Provider>
)
})
9 changes: 2 additions & 7 deletions v3/src/components/container/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import { logMessageWithReplacement, logStringifiedObjectMessage } from "../../li
import { isFreeTileRow } from "../../models/document/free-tile-row"
import { isMosaicTileRow } from "../../models/document/mosaic-tile-row"
import { getSharedModelManager } from "../../models/tiles/tile-environment"
import { uiState } from "../../models/ui-state"
import { urlParams } from "../../utilities/url-params"
import { EditFormulaModal } from "../common/edit-formula-modal"
import { EditAttributeFormulaModal } from "../common/edit-attribute-formula-modal"
import { AttributeDragOverlay } from "../drag-drop/attribute-drag-overlay"
import { PluginAttributeDrag } from "../drag-drop/plugin-attribute-drag"
import { kContainerClass } from "./container-constants"
Expand Down Expand Up @@ -83,11 +82,7 @@ export const Container: React.FC = observer(function Container() {
xOffset={dataInteractiveState.draggingXOffset}
yOffset={dataInteractiveState.draggingYOffset}
/>
<EditFormulaModal
attributeId={uiState.editFormulaAttributeId}
isOpen={!!uiState.editFormulaAttributeId}
onClose={() => uiState.setEditFormulaAttributeId()}
/>
<EditAttributeFormulaModal />
</div>
</DocumentContainerContext.Provider>
)
Expand Down
Loading

0 comments on commit 9e67584

Please sign in to comment.