Remove artificial delays from copydown function, fix issues with select components, ensure pointer cursor shows in copydown state, ensure table scroll position is reset on unmount

This commit is contained in:
2025-03-14 19:23:47 -04:00
parent d0a83c04ca
commit a88dbb8486
10 changed files with 136 additions and 54 deletions

View File

@@ -4,10 +4,6 @@
* Red outline + alert circle icon with tooltip if cell is NOT empty and isn't valid
8. When you enter a value in 2+ cells before validation finishes, contents from all edited cells get erased when validation finishes
9. Import dialog state not fully reset when closing? (validate data step appears scrolled to the middle of the table where I left it)
11. Copy down needs to show a loading state on the cells that it will copy to
15. Enhance copy down feature by allowing user to choose the last cell to copy to, instead of going all the way to the bottom
## Do NOT change or edit
* Anything related to AI validation
@@ -23,9 +19,11 @@
✅FIXED 6. Need to ensure all cell's contents don't overflow the input (truncate). COO does this currently, probably more
✅FIXED 7. The template select cell is expanding, needs to be fixed size and truncate
✅FIXED 10. UPC column doesn't need to show loading state when Item Number is being processed, only show on item number column
✅FIXED 11. Copy down needs to show a loading state on the cells that it will copy to
✅FIXED 12. Shipping restrictions/tax category should default to ID 0 if we didn't get it elsewhere
✅FIXED 13. Header row should be sticky (both up/down and left/right)
✅FIXED 14. Need a way to scroll around table if user doesn't have mouse wheel for left/right
✅FIXED 15. Enhance copy down feature by allowing user to choose the last cell to copy to, instead of going all the way to the bottom
---------

View File

