mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 11:35:24 +02:00
fix: portal all filter popovers to prevent occlusion by sticky columns
This commit is contained in:
@@ -32,7 +32,8 @@ export function ColumnFilterPopover({
|
|||||||
onClose,
|
onClose,
|
||||||
title,
|
title,
|
||||||
className,
|
className,
|
||||||
zIndex = 50
|
zIndex = 50,
|
||||||
|
triggerId
|
||||||
}: ColumnFilterPopoverProps) {
|
}: ColumnFilterPopoverProps) {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [activeTab, setActiveTab] = useState<'values' | 'condition'>('values');
|
const [activeTab, setActiveTab] = useState<'values' | 'condition'>('values');
|
||||||
@@ -52,15 +53,16 @@ export function ColumnFilterPopover({
|
|||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
setPortalContainer(container);
|
setPortalContainer(container);
|
||||||
|
|
||||||
const btns = document.querySelectorAll('.filter-trigger-btn');
|
if (triggerId) {
|
||||||
btns.forEach(btn => {
|
const trigger = document.getElementById(triggerId);
|
||||||
if (btn instanceof HTMLElement) {
|
if (trigger) {
|
||||||
const rect = btn.getBoundingClientRect();
|
const rect = trigger.getBoundingClientRect();
|
||||||
const currentLeft = rect.left;
|
setPosition({
|
||||||
const currentTop = rect.bottom + 4;
|
top: rect.bottom + window.scrollY + 4,
|
||||||
setPosition({ top: currentTop, left: currentLeft });
|
left: Math.min(rect.left + window.scrollX, window.innerWidth - 300)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (document.body.contains(container)) {
|
if (document.body.contains(container)) {
|
||||||
@@ -195,7 +197,11 @@ export function ColumnFilterPopover({
|
|||||||
"absolute w-72 bg-slate-800 border border-slate-700 rounded-lg shadow-2xl p-3 flex flex-col gap-3 animate-in fade-in zoom-in-95 duration-100 pointer-events-auto",
|
"absolute w-72 bg-slate-800 border border-slate-700 rounded-lg shadow-2xl p-3 flex flex-col gap-3 animate-in fade-in zoom-in-95 duration-100 pointer-events-auto",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={{ zIndex: 9999 }}
|
style={{
|
||||||
|
zIndex: 9999,
|
||||||
|
top: position.top - window.scrollY,
|
||||||
|
left: position.left - window.scrollX
|
||||||
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useMemo, useRef, useCallback, useEffect } from 'react';
|
import React, { useState, useMemo, useRef, useCallback, useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { ExcelRow } from '../types';
|
import { ExcelRow } from '../types';
|
||||||
import { useColumns } from '../contexts/ColumnsContext';
|
import { useColumns } from '../contexts/ColumnsContext';
|
||||||
import {
|
import {
|
||||||
@@ -1149,7 +1150,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<table ref={tableRef} className="w-full text-sm border-collapse table-fixed">
|
<table ref={tableRef} className="w-full text-sm border-collapse table-fixed">
|
||||||
<thead className="sticky top-0 z-10 bg-slate-900 border-b border-slate-700">
|
<thead className="sticky top-0 z-[100] bg-slate-900 border-b border-slate-700">
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{
|
<th style={{
|
||||||
width: columnWidths.articleNo,
|
width: columnWidths.articleNo,
|
||||||
@@ -1166,9 +1167,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
Article No <SortIcon current={sortConfig.key === 'articleNo' ? sortConfig.direction : null} />
|
Article No <SortIcon current={sortConfig.key === 'articleNo' ? sortConfig.direction : null} />
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
|
id="filter-trigger-sku"
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'sku' ? null : 'sku'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'sku' ? null : 'sku'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||||
articleNoColFilter.terms.some(t => t.trim() !== '') ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
articleNoColFilter.terms.some(t => t.trim() !== '') ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -1177,6 +1179,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
</div>
|
</div>
|
||||||
{openFilter === 'sku' && (
|
{openFilter === 'sku' && (
|
||||||
<TextFilterPopover
|
<TextFilterPopover
|
||||||
|
triggerId="filter-trigger-sku"
|
||||||
value={articleNoColFilter}
|
value={articleNoColFilter}
|
||||||
onChange={setArticleNoColFilter}
|
onChange={setArticleNoColFilter}
|
||||||
onClose={() => setOpenFilter(null)}
|
onClose={() => setOpenFilter(null)}
|
||||||
@@ -1199,9 +1202,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
{isPinned('articleName') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
{isPinned('articleName') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
Article Name <SortIcon current={sortConfig.key === 'articleName' ? sortConfig.direction : null} /></span>
|
Article Name <SortIcon current={sortConfig.key === 'articleName' ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
|
id="filter-trigger-name"
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'name' ? null : 'name'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'name' ? null : 'name'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||||
nameColFilter.terms.some(t => t.trim() !== '') ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
nameColFilter.terms.some(t => t.trim() !== '') ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -1210,6 +1214,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
</div>
|
</div>
|
||||||
{openFilter === 'name' && (
|
{openFilter === 'name' && (
|
||||||
<TextFilterPopover
|
<TextFilterPopover
|
||||||
|
triggerId="filter-trigger-name"
|
||||||
value={nameColFilter}
|
value={nameColFilter}
|
||||||
onChange={setNameColFilter}
|
onChange={setNameColFilter}
|
||||||
onClose={() => setOpenFilter(null)}
|
onClose={() => setOpenFilter(null)}
|
||||||
@@ -1232,9 +1237,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
{isPinned('line') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
{isPinned('line') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
Line <SortIcon current={sortConfig.key === 'line' ? sortConfig.direction : null} /></span>
|
Line <SortIcon current={sortConfig.key === 'line' ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
|
id="filter-trigger-line"
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'line' ? null : 'line'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'line' ? null : 'line'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||||
lineMultiFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
lineMultiFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -1243,6 +1249,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
</div>
|
</div>
|
||||||
{openFilter === 'line' && (
|
{openFilter === 'line' && (
|
||||||
<ColumnFilterPopover
|
<ColumnFilterPopover
|
||||||
|
triggerId="filter-trigger-line"
|
||||||
uniqueValues={uniqueLines}
|
uniqueValues={uniqueLines}
|
||||||
selectedValues={lineMultiFilter}
|
selectedValues={lineMultiFilter}
|
||||||
onToggle={val => setLineMultiFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
onToggle={val => setLineMultiFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
||||||
@@ -1268,9 +1275,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
{isPinned('classification') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
{isPinned('classification') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
Classification <SortIcon current={sortConfig.key === 'classification' ? sortConfig.direction : null} /></span>
|
Classification <SortIcon current={sortConfig.key === 'classification' ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
|
id="filter-trigger-class"
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'classification' ? null : 'classification'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'classification' ? null : 'classification'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||||
classificationFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
classificationFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -1279,6 +1287,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
</div>
|
</div>
|
||||||
{openFilter === 'classification' && (
|
{openFilter === 'classification' && (
|
||||||
<ColumnFilterPopover
|
<ColumnFilterPopover
|
||||||
|
triggerId="filter-trigger-class"
|
||||||
uniqueValues={uniqueClassifications}
|
uniqueValues={uniqueClassifications}
|
||||||
selectedValues={classificationFilter}
|
selectedValues={classificationFilter}
|
||||||
onToggle={val => setClassificationFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
onToggle={val => setClassificationFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
||||||
@@ -1299,14 +1308,16 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
{isPinned('productType') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
{isPinned('productType') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
Type <SortIcon current={sortConfig.key === 'productType' ? sortConfig.direction : null} />
|
Type <SortIcon current={sortConfig.key === 'productType' ? sortConfig.direction : null} />
|
||||||
<button
|
<button
|
||||||
|
id="filter-trigger-type"
|
||||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); setOpenFilter(openFilter === 'productType' ? null : 'productType'); }}
|
onClick={(e: React.MouseEvent) => { e.stopPropagation(); setOpenFilter(openFilter === 'productType' ? null : 'productType'); }}
|
||||||
className={cn('ml-1 p-0.5 rounded transition-colors', productTypeFilter.length > 0 ? 'text-purple-400 bg-purple-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100')}
|
className={cn('ml-1 p-0.5 rounded transition-colors filter-trigger-btn', productTypeFilter.length > 0 ? 'text-purple-400 bg-purple-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100')}
|
||||||
>
|
>
|
||||||
<Filter className="w-3 h-3" />
|
<Filter className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
{openFilter === 'productType' && (
|
{openFilter === 'productType' && (
|
||||||
<ColumnFilterPopover
|
<ColumnFilterPopover
|
||||||
|
triggerId="filter-trigger-type"
|
||||||
uniqueValues={uniqueProductTypes}
|
uniqueValues={uniqueProductTypes}
|
||||||
selectedValues={productTypeFilter}
|
selectedValues={productTypeFilter}
|
||||||
onToggle={(val: string) => setProductTypeFilter((prev: string[]) => prev.includes(val) ? prev.filter((v: string) => v !== val) : [...prev, val])}
|
onToggle={(val: string) => setProductTypeFilter((prev: string[]) => prev.includes(val) ? prev.filter((v: string) => v !== val) : [...prev, val])}
|
||||||
@@ -1352,21 +1363,23 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
{isPinned(colKey) && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
{isPinned(colKey) && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
|
{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
|
id={`filter-trigger-${colKey}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setOpenFilter(openFilter === `prc_${col.index}` ? null : `prc_${col.index}`);
|
setOpenFilter(openFilter === colKey ? null : colKey);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||||
dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Filter className="w-3 h-3" />
|
<Filter className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{openFilter === `prc_${col.index}` && (
|
{openFilter === colKey && (
|
||||||
<ColumnFilterPopover
|
<ColumnFilterPopover
|
||||||
uniqueValues={dynamicColUniqueValues[col.index] || []}
|
triggerId={`filter-trigger-${colKey}`}
|
||||||
|
uniqueValues={dynamicColUniqueValues[col.index] || []}
|
||||||
selectedValues={dynamicColFilters[col.index] || []}
|
selectedValues={dynamicColFilters[col.index] || []}
|
||||||
onToggle={val => setDynamicColFilters(prev => {
|
onToggle={val => setDynamicColFilters(prev => {
|
||||||
const current = prev[col.index] || [];
|
const current = prev[col.index] || [];
|
||||||
@@ -1399,9 +1412,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
{isPinned('unitsOuter') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
{isPinned('unitsOuter') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
Units/Outer <SortIcon current={sortConfig.key === 'unitsOuter' ? sortConfig.direction : null} /></span>
|
Units/Outer <SortIcon current={sortConfig.key === 'unitsOuter' ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
|
id="filter-trigger-unitsOuter"
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'unitsOuter' ? null : 'unitsOuter'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'unitsOuter' ? null : 'unitsOuter'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||||
unitsOuterFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
unitsOuterFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -1410,6 +1424,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
</div>
|
</div>
|
||||||
{openFilter === 'unitsOuter' && (
|
{openFilter === 'unitsOuter' && (
|
||||||
<ColumnFilterPopover
|
<ColumnFilterPopover
|
||||||
|
triggerId="filter-trigger-unitsOuter"
|
||||||
uniqueValues={uniqueUnitsOuter}
|
uniqueValues={uniqueUnitsOuter}
|
||||||
selectedValues={unitsOuterFilter}
|
selectedValues={unitsOuterFilter}
|
||||||
onToggle={val => setUnitsOuterFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
onToggle={val => setUnitsOuterFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
||||||
@@ -1436,9 +1451,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
{isPinned('outerW') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
{isPinned('outerW') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
Outer W <SortIcon current={sortConfig.key === 'outerW' ? sortConfig.direction : null} /></span>
|
Outer W <SortIcon current={sortConfig.key === 'outerW' ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
|
id="filter-trigger-outerW"
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerW' ? null : 'outerW'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerW' ? null : 'outerW'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||||
outerWFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
outerWFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -1447,6 +1463,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
</div>
|
</div>
|
||||||
{openFilter === 'outerW' && (
|
{openFilter === 'outerW' && (
|
||||||
<ColumnFilterPopover
|
<ColumnFilterPopover
|
||||||
|
triggerId="filter-trigger-outerW"
|
||||||
uniqueValues={uniqueOuterW}
|
uniqueValues={uniqueOuterW}
|
||||||
selectedValues={outerWFilter}
|
selectedValues={outerWFilter}
|
||||||
onToggle={val => setOuterWFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
onToggle={val => setOuterWFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
||||||
@@ -1473,9 +1490,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
{isPinned('outerL') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
{isPinned('outerL') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
Outer L <SortIcon current={sortConfig.key === 'outerL' ? sortConfig.direction : null} /></span>
|
Outer L <SortIcon current={sortConfig.key === 'outerL' ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
|
id="filter-trigger-outerL"
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerL' ? null : 'outerL'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerL' ? null : 'outerL'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||||
outerLFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
outerLFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -1484,6 +1502,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
</div>
|
</div>
|
||||||
{openFilter === 'outerL' && (
|
{openFilter === 'outerL' && (
|
||||||
<ColumnFilterPopover
|
<ColumnFilterPopover
|
||||||
|
triggerId="filter-trigger-outerL"
|
||||||
uniqueValues={uniqueOuterL}
|
uniqueValues={uniqueOuterL}
|
||||||
selectedValues={outerLFilter}
|
selectedValues={outerLFilter}
|
||||||
onToggle={val => setOuterLFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
onToggle={val => setOuterLFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
||||||
@@ -1510,9 +1529,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
{isPinned('outerH') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
{isPinned('outerH') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
Outer H <SortIcon current={sortConfig.key === 'outerH' ? sortConfig.direction : null} /></span>
|
Outer H <SortIcon current={sortConfig.key === 'outerH' ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
|
id="filter-trigger-outerH"
|
||||||
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerH' ? null : 'outerH'); }}
|
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'outerH' ? null : 'outerH'); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||||
outerHFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
outerHFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -1521,6 +1541,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
</div>
|
</div>
|
||||||
{openFilter === 'outerH' && (
|
{openFilter === 'outerH' && (
|
||||||
<ColumnFilterPopover
|
<ColumnFilterPopover
|
||||||
|
triggerId="filter-trigger-outerH"
|
||||||
uniqueValues={uniqueOuterH}
|
uniqueValues={uniqueOuterH}
|
||||||
selectedValues={outerHFilter}
|
selectedValues={outerHFilter}
|
||||||
onToggle={val => setOuterHFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
onToggle={val => setOuterHFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
|
||||||
@@ -1551,21 +1572,23 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
{isPinned(colKey) && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
{isPinned(colKey) && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||||
{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
|
{col.name} <SortIcon current={sortConfig.key === col.index ? sortConfig.direction : null} /></span>
|
||||||
<button
|
<button
|
||||||
|
id={`filter-trigger-${colKey}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setOpenFilter(openFilter === `con_${col.index}` ? null : `con_${col.index}`);
|
setOpenFilter(openFilter === colKey ? null : colKey);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0',
|
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
|
||||||
dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
dynamicColFilters[col.index]?.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Filter className="w-3 h-3" />
|
<Filter className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{openFilter === `con_${col.index}` && (
|
{openFilter === colKey && (
|
||||||
<ColumnFilterPopover
|
<ColumnFilterPopover
|
||||||
uniqueValues={dynamicColUniqueValues[col.index] || []}
|
triggerId={`filter-trigger-${colKey}`}
|
||||||
|
uniqueValues={dynamicColUniqueValues[col.index] || []}
|
||||||
selectedValues={dynamicColFilters[col.index] || []}
|
selectedValues={dynamicColFilters[col.index] || []}
|
||||||
onToggle={val => setDynamicColFilters(prev => {
|
onToggle={val => setDynamicColFilters(prev => {
|
||||||
const current = prev[col.index] || [];
|
const current = prev[col.index] || [];
|
||||||
@@ -2002,12 +2025,24 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Text filter popover ───────────────────────────────────────────────────────
|
// ── Text filter popover ───────────────────────────────────────────────────────
|
||||||
function TextFilterPopover({ value, onChange, onClose, zIndex = 50 }: {
|
function TextFilterPopover({ value, onChange, onClose, triggerId }: {
|
||||||
value: { terms: string[]; op: 'and' | 'or' };
|
value: { terms: string[]; op: 'and' | 'or' };
|
||||||
onChange: (v: { terms: string[]; op: 'and' | 'or' }) => void;
|
onChange: (v: { terms: string[]; op: 'and' | 'or' }) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
zIndex?: number;
|
triggerId: string;
|
||||||
}) {
|
}) {
|
||||||
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const trigger = document.getElementById(triggerId);
|
||||||
|
if (trigger) {
|
||||||
|
const rect = trigger.getBoundingClientRect();
|
||||||
|
setPosition({
|
||||||
|
top: rect.bottom + window.scrollY + 4,
|
||||||
|
left: Math.min(rect.left + window.scrollX, window.innerWidth - 300)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [triggerId]);
|
||||||
const addTerm = () => {
|
const addTerm = () => {
|
||||||
if (value.terms.length < 5) {
|
if (value.terms.length < 5) {
|
||||||
onChange({ ...value, terms: [...value.terms, ''] });
|
onChange({ ...value, terms: [...value.terms, ''] });
|
||||||
@@ -2030,8 +2065,15 @@ function TextFilterPopover({ value, onChange, onClose, zIndex = 50 }: {
|
|||||||
onChange({ ...value, terms: next });
|
onChange({ ...value, terms: next });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return createPortal(
|
||||||
<div className="absolute top-full left-0 mt-1 w-72 bg-slate-800 border border-slate-700 rounded-lg shadow-2xl p-4 flex flex-col gap-4 animate-in fade-in zoom-in-95 duration-100" style={{ zIndex }} onClick={e => e.stopPropagation()}>
|
<div
|
||||||
|
className="fixed z-[9999] w-72 bg-slate-800 border border-slate-700 rounded-lg shadow-2xl p-4 flex flex-col gap-4 animate-in fade-in zoom-in-95 duration-100"
|
||||||
|
style={{
|
||||||
|
top: position.top - window.scrollY,
|
||||||
|
left: position.left - window.scrollX
|
||||||
|
}}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Show rows where field contains:</label>
|
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Show rows where field contains:</label>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user