Ok here comes the longer description of my solution.
I have mainTable2 set up with an option list for editableCol. Those values are integers, but with string labels.
In the bottom right you see a filter-button. That's actually not a button but a "copy" of mainTable2, here called filterTableDummy.
With "copy" I mean it takes the first 100 rows of mainTable2, enough to infer data types properly, but not affecting performance.
filterTableDummy has minimal size and grey background, and a Filter-toolbar in the top. This makes the whole table look like a single button.
There's also en event handler which calls a script called filterTableDummyChangeFilterHandler on "Change filter"
Moreover, filterTableDummy also has a Format-script for its dynamic columns (both mainTable2 and filterTableDummy are dynamic so that they can be populated with different source tables' data depending on which source table user clicks in a sidebar). The format script ensures "Option list"-columns are formatted as "tag", and not "decimal" which they are otherwise inferred as.
So now to the fun stuff. As mentioned above, once user changes the filter the script filterTableDummyChangeFilterHandler is triggered.
That script takes filterTableDummy._columnOptionList and uses the filters (filterTableDummy.filterStack) on filterTableDummy._columnOptionList.[column_in_question].labelByIndex to find any label matching a filter. It then picks up the corresponding value (filterTableDummy._columnOptionList.[column_in_question].valueByIndex). Using this info it assemles a new filterStack and sets it on mainTable2. All DONE!
So why using filterTableDummy rather than a filter toolbar on mainTable2? Because when we set the manipulated filterStack the filters user sees will have the numerical value rather than the label user just set. Which is confusing (and it also creates "infinite loops" one would have to handle). Same issue happens with the stand-alone Filter component (which also doesn't handle dynamic columns).
A gif of how it's functioning:
Here's the filterTableDummyChangeFilterHandler script in full:
// Function to update the filterStack with string-based operator logic
function updateFilterStack(filterStack, _columnKey, _columnOptionList) {
if (!filterStack) return null;
const updatedFilters = [];
filterStack.filters.forEach(filter => {
const { columnId, operator, value } = filter;
// Skip matching if the filter's value is empty
if (value === undefined || value === null || value === "") {
return;
}
// Prepare the filter value for comparison (convert to lowercase for case-insensitive matching)
const normalizedValue = value.toString().toLowerCase();
// Find the corresponding columnKey
const columnKey = Object.keys(_columnKey).find(
key => _columnKey[key] === columnId
);
if (columnKey && _columnOptionList[columnKey]) {
// Find matching validation_ids
let matchingIds = [];
for (let index in _columnOptionList[columnKey].labelByIndex) {
let val = _columnOptionList[columnKey].labelByIndex[index];
const validationId = _columnOptionList[columnKey].valueByIndex[index];
// Treat undefined or null as an empty string for comparison
if (val === undefined || val === null) {
val = "";
}
// Prepare the entry value for comparison (convert to lowercase)
const normalizedEntryValue = val.toString().toLowerCase();
switch (operator) {
// Operators applicable to all strings
case "isEmpty":
if (normalizedEntryValue === "") {
matchingIds.push(validationId);
}
break;
case "isNotEmpty":
if (normalizedEntryValue !== "") {
matchingIds.push(validationId);
}
break;
case "is":
if (normalizedEntryValue === normalizedValue) {
matchingIds.push(validationId);
}
break;
case "isNot":
if (normalizedEntryValue !== normalizedValue) {
matchingIds.push(validationId);
}
break;
case "includes":
if (normalizedEntryValue.includes(normalizedValue)) {
matchingIds.push(validationId);
}
break;
case "doesNotInclude":
if (!normalizedEntryValue.includes(normalizedValue)) {
matchingIds.push(validationId);
}
break;
default:
// Handle other operators if needed
break;
}
}
// Update filters based on matchingIds
if (matchingIds.length > 0) {
if (matchingIds.length === 1) {
// Single match
updatedFilters.push({
id: uuid.v4(),
columnId: columnId,
operator: "=",
value: matchingIds[0].toString()
});
} else {
// Multiple matches: create sub-filters combined with "or"
const subFilters = matchingIds.map(id => ({
id: uuid.v4(),
columnId: columnId,
operator: "is",
value: id.toString()
}));
updatedFilters.push({
id: uuid.v4(),
operator: "or",
filters: subFilters
});
}
} else {
// No matches found: add an impossible value filter
updatedFilters.push({
id: uuid.v4(),
columnId: columnId,
operator: "=",
value: "hf983f9)37f6&#$?" // some value that will never match
});
}
} else {
// No columnKey or mappedData found: keep the filter unchanged
updatedFilters.push(filter);
}
});
// Update the filterStack with the new filters
filterStack.filters = updatedFilters;
return filterStack;
}
// Use the function
const updatedFilterStack = updateFilterStack(filterTableDummy.filterStack, mainTable2._columnKey, mainTable2._columnOptionList);
if (updatedFilterStack) {
mainTable2.setFilterStack({filters: updatedFilterStack.filters, operator: updatedFilterStack.operator})
}
else {
mainTable2.clearFilterStack();
}