Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(filters): add new IN_COLLECTION operator to allow searching cell value as Array #282

Merged
merged 6 commits into from
Mar 18, 2021
Merged

feat(filters): add new IN_COLLECTION operator to allow searching cell value as Array #282

merged 6 commits into from
Mar 18, 2021

Conversation

ArnaudVanHerck
Copy link
Contributor

We are having issues with our implementation of SlickGrid (through Aurelia-SlickGrid) where the search terms sometimes can contain whitespaces. When the testFilterCondition function runs with the operator 'inContains' the terms get split on whitespaces. This issue got solved for us when using camelCasing or kebabCasing because this removes whitespaces from the search terms and cellValue before sending them to the testFilterCondition function. Because the 'toKebabCase' function needs a string and we know that multiple-select.js also provides a string, a toString() was added to the searchTerm without the need of worrying about possible type conficts (as far as my understanding of the code).

@codecov
Copy link

codecov bot commented Mar 15, 2021

Codecov Report

Merging #282 (277c8b6) into master (e44145d) will not change coverage.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff            @@
##            master      #282   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          209       209           
  Lines        12433     12482   +49     
  Branches      4252      4277   +25     
=========================================
+ Hits         12433     12482   +49     
Impacted Files Coverage Δ
packages/common/src/enums/operatorType.enum.ts 100.00% <100.00%> (ø)
...lter-conditions/collectionSearchFilterCondition.ts 100.00% <100.00%> (ø)
...es/common/src/filter-conditions/filterUtilities.ts 100.00% <100.00%> (ø)
packages/common/src/editors/textEditor.ts 100.00% <0.00%> (ø)
packages/common/src/services/utilities.ts 100.00% <0.00%> (ø)
packages/common/src/editors/floatEditor.ts 100.00% <0.00%> (ø)
packages/common/src/filters/inputFilter.ts 100.00% <0.00%> (ø)
packages/common/src/editors/integerEditor.ts 100.00% <0.00%> (ø)
packages/common/src/services/sort.service.ts 100.00% <0.00%> (ø)
packages/common/src/editors/longTextEditor.ts 100.00% <0.00%> (ø)
... and 5 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e44145d...277c8b6. Read the comment docs.

@ghiscoding
Copy link
Owner

ghiscoding commented Mar 15, 2021

hmm I'm not sure I like this approach, it seems the goal of this is simply to remove white spaces, why don't we fix the source instead? Because if I look at your code change, I'm quite sure this will have a perf impact and I'm not willing to add that for that reason. Where do you see that it gets split by white space? Can't we just add a trim somewhere? I find you're adding lot of complexity for something that should be simple and will hit perf.

@ArnaudVanHerck
Copy link
Contributor Author

ArnaudVanHerck commented Mar 15, 2021

Yes, I understand your concern about the performance hit. I just assumed you'd prefer me reusing existing functions. 😃

From my understanding the problem occurs when using a multiselect filter on an column that contains an array of strings, each string can contain white spaces. The array of selected options seem to get whitespace separated. When these values get send to the testFilterCondition function the 'inContains' operator splits the search string coming from the multiselect filter on white spaces. This is where our issue lies. Our search string, e.g. Group ABC, gets parted in Group and ABC. This makes that it will never get a hit on the value. Because the testFilterCondition function seems to search for Group ABC in the strings Group and ABC.

@ghiscoding
Copy link
Owner

ghiscoding commented Mar 15, 2021

Are you using the IN or the IN_CONTAINS operator? Because I did a change recently (PR #262) on that latter operator because it wasn't working as it should have.

case 'IN_CONTAINS':
if (value2 && Array.isArray(value2) && typeof value1 === 'string') {
return value2.some(item => value1.split(/[\s,]+/).includes(item));
}
return false;

The IN_CONTAINS does a split on comma and it expects the search terms to have comma but typically the multiple-select.js uses the IN or NOT_IN operators. I rarely ever use this IN_CONTAINS operator, the only time I used it in all of the demos was on this Example 25 and I don't think I ever used it with the multiple-select

What are you using that with? I mean is that a local grid (JSON dataset) or is it with a Backend Service (OData/GraphQL)? It's not provided in your issue, and perhaps show a print screen of the issue. It would be useful to know how to reproduce so that we could maybe add that to one of the demo to test it out.

image

@ArnaudVanHerck
Copy link
Contributor Author

ArnaudVanHerck commented Mar 16, 2021

We're using SlickGrid with json (local grid) and we're using the IN operator on columns where the cells only contain a string and we're using the IN_CONTAINS operator on columns where the cells contain an array of strings.
When I test the code you quoted with a simple string with white space, the string gets split on the white space.
E.g. : 'Group ABC'.split(/[\s,]+/) becomes an array containing ['Group', 'ABC'] which is our issue right now.
I see now that the filter items from multi-select.js seem to come as an array of strings and that the IN_CONTAINS operator splits the cellValue not the select options (my bad, understood that part wrong).
So it seems that the issue is the REGEX used in the split. Because the cellValue from the array gets comma separated it shouldn't split on whitespaces. When I changes the REGEX to '.split(/[,]+/)' it seems to work as expected (which it seems you changed it to in PR #262 but that's not how it looks like in master right now).

