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 { format, toDate } from 'date-fns';
import _isString from 'lodash/isString'; 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'; export type FilterMultipleValuesMode = 'is' | 'is_not' | 'contains' | 'begins' | 'ends';
@@ -61,3 +64,29 @@ export function createMultiLineFilter(mode: FilterMultipleValuesMode, text: stri
} }
return res; 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 FontIcon from '../icons/FontIcon.svelte';
import Link from './Link.svelte'; import Link from './Link.svelte';
import TableControl from './TableControl.svelte'; import TableControl from './TableControl.svelte';
import { writable } from 'svelte/store';
export let title; export let title;
export let collection; export let collection;
@@ -12,6 +13,9 @@
export let hideDisplayName = false; export let hideDisplayName = false;
export let clickable = false; export let clickable = false;
export let onAddNew = null; export let onAddNew = null;
export let displayNameFieldName = null;
export let filters = writable({});
let collapsed = false; let collapsed = false;
</script> </script>
@@ -43,14 +47,16 @@
rows={collection || []} rows={collection || []}
columns={_.compact([ columns={_.compact([
!hideDisplayName && { !hideDisplayName && {
fieldName: 'displayName', fieldName: displayNameFieldName || 'displayName',
header: 'Name', header: 'Name',
slot: -1, slot: -1,
sortable: true, sortable: true,
filterable: !!displayNameFieldName,
}, },
...columns, ...columns,
])} ])}
{clickable} {clickable}
{filters}
on:clickrow on:clickrow
> >
<svelte:fragment slot="-1" let:row let:col> <svelte:fragment slot="-1" let:row let:col>

View File

@@ -9,6 +9,8 @@
slot?: number; slot?: number;
isHighlighted?: Function; isHighlighted?: Function;
sortable?: boolean; sortable?: boolean;
filterable?: boolean;
filteredExpression?: (row: any) => string;
} }
</script> </script>
@@ -19,6 +21,10 @@
import keycodes from '../utility/keycodes'; import keycodes from '../utility/keycodes';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import FontIcon from '../icons/FontIcon.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 columns: (TableControlColumn | false)[];
export let rows; export let rows;
@@ -36,6 +42,7 @@
export let checkedKeys = null; export let checkedKeys = null;
export let onSetCheckedKeys = null; export let onSetCheckedKeys = null;
export let extractCheckedKey = x => x.id; export let extractCheckedKey = x => x.id;
export let filters = null;
const dispatch = createEventDispatcher(); 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 sortedByField = null;
let sortOrderIsDesc = false; 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; $: sortedRows = sortOrderIsDesc ? [...sortedRowsTmp].reverse() : sortedRowsTmp;
</script> </script>
@@ -101,6 +126,25 @@
</th> </th>
{/each} {/each}
</tr> </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> </thead>
<tbody> <tbody>
{#each sortedRows as row, index} {#each sortedRows as row, index}
@@ -232,4 +276,15 @@
border-collapse: separate; border-collapse: separate;
border-left: 1px solid var(--theme-border); 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> </style>

View File

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