- 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.
440 lines
12 KiB
JavaScript
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
|