@@ -20,7 +20,7 @@ import {
AlertDialogOverlay,
} from "@/components/ui/alert-dialog"
import { useRsi } from "../hooks/useRsi"
import { useState } from "react"
import { useState, useCallback } from "react"
type Props = {
children: React.ReactNode
@@ -32,6 +32,22 @@ export const ModalWrapper = ({ children, isOpen, onClose }: Props) => {
const { rtl, translations } = useRsi()
const [showCloseAlert, setShowCloseAlert] = useState(false)
// Create a handler that resets scroll positions before closing
const handleClose = useCallback(() => {
// Reset all scroll positions in the dialog
const scrollContainers = document.querySelectorAll('.overflow-auto, .overflow-scroll');
scrollContainers.forEach(container => {
if (container instanceof HTMLElement) {
// Reset scroll position to top-left
container.scrollTop = 0;
container.scrollLeft = 0;
}
});
// Call the original onClose handler
onClose();
}, [onClose]);
return (
<>
<Dialog open={isOpen} onOpenChange={() => setShowCloseAlert(true)} modal>
@@ -76,7 +92,7 @@ export const ModalWrapper = ({ children, isOpen, onClose }: Props) => {
<AlertDialogCancel onClick={() => setShowCloseAlert(false)}>
{translations.alerts.confirmClose.cancelButtonTitle}
</AlertDialogCancel>
<AlertDialogAction onClick={onClose}>
<AlertDialogAction onClick={handleClose}>
{translations.alerts.confirmClose.exitButtonTitle}
</AlertDialogAction>
</AlertDialogFooter>

View File

@@ -1,17 +1,32 @@
import { StepState, StepType, UploadFlow } from "./UploadFlow"
import { useRsi } from "../hooks/useRsi"
import { useRef, useState } from "react"
import { useRef, useState, useEffect } from "react"
import { steps, stepTypeToStepIndex, stepIndexToStepType } from "../utils/steps"
import { CgCheck } from "react-icons/cg"
const CheckIcon = ({ color }: { color: string }) => <CgCheck size="24" className={color} />
export const Steps = () => {
const { initialStepState, translations, isNavigationEnabled } = useRsi()
const { initialStepState, translations, isNavigationEnabled, isOpen } = useRsi()
const initialStep = stepTypeToStepIndex(initialStepState?.type)
const [activeStep, setActiveStep] = useState(initialStep)
const [state, setState] = useState<StepState>(initialStepState || { type: StepType.upload })
const history = useRef<StepState[]>([])
const prevIsOpen = useRef(isOpen)
// Reset state when dialog is reopened
useEffect(() => {
// Check if dialog was closed and is now open again
if (isOpen && !prevIsOpen.current) {
// Reset to initial state
setActiveStep(initialStep)
setState(initialStepState || { type: StepType.upload })
history.current = []
}
// Update previous isOpen value
prevIsOpen.current = isOpen
}, [isOpen, initialStep, initialStepState])
const onClickStep = (stepIndex: number) => {
const type = stepIndexToStepType(stepIndex)

View File

@@ -331,6 +331,7 @@ const ItemNumberCell = React.memo(({
minWidth: `${width}px`,
maxWidth: `${width}px`,
boxSizing: 'border-box',
cursor: isInTargetRow ? 'pointer' : undefined,
...(isSourceCell ? { backgroundColor: '#dbeafe', borderRadius: '0.375rem', padding: 0, boxShadow: '0 0 0 2px #3b82f6' } :
isSelectedTarget ? { backgroundColor: '#bfdbfe', borderRadius: '0.375rem', padding: 0 } :
isInTargetRow && isTargetRowHovered ? { backgroundColor: '#dbeafe', borderRadius: '0.375rem', padding: 0, cursor: 'pointer' } :
@@ -367,7 +368,6 @@ const ItemNumberCell = React.memo(({
<TooltipContent side="right">
<div className="flex flex-col">
<p className="font-medium">Copy value to rows below</p>
<p className="text-xs text-muted-foreground">Click to select target rows</p>
</div>
</TooltipContent>
</Tooltip>
@@ -535,6 +535,7 @@ const ValidationCell = ({
minWidth: `${width}px`,
maxWidth: `${width}px`,
boxSizing: 'border-box',
cursor: isInTargetRow ? 'pointer' : undefined,
...(isSourceCell ? { backgroundColor: '#dbeafe', borderRadius: '0.375rem', padding: 0, boxShadow: '0 0 0 2px #3b82f6' } :
isSelectedTarget ? { backgroundColor: '#bfdbfe', borderRadius: '0.375rem', padding: 0 } :
isInTargetRow && isTargetRowHovered ? { backgroundColor: '#dbeafe', borderRadius: '0.375rem', padding: 0, cursor: 'pointer' } :
@@ -571,7 +572,6 @@ const ValidationCell = ({
<TooltipContent side="right">
<div className="flex flex-col">
<p className="font-medium">Copy value to rows below</p>
<p className="text-xs text-muted-foreground">Click to select target rows</p>
</div>
</TooltipContent>
</Tooltip>

View File

@@ -866,34 +866,20 @@ const ValidationContainer = <T extends string>({
return newSet;
});
// Use setTimeout to allow the UI to update with loading state before processing
setTimeout(() => {
// Update each row sequentially with a small delay for visual feedback
const updateSequentially = async () => {
for (let i = 0; i < rowsToUpdate.length; i++) {
// Update all rows immediately
rowsToUpdate.forEach((_, i) => {
const targetRowIndex = rowIndex + 1 + i;
// Update the row with the copied value
enhancedUpdateRow(targetRowIndex, fieldKey as T, valueCopy);
// Remove loading state after a short delay
setTimeout(() => {
// Remove loading state
setValidatingCells(prev => {
const newSet = new Set(prev);
newSet.delete(`${targetRowIndex}-${fieldKey}`);
return newSet;
});
}, 100); // Short delay before removing loading state
// Add a small delay between updates for visual effect
if (i < rowsToUpdate.length - 1) {
await new Promise(resolve => setTimeout(resolve, 50));
}
}
};
updateSequentially();
}, 50);
});
}, [data, enhancedUpdateRow, setValidatingCells]);
// Memoize the enhanced validation table component
@@ -1032,6 +1018,20 @@ const ValidationContainer = <T extends string>({
}
}, [filteredData]);
// Add cleanup effect to reset scroll position when component unmounts
useEffect(() => {
return () => {
// Reset the last scroll position reference
lastScrollPosition.current = { left: 0, top: 0 };
// Reset the scroll container if it exists
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTop = 0;
scrollContainerRef.current.scrollLeft = 0;
}
};
}, []);
return (
<div className="flex flex-col h-[calc(100vh-10rem)] overflow-hidden">
<div className="flex-1 overflow-hidden">

View File

@@ -385,6 +385,23 @@ const ValidationTable = <T extends string>({
return (
<CopyDownContext.Provider value={copyDownContextValue}>
<div className="min-w-max relative">
{/* Add global styles for copy down mode */}
{isInCopyDownMode && (
<style>
{`
.copy-down-target-row,
.copy-down-target-row *,
.copy-down-target-row input,
.copy-down-target-row textarea,
.copy-down-target-row div,
.copy-down-target-row button,
.target-row-cell,
.target-row-cell * {
cursor: pointer !important;
}
`}
</style>
)}
{isInCopyDownMode && sourceRowIndex !== null && sourceFieldKey !== null && (
<div className="sticky top-0 z-30 h-0 overflow-visible">
<div
@@ -467,8 +484,15 @@ const ValidationTable = <T extends string>({
"hover:bg-muted/50",
row.getIsSelected() ? "bg-muted/50" : "",
validationErrors.get(data.indexOf(row.original)) &&
Object.keys(validationErrors.get(data.indexOf(row.original)) || {}).length > 0 ? "bg-red-50/40" : ""
Object.keys(validationErrors.get(data.indexOf(row.original)) || {}).length > 0 ? "bg-red-50/40" : "",
// Add cursor-pointer class when in copy down mode for target rows
isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? "cursor-pointer copy-down-target-row" : ""
)}
style={{
// Force cursor pointer on all target rows
cursor: isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? 'pointer' : undefined,
position: 'relative' // Ensure we can position the overlay
}}
onMouseEnter={() => handleRowMouseEnter(row.index)}
>
{row.getVisibleCells().map((cell, cellIndex) => {
@@ -481,8 +505,11 @@ const ValidationTable = <T extends string>({
minWidth: `${width}px`,
maxWidth: `${width}px`,
boxSizing: 'border-box',
padding: '0'
padding: '0',
// Force cursor pointer on all cells in target rows
cursor: isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? 'pointer' : undefined
}}
className={isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? "target-row-cell" : ""}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>

View File

@@ -177,7 +177,9 @@ const InputCell = <T extends string>({
hasClass('!border-blue-200') ? '#bfdbfe' :
hasClass('!border-blue-200') && isHovered ? '#bfdbfe' :
undefined,
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined,
borderWidth: hasClass('!border-blue-500') || hasClass('!border-blue-200') ? '0px' : undefined,
cursor: hasClass('hover:!bg-blue-100') ? 'pointer' : undefined
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
@@ -225,7 +227,9 @@ const InputCell = <T extends string>({
borderColor: hasClass('!border-blue-500') ? '#3b82f6' :
hasClass('!border-blue-200') ? '#bfdbfe' :
undefined,
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined,
borderWidth: hasClass('!border-blue-500') || hasClass('!border-blue-200') ? '0px' : undefined,
cursor: hasClass('hover:!bg-blue-100') ? 'pointer' : undefined
}}
/>
) : (
@@ -245,7 +249,9 @@ const InputCell = <T extends string>({
hasClass('!border-blue-200') ? '#bfdbfe' :
hasClass('!border-blue-200') && isHovered ? '#bfdbfe' :
undefined,
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined,
borderWidth: hasClass('!border-blue-500') || hasClass('!border-blue-200') ? '0px' : undefined,
cursor: hasClass('hover:!bg-blue-100') ? 'pointer' : 'text'
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}

View File

@@ -191,9 +191,17 @@ const MultiSelectCell = <T extends string>({
if (onEndEdit) onEndEdit();
} else if (newOpen && !open) {
// Sync internal state with external state when opening
setInternalValue(value);
setInternalValue(Array.isArray(value) ? value : []);
setSearchQuery(""); // Reset search query on open
if (onStartEdit) onStartEdit();
} else if (!newOpen) {
// Handle case when dropdown is already closed but handleOpenChange is called
// This ensures values are saved when clicking the chevron to close
if (internalValue.length !== value.length ||
internalValue.some((v, i) => v !== value[i])) {
onChange(internalValue);
}
if (onEndEdit) onEndEdit();
}
}, [open, internalValue, value, onChange, onStartEdit, onEndEdit]);
@@ -343,7 +351,8 @@ const MultiSelectCell = <T extends string>({
hasClass('!border-blue-200') ? '#bfdbfe' :
hasClass('!border-blue-200') && isHovered ? '#bfdbfe' :
undefined,
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined,
cursor: hasClass('hover:!bg-blue-100') ? 'pointer' : undefined
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
@@ -383,7 +392,8 @@ const MultiSelectCell = <T extends string>({
hasClass('!border-blue-200') && isHovered ? '#bfdbfe' :
undefined,
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined,
borderWidth: hasClass('!border-blue-500') || hasClass('!border-blue-200') ? '0px' : undefined
borderWidth: hasClass('!border-blue-500') || hasClass('!border-blue-200') ? '0px' : undefined,
cursor: hasClass('hover:!bg-blue-100') ? 'pointer' : undefined
}}
onClick={(e) => {
// Don't open the dropdown if we're in copy down mode
@@ -395,7 +405,11 @@ const MultiSelectCell = <T extends string>({
// Only prevent default and stop propagation if not in copy down mode
e.preventDefault();
e.stopPropagation();
setOpen(!open);
// Toggle the open state and call handleOpenChange to ensure values are saved
const newOpenState = !open;
setOpen(newOpenState);
handleOpenChange(newOpenState);
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}

View File

@@ -149,7 +149,9 @@ const MultilineInput = <T extends string>({
hasClass('!border-blue-200') ? '#bfdbfe' :
hasClass('!border-blue-200') && isHovered ? '#bfdbfe' :
undefined,
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined,
borderWidth: hasClass('!border-blue-500') || hasClass('!border-blue-200') ? '0px' : undefined,
cursor: hasClass('hover:!bg-blue-100') ? 'pointer' : undefined
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
@@ -181,7 +183,9 @@ const MultilineInput = <T extends string>({
hasClass('!border-blue-200') ? '#bfdbfe' :
hasClass('!border-blue-200') && isHovered ? '#bfdbfe' :
undefined,
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined,
borderWidth: hasClass('!border-blue-500') || hasClass('!border-blue-200') ? '0px' : undefined,
cursor: hasClass('hover:!bg-blue-100') ? 'pointer' : 'pointer'
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}

View File

@@ -163,7 +163,8 @@ const SelectCell = <T extends string>({
hasClass('!border-blue-200') ? '#bfdbfe' :
hasClass('!border-blue-200') && isHovered ? '#bfdbfe' :
undefined,
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined,
cursor: hasClass('hover:!bg-blue-100') ? 'pointer' : undefined
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
@@ -207,18 +208,19 @@ const SelectCell = <T extends string>({
hasClass('!border-blue-200') && isHovered ? '#bfdbfe' :
undefined,
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined,
borderWidth: hasClass('!border-blue-500') || hasClass('!border-blue-200') ? '0px' : undefined
borderWidth: hasClass('!border-blue-500') || hasClass('!border-blue-200') ? '0px' : undefined,
cursor: hasClass('hover:!bg-blue-100') ? 'pointer' : undefined
}}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
// Don't open the dropdown if we're in copy down mode
if (hasClass('!bg-blue-100') || hasClass('!bg-blue-200') || hasClass('hover:!bg-blue-100')) {
// Let the parent cell handle the click
// Let the parent cell handle the click by NOT preventing default or stopping propagation
return;
}
// Only prevent default and stop propagation if not in copy down mode
e.preventDefault();
e.stopPropagation();
setOpen(!open);
if (!open && onStartEdit) onStartEdit();
}}