- Updated multiple dashboard components (Invoices, PaymentPolicies, Payments, TaxRecords, FilamentStocks, OrderItems, PartStocks, ProductStocks, PurchaseOrders, Shipments, StockAudits, StockEvents, StockLocations, StockTransfers, AppPasswords, AuditLogs) to incorporate ListViewTabs and ListViewEditButtons for enhanced user interaction. - Introduced ObjectListViewProvider to manage view state and filter/sort changes across components, improving maintainability and user experience. - Added state management for active views and filter/sort handling to streamline component functionality.
432 lines
12 KiB
JavaScript
432 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
|
|
}) => {
|
|
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 (!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])
|
|
|
|
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
|
|
}
|
|
|
|
export default SegmentedNav
|