Table not searching tags

I have a table created from a MySql query. One of the columns is type tags. It pulls in an array of integers from the database, and then converts those to names using a mapped option list. It is working beautifully, but when I search the table, it does not search the tags. This is problematic, because that is one of the primary function of what tags are supposed to do. What am I missing here?

This is the database that is creating the table (the column in question)
Database Entries

This is the database that has the mapped values:
OptionListDatabaseEntries

This is what the table looks like
Displayed Tags

Here are the settings for my mapped option list

In this image you can see that I searched for Sarah, and no entries with Sarah as the Mission Coordinator show up. If you refer back to the image of what the table looks like, you can see at least one trip with Sarah as the Mission Coordinator.

2 Likes

Hey @AIM_Rhys and welcome to the forum!

So the search term is actually searching through you values, and it is excluding your tag labels, that's why it is not finding your tags.

A workaround would be to create another column, hide it, and set your value as item.name. The search term/bar will actually look through values in hidden rows.

Miguel,

Thanks for the response. I think this is a great solution, but I can't get it to work. I've tried a variety of solutions, but I can only get the value to display the ID numbers, not the actual names. Can you please elaborate on how I would make this work?

1 Like

Sorry, I meant to say that I tried multiple variations of this solution.

1 Like

Hey @AIM_Rhys,

So, you can add a custom column clicking in the "+" button:

image

You then select the data source using the same data source and select the type as tag.

What will change, is that in the mapped options, instead of adding {{ item.id }} within value, you will add {{ item.name }}, and also within the label.

then you will just hide this column which will be used only for the purposes of the search component to be able to find whatever is in the value of the tag

I'm sorry, I think I'm still not understading what you mean. I have set up the new columns options as follows:

This solution results in no data displaying in the column, because now the mapped field is trying to match the id from the source to the name column in the mapped values database, and there are no matching entries in the mapped values database. If I check the allow custom values box, then I get the source ID displayed in the table, and I still can't search by name.

1 Like

I have a workaround that I will share for anyone else struggling with this. It's not ideal, but it's functional for now. Someday I hope that this will be resolved so that tags are searchable by their labels, not their values.

I used Miguels idea to create a hidden column, with the same data, but instead of a tags column I made it a string. So it just displays the raw ID values. Then, I added a drop down box at the top of the table, using the mapped values database as the mapped source for the drop down.

Then I added a filter rule to the table to filter on that drop down. Now the dropdown box contains all of the names that I need, and will update automatically if staff are added or removed, and when I use that drop down to select a name, it shows me only rows with that person tagged.

A consequence of doing it this way, which could be positive or negative depending on your use case, is that it can only filter based on one column, so if you have names in more than one column, you have to choose which one is your source. Not ideal in my situation, but potentially preferable for others.

I am NOT marking this as solution because it is only a workaround, and I would still like to resolve the issue of tags not being searchable.

3 Likes

Hey @AIM_Rhys,

Apologies for the incomplete answer, it was late and I wanted to reply something before logging off.

One step missing for the mapped options to work is indeed mapping your ids to the values.

So in the column settings, within "mapped value" you can add a line like this one:

{{ 
  item.map(coordinator_id => {
    const staff = ImportMission.data.find(item => item.id ===coordinator_id);
    return staff ? staff.name : null;
  }).filter(name => name !== null)
}}

Be sure to replace ImportMission with the actual name of your query whre names are mapped .

This will basically replace the id items in each row, with the names for each corresponding item. With this. you can now mapped them using {{ item.name}} or indeed just leave it as a string as it will be hidden anyway.

Hope this helps!

I had the same issue but went for a different solution where you don't need an addition column, and it also works for server-side pagination etc. However it will be a bit harder to implement for many operators since you have to apply the operator in you own code.

I won't post all code now, but the procedure is this:

  • Use a Filter-component (or some other component) to generate a filterStack, just like usual.
  • Before setting the filterStack on theTable we want to edit it and replace any label (e.g. "Bike") with its id/value (e.g. 123). We can get all info needed to make this replacement from theTable.filterStack, theTable._columnKey and theTable._columnOptionList (valueByIndex and labelByIndex)
  • When we have replace labels with values in our filterStack we can just apply it to theTable to set the filtering properly.
1 Like

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:
Recording 2024-10-31 114000

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();
}


2 Likes

