Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

182849961 table advance selection #1527

Merged
merged 14 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 147 additions & 3 deletions v3/src/components/case-table/collection-table.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { comparer } from "mobx"
import { observer } from "mobx-react-lite"
import React, { useCallback, useEffect, useMemo, useRef } from "react"
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import DataGrid, { CellKeyboardEvent, DataGridHandle } from "react-data-grid"
import { kCollectionTableBodyDropZoneBaseId } from "./case-table-drag-drop"
import {
Expand All @@ -25,6 +25,9 @@
import { IAttribute } from "../../models/data/attribute"
import { IDataSet } from "../../models/data/data-set"
import { createAttributesNotification } from "../../models/data/data-set-notifications"
import {
collectionCaseIdFromIndex, collectionCaseIndexFromId, selectCases, setSelectedCases
} from "../../models/data/data-set-utils"
import { uiState } from "../../models/ui-state"
import { uniqueName } from "../../utilities/js-utils"
import { mstReaction } from "../../utilities/mst-reaction"
Expand All @@ -39,6 +42,8 @@

type OnNewCollectionDropFn = (dataSet: IDataSet, attrId: string, beforeCollectionId: string) => void

const kScrollMargin = 35

// custom renderers for use with RDG
const renderers: TRenderers = { renderRow: customRenderRow }

Expand All @@ -63,6 +68,10 @@
const { handleWhiteSpaceClick } = useWhiteSpaceClick({ gridRef })
const forceUpdate = useForceUpdate()
const { isTileSelected } = useTileModelContext()
const [isSelecting, setIsSelecting] = useState(false)
const [selectionStartRowIdx, setSelectionStartRowIdx] = useState<number | null>(null)
const initialPointerDownPosition = useRef({ x: 0, y: 0 })
const kPointerMovementThreshold = 3

useEffect(function setGridElement() {
const element = gridRef.current?.element
Expand Down Expand Up @@ -192,22 +201,157 @@
event.preventGridDefault()
navigateToNextRow(event.shiftKey)
}
if ((event.key === "ArrowDown" || event.key === "ArrowUp")) {
const caseId = args.row.__id__

Check warning on line 205 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L205

Added line #L205 was not covered by tests
const isCaseSelected = data?.isCaseSelected(caseId)
const isExtending = event.shiftKey || event.altKey || event.metaKey
const currentSelectionIdx = collectionCaseIndexFromId(caseId, data, collectionId)

Check warning on line 208 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L208

Added line #L208 was not covered by tests

if (currentSelectionIdx != null) {
const nextIndex = event.key === "ArrowDown" ? currentSelectionIdx + 1 : currentSelectionIdx - 1
const nextCaseId = collectionCaseIdFromIndex(nextIndex, data, collectionId)

Check warning on line 212 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L212

Added line #L212 was not covered by tests
if (nextCaseId) {
const isNextCaseSelected = data?.isCaseSelected(nextCaseId)
if (isExtending) {
if (isNextCaseSelected) {
selectCases([caseId], data, !isCaseSelected)
} else {
selectCases([nextCaseId], data)

Check warning on line 219 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L217-L219

Added lines #L217 - L219 were not covered by tests
}
} else {
setSelectedCases([nextCaseId], data)

Check warning on line 222 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L221-L222

Added lines #L221 - L222 were not covered by tests
}
}
}
}
}

const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
const handleClick = (event: React.PointerEvent<HTMLDivElement>) => {
// See if mouse has moved beyond kMouseMovementThreshold since initial mousedown
// If it has, then it is not a click
const deltaX = event.clientX - initialPointerDownPosition.current.x
const deltaY = event.clientY - initialPointerDownPosition.current.y
const distanceMoved = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
const pointerHasMoved = distanceMoved > kPointerMovementThreshold
if (pointerHasMoved) {
initialPointerDownPosition.current = { x: 0, y: 0 }
return
}

// the grid element is the target when clicking outside the cells (otherwise, the cell is the target)
if (isTileSelected() && event.target === gridRef.current?.element) {
handleWhiteSpaceClick()
initialPointerDownPosition.current = { x: 0, y: 0 }

Check warning on line 244 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L244

Added line #L244 was not covered by tests
}
}

const scrollInterval = useRef<NodeJS.Timeout | null>(null)
const mouseY = useRef<number>(0)

const marqueeSelectCases = useCallback((startIdx: number, endIdx: number) => {
const newSelectedRows = []
const start = Math.min(startIdx, endIdx)
const end = Math.max(startIdx, endIdx)
for (let i = start; i <= end; i++) {
newSelectedRows.push(i)

Check warning on line 256 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L252-L256

Added lines #L252 - L256 were not covered by tests
}
const selectedCaseIds = newSelectedRows
.map(idx => collectionCaseIdFromIndex(idx, data, collectionId))
.filter((id): id is string => id !== undefined)
setSelectedCases(selectedCaseIds, data)

Check warning on line 261 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L258-L261

Added lines #L258 - L261 were not covered by tests
}, [collectionId, data])

const startAutoScroll = useCallback((clientY: number) => {
const grid = gridRef.current?.element
const rowHeight = collectionTableModel?.rowHeight
if (!grid || !rowHeight) return

const scrollSpeed = 50

scrollInterval.current = setInterval(() => {
const { top, bottom } = grid.getBoundingClientRect()
let scrolledToRowIdx = null

Check warning on line 273 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L272-L273

Added lines #L272 - L273 were not covered by tests

if (mouseY.current < top + kScrollMargin) {
grid.scrollTop -= scrollSpeed
const scrolledTop = grid.scrollTop
scrolledToRowIdx = Math.floor(scrolledTop / rowHeight)

Check warning on line 278 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L276-L278

Added lines #L276 - L278 were not covered by tests
} else if (mouseY.current > bottom - kScrollMargin) {
grid.scrollTop += scrollSpeed
const scrolledBottom = grid.scrollTop + grid.clientHeight - 1
scrolledToRowIdx = Math.floor(scrolledBottom / rowHeight)

Check warning on line 282 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L280-L282

Added lines #L280 - L282 were not covered by tests

}
if (scrolledToRowIdx != null && selectionStartRowIdx != null && scrolledToRowIdx >= 0 &&
(rows?.length && scrolledToRowIdx < rows?.length)) {
marqueeSelectCases(selectionStartRowIdx, scrolledToRowIdx)

Check warning on line 287 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L287

Added line #L287 was not covered by tests
}
}, 25)
}, [collectionTableModel, marqueeSelectCases, rows?.length, selectionStartRowIdx])

const stopAutoScroll = useCallback(() => {
if (scrollInterval.current) {
clearInterval(scrollInterval.current)
scrollInterval.current = null
}
}, [])

// Helper function to get the row index from a mouse event
const getRowIndexFromEvent = useCallback((event: React.PointerEvent) => {
const target = event.target as HTMLElement
const closestDataCell = target.closest('.codap-data-cell')
const caseId = closestDataCell?.className.split(" ").find(c => c.startsWith("rowId-"))?.split("-")[1]
const rowIdx = caseId ? collectionCaseIndexFromId(caseId, data, collectionId) : null
return rowIdx
}, [collectionId, data])

const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
initialPointerDownPosition.current = { x: event.clientX, y: event.clientY }
const startRowIdx = getRowIndexFromEvent(event)
if (startRowIdx != null) {
setSelectionStartRowIdx(startRowIdx)
setIsSelecting(true)
startAutoScroll(event.clientY)
mouseY.current = event.clientY
}
}

