Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions media/webview.css
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,31 @@ body.vscode-high-contrast #grid-container.cm-on {
.csv-filter-input:focus { border-color: var(--vscode-focusBorder, #007fd4); }
.csv-filter-input::placeholder { color: var(--vscode-input-placeholderForeground, #666); }
.csv-filter-actions { display: flex; gap: 8px; margin: 5px 0 6px; }
.csv-filter-master {
display: flex;
align-items: center;
gap: 6px;
padding: 6px;
margin: 6px 0;
font-size: 12px;
font-weight: 600;
cursor: pointer;
border-radius: 3px;
position: relative;
}
.csv-filter-master::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: -4px;
height: 1px;
background: var(--vscode-panel-border, #3c3c3c);
}
.csv-filter-master:hover { background: var(--vscode-list-hoverBackground, #2a2d2e); }
.csv-filter-master input { cursor: pointer; margin: 0; flex: none; accent-color: var(--vscode-focusBorder, #007fd4); }
.csv-filter-master-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.csv-filter-master-count { font-size: 11px; font-weight: 400; opacity: 0.6; flex: none; }
.csv-filter-link {
background: none;
border: none;
Expand All @@ -689,9 +714,9 @@ body.vscode-high-contrast #grid-container.cm-on {
.csv-filter-value-row {
display: flex;
align-items: center;
gap: 7px;
padding: 2px;
border-radius: 2px;
gap: 6px;
padding: 4px 6px;
border-radius: 3px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
Expand Down Expand Up @@ -1233,4 +1258,4 @@ body.vscode-high-contrast #grid-container.cm-on {
background-color: rgba(209,134,22,0.32) !important;
color: var(--vscode-charts-orange, #d18616) !important;
font-weight: 700 !important;
}
}
58 changes: 38 additions & 20 deletions src/webview/grid/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function createCombinedFilter(colType: ColType): any {
_searchQuery = '';
truncated = false;
_renderValuesList: (() => void) | null = null;
_displayedValues: string[] = [];

init(params: any) {
this.params = params;
Expand Down Expand Up @@ -291,39 +292,55 @@ export function createCombinedFilter(colType: ColType): any {
searchInp.value = this._searchQuery;
valSec.appendChild(searchInp);

const actions = document.createElement('div');
actions.className = 'csv-filter-actions';
const selAll = document.createElement('button');
selAll.className = 'csv-filter-link';
selAll.textContent = 'Select All';
const deselAll = document.createElement('button');
deselAll.className = 'csv-filter-link';
deselAll.textContent = 'Deselect All';
actions.appendChild(selAll);
actions.appendChild(deselAll);
valSec.appendChild(actions);
const masterRow = document.createElement('label');
masterRow.className = 'csv-filter-master';
const masterCb = document.createElement('input');
masterCb.type = 'checkbox';
const masterLabel = document.createElement('span');
masterLabel.className = 'csv-filter-master-label';
masterLabel.textContent = 'Select all';
const masterCount = document.createElement('span');
masterCount.className = 'csv-filter-master-count';
masterRow.appendChild(masterCb);
masterRow.appendChild(masterLabel);
masterRow.appendChild(masterCount);
valSec.appendChild(masterRow);

const listDiv = document.createElement('div');
listDiv.className = 'csv-filter-values-list';
valSec.appendChild(listDiv);

const syncMaster = () => {

@Robin-Reiche Robin-Reiche Jun 18, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommendation: keep the scoped behavior and make the scope explicit here. syncMaster computes checked and total over _displayedValues while isFilterActive() works over allValues, so with a search active the master can read a full 'N / N' while the column is still filtered. The clean fix is the label: when _searchQuery is non-empty, set the master label to 'Select all matches' and back to 'Select all' when empty. Then 'N / N matches checked' is honest, the column funnel icon stays as the real 'this column is filtered' indicator and nothing about the value logic changes. This is exactly what Excel does with '(Select All Search Results)'.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, this logic is intentional. The master record reflects the scoped/filtered values currently on display, similar to Excel, rather than the column's filter status.

const displayed = this._displayedValues;
const total = displayed.length;
let checked = 0;
for (const v of displayed) if (this.checkedValues.has(v)) checked++;
masterCb.checked = total > 0 && checked === total;
masterCb.indeterminate = checked > 0 && checked < total;
masterCb.disabled = total === 0;
masterCount.textContent = total > 0 ? `${checked} / ${total}` : '';
masterLabel.textContent = this._searchQuery ? 'Select all matches' : 'Select all';
};

const renderList = () => {
listDiv.innerHTML = '';
const q = this._searchQuery.toLowerCase();

// Only values that pass ALL active conditions
let items: { label: string; value: string; isBlank: boolean }[] = [];
if (this._showBlankInList()) items.push({ label: '(Blank)', value: '__blank__', isBlank: true });
this._valuesPassingCondition().forEach(v => items.push({ label: v, value: v, isBlank: false }));
if (q) items = items.filter(it => it.label.toLowerCase().includes(q));

this._displayedValues = items.map(it => it.value);

if (items.length === 0) {
const empty = document.createElement('div');
empty.className = 'csv-filter-empty';
empty.textContent = this._hasAnyActiveCondition()
? 'No values match this condition'
: 'No matching values';
listDiv.appendChild(empty);
syncMaster();
return;
}
items.forEach(item => {
Expand All @@ -335,6 +352,7 @@ export function createCombinedFilter(colType: ColType): any {
cb.addEventListener('change', () => {
if (cb.checked) this.checkedValues.add(item.value);
else this.checkedValues.delete(item.value);
syncMaster();
this.params.filterChangedCallback();
});
const span = document.createElement('span');
Expand All @@ -350,21 +368,21 @@ export function createCombinedFilter(colType: ColType): any {
note.textContent = 'Showing first 2000 unique values';
listDiv.appendChild(note);
}
syncMaster();
};

this._renderValuesList = renderList;

selAll.addEventListener('click', () => {
this._valuesPassingCondition().forEach(v => this.checkedValues.add(v));
if (this._showBlankInList()) this.checkedValues.add('__blank__');
renderList();
this.params.filterChangedCallback();
});
deselAll.addEventListener('click', () => {
this.checkedValues.clear();
masterCb.addEventListener('change', () => {

@Robin-Reiche Robin-Reiche Jun 18, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up on the blank value: with the 'Select all matches' label above, leaving (Blank) unselected while a non-matching search is active is actually correct, because (Blank) is not a match. No special-case is needed here. A user who wants blanks back just clears the search and ticks (Blank). I had first suggested force-adding blank in this handler, but that only fits the old global model and would be inconsistent under scoping (it touches a non-match). So the label change is the real fix. This handler can stay as is.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, the master now re-adds blank via _showBlankInList() when checking, mirroring the old Select All.

const check = masterCb.checked;
for (const v of this._displayedValues) {
if (check) this.checkedValues.add(v);
else this.checkedValues.delete(v);
}
renderList();
this.params.filterChangedCallback();
});

searchInp.addEventListener('input', () => {
this._searchQuery = searchInp.value;
renderList();
Expand Down