feat: enhance DeesTable with server-side search and Lucene filtering capabilities
This commit is contained in:
@@ -28,6 +28,9 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
|
|||||||
})
|
})
|
||||||
public value: boolean = false;
|
public value: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public indeterminate: boolean = false;
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -166,7 +169,15 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
`
|
`
|
||||||
: html``}
|
: this.indeterminate
|
||||||
|
? html`
|
||||||
|
<span class="checkmark">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5 12H19" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
: html``}
|
||||||
</div>
|
</div>
|
||||||
<div class="label-container">
|
<div class="label-container">
|
||||||
${this.label ? html`<div class="checkbox-label">${this.label}</div>` : ''}
|
${this.label ? html`<div class="checkbox-label">${this.label}</div>` : ''}
|
||||||
|
@@ -42,7 +42,9 @@ export function getViewData<T>(
|
|||||||
sortKey?: string,
|
sortKey?: string,
|
||||||
sortDir?: 'asc' | 'desc' | null,
|
sortDir?: 'asc' | 'desc' | null,
|
||||||
filterText?: string,
|
filterText?: string,
|
||||||
columnFilters?: Record<string, string>
|
columnFilters?: Record<string, string>,
|
||||||
|
filterMode: 'table' | 'data' = 'table',
|
||||||
|
lucenePredicate?: (row: T) => boolean
|
||||||
): T[] {
|
): T[] {
|
||||||
let arr = data.slice();
|
let arr = data.slice();
|
||||||
const ft = (filterText || '').trim().toLowerCase();
|
const ft = (filterText || '').trim().toLowerCase();
|
||||||
@@ -52,23 +54,39 @@ export function getViewData<T>(
|
|||||||
arr = arr.filter((row) => {
|
arr = arr.filter((row) => {
|
||||||
// column filters (AND across columns)
|
// column filters (AND across columns)
|
||||||
for (const k of cfKeys) {
|
for (const k of cfKeys) {
|
||||||
const col = effectiveColumns.find((c) => String(c.key) === k);
|
if (filterMode === 'data') {
|
||||||
if (!col || col.hidden || col.filterable === false) continue;
|
// raw object check for that key
|
||||||
const val = getCellValue(row, col);
|
const val = (row as any)[k];
|
||||||
const s = String(val ?? '').toLowerCase();
|
const s = String(val ?? '').toLowerCase();
|
||||||
const needle = String(cf[k]).toLowerCase();
|
const needle = String(cf[k]).toLowerCase();
|
||||||
if (!s.includes(needle)) return false;
|
if (!s.includes(needle)) return false;
|
||||||
}
|
} else {
|
||||||
// global filter (OR across visible columns)
|
const col = effectiveColumns.find((c) => String(c.key) === k);
|
||||||
if (ft) {
|
if (!col || col.hidden || col.filterable === false) continue;
|
||||||
let any = false;
|
|
||||||
for (const col of effectiveColumns) {
|
|
||||||
if (col.hidden) continue;
|
|
||||||
const val = getCellValue(row, col);
|
const val = getCellValue(row, col);
|
||||||
const s = String(val ?? '').toLowerCase();
|
const s = String(val ?? '').toLowerCase();
|
||||||
if (s.includes(ft)) {
|
const needle = String(cf[k]).toLowerCase();
|
||||||
any = true;
|
if (!s.includes(needle)) return false;
|
||||||
break;
|
}
|
||||||
|
}
|
||||||
|
// global filter (OR across visible columns) or lucene predicate
|
||||||
|
if (ft) {
|
||||||
|
if (lucenePredicate) {
|
||||||
|
if (!lucenePredicate(row)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let any = false;
|
||||||
|
if (filterMode === 'data') {
|
||||||
|
for (const val of Object.values(row as any)) {
|
||||||
|
const s = String(val ?? '').toLowerCase();
|
||||||
|
if (s.includes(ft)) { any = true; break; }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const col of effectiveColumns) {
|
||||||
|
if (col.hidden) continue;
|
||||||
|
const val = getCellValue(row, col);
|
||||||
|
const s = String(val ?? '').toLowerCase();
|
||||||
|
if (s.includes(ft)) { any = true; break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!any) return false;
|
if (!any) return false;
|
||||||
|
@@ -537,6 +537,48 @@ export const demoFunc = () => html`
|
|||||||
dataName="employees"
|
dataName="employees"
|
||||||
></dees-table>
|
></dees-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section"
|
||||||
|
@searchRequest=${async (e: CustomEvent) => {
|
||||||
|
const { query } = e.detail || { query: '' };
|
||||||
|
const table = document.getElementById('serverSearchDemo') as any;
|
||||||
|
const baseData = [
|
||||||
|
{ id: 1, name: 'Alice', city: 'Berlin', title: 'Engineer' },
|
||||||
|
{ id: 2, name: 'Bob', city: 'Paris', title: 'Designer' },
|
||||||
|
{ id: 3, name: 'Charlie', city: 'London', title: 'Manager' },
|
||||||
|
{ id: 4, name: 'Diana', city: 'Madrid', title: 'Engineer' },
|
||||||
|
{ id: 5, name: 'Ethan', city: 'Rome', title: 'Support' },
|
||||||
|
];
|
||||||
|
// Simulate async request
|
||||||
|
await new Promise((r) => setTimeout(r, 300));
|
||||||
|
const q = String(query || '').toLowerCase();
|
||||||
|
const filtered = q
|
||||||
|
? baseData.filter((r) => Object.values(r).some((v) => String(v).toLowerCase().includes(q)))
|
||||||
|
: baseData;
|
||||||
|
table.data = filtered;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 class="demo-title">Server Search (New)</h2>
|
||||||
|
<p class="demo-description">Select Server mode, type a query, and watch the table fetch simulated results.</p>
|
||||||
|
<dees-table
|
||||||
|
id="serverSearchDemo"
|
||||||
|
heading1="People (Server Search)"
|
||||||
|
heading2="Click Search, choose Server mode, and type"
|
||||||
|
.columns=${[
|
||||||
|
{ key: 'name', header: 'Name' },
|
||||||
|
{ key: 'city', header: 'City' },
|
||||||
|
{ key: 'title', header: 'Title' },
|
||||||
|
]}
|
||||||
|
.data=${[
|
||||||
|
{ id: 1, name: 'Alice', city: 'Berlin', title: 'Engineer' },
|
||||||
|
{ id: 2, name: 'Bob', city: 'Paris', title: 'Designer' },
|
||||||
|
{ id: 3, name: 'Charlie', city: 'London', title: 'Manager' },
|
||||||
|
{ id: 4, name: 'Diana', city: 'Madrid', title: 'Engineer' },
|
||||||
|
{ id: 5, name: 'Ethan', city: 'Rome', title: 'Support' },
|
||||||
|
]}
|
||||||
|
dataName="people"
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@@ -13,6 +13,7 @@ import {
|
|||||||
getCellValue as getCellValueFn,
|
getCellValue as getCellValueFn,
|
||||||
getViewData as getViewDataFn,
|
getViewData as getViewDataFn,
|
||||||
} from './data.js';
|
} from './data.js';
|
||||||
|
import { compileLucenePredicate } from './lucene.js';
|
||||||
|
|
||||||
export type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js';
|
export type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js';
|
||||||
|
|
||||||
@@ -174,6 +175,12 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
@property({ type: Boolean, reflect: true, attribute: 'sticky-header' })
|
@property({ type: Boolean, reflect: true, attribute: 'sticky-header' })
|
||||||
public stickyHeader: boolean = false;
|
public stickyHeader: boolean = false;
|
||||||
|
|
||||||
|
// search row state
|
||||||
|
@property({ type: String })
|
||||||
|
public searchMode: 'table' | 'data' | 'server' = 'table';
|
||||||
|
private __searchTextSub?: { unsubscribe?: () => void };
|
||||||
|
private __searchModeSub?: { unsubscribe?: () => void };
|
||||||
|
|
||||||
// selection (Phase 1)
|
// selection (Phase 1)
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
public selectionMode: 'none' | 'single' | 'multi' = 'none';
|
public selectionMode: 'none' | 'single' | 'multi' = 'none';
|
||||||
@@ -194,6 +201,23 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
? computeEffectiveColumnsFn(this.columns, this.augmentFromDisplayFunction, this.displayFunction, this.data)
|
? computeEffectiveColumnsFn(this.columns, this.augmentFromDisplayFunction, this.displayFunction, this.data)
|
||||||
: computeColumnsFromDisplayFunctionFn(this.displayFunction, this.data);
|
: computeColumnsFromDisplayFunctionFn(this.displayFunction, this.data);
|
||||||
|
|
||||||
|
const lucenePred = compileLucenePredicate<T>(
|
||||||
|
this.filterText,
|
||||||
|
this.searchMode === 'data' ? 'data' : 'table',
|
||||||
|
effectiveColumns
|
||||||
|
);
|
||||||
|
|
||||||
|
const viewData = getViewDataFn(
|
||||||
|
this.data,
|
||||||
|
effectiveColumns,
|
||||||
|
this.sortKey,
|
||||||
|
this.sortDir,
|
||||||
|
this.filterText,
|
||||||
|
this.columnFilters,
|
||||||
|
this.searchMode === 'data' ? 'data' : 'table',
|
||||||
|
lucenePred || undefined
|
||||||
|
);
|
||||||
|
(this as any)._lastViewData = viewData;
|
||||||
return html`
|
return html`
|
||||||
<div class="mainbox">
|
<div class="mainbox">
|
||||||
<!-- the heading part -->
|
<!-- the heading part -->
|
||||||
@@ -270,10 +294,11 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
${this.selectionMode === 'multi'
|
${this.selectionMode === 'multi'
|
||||||
? html`
|
? html`
|
||||||
<dees-input-checkbox
|
<dees-input-checkbox
|
||||||
.value=${this.areAllSelected()}
|
.value=${this.areAllVisibleSelected()}
|
||||||
|
.indeterminate=${this.isVisibleSelectionIndeterminate()}
|
||||||
@newValue=${(e: CustomEvent<boolean>) => {
|
@newValue=${(e: CustomEvent<boolean>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.setSelectAll(e.detail === true);
|
this.setSelectVisible(e.detail === true);
|
||||||
}}
|
}}
|
||||||
></dees-input-checkbox>
|
></dees-input-checkbox>
|
||||||
`
|
`
|
||||||
@@ -327,7 +352,7 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
: html``}
|
: html``}
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${getViewDataFn(this.data, effectiveColumns, this.sortKey, this.sortDir, this.filterText, this.columnFilters).map((itemArg, rowIndex) => {
|
${viewData.map((itemArg, rowIndex) => {
|
||||||
const getTr = (elementArg: HTMLElement): HTMLElement => {
|
const getTr = (elementArg: HTMLElement): HTMLElement => {
|
||||||
if (elementArg.tagName === 'TR') {
|
if (elementArg.tagName === 'TR') {
|
||||||
return elementArg;
|
return elementArg;
|
||||||
@@ -528,6 +553,53 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
console.log(this.dataActions);
|
console.log(this.dataActions);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
};
|
};
|
||||||
|
// wire search inputs
|
||||||
|
this.wireSearchInputs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private __debounceTimer?: any;
|
||||||
|
private debounceRun(fn: () => void, ms = 200) {
|
||||||
|
if (this.__debounceTimer) clearTimeout(this.__debounceTimer);
|
||||||
|
this.__debounceTimer = setTimeout(fn, ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
private wireSearchInputs() {
|
||||||
|
const searchTextEl: any = this.shadowRoot?.querySelector('.searchGrid dees-input-text');
|
||||||
|
const searchModeEl: any = this.shadowRoot?.querySelector('.searchGrid dees-input-multitoggle');
|
||||||
|
if (searchTextEl && !this.__searchTextSub) {
|
||||||
|
this.__searchTextSub = searchTextEl.changeSubject.subscribe((el: any) => {
|
||||||
|
const val: string = el?.value ?? '';
|
||||||
|
this.debounceRun(() => {
|
||||||
|
if (this.searchMode === 'server') {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('searchRequest', {
|
||||||
|
detail: { query: val, mode: 'server' },
|
||||||
|
bubbles: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.setFilterText(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (searchModeEl && !this.__searchModeSub) {
|
||||||
|
this.__searchModeSub = searchModeEl.changeSubject.subscribe((el: any) => {
|
||||||
|
const mode: string = el?.selectedOption || el?.value || 'table';
|
||||||
|
if (mode === 'table' || mode === 'data' || mode === 'server') {
|
||||||
|
this.searchMode = mode as any;
|
||||||
|
// When switching modes, re-apply current text input
|
||||||
|
const val: string = searchTextEl?.value ?? '';
|
||||||
|
this.debounceRun(() => {
|
||||||
|
if (this.searchMode === 'server') {
|
||||||
|
this.dispatchEvent(new CustomEvent('searchRequest', { detail: { query: val, mode: 'server' }, bubbles: true }));
|
||||||
|
} else {
|
||||||
|
this.setFilterText(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,25 +749,31 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private areAllSelected(): boolean {
|
private areAllVisibleSelected(): boolean {
|
||||||
return this.data.length > 0 && this.selectedIds.size === this.data.length;
|
const view: T[] = (this as any)._lastViewData || [];
|
||||||
}
|
if (view.length === 0) return false;
|
||||||
|
for (const r of view) {
|
||||||
private toggleSelectAll() {
|
if (!this.selectedIds.has(this.getRowId(r))) return false;
|
||||||
if (this.areAllSelected()) {
|
|
||||||
this.selectedIds.clear();
|
|
||||||
} else {
|
|
||||||
this.selectedIds = new Set(this.data.map((r) => this.getRowId(r)));
|
|
||||||
}
|
}
|
||||||
this.emitSelectionChange();
|
return true;
|
||||||
this.requestUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setSelectAll(checked: boolean) {
|
private isVisibleSelectionIndeterminate(): boolean {
|
||||||
|
const view: T[] = (this as any)._lastViewData || [];
|
||||||
|
if (view.length === 0) return false;
|
||||||
|
let count = 0;
|
||||||
|
for (const r of view) {
|
||||||
|
if (this.selectedIds.has(this.getRowId(r))) count++;
|
||||||
|
}
|
||||||
|
return count > 0 && count < view.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setSelectVisible(checked: boolean) {
|
||||||
|
const view: T[] = (this as any)._lastViewData || [];
|
||||||
if (checked) {
|
if (checked) {
|
||||||
this.selectedIds = new Set(this.data.map((r) => this.getRowId(r)));
|
for (const r of view) this.selectedIds.add(this.getRowId(r));
|
||||||
} else {
|
} else {
|
||||||
this.selectedIds.clear();
|
for (const r of view) this.selectedIds.delete(this.getRowId(r));
|
||||||
}
|
}
|
||||||
this.emitSelectionChange();
|
this.emitSelectionChange();
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
|
158
ts_web/elements/dees-table/lucene.ts
Normal file
158
ts_web/elements/dees-table/lucene.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import type { Column } from './types.js';
|
||||||
|
|
||||||
|
type FilterMode = 'table' | 'data';
|
||||||
|
|
||||||
|
export type RowPredicate<T> = (row: T) => boolean;
|
||||||
|
|
||||||
|
interface Term {
|
||||||
|
field?: string; // if undefined, match across all fields
|
||||||
|
value?: string; // lowercased string
|
||||||
|
negate?: boolean;
|
||||||
|
range?: { lower: string; upper: string; inclusive: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Clause {
|
||||||
|
terms: Term[]; // AND across terms
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LuceneQuery {
|
||||||
|
clauses: Clause[]; // OR across clauses
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripQuotes(s: string): string {
|
||||||
|
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
||||||
|
return s.slice(1, -1);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitByOr(input: string): string[] {
|
||||||
|
return input.split(/\s+OR\s+/i).map((s) => s.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitByAnd(input: string): string[] {
|
||||||
|
return input.split(/\s+AND\s+/i).map((s) => s.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTerm(raw: string): Term | null {
|
||||||
|
if (!raw) return null;
|
||||||
|
let negate = false;
|
||||||
|
// handle NOT prefix or leading '-'
|
||||||
|
const notMatch = raw.match(/^\s*(NOT\s+|-)\s*(.*)$/i);
|
||||||
|
if (notMatch) {
|
||||||
|
negate = true;
|
||||||
|
raw = notMatch[2];
|
||||||
|
}
|
||||||
|
// range: field:[lower TO upper]
|
||||||
|
const rangeMatch = raw.match(/^([^:\s]+)\s*:\s*\[(.*?)\s+TO\s+(.*?)\]$/i);
|
||||||
|
if (rangeMatch) {
|
||||||
|
return {
|
||||||
|
field: rangeMatch[1],
|
||||||
|
negate,
|
||||||
|
range: { lower: stripQuotes(rangeMatch[2]).toLowerCase(), upper: stripQuotes(rangeMatch[3]).toLowerCase(), inclusive: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// field:value (value may be quoted)
|
||||||
|
const m = raw.match(/^([^:\s]+)\s*:\s*("[^"]*"|'[^']*'|[^"'\s]+)$/);
|
||||||
|
if (m) {
|
||||||
|
return { field: m[1], value: stripQuotes(m[2]).toLowerCase(), negate };
|
||||||
|
}
|
||||||
|
// plain term
|
||||||
|
if (raw.length > 0) {
|
||||||
|
return { value: stripQuotes(raw).toLowerCase(), negate };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLucene(input: string): LuceneQuery | null {
|
||||||
|
if (!input) return null;
|
||||||
|
const clauses = splitByOr(input).map((clauseStr) => {
|
||||||
|
const terms = splitByAnd(clauseStr)
|
||||||
|
.map(parseTerm)
|
||||||
|
.filter((t): t is Term => !!t && !!t.value);
|
||||||
|
return { terms } as Clause;
|
||||||
|
}).filter((c) => c.terms.length > 0);
|
||||||
|
if (clauses.length === 0) return null;
|
||||||
|
return { clauses };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compileLucenePredicate<T>(
|
||||||
|
input: string,
|
||||||
|
mode: FilterMode,
|
||||||
|
columns: Column<T>[]
|
||||||
|
): RowPredicate<T> | null {
|
||||||
|
const ast = parseLucene(input);
|
||||||
|
if (!ast) return null;
|
||||||
|
const colMap = new Map<string, Column<T>>(
|
||||||
|
columns.map((c) => [String(c.key), c])
|
||||||
|
);
|
||||||
|
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
|
||||||
|
const coerce = (s: any) => {
|
||||||
|
const str = String(s ?? '').toLowerCase();
|
||||||
|
const num = Number(str);
|
||||||
|
const date = Date.parse(str);
|
||||||
|
if (!Number.isNaN(num) && str.trim() !== '') return { t: 'n' as const, v: num };
|
||||||
|
if (!Number.isNaN(date)) return { t: 'd' as const, v: date };
|
||||||
|
return { t: 's' as const, v: str };
|
||||||
|
};
|
||||||
|
const inRange = (val: any, lower: string, upper: string) => {
|
||||||
|
const a = coerce(val);
|
||||||
|
const lo = coerce(lower);
|
||||||
|
const up = coerce(upper);
|
||||||
|
// if types differ, compare string forms
|
||||||
|
if (a.t !== lo.t || a.t !== up.t) {
|
||||||
|
const as = String(val ?? '').toLowerCase();
|
||||||
|
return cmp(as, lower) >= 0 && cmp(as, upper) <= 0;
|
||||||
|
}
|
||||||
|
return a.v >= (lo.v as number) && a.v <= (up.v as number);
|
||||||
|
};
|
||||||
|
return (row: T) => {
|
||||||
|
for (const clause of ast.clauses) {
|
||||||
|
let clauseOk = true;
|
||||||
|
for (const term of clause.terms) {
|
||||||
|
let ok = false;
|
||||||
|
if (term.range && term.field) {
|
||||||
|
// range compare on field
|
||||||
|
if (mode === 'data') {
|
||||||
|
ok = inRange((row as any)[term.field], term.range.lower, term.range.upper);
|
||||||
|
} else {
|
||||||
|
const col = colMap.get(term.field);
|
||||||
|
if (!col || col.hidden) { ok = false; } else {
|
||||||
|
const val = col.value ? col.value(row) : (row as any)[col.key as any];
|
||||||
|
ok = inRange(val, term.range.lower, term.range.upper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (term.field && term.value != null) {
|
||||||
|
if (mode === 'data') {
|
||||||
|
const s = String((row as any)[term.field] ?? '').toLowerCase();
|
||||||
|
ok = s.includes(term.value);
|
||||||
|
} else {
|
||||||
|
const col = colMap.get(term.field);
|
||||||
|
if (!col || col.hidden === true) { ok = false; }
|
||||||
|
else {
|
||||||
|
const val = col.value ? col.value(row) : (row as any)[col.key as any];
|
||||||
|
const s = String(val ?? '').toLowerCase();
|
||||||
|
ok = s.includes(term.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (term.value != null) {
|
||||||
|
// search across all visible/raw fields
|
||||||
|
if (mode === 'data') {
|
||||||
|
ok = Object.values(row as any).some((v) => String(v ?? '').toLowerCase().includes(term.value!));
|
||||||
|
} else {
|
||||||
|
ok = columns.some((col) => {
|
||||||
|
if (col.hidden) return false;
|
||||||
|
const val = col.value ? col.value(row) : (row as any)[col.key as any];
|
||||||
|
const s = String(val ?? '').toLowerCase();
|
||||||
|
return s.includes(term.value!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (term.negate) ok = !ok;
|
||||||
|
if (!ok) { clauseOk = false; break; }
|
||||||
|
}
|
||||||
|
if (clauseOk) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
Reference in New Issue
Block a user