Skip to content

Commit

Permalink
feat: enhanced string column filter (#1367)
Browse files Browse the repository at this point in the history
  • Loading branch information
csmig authored Sep 10, 2024
1 parent b3a6f4b commit efb4ad6
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 47 deletions.
10 changes: 10 additions & 0 deletions client/src/css/stigman.css
Original file line number Diff line number Diff line change
Expand Up @@ -2263,4 +2263,14 @@ td.x-grid3-hd-over .x-grid3-hd-inner {
}
.x-item-disabled {
filter: saturate(0)
}

.sm-match-case-icon {
background-image: url(../img/match-case.svg);
background-size: contain;
}

.sm-match-word-icon {
background-image: url(../img/match-word.svg);
background-size: contain;
}
4 changes: 4 additions & 0 deletions client/src/img/match-case.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions client/src/img/match-word.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
213 changes: 166 additions & 47 deletions client/src/js/SM/ColumnFilters.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,9 @@ SM.ColumnFilters.extend = function extend (extended = Ext.grid.GridView) {

// // iterate the menu items and set the condition(s) for each dataIndex
for (const stringItem of stringItems) {
const dataIndex = stringItem.filter.dataIndex
const value = stringItem.getValue()
if (value) {
conditions[dataIndex] = value
if (value.value) {
conditions[stringItem.filter.dataIndex] = value
}
}
for (const selectItem of selectItems) {
Expand All @@ -99,24 +98,32 @@ SM.ColumnFilters.extend = function extend (extended = Ext.grid.GridView) {
for (const dataIndex of Object.keys(conditions)) {
filterFns.push({
fn: function (record) {
const value = record.data[dataIndex]
if (Array.isArray(value)) {
const cellValue = record.data[dataIndex]
const condition = conditions[dataIndex]
if (Array.isArray(cellValue)) {
// the record data is an Array of values
if (Array.isArray(conditions[dataIndex])) {
if (conditions[dataIndex].includes('') && value.length === 0) return true
return value.some( v => conditions[dataIndex].includes(v))
if (Array.isArray(condition)) {
if (condition.includes('') && cellValue.length === 0) return true
return cellValue.some( v => condition.includes(v))
}
}

// the record data is a scalar value (we're missing object handling?)
if (Array.isArray(conditions[dataIndex])) {
return conditions[dataIndex].includes(value)
if (Array.isArray(condition)) {
return condition.includes(cellValue)
}
else {
// match case-insensitive condition anywhere in value
const a = value.toLowerCase()
const b = conditions[dataIndex].toLowerCase()
return a.indexOf(b) > -1
// string matches
const a = condition.matchCase ? cellValue : cellValue.toLowerCase()
const b = condition.matchCase ? condition.value : condition.value.toLowerCase()
let found
if (condition.matchWord) {
found = a.search(new RegExp(`\\b${b}\\b`))
}
else {
found = a.indexOf(b)
}
return condition.condition ? found > -1 : found === -1
}
}
})
Expand All @@ -126,14 +133,16 @@ SM.ColumnFilters.extend = function extend (extended = Ext.grid.GridView) {
onFilterChange: function (item, value) {
switch (item.filter.type) {
case 'string':
item.column.filtered = !!(item.getValue())
item.column.filtered = !!(item.getValue()?.value)
break
case 'values':
const hmenuItems = this.hmenu.items.items
const hmenuPeers = hmenuItems.filter( i => i.filter?.type === 'values' && i.filter?.dataIndex === item.filter.dataIndex)
const hmenuPeersChecked = hmenuPeers.map( i => i.checked)
item.column.filtered = hmenuPeersChecked.includes(false)
break
{
const hmenuItems = this.hmenu.items.items
const hmenuPeers = hmenuItems.filter( i => i.filter?.type === 'values' && i.filter?.dataIndex === item.filter.dataIndex)
const hmenuPeersChecked = hmenuPeers.map( i => i.checked)
item.column.filtered = hmenuPeersChecked.includes(false)
break
}
case 'selectall':
item.column.filtered = !(!!value)
break
Expand Down Expand Up @@ -273,33 +282,27 @@ SM.ColumnFilters.extend = function extend (extended = Ext.grid.GridView) {

for (const col of this.cm.config) {
switch (col.filter?.type) {
case 'string':
case 'string': {
if (col.renderer) {
col.configRenderer = col.renderer
col.renderer = SM.ColumnFilters.Renderers.highlighterShim
}
const stringItem = hmenu.add(new SM.ColumnFilters.SearchTextField({
emptyText: "Contains...",
height: 24,
const stringItem = hmenu.add(new SM.ColumnFilters.StringPanel({
hideOnClick: false,
column: col,
filter: { dataIndex: col.dataIndex, type: 'string'},
enableKeyEvents: true,
hideParent: true,
listeners: {
input: function (item, e) {
_this.onFilterChange(item, item.value)
filterchanged: function (panel) {
_this.onFilterChange(panel, panel.getValue())
},
keyup: function (item, e) {
const k = e.getKey()
if (k == e.RETURN) {
e.stopEvent();
hmenu.hide(true)
}
enterkey: function () {
hmenu.hide(true)
}
}
}))
hmenu.filterItems.stringItems.push(stringItem)
break
}
case 'values':
dynamicColumns.push(col)
break
Expand All @@ -314,28 +317,143 @@ SM.ColumnFilters.extend = function extend (extended = Ext.grid.GridView) {
SM.ColumnFilters.GridView = SM.ColumnFilters.extend(Ext.grid.GridView)
SM.ColumnFilters.GridViewBuffered = SM.ColumnFilters.extend(Ext.ux.grid.BufferView)

SM.ColumnFilters.SearchTextField = Ext.extend(Ext.form.TextField, {
SM.ColumnFilters.StringMatchTextField = Ext.extend(Ext.form.TextField, {
initComponent: function () {
const config = {
autoCreate: {tag: 'input', type: 'search', size: '20', autocomplete: 'off'}
autoCreate: {tag: 'input', type: 'search', size: '20', autocomplete: 'off'},
enableKeyEvents: true
}
Ext.apply(this, Ext.apply(this.initialConfig, config))
SM.ColumnFilters.SearchTextField.superclass.initComponent.call(this)
this.superclass().initComponent.call(this)
this.addEvents( 'input' )
},
initEvents: function () {
SM.ColumnFilters.SearchTextField.superclass.initEvents.call(this)
this.superclass().initEvents.call(this)
this.mon(this.el, {
scope: this,
input: this.onInput
})
},
onInput: function (e) {
this.column.filter.value = this.getValue()
this.fireEvent('input', this, e);
}
})

SM.ColumnFilters.StringMatchConditionComboBox = Ext.extend(Ext.form.ComboBox, {
initComponent: function () {
const store = new Ext.data.ArrayStore({
fields: ['display', 'value'],
data: [['Includes', true], ['Excludes', false]]
})
const config = {
listClass: 'x-menu',
store,
triggerAction: 'all',
mode: 'local',
editable: false,
valueField: 'value',
displayField: 'display'
}
Ext.apply(this, Ext.apply(this.initialConfig, config))
this.superclass().initComponent.call(this)
this.setValue(true)
}
})

SM.ColumnFilters.StringMatchCaseButton = Ext.extend(Ext.Button, {
initComponent: function () {
const config = {
enableToggle: true,
border: false,
iconCls: 'sm-match-case-icon',
tooltip: 'Match case'
}
Ext.apply(this, Ext.apply(this.initialConfig, config))
this.superclass().initComponent.call(this)
}
})

SM.ColumnFilters.StringMatchWordButton = Ext.extend(Ext.Button, {
initComponent: function () {
const config = {
enableToggle: true,
border: false,
iconCls: 'sm-match-word-icon',
tooltip: 'Match word'
}
Ext.apply(this, Ext.apply(this.initialConfig, config))
this.superclass().initComponent.call(this)
}
})

SM.ColumnFilters.StringPanel = Ext.extend(Ext.Panel, {
initComponent: function () {
const _this = this

const onFilterChange = function () {
_this.column.filter.value = getValue()
_this.fireEvent('filterchanged', _this)
}

const conditionComboBox = new SM.ColumnFilters.StringMatchConditionComboBox({
flex: 1,
listeners: {
select: onFilterChange
}
})
const matchCaseButton = new SM.ColumnFilters.StringMatchCaseButton({
width: 24,
listeners: {
toggle: onFilterChange
}
})
const matchWordButton = new SM.ColumnFilters.StringMatchWordButton({
width: 24,
listeners: {
toggle: onFilterChange
}
})
const textfield = new SM.ColumnFilters.StringMatchTextField({
height: 24,
listeners: {
input: onFilterChange,
keyup: function (item, e) {
const k = e.getKey()
if (k == e.RETURN) {
e.stopEvent()
_this.fireEvent('enterkey')
}
}
}
})

function getValue () {
return {
value: textfield.getValue() ?? '',
condition: conditionComboBox.getValue(),
matchCase: matchCaseButton.pressed,
matchWord: matchWordButton.pressed,
}
}
const config = {
getValue,
items: [
{
layout: 'hbox',
items: [
conditionComboBox,
matchCaseButton,
matchWordButton
]
},
textfield
]
}
Ext.apply(this, Ext.apply(this.initialConfig, config))
this.superclass().initComponent.call(this)
}
})

SM.ColumnFilters.Scorers = {
severity: {
low: 2,
Expand All @@ -347,15 +465,12 @@ SM.ColumnFilters.Scorers = {
SM.ColumnFilters.CompareFns = {
severity: (a, b) => {
return SM.ColumnFilters.Scorers.severity[a] - SM.ColumnFilters.Scorers.severity[b]
},
labels: (a, b) => {

},
labelIds: (a, b, collectionId) => {
if (a === "") return -1;
if (b === "") return 1;
return SM.Cache.getCollectionLabel(collectionId, a).name.localeCompare(SM.Cache.getCollectionLabel(collectionId, b).name)
},
}
}

SM.ColumnFilters.Renderers = {
Expand Down Expand Up @@ -404,11 +519,15 @@ SM.ColumnFilters.Renderers = {
}
},
highlighterShim: function (v, m, r, ri, ci, s) {
if (this.filter?.type === 'string' && this.filter?.value) {
const re = new RegExp(SM.he(this.filter.value),'gi')
v = v.replace(re,'<span class="sm-text-highlight">$&</span>')
if (this.filter?.type === 'string' && this.filter.value?.value && this.filter.value.condition) {
let searchStr = SM.he(this.filter.value.value)
const flags = `g${this.filter.value.matchCase ? '' : 'i'}`
if (this.filter.value.matchWord) {
searchStr = `\\b${searchStr}\\b`
}
v = v.replace(new RegExp(searchStr, flags),'<span class="sm-text-highlight">$&</span>')
}
return this.configRenderer ? this.configRenderer.call(this, v, m, r, ri, ci, s) : v
return this.configRenderer ? this.configRenderer(v, m, r, ri, ci, s) : v
},
labels: function (labelId, collectionId) {
if (!labelId) return '<i>(No value)</i>'
Expand Down

0 comments on commit efb4ad6

Please sign in to comment.