const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
if (isSelecting && selectionStartRowIdx !== null) {
const currentRowIdx = getRowIndexFromEvent(event as React.PointerEvent)

Check warning on line 321 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L321

Added line #L321 was not covered by tests
if (currentRowIdx != null) {
marqueeSelectCases(selectionStartRowIdx, currentRowIdx)

Check warning on line 323 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L323

Added line #L323 was not covered by tests
}
mouseY.current = event.clientY

Check warning on line 325 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L325

Added line #L325 was not covered by tests
const grid = gridRef.current?.element
if (grid) {
const { top, bottom } = grid.getBoundingClientRect()

Check warning on line 328 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L328

Added line #L328 was not covered by tests
if (mouseY.current < top + kScrollMargin || mouseY.current > bottom - kScrollMargin) {
if (!scrollInterval.current) {
startAutoScroll(mouseY.current)

Check warning on line 331 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L331

Added line #L331 was not covered by tests
}
} else {
stopAutoScroll()

Check warning on line 334 in v3/src/components/case-table/collection-table.tsx

View check run for this annotation

Codecov / codecov/patch

v3/src/components/case-table/collection-table.tsx#L333-L334

Added lines #L333 - L334 were not covered by tests
}
}
}
}

const handlePointerLeaveOrUp = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
setIsSelecting(false)
setSelectionStartRowIdx(null)
stopAutoScroll()
}, [stopAutoScroll])

