SYNC: filterable table control

This commit is contained in:
SPRINX0\prochazka
2025-04-08 13:54:04 +02:00
committed by Diflow
parent fc43b35628
commit 13d057e4f7
4 changed files with 99 additions and 3 deletions

View File

@@ -1,6 +1,9 @@
import { arrayToHexString, isTypeDateTime } from 'dbgate-tools';
import { arrayToHexString, evalFilterBehaviour, isTypeDateTime } from 'dbgate-tools';
import { format, toDate } from 'date-fns';
import _isString from 'lodash/isString';
import _cloneDeepWith from 'lodash/cloneDeepWith';
import { Condition, Expression } from 'dbgate-sqltree';
import { parseFilter } from './parseFilter';
export type FilterMultipleValuesMode = 'is' | 'is_not' | 'contains' | 'begins' | 'ends';
@@ -61,3 +64,29 @@ export function createMultiLineFilter(mode: FilterMultipleValuesMode, text: stri
}
return res;
}
export function compileCompoudEvalCondition(filters: { [column: string]: string }): Condition {
if (!filters) return null;
const conditions = [];
for (const name in filters) {
try {
const condition = parseFilter(filters[name], evalFilterBehaviour);
const replaced = _cloneDeepWith(condition, (expr: Expression) => {
if (expr.exprType == 'placeholder')
return {
exprType: 'column',
columnName: name,
};
});
conditions.push(replaced);
} catch (err) {
// filter parse error - ignore filter
}
}
if (conditions.length == 0) return null;
return {
conditionType: 'and',
conditions,
};
}

View File

@@ -3,6 +3,7 @@
import FontIcon from '../icons/FontIcon.svelte';
import Link from './Link.svelte';
import TableControl from './TableControl.svelte';
import { writable } from 'svelte/store';
export let title;
export let collection;
@@ -12,6 +13,9 @@
export let hideDisplayName = false;
export let clickable = false;
export let onAddNew = null;
export let displayNameFieldName = null;
export let filters = writable({});
let collapsed = false;
</script>
@@ -43,14 +47,16 @@
rows={collection || []}
columns={_.compact([
!hideDisplayName && {
fieldName: 'displayName',
fieldName: displayNameFieldName || 'displayName',
header: 'Name',
slot: -1,
sortable: true,
filterable: !!displayNameFieldName,
},
...columns,
])}
{clickable}
{filters}
on:clickrow
>
<svelte:fragment slot="-1" let:row let:col>

View File

@@ -9,6 +9,8 @@
slot?: number;
isHighlighted?: Function;
sortable?: boolean;
filterable?: boolean;
filteredExpression?: (row: any) => string;
}
</script>
@@ -19,6 +21,10 @@
import keycodes from '../utility/keycodes';
import { createEventDispatcher } from 'svelte';
import FontIcon from '../icons/FontIcon.svelte';
import DataFilterControl from '../datagrid/DataFilterControl.svelte';
import { evalFilterBehaviour } from 'dbgate-tools';
import { evaluateCondition } from 'dbgate-sqltree';
import { compileCompoudEvalCondition } from 'dbgate-filterparser';
export let columns: (TableControlColumn | false)[];
export let rows;
@@ -36,6 +42,7 @@
export let checkedKeys = null;
export let onSetCheckedKeys = null;
export let extractCheckedKey = x => x.id;
export let filters = null;
const dispatch = createEventDispatcher();
@@ -54,10 +61,28 @@
}
};
function filterRows(rows, filters) {
const condition = compileCompoudEvalCondition(filters);
if (!condition) return rows;
return rows.filter(row => {
const newrow = { ...row };
for (const col of columnList) {
if (col.filteredExpression) {
newrow[col.fieldName] = col.filteredExpression(row);
}
}
return evaluateCondition(condition, newrow);
});
}
let sortedByField = null;
let sortOrderIsDesc = false;
$: sortedRowsTmp = sortedByField ? _.sortBy(rows || [], sortedByField) : rows;
$: filteredRows = filters ? filterRows(rows, $filters) : rows;
$: sortedRowsTmp = sortedByField ? _.sortBy(filteredRows || [], sortedByField) : filteredRows;
$: sortedRows = sortOrderIsDesc ? [...sortedRowsTmp].reverse() : sortedRowsTmp;
</script>
@@ -101,6 +126,25 @@
</th>
{/each}
</tr>
{#if filters}
<tr>
{#if checkedKeys}
<td></td>
{/if}
{#each columnList as col}
<td class="filter-cell" class:empty-cell={!col.filterable}>
{#if col.filterable}
<DataFilterControl
filterBehaviour={evalFilterBehaviour}
filter={$filters[col.fieldName]}
setFilter={value => filters.update(f => ({ ...f, [col.fieldName]: value }))}
placeholder="Data filter"
/>
{/if}
</td>
{/each}
</tr>
{/if}
</thead>
<tbody>
{#each sortedRows as row, index}
@@ -232,4 +276,15 @@
border-collapse: separate;
border-left: 1px solid var(--theme-border);
}
.filter-cell {
text-align: left;
overflow: hidden;
margin: 0;
padding: 0;
}
.empty-cell {
background-color: var(--theme-bg-1);
}
</style>

View File

@@ -192,6 +192,7 @@
clickable
on:clickrow={e => showModal(ColumnEditorModal, { columnInfo: e.detail, tableInfo, setTableInfo, driver })}
onAddNew={isWritable ? addColumn : null}
displayNameFieldName="columnName"
columns={[
!driver?.dialect?.specificNullabilityImplementation && {
fieldName: 'notNull',
@@ -203,11 +204,13 @@
fieldName: 'dataType',
header: 'Data Type',
sortable: true,
filterable: true,
},
{
fieldName: 'defaultValue',
header: 'Default value',
sortable: true,
filterable: true,
},
driver?.dialect?.columnProperties?.isSparse && {
fieldName: 'isSparse',
@@ -219,6 +222,7 @@
fieldName: 'computedExpression',
header: 'Computed Expression',
sortable: true,
filterable: true,
},
driver?.dialect?.columnProperties?.isPersisted && {
fieldName: 'isPersisted',
@@ -242,10 +246,12 @@
fieldName: 'columnComment',
header: 'Comment',
sortable: true,
filterable: true,
},
isWritable
? {
fieldName: 'actions',
filterable: false,
slot: 3,
}
: null,