ICTU/quality-time

View on GitHub
components/frontend/src/dashboard/CardDashboard.js

Summary

Maintainability
A
2 hrs
Test Coverage
import { array, arrayOf, bool, element, func } from "prop-types"
import { useEffect, useState } from "react"
import RGL, { WidthProvider } from "react-grid-layout"

import { accessGranted, EDIT_REPORT_PERMISSION, Permissions } from "../context/Permissions"

const ReactGridLayout = WidthProvider(RGL)

function cardDivs(cards, dragging, isDragging) {
    return cards.map((card) => (
        <div
            onClickCapture={(e) => {
                if (isDragging(e)) {
                    e.stopPropagation()
                }
            }}
            key={card.key}
        >
            {card}
        </div>
    ))
}
cardDivs.propTypes = {
    cards: arrayOf(element),
    dragging: bool,
    isDragging: func,
}

export function CardDashboard({ cards, initialLayout, saveLayout }) {
    const cols = 32
    const cardWidth = 4
    const cardHeight = 6
    const [mousePos, setMousePos] = useState([0, 0, 0])
    const [dragging, setDragging] = useState(false)
    useEffect(() => {
        const dashboard = document.getElementById("dashboard")
        Promise.all(dashboard.getAnimations().map((animation) => animation.finished))
            .then(() => dashboard.classList.add("animated")) // The animated class is used by the renderer to wait for animations to finish
            .catch((error) => console.log(error)) // No toast message: chances this goes wrong are small and a toaster could end up in the PDF
    }, [])
    if (cards.length === 0) {
        return null
    }
    const cardKeys = cards.map((card) => card.key)
    const layout = (initialLayout ?? []).filter((layoutItem) => cardKeys.includes(layoutItem.i))
    const layoutItemIds = layout.map((layoutItem) => layoutItem.i)
    const newCards = cards.filter((card) => !layoutItemIds.includes(card.key))
    const maxY = layout.length === 0 ? 0 : Math.max(...layout.map((card) => card.y)) + cardHeight
    newCards.forEach((card, index) =>
        layout.push({
            i: card.key,
            x: (index * cardWidth) % cols,
            y: maxY + cardHeight * Math.trunc((cardWidth * index) / cols),
            w: cardWidth,
            h: cardHeight,
            isResizable: false,
        }),
    )

    function onDragStart(_currentLayout, _oldItem, _newItem, _placeholder, event) {
        setDragging(true)
        const now = new Date()
        setMousePos([event.clientX, event.clientY, now.getTime()])
    }

    function onDragStop(newLayout, _oldItem, _newItem, _placeholder, event) {
        if (isDragging(event) && newLayout !== layout) {
            saveLayout(newLayout)
        }
        setTimeout(() => setDragging(false), 200) // User was dragging, prevent click event propagation
    }

    function isDragging(event) {
        if (dragging) {
            const now = new Date()
            const distanceX = Math.abs(event.clientX - mousePos[0])
            const distanceY = Math.abs(event.clientY - mousePos[1])
            const timedelta = now.getTime() - mousePos[2]
            return distanceX > 10 || distanceY > 10 || timedelta > 250
        }
        return false
    }

    return (
        <Permissions.Consumer>
            {(permissions) => (
                <ReactGridLayout
                    cols={cols}
                    compactType={null}
                    isDraggable={accessGranted(permissions, [EDIT_REPORT_PERMISSION])}
                    layout={layout}
                    onDragStart={onDragStart}
                    onDragStop={onDragStop}
                    preventCollision={true}
                    rowHeight={24}
                    style={{
                        zIndex: "0", // Prevent cards from being shown above the settings panel after being clicked
                    }}
                    useCSSTransforms={false} // Don't fly-in the cards from the top left
                >
                    {cardDivs(cards, dragging, isDragging)}
                </ReactGridLayout>
            )}
        </Permissions.Consumer>
    )
}
CardDashboard.propTypes = {
    cards: arrayOf(element),
    initialLayout: array,
    saveLayout: func,
}