From 882af80f3d5d569c64bfc02ed64d75da8028f9f9 Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Mon, 2 Oct 2023 09:11:52 -0400 Subject: [PATCH 1/8] feat: Added re-order and move attributes [PT-186024265] [PT-186024290] This adds a draggable table context with a draggable header element that allows users to re-order columns in the collection and move elements to new collections. This also has some existing missing key cleanup work in it. NOTE: This does not use DnDKit as that does not work with tables. --- src/components/app.tsx | 2 + src/components/draggable-table-header.tsx | 34 ++++++ src/components/flat-table.tsx | 14 ++- src/components/landscape-view.tsx | 26 +++-- src/components/nested-table.tsx | 48 +++++--- src/components/portrait-view.tsx | 24 ++-- src/hooks/useDraggableTable.ts | 136 ++++++++++++++++++++++ src/hooks/useDragging.tsx | 58 ++++----- src/types.tsx | 4 +- 9 files changed, 279 insertions(+), 67 deletions(-) create mode 100644 src/components/draggable-table-header.tsx create mode 100644 src/hooks/useDraggableTable.ts diff --git a/src/components/app.tsx b/src/components/app.tsx index 230c74d..ca37759 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -76,6 +76,8 @@ function App() { handleSelectDataSet={handleSelectDataSet} updateInteractiveState={updateInteractiveState} handleShowComponent={handleShowComponent} + handleUpdateAttributePosition={handleUpdateAttributePosition} + handleSetCollections={handleSetCollections} /> ); diff --git a/src/components/draggable-table-header.tsx b/src/components/draggable-table-header.tsx new file mode 100644 index 0000000..1d3b43c --- /dev/null +++ b/src/components/draggable-table-header.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { useDraggableTableContext } from "../hooks/useDraggableTable"; + +const border = "3px solid #FBF719"; +const borderLeft = border; +const borderRight = border; + +interface DraggagleTableHeaderProps { + collectionId: number; + attrTitle: string; + colSpan?: number; +} + +export const DraggagleTableHeader: React.FC = ({collectionId, attrTitle, children}) => { + const {dragOverId, dragSide, handleDragStart, handleDragOver, handleOnDrop, handleDragEnter, + handleDragLeave} = useDraggableTableContext(); + const id = `${collectionId}-${attrTitle}`; + const style: React.CSSProperties = id === dragOverId ? (dragSide === "left" ? {borderLeft} : {borderRight}) : {}; + + return ( + + {children} + + ); +}; diff --git a/src/components/flat-table.tsx b/src/components/flat-table.tsx index 2224b6b..681b44a 100644 --- a/src/components/flat-table.tsx +++ b/src/components/flat-table.tsx @@ -1,5 +1,6 @@ import React from "react"; import { ITableProps } from "../types"; +import { DraggagleTableHeader } from "./draggable-table-header"; import css from "./tables.scss"; @@ -23,11 +24,18 @@ export const FlatTable = (props: IFlatProps) => { {collections[0].title} } - {collection.attrs.map((attr: any) => {attr.title})} + {collection.attrs.map((attr: any) => + + {attr.title} + )} - {items.length && items.map((item) => { + {items.map((item, index) => { return ( - {mapCellsFromValues(item)} + {mapCellsFromValues(`row-${index}`, item)} ); })} diff --git a/src/components/landscape-view.tsx b/src/components/landscape-view.tsx index 918a9a2..cc6db03 100644 --- a/src/components/landscape-view.tsx +++ b/src/components/landscape-view.tsx @@ -1,5 +1,6 @@ import React from "react"; import { ICollection, IProcessedCaseObj, ITableProps } from "../types"; +import { DraggagleTableHeader } from "./draggable-table-header"; import css from "./tables.scss"; @@ -18,9 +19,9 @@ export const LandscapeView = (props: ITableProps) => { {parentColl.name} } - {firstRowValues.map(values => mapHeadersFromValues(values))} + {firstRowValues.map(values => mapHeadersFromValues(parentColl.id, "first-row", values))} - {firstRowValues.map(values => mapCellsFromValues(values))} + {firstRowValues.map(values => mapCellsFromValues("first-row", values))} {parentColl.cases.map((caseObj) => { return ( @@ -30,7 +31,7 @@ export const LandscapeView = (props: ITableProps) => { style={{...paddingStyle, verticalAlign: "top"}} colSpan={Object.values(caseObj.values).length}>
- {renderColFromCaseObj(caseObj)} + {renderColFromCaseObj(parentColl, caseObj)}
); @@ -40,7 +41,7 @@ export const LandscapeView = (props: ITableProps) => { ); }; - const renderColFromCaseObj = (caseObj: IProcessedCaseObj, index?: number) => { + const renderColFromCaseObj = (collection: ICollection, caseObj: IProcessedCaseObj, index?: number) => { const {children, values} = caseObj; const isFirstIndex = index === 0; if (!children.length) { @@ -52,8 +53,12 @@ export const LandscapeView = (props: ITableProps) => { {caseObj.collection.name} } - {isFirstIndex && {mapHeadersFromValues(values)}} - {mapCellsFromValues(values)} + {isFirstIndex && + + {mapHeadersFromValues(collection.id, `first-row-${index}`, values)} + + } + {mapCellsFromValues(`row-${index}`, values)} ); } else { @@ -68,7 +73,7 @@ export const LandscapeView = (props: ITableProps) => { {anyChildHasChildren ? renderNestedTable(filteredCollection) : caseObj.children.map((child: IProcessedCaseObj, i: number) => { - return renderColFromCaseObj(child, i); + return renderColFromCaseObj(collection, child, i); }) } @@ -86,7 +91,12 @@ export const LandscapeView = (props: ITableProps) => { - + + {selectedDataSet.name} + {renderNestedTable(parentColl[0])} diff --git a/src/components/nested-table.tsx b/src/components/nested-table.tsx index aaf30c7..906cc2d 100644 --- a/src/components/nested-table.tsx +++ b/src/components/nested-table.tsx @@ -5,6 +5,8 @@ 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 { DraggagleTableHeader } from "./draggable-table-header"; import css from "./nested-table.scss"; @@ -21,14 +23,23 @@ interface IProps { handleSelectDataSet: (e: React.ChangeEvent) => void updateInteractiveState: (update: Partial) => void handleShowComponent: () => void + handleSetCollections: (collections: Array) => void + handleUpdateAttributePosition: (collection: ICollection, attrName: string, newPosition: number) => void, } export const NestedTable = (props: IProps) => { const {selectedDataSet, dataSets, collections, items, interactiveState, - handleSelectDataSet, updateInteractiveState, handleShowComponent} = props; + handleSelectDataSet, updateInteractiveState, handleShowComponent, + handleSetCollections, handleUpdateAttributePosition} = props; const [collectionClasses, setCollectionClasses] = useState>([]); const [paddingStyle, setPaddingStyle] = useState>({padding: "0px"}); + const draggableTable = useDraggableTable({ + collections, + handleSetCollections, + handleUpdateAttributePosition + }); + useEffect(() => { if (collections.length) { const classes = collections.map((coll: ICollection, idx: number) => { @@ -75,30 +86,31 @@ export const NestedTable = (props: IProps) => { updateInteractiveState({displayMode: e.target.value}); }, [updateInteractiveState]); - const mapHeadersFromValues = (values: IValues) => { + const mapHeadersFromValues = (collectionId: number, rowKey: string, values: IValues) => { return ( <> {(Object.keys(values)).map((key, index) => { if (typeof values[key] === "string" || typeof values[key] === "number") { - return (); - } + return ( + {key} + + ); } - )} + })} ); }; - const mapCellsFromValues = (values: IValues) => { - return ( - <> - {(Object.values(values)).map((val, index) => { - if (typeof val === "string" || typeof val === "number") { - return (); - } - } - )} - - ); + const mapCellsFromValues = (rowKey: string, values: IValues) => { + return Object.values(values).map((val, index) => { + if (typeof val === "string" || typeof val === "number") { + return (); + } + }); }; const getValueLength = (firstRow: Array) => { @@ -143,7 +155,9 @@ export const NestedTable = (props: IProps) => { padding={interactiveState.padding} displayMode={interactiveState.displayMode} /> - {selectedDataSet && renderTable()} + + {selectedDataSet && renderTable()} + ); }; diff --git a/src/components/portrait-view.tsx b/src/components/portrait-view.tsx index 1451ad6..4aa6ac9 100644 --- a/src/components/portrait-view.tsx +++ b/src/components/portrait-view.tsx @@ -1,5 +1,6 @@ import React from "react"; import { ICollection, IProcessedCaseObj, ITableProps } from "../types"; +import { DraggagleTableHeader } from "./draggable-table-header"; import css from "./tables.scss"; @@ -17,30 +18,35 @@ export const PortraitView = (props: ITableProps) => { - + + {parentColl.name} + - {parentColl.cases.map((caseObj, index) => renderRowFromCaseObj(caseObj, index))} + {parentColl.cases.map((caseObj, index) => renderRowFromCaseObj(caseObj.collection.id, caseObj, index))} ); }; - const renderRowFromCaseObj = (caseObj: IProcessedCaseObj, index?: null|number) => { + const renderRowFromCaseObj = (collectionId: number, caseObj: IProcessedCaseObj, index?: null|number) => { const {children, values} = caseObj; if (!children.length) { return ( - {mapCellsFromValues(values)} + {mapCellsFromValues(`row-${index}`, values)} ); } else { return ( <> {index === 0 ? - {mapHeadersFromValues(values)} + {mapHeadersFromValues(collectionId, `first-row-${index}`, values)} : "" } - {mapCellsFromValues(values)} + {mapCellsFromValues(`parent-row-${index}`, values)}
{selectedDataSet.name}
{key}{val}{val}{selectedDataSet.name}
{parentColl.name}
{showHeaders ? children[0].collection.name : ""}
@@ -49,13 +55,13 @@ export const PortraitView = (props: ITableProps) => { return ( <> - {mapHeadersFromValues(child.values)} + {mapHeadersFromValues(child.collection.id, `child-row-${index}-${i}`, child.values)} - {renderRowFromCaseObj(child, i)} + {renderRowFromCaseObj(child.collection.id, child, i)} ); } else { - return (renderRowFromCaseObj(child, i)); + return (renderRowFromCaseObj(child.collection.id, child, i)); } })} diff --git a/src/hooks/useDraggableTable.ts b/src/hooks/useDraggableTable.ts new file mode 100644 index 0000000..499e9ff --- /dev/null +++ b/src/hooks/useDraggableTable.ts @@ -0,0 +1,136 @@ +import React, { useContext, useState, createContext, useCallback, useRef } from "react"; +import { ICollection } from "../types"; +import { updateCollectionAttributes } from "./useDragging"; + +type Side = "left"|"right"; + +type DraggableTableContextType = { + handleDragStart: (e: React.DragEvent) => void + handleDragOver: (e: React.DragEvent) => void + handleDragEnter: (e: React.DragEvent) => void + handleDragLeave: (e: React.DragEvent) => void + handleOnDrop: (e: React.DragEvent) => void + dragOverId: string | undefined + dragSide: Side | undefined +}; + +export const DraggableTableContext = createContext({ + handleDragStart: () => undefined, + handleDragOver: () => undefined, + handleDragEnter: () => undefined, + handleDragLeave: () => undefined, + handleOnDrop: () => undefined, + dragOverId: undefined, + dragSide: undefined, + } +); + +interface IUseDraggableTableOptions { + collections: Array, + handleSetCollections: (collections: Array) => void, + handleUpdateAttributePosition: (collection: ICollection, attrName: string, newPosition: number) => void, +} + +export const useDraggableTable = (options: IUseDraggableTableOptions) => { + const {collections, handleSetCollections, handleUpdateAttributePosition} = options; + const [dragId, setDragId] = useState(undefined); + const [dragOverId, setDragOverId] = useState(undefined); + const [dragSide, setDragSide] = useState("left"); + const dragOverRectRef = useRef(undefined); + + const getItemId = (e: React.DragEvent) => (e.target as HTMLElement)?.dataset?.id; + + const getCollectionAndAttribute = useCallback((id?: string) => { + let collection: ICollection|undefined = undefined; + + if (!id) { + return undefined; + } + + const [collectionId, attrTitle] = id.split("-"); + collection = collections.find(c => collectionId === `${c.id}`); + if (!collection) { + return undefined; + } + + return {collection, attr: collection.attrs.find(a => a.title === attrTitle)}; + }, [collections]); + + const updateDragSide = (e: React.DragEvent) => { + let side: Side|undefined = undefined; + if (dragOverRectRef.current) { + side = e.clientX < dragOverRectRef.current.left + dragOverRectRef.current.width/2 ? "left" : "right"; + } + setDragSide(side); + }; + + const handleDragStart = (e: React.DragEvent) => { + setDragId(getItemId(e)); + }; + + const handleDragEnter = (e: React.DragEvent) => { + dragOverRectRef.current = (e.target as HTMLElement).getBoundingClientRect?.(); + updateDragSide(e); + setDragOverId(getItemId(e)); + }; + + const handleDragOver = (e: React.DragEvent) => { + // allow drag overs on other headers if it has an item id set + if (getItemId(e) === dragOverId) { + updateDragSide(e); + e.preventDefault(); + } else { + dragOverRectRef.current = undefined; + } + }; + + const handleDragLeave = useCallback((e: React.DragEvent) => { + // if moving out of a drag zone into a non-drag zone clear the drag over id + if (getItemId(e) === dragOverId) { + setDragOverId(undefined); + } + }, [dragOverId]); + + const handleOnDrop = useCallback((e: React.DragEvent) => { + const targetId = getItemId(e); + if (dragId && targetId) { + const source = getCollectionAndAttribute(dragId); + const target = getCollectionAndAttribute(targetId); + + if (source && target && (source.collection !== target.collection || source.attr !== target.attr)) { + const sourceIndex = source.collection.attrs.indexOf(source.attr); + const targetIndex = target.collection.attrs.indexOf(target.attr); + const newIndex = dragSide === "left" ? targetIndex : targetIndex + 1; + + if (target.collection.id === source.collection.id) { + if (sourceIndex !== newIndex) { + const newCollections = updateCollectionAttributes(collections, source.collection, target.collection, + sourceIndex, newIndex, null); + handleSetCollections(newCollections); + handleUpdateAttributePosition(source.collection, source.attr.name, newIndex); + } + } else { + const newCollections = updateCollectionAttributes(collections, source.collection, target.collection, + sourceIndex, newIndex, source.attr); + handleSetCollections(newCollections); + handleUpdateAttributePosition(target.collection, source.attr.name, newIndex); + } + } + } + + setDragOverId(undefined); + }, [dragSide, collections, dragId, getCollectionAndAttribute, handleSetCollections, handleUpdateAttributePosition]); + + return { + handleDragStart, + handleDragOver, + handleDragEnter, + handleDragLeave, + handleOnDrop, + dragOverId, + dragSide + }; +}; + +export const useDraggableTableContext = () => useContext(DraggableTableContext); + diff --git a/src/hooks/useDragging.tsx b/src/hooks/useDragging.tsx index 6caae69..d055173 100644 --- a/src/hooks/useDragging.tsx +++ b/src/hooks/useDragging.tsx @@ -3,6 +3,31 @@ import { ICollection } from "../types"; import { DragEndEvent, DragOverEvent, DragStartEvent } from "@dnd-kit/core"; import { arrayMove } from "@dnd-kit/sortable"; +export const updateCollectionAttributes = (collections: ICollection[], sourceCollection: ICollection, + targetCollection: ICollection, activeIdx: number, newIdx: number, attr: any) => { + const getIndex = (coll: ICollection) => collections.findIndex(c => c.id === coll.id); + + const collectionIndex = getIndex(targetCollection); + const newCollection = {...targetCollection}; + const newCollections = [...collections]; + + if (sourceCollection?.id === targetCollection.id) { + newCollection.attrs = arrayMove(newCollection.attrs, activeIdx, newIdx); + } else { + if (sourceCollection) { + // Remove attribute from parent collection. + const indexOfParentCollection = getIndex(sourceCollection); + const newParentCollection = {...sourceCollection}; + newParentCollection.attrs.splice(activeIdx, 1); + newCollections[indexOfParentCollection] = newParentCollection; + // And add to target collection. + newCollection.attrs.splice(newIdx, 0, attr); + } + } + + newCollections[collectionIndex] = newCollection; + return newCollections; +}; interface IUseDragging { collections: Array, handleSetCollections: (collections: Array) => void, @@ -31,32 +56,6 @@ export const useDragging = (props: IUseDragging) => { setTargetCollection(target); }; - const setCollectionAttributes = (collection: ICollection, activeIdx: number, - newIdx: number, attr: any) => { - const getIndex = (coll: ICollection) => collections.findIndex(c => c.id === coll.id); - - const collectionIndex = getIndex(collection); - const newCollection = {...collection}; - const newCollections = [...collections]; - - if (parentCollection?.id === collection.id) { - newCollection.attrs = arrayMove(newCollection.attrs, activeIdx, newIdx); - } else { - if (parentCollection) { - // Remove attribute from parent collection. - const indexOfParentCollection = getIndex(parentCollection); - const newParentCollection = {...parentCollection}; - newParentCollection.attrs.splice(activeIdx, 1); - newCollections[indexOfParentCollection] = newParentCollection; - // And add to target collection. - newCollection.attrs.splice(newIdx, 0, attr); - } - } - - newCollections[collectionIndex] = newCollection; - handleSetCollections(newCollections); - }; - const handleDragEnd = (e: DragEndEvent) => { const { active, over } = e; const eventObjsDefined = active && over; @@ -70,7 +69,9 @@ export const useDragging = (props: IUseDragging) => { // If the new index is greater than the active index, CODAP will place behind one. // Set to +1 to fix this. const newIndex = overIndex > activeIndex ? overIndex + 1 : overIndex; - setCollectionAttributes(parentCollection, activeIndex, overIndex, null); + const newCollections = updateCollectionAttributes(collections, parentCollection, + parentCollection, activeIndex, overIndex, null); + handleSetCollections(newCollections); handleUpdateAttributePosition(parentCollection, activeAttr.name, newIndex); } else { // If we try to move an attr from one column to the end of another, the attr will automatically be @@ -81,7 +82,8 @@ export const useDragging = (props: IUseDragging) => { const distanceFromTarget = translated ? overRect.top - translated.top : null; const isBelowTarget = distanceFromTarget && distanceFromTarget <= .5 * overRect.height; const newIndex = isBelowTarget ? overIndex + 1 : overIndex; - setCollectionAttributes(targetCollection, activeIndex, newIndex, activeAttr); + handleSetCollections(updateCollectionAttributes(collections, parentCollection, + targetCollection, activeIndex, newIndex, activeAttr)); handleUpdateAttributePosition(targetCollection, activeAttr.name, newIndex); } } diff --git a/src/types.tsx b/src/types.tsx index b7b6931..cc2760d 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -55,8 +55,8 @@ export interface ITableProps { getClassName: (caseObj: IProcessedCaseObj) => string, selectedDataSet: IDataSet, collections: Array, - mapCellsFromValues: (values: IValues) => void, - mapHeadersFromValues: (values: IValues) => void, + mapCellsFromValues: (rowKey: string, values: IValues) => void, + mapHeadersFromValues: (collectionId: number, rowKey: string, values: IValues) => void, getValueLength: (firstRow: Array) => number paddingStyle: Record } From d97631445674cdb7bc5c165d7670c31d72bec31d Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Mon, 2 Oct 2023 16:18:59 -0400 Subject: [PATCH 2/8] Changed pixel border with to 5px --- src/components/draggable-table-header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/draggable-table-header.tsx b/src/components/draggable-table-header.tsx index 1d3b43c..c5a4580 100644 --- a/src/components/draggable-table-header.tsx +++ b/src/components/draggable-table-header.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useDraggableTableContext } from "../hooks/useDraggableTable"; -const border = "3px solid #FBF719"; +const border = "5px solid #FBF719"; const borderLeft = border; const borderRight = border; From a75f218cfed06457833a8bc7051cd28a089aabd4 Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Tue, 3 Oct 2023 05:28:16 -0400 Subject: [PATCH 3/8] Reverted main table header in portrait view to use non-draggable header --- src/components/portrait-view.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/portrait-view.tsx b/src/components/portrait-view.tsx index 4aa6ac9..15fd7c2 100644 --- a/src/components/portrait-view.tsx +++ b/src/components/portrait-view.tsx @@ -1,6 +1,5 @@ import React from "react"; import { ICollection, IProcessedCaseObj, ITableProps } from "../types"; -import { DraggagleTableHeader } from "./draggable-table-header"; import css from "./tables.scss"; @@ -18,12 +17,7 @@ export const PortraitView = (props: ITableProps) => { - - {parentColl.name} - + {parentColl.cases.map((caseObj, index) => renderRowFromCaseObj(caseObj.collection.id, caseObj, index))} From 15480e3817a26a7acd7b47cea97f81a25beacbe8 Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Tue, 3 Oct 2023 06:18:56 -0400 Subject: [PATCH 4/8] Draggable table updates: - Added yellow border during dragging on data rows also instead of just headers - Updated mouse pointer to use grab/grabbing values while dragging --- src/components/draggable-table-header.tsx | 34 ------------ src/components/draggable-table-tags.tsx | 63 +++++++++++++++++++++++ src/components/flat-table.tsx | 19 +++++-- src/components/landscape-view.tsx | 8 +-- src/components/nested-table.tsx | 17 ++++-- src/components/portrait-view.tsx | 4 +- src/components/tables.scss | 7 +++ src/hooks/useDraggableTable.ts | 10 +++- src/types.tsx | 2 +- 9 files changed, 115 insertions(+), 49 deletions(-) delete mode 100644 src/components/draggable-table-header.tsx create mode 100644 src/components/draggable-table-tags.tsx diff --git a/src/components/draggable-table-header.tsx b/src/components/draggable-table-header.tsx deleted file mode 100644 index c5a4580..0000000 --- a/src/components/draggable-table-header.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react"; -import { useDraggableTableContext } from "../hooks/useDraggableTable"; - -const border = "5px solid #FBF719"; -const borderLeft = border; -const borderRight = border; - -interface DraggagleTableHeaderProps { - collectionId: number; - attrTitle: string; - colSpan?: number; -} - -export const DraggagleTableHeader: React.FC = ({collectionId, attrTitle, children}) => { - const {dragOverId, dragSide, handleDragStart, handleDragOver, handleOnDrop, handleDragEnter, - handleDragLeave} = useDraggableTableContext(); - const id = `${collectionId}-${attrTitle}`; - const style: React.CSSProperties = id === dragOverId ? (dragSide === "left" ? {borderLeft} : {borderRight}) : {}; - - return ( - - ); -}; diff --git a/src/components/draggable-table-tags.tsx b/src/components/draggable-table-tags.tsx new file mode 100644 index 0000000..0b9e93e --- /dev/null +++ b/src/components/draggable-table-tags.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { useDraggableTableContext, Side } from "../hooks/useDraggableTable"; + +import css from "./tables.scss"; + +const border = "5px solid #FBF719"; +const borderLeft = border; +const borderRight = border; + +const getIdAndStyle = (collectionId: number, attrTitle: string, dragOverId?: string, dragSide?: Side) + : {id: string, style: React.CSSProperties} => { + const id = `${collectionId}-${attrTitle}`; + return { + id, + style: id === dragOverId ? (dragSide === "left" ? {borderLeft} : {borderRight}) : {} + }; +}; + +interface DraggagleTableHeaderProps { + collectionId: number; + attrTitle: string; + colSpan?: number; +} + +export const DraggagleTableHeader: React.FC = ({collectionId, attrTitle, children}) => { + const {dragOverId, dragSide, handleDragStart, handleDragOver, handleOnDrop, handleDragEnter, + handleDragLeave, handleDragEnd} = useDraggableTableContext(); + const {id, style} = getIdAndStyle(collectionId, attrTitle, dragOverId, dragSide); + + return ( + + ); +}; + +interface DraggagleTableDataProps { + collectionId: number; + attrTitle: string; +} + +export const DraggagleTableData: React.FC = ({collectionId, attrTitle, children}) => { + const {dragOverId, dragSide} = useDraggableTableContext(); + const {id, style} = getIdAndStyle(collectionId, attrTitle, dragOverId, dragSide); + + return ( + + ); +}; + diff --git a/src/components/flat-table.tsx b/src/components/flat-table.tsx index 681b44a..cf6c365 100644 --- a/src/components/flat-table.tsx +++ b/src/components/flat-table.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { ITableProps } from "../types"; -import { DraggagleTableHeader } from "./draggable-table-header"; +import { ITableProps, IValues } from "../types"; +import { DraggagleTableHeader } from "./draggable-table-tags"; import css from "./tables.scss"; @@ -13,6 +13,17 @@ export const FlatTable = (props: IFlatProps) => { const collection = collections[0]; const {className} = collectionClasses[0]; + const titles = collection.attrs.map(attr => attr.title); + const orderedItems = items.map(item => { + const orderedItem: IValues = {}; + titles.forEach(title => { + orderedItem[title] = item[title]; + }); + return orderedItem; + }); + + console.log("items", items); + return (
{selectedDataSet.name}
{parentColl.name}
- {children} - + {children} + + {children} +
@@ -33,9 +44,9 @@ export const FlatTable = (props: IFlatProps) => { {attr.title} )} - {items.map((item, index) => { + {orderedItems.map((item, index) => { return ( - {mapCellsFromValues(`row-${index}`, item)} + {mapCellsFromValues(collection.id, `row-${index}`, item)} ); })} diff --git a/src/components/landscape-view.tsx b/src/components/landscape-view.tsx index cc6db03..672c6de 100644 --- a/src/components/landscape-view.tsx +++ b/src/components/landscape-view.tsx @@ -1,6 +1,6 @@ import React from "react"; import { ICollection, IProcessedCaseObj, ITableProps } from "../types"; -import { DraggagleTableHeader } from "./draggable-table-header"; +import { DraggagleTableHeader } from "./draggable-table-tags"; import css from "./tables.scss"; @@ -21,7 +21,9 @@ export const LandscapeView = (props: ITableProps) => { {firstRowValues.map(values => mapHeadersFromValues(parentColl.id, "first-row", values))} - {firstRowValues.map(values => mapCellsFromValues("first-row", values))} + + {firstRowValues.map(values => mapCellsFromValues(parentColl.id, "first-row", values))} + {parentColl.cases.map((caseObj) => { return ( @@ -58,7 +60,7 @@ export const LandscapeView = (props: ITableProps) => { {mapHeadersFromValues(collection.id, `first-row-${index}`, values)} } - {mapCellsFromValues(`row-${index}`, values)} + {mapCellsFromValues(collection.id, `row-${index}`, values)} ); } else { diff --git a/src/components/nested-table.tsx b/src/components/nested-table.tsx index 906cc2d..ac09d88 100644 --- a/src/components/nested-table.tsx +++ b/src/components/nested-table.tsx @@ -6,7 +6,7 @@ import { Menu } from "./menu"; import { LandscapeView } from "./landscape-view"; import { FlatTable } from "./flat-table"; import { DraggableTableContext, useDraggableTable } from "../hooks/useDraggableTable"; -import { DraggagleTableHeader } from "./draggable-table-header"; +import { DraggagleTableData, DraggagleTableHeader } from "./draggable-table-tags"; import css from "./nested-table.scss"; @@ -105,10 +105,19 @@ export const NestedTable = (props: IProps) => { ); }; - const mapCellsFromValues = (rowKey: string, values: IValues) => { - return Object.values(values).map((val, index) => { + const mapCellsFromValues = (collectionId: number, rowKey: string, values: IValues) => { + return Object.keys(values).map((key, index) => { + const val = values[key]; if (typeof val === "string" || typeof val === "number") { - return (); + return ( + + {val} + + ); } }); }; diff --git a/src/components/portrait-view.tsx b/src/components/portrait-view.tsx index 15fd7c2..19cfcf2 100644 --- a/src/components/portrait-view.tsx +++ b/src/components/portrait-view.tsx @@ -28,7 +28,7 @@ export const PortraitView = (props: ITableProps) => { const {children, values} = caseObj; if (!children.length) { return ( - {mapCellsFromValues(`row-${index}`, values)} + {mapCellsFromValues(collectionId, `row-${index}`, values)} ); } else { return ( @@ -40,7 +40,7 @@ export const PortraitView = (props: ITableProps) => { : "" } - {mapCellsFromValues(`parent-row-${index}`, values)} + {mapCellsFromValues(collectionId, `parent-row-${index}`, values)}
{val}
diff --git a/src/components/tables.scss b/src/components/tables.scss index 1f74ab5..0c11d6c 100644 --- a/src/components/tables.scss +++ b/src/components/tables.scss @@ -158,3 +158,10 @@ table { width: 100%; height: 100%; } + +.draggable { + cursor: grab; +} +.dragging { + cursor: grabbing; +} \ No newline at end of file diff --git a/src/hooks/useDraggableTable.ts b/src/hooks/useDraggableTable.ts index 499e9ff..ba68d87 100644 --- a/src/hooks/useDraggableTable.ts +++ b/src/hooks/useDraggableTable.ts @@ -2,10 +2,11 @@ import React, { useContext, useState, createContext, useCallback, useRef } from import { ICollection } from "../types"; import { updateCollectionAttributes } from "./useDragging"; -type Side = "left"|"right"; +export type Side = "left"|"right"; type DraggableTableContextType = { handleDragStart: (e: React.DragEvent) => void + handleDragEnd: (e: React.DragEvent) => void handleDragOver: (e: React.DragEvent) => void handleDragEnter: (e: React.DragEvent) => void handleDragLeave: (e: React.DragEvent) => void @@ -16,6 +17,7 @@ type DraggableTableContextType = { export const DraggableTableContext = createContext({ handleDragStart: () => undefined, + handleDragEnd: () => undefined, handleDragOver: () => undefined, handleDragEnter: () => undefined, handleDragLeave: () => undefined, @@ -65,9 +67,14 @@ export const useDraggableTable = (options: IUseDraggableTableOptions) => { }; const handleDragStart = (e: React.DragEvent) => { + (e.target as HTMLElement).classList.add("dragging"); setDragId(getItemId(e)); }; + const handleDragEnd = (e: React.DragEvent) => { + (e.target as HTMLElement).classList.remove("dragging"); + }; + const handleDragEnter = (e: React.DragEvent) => { dragOverRectRef.current = (e.target as HTMLElement).getBoundingClientRect?.(); updateDragSide(e); @@ -123,6 +130,7 @@ export const useDraggableTable = (options: IUseDraggableTableOptions) => { return { handleDragStart, + handleDragEnd, handleDragOver, handleDragEnter, handleDragLeave, diff --git a/src/types.tsx b/src/types.tsx index cc2760d..af1b292 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -55,7 +55,7 @@ export interface ITableProps { getClassName: (caseObj: IProcessedCaseObj) => string, selectedDataSet: IDataSet, collections: Array, - mapCellsFromValues: (rowKey: string, values: IValues) => void, + mapCellsFromValues: (collectionId: number, rowKey: string, values: IValues) => void, mapHeadersFromValues: (collectionId: number, rowKey: string, values: IValues) => void, getValueLength: (firstRow: Array) => number paddingStyle: Record From dd8ab0a0f52e971a487fdbd18099b43a60c4a1cc Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Tue, 3 Oct 2023 07:08:39 -0400 Subject: [PATCH 5/8] Removed debug console log --- src/components/flat-table.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/flat-table.tsx b/src/components/flat-table.tsx index cf6c365..1b46f1b 100644 --- a/src/components/flat-table.tsx +++ b/src/components/flat-table.tsx @@ -22,8 +22,6 @@ export const FlatTable = (props: IFlatProps) => { return orderedItem; }); - console.log("items", items); - return (
From a65e32aedc4160e5461ff5052e1c83ea0197fe89 Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Tue, 3 Oct 2023 09:33:28 -0400 Subject: [PATCH 6/8] Removed setting of draggable/dragging classes --- src/components/draggable-table-tags.tsx | 7 +++---- src/components/tables.scss | 6 ------ src/hooks/useDraggableTable.ts | 8 -------- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/components/draggable-table-tags.tsx b/src/components/draggable-table-tags.tsx index 0b9e93e..bd69d7e 100644 --- a/src/components/draggable-table-tags.tsx +++ b/src/components/draggable-table-tags.tsx @@ -24,7 +24,7 @@ interface DraggagleTableHeaderProps { export const DraggagleTableHeader: React.FC = ({collectionId, attrTitle, children}) => { const {dragOverId, dragSide, handleDragStart, handleDragOver, handleOnDrop, handleDragEnter, - handleDragLeave, handleDragEnd} = useDraggableTableContext(); + handleDragLeave} = useDraggableTableContext(); const {id, style} = getIdAndStyle(collectionId, attrTitle, dragOverId, dragSide); return ( @@ -34,7 +34,6 @@ export const DraggagleTableHeader: React.FC = ({colle draggable={true} className={css.draggable} onDragStart={handleDragStart} - onDragEnd={handleDragEnd} onDragOver={handleDragOver} onDrop={handleOnDrop} onDragEnter={handleDragEnter} @@ -52,10 +51,10 @@ interface DraggagleTableDataProps { export const DraggagleTableData: React.FC = ({collectionId, attrTitle, children}) => { const {dragOverId, dragSide} = useDraggableTableContext(); - const {id, style} = getIdAndStyle(collectionId, attrTitle, dragOverId, dragSide); + const {style} = getIdAndStyle(collectionId, attrTitle, dragOverId, dragSide); return ( - ); diff --git a/src/components/tables.scss b/src/components/tables.scss index 0c11d6c..8b75573 100644 --- a/src/components/tables.scss +++ b/src/components/tables.scss @@ -159,9 +159,3 @@ table { height: 100%; } -.draggable { - cursor: grab; -} -.dragging { - cursor: grabbing; -} \ No newline at end of file diff --git a/src/hooks/useDraggableTable.ts b/src/hooks/useDraggableTable.ts index ba68d87..dd355bc 100644 --- a/src/hooks/useDraggableTable.ts +++ b/src/hooks/useDraggableTable.ts @@ -6,7 +6,6 @@ export type Side = "left"|"right"; type DraggableTableContextType = { handleDragStart: (e: React.DragEvent) => void - handleDragEnd: (e: React.DragEvent) => void handleDragOver: (e: React.DragEvent) => void handleDragEnter: (e: React.DragEvent) => void handleDragLeave: (e: React.DragEvent) => void @@ -17,7 +16,6 @@ type DraggableTableContextType = { export const DraggableTableContext = createContext({ handleDragStart: () => undefined, - handleDragEnd: () => undefined, handleDragOver: () => undefined, handleDragEnter: () => undefined, handleDragLeave: () => undefined, @@ -67,14 +65,9 @@ export const useDraggableTable = (options: IUseDraggableTableOptions) => { }; const handleDragStart = (e: React.DragEvent) => { - (e.target as HTMLElement).classList.add("dragging"); setDragId(getItemId(e)); }; - const handleDragEnd = (e: React.DragEvent) => { - (e.target as HTMLElement).classList.remove("dragging"); - }; - const handleDragEnter = (e: React.DragEvent) => { dragOverRectRef.current = (e.target as HTMLElement).getBoundingClientRect?.(); updateDragSide(e); @@ -130,7 +123,6 @@ export const useDraggableTable = (options: IUseDraggableTableOptions) => { return { handleDragStart, - handleDragEnd, handleDragOver, handleDragEnter, handleDragLeave, From b098b0aaf7d8e69dc7251d3ebd02953762694222 Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Tue, 3 Oct 2023 10:15:27 -0400 Subject: [PATCH 7/8] Added yellow border and drop handler on case headers also --- src/components/draggable-table-tags.tsx | 51 +++++++++++++++++++++++-- src/components/portrait-view.tsx | 14 ++++--- src/hooks/useDraggableTable.ts | 2 +- 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/components/draggable-table-tags.tsx b/src/components/draggable-table-tags.tsx index bd69d7e..5910e6c 100644 --- a/src/components/draggable-table-tags.tsx +++ b/src/components/draggable-table-tags.tsx @@ -7,13 +7,15 @@ const border = "5px solid #FBF719"; const borderLeft = border; const borderRight = border; +const getStyle = (id: string, dragOverId?: string, dragSide?: Side) => { + return id === dragOverId ? (dragSide === "left" ? {borderLeft} : {borderRight}) : {}; +}; + const getIdAndStyle = (collectionId: number, attrTitle: string, dragOverId?: string, dragSide?: Side) : {id: string, style: React.CSSProperties} => { const id = `${collectionId}-${attrTitle}`; - return { - id, - style: id === dragOverId ? (dragSide === "left" ? {borderLeft} : {borderRight}) : {} - }; + const style = getStyle(id, dragOverId, dragSide); + return { id, style }; }; interface DraggagleTableHeaderProps { @@ -44,9 +46,35 @@ export const DraggagleTableHeader: React.FC = ({colle ); }; +interface DroppableTableHeaderProps { + collectionId: number; +} + +export const DroppableTableHeader: React.FC = ({collectionId, children}) => { + const {dragOverId, handleDragOver, handleOnDrop, handleDragEnter, + handleDragLeave} = useDraggableTableContext(); + + const id = `${collectionId}`; + const style = getStyle(id, dragOverId, "left"); + + return ( + + ); +}; + interface DraggagleTableDataProps { collectionId: number; attrTitle: string; + style?: React.CSSProperties; } export const DraggagleTableData: React.FC = ({collectionId, attrTitle, children}) => { @@ -60,3 +88,18 @@ export const DraggagleTableData: React.FC = ({collectio ); }; +interface DroppableTableDataProps { + collectionId: number; + style?: React.CSSProperties; +} + +export const DroppableTableData: React.FC = ({collectionId, style, children}) => { + const {dragOverId, dragSide} = useDraggableTableContext(); + const dragStyle = getStyle(`${collectionId}`, dragOverId, dragSide); + + return ( + + ); +}; diff --git a/src/components/portrait-view.tsx b/src/components/portrait-view.tsx index 19cfcf2..1e84b08 100644 --- a/src/components/portrait-view.tsx +++ b/src/components/portrait-view.tsx @@ -1,5 +1,6 @@ import React from "react"; import { ICollection, IProcessedCaseObj, ITableProps } from "../types"; +import { DroppableTableData, DroppableTableHeader } from "./draggable-table-tags"; import css from "./tables.scss"; @@ -26,6 +27,7 @@ export const PortraitView = (props: ITableProps) => { const renderRowFromCaseObj = (collectionId: number, caseObj: IProcessedCaseObj, index?: null|number) => { const {children, values} = caseObj; + if (!children.length) { return ( {mapCellsFromValues(collectionId, `row-${index}`, values)} @@ -33,15 +35,17 @@ export const PortraitView = (props: ITableProps) => { } else { return ( <> - {index === 0 ? + {index === 0 && {mapHeadersFromValues(collectionId, `first-row-${index}`, values)} - - : "" + {showHeaders && ( + {children[0].collection.name} + )} + } {mapCellsFromValues(collectionId, `parent-row-${index}`, values)} - + ); diff --git a/src/hooks/useDraggableTable.ts b/src/hooks/useDraggableTable.ts index dd355bc..62b1e27 100644 --- a/src/hooks/useDraggableTable.ts +++ b/src/hooks/useDraggableTable.ts @@ -99,7 +99,7 @@ export const useDraggableTable = (options: IUseDraggableTableOptions) => { if (source && target && (source.collection !== target.collection || source.attr !== target.attr)) { const sourceIndex = source.collection.attrs.indexOf(source.attr); - const targetIndex = target.collection.attrs.indexOf(target.attr); + const targetIndex = target.attr ? target.collection.attrs.indexOf(target.attr) : target.collection.attrs.length; const newIndex = dragSide === "left" ? targetIndex : targetIndex + 1; if (target.collection.id === source.collection.id) { From 73b4f8da09279af7f961959b3f61a2cd2306b085 Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Tue, 3 Oct 2023 10:25:00 -0400 Subject: [PATCH 8/8] Fixed border side when hovering over case headers --- src/hooks/useDraggableTable.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/hooks/useDraggableTable.ts b/src/hooks/useDraggableTable.ts index 62b1e27..0dcf40e 100644 --- a/src/hooks/useDraggableTable.ts +++ b/src/hooks/useDraggableTable.ts @@ -59,7 +59,13 @@ export const useDraggableTable = (options: IUseDraggableTableOptions) => { const updateDragSide = (e: React.DragEvent) => { let side: Side|undefined = undefined; if (dragOverRectRef.current) { - side = e.clientX < dragOverRectRef.current.left + dragOverRectRef.current.width/2 ? "left" : "right"; + const target = getCollectionAndAttribute(getItemId(e)); + if (target?.attr) { + side = e.clientX < dragOverRectRef.current.left + dragOverRectRef.current.width/2 ? "left" : "right"; + } else { + // no attribute means this is a case header with no attribute so always show insert to the left + side = "left"; + } } setDragSide(side); };
+ {children} + {children} + + {children} +
{showHeaders ? children[0].collection.name : ""}
+ {caseObj.children.map((child, i) => { @@ -60,7 +64,7 @@ export const PortraitView = (props: ITableProps) => { })}
-