Skip to content

Commit e4a6679

Browse files
committed
feat(filter): reorder filter panel conditions
1 parent c6c763d commit e4a6679

6 files changed

Lines changed: 350 additions & 2 deletions

File tree

e2e/filtering.spec.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ test.describe('filtering', () => {
3939
const filterPanel = page.locator(SELECTORS.filterPanel);
4040
await expect(filterPanel).toBeVisible();
4141
await filterPanel.getByRole('combobox').selectOption({ label: 'Contains' });
42+
await expect(filterPanel.locator('.reorder-button')).toHaveCount(0);
4243
await page.locator(SELECTORS.filterInput).fill('Admin');
4344

4445
await expectVisibleColumnValues(page, 1, ['Alice', 'Cara']);
@@ -105,6 +106,7 @@ test.describe('filtering', () => {
105106

106107
const row = filterPanel.locator('.multi-filter-list-row').first();
107108
await expect(row).toBeVisible();
109+
await expect(row.locator(':scope > .reorder-button')).toHaveCount(1);
108110
await expect(row.locator(':scope > .select-input')).toHaveCount(1);
109111
await expect(row.locator(':scope > .multi-filter-list-action')).toHaveCount(1);
110112
await expect(row.locator('.select-input .multi-filter-list-action')).toHaveCount(0);
@@ -129,6 +131,74 @@ test.describe('filtering', () => {
129131
expect(isSingleRow).toBe(true);
130132
});
131133

134+
test('reorders filter conditions with the drag handle', async ({ page }) => {
135+
const source: SampleRow[] = [
136+
{ id: 501, name: 'Alice', role: 'Admin', city: 'Lisbon' },
137+
{ id: 502, name: 'Ben', role: 'Engineer', city: 'Porto' },
138+
];
139+
140+
const columns = buildColumns([
141+
{ prop: 'id', name: 'ID' },
142+
{ prop: 'name', name: 'Name' },
143+
{ prop: 'role', name: 'Role', filter: true, ...withHeaderTestId('reorder-filter-role') },
144+
{ prop: 'city', name: 'City' },
145+
]);
146+
147+
await mountGrid(page, { columns, source, filter: true });
148+
149+
await page
150+
.getByTestId('reorder-filter-role')
151+
.locator(SELECTORS.filterButton)
152+
.click();
153+
154+
const filterPanel = page.locator(SELECTORS.filterPanel);
155+
const filterInputs = page.locator(SELECTORS.filterInput);
156+
await expect(filterPanel).toBeVisible();
157+
await filterPanel.getByRole('combobox').selectOption({ label: 'Contains' });
158+
await filterInputs.fill('Admin');
159+
await filterPanel.locator('#add-filter').selectOption({ label: 'Equal' });
160+
await filterInputs.nth(1).fill('Engineer');
161+
162+
await expect(filterPanel.locator('.multi-filter-list-row')).toHaveCount(2);
163+
await expect(filterPanel.locator('.select-filter').nth(0)).toHaveValue('contains');
164+
await expect(filterPanel.locator('.select-filter').nth(1)).toHaveValue('eq');
165+
166+
await filterPanel.evaluate((panel) => {
167+
const rows = Array.from(panel.querySelectorAll('.multi-filter-list-row'));
168+
const sourceHandle = rows[1].querySelector('.reorder-button');
169+
const targetHandle = rows[0].querySelector('.reorder-button');
170+
if (!sourceHandle || !targetHandle) {
171+
throw new Error('Filter reorder controls were not found');
172+
}
173+
const dataTransfer = new DataTransfer();
174+
sourceHandle.dispatchEvent(new DragEvent('dragstart', {
175+
bubbles: true,
176+
cancelable: true,
177+
dataTransfer,
178+
}));
179+
targetHandle.dispatchEvent(new DragEvent('dragover', {
180+
bubbles: true,
181+
cancelable: true,
182+
dataTransfer,
183+
}));
184+
targetHandle.dispatchEvent(new DragEvent('drop', {
185+
bubbles: true,
186+
cancelable: true,
187+
dataTransfer,
188+
}));
189+
sourceHandle.dispatchEvent(new DragEvent('dragend', {
190+
bubbles: true,
191+
cancelable: true,
192+
dataTransfer,
193+
}));
194+
});
195+
196+
await expect(filterPanel.locator('.select-filter').nth(0)).toHaveValue('eq');
197+
await expect(filterPanel.locator('.select-filter').nth(1)).toHaveValue('contains');
198+
await expect(filterInputs.nth(0)).toHaveValue('Engineer');
199+
await expect(filterInputs.nth(1)).toHaveValue('Admin');
200+
});
201+
132202
test('reapplies active filters after source replacement', async ({ page }) => {
133203
const source: SampleRow[] = [
134204
{ id: 501, name: 'Alice', role: 'Admin', city: 'Lisbon' },

src/plugins/filter/filter.button.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const FILTER_BUTTON_ACTIVE = 'active';
77
export const FILTER_PROP = 'hasFilter';
88
export const AND_OR_BUTTON = 'and-or-button';
99
export const TRASH_BUTTON = 'trash-button';
10+
export const REORDER_BUTTON = 'reorder-button';
1011

1112
type Props = {
1213
column: ColumnRegular;
@@ -43,6 +44,54 @@ export const AndOrButton = ({ text }: any) => {
4344
return <button class={{ [AND_OR_BUTTON]: true, 'light revo-button': true }}>{text}</button>;
4445
};
4546

47+
type ReorderButtonProps = {
48+
dragging?: boolean;
49+
dragOver?: boolean;
50+
onDragStart?: (event: DragEvent) => void;
51+
onDragEnd?: (event: DragEvent) => void;
52+
onDragOver?: (event: DragEvent) => void;
53+
onDragLeave?: (event: DragEvent) => void;
54+
onDrop?: (event: DragEvent) => void;
55+
};
56+
57+
export const ReorderButton = ({
58+
dragging,
59+
dragOver,
60+
onDragStart,
61+
onDragEnd,
62+
onDragOver,
63+
onDragLeave,
64+
onDrop,
65+
}: ReorderButtonProps) => {
66+
const applyClass = (el?: HTMLButtonElement) => {
67+
if (!el) {
68+
return;
69+
}
70+
el.className = [
71+
REORDER_BUTTON,
72+
dragging ? 'filter-row-dragging' : '',
73+
dragOver ? 'filter-row-drag-over' : '',
74+
].filter(Boolean).join(' ');
75+
};
76+
77+
return (
78+
<button
79+
type="button"
80+
ref={applyClass}
81+
draggable={true}
82+
title="Reorder filter"
83+
aria-label="reorder filter"
84+
onDragStart={onDragStart}
85+
onDragEnd={onDragEnd}
86+
onDragOver={onDragOver}
87+
onDragLeave={onDragLeave}
88+
onDrop={onDrop}
89+
>
90+
::
91+
</button>
92+
);
93+
};
94+
4695
export function isFilterBtn(e: HTMLElement) {
4796
if (e.classList.contains(FILTER_BUTTON_CLASS)) {
4897
return true;

src/plugins/filter/filter.panel.tsx

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
} from '@stencil/core';
1616
import debounce from 'lodash/debounce';
1717

18-
import { AndOrButton, isFilterBtn, TrashButton } from './filter.button';
18+
import { AndOrButton, isFilterBtn, ReorderButton, TrashButton } from './filter.button';
1919
import '../../utils/closest.polifill';
2020
import {
2121
FilterCaptions,
@@ -25,6 +25,11 @@ import {
2525
} from './filter.types';
2626
import type { ColumnProp } from '@type';
2727
import { FilterType } from './filter.indexed';
28+
import {
29+
getFilterReorderId,
30+
moveFilterItem,
31+
setFilterReorderData,
32+
} from './filter.reorder';
2833

2934
const defaultType: FilterType = 'none';
3035

@@ -63,6 +68,8 @@ export class FilterPanel {
6368
@State() currentFilterType: FilterType = defaultType;
6469
@State() changes: ShowData | undefined;
6570
@State() filterItems: MultiFilterItem = {};
71+
@State() draggedFilterId: number | undefined;
72+
@State() dragOverFilterId: number | undefined;
6673
@Prop() filterNames: Record<string, string> = {};
6774
@Prop() filterEntities: Record<string, LogicFunction> = {};
6875
@Prop() filterCaptions: Partial<FilterCaptions> | undefined;
@@ -137,6 +144,7 @@ export class FilterPanel {
137144
if (typeof prop === 'undefined') return '';
138145

139146
const propFilters = this.filterItems[prop] ?? [];
147+
const visibleFilterCount = propFilters.filter(filter => !filter.hidden).length;
140148
const capts = Object.assign(
141149
this.filterCaptionsInternal,
142150
this.filterCaptions,
@@ -164,7 +172,18 @@ export class FilterPanel {
164172

165173
return (
166174
<div key={filter.id} class={FILTER_LIST_CLASS}>
167-
<div class="multi-filter-list-row">
175+
<div ref={el => el?.classList.add('multi-filter-list-row')}>
176+
{visibleFilterCount > 1 ? (
177+
<ReorderButton
178+
dragging={this.draggedFilterId === filter.id}
179+
dragOver={this.dragOverFilterId === filter.id && this.draggedFilterId !== filter.id}
180+
onDragStart={e => this.onFilterDragStart(e, filter.id)}
181+
onDragEnd={() => this.onFilterDragEnd()}
182+
onDragOver={e => this.onFilterDragOver(e, filter.id)}
183+
onDragLeave={() => this.onFilterDragLeave(filter.id)}
184+
onDrop={e => this.onFilterDrop(e, prop, filter.id)}
185+
/>
186+
) : ''}
168187
<div class={{ 'select-input': true }}>
169188
<select
170189
class="select-css select-filter"
@@ -326,6 +345,58 @@ export class FilterPanel {
326345
}
327346
}
328347

348+
private onFilterDragStart(e: DragEvent, id: number) {
349+
this.draggedFilterId = id;
350+
setFilterReorderData(e.dataTransfer, id);
351+
}
352+
353+
private onFilterDragOver(e: DragEvent, id: number) {
354+
if (this.draggedFilterId === undefined || this.draggedFilterId === id) {
355+
return;
356+
}
357+
e.preventDefault();
358+
if (e.dataTransfer) {
359+
e.dataTransfer.dropEffect = 'move';
360+
}
361+
this.dragOverFilterId = id;
362+
}
363+
364+
private onFilterDragLeave(id: number) {
365+
if (this.dragOverFilterId === id) {
366+
this.dragOverFilterId = undefined;
367+
}
368+
}
369+
370+
private onFilterDrop(e: DragEvent, prop: ColumnProp, targetId: number) {
371+
e.preventDefault();
372+
const sourceId = this.draggedFilterId ?? getFilterReorderId(e.dataTransfer);
373+
this.onFilterDragEnd();
374+
375+
if (sourceId === undefined) {
376+
return;
377+
}
378+
379+
const items = this.filterItems[prop];
380+
if (!items) {
381+
return;
382+
}
383+
384+
if (!moveFilterItem(items, sourceId, targetId)) {
385+
return;
386+
}
387+
388+
this.filterId++;
389+
390+
if (!this.disableDynamicFiltering) {
391+
this.debouncedApplyFilter();
392+
}
393+
}
394+
395+
private onFilterDragEnd() {
396+
this.draggedFilterId = undefined;
397+
this.dragOverFilterId = undefined;
398+
}
399+
329400
private toggleFilterAndOr(id: number) {
330401
this.assertChanges();
331402

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { FilterData } from './filter.types';
2+
3+
const FILTER_REORDER_MIME = 'text/revogrid-filter-id';
4+
5+
export function setFilterReorderData(dataTransfer: DataTransfer | null, id: number) {
6+
if (!dataTransfer) {
7+
return;
8+
}
9+
dataTransfer.effectAllowed = 'move';
10+
dataTransfer.setData(FILTER_REORDER_MIME, String(id));
11+
dataTransfer.setData('text/plain', String(id));
12+
}
13+
14+
export function getFilterReorderId(dataTransfer: DataTransfer | null): number | undefined {
15+
if (!dataTransfer) {
16+
return;
17+
}
18+
const rawId = dataTransfer.getData(FILTER_REORDER_MIME) || dataTransfer.getData('text/plain');
19+
const normalizedId = rawId.trim();
20+
if (!normalizedId) {
21+
return;
22+
}
23+
const id = Number(normalizedId);
24+
return Number.isFinite(id) ? id : undefined;
25+
}
26+
27+
export function moveFilterItem(items: FilterData[], sourceId: number, targetId: number): boolean {
28+
if (sourceId === targetId) {
29+
return false;
30+
}
31+
32+
const sourceIndex = items.findIndex(item => item.id === sourceId);
33+
const targetIndex = items.findIndex(item => item.id === targetId);
34+
if (sourceIndex === -1 || targetIndex === -1 || sourceIndex === targetIndex) {
35+
return false;
36+
}
37+
38+
const relationsByPosition = items.map(item => item.relation ?? 'and');
39+
const [movedItem] = items.splice(sourceIndex, 1);
40+
items.splice(targetIndex, 0, movedItem);
41+
items.forEach((item, index) => {
42+
item.relation = index === items.length - 1
43+
? 'and'
44+
: relationsByPosition[index] ?? 'and';
45+
});
46+
return true;
47+
}

src/plugins/filter/filter.style.scss

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,32 @@ revogr-filter-panel {
212212
width: 1em;
213213
}
214214
}
215+
216+
.reorder-button {
217+
border: 0;
218+
background: transparent;
219+
color: var(--revo-grid-filter-panel-reorder-color, #6b7280);
220+
cursor: grab;
221+
font-family: monospace;
222+
font-size: 12px;
223+
letter-spacing: 0;
224+
line-height: 1;
225+
padding: 6px 2px;
226+
transform: scaleX(0.8);
227+
width: 16px;
228+
229+
&.filter-row-dragging {
230+
opacity: 0.55;
231+
}
232+
233+
&.filter-row-drag-over {
234+
color: var(--revo-grid-filter-panel-reorder-accent, #007cb2);
235+
}
236+
237+
&:active {
238+
cursor: grabbing;
239+
}
240+
}
215241
}
216242

217243
.add-filter-divider {

0 commit comments

Comments
 (0)