diff --git a/src/components/app.tsx b/src/components/app.tsx index d2d4db7..8faf0c2 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -11,10 +11,10 @@ function App() { const {connected, selectedDataSet, dataSets, collections, cases, interactiveState, updateInteractiveState: _updateInteractiveState, init, handleSelectDataSet: _handleSelectDataSet, handleUpdateAttributePosition, - handleAddCollection, handleAddAttribute, handleSetCollections, handleSelectSelf, + handleAddCollection, handleAddAttribute, handleSelectSelf, updateTitle, selectCODAPCases, listenForSelectionChanges, - handleCreateCollectionFromAttribute, handleUpdateCollections - } = useCodapState(); + handleCreateCollectionFromAttribute, handleSetCollections, + handleSortAttribute, editCaseValue } = useCodapState(); useEffect(() => { init(); @@ -102,9 +102,9 @@ function App() { updateInteractiveState={updateInteractiveState} handleShowComponent={handleShowComponent} handleUpdateAttributePosition={handleUpdateAttributePosition} - handleSetCollections={handleSetCollections} handleCreateCollectionFromAttribute={handleCreateCollectionFromAttribute} - handleUpdateCollections={handleUpdateCollections} + editCaseValue={editCaseValue} + handleSortAttribute={handleSortAttribute} /> ); @@ -120,8 +120,8 @@ function App() { updateInteractiveState={updateInteractiveState} handleAddCollection={handleAddCollection} handleAddAttribute={handleAddAttribute} - handleSetCollections={handleSetCollections} handleShowComponent={handleShowComponent} + handleSetCollections={handleSetCollections} /> ); diff --git a/src/components/card-view/card-view.tsx b/src/components/card-view/card-view.tsx index 60c8406..90fbd70 100644 --- a/src/components/card-view/card-view.tsx +++ b/src/components/card-view/card-view.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo } from "react"; +import { observer } from "mobx-react-lite"; import { InteractiveState } from "../../hooks/useCodapState"; import { IDataSet, ICollections, ICaseObjCommon, ICollection } from "../../types"; import { Menu } from "../menu"; @@ -7,7 +8,7 @@ import { CaseView } from "./case-view"; import css from "./card-view.scss"; interface ICardViewProps { - selectedDataSet: any; + selectedDataSet: IDataSet | null; dataSets: IDataSet[]; collections: ICollections; interactiveState: InteractiveState @@ -17,13 +18,14 @@ interface ICardViewProps { codapSelectedCase: ICaseObjCommon|undefined; } -export const CardView = (props: ICardViewProps) => { +export const CardView = observer(function CardView(props: ICardViewProps) { const {collections, dataSets, selectedDataSet, updateTitle, selectCases, codapSelectedCase, handleSelectDataSet} = props; const rootCollection = useMemo(() => { return collections.find((c: ICollection) => !c.parent); - }, [collections]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [collections, collections.length]); const attrs = useMemo(() => { const result: Record = {}; @@ -33,7 +35,8 @@ export const CardView = (props: ICardViewProps) => { }); }); return result; - }, [collections]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [collections, collections.length]); useEffect(() => { if (selectedDataSet?.title) { @@ -88,5 +91,4 @@ export const CardView = (props: ICardViewProps) => { /> ); -}; - +}); diff --git a/src/components/card-view/case-attr-view.tsx b/src/components/card-view/case-attr-view.tsx index f14ede6..1bf205a 100644 --- a/src/components/card-view/case-attr-view.tsx +++ b/src/components/card-view/case-attr-view.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import css from "./card-view.scss"; @@ -8,7 +9,7 @@ interface ICaseAttrViewProps { attr: any; } -export const CaseAttrView = ({name, value, attr}: ICaseAttrViewProps) => { +export const CaseAttrView = observer(function CaseAttrView({name, value, attr}: ICaseAttrViewProps) { const unit = attr.unit ? ` (${attr.unit})` : ""; return ( @@ -17,4 +18,4 @@ export const CaseAttrView = ({name, value, attr}: ICaseAttrViewProps) => { {value} ); -}; +}); diff --git a/src/components/card-view/case-attrs-view.tsx b/src/components/card-view/case-attrs-view.tsx index 8f373ea..f3086e7 100644 --- a/src/components/card-view/case-attrs-view.tsx +++ b/src/components/card-view/case-attrs-view.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import { IProcessedCaseObj } from "../../types"; import { CaseAttrView } from "./case-attr-view"; @@ -9,14 +10,14 @@ interface ICaseAttrsViewProps { attrs: Record; } -export const CaseAttrsView = ({caseItem: {values}, attrs}: ICaseAttrsViewProps) => { - const keys = Object.keys(values); +export const CaseAttrsView = observer(function CaseAttrsView({caseItem: {values}, attrs}: ICaseAttrsViewProps) { + const keys = [...values.keys()]; return ( - {keys.map(key => )} + {keys.map(key => )}
); -}; +}); diff --git a/src/components/card-view/case-view.tsx b/src/components/card-view/case-view.tsx index 4301218..e513a98 100644 --- a/src/components/card-view/case-view.tsx +++ b/src/components/card-view/case-view.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { observer } from "mobx-react-lite"; import { IProcessedCaseObj } from "../../types"; import { CaseAttrsView } from "./case-attrs-view"; import Arrow from "../../assets/arrow.svg"; @@ -22,7 +23,7 @@ interface ICaseViewProps { codapSelectedCaseLineage: number[]; } -export const CaseView = (props: ICaseViewProps) => { +export const CaseView = observer(function CaseView(props: ICaseViewProps) { const {cases, attrs, level, selectCases, codapSelectedCaseLineage} = props; // default to the first case @@ -84,4 +85,4 @@ export const CaseView = (props: ICaseViewProps) => { } ); -}; +}); diff --git a/src/components/draggable-table-tags.tsx b/src/components/draggable-table-tags.tsx index 947bcdb..0f976f3 100644 --- a/src/components/draggable-table-tags.tsx +++ b/src/components/draggable-table-tags.tsx @@ -1,11 +1,11 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; +import { observer } from "mobx-react-lite"; +import { getAttribute, IResult } from "@concord-consortium/codap-plugin-api"; import { useDraggableTableContext, Side } from "../hooks/useDraggableTable"; import { useTableTopScrollTopContext } from "../hooks/useTableScrollTop"; -import { useCodapState } from "../hooks/useCodapState"; -import { getAttribute } from "@concord-consortium/codap-plugin-api"; import { getCollectionById } from "../utils/apiHelpers"; -import { PropsWithChildren } from "../types"; +import { IProcessedCaseObj, PropsWithChildren } from "../types"; import { EditableTableCell } from "./editable-table-cell"; import AddIcon from "../assets/plus-level-1.svg"; @@ -38,265 +38,262 @@ interface DraggagleTableHeaderProps { colSpan?: number; dataSetName: string; dataSetTitle: string; + handleSortAttribute: (dataSetName: string, attributeId: number, isDescending: boolean) => void; + isParent?: boolean; } -export const DraggagleTableHeader: React.FC> = (props) => { - const {collectionId, attrTitle, dataSetName, children} = props; - const {dragOverId, dragSide, handleDragStart, handleDragOver, handleOnDrop, handleDragEnter, - handleDragLeave, handleDragEnd} = useDraggableTableContext(); - const {handleSortAttribute} = useCodapState(); - const {id, style} = getIdAndStyle(collectionId, attrTitle, dragOverId, dragSide); - const headerRef = useRef(null); - const [showDropdownIcon, setShowDropdownIcon] = useState(false); - const [showHeaderMenu, setShowHeaderMenu] = useState(false); - const headerPos = headerRef.current?.getBoundingClientRect(); - const headerMenuRef = useRef(null); - const tableContainer = document.querySelector(".nested-table-nestedTableWrapper"); - - useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - if (headerMenuRef.current && !headerMenuRef.current.contains(e.target as Node)) { - setShowHeaderMenu(false); +export const DraggableTableHeader: React.FC> = + observer(function DraggagleTableHeader(props) { + const {collectionId, attrTitle, dataSetName, children, handleSortAttribute} = props; + const {dragOverId, dragSide, handleDragStart, handleDragOver, handleOnDrop, handleDragEnter, + handleDragLeave, handleDragEnd} = useDraggableTableContext(); + const {id, style} = getIdAndStyle(collectionId, attrTitle, dragOverId, dragSide); + const headerRef = useRef(null); + const [showDropdownIcon, setShowDropdownIcon] = useState(false); + const [showHeaderMenu, setShowHeaderMenu] = useState(false); + const headerPos = headerRef.current?.getBoundingClientRect(); + const headerMenuRef = useRef(null); + const tableContainer = document.querySelector(".nested-table-nestedTableWrapper"); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (headerMenuRef.current && !headerMenuRef.current.contains(e.target as Node)) { + setShowHeaderMenu(false); + } + }; + if (showHeaderMenu) { + document.addEventListener("mousedown", handleClickOutside); + } else { + document.removeEventListener("mousedown", handleClickOutside); } + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [showHeaderMenu, tableContainer]); + + const handleShowHeaderMenu = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setShowHeaderMenu(!showHeaderMenu); }; - if (showHeaderMenu) { - document.addEventListener("mousedown", handleClickOutside); - } else { - document.removeEventListener("mousedown", handleClickOutside); - } - return () => { - document.removeEventListener("mousedown", handleClickOutside); + + const handleSortAttr = async (e: React.ChangeEvent) => { + const isDescending = e.target.value === "desc"; + const collectionName = await getCollectionById(dataSetName, collectionId); + const attribute = (await getAttribute(dataSetName, collectionName, attrTitle)).values; + handleSortAttribute(dataSetName, attribute.id, isDescending); + setShowHeaderMenu(false); }; - }, [showHeaderMenu, tableContainer]); + return ( + <> + setShowDropdownIcon(true)} + onMouseLeave={() => setShowDropdownIcon(false)} + onClick={handleShowHeaderMenu} + > +
+
{children}
+ {showDropdownIcon && +
+ +
+ } +
+ + { showHeaderMenu && tableContainer && headerPos && + createPortal( +
+ +
, + tableContainer + ) + } + + ); +}); + +interface DroppableTableHeaderProps { + collectionId: number; +} - const handleShowHeaderMenu = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - setShowHeaderMenu(!showHeaderMenu); - }; +export const DroppableTableHeader: React.FC> = + observer(function DroppableTableHeader(props) { + const {collectionId, children} = props; + const {dragOverId, handleDragOver, handleOnDrop, handleDragEnter, + handleDragLeave} = useDraggableTableContext(); + const id = `${collectionId}`; + const style = getStyle(id, dragOverId, "left"); - const handleSortAttr = async (e: React.ChangeEvent) => { - const isDescending = e.target.value === "desc"; - const collectionName = await getCollectionById(dataSetName, collectionId); - const attribute = (await getAttribute(dataSetName, collectionName, attrTitle)).values; - handleSortAttribute(dataSetName, attribute.id, isDescending); - setShowHeaderMenu(false); - }; - return ( - <> + return ( setShowDropdownIcon(true)} - onMouseLeave={() => setShowDropdownIcon(false)} - onClick={handleShowHeaderMenu} > -
-
{children}
- {showDropdownIcon && -
- -
- } -
+ {children} - { showHeaderMenu && tableContainer && headerPos && - createPortal( -
- -
, - tableContainer - ) - } - - ); -}; - -interface DroppableTableHeaderProps { - collectionId: number; -} - -export const DroppableTableHeader: React.FC> = (props) => { - const {collectionId, children} = props; - const {dragOverId, handleDragOver, handleOnDrop, handleDragEnter, - handleDragLeave} = useDraggableTableContext(); - - const id = `${collectionId}`; - const style = getStyle(id, dragOverId, "left"); - - return ( - - {children} - - ); -}; + ); +}); interface DraggagleTableDataProps { collectionId: number; attrTitle: string; - caseId: string; + caseObj: IProcessedCaseObj; style?: React.CSSProperties; isParent?: boolean; resizeCounter?: number; parentLevel?: number; selectedDataSetName: string; - handleUpdateCollections: () => void; + editCaseValue: (newValue: string, caseObj: IProcessedCaseObj, attrTitle: string) => Promise; } -export const DraggagleTableData: React.FC> = (props) => { - const {collectionId, attrTitle, children, caseId, isParent, resizeCounter, parentLevel=0, - selectedDataSetName, handleUpdateCollections} = props; - const {dragOverId, dragSide} = useDraggableTableContext(); - const {style} = getIdAndStyle(collectionId, attrTitle, dragOverId, dragSide); - const {tableScrollTop, scrollY} = useTableTopScrollTopContext(); - - const cellRef = useRef(null); - - const cellTextTop = useMemo (() =>{ - if (!cellRef.current || !isParent) { - return 0; - } else { - const {top, bottom, height} = cellRef.current.getBoundingClientRect(); - const stickyHeaders = tableScrollTop === 0; - const stickyHeaderHeight = (kMinNumHeaders + parentLevel) * kCellHeight; - const visibleTop = stickyHeaders ? Math.max(top, stickyHeaderHeight) : top; - const visibleBottom = Math.min(window.innerHeight, bottom); - const availableHeight = Math.abs(visibleBottom - visibleTop); +export const DraggagleTableData: React.FC> = + observer(function DraggagleTableData(props) { + const {collectionId, attrTitle, children, caseObj, isParent, resizeCounter, parentLevel=0, editCaseValue} = props; + const {dragOverId, dragSide} = useDraggableTableContext(); + const {style} = getIdAndStyle(collectionId, attrTitle, dragOverId, dragSide); + const {tableScrollTop, scrollY} = useTableTopScrollTopContext(); - let newTop; + const cellRef = useRef(null); - if (top >= visibleTop && bottom <= visibleBottom) { // the whole cell is visible + const cellTextTop = useMemo (() =>{ + if (!cellRef.current || !isParent) { return 0; - } else if (top < visibleTop && bottom < window.innerHeight) { - // we can see the bottom border of the cell but not the top - const hiddenHeightOfCell = height - availableHeight; - newTop = Math.max(0, (hiddenHeightOfCell - kCellHeight + (availableHeight / 2))); - } else if (top >= visibleTop && bottom > visibleBottom) { - // we can see the top border of the cell but not the bottom - newTop = Math.max(0, ((availableHeight) / 2)); } else { - // we are in the middle of a cell - we can see neither the top nor the bottom border - const hiddenTopPartOfCell = Math.max(0, visibleTop - top); - newTop = Math.max(0, (hiddenTopPartOfCell - kCellHeight + (availableHeight) / 2)); + const {top, bottom, height} = cellRef.current.getBoundingClientRect(); + const stickyHeaders = tableScrollTop === 0; + const stickyHeaderHeight = (kMinNumHeaders + parentLevel) * kCellHeight; + const visibleTop = stickyHeaders ? Math.max(top, stickyHeaderHeight) : top; + const visibleBottom = Math.min(window.innerHeight, bottom); + const availableHeight = Math.abs(visibleBottom - visibleTop); + + let newTop; + + if (top >= visibleTop && bottom <= visibleBottom) { // the whole cell is visible + return 0; + } else if (top < visibleTop && bottom < window.innerHeight) { + // we can see the bottom border of the cell but not the top + const hiddenHeightOfCell = height - availableHeight; + newTop = Math.max(0, (hiddenHeightOfCell - kCellHeight + (availableHeight / 2))); + } else if (top >= visibleTop && bottom > visibleBottom) { + // we can see the top border of the cell but not the bottom + newTop = Math.max(0, ((availableHeight) / 2)); + } else { + // we are in the middle of a cell - we can see neither the top nor the bottom border + const hiddenTopPartOfCell = Math.max(0, visibleTop - top); + newTop = Math.max(0, (hiddenTopPartOfCell - kCellHeight + (availableHeight) / 2)); + } + return newTop; } - return newTop; - } - // resizeCounter is a hack to force rerender of text positioning when window is resized - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tableScrollTop, isParent, scrollY, parentLevel, resizeCounter]); + // resizeCounter is a hack to force rerender of text positioning when window is resized + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tableScrollTop, isParent, scrollY, parentLevel, resizeCounter]); + + const EditableCell = () => { + return ( + + ); + }; - const EditableCell = () => { + const textStyle: React.CSSProperties = {top: cellTextTop}; + if (cellTextTop === 0) { + textStyle.alignContent = "center"; + textStyle.bottom = 0; + } return ( - - {children} - + + {isParent + ? <> + {children} +
+ +
+ + : + } + ); - }; - - const textStyle: React.CSSProperties = {top: cellTextTop}; - if (cellTextTop === 0) { - textStyle.alignContent = "center"; - textStyle.bottom = 0; - } - return ( - - {isParent - ? <> - {children} -
- -
- - : - } - - ); -}; +}); interface DroppableTableDataProps { collectionId: number; style?: React.CSSProperties; } -export const DroppableTableData: React.FC> = (props) => { - const {collectionId, style, children} = props; - const {dragOverId, dragSide} = useDraggableTableContext(); - const dragStyle = getStyle(`${collectionId}`, dragOverId, dragSide); +export const DroppableTableData: React.FC> = + observer(function DroppableTableData(props) { + const {collectionId, style, children} = props; + const {dragOverId, dragSide} = useDraggableTableContext(); + const dragStyle = getStyle(`${collectionId}`, dragOverId, dragSide); - return ( - - {children} - - ); -}; + return ( + + {children} + + ); +}); interface DraggableTableContainerProps { collectionId?: number|string; } -export const DraggableTableContainer: React.FC> = (props) => { - const {collectionId, children} = props; - const {dragging, dragOverId, handleDragOver, handleOnDrop, handleDragEnter, - handleDragLeave} = useDraggableTableContext(); - - - const id = collectionId ? `parent:${collectionId}` : `parent:root`; - const hovering = id === dragOverId; - const style: React.CSSProperties = { - display: dragging ? "table-cell" : "none", - backgroundColor: hovering ? highlightColor : undefined, - }; - - return ( - - - - - - - -
- - {hovering &&
Drop to create new collection
} -
{children}
- ); -}; - +export const DraggableTableContainer: React.FC> = + observer(function DraggableTableContainer(props) { + const {collectionId, children} = props; + const {dragging, dragOverId, handleDragOver, handleOnDrop, handleDragEnter, + handleDragLeave} = useDraggableTableContext(); + const id = collectionId ? `parent:${collectionId}` : `parent:root`; + const hovering = id === dragOverId; + const style: React.CSSProperties = { + display: dragging ? "table-cell" : "none", + backgroundColor: hovering ? highlightColor : undefined, + }; + return ( + + + + + + + +
+ + {hovering &&
Drop to create new collection
} +
{children}
+ ); +}); diff --git a/src/components/editable-table-cell.tsx b/src/components/editable-table-cell.tsx index 1141605..30e5efe 100644 --- a/src/components/editable-table-cell.tsx +++ b/src/components/editable-table-cell.tsx @@ -1,20 +1,20 @@ -import React, { ReactNode, useState } from "react"; +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import { Editable, EditablePreview, EditableInput } from "@chakra-ui/react"; -import { updateCaseById } from "@concord-consortium/codap-plugin-api"; +import { IResult } from "@concord-consortium/codap-plugin-api"; +import { IProcessedCaseObj } from "../types"; import css from "./editable-table-cell.scss"; interface IProps { attrTitle: string; - caseId: string; - children: ReactNode; - handleUpdateCollections: () => void; - selectedDataSetName: string; + case: IProcessedCaseObj; + editCaseValue: (newValue: string, cCase: IProcessedCaseObj, attrTitle: string) => Promise; } -export const EditableTableCell = (props: IProps) => { - const { attrTitle, caseId, children, handleUpdateCollections, selectedDataSetName } = props; - const displayValue = String(children); +export const EditableTableCell = observer(function EditableTableCell(props: IProps) { + const { attrTitle, case: cCase, editCaseValue } = props; + const displayValue = cCase.values.get(attrTitle); const [editingValue, setEditingValue] = useState(displayValue); const [isEditing, setIsEditing] = useState(false); @@ -28,11 +28,14 @@ export const EditableTableCell = (props: IProps) => { }; const handleSubmit = async (newValue: string) => { + if (newValue === displayValue) { + setIsEditing(false); + return; + } + try { - await updateCaseById(selectedDataSetName, caseId, {[attrTitle]: newValue}); - setEditingValue(newValue); + await editCaseValue(newValue, cCase, attrTitle); setIsEditing(false); - handleUpdateCollections(); } catch (e) { console.error("Case not updated: ", e); } @@ -54,4 +57,4 @@ export const EditableTableCell = (props: IProps) => { ); -}; +}); diff --git a/src/components/flat-table.tsx b/src/components/flat-table.tsx index ed9d636..c8c5348 100644 --- a/src/components/flat-table.tsx +++ b/src/components/flat-table.tsx @@ -1,59 +1,80 @@ import React from "react"; -import { CaseValuesWithId, ITableProps } from "../types"; -import { DraggableTableContainer, DraggagleTableHeader } from "./draggable-table-tags"; +import { observer } from "mobx-react-lite"; +import { IResult } from "@concord-consortium/codap-plugin-api"; +import { IProcessedCaseObj, ITableProps } from "../types"; +import { DraggableTableContainer, DraggableTableHeader } from "./draggable-table-tags"; import { getAttrPrecisions, getAttrTypes, getAttrVisibility } from "../utils/utils"; +import { TableCell } from "./table-cell"; import css from "./tables.scss"; interface IFlatProps extends ITableProps { - cases: CaseValuesWithId[] + cases: IProcessedCaseObj[] + editCaseValue: (newValue: string, cCase: IProcessedCaseObj, attrTitle: string) => Promise; + handleSortAttribute: (context: string, attrId: number, isDescending: boolean) => void; } -export const FlatTable = (props: IFlatProps) => { - const {selectedDataSet, collections, collectionClasses, cases, mapCellsFromValues, showHeaders } = props; +export const FlatTable = observer(function FlatTable(props: IFlatProps) { + const {selectedDataSet, collections, collectionClasses, handleSortAttribute, showHeaders, editCaseValue } = props; const collection = collections[0]; const {className} = collectionClasses[0]; const attrVisibilities = getAttrVisibility(collections); const collectionAttrsToUse = collection.attrs.filter(attr => !attrVisibilities[attr.title]); - const titles = collectionAttrsToUse.map(attr => attr.title); const precisions = getAttrPrecisions(collections); const attrTypes = getAttrTypes(collections); - const orderedCases = cases.map(c => { - const orderedCase: CaseValuesWithId = {id: c.id}; - titles.forEach(title => { - orderedCase[title] = c.values[title]; - }); - return orderedCase; - }); return ( - + {showHeaders && - + } {collectionAttrsToUse.map((attr: any) => - {attr.title} - )} + )} - {orderedCases.map((c, index) => { + {collection.cases.map((c, index) => { + const caseValuesKeys = [...c.values.keys()]; + // sort each case's values to match the order of `titles` + caseValuesKeys.sort((a, b) => { + return titles.indexOf(String(a)) - titles.indexOf(String(b)); + }); return ( - {mapCellsFromValues(collection.id, `row-${index}`, c, precisions, attrTypes, attrVisibilities )} + {caseValuesKeys.map((key, i) => { + return ( + + ); + })} ); })} @@ -61,4 +82,4 @@ export const FlatTable = (props: IFlatProps) => {
{selectedDataSet.title}{selectedDataSet.title}
{collections[0].title}{collections[0].title}
); -}; +}); diff --git a/src/components/hierarchy-view/add-buttons.tsx b/src/components/hierarchy-view/add-buttons.tsx index b7877ef..c1c768a 100644 --- a/src/components/hierarchy-view/add-buttons.tsx +++ b/src/components/hierarchy-view/add-buttons.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react-lite"; import { IBoundingBox, ICollection } from "../../types"; import AddIcon from "../../assets/add-icon.svg"; @@ -9,7 +10,7 @@ interface IProps { handleAddAttribute: (coll: ICollection, newAttrName: string) => void } -export const AddAttribute = ({collection, handleAddAttribute}: IProps) => { +export const AddAttribute = observer(function AddAttribute({collection, handleAddAttribute}: IProps) { const [showInput, setShowInput] = useState(false); const [newAttrName, setNewAttrName] = useState("newAttr"); const ref = useRef(null); @@ -72,7 +73,7 @@ export const AddAttribute = ({collection, handleAddAttribute}: IProps) => { ); -}; +}); interface IAddCollectionProps { levelBBox: IBoundingBox, @@ -80,7 +81,8 @@ interface IAddCollectionProps { collections: Array } -export const AddCollection = ({levelBBox, handleAddCollection, collections}: IAddCollectionProps) => { +export const AddCollection = +observer(function AddCollection({levelBBox, handleAddCollection, collections}: IAddCollectionProps) { const {top, left, width} = levelBBox; const style: React.CSSProperties = {left: left + width, top, position: "absolute"}; @@ -131,4 +133,4 @@ export const AddCollection = ({levelBBox, handleAddCollection, collections}: IAd {showTitleBox ? renderTitleBox() : renderAddButon()} ); -}; +}); diff --git a/src/components/hierarchy-view/attr.tsx b/src/components/hierarchy-view/attr.tsx index e999eec..5918f66 100644 --- a/src/components/hierarchy-view/attr.tsx +++ b/src/components/hierarchy-view/attr.tsx @@ -1,10 +1,11 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import css from "./attr.scss"; -export const Attr = ({attr}: {attr: any}) => { +export const Attr = observer(function Attr({attr}: {attr: any}) { return (
{attr.name}
); -}; +}); diff --git a/src/components/hierarchy-view/collection.tsx b/src/components/hierarchy-view/collection.tsx index 679456b..6f13ac0 100644 --- a/src/components/hierarchy-view/collection.tsx +++ b/src/components/hierarchy-view/collection.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react-lite"; import { IBoundingBox, ICollection } from "../../types"; import { SortableContext, rectSwappingStrategy } from "@dnd-kit/sortable"; import { SortableAttr } from "./sortable-attr"; @@ -19,7 +20,7 @@ interface ICollectionProps { const AttrsGap = 10; const CollectionOffset = 15; -export const Collection = (props: ICollectionProps) => { +export const Collection = observer(function Collection(props: ICollectionProps) { const {collection, index, isLast, handleAddAttribute} = props; const style: React.CSSProperties = {marginTop: index * CollectionOffset, gap: AttrsGap}; const levelRef = useRef(null); @@ -52,4 +53,4 @@ export const Collection = (props: ICollectionProps) => { {!isLast && } ); -}; +}); diff --git a/src/components/hierarchy-view/hierarchy.tsx b/src/components/hierarchy-view/hierarchy.tsx index 97f6eb3..7ddcabc 100644 --- a/src/components/hierarchy-view/hierarchy.tsx +++ b/src/components/hierarchy-view/hierarchy.tsx @@ -1,9 +1,10 @@ import React from "react"; +import { observer } from "mobx-react-lite"; +import { DndContext, DragOverlay, DropAnimation, closestCorners, + defaultDropAnimation } from "@dnd-kit/core"; import { InteractiveState } from "../../hooks/useCodapState"; import { useWindowResized } from "../../hooks/useWindowResized"; import { useDragging } from "../../hooks/useDragging"; -import { DndContext, DragOverlay, DropAnimation, closestCorners, - defaultDropAnimation } from "@dnd-kit/core"; import { IDataSet, ICollections, ICollection } from "../../types"; import { Collection } from "./collection"; import { Menu } from "../menu"; @@ -13,7 +14,7 @@ import css from "./hierarchy.scss"; const CollectionGap = 23; interface IProps { - selectedDataSet: any; + selectedDataSet: IDataSet | null; dataSets: IDataSet[]; collections: ICollections; interactiveState: InteractiveState @@ -26,7 +27,7 @@ interface IProps { handleShowComponent: () => void } -export const Hierarchy = (props: IProps) => { +export const Hierarchy = observer(function Hierarchy(props: IProps) { const {selectedDataSet, dataSets, collections, handleSelectDataSet, handleSetCollections, handleUpdateAttributePosition, handleAddCollection, handleAddAttribute, handleShowComponent} = props; @@ -90,6 +91,6 @@ export const Hierarchy = (props: IProps) => { {selectedDataSet && renderHeirarchy()} ); -}; +}); export default Hierarchy; diff --git a/src/components/hierarchy-view/sortable-attr.tsx b/src/components/hierarchy-view/sortable-attr.tsx index c8db51d..0aadbf7 100644 --- a/src/components/hierarchy-view/sortable-attr.tsx +++ b/src/components/hierarchy-view/sortable-attr.tsx @@ -1,9 +1,10 @@ import React from "react"; import { CSS } from "@dnd-kit/utilities"; import { useSortable } from "@dnd-kit/sortable"; +import { observer } from "mobx-react-lite"; import {Attr} from "./attr"; -export const SortableAttr = ({attr}: {attr: any}) => { +export const SortableAttr = observer(function SortableAttr({attr}: {attr: any}) { const {attributes, listeners, setNodeRef, transform, transition, isDragging} = useSortable({id: attr.cid}); const style = { transform: CSS.Transform.toString(transform), @@ -17,4 +18,4 @@ export const SortableAttr = ({attr}: {attr: any}) => { ); -}; +}); diff --git a/src/components/landscape-view.tsx b/src/components/landscape-view.tsx index 3516b9d..4e50b85 100644 --- a/src/components/landscape-view.tsx +++ b/src/components/landscape-view.tsx @@ -1,19 +1,22 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import { ICollection, IProcessedCaseObj, ITableProps } from "../types"; -import { DraggagleTableHeader } from "./draggable-table-tags"; +import { DraggableTableHeader } from "./draggable-table-tags"; import { getAttrPrecisions, getAttrTypes, getAttrVisibility } from "../utils/utils"; import css from "./tables.scss"; -export const LandscapeView = (props: ITableProps) => { +export const LandscapeView = observer(function LandscapeView(props: ITableProps) { const {mapCellsFromValues, mapHeadersFromValues, showHeaders, collectionClasses, - getClassName, selectedDataSet, collections, getValueLength, paddingStyle} = props; + getClassName, selectedDataSet, collections, getValueLength, paddingStyle, handleSortAttribute} = props; const renderNestedTable = (parentColl: ICollection) => { const headers = parentColl.cases.map((caseObj) => caseObj.values); const firstRowValues = parentColl.cases.map(caseObj => { return {...caseObj.values, id: caseObj.id}; }); + + const parentCase = parentColl.cases[0]; const valueCount = getValueLength(firstRowValues); const className = getClassName(parentColl.cases[0]); const precisions = getAttrPrecisions(collections); @@ -31,7 +34,7 @@ export const LandscapeView = (props: ITableProps) => { {firstRowValues.map(values => mapCellsFromValues( - parentColl.id, "first-row", values, precisions, attrTypes, attrVisibilities + parentColl.id, "first-row", parentCase, precisions, attrTypes, attrVisibilities )) } @@ -55,8 +58,7 @@ export const LandscapeView = (props: ITableProps) => { }; const renderColFromCaseObj = (collection: ICollection, caseObj: IProcessedCaseObj, index?: number) => { - const {children, id, values} = caseObj; - const caseValuesWithId = {...values, id}; + const {children, values} = caseObj; const isFirstIndex = index === 0; const precisions = getAttrPrecisions(collections); const attrTypes = getAttrTypes(collections); @@ -78,7 +80,7 @@ export const LandscapeView = (props: ITableProps) => { } {mapCellsFromValues( - collection.id, `row-${index}`, caseValuesWithId, precisions, attrTypes, attrVisibilities + collection.id, `row-${index}`, caseObj, precisions, attrTypes, attrVisibilities ) } @@ -114,15 +116,16 @@ export const LandscapeView = (props: ITableProps) => { - {selectedDataSet.name} - + {renderNestedTable(parentColl[0])} @@ -135,4 +138,4 @@ export const LandscapeView = (props: ITableProps) => { {collections.length && collectionClasses.length && renderTable()} ); -}; +}); diff --git a/src/components/menu.tsx b/src/components/menu.tsx index 58bce98..61262e1 100644 --- a/src/components/menu.tsx +++ b/src/components/menu.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import { IDataSet } from "../types"; import css from "./menu.scss"; @@ -21,7 +22,7 @@ const portrait = "Portrait"; const landscape = "Landscape"; const none = ""; -export const Menu = (props: IProps) => { +export const Menu = observer(function Menu(props: IProps) { const {handleSelectDataSet, dataSets, handleSelectDisplayMode, togglePadding, showHeaders, padding, toggleShowHeaders, displayMode, selectedDataSet, showDisplayMode} = props; @@ -31,9 +32,10 @@ export const Menu = (props: IProps) => {
Select a Dataset:
@@ -41,10 +43,10 @@ export const Menu = (props: IProps) => { {showDisplayMode && handleSelectDisplayMode &&
Display mode: - {displayModes.map((mode) => { return ( - ); @@ -66,4 +68,4 @@ export const Menu = (props: IProps) => { }
); -}; +}); diff --git a/src/components/nested-table.tsx b/src/components/nested-table.tsx index 5858d1c..372ce67 100644 --- a/src/components/nested-table.tsx +++ b/src/components/nested-table.tsx @@ -1,13 +1,14 @@ import React, { useCallback, useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { IResult } from "@concord-consortium/codap-plugin-api"; import { InteractiveState } from "../hooks/useCodapState"; -import { ICollection, IProcessedCaseObj, Values, ICollectionClass, IDataSet, - ICollections, CaseValuesWithId } from "../types"; +import { ICollection, IProcessedCaseObj, Values, ICollectionClass, IDataSet, ICollections } from "../types"; import { PortraitView } from "./portrait-view"; import { Menu } from "./menu"; import { LandscapeView } from "./landscape-view"; import { FlatTable } from "./flat-table"; import { DraggableTableContext, useDraggableTable } from "../hooks/useDraggableTable"; -import { DraggagleTableData, DraggagleTableHeader } from "./draggable-table-tags"; +import { DraggagleTableData, DraggableTableHeader } from "./draggable-table-tags"; import css from "./nested-table.scss"; @@ -16,31 +17,30 @@ const landscape = "Landscape"; const none = ""; interface IProps { - selectedDataSet: any; + selectedDataSet: IDataSet | null; dataSets: IDataSet[]; collections: ICollections; - cases: CaseValuesWithId[]; + cases: IProcessedCaseObj[]; interactiveState: InteractiveState handleSelectDataSet: (e: React.ChangeEvent) => void updateInteractiveState: (update: Partial) => void handleShowComponent: () => void - handleSetCollections: (collections: Array) => void handleUpdateAttributePosition: (collection: ICollection, attrName: string, newPosition: number) => void handleCreateCollectionFromAttribute: (collection: ICollection, attr: any, parent: number|string) => Promise - handleUpdateCollections: () => void + handleSortAttribute: (context: string, attrId: number, isDescending: boolean) => void; + editCaseValue: (newValue: string, caseObj: IProcessedCaseObj, attrTitle: string) => Promise; } -export const NestedTable = (props: IProps) => { +export const NestedTable = observer(function NestedTable(props: IProps) { const {selectedDataSet, dataSets, collections, cases, interactiveState, handleSelectDataSet, updateInteractiveState, handleShowComponent, - handleSetCollections, handleUpdateAttributePosition, - handleCreateCollectionFromAttribute, handleUpdateCollections} = props; - const [collectionClasses, setCollectionClasses] = useState>([]); + handleUpdateAttributePosition, handleCreateCollectionFromAttribute, + handleSortAttribute, editCaseValue} = props; + const [collectionClasses, setCollectionClasses] = useState([]); const [paddingStyle, setPaddingStyle] = useState>({padding: "0px"}); const draggableTable = useDraggableTable({ collections, - handleSetCollections, handleUpdateAttributePosition, handleCreateCollectionFromAttribute }); @@ -57,7 +57,7 @@ export const NestedTable = (props: IProps) => { } else { setCollectionClasses([]); } - }, [collections]); + }, [collections, collections.length]); useEffect(() => { if (!interactiveState.dataSetName) { @@ -93,19 +93,23 @@ export const NestedTable = (props: IProps) => { const mapHeadersFromValues = (collectionId: number, rowKey: string, values: Values, attrVisibilities: Record) => { + if (!selectedDataSet) return null; + + const caseValuesKeys = [...values.keys()]; return ( <> - {(Object.keys(values)).map((key, index) => { - if (!attrVisibilities[key] && (typeof values[key] === "string" || typeof values[key] === "number")) { + {caseValuesKeys.map((key, index) => { + if (!attrVisibilities[key] && (typeof values.get(key) === "string" || typeof values.get(key) === "number")) { return ( - {key} - + ); } })} @@ -113,28 +117,31 @@ export const NestedTable = (props: IProps) => { ); }; - const mapCellsFromValues = (collectionId: number, rowKey: string, aCase: Values, + const mapCellsFromValues = (collectionId: number, rowKey: string, cCase: IProcessedCaseObj, precisions: Record, attrTypes: Record, attrVisibilities: Record, isParent?: boolean, resizeCounter?: number, parentLevel?: number) => { - return Object.keys(aCase).map((key, index) => { + if (!selectedDataSet) return null; + + const aCase = cCase.values; + return [...aCase.keys()].map((key, index) => { if (key === "id") return null; - const isWholeNumber = aCase[key] % 1 === 0; + const isWholeNumber = aCase.get(key) % 1 === 0; const precision = precisions[key]; // Numbers are sometimes passed in from CODAP as a string so we use the attribute type to // determine if it should be parsed as a number. // Numbers that are whole numbers are treated as integers, so we should ignore the precision. // Numeric cells that are empty should be treated as empty strings. const isNumericType = attrTypes[key] === "numeric"; - const hasValue = aCase[key] !== ""; - const parsedValue = parseFloat(aCase[key]); + const hasValue = aCase.get(key) !== ""; + const parsedValue = parseFloat(aCase.get(key)); const isNumber = !isNaN(parsedValue); const hasPrecision = precision !== undefined; - const defaultValue = aCase[key]; - const isNumberType = typeof aCase[key] === "number"; + const defaultValue = aCase.get(key); + const isNumberType = typeof aCase.get(key) === "number"; let val; if (isNumericType && hasValue && isNumber) { - val = isWholeNumber ? parseInt(aCase[key], 10) + val = isWholeNumber ? parseInt(aCase.get(key), 10) : parsedValue.toFixed(hasPrecision ? precision : 2); } else if (!isNumericType && isNumberType && hasValue) { val = defaultValue.toFixed(hasPrecision ? precision : 2); @@ -149,14 +156,14 @@ export const NestedTable = (props: IProps) => { return ( {val} @@ -175,10 +182,13 @@ export const NestedTable = (props: IProps) => { }; const renderTable = () => { + if (!selectedDataSet) return null; + const isNoHierarchy = collections.length === 1; const classesExist = collectionClasses.length > 0; const tableProps = {showHeaders: interactiveState.showHeaders, collectionClasses, collections, selectedDataSet, - getClassName, mapHeadersFromValues, mapCellsFromValues, getValueLength, paddingStyle, handleUpdateCollections}; + getClassName, mapHeadersFromValues, mapCellsFromValues, getValueLength, paddingStyle, editCaseValue, + handleSortAttribute}; const flatProps = {...tableProps, cases}; if (isNoHierarchy && classesExist) { return ; @@ -193,7 +203,7 @@ export const NestedTable = (props: IProps) => { } }; - const showDisplayMode = collections.length > 1 && selectedDataSet; + const showDisplayMode = collections.length > 1 && !!selectedDataSet; return (
@@ -214,6 +224,6 @@ export const NestedTable = (props: IProps) => {
); -}; +}); export default NestedTable; diff --git a/src/components/portrait-view.tsx b/src/components/portrait-view.tsx index b13a8c0..2aa6595 100644 --- a/src/components/portrait-view.tsx +++ b/src/components/portrait-view.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; +import { observer } from "mobx-react-lite"; import { ICollection, IProcessedCaseObj, ITableProps } from "../types"; import { DraggableTableContainer, DroppableTableData, DroppableTableHeader } from "./draggable-table-tags"; import { TableScrollTopContext, useTableScrollTop } from "../hooks/useTableScrollTop"; @@ -13,16 +14,15 @@ export type PortraitViewRowProps = {collectionId: number, caseObj: IProcessedCas isParent: boolean, resizeCounter: number, parentLevel?: number} & ITableProps; -export const PortraitViewRow = (props: PortraitViewRowProps) => { +export const PortraitViewRow = observer(function PortraitViewRow(props: PortraitViewRowProps) { const {paddingStyle, mapCellsFromValues, mapHeadersFromValues, showHeaders, precisions, attrTypes, attrVisibilities, getClassName, collectionId, caseObj, index, isParent, resizeCounter, parentLevel} = props; - const {children, id, values} = caseObj; - const caseValuesWithId = {...values, id}; + const {children, values} = caseObj; if (!children.length) { return (
- {mapCellsFromValues(collectionId, `row-${index}`, caseValuesWithId, precisions, attrTypes, attrVisibilities)} + {mapCellsFromValues(collectionId, `row-${index}`, caseObj, precisions, attrTypes, attrVisibilities)} ); } else { @@ -38,7 +38,7 @@ export const PortraitViewRow = (props: PortraitViewRowProps) => { } {mapCellsFromValues( - collectionId, `parent-row-${index}`, caseValuesWithId, precisions, attrTypes, attrVisibilities, + collectionId, `parent-row-${index}`, caseObj, precisions, attrTypes, attrVisibilities, isParent, resizeCounter, parentLevel )} @@ -76,9 +76,9 @@ export const PortraitViewRow = (props: PortraitViewRowProps) => { ); } -}; +}); -export const PortraitView = (props: ITableProps) => { +export const PortraitView = observer(function PortraitView(props: ITableProps) { const {collectionClasses, selectedDataSet, collections, getValueLength} = props; const tableRef = useRef(null); const tableScrollTop = useTableScrollTop(tableRef); @@ -92,18 +92,17 @@ export const PortraitView = (props: ITableProps) => { return t; }, []); - useEffect(() => { const handleIntersection = (entries: IntersectionObserverEntry[], o: any) => { setResizeCounter((prevState) => prevState + 1); }; - const observer = new IntersectionObserver(handleIntersection, {threshold: thresh}); + const intersectionObserver = new IntersectionObserver(handleIntersection, {threshold: thresh}); document.querySelectorAll(`.parent-row`).forEach((row) => { - observer.observe(row); + intersectionObserver.observe(row); }); return () => { document.querySelectorAll(`.parent-row`).forEach((row) => { - observer.unobserve(row); + intersectionObserver.unobserve(row); }); }; @@ -156,4 +155,4 @@ export const PortraitView = (props: ITableProps) => { ); -}; +}); diff --git a/src/components/table-cell.tsx b/src/components/table-cell.tsx new file mode 100644 index 0000000..c94b16f --- /dev/null +++ b/src/components/table-cell.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { observer } from "mobx-react-lite"; +import { IResult } from "@concord-consortium/codap-plugin-api"; +import { DraggagleTableData } from "./draggable-table-tags"; +import { IProcessedCaseObj } from "../types"; + +interface IProps { + key: string; + index: number; + collectionId: number; + rowKey: string; + caseObj: IProcessedCaseObj; + attributeName: string; + cellValue: any; + precision: number; + attrType?: string|null; + isHidden: boolean; + isParent?: boolean; + selectedDataSet: string; + parentLevel?: number; + editCaseValue: (newValue: string, caseObj: IProcessedCaseObj, attrTitle: string) => Promise; +} + +export const TableCell: React.FC = observer(function TableCell(props) { + const { attributeName, cellValue, precision, attrType, isHidden, isParent, selectedDataSet, + collectionId, rowKey, caseObj, parentLevel, index, editCaseValue } = props; + + if (attributeName === "id" || (typeof cellValue !== "string" && typeof cellValue !== "number") || isHidden ) { + return null; + } + + let displayValue: string|number; + const isNumericType= attrType === "numeric"; + const hasValue= cellValue !== ""; + const parsedValue: number = typeof cellValue === "string" ? parseFloat(cellValue) : NaN; + const isNumber= !isNaN(parsedValue); + const hasPrecision= precision !== undefined; + const defaultValue: string | number = cellValue; + const isNumberType= typeof cellValue === "number"; + + if (isNumericType && hasValue && isNumber) { + const cellValAsNumber = Number(cellValue); + const isWholeNumber: boolean = cellValAsNumber % 1 === 0; + displayValue = isWholeNumber + ? parseInt(cellValue as string, 10) + : parsedValue.toFixed(hasPrecision ? precision : 2); + } else if (!isNumericType && isNumberType && hasValue) { + displayValue = (cellValue as number).toFixed(hasPrecision ? precision : 2); + } else { + displayValue = defaultValue; + } + + return ( + + {displayValue} + + ); +}); diff --git a/src/hooks/useCodapState.tsx b/src/hooks/useCodapState.tsx index 3c75e7f..abc1592 100644 --- a/src/hooks/useCodapState.tsx +++ b/src/hooks/useCodapState.tsx @@ -12,12 +12,13 @@ import { createCollectionFromAttribute, createNewCollection, updateAttributePosition, + updateCaseById, } from "@concord-consortium/codap-plugin-api"; -import { getSnapshot } from "mobx-state-tree"; +import { applySnapshot, unprotect } from "mobx-state-tree"; import { getCases, getDataSetCollections, sortAttribute } from "../utils/apiHelpers"; -import { ICollection, IDataSet } from "../types"; -import { DataSetModel, DataSetModelType, DataSetsModel } from "../models/datasets"; -import { CollectionModel } from "../models/collections"; +import { ICollection, IDataSet, IProcessedCaseObj } from "../types"; +import { DataSetsModel, DataSetsModelType } from "../models/datasets"; +import { CollectionsModel, CollectionsModelType } from "../models/collections"; const iFrameDescriptor = { version: "0.5.0", @@ -39,13 +40,20 @@ export interface InteractiveState { export const useCodapState = () => { const [connected, setConnected] = useState(false); - const [dataSets, setDataSets] = useState([]); - const [selectedDataSet, setSelectedDataSet] = useState(null); - - // const [selectedDataSet, setSelectedDataSet] = useState(null); + const [dataSets] = useState(() => { + const newDataSets = DataSetsModel.create(); + unprotect(newDataSets); + return newDataSets; + }); + const [selectedDataSet, setSelectedDataSet] = useState(null); const [selectedDataSetName, setSelectedDataSetName] = useState(""); - const [collections, setCollections] = useState([]); - const [cases, setCases] = useState([]); + const [collections] = useState(() => { + const newCollectionsModel = CollectionsModel.create(); + unprotect(newCollectionsModel); + return newCollectionsModel; + }); + + const [cases, setCases] = useState([]); const [interactiveState, setInteractiveState] = useState({ view: null, dataSetName: null, @@ -54,19 +62,13 @@ export const useCodapState = () => { displayMode: "" }); - const handleDocumentChangeNotice = useCallback(() => getDataSets(), []); + const getDataSets = useCallback(async () => { + const dataContexts = await getListOfDataContexts(); + const datasets: IDataSet[] = dataContexts.values; + applySnapshot(dataSets, datasets); + }, [dataSets]); - const getDataSets = async () => { - const sets = await getListOfDataContexts(); - const dataSetModels = sets.values.map((d: IDataSet) => { - const { guid, id, name, title } = d; - const dataSetModel = DataSetModel.create({guid, id, name, title}); - return dataSetModel; - }); - const dataSetsModel = DataSetsModel.create({dataSets: dataSetModels}); - const _dataSets = getSnapshot(dataSetsModel); - setDataSets(_dataSets.dataSets); - }; + const handleDocumentChangeNotice = useCallback(() => getDataSets(), [getDataSets]); const handleSetDataSet = async (name: string|null) => { let dataSetInfo = null; @@ -90,7 +92,6 @@ export const useCodapState = () => { setConnected(true); }; - useEffect(() => { const handleDataContextChangeNotice = (iMessage: any) => { if (iMessage.resource === `dataContextChangeNotice[${selectedDataSetName}]`) { @@ -133,7 +134,9 @@ export const useCodapState = () => { }; const setUpNotifications = () => { - addDataContextChangeListener(selectedDataSet.name, handleDataContextChangeNotice); + if (selectedDataSet) { + addDataContextChangeListener(selectedDataSet.name, handleDataContextChangeNotice); + } }; if (selectedDataSet) { @@ -151,21 +154,19 @@ export const useCodapState = () => { const collectionModels = colls.map((coll: ICollection) => { const { areParentChildLinksConfigured, attrs, caseName, cases: _cases, childAttrName, collapseChildren, guid, id, name, parent, title, type } = coll; - return CollectionModel.create({ + return { areParentChildLinksConfigured, attrs, caseName, cases: _cases, childAttrName, collapseChildren, guid, id, name, parent, title, type - }); + }; }); - const _collections: ICollection[] = collectionModels.map(model => getSnapshot(model)); - setCollections(_collections); - //setCollections(colls); - }, [selectedDataSetName]); + applySnapshot(collections, collectionModels); + }, [collections, selectedDataSetName]); useEffect(() => { if (selectedDataSet) { updateCollections(); } else { - setCollections([]); + collections.clear(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedDataSet]); @@ -187,27 +188,34 @@ export const useCodapState = () => { const handleSelectDataSet = (name: string) => { - const selected = dataSets.find((d) => d.title === name); - return selected ? handleSetDataSet(selected.name) : handleSetDataSet(""); + handleSetDataSet(name); }; const getCollectionNameFromId = (id: number) => { - return collections.find((c: ICollection) => c.id === id)?.name; + return collections.find(c => c.id === id)?.name; }; const handleUpdateAttributePosition = async (coll: ICollection, attrName: string, position: number) => { - await updateAttributePosition(selectedDataSet.name, coll.name, attrName, position); + if (selectedDataSet) { + await updateAttributePosition(selectedDataSet.name, coll.name, attrName, position); + // update collections because CODAP does not send dataContextChangeNotice + updateCollections(); + } }; const handleAddCollection = async (newCollectionName: string) => { - await createNewCollection(selectedDataSet.name, newCollectionName, [{"name": "newAttr"}]); - // update collections because CODAP does not send dataContextChangeNotice - updateCollections(); + if (selectedDataSet) { + await createNewCollection(selectedDataSet.name, newCollectionName, [{"name": "newAttr"}]); + // update collections because CODAP does not send dataContextChangeNotice + updateCollections(); + } }; const handleCreateCollectionFromAttribute = async (collection: ICollection, attr: any, parent: number|string) => { - await createCollectionFromAttribute(selectedDataSet.name, collection.name, attr, parent); + if (selectedDataSet) { + await createCollectionFromAttribute(selectedDataSet.name, collection.name, attr, parent); + } }; const handleSortAttribute = async (context: string, attrId: number, isDescending: boolean) => { @@ -215,6 +223,8 @@ export const useCodapState = () => { }; const handleAddAttribute = async (collection: ICollection, attrName: string) => { + if (!selectedDataSet) return; + const proposedName = attrName.length ? attrName : "newAttr"; let newAttributeName; const allAttributes: Array = []; @@ -279,13 +289,40 @@ export const useCodapState = () => { } }, [selectedDataSet]); + const editCaseValue = async (newValue: string, caseObj: IProcessedCaseObj, attrTitle: string) => { + let request; + + try { + request = await updateCaseById(selectedDataSetName, caseObj.id, {[attrTitle]: newValue}); + if (request.success) { + caseObj.values.set(attrTitle, newValue); + } + } catch (e) { + console.error("Case not updated: ", e); + } + + return request; + }; + + const handleSetCollections = (colls: ICollection[]) => { + const newCollectionModels = colls.map((coll: ICollection) => { + const { areParentChildLinksConfigured, attrs, caseName, cases: _cases, childAttrName, + collapseChildren, guid, id, name, parent, title, type } = coll; + return { + areParentChildLinksConfigured, attrs, caseName, cases: _cases, childAttrName, + collapseChildren, guid, id, name, parent, title, type + }; + }); + applySnapshot(collections, newCollectionModels); + }; + return { init, handleSelectSelf, dataSets, selectedDataSet, collections, - handleSetCollections: setCollections, + handleSetCollections, handleSelectDataSet, getCollectionNameFromId, updateInteractiveState, @@ -300,6 +337,7 @@ export const useCodapState = () => { selectCODAPCases, listenForSelectionChanges, handleCreateCollectionFromAttribute, - handleUpdateCollections: updateCollections + handleUpdateCollections: updateCollections, + editCaseValue }; }; diff --git a/src/hooks/useDraggableTable.ts b/src/hooks/useDraggableTable.ts index 5f52266..6882e12 100644 --- a/src/hooks/useDraggableTable.ts +++ b/src/hooks/useDraggableTable.ts @@ -30,7 +30,6 @@ export const DraggableTableContext = createContext({ interface IUseDraggableTableOptions { collections: Array, - handleSetCollections: (collections: Array) => void, handleUpdateAttributePosition: (collection: ICollection, attrName: string, newPosition: number) => void, handleCreateCollectionFromAttribute: (collection: ICollection, attr: any, parent: number|string) => Promise } diff --git a/src/models/cases.ts b/src/models/cases.ts index 7e965a8..9337d1c 100644 --- a/src/models/cases.ts +++ b/src/models/cases.ts @@ -1,23 +1,16 @@ import { Instance, types } from "mobx-state-tree"; const CaseCollectionModel = types.model("CaseCollectionModel", { - id: types.number, + id: types.identifierNumber, name: types.string }); export const CaseModel = types.model("CaseModel", { children: types.array(types.late((): any => CaseModel)), collection: CaseCollectionModel, - id: types.number, + id: types.identifierNumber, parent: types.maybe(types.number), values: types.map(types.frozen()) }); export type CaseModelType = Instance; - -export const CasesModel = types.model("CasesModel", { - cases: types.array(CaseModel), - selectedCase: types.maybe(types.number) -}); - -export type CasesModelType = Instance; diff --git a/src/models/collections.ts b/src/models/collections.ts index 72f78fe..fdbc607 100644 --- a/src/models/collections.ts +++ b/src/models/collections.ts @@ -10,7 +10,7 @@ export const CollectionModel = types.model("CollectionModel", { childAttrName: types.maybe(types.string), collapseChildren: types.maybe(types.boolean), guid: types.number, - id: types.number, + id: types.identifierNumber, name: types.string, parent: types.maybe(types.number), title: types.string, @@ -19,8 +19,6 @@ export const CollectionModel = types.model("CollectionModel", { export type CollectionModelType = Instance; -export const CollectionsModel = types.model("CollectionsModel", { - collections: types.array(CollectionModel) -}); +export const CollectionsModel = types.array(CollectionModel); export type CollectionsModelType = Instance; diff --git a/src/models/datasets.ts b/src/models/datasets.ts index feea1ea..74fab0d 100644 --- a/src/models/datasets.ts +++ b/src/models/datasets.ts @@ -2,16 +2,12 @@ import { Instance, types } from "mobx-state-tree"; export const DataSetModel = types.model("DataSetModel", { guid: types.number, - id: types.number, + id: types.identifierNumber, name: types.string, title: types.optional(types.string, "") }); export type DataSetModelType = Instance; -export const DataSetsModel = types.model("DataSetsModel", { - dataSets: types.array(DataSetModel), - selectedDataSet: types.maybe(types.number) -}); - +export const DataSetsModel = types.array(DataSetModel); export type DataSetsModelType = Instance; diff --git a/src/models/interactive-state.ts b/src/models/interactive-state.ts deleted file mode 100644 index 7d71b64..0000000 --- a/src/models/interactive-state.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Instance, types } from "mobx-state-tree"; - -export const InteractiveStateModel = types.model("InteractiveStateModel", { - dataSetName: types.maybe(types.string), - displayMode: types.optional(types.string, ""), - padding: types.optional(types.boolean, false), - showHeaders: types.optional(types.boolean, false), - view: types.maybe(types.string) -}); - -export type InteractiveStateModelType = Instance; diff --git a/src/types.ts b/src/types.ts index d4d708b..b980dc0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,27 +15,27 @@ interface IAttribute { type:string; } export interface IDataSet { - guid: number, - id: number, - name: string, - title: string + guid: number; + id: number; + name: string; + title: string; } export interface ICollection { - areParentChildLinksConfigured: boolean, - attrs: IAttribute[], - caseName?: string, - cases: IProcessedCaseObj[], - childAttrName?: string, - collapseChildren?: boolean, - defaults?: any, - guid: number, - id: number, - labels?: any, - name: string, - parent?: number, - title: string, - type: string + areParentChildLinksConfigured: boolean; + attrs: IAttribute[]; + caseName?: string; + cases: IProcessedCaseObj[]; + childAttrName?: string; + collapseChildren?: boolean; + defaults?: any; + guid: number; + id: number; + labels?: any; + name: string; + parent?: number; + title: string; + type: string; } export type ICollections = Array; @@ -44,24 +44,20 @@ export interface ICaseObjCommon { collection: { name: string, id: number - }, - id: number, - parent?: number, - values: Values + }; + id: number; + parent?: number; + values: Values; } -export type Values = Record; - -export type CaseValuesWithId = Values & { - id: number -}; +export type Values = Map; export interface ICaseObj extends ICaseObjCommon { - children: Array + children: Array; } export interface IProcessedCaseObj extends ICaseObjCommon { - children: Array + children: Array; } export interface ICollectionClass { @@ -70,20 +66,20 @@ export interface ICollectionClass { } export interface ITableProps { - showHeaders: boolean, - collectionClasses: Array, - getClassName: (caseObj: IProcessedCaseObj) => string, - selectedDataSet: IDataSet, - collections: ICollection[], - mapCellsFromValues: (collectionId: number, rowKey: string, caseValuesWithId: Values, + showHeaders: boolean; + collectionClasses: Array; + getClassName: (caseObj: IProcessedCaseObj) => string; + selectedDataSet: IDataSet; + collections: ICollection[]; + mapCellsFromValues: (collectionId: number, rowKey: string, caseObj: IProcessedCaseObj, precisions: Record, attrTypes: Record, attrVisibilities: Record, isParent?: boolean, resizeCounter?: number, - parentLevel?: number) => ReactNode | ReactNode[], + parentLevel?: number) => ReactNode | ReactNode[]; mapHeadersFromValues: (collectionId: number, rowKey: string, values: Values, - attrVisibilities: Record) => ReactNode | ReactNode[], - getValueLength: (firstRow: Array) => number - paddingStyle: Record - handleUpdateCollections: () => void + attrVisibilities: Record) => ReactNode | ReactNode[]; + getValueLength: (firstRow: Array) => number; + paddingStyle: Record; + handleSortAttribute: (context: string, attrId: number, isDescending: boolean) => void; } export interface IBoundingBox { diff --git a/src/utils/apiHelpers.ts b/src/utils/apiHelpers.ts index f849964..3ca6cd6 100644 --- a/src/utils/apiHelpers.ts +++ b/src/utils/apiHelpers.ts @@ -6,7 +6,7 @@ import { getCaseByID, codapInterface } from "@concord-consortium/codap-plugin-api"; -import { ICollection, ICollections } from "../types"; +import { ICollection, ICollections, IProcessedCaseObj } from "../types"; export const getCases = async (selectedDataSetName: string, collName: string) => { const processCase = async (caseObj: any) => { @@ -31,7 +31,7 @@ export const getCases = async (selectedDataSetName: string, collName: string) => for (let i = 0; i < caseCount; i++) { const caseByIndex = await getCaseByIndex(selectedDataSetName, collName, i); const caseObj = caseByIndex.values.case; - const processedCase = await processCase(caseObj); + const processedCase: IProcessedCaseObj = await processCase(caseObj); cases.push(processedCase); } return cases;