@Lars_Fredholm
Thank you so much for the detailed explanation of your solution. I'm going to stick with my workaround with the select box at the top of the page for now, since it is working.

If I'm going to invest more time in an alternate solution, I want it to be a solution that allows for using the search box to search the entire table.

I'm sure someone in the future will find this post and be grateful for your solution!

1 Like

@MiguelOrtiz
Thank you for being patient and following through on this.

When I use your script, ReTool gives me an error that item.map is not a function.

Hi @AIM_Rhys,

Hopefully we will get there!

Ok, so from your first post, I saw your table's column has a json format with an object and a list of integers within it, e.g. [2,6]. Is your query in the app pulling it as a string or as an object?

I've assumed it is as as you have been able to set up a tags column properly (as from your first post's screenshot as well)

The error "item.map is not a function" is most likely because your source relationship_manager_v2 is not an array. So when you click on your table's data source state, you should be seeing it like this:

image

This is the info for items in a row. What the script will do is create a new array, by taking all of those integers in the object, searching for those ids in your other query, i.e. ImportMissionCoordinatorTags.data and find the value you're searching, in this case "name". As such, [1,2,3,] will become [John,Mary,George].

Because the find method won't work if for one of your integers there is no match in your importmissioncoordinatortags data, we have added fallbacks to null.

Hope this helps!

It is pulling it as an array I believe. I have to use {{ JSON.parse(item) }} in the mapped value of the tags field in order for it to map them correctly.

So, I updated your code to look like this:

{{ 
  JSON.parse(item).map(coordinator_id => {
    const staff = ImportMissionCoordiantorTags.data.find(item => item.id === coordinator_id);
    return staff ? staff.name : null;
  }).filter(name => name !== null)
}}

And now it is telling me TypeError: ImportMissionCoordiantorTags.data.find is not a function which is true. It is a SQL query, not a function.

Ah, too bad I hadn't realized the same issue is (of course) also occuring when "Sorting" a tag-column :man_facepalming:
To solve that we probably need additional hidden columns with the actual labels to begin with (i.e. in line with @MiguelOrtiz proposal). And when the user sets a sorting on theColumnWithIds we hijack the sortArray, then modify it to be applied on theColumnWithLabels, set it back again.

1 Like

Hey @AIM_Rhys,

Try transforming your ImportMissionCoordiantorTags.data to an Array. You can do it directly in your query, in the transform results section by writing return formatDataAsArray (data).

Alternatively, you can do it directly in the script

formatDataAsArray(ImportMissionCoordiantorTags.data)

Miguel,

Here is my full updated code:

{{ 
  JSON.parse(item).map(coordinator_id => {
    const staff = formatDataAsArray(ImportMissionCoordiantorTags.data).find(item => item.id === coordinator_id);
    return staff ? staff.name : null;
  }).filter(name => name !== null)
}}

I'm not getting any errors with this, but everything is resolving as null

I also tried creating a sql statement that would only return the name associated with the id that was passed to it, but I couldn't get that to work either.

Hey @AIM_Rhys

could it be the n in Name needs to be capitalized?

{{ 
  JSON.parse(item).map(coordinator_id => {
    const staff = formatDataAsArray(ImportMissionCoordiantorTags.data).find(item => item.id === coordinator_id);
    return staff ? staff.Name : null;
  }).filter(Name => Name !== null)
}}
1 Like

@MiguelOrtiz
That works! Thank you so much for all of your assistance.

A step by step for anyone else trying to accomplish this that comes across this thread.

Assuming that your column has properly formatted JSON with id's that correlate to the actual data.

Step 1:
Create a new column with the same source column as your JSON Tags column

Step 2:
Set the new column type to String

Step 3:
Use this code in the mapped value field:

{{ 
  JSON.parse(item).map(coordinator_id => {
    const staff = formatDataAsArray(ImportMissionCoordiantorTags.data).find(item => item.id === coordinator_id);
    return staff ? staff.Name : null;
  }).filter(Name => Name !== null)
}}

Notes about this code:
ImportMissionCoordinatorTags is a SQL script that returns a JSON with fields id and Name. You can adjust the formatDataAsArray(ImportMissionCoordiantorTags.data).find(item => item.id === coordinator_id) portion of the code with your of mapped values, wherever those come from.

1 Like

Hey @AIM_Rhys,

I'm glad we finally got there! Thanks for sharing the step by step.