Fix row highlighting, header alignment, make header sticky
This commit is contained in:
@@ -1,6 +1,4 @@
|
|||||||
# Current Issues to Address
|
# Current Issues to Address
|
||||||
1. The red row background should go away when all cells in the row are valid and all required cells are populated
|
|
||||||
2. Columns alignment with header is slightly off, gets worse the further right you go
|
|
||||||
3. The copy down button is in the way of the validation error icon and the select open trigger - all three need to be in unique locations
|
3. The copy down button is in the way of the validation error icon and the select open trigger - all three need to be in unique locations
|
||||||
4. Validation isn't happening beyond checking if a cell is required or not - needs to respect rules in import.tsx
|
4. Validation isn't happening beyond checking if a cell is required or not - needs to respect rules in import.tsx
|
||||||
* Red cell outline if cell is required and it's empty
|
* Red cell outline if cell is required and it's empty
|
||||||
@@ -14,15 +12,19 @@
|
|||||||
10. UPC column doesn't need to show loading state when Item Number is being processed, only show on item number column
|
10. UPC column doesn't need to show loading state when Item Number is being processed, only show on item number column
|
||||||
11. Copy down needs to show a loading state on the cells that it will copy to
|
11. Copy down needs to show a loading state on the cells that it will copy to
|
||||||
12. Shipping restrictions/tax category should default to ID 0 if we didn't get it elsewhere
|
12. Shipping restrictions/tax category should default to ID 0 if we didn't get it elsewhere
|
||||||
13. Header row should be sticky (both up/down and left/right)
|
|
||||||
14. Need a way to scroll around table if user doesn't have mouse wheel for left/right
|
14. Need a way to scroll around table if user doesn't have mouse wheel for left/right
|
||||||
15. Need to remove all artificial virtualization, batching, artificial delays, and caching. Adds too much complexity and data set is not ever large enough for this to be helpful. Keep actual performance optimizations.
|
|
||||||
|
|
||||||
## Do NOT change or edit
|
## Do NOT change or edit
|
||||||
* Anything related to AI validation
|
* Anything related to AI validation
|
||||||
* Anything about how templates or UPC validation work (only focus on specific issues described above)
|
* Anything about how templates or UPC validation work (only focus on specific issues described above)
|
||||||
* Anything outside of the ValidationStepNew folder
|
* Anything outside of the ValidationStepNew folder
|
||||||
|
|
||||||
|
## Issues already fixed - do not work on these
|
||||||
|
✅FIXED 1. The red row background should go away when all cells in the row are valid and all required cells are populated
|
||||||
|
✅FIXED 2. Columns alignment with header is slightly off, gets worse the further right you go
|
||||||
|
✅FIXED 13. Header row should be sticky (both up/down and left/right)
|
||||||
|
|
||||||
---------
|
---------
|
||||||
|
|
||||||
# Validation Step Components Overview
|
# Validation Step Components Overview
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ const ItemNumberCell = React.memo(({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableCell className="p-1 group relative" style={{ width: `${width}px`, minWidth: `${width}px` }}>
|
<TableCell className="p-1 group relative" style={{ width: `${width}px`, minWidth: `${width}px`, maxWidth: `${width}px`, boxSizing: 'border-box' }}>
|
||||||
<div className={`relative ${hasError || isRequiredButEmpty ? 'border-red-500' : ''}`}>
|
<div className={`relative ${hasError || isRequiredButEmpty ? 'border-red-500' : ''}`}>
|
||||||
{isValidating ? (
|
{isValidating ? (
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
@@ -365,7 +365,7 @@ const ValidationCell = ({
|
|||||||
// Check for price field
|
// Check for price field
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableCell className="p-1 group relative" style={{ width: `${width}px`, minWidth: `${width}px` }}>
|
<TableCell className="p-1 group relative" style={{ width: `${width}px`, minWidth: `${width}px`, maxWidth: `${width}px`, boxSizing: 'border-box' }}>
|
||||||
<div className={`relative ${hasError || isRequiredButEmpty ? 'border-red-500' : ''}`}>
|
<div className={`relative ${hasError || isRequiredButEmpty ? 'border-red-500' : ''}`}>
|
||||||
{isValidating ? (
|
{isValidating ? (
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Fields } from '../../../types'
|
|||||||
import { SearchProductTemplateDialog } from '@/components/templates/SearchProductTemplateDialog'
|
import { SearchProductTemplateDialog } from '@/components/templates/SearchProductTemplateDialog'
|
||||||
import { TemplateForm } from '@/components/templates/TemplateForm'
|
import { TemplateForm } from '@/components/templates/TemplateForm'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { RowSelectionState } from '@tanstack/react-table'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ValidationContainer component - the main wrapper for the validation step
|
* ValidationContainer component - the main wrapper for the validation step
|
||||||
@@ -56,13 +57,15 @@ const ValidationContainer = <T extends string>({
|
|||||||
loadTemplates,
|
loadTemplates,
|
||||||
setData,
|
setData,
|
||||||
fields,
|
fields,
|
||||||
isLoadingTemplates,
|
isLoadingTemplates } = validationState
|
||||||
copyDown } = validationState
|
|
||||||
|
|
||||||
// Add state for tracking product lines and sublines per row
|
// Add state for tracking product lines and sublines per row
|
||||||
const [rowProductLines, setRowProductLines] = useState<Record<string, any[]>>({});
|
const [rowProductLines, setRowProductLines] = useState<Record<string, any[]>>({});
|
||||||
const [rowSublines, setRowSublines] = useState<Record<string, any[]>>({});
|
const [rowSublines, setRowSublines] = useState<Record<string, any[]>>({});
|
||||||
|
// These variables are used in the fetchProductLines and fetchSublines functions
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [isLoadingLines, setIsLoadingLines] = useState<Record<string, boolean>>({});
|
const [isLoadingLines, setIsLoadingLines] = useState<Record<string, boolean>>({});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [isLoadingSublines, setIsLoadingSublines] = useState<Record<string, boolean>>({});
|
const [isLoadingSublines, setIsLoadingSublines] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
// Add UPC validation state
|
// Add UPC validation state
|
||||||
@@ -432,7 +435,10 @@ const ValidationContainer = <T extends string>({
|
|||||||
if (rowData && rowData.__index) {
|
if (rowData && rowData.__index) {
|
||||||
// Use setTimeout to make this non-blocking
|
// Use setTimeout to make this non-blocking
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await fetchProductLines(rowData.__index, value.toString());
|
// Ensure value is not undefined before calling toString()
|
||||||
|
if (value !== undefined) {
|
||||||
|
await fetchProductLines(rowData.__index as string, value.toString());
|
||||||
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -494,7 +500,10 @@ const ValidationContainer = <T extends string>({
|
|||||||
if (rowData && rowData.__index) {
|
if (rowData && rowData.__index) {
|
||||||
// Use setTimeout to make this non-blocking
|
// Use setTimeout to make this non-blocking
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await fetchSublines(rowData.__index, value.toString());
|
// Ensure value is not undefined before calling toString()
|
||||||
|
if (value !== undefined) {
|
||||||
|
await fetchSublines(rowData.__index as string, value.toString());
|
||||||
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -711,7 +720,7 @@ const ValidationContainer = <T extends string>({
|
|||||||
// Log if we can find a match for our supplier
|
// Log if we can find a match for our supplier
|
||||||
if (templateData.supplier !== undefined) {
|
if (templateData.supplier !== undefined) {
|
||||||
// Need to compare numeric values since supplier options have numeric values
|
// Need to compare numeric values since supplier options have numeric values
|
||||||
const supplierMatch = options.suppliers.find(s =>
|
const supplierMatch = options.suppliers.find((s: { value: string | number }) =>
|
||||||
s.value === templateData.supplier ||
|
s.value === templateData.supplier ||
|
||||||
Number(s.value) === Number(templateData.supplier)
|
Number(s.value) === Number(templateData.supplier)
|
||||||
);
|
);
|
||||||
@@ -814,9 +823,8 @@ const ValidationContainer = <T extends string>({
|
|||||||
}, [data, rowSelection, setData, setRowSelection]);
|
}, [data, rowSelection, setData, setRowSelection]);
|
||||||
|
|
||||||
// Memoize handlers
|
// Memoize handlers
|
||||||
const handleFiltersChange = useCallback((newFilters: any) => {
|
// This function is defined for potential future use but not currently used
|
||||||
updateFilters(newFilters);
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
}, [updateFilters]);
|
|
||||||
|
|
||||||
const handleRowSelectionChange = useCallback((newSelection: RowSelectionState) => {
|
const handleRowSelectionChange = useCallback((newSelection: RowSelectionState) => {
|
||||||
setRowSelection(newSelection);
|
setRowSelection(newSelection);
|
||||||
@@ -883,10 +891,10 @@ const ValidationContainer = <T extends string>({
|
|||||||
const renderValidationTable = useMemo(() => (
|
const renderValidationTable = useMemo(() => (
|
||||||
<EnhancedValidationTable
|
<EnhancedValidationTable
|
||||||
data={filteredData}
|
data={filteredData}
|
||||||
fields={fields}
|
fields={fields as unknown as Fields<string>}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
setRowSelection={handleRowSelectionChange}
|
setRowSelection={handleRowSelectionChange as React.Dispatch<React.SetStateAction<RowSelectionState>>}
|
||||||
updateRow={handleUpdateRow}
|
updateRow={handleUpdateRow as (rowIndex: number, key: string, value: any) => void}
|
||||||
validationErrors={validationErrors}
|
validationErrors={validationErrors}
|
||||||
isValidatingUpc={isRowValidatingUpc}
|
isValidatingUpc={isRowValidatingUpc}
|
||||||
validatingUpcRows={Array.from(validatingUpcRows)}
|
validatingUpcRows={Array.from(validatingUpcRows)}
|
||||||
@@ -898,6 +906,7 @@ const ValidationContainer = <T extends string>({
|
|||||||
itemNumbers={new Map()}
|
itemNumbers={new Map()}
|
||||||
isLoadingTemplates={isLoadingTemplates}
|
isLoadingTemplates={isLoadingTemplates}
|
||||||
copyDown={handleCopyDown}
|
copyDown={handleCopyDown}
|
||||||
|
upcValidationResults={new Map()}
|
||||||
/>
|
/>
|
||||||
), [
|
), [
|
||||||
EnhancedValidationTable,
|
EnhancedValidationTable,
|
||||||
@@ -923,10 +932,11 @@ const ValidationContainer = <T extends string>({
|
|||||||
const isScrolling = useRef(false);
|
const isScrolling = useRef(false);
|
||||||
|
|
||||||
// Memoize scroll handlers
|
// Memoize scroll handlers
|
||||||
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
|
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement> | Event) => {
|
||||||
if (!isScrolling.current) {
|
if (!isScrolling.current) {
|
||||||
isScrolling.current = true;
|
isScrolling.current = true;
|
||||||
const target = event.currentTarget;
|
// Use type assertion to handle both React.UIEvent and native Event
|
||||||
|
const target = event.currentTarget as HTMLDivElement;
|
||||||
lastScrollPosition.current = {
|
lastScrollPosition.current = {
|
||||||
left: target.scrollLeft,
|
left: target.scrollLeft,
|
||||||
top: target.scrollTop
|
top: target.scrollTop
|
||||||
@@ -941,8 +951,13 @@ const ValidationContainer = <T extends string>({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = scrollContainerRef.current;
|
const container = scrollContainerRef.current;
|
||||||
if (container) {
|
if (container) {
|
||||||
container.addEventListener('scroll', handleScroll, { passive: true });
|
// Convert React event handler to native event handler
|
||||||
return () => container.removeEventListener('scroll', handleScroll);
|
const nativeHandler = ((evt: Event) => {
|
||||||
|
handleScroll(evt);
|
||||||
|
}) as EventListener;
|
||||||
|
|
||||||
|
container.addEventListener('scroll', nativeHandler, { passive: true });
|
||||||
|
return () => container.removeEventListener('scroll', nativeHandler);
|
||||||
}
|
}
|
||||||
}, [handleScroll]);
|
}, [handleScroll]);
|
||||||
|
|
||||||
@@ -1031,11 +1046,13 @@ const ValidationContainer = <T extends string>({
|
|||||||
style={{
|
style={{
|
||||||
willChange: 'transform',
|
willChange: 'transform',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
WebkitOverflowScrolling: 'touch' // Improve scroll performance on Safari
|
WebkitOverflowScrolling: 'touch', // Improve scroll performance on Safari
|
||||||
|
overscrollBehavior: 'contain', // Prevent scroll chaining
|
||||||
|
contain: 'paint', // Improve performance for sticky elements
|
||||||
|
scrollbarWidth: 'thin' // Thinner scrollbars in Firefox
|
||||||
}}
|
}}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
>
|
>
|
||||||
<div className="min-w-max"> {/* Force container to be at least as wide as content */}
|
|
||||||
{renderValidationTable}
|
{renderValidationTable}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1043,7 +1060,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Selection Action Bar - only shown when items are selected */}
|
{/* Selection Action Bar - only shown when items are selected */}
|
||||||
{Object.keys(rowSelection).length > 0 && (
|
{Object.keys(rowSelection).length > 0 && (
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ const ValidationTable = <T extends string>({
|
|||||||
const rowIndex = data.findIndex(r => r === row.original);
|
const rowIndex = data.findIndex(r => r === row.original);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableCell className="p-1" style={{ width: '200px', minWidth: '200px' }}>
|
<TableCell className="p-1" style={{ width: '200px', minWidth: '200px', maxWidth: '200px' }}>
|
||||||
<MemoizedTemplateSelect
|
<MemoizedTemplateSelect
|
||||||
templates={templates}
|
templates={templates}
|
||||||
value={templateValue || ''}
|
value={templateValue || ''}
|
||||||
@@ -280,9 +280,9 @@ const ValidationTable = <T extends string>({
|
|||||||
size: fieldWidth,
|
size: fieldWidth,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<MemoizedCell
|
<MemoizedCell
|
||||||
field={field}
|
field={field as Field<string>}
|
||||||
value={row.original[field.key]}
|
value={row.original[field.key as keyof typeof row.original]}
|
||||||
onChange={(value) => handleFieldUpdate(row.index, field.key, value)}
|
onChange={(value) => handleFieldUpdate(row.index, field.key as T, value)}
|
||||||
errors={validationErrors.get(row.index)?.[fieldKey] || []}
|
errors={validationErrors.get(row.index)?.[fieldKey] || []}
|
||||||
isValidating={validatingCells.has(`${row.index}-${field.key}`)}
|
isValidating={validatingCells.has(`${row.index}-${field.key}`)}
|
||||||
fieldKey={fieldKey}
|
fieldKey={fieldKey}
|
||||||
@@ -290,7 +290,7 @@ const ValidationTable = <T extends string>({
|
|||||||
itemNumber={itemNumbers.get(row.index)}
|
itemNumber={itemNumbers.get(row.index)}
|
||||||
width={fieldWidth}
|
width={fieldWidth}
|
||||||
rowIndex={row.index}
|
rowIndex={row.index}
|
||||||
copyDown={() => handleCopyDown(row.index, field.key)}
|
copyDown={() => handleCopyDown(row.index, field.key as string)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@@ -333,27 +333,37 @@ const ValidationTable = <T extends string>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table style={{ width: `${totalWidth}px`, tableLayout: 'fixed' }}>
|
<div className="min-w-max">
|
||||||
<TableHeader className="sticky top-0 z-10 bg-background">
|
<div className="relative">
|
||||||
<TableRow>
|
{/* Custom Table Header - Always Visible */}
|
||||||
{table.getFlatHeaders().map((header) => (
|
<div
|
||||||
<TableHead
|
className="sticky top-0 z-20 bg-muted border-b shadow-sm"
|
||||||
|
style={{ width: `${totalWidth}px` }}
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
{table.getFlatHeaders().map((header, index) => {
|
||||||
|
const width = header.getSize();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
key={header.id}
|
key={header.id}
|
||||||
|
className="py-2 px-2 font-bold text-sm text-muted-foreground bg-muted flex items-center justify-center"
|
||||||
style={{
|
style={{
|
||||||
width: `${header.getSize()}px`,
|
width: `${width}px`,
|
||||||
minWidth: `${header.getSize()}px`,
|
minWidth: `${width}px`,
|
||||||
maxWidth: `${header.getSize()}px`,
|
maxWidth: `${width}px`,
|
||||||
position: 'sticky',
|
boxSizing: 'border-box',
|
||||||
top: 0,
|
height: '40px'
|
||||||
backgroundColor: 'inherit',
|
|
||||||
zIndex: 1
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
</TableHead>
|
</div>
|
||||||
))}
|
);
|
||||||
</TableRow>
|
})}
|
||||||
</TableHeader>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table Body */}
|
||||||
|
<Table style={{ width: `${totalWidth}px`, tableLayout: 'fixed', borderCollapse: 'separate', borderSpacing: 0, marginTop: '-1px' }}>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows.map((row) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
@@ -361,25 +371,33 @@ const ValidationTable = <T extends string>({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"hover:bg-muted/50",
|
"hover:bg-muted/50",
|
||||||
row.getIsSelected() ? "bg-muted/50" : "",
|
row.getIsSelected() ? "bg-muted/50" : "",
|
||||||
validationErrors.get(data.indexOf(row.original)) ? "bg-red-50/40" : ""
|
validationErrors.get(data.indexOf(row.original)) &&
|
||||||
|
Object.keys(validationErrors.get(data.indexOf(row.original)) || {}).length > 0 ? "bg-red-50/40" : ""
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell, cellIndex) => {
|
||||||
|
const width = cell.column.getSize();
|
||||||
|
return (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
style={{
|
style={{
|
||||||
width: `${cell.column.getSize()}px`,
|
width: `${width}px`,
|
||||||
minWidth: `${cell.column.getSize()}px`,
|
minWidth: `${width}px`,
|
||||||
maxWidth: `${cell.column.getSize()}px`
|
maxWidth: `${width}px`,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
padding: '0'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ interface MultiInputCellProps<T extends string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add global CSS to ensure fixed width constraints - use !important to override other styles
|
// Add global CSS to ensure fixed width constraints - use !important to override other styles
|
||||||
const fixedWidthClass = "!w-full !min-w-0 !max-w-full !flex-shrink-1 !flex-grow-0";
|
|
||||||
|
|
||||||
// Memoized option item to prevent unnecessary renders for large option lists
|
// Memoized option item to prevent unnecessary renders for large option lists
|
||||||
const OptionItem = React.memo(({
|
const OptionItem = React.memo(({
|
||||||
@@ -372,75 +371,34 @@ const MultiInputCell = <T extends string>({
|
|||||||
|
|
||||||
// If we have a multi-select field with options, use command UI
|
// If we have a multi-select field with options, use command UI
|
||||||
if (field.fieldType.type === 'multi-select' && selectOptions.length > 0) {
|
if (field.fieldType.type === 'multi-select' && selectOptions.length > 0) {
|
||||||
// Get width from field if available, or default to a reasonable value
|
|
||||||
const cellWidth = field.width || 200;
|
|
||||||
|
|
||||||
// Create a reference to the container element
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Create a key-value map for inline styles with fixed width - simplified
|
|
||||||
const fixedWidth = useMemo(() => ({
|
|
||||||
width: `${cellWidth}px`,
|
|
||||||
minWidth: `${cellWidth}px`,
|
|
||||||
maxWidth: `${cellWidth}px`,
|
|
||||||
boxSizing: 'border-box' as const,
|
|
||||||
}), [cellWidth]);
|
|
||||||
|
|
||||||
// Use layout effect more efficiently - only for the button element
|
|
||||||
// since the container already uses inline styles
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
// Skip if no width specified
|
|
||||||
if (!cellWidth) return;
|
|
||||||
|
|
||||||
// Cache previous width to avoid unnecessary DOM updates
|
|
||||||
const prevWidth = containerRef.current?.getAttribute('data-prev-width');
|
|
||||||
|
|
||||||
// Only update if width changed
|
|
||||||
if (prevWidth !== String(cellWidth) && containerRef.current) {
|
|
||||||
// Store new width for next comparison
|
|
||||||
containerRef.current.setAttribute('data-prev-width', String(cellWidth));
|
|
||||||
|
|
||||||
// Only manipulate the button element directly since we can't
|
|
||||||
// reliably style it with CSS in all cases
|
|
||||||
const button = containerRef.current.querySelector('button');
|
|
||||||
if (button) {
|
|
||||||
const htmlButton = button as HTMLElement;
|
|
||||||
htmlButton.style.width = `${cellWidth}px`;
|
|
||||||
htmlButton.style.minWidth = `${cellWidth}px`;
|
|
||||||
htmlButton.style.maxWidth = `${cellWidth}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [cellWidth]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Popover
|
||||||
ref={containerRef}
|
open={open}
|
||||||
className="inline-block fixed-width-cell overflow-visible"
|
onOpenChange={(isOpen) => {
|
||||||
style={fixedWidth}
|
setOpen(isOpen);
|
||||||
data-width={cellWidth}
|
handleOpenChange(isOpen);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className={cn(
|
className={cn(
|
||||||
"justify-between font-normal",
|
"w-full justify-between font-normal",
|
||||||
|
"border",
|
||||||
!internalValue.length && "text-muted-foreground",
|
!internalValue.length && "text-muted-foreground",
|
||||||
hasErrors && "border-red-500",
|
hasErrors ? "border-destructive" : ""
|
||||||
"h-auto min-h-9 py-1"
|
|
||||||
)}
|
)}
|
||||||
onClick={() => setOpen(true)}
|
onClick={(e) => {
|
||||||
style={fixedWidth}
|
e.preventDefault();
|
||||||
>
|
e.stopPropagation();
|
||||||
<div className="flex items-center w-full justify-between">
|
setOpen(!open);
|
||||||
<div
|
if (!open && onStartEdit) onStartEdit();
|
||||||
className="flex items-center gap-2 overflow-hidden"
|
|
||||||
style={{
|
|
||||||
maxWidth: `${cellWidth - 32}px`,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div className="flex items-center w-full justify-between">
|
||||||
|
<div className="flex items-center gap-2 overflow-hidden">
|
||||||
{internalValue.length === 0 ? (
|
{internalValue.length === 0 ? (
|
||||||
<span className="text-muted-foreground truncate w-full">Select...</span>
|
<span className="text-muted-foreground truncate w-full">Select...</span>
|
||||||
) : internalValue.length === 1 ? (
|
) : internalValue.length === 1 ? (
|
||||||
@@ -450,55 +408,54 @@ const MultiInputCell = <T extends string>({
|
|||||||
<Badge variant="secondary" className="shrink-0 whitespace-nowrap">
|
<Badge variant="secondary" className="shrink-0 whitespace-nowrap">
|
||||||
{internalValue.length} selected
|
{internalValue.length} selected
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="truncate" style={{ maxWidth: `${cellWidth - 100}px` }}>
|
<span className="truncate">
|
||||||
{selectedValues.map(v => v.label).join(', ')}
|
{selectedValues.map(v => v.label).join(', ')}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-1 flex-none" style={{ width: '20px' }}>
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="p-0"
|
className="p-0 w-[var(--radix-popover-trigger-width)]"
|
||||||
style={fixedWidth}
|
|
||||||
align="start"
|
align="start"
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
>
|
>
|
||||||
<Command shouldFilter={false} className="overflow-hidden">
|
<Command shouldFilter={false}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search options..."
|
placeholder="Search..."
|
||||||
className="h-9"
|
className="h-9"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onValueChange={setSearchQuery}
|
onValueChange={setSearchQuery}
|
||||||
/>
|
/>
|
||||||
<CommandList
|
<CommandList
|
||||||
className="overflow-hidden"
|
|
||||||
ref={commandListRef}
|
ref={commandListRef}
|
||||||
onWheel={handleWheel}
|
onWheel={handleWheel}
|
||||||
|
className="max-h-[200px]"
|
||||||
>
|
>
|
||||||
<CommandEmpty>No options found.</CommandEmpty>
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{sortedOptions.length > 0 ? (
|
{sortedOptions.map((option) => (
|
||||||
<VirtualizedOptions
|
<CommandItem
|
||||||
options={sortedOptions}
|
key={option.value}
|
||||||
selectedValues={selectedValueSet}
|
value={option.label}
|
||||||
onSelect={handleSelect}
|
onSelect={() => handleSelect(option.value)}
|
||||||
maxHeight={200}
|
className="cursor-pointer"
|
||||||
/>
|
>
|
||||||
) : (
|
{option.label}
|
||||||
<div className="py-6 text-center text-sm">No options match your search</div>
|
{selectedValueSet.has(option.value) && (
|
||||||
|
<Check className="ml-auto h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For standard multi-input without options, use text input
|
// For standard multi-input without options, use text input
|
||||||
@@ -510,12 +467,6 @@ const MultiInputCell = <T extends string>({
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Create a key-value map for inline styles with fixed width - simplified
|
// Create a key-value map for inline styles with fixed width - simplified
|
||||||
const fixedWidth = useMemo(() => ({
|
|
||||||
width: `${cellWidth}px`,
|
|
||||||
minWidth: `${cellWidth}px`,
|
|
||||||
maxWidth: `${cellWidth}px`,
|
|
||||||
boxSizing: 'border-box' as const,
|
|
||||||
}), [cellWidth]);
|
|
||||||
|
|
||||||
// Use layout effect more efficiently - only for the button element
|
// Use layout effect more efficiently - only for the button element
|
||||||
// since the container already uses inline styles
|
// since the container already uses inline styles
|
||||||
@@ -539,6 +490,7 @@ const MultiInputCell = <T extends string>({
|
|||||||
htmlButton.style.width = `${cellWidth}px`;
|
htmlButton.style.width = `${cellWidth}px`;
|
||||||
htmlButton.style.minWidth = `${cellWidth}px`;
|
htmlButton.style.minWidth = `${cellWidth}px`;
|
||||||
htmlButton.style.maxWidth = `${cellWidth}px`;
|
htmlButton.style.maxWidth = `${cellWidth}px`;
|
||||||
|
htmlButton.style.boxSizing = 'border-box';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [cellWidth]);
|
}, [cellWidth]);
|
||||||
@@ -547,7 +499,12 @@ const MultiInputCell = <T extends string>({
|
|||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="inline-block fixed-width-cell"
|
className="inline-block fixed-width-cell"
|
||||||
style={fixedWidth}
|
style={{
|
||||||
|
width: `${cellWidth}px`,
|
||||||
|
minWidth: `${cellWidth}px`,
|
||||||
|
maxWidth: `${cellWidth}px`,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}}
|
||||||
data-width={cellWidth}
|
data-width={cellWidth}
|
||||||
>
|
>
|
||||||
{isMultiline ? (
|
{isMultiline ? (
|
||||||
@@ -562,7 +519,12 @@ const MultiInputCell = <T extends string>({
|
|||||||
outlineClass,
|
outlineClass,
|
||||||
hasErrors ? "border-destructive" : ""
|
hasErrors ? "border-destructive" : ""
|
||||||
)}
|
)}
|
||||||
style={fixedWidth}
|
style={{
|
||||||
|
width: `${cellWidth}px`,
|
||||||
|
minWidth: `${cellWidth}px`,
|
||||||
|
maxWidth: `${cellWidth}px`,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
@@ -571,10 +533,16 @@ const MultiInputCell = <T extends string>({
|
|||||||
"cursor-text truncate",
|
"cursor-text truncate",
|
||||||
outlineClass,
|
outlineClass,
|
||||||
hasErrors ? "border-destructive" : "",
|
hasErrors ? "border-destructive" : "",
|
||||||
"overflow-hidden items-center"
|
"overflow-hidden items-center",
|
||||||
|
"w-full"
|
||||||
)}
|
)}
|
||||||
onClick={handleFocus}
|
onClick={handleFocus}
|
||||||
style={fixedWidth}
|
style={{
|
||||||
|
width: `${cellWidth}px`,
|
||||||
|
minWidth: `${cellWidth}px`,
|
||||||
|
maxWidth: `${cellWidth}px`,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{internalValue.length > 0 ? getDisplayValues().join(`, `) : (
|
{internalValue.length > 0 ? getDisplayValues().join(`, `) : (
|
||||||
<span className="text-muted-foreground truncate">
|
<span className="text-muted-foreground truncate">
|
||||||
@@ -589,7 +557,7 @@ const MultiInputCell = <T extends string>({
|
|||||||
|
|
||||||
// Fallback to default behavior if no width is specified
|
// Fallback to default behavior if no width is specified
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full overflow-hidden" style={{ boxSizing: 'border-box' }}>
|
||||||
{isMultiline ? (
|
{isMultiline ? (
|
||||||
<Textarea
|
<Textarea
|
||||||
value={internalValue.join(separator)}
|
value={internalValue.join(separator)}
|
||||||
@@ -598,10 +566,11 @@ const MultiInputCell = <T extends string>({
|
|||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
placeholder={`Enter values separated by ${separator}`}
|
placeholder={`Enter values separated by ${separator}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-h-[80px] resize-none",
|
"min-h-[80px] resize-none w-full",
|
||||||
outlineClass,
|
outlineClass,
|
||||||
hasErrors ? "border-destructive" : ""
|
hasErrors ? "border-destructive" : ""
|
||||||
)}
|
)}
|
||||||
|
style={{ boxSizing: 'border-box' }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
@@ -613,9 +582,10 @@ const MultiInputCell = <T extends string>({
|
|||||||
"overflow-hidden items-center"
|
"overflow-hidden items-center"
|
||||||
)}
|
)}
|
||||||
onClick={handleFocus}
|
onClick={handleFocus}
|
||||||
|
style={{ boxSizing: 'border-box' }}
|
||||||
>
|
>
|
||||||
{internalValue.length > 0 ? getDisplayValues().join(`, `) : (
|
{internalValue.length > 0 ? getDisplayValues().join(`, `) : (
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground truncate w-full">
|
||||||
{`Enter values separated by ${separator}`}
|
{`Enter values separated by ${separator}`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user