if (!data || !rows || !visibleAttributes.length) return null

return (
<div className={`collection-table collection-${collectionId}`}>
<CollectionTableSpacer selectedFillColor={selectedFillColor}
onWhiteSpaceClick={handleWhiteSpaceClick} onDrop={handleNewCollectionDrop} />
<div className="collection-table-and-title" ref={setNodeRef} onClick={handleClick}>
<div className="collection-table-and-title" ref={setNodeRef} onClick={handleClick}
onPointerDown={handlePointerDown} onPointerMove={handlePointerMove} onPointerUp={handlePointerLeaveOrUp}
onPointerLeave={handlePointerLeaveOrUp}>
<CollectionTitle onAddNewAttribute={handleAddNewAttribute} showCount={true} />
<DataGrid ref={gridRef} className="rdg-light" data-testid="collection-table-grid" renderers={renderers}
columns={columns} rows={rows} headerRowHeight={+styles.headerRowHeight} rowKeyGetter={rowKey}
Expand Down
4 changes: 2 additions & 2 deletions v3/src/components/case-table/use-columns.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { comparer } from "mobx"
import { useEffect, useState } from "react"
import { clsx } from "clsx"
import { useCaseMetadata } from "../../hooks/use-case-metadata"
import { useCollectionContext, useParentCollectionContext } from "../../hooks/use-collection-context"
import { IAttribute } from "../../models/data/attribute"
Expand All @@ -12,7 +13,6 @@ import { kDefaultColumnWidth, TColumn } from "./case-table-types"
import CellTextEditor from "./cell-text-editor"
import ColorCellTextEditor from "./color-cell-text-editor"
import { ColumnHeader } from "./column-header"
import clsx from "clsx"

interface IUseColumnsProps {
data?: IDataSet
Expand Down Expand Up @@ -49,7 +49,7 @@ export const useColumns = ({ data, indexColumn }: IUseColumnsProps) => {
resizable: true,
headerCellClass: `codap-column-header`,
renderHeaderCell: ColumnHeader,
cellClass: clsx("codap-data-cell", {"formula-column": hasFormula}),
cellClass: row => clsx("codap-data-cell", `rowId-${row.__id__}`, {"formula-column": hasFormula}),
renderCell: AttributeValueCell,
editable: row => isCaseEditable(data, row.__id__),
renderEditCell: isEditable
Expand Down
2 changes: 1 addition & 1 deletion v3/src/components/case-tile-common/index-menu-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const IndexMenuList = ({caseId, index}: IProps) => {
itemKey: `DG.CaseTable.indexMenu.delete${deletableSelectedItems.length === 1 ? "Case" : "Cases" }`,
isEnabled: () => deletableSelectedItems.length >= 1,
handleClick: () => {
if (data?.selection.size) {
if (deletableSelectedItems && data) {
removeCasesWithCustomUndoRedo(data, deletableSelectedItems)
}
}
Expand Down
Loading