Tom Butcher 9d1a032e10 Refactor Dashboard Components to Remove Unused State Management
- Removed unnecessary useState hooks for view state management in multiple dashboard components (Invoices, PaymentPolicies, Payments, TaxRecords, FilamentStocks, OrderItems, PartStocks, ProductStocks, PurchaseOrders, Shipments, StockAudits, StockEvents, StockLocations, StockTransfers, AppPasswords, AuditLogs, Couriers, CourierServices, DocumentJobs, DocumentPrinters, DocumentSizes, DocumentTemplates, Filaments, FilamentSkus).
- Updated ObjectListViewProvider integration to streamline component functionality and improve maintainability.
- Adjusted CSS variables in App.css for enhanced styling consistency across components.
2026-09-02 22:41:49 +01:00

440 lines
12 KiB
JavaScript

import PropTypes from 'prop-types'
import {
createContext,
useCallback,
useContext,
useLayoutEffect,
useRef,
useState
} from 'react'
import { HolderOutlined } from '@ant-design/icons'
import classNames from 'classnames'
const SegmentedNavReorderContext = createContext(null)
const getEnabledOptions = (options, disabled) =>
options.filter((option) => !disabled && !option.disabled)
const getItemMetrics = (element) => {
if (!element) {
return null
}
return {
left: element.offsetLeft,
top: element.offsetTop,
width: element.offsetWidth,
height: element.offsetHeight
}
}
export const SegmentedNavDragHandle = ({ value }) => {
const reorder = useContext(SegmentedNavReorderContext)
const dragProps = reorder?.getDragHandleProps?.(value)
if (!dragProps) {
return null
}
return (
<span
className='segmented-nav-drag-handle overview-drag-handle'
draggable
onClick={(event) => event.stopPropagation()}
onMouseDown={(event) => event.stopPropagation()}
onDragStart={(event) => dragProps.onDragStart(event)}
onDragEnd={dragProps.onDragEnd}
>
<HolderOutlined />
</span>
)
}
SegmentedNavDragHandle.propTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired
}
const SegmentedNav = ({
value,
options = [],
onChange,
onReorder,
className,
disabled = false,
animated = true
}) => {
const groupRef = useRef(null)
const thumbRef = useRef(null)
const itemRefs = useRef(new Map())
const prevValueRef = useRef(value)
const transitionGenRef = useRef(0)
const finishGenRef = useRef(0)
const [isAnimating, setIsAnimating] = useState(false)
const [thumbRect, setThumbRect] = useState(null)
const [draggedValue, setDraggedValue] = useState(null)
const [dropTarget, setDropTarget] = useState(null)
const enabledOptions = getEnabledOptions(options, disabled)
const isReordering = Boolean(onReorder && draggedValue != null)
const setItemRef = useCallback((optionValue, element) => {
if (element) {
itemRefs.current.set(optionValue, element)
} else {
itemRefs.current.delete(optionValue)
}
}, [])
useLayoutEffect(() => {
if (prevValueRef.current === value) {
return undefined
}
const fromMetrics = getItemMetrics(
itemRefs.current.get(prevValueRef.current)
)
const toMetrics = getItemMetrics(itemRefs.current.get(value))
prevValueRef.current = value
if (!animated) {
setIsAnimating(false)
setThumbRect(null)
return undefined
}
if (!fromMetrics || !toMetrics) {
return undefined
}
const transitionGen = transitionGenRef.current + 1
transitionGenRef.current = transitionGen
setIsAnimating(true)
setThumbRect(fromMetrics)
let endFrame = 0
const startFrame = requestAnimationFrame(() => {
endFrame = requestAnimationFrame(() => {
if (transitionGenRef.current === transitionGen) {
finishGenRef.current = transitionGen
setThumbRect(toMetrics)
}
})
})
return () => {
cancelAnimationFrame(startFrame)
cancelAnimationFrame(endFrame)
}
}, [value, animated])
const finishAnimation = useCallback(() => {
setIsAnimating(false)
setThumbRect(null)
}, [])
const handleThumbTransitionEnd = useCallback(
(event) => {
if (event.target !== thumbRef.current) {
return
}
if (
(event.propertyName === 'transform' ||
event.propertyName === 'width') &&
finishGenRef.current === transitionGenRef.current
) {
finishAnimation()
}
},
[finishAnimation]
)
const clearDragState = useCallback(() => {
setDraggedValue(null)
setDropTarget(null)
}, [])
const handleDragStart = useCallback(
(event, optionValue) => {
if (!onReorder) return
event.stopPropagation()
const item = event.currentTarget.closest('.segmented-nav-item')
if (item) {
const rect = item.getBoundingClientRect()
event.dataTransfer.setDragImage(
item,
event.clientX - rect.left,
event.clientY - rect.top
)
}
setDraggedValue(optionValue)
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.setData('text/plain', String(optionValue))
},
[onReorder]
)
const handleDragEnd = useCallback(() => {
clearDragState()
}, [clearDragState])
const handleItemDragOver = useCallback(
(event, option) => {
if (!onReorder || draggedValue == null || !option.reorderable) {
return
}
if (String(option.value) === String(draggedValue)) {
return
}
event.preventDefault()
event.stopPropagation()
event.dataTransfer.dropEffect = 'move'
const rect = event.currentTarget.getBoundingClientRect()
setDropTarget({
value: option.value,
insertBefore: event.clientX < rect.left + rect.width / 2
})
},
[draggedValue, onReorder]
)
const handleItemDrop = useCallback(
(event, option) => {
if (!onReorder || draggedValue == null || !option.reorderable) {
return
}
event.preventDefault()
event.stopPropagation()
const target = dropTarget || {
value: option.value,
insertBefore: true
}
if (String(draggedValue) !== String(target.value)) {
onReorder(draggedValue, target.value, target.insertBefore)
}
clearDragState()
},
[clearDragState, draggedValue, dropTarget, onReorder]
)
const handleGroupDragOver = useCallback(
(event) => {
if (!onReorder || draggedValue == null) {
return
}
const reorderableOptions = options.filter((option) => option.reorderable)
if (reorderableOptions.length === 0) {
return
}
if (
event.target instanceof Element &&
event.target.closest('.segmented-nav-item')
) {
return
}
event.preventDefault()
event.dataTransfer.dropEffect = 'move'
const lastOption = reorderableOptions[reorderableOptions.length - 1]
setDropTarget({
value: lastOption.value,
insertBefore: false
})
},
[draggedValue, onReorder, options]
)
const handleGroupDrop = useCallback(
(event) => {
if (!onReorder || draggedValue == null) {
return
}
if (
event.target instanceof Element &&
event.target.closest('.segmented-nav-item')
) {
return
}
event.preventDefault()
event.stopPropagation()
const reorderableOptions = options.filter((option) => option.reorderable)
const lastOption = reorderableOptions[reorderableOptions.length - 1]
if (lastOption && String(draggedValue) !== String(lastOption.value)) {
onReorder(draggedValue, lastOption.value, false)
}
clearDragState()
},
[clearDragState, draggedValue, onReorder, options]
)
const getDragHandleProps = useCallback(
(optionValue) => {
const option = options.find(
(item) => String(item.value) === String(optionValue)
)
if (!onReorder || !option?.reorderable) {
return null
}
return {
onDragStart: (event) => handleDragStart(event, optionValue),
onDragEnd: handleDragEnd
}
},
[handleDragEnd, handleDragStart, onReorder, options]
)
const handleKeyDown = (event, optionIndex) => {
if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft') {
return
}
event.preventDefault()
const direction = event.key === 'ArrowRight' ? 1 : -1
const currentEnabledIndex = enabledOptions.findIndex(
(option) => option.value === value
)
const startIndex =
currentEnabledIndex === -1 ? optionIndex : currentEnabledIndex
const nextIndex =
(startIndex + direction + enabledOptions.length) % enabledOptions.length
onChange?.(enabledOptions[nextIndex].value)
}
const reorderContextValue = onReorder ? { getDragHandleProps } : null
return (
<SegmentedNavReorderContext.Provider value={reorderContextValue}>
<div
className={classNames('segmented-nav', className, {
'segmented-nav-disabled': disabled,
'segmented-nav-reordering': isReordering
})}
role='radiogroup'
>
<div
className={classNames('segmented-nav-group', {
'segmented-nav-animating': isAnimating
})}
ref={groupRef}
onDragOver={handleGroupDragOver}
onDrop={handleGroupDrop}
>
{isAnimating && thumbRect && (
<div
ref={thumbRef}
className='segmented-nav-thumb'
style={{
width: thumbRect.width,
height: thumbRect.height,
transform: `translate3d(${thumbRect.left}px, ${thumbRect.top}px, 0)`
}}
onTransitionEnd={handleThumbTransitionEnd}
aria-hidden='true'
/>
)}
{options.map((option, index) => {
const isSelected = value === option.value
const isDisabled = disabled || option.disabled
const isDragging =
draggedValue != null &&
String(draggedValue) === String(option.value)
const isDropTarget =
dropTarget != null &&
String(dropTarget.value) === String(option.value) &&
String(draggedValue) !== String(option.value)
return (
<button
key={option.value}
ref={(element) => setItemRef(option.value, element)}
type='button'
role='radio'
aria-checked={isSelected}
disabled={isDisabled}
tabIndex={isSelected ? 0 : -1}
className={classNames('segmented-nav-item', option.className, {
'segmented-nav-item-selected': isSelected,
'segmented-nav-item-disabled': isDisabled,
'segmented-nav-item-dragging': isDragging,
'segmented-nav-item-drop-before':
isDropTarget && dropTarget.insertBefore,
'segmented-nav-item-drop-after':
isDropTarget && !dropTarget.insertBefore,
'segmented-nav-item-icon': option.icon
})}
onClick={() => {
if (!isDisabled) {
onChange?.(option.value)
}
}}
onKeyDown={(event) => handleKeyDown(event, index)}
onDragOver={
option.reorderable
? (event) => handleItemDragOver(event, option)
: undefined
}
onDrop={
option.reorderable
? (event) => handleItemDrop(event, option)
: undefined
}
>
{option.icon && (
<span className='segmented-nav-item-icon-contents'>
{option.icon}
</span>
)}
{option.label && (
<span className='segmented-nav-item-label'>
{option.label}
</span>
)}
</button>
)
})}
</div>
</div>
</SegmentedNavReorderContext.Provider>
)
}
SegmentedNav.propTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
options: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
label: PropTypes.node,
disabled: PropTypes.bool,
reorderable: PropTypes.bool,
className: PropTypes.string
})
),
onChange: PropTypes.func,
onReorder: PropTypes.func,
className: PropTypes.string,
disabled: PropTypes.bool,
animated: PropTypes.bool
}
export default SegmentedNav