I'll try to add screenshots later if you want. 😄

@ghiscoding
Copy link
Owner

ghiscoding commented Mar 16, 2021

hmm it seems that the reason you are using the IN_CONTAINS is not why I created this Operator, I mainly created it to do a split on comma (and/or spaces in some cases) on an input text filter, but in your use case you just want it to compare against an array. I'd be ok with removing the space from the regex but this will not be bullet proof because in your use case if your string as a comma then you'll be back to square one and have problems again because it will split again... so I think a better approach would be to simply add a new Operator, I'm thinking of IN_COLLECTION and that would be the same code as the IN_CONTAINS but without the split because your cell is already an array (and by looking at the code, we should also add NOT_IN_COLLECTION operator).
Something like this

case 'IN_CONTAINS':
      if (value2 && Array.isArray(value2) && typeof value1 === 'string') {
        return value2.some(item => value1.split(/[\s,]+/).includes(item));
      }
      return false;
case 'IN_COLLECTION':
      if (value1 && value2 && Array.isArray(value1) && Array.isArray(value2)) {
        return value2.some(item => value1.includes(item));
      }
      return false;

When I changes the REGEX to '.split(/[,]+/)' it seems to work as expected (which it seems you changed it to in PR #262 but that's not how it looks like in master right now).

I'm not sure what you mean here? The code in that PR for that switch case is what is in master right now and it was shipped in last version 0.11.x. If you don't have that code on your side it might be because you didn't update to the latest version? You can see what was shipped on the releases page or in the changelog

@ArnaudVanHerck
Copy link
Contributor Author

An new IN_COLLECTION operator could work. But then the cell value shouldn't be combined to one string as it does today in the executeCollectionSearchFilterCondition file. Do we need a new search filter condition then maybe too? Or are we adding a check to cell value to see if it's an array or not before sending it to the test function?

What I meant is that the commit you're referring to in PR #262 doesn't split on whitespaces just comma but that got added in PR #267 😄

@ghiscoding
Copy link
Owner

ghiscoding commented Mar 16, 2021

Oh I didn't know it was using a string every time, do you mean this line (on the last part)?

const cellValue = (options.cellValue === undefined || options.cellValue === null) ? '' : `${options.cellValue}`;

I think I made it a string mainly to convert numbers to string, we probably need to find a better way so that it works with an array of string. Maybe just casting as a string when it's a number should be sufficient

const cellValue = (options.cellValue === undefined || options.cellValue === null) ? '' : typeof options.cellValue === 'number' ? `${options.cellValue}` : options.cellValue; 

What I meant is that the commit you're referring to in PR #262 doesn't split on whitespaces just comma but that got added in PR #267

Thanks for the find and so I would rather keep that then because we use the Composite Editor extensively in our project and I don't want to create new bugs by changing the regex. So I think the IN_COLLECTION and NOT_IN_COLLECTION is probably the best way to go.

Would you like to contribute and test it out with your code? If so, you'll have to start adding both Operators in the following 2 files operatorType.enum.ts and operatorString.type.ts and then modify the other files which you know about.

Also, if you don't know how to push this code change to your project and test it out, you can simply run the build in this project and copy the build package dist and replace it in your node_modules, I can provide more info if you need, and that is basically the steps I now do in testing Aurelia-Slickgrid before making an official version (or you can also npm links but I never tried myself, I should probably give it a try though)

@ArnaudVanHerck
Copy link
Contributor Author

Yes, that's the code that makes it a combined string. I'm just wondering how other types get affected by the change in this string conversation you're proposing.
const cellValue = (options.cellValue === undefined || options.cellValue === null) ? '' : typeof options.cellValue === 'number' ? `${options.cellValue}` : options.cellValue;
In case we'd be looking for e.g. a boolean, would it need to be parsed to a string too?. Maybe some extra testing should done to make sure nothing else stops working?

Thank you for pointing me in the right direction, I'll try the proposed changes our project and update the PR if everything works as expected. 👍

@ghiscoding
Copy link
Owner

ghiscoding commented Mar 16, 2021

ah yeah I forgot about the boolean values, it's probably better to include them as well because the multiple-select.js only works with string if I recall correctly and also when you type a character on a regular text input filter it will always be a string and I still want number/boolean to work and that is why that string casting is there.

Maybe we can simply go the inverse and only check for arrays

const cellValue = (options.cellValue === undefined || options.cellValue === null) 
  ? '' 
  : Array.isArray(options.cellValue) ? options.cellValue : `${options.cellValue}`;

You might need to put it all in 1 line though because I think ESLint will complain if it's not

It's a good discussion, I'm always happy to have contributions 😉

@ArnaudVanHerck
Copy link
Contributor Author

I updated the PR now, and did the following changes:

  • Added the InCollection and NotInCollection operator types
  • Added these types to the isCollectionOperator function
  • Changed the executeCollectionSearchFilterCondition function so that it doesn't make a string of the array but of the items in the array
  • Added tests to check if the executeCollectionSearchFilterCondition function behaves as expected with types other than string.
  • Added tests to check if the testFilterCondition behaves as expected

@ghiscoding
Copy link
Owner

ghiscoding commented Mar 17, 2021

Wow that is pretty good, I got nothing to say on the PR, it's perfect 😄
Have you had a chance to try it in your own project?

Were you done with the PR, I would be ready to merge as is.

EDIT

Actually the Example 7 Cypress E2E tests keeps failing on the last column "Prerequisites" which is a multiple-select dropdown. I'm not sure what makes it fail though, it could be the Array.isArray(options.cellValue) or the switch/case. I could take a look later if I get a chance to clone your PR, you can also try it yourself by running npm run cypress and run the Example 7 test

@ArnaudVanHerck
Copy link
Contributor Author

Yes, I saw that 😢
I'm trying to get cypress to work but it's fighting me a bit 😉

@ghiscoding ghiscoding changed the title Fix/add kebabCasing to cellValue and searchTerms in executeCollectionSearchFilterCondition feat(filters): add new IN_COLLECTION operator to allow searching cell value as Array Mar 17, 2021
@ghiscoding
Copy link
Owner

I'm not sure why Cypress dashboard doesn't me those tests but if I look at the GitHub Actions, the failing test is the following (which is the last test on

1) should open the "Prerequisites" Filter then choose "Task 3", "Task 4" and "Task 8" from the list and expect to see 2 rows of data in the grid

  30 passing (33s)
  1 failing

The Cypress test code has the following steps, I suggest to try the steps manually on that Example 7

it('should open the "Prerequisites" Filter then choose "Task 3", "Task 4" and "Task 8" from the list and expect to see 2 rows of data in the grid', () => {
cy.get('div.ms-filter.filter-prerequisites')
.trigger('click');
cy.get('.ms-drop')
.contains(/^Task 3$/) // use regexp to avoid finding first Task 3 which is in fact Task 399
.click();
cy.get('.ms-drop')
.contains(/^Task 4$/)
.click();
cy.get('.ms-drop')
.contains(/^Task 8$/)
.click();
cy.get('.ms-ok-button')
.click();
cy.get('.slick-row')
.should('have.length', 2);
cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(2)`).should('contain', 'Task 4');
cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(2)`).should('contain', 'Task 8');
});

