Copilot commented on code in PR #4240:
URL: https://github.com/apache/streampipes/pull/4240#discussion_r2918032553
##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.ts:
##########
@@ -325,131 +666,195 @@ export class TableWidgetComponent extends
BaseDataExplorerWidgetDirective<TableW
}
private filterRows(rows: TableRow[]): TableRow[] {
- const searchTerm = (
+ let result = [...rows];
+ const search = (
this.dataExplorerWidget.visualizationConfig.searchValue ?? ''
)
.trim()
.toLowerCase();
-
- if (!searchTerm) {
- return [...rows];
+ if (search) {
+ result = result.filter(row =>
+ this.columnNames.some(c =>
+ String(this.formatCellValue(c, row[c]))
+ .toLowerCase()
+ .includes(search),
+ ),
+ );
}
-
- return rows.filter(row =>
- this.columnNames.some(column =>
- String(this.formatCellValue(column, row[column]))
- .toLowerCase()
- .includes(searchTerm),
- ),
- );
+ for (const col of this.columnNames) {
+ const f = this.columnFilters[col];
+ if (f && f.size < this.getAllUniqueValues(col).length) {
+ result = result.filter(row =>
+ f.has(this.formatForFilter(row, col)),
+ );
+ }
+ const adv = this.advancedFilters[col];
+ if (adv) result = this.applyAdvancedFilterToRows(result, col, adv);
+ }
+ return result;
}
- private sortRows(rows: TableRow[]): TableRow[] {
- if (!this.sortColumn || this.sortDirection === '') {
- return [...rows];
+ private applyAdvancedFilterToRows(
+ rows: TableRow[],
+ column: string,
+ adv: AdvancedFilter,
+ ): TableRow[] {
+ if (adv.type === 'Top 10') {
+ const top = rows
+ .map(r => ({ r, n: this.toNumber(r[column]) }))
+ .filter(
+ (e): e is { r: TableRow; n: number } => e.n !== undefined,
+ )
+ .sort((a, b) => b.n - a.n)
+ .slice(0, 10);
+ const set = new Set(top.map(e => e.r));
+ return rows.filter(r => set.has(r));
}
+ if (adv.type === 'Above average' || adv.type === 'Below average') {
+ const nums = rows
+ .map(r => this.toNumber(r[column]))
+ .filter((n): n is number => n !== undefined);
+ if (!nums.length) return rows;
+ const avg = nums.reduce((s, n) => s + n, 0) / nums.length;
+ return rows.filter(r => {
+ const n = this.toNumber(r[column]);
+ return (
+ n !== undefined &&
+ (adv.type === 'Above average' ? n > avg : n < avg)
+ );
+ });
+ }
+ return rows.filter(r => this.passesAdvancedFilter(r, column, adv));
+ }
- const directionMultiplier = this.sortDirection === 'asc' ? 1 : -1;
- return [...rows].sort((rowA, rowB) => {
- const comparison = this.compareValues(
- rowA[this.sortColumn],
- rowB[this.sortColumn],
- this.sortColumn,
- );
+ private passesAdvancedFilter(
+ row: TableRow,
+ column: string,
+ adv: AdvancedFilter,
+ ): boolean {
+ const raw = row[column];
+ if (this.isNumericColumn(column) || column === 'time') {
+ const n =
+ column === 'time'
+ ? new Date(raw as string | number).getTime()
+ : this.toNumber(raw);
+ const t1 = Number(adv.value);
+ const t2 = Number(adv.value2);
+ switch (adv.type) {
+ case 'Equals':
+ return n === t1;
+ case 'Does not equal':
+ return n !== t1;
+ case 'Greater than':
+ return n !== undefined && n > t1;
+ case 'Greater than or equal to':
+ return n !== undefined && n >= t1;
+ case 'Less than':
+ return n !== undefined && n < t1;
+ case 'Less than or equal to':
+ return n !== undefined && n <= t1;
+ case 'Between':
+ return (
+ n !== undefined &&
Review Comment:
In numeric/time advanced filters, `n` can be `undefined` (non-numeric) or
`NaN` (invalid date from `new Date(...).getTime()`). The current `Does not
equal` case (`return n !== t1`) will pass rows with `undefined`/`NaN`, and
`Equals` will silently fail with `NaN`, which is likely not intended. Consider
normalizing invalid numbers to `undefined` (e.g., treat `NaN` as `undefined`)
and making `Equals`/`Does not equal` consistent about whether blanks should
match.
```suggestion
const nRaw =
column === 'time'
? new Date(raw as string | number).getTime()
: this.toNumber(raw);
const n = Number.isFinite(nRaw) ? nRaw : undefined;
const t1Raw = Number(adv.value);
const t1 = Number.isFinite(t1Raw) ? t1Raw : undefined;
const t2Raw = Number(adv.value2);
const t2 = Number.isFinite(t2Raw) ? t2Raw : undefined;
switch (adv.type) {
case 'Equals':
return n !== undefined && t1 !== undefined && n === t1;
case 'Does not equal':
return n !== undefined && t1 !== undefined && n !== t1;
case 'Greater than':
return n !== undefined && t1 !== undefined && n > t1;
case 'Greater than or equal to':
return n !== undefined && t1 !== undefined && n >= t1;
case 'Less than':
return n !== undefined && t1 !== undefined && n < t1;
case 'Less than or equal to':
return n !== undefined && t1 !== undefined && n <= t1;
case 'Between':
return (
n !== undefined &&
t1 !== undefined &&
t2 !== undefined &&
```
##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.ts:
##########
@@ -325,131 +666,195 @@ export class TableWidgetComponent extends
BaseDataExplorerWidgetDirective<TableW
}
private filterRows(rows: TableRow[]): TableRow[] {
- const searchTerm = (
+ let result = [...rows];
+ const search = (
this.dataExplorerWidget.visualizationConfig.searchValue ?? ''
)
.trim()
.toLowerCase();
-
- if (!searchTerm) {
- return [...rows];
+ if (search) {
+ result = result.filter(row =>
+ this.columnNames.some(c =>
+ String(this.formatCellValue(c, row[c]))
+ .toLowerCase()
+ .includes(search),
+ ),
+ );
}
-
- return rows.filter(row =>
- this.columnNames.some(column =>
- String(this.formatCellValue(column, row[column]))
- .toLowerCase()
- .includes(searchTerm),
- ),
- );
+ for (const col of this.columnNames) {
+ const f = this.columnFilters[col];
+ if (f && f.size < this.getAllUniqueValues(col).length) {
+ result = result.filter(row =>
+ f.has(this.formatForFilter(row, col)),
+ );
+ }
+ const adv = this.advancedFilters[col];
+ if (adv) result = this.applyAdvancedFilterToRows(result, col, adv);
+ }
+ return result;
}
- private sortRows(rows: TableRow[]): TableRow[] {
- if (!this.sortColumn || this.sortDirection === '') {
- return [...rows];
+ private applyAdvancedFilterToRows(
+ rows: TableRow[],
+ column: string,
+ adv: AdvancedFilter,
+ ): TableRow[] {
+ if (adv.type === 'Top 10') {
+ const top = rows
+ .map(r => ({ r, n: this.toNumber(r[column]) }))
+ .filter(
+ (e): e is { r: TableRow; n: number } => e.n !== undefined,
+ )
+ .sort((a, b) => b.n - a.n)
+ .slice(0, 10);
+ const set = new Set(top.map(e => e.r));
+ return rows.filter(r => set.has(r));
}
+ if (adv.type === 'Above average' || adv.type === 'Below average') {
+ const nums = rows
+ .map(r => this.toNumber(r[column]))
+ .filter((n): n is number => n !== undefined);
+ if (!nums.length) return rows;
+ const avg = nums.reduce((s, n) => s + n, 0) / nums.length;
+ return rows.filter(r => {
+ const n = this.toNumber(r[column]);
+ return (
+ n !== undefined &&
+ (adv.type === 'Above average' ? n > avg : n < avg)
+ );
+ });
+ }
Review Comment:
Advanced filter options treat the `time` column as numeric, but `Top
10`/`Above average`/`Below average` are implemented using
`toNumber(r[column])`. For typical timestamp values (ISO strings, Date, etc.)
`toNumber` returns `undefined`, so these filters effectively do nothing for
`time` even though they are offered in the UI. Consider handling `time`
explicitly (convert to `Date.getTime()` consistently) in these branches.
##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.html:
##########
@@ -56,22 +57,46 @@
isHighlightedColumn(column),
}"
>
- <button
- type="button"
- class="sort-trigger"
- (click)="sortBy(column)"
- >
- <span>
- @if (column === 'time') {
- {{ 'Time' | translate }}
- } @else {
- {{ headerLabel(column) }}
- }
- </span>
- <mat-icon class="sort-icon">
- {{ sortIcon(column) }}
- </mat-icon>
- </button>
+ <div class="th-inner">
+ <button
+ type="button"
+ class="sort-trigger"
+ (click)="sortBy(column)"
+ >
+ <span>
+ @if (column === 'time') {
+ {{ 'Time' | translate }}
+ } @else {
+ {{ headerLabel(column) }}
+ }
+ </span>
+ <mat-icon class="sort-icon">
+ {{ sortIcon(column) }}
+ </mat-icon>
+ </button>
+ <button
+ type="button"
+ class="column-filter-trigger"
+ [ngClass]="{
+ 'filter-active':
+ hasActiveFilter(column),
+ 'filter-open':
+ isColumnFilterOpen(column),
+ }"
+ (click)="
+ toggleColumnFilter(
+ column,
+ $event
+ )
+ "
+ >
+ <mat-icon
+ class="column-filter-icon"
+ >
+ filter_list
+ </mat-icon>
+ </button>
Review Comment:
The filter icon button is icon-only and currently has no accessible name.
Add an `aria-label`/`[attr.aria-label]` (and ideally a tooltip) so screen
readers can announce what the button does (e.g., "Filter column").
##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.scss:
##########
@@ -47,11 +47,10 @@
}
.analytics-table th {
- position: sticky;
- top: 0;
- z-index: 2;
+ position: relative;
padding: 0;
background: var(--color-bg-0);
+ overflow: visible;
}
Review Comment:
Changing `.analytics-table th` from `position: sticky; top: 0; z-index: ...`
to `position: relative` removes the sticky table header behavior. Since the
table body is inside an overflow scroll container, this is likely a regression
in usability (header no longer stays visible while scrolling). Consider
restoring `position: sticky` and adjusting stacking/overflow rules so the
dropdown can render correctly without sacrificing the sticky header.
##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.ts:
##########
@@ -161,14 +228,304 @@ export class TableWidgetComponent extends
BaseDataExplorerWidgetDirective<TableW
this.sortDirection = 'asc';
} else if (this.sortDirection === 'asc') {
this.sortDirection = 'desc';
- } else if (this.sortDirection === 'desc') {
+ } else {
this.sortDirection = '';
this.sortColumn = '';
+ }
+ this.applyTableState(false);
+ }
+
+ toggleColumnFilter(column: string, event: MouseEvent): void {
+ event.stopPropagation();
+ if (this.openFilterColumn === column) {
+ this.closeFilter();
+ return;
+ }
+ this.openFilterColumn = column;
+ this.showAdvancedPanel = null;
+ this.columnSearchTerms[column] ??= '';
+ const rect = (
+ event.currentTarget as HTMLElement
+ ).getBoundingClientRect();
+ const root = this.elRef.nativeElement.getBoundingClientRect();
+ const leftRel = rect.left - root.left;
+ const finalLeft =
+ root.right - rect.left >= DROPDOWN_MAX_WIDTH +
DROPDOWN_EDGE_PADDING
+ ? leftRel
+ : Math.max(
+ 0,
+ leftRel -
+ (DROPDOWN_MAX_WIDTH - rect.width) -
+ DROPDOWN_EDGE_PADDING,
+ );
+ this.dropdownStyle = {
+ 'position': 'absolute',
+ 'top': `${rect.bottom - root.top}px`,
+ 'left': `${finalLeft}px`,
+ 'z-index': '9999',
+ };
+ }
+
+ isColumnFilterOpen = (column: string): boolean =>
+ this.openFilterColumn === column;
+
+ hasActiveFilter(column: string): boolean {
+ if (this.advancedFilters[column]) return true;
+ const f = this.columnFilters[column];
+ return !!f && f.size < this.getAllUniqueValues(column).length;
+ }
+
+ getAllUniqueValues = (column: string): string[] =>
+ this.extractUniqueValues(this.rows, column);
+
+ getVisibleUniqueValues(column: string): string[] {
+ let baseRows = this.getRowsFilteredByOtherColumns(column);
+ const adv = this.advancedFilters[column];
+ if (adv)
+ baseRows = this.applyAdvancedFilterToRows(baseRows, column, adv);
+ return this.extractUniqueValues(baseRows, column);
+ }
+
+ getLivePreviewValues(column: string): string[] {
+ let baseRows = this.getRowsFilteredByOtherColumns(column);
+ const adv = this.advancedFilters[column];
+ if (adv)
+ baseRows = this.applyAdvancedFilterToRows(baseRows, column, adv);
+ if (
+ this.showAdvancedPanel === column &&
+ this.selectedAdvancedType &&
+ (this.advancedInputValue ||
+ !this.needsInput(this.selectedAdvancedType))
+ ) {
+ baseRows = this.applyAdvancedFilterToRows(baseRows, column, {
+ type: this.selectedAdvancedType,
+ value: this.advancedInputValue,
+ value2: this.advancedInputValue2,
+ });
+ }
+ return this.extractUniqueValues(baseRows, column);
+ }
+
+ getFilteredUniqueValues(column: string): string[] {
+ const term = (this.columnSearchTerms[column] ?? '')
+ .trim()
+ .toLowerCase();
+ const values =
+ this.showAdvancedPanel === column
+ ? this.getLivePreviewValues(column)
+ : this.getVisibleUniqueValues(column);
+ return term
+ ? values.filter(v => v.toLowerCase().includes(term))
+ : values;
+ }
+
+ isValueChecked = (column: string, value: string): boolean =>
+ this.columnFilters[column]?.has(value) ?? true;
+
+ toggleValue(column: string, value: string): void {
+ this.ensureColumnFilter(column);
+ const f = this.columnFilters[column];
+ if (f.has(value)) {
+ f.delete(value);
} else {
- this.sortDirection = 'asc';
+ f.add(value);
}
+ this.applyTableState(true);
+ }
- this.applyTableState(false);
+ areAllValuesSelected(column: string): boolean {
+ const f = this.columnFilters[column];
+ if (!f) return true;
+ return this.getAllUniqueValues(column).every(v => f.has(v));
+ }
+
+ toggleAllValues(column: string): void {
+ this.ensureColumnFilter(column);
+ const f = this.columnFilters[column];
+ const all = this.getAllUniqueValues(column);
+ const allSelected = all.every(v => f.has(v));
+ all.forEach(v => (allSelected ? f.delete(v) : f.add(v)));
+ this.applyTableState(true);
+ }
+
+ hasSearchOrAdvanced = (column: string): boolean =>
+ !!this.columnSearchTerms[column]?.trim() ||
+ this.showAdvancedPanel === column;
+
+ areDisplayedValuesSelected(column: string): boolean {
+ const f = this.columnFilters[column];
+ if (!f) return true;
+ return this.getFilteredUniqueValues(column).every(v => f.has(v));
+ }
+
+ toggleDisplayedValues(column: string): void {
+ this.ensureColumnFilter(column);
+ const f = this.columnFilters[column];
+ const displayed = this.getFilteredUniqueValues(column);
+ displayed.forEach(v => f.add(v));
+ this.applyTableState(true);
+ }
+
+ onColumnSearchChange(column: string, term: string): void {
+ this.columnSearchTerms[column] = term;
+ }
+
+ clearColumnFilter(column: string): void {
+ delete this.columnFilters[column];
+ delete this.advancedFilters[column];
+ this.columnSearchTerms[column] = '';
+ this.showAdvancedPanel = null;
+ this.applyTableState(true);
+ }
+
+ onSearchKeydown(event: KeyboardEvent): void {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ this.closeFilter();
+ }
+ }
+
+ onAdvancedInputKeydown(
+ event: KeyboardEvent,
+ column: string,
+ inputIndex: number,
+ ): void {
+ if (event.key !== 'Enter') return;
+ event.preventDefault();
+ if (
+ inputIndex === 1 &&
+ this.needsSecondInput(this.selectedAdvancedType)
+ ) {
+ this.elRef.nativeElement
+ .querySelectorAll('.advanced-input')?.[1]
+ ?.focus();
+ } else {
+ this.applyAdvancedFilter(column);
+ }
+ }
+
+ onFilterDropdownClick = (event: MouseEvent): void =>
+ event.stopPropagation();
+
+ getAdvancedFilterOptions = (column: string): string[] =>
+ this.isNumericColumn(column) || column === 'time'
+ ? NUMERIC_FILTER_OPTIONS
+ : TEXT_FILTER_OPTIONS;
+
+ getAdvancedFilterLabel = (column: string): string =>
+ this.isNumericColumn(column) || column === 'time'
+ ? 'Number Filters'
+ : 'Text Filters';
+
+ toggleAdvancedPanel(column: string, event: MouseEvent): void {
+ event.stopPropagation();
+ if (this.showAdvancedPanel === column) {
+ this.showAdvancedPanel = null;
+ return;
+ }
+ this.showAdvancedPanel = column;
+ const existing = this.advancedFilters[column];
+ this.selectedAdvancedType = existing?.type ?? '';
+ this.advancedInputValue = existing?.value ?? '';
+ this.advancedInputValue2 = existing?.value2 ?? '';
+ }
+
+ selectAdvancedType(type: string): void {
+ this.selectedAdvancedType = type;
+ this.advancedInputValue = '';
+ this.advancedInputValue2 = '';
+ }
+
+ needsInput = (type: string): boolean => !NO_INPUT_TYPES.has(type);
+ needsSecondInput = (type: string): boolean => type === 'Between';
+
+ applyAdvancedFilter(column: string): void {
+ this.advancedFilters[column] = {
+ type: this.selectedAdvancedType,
+ value: this.advancedInputValue,
+ value2: this.advancedInputValue2,
+ };
+ this.showAdvancedPanel = null;
+ this.applyTableState(true);
+ }
Review Comment:
`applyAdvancedFilter` stores the selected type/value without validating
required inputs. For numeric filters (including `Between`) an empty
`advancedInputValue`/`advancedInputValue2` will be coerced to `0` via
`Number('')`, leading to incorrect filtering and showing an active filter even
though the user didn't enter a value. Consider disabling the OK button until
required inputs are present, or validating and not applying the filter when
inputs are missing/invalid.
##########
ui/package-lock.json:
##########
@@ -377,40 +377,6 @@
}
}
},
- "node_modules/@angular-devkit/architect/node_modules/chokidar": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
- "integrity":
"sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "readdirp": "^4.0.1"
- },
- "engines": {
- "node": ">= 14.16.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/@angular-devkit/architect/node_modules/readdirp": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
- "integrity":
"sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">= 14.18.0"
- },
- "funding": {
- "type": "individual",
- "url": "https://paulmillr.com/funding/"
- }
- },
"node_modules/@angular-devkit/schematics": {
Review Comment:
`ui/package-lock.json` has a large set of dependency entry removals, but
this PR doesn't appear to change any npm dependencies (the feature uses
existing Angular/Material modules). This makes the diff noisy and can cause
unnecessary lockfile churn/merge conflicts. Consider reverting the lockfile
changes or documenting why a lockfile regeneration is required (and ensuring it
was generated with the repo's expected npm version).
##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.ts:
##########
@@ -161,14 +228,304 @@ export class TableWidgetComponent extends
BaseDataExplorerWidgetDirective<TableW
this.sortDirection = 'asc';
} else if (this.sortDirection === 'asc') {
this.sortDirection = 'desc';
- } else if (this.sortDirection === 'desc') {
+ } else {
this.sortDirection = '';
this.sortColumn = '';
+ }
+ this.applyTableState(false);
+ }
+
+ toggleColumnFilter(column: string, event: MouseEvent): void {
+ event.stopPropagation();
+ if (this.openFilterColumn === column) {
+ this.closeFilter();
+ return;
+ }
+ this.openFilterColumn = column;
+ this.showAdvancedPanel = null;
+ this.columnSearchTerms[column] ??= '';
+ const rect = (
+ event.currentTarget as HTMLElement
+ ).getBoundingClientRect();
+ const root = this.elRef.nativeElement.getBoundingClientRect();
+ const leftRel = rect.left - root.left;
+ const finalLeft =
+ root.right - rect.left >= DROPDOWN_MAX_WIDTH +
DROPDOWN_EDGE_PADDING
+ ? leftRel
+ : Math.max(
+ 0,
+ leftRel -
+ (DROPDOWN_MAX_WIDTH - rect.width) -
+ DROPDOWN_EDGE_PADDING,
+ );
+ this.dropdownStyle = {
+ 'position': 'absolute',
+ 'top': `${rect.bottom - root.top}px`,
+ 'left': `${finalLeft}px`,
+ 'z-index': '9999',
+ };
+ }
+
+ isColumnFilterOpen = (column: string): boolean =>
+ this.openFilterColumn === column;
+
+ hasActiveFilter(column: string): boolean {
+ if (this.advancedFilters[column]) return true;
+ const f = this.columnFilters[column];
+ return !!f && f.size < this.getAllUniqueValues(column).length;
+ }
+
+ getAllUniqueValues = (column: string): string[] =>
+ this.extractUniqueValues(this.rows, column);
+
Review Comment:
`getAllUniqueValues` recomputes and sorts unique values from `this.rows`
each time it's called. It's invoked from template bindings (`hasActiveFilter`,
`areAllValuesSelected`) and inside filtering loops, which can become expensive
on large datasets (repeated O(rows * columns) scans during change detection).
Consider caching unique values/lengths per column when `rows` change (e.g., in
`onDataReceived`) and reusing the cached results.
##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.html:
##########
@@ -154,4 +179,170 @@
</mat-paginator>
</div>
}
+ @if (openFilterColumn) {
+ <div
+ class="column-filter-dropdown"
+ [ngStyle]="dropdownStyle"
+ (click)="onFilterDropdownClick($event)"
+ >
+ @if (hasActiveFilter(openFilterColumn)) {
+ <button
+ type="button"
+ class="clear-filter-btn"
+ (click)="clearColumnFilter(openFilterColumn)"
+ >
+ <mat-icon class="clear-filter-icon"
+ >filter_list_off</mat-icon
+ >
+ Clear Filter
+ </button>
+ }
+ <button
+ type="button"
+ class="advanced-filter-btn"
+ (click)="toggleAdvancedPanel(openFilterColumn, $event)"
+ >
+ {{ getAdvancedFilterLabel(openFilterColumn) }}
Review Comment:
Several new UI strings are hard-coded and bypass i18n (e.g., "Clear Filter",
"Search...", "(Select All)", "OK", "Cancel", "Clear", "Value..."). This
component already uses `TranslatePipe` for other text, so these should also be
moved to translation keys/pipes (including placeholders/aria-labels) to keep
localization consistent.
##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.ts:
##########
@@ -161,14 +228,304 @@ export class TableWidgetComponent extends
BaseDataExplorerWidgetDirective<TableW
this.sortDirection = 'asc';
} else if (this.sortDirection === 'asc') {
this.sortDirection = 'desc';
- } else if (this.sortDirection === 'desc') {
+ } else {
this.sortDirection = '';
this.sortColumn = '';
+ }
+ this.applyTableState(false);
+ }
+
+ toggleColumnFilter(column: string, event: MouseEvent): void {
+ event.stopPropagation();
+ if (this.openFilterColumn === column) {
+ this.closeFilter();
+ return;
+ }
+ this.openFilterColumn = column;
+ this.showAdvancedPanel = null;
+ this.columnSearchTerms[column] ??= '';
+ const rect = (
+ event.currentTarget as HTMLElement
+ ).getBoundingClientRect();
+ const root = this.elRef.nativeElement.getBoundingClientRect();
+ const leftRel = rect.left - root.left;
+ const finalLeft =
+ root.right - rect.left >= DROPDOWN_MAX_WIDTH +
DROPDOWN_EDGE_PADDING
+ ? leftRel
+ : Math.max(
+ 0,
+ leftRel -
+ (DROPDOWN_MAX_WIDTH - rect.width) -
+ DROPDOWN_EDGE_PADDING,
+ );
+ this.dropdownStyle = {
+ 'position': 'absolute',
+ 'top': `${rect.bottom - root.top}px`,
+ 'left': `${finalLeft}px`,
+ 'z-index': '9999',
+ };
+ }
+
+ isColumnFilterOpen = (column: string): boolean =>
+ this.openFilterColumn === column;
+
+ hasActiveFilter(column: string): boolean {
+ if (this.advancedFilters[column]) return true;
+ const f = this.columnFilters[column];
+ return !!f && f.size < this.getAllUniqueValues(column).length;
+ }
+
+ getAllUniqueValues = (column: string): string[] =>
+ this.extractUniqueValues(this.rows, column);
+
+ getVisibleUniqueValues(column: string): string[] {
+ let baseRows = this.getRowsFilteredByOtherColumns(column);
+ const adv = this.advancedFilters[column];
+ if (adv)
+ baseRows = this.applyAdvancedFilterToRows(baseRows, column, adv);
+ return this.extractUniqueValues(baseRows, column);
+ }
+
+ getLivePreviewValues(column: string): string[] {
+ let baseRows = this.getRowsFilteredByOtherColumns(column);
+ const adv = this.advancedFilters[column];
+ if (adv)
+ baseRows = this.applyAdvancedFilterToRows(baseRows, column, adv);
+ if (
+ this.showAdvancedPanel === column &&
+ this.selectedAdvancedType &&
+ (this.advancedInputValue ||
+ !this.needsInput(this.selectedAdvancedType))
+ ) {
+ baseRows = this.applyAdvancedFilterToRows(baseRows, column, {
+ type: this.selectedAdvancedType,
+ value: this.advancedInputValue,
+ value2: this.advancedInputValue2,
+ });
+ }
+ return this.extractUniqueValues(baseRows, column);
+ }
+
+ getFilteredUniqueValues(column: string): string[] {
+ const term = (this.columnSearchTerms[column] ?? '')
+ .trim()
+ .toLowerCase();
+ const values =
+ this.showAdvancedPanel === column
+ ? this.getLivePreviewValues(column)
+ : this.getVisibleUniqueValues(column);
+ return term
+ ? values.filter(v => v.toLowerCase().includes(term))
+ : values;
+ }
+
+ isValueChecked = (column: string, value: string): boolean =>
+ this.columnFilters[column]?.has(value) ?? true;
+
+ toggleValue(column: string, value: string): void {
+ this.ensureColumnFilter(column);
+ const f = this.columnFilters[column];
+ if (f.has(value)) {
+ f.delete(value);
} else {
- this.sortDirection = 'asc';
+ f.add(value);
}
+ this.applyTableState(true);
+ }
- this.applyTableState(false);
+ areAllValuesSelected(column: string): boolean {
+ const f = this.columnFilters[column];
+ if (!f) return true;
+ return this.getAllUniqueValues(column).every(v => f.has(v));
+ }
+
+ toggleAllValues(column: string): void {
+ this.ensureColumnFilter(column);
+ const f = this.columnFilters[column];
+ const all = this.getAllUniqueValues(column);
+ const allSelected = all.every(v => f.has(v));
+ all.forEach(v => (allSelected ? f.delete(v) : f.add(v)));
+ this.applyTableState(true);
+ }
+
+ hasSearchOrAdvanced = (column: string): boolean =>
+ !!this.columnSearchTerms[column]?.trim() ||
+ this.showAdvancedPanel === column;
+
+ areDisplayedValuesSelected(column: string): boolean {
+ const f = this.columnFilters[column];
+ if (!f) return true;
+ return this.getFilteredUniqueValues(column).every(v => f.has(v));
+ }
+
+ toggleDisplayedValues(column: string): void {
+ this.ensureColumnFilter(column);
+ const f = this.columnFilters[column];
+ const displayed = this.getFilteredUniqueValues(column);
+ displayed.forEach(v => f.add(v));
Review Comment:
`toggleDisplayedValues` only adds the currently displayed values to the
filter set. Since the checkbox in the UI behaves like a toggle, clicking it
when all displayed values are already selected should also be able to
deselect/uncheck them (e.g., remove the displayed values or toggle based on
`areDisplayedValuesSelected`).
```suggestion
const allDisplayedSelected = displayed.every(v => f.has(v));
displayed.forEach(v => (allDisplayedSelected ? f.delete(v) :
f.add(v)));
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]