@ArnaudVanHerck
Copy link
Contributor Author

Yes, I'm experiencing a similar issue, I cannot seem to run the cypress tests locally.
Do you have the possibility to test the steps manually? I'm not sure how to get that part working either 😕

@ghiscoding
Copy link
Owner

hmm I sent you the code for the Cypress tests, I thought it was readable but the title of the test should tell you what to do in a general way

it('should open the "Prerequisites" Filter then choose "Task 3", "Task 4" and "Task 8" from the list and expect to see 2 rows of data in the grid')

Note that a few months back, we can no longer open Cypress directly from VSCode, it throws some kind of memory heap size error of some kind. It was reported on VSCode but they never really addressed it, if that is what you're doing then simply open a separate terminal and run cypress from that terminal (outside of VSCode) and that is how I got it to work.

@ArnaudVanHerck ArnaudVanHerck deleted the fix/add-kebabcase-to-collectionsearchfiltercondition branch March 17, 2021 21:24
@ArnaudVanHerck ArnaudVanHerck restored the fix/add-kebabcase-to-collectionsearchfiltercondition branch March 17, 2021 21:28
@ghiscoding
Copy link
Owner

@ArnaudVanHerck I saw you closed/reopened the PR (maybe to trigger another test) and it still fails on the exact same Cypress test, not sure what it is yet but I'll definitely take a look at it a bit later today. At least what we know so far is that there's really something on that test and it's not a flaky test because it's always the same one that fails.... to be continued ;)

@ArnaudVanHerck
Copy link
Contributor Author

@ghiscoding I tried to rename the branch in order for it to reflect the PR better but apparently that closed the PR 😋
I finally got the project running locally (I got stuck after apparently running yarn run bootstrap multiple times). I figured out what was the issues with the test and added a check on the operator when setting cellValue to a string. I'm running the tests now to see if everything still works as expected.

@@ -7,7 +7,7 @@ import { testFilterCondition } from './filterUtilities';
*/
export const executeCollectionSearchFilterCondition: FilterCondition = (options: FilterConditionOption) => {
// multiple-select will always return text, so we should make our cell values text as well
const cellValue = (options.cellValue === undefined || options.cellValue === null) ? '' : `${options.cellValue}`;
const cellValue = (options.cellValue === undefined || options.cellValue === null) ? '' : ((options.operator === 'IN_COLLECTION' || options.operator === 'NOT_IN_COLLECTION') && Array.isArray(options.cellValue)) ? options.cellValue.map(value => `${value}`) : `${options.cellValue}`;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this line is starting to be way too lengthy, could we split this into multiple lines to increase readability. Actually, I rewrote the entire function and the unit tests are still good, could you please use the code below, I find it more readable. Thanks

export const executeCollectionSearchFilterCondition: FilterCondition = (options: FilterConditionOption) => {
  // multiple-select will always return text, so we should make our cell values text as well
  const filterOperator = options.operator;
  let cellValue: string | string[];
  if (Array.isArray(options.cellValue) && (filterOperator === 'IN_COLLECTION' || filterOperator === 'NOT_IN_COLLECTION')) {
    cellValue = options.cellValue.map(value => `${value}`);
  } else {
    cellValue = (options.cellValue === undefined || options.cellValue === null) ? '' : `${options.cellValue}`;
  }

  return testFilterCondition(filterOperator || 'IN', cellValue, options.searchTerms || []);
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, yes that line got quite long now. I'm happy to change it, I'll update it soon 👍

@ghiscoding
Copy link
Owner

Alright, so I ran all the unit tests and I also wanted to test it out in Aurelia-Slickgrid before approving this PR and all the Aurelia-Slickgrid Cypress tests are passing (all 400 tests), so I'll be happy to merge after you do the code change I asked in previous comment, the rest is all great, nice PR. Thanks

@ArnaudVanHerck
Copy link
Contributor Author

I added a nullcheck to the array given that the cellValue got typed as string or string[]. Otherwise it feels like, in case of an empty array, it would return null and not an empty array.

export const executeCollectionSearchFilterCondition: FilterCondition = (options: FilterConditionOption) => {
  // multiple-select will always return text, so we should make our cell values text as well
  const filterOperator = options.operator;
  let cellValue: string | string[];
  if (Array.isArray(options.cellValue) && (filterOperator === 'IN_COLLECTION' || filterOperator === 'NOT_IN_COLLECTION')) {
    cellValue = (!!options.cellValue.length ? options.cellValue.map(value => `${value}`) : []);
  } else {
    cellValue = (options.cellValue === undefined || options.cellValue === null) ? '' : `${options.cellValue}`;
  }

  return testFilterCondition(filterOperator || 'IN', cellValue, options.searchTerms || []);
};

What do you think?

@ghiscoding
Copy link
Owner

sure that'd be ok

@ghiscoding ghiscoding merged commit ecce93c into ghiscoding:master Mar 18, 2021
@ghiscoding
Copy link
Owner

Thanks for the great contribution, good PR, good feedback, all good 😄
Expect a release by end of this week or early next week 🚀

@ArnaudVanHerck
Copy link
Contributor Author

Thank you for the feedback and your patience. And thank you for you hard work on these packages 🥇

@ArnaudVanHerck ArnaudVanHerck deleted the fix/add-kebabcase-to-collectionsearchfiltercondition branch March 19, 2021 07:56
@ghiscoding
Copy link
Owner

@ArnaudVanHerck
This is now live with a few other things, see the new Slickgrid-Universal changelog and the new Aurelia-Slickgrid version Release 3.5.0

Cheers ⭐ and thanks again for the contribution

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants