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

NEW Handle WithinRangeFilter in searchable_fields and SearchContext #11602

Merged

Conversation

GuySartorelli
Copy link
Member

@GuySartorelli GuySartorelli commented Feb 11, 2025

Important

This PR has multiple commits. Do not squash - or if you do, use the second commit's message

This PR enables using WithinRangeFilter to filter by numeric, date, and time fields in searchable_fields. It can also be used with other field types (see the varchar unit test scenario) if some additional config is provided.

This PR is required so that the current SiteTree filter on LastEdited can be performed using a SearchContext instead of using the current custom weird filter it uses.

Note that this PR does not include any updates to the css e.g. to make date fields look nicer - that will come in silverstripe/silverstripe-admin#1897. For now we're adding just the functionality itself.

See the docs or unit tests for configuration that you can use to test locally.

Issue

@GuySartorelli GuySartorelli marked this pull request as draft February 11, 2025 04:24
@GuySartorelli GuySartorelli force-pushed the pulls/6.0/withinrangefilter branch 2 times, most recently from aa9c34f to c32919f Compare February 11, 2025 04:29
@GuySartorelli GuySartorelli force-pushed the pulls/6.0/withinrangefilter branch 5 times, most recently from 4fa4ff0 to b81437c Compare February 11, 2025 23:51
Comment on lines -14 to -27
/**
* The minimum value for a signed 64-bit integer.
* Defined as string instead of int otherwise will end up as a float
* on 64-bit systems
*
* When this is cast to an int in IntFieldValidator::__construct()
* it will be properly cast to an int
*/
protected const MIN_INT = '-9223372036854775808';

/**
* The maximum value for a signed 64-bit integer.
*/
protected const MAX_INT = '9223372036854775807';
Copy link
Member Author

Choose a reason for hiding this comment

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

These are moved to DBBigInt so they can be used with the new getMinValue() and getMaxValue().

Comment on lines -12 to -22
/**
* The minimum value for a signed 32-bit integer.
* Defined as string instead of int because be cast to a float
* on 32-bit systems if defined as an int
*/
protected const MIN_INT = '-2147483648';

/**
* The maximum value for a signed 32-bit integer.
*/
protected const MAX_INT = '2147483647';
Copy link
Member Author

Choose a reason for hiding this comment

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

These are moved to DBInt so they can be used with the new getMinValue() and getMaxValue().

));
}
}
$field = $this->scaffoldSingleSearchField($fieldName, $spec, $params);
Copy link
Member Author

Choose a reason for hiding this comment

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

This was a lot of code to parse through just to see that it's all for this singular purpose. Made sense to abstract into its own method here to encapsulate this functionality, reducing cognitive load for anyone working in this area in the future.

Comment on lines -3906 to +3944
} elseif (is_array($specOrName) && ($relObject = $this->relObject($identifier))) {
} elseif (is_array($specOrName)) {
Copy link
Member Author

Choose a reason for hiding this comment

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

The condition would cause the following problem:

You have config like this:

[
    'MyRelation' => [
        'filter' => 'ExactMatch',
    ],
]

Because this is an array, but the relObject returns null, this condition isn't met. So we go to the else clause. This turns the config into this:

[
    'MyRelation' => [
        'filter' => [
            'filter' => 'ExactMatch',
        ],
    ],
]

Comment on lines +3949 to +3958
'filter' => $relObject?->config()->get('default_search_filter_class'),
'dataType' => $relObject ? get_class($relObject) : null,
Copy link
Member Author

Choose a reason for hiding this comment

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

If there's no relObject, these will just default to null. If the $specOrName array doesn't fill them in, they'll get filled in further down the line.

}

#[DataProvider('provideScaffoldSearchFields')]
public function testScaffoldSearchFields(array $config, string $generalSearchFieldName, array $expected): void
Copy link
Member Author

Choose a reason for hiding this comment

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

We had no test coverage for DataObject::scaffoldSearchFields()! So I've done reasonably comprehensive test scenarios here.

Comment on lines +24 to +33
public function searchableFields()
{
// Explicitly only include this custom field
return [
'WhatIsThis' => [
'field' => TextField::class,
'title' => $this->fieldLabel('WhatIsThis'),
],
];
}
Copy link
Member Author

@GuySartorelli GuySartorelli Feb 11, 2025

Choose a reason for hiding this comment

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

Needed because you can't have a non-db field in searchable_fields but you can have one in this method directly. See #11602 (comment)

Comment on lines -219 to -228
// test that you can filterBy if searchable_fields even if it is not a legit field
// this is because we're making a blind assumption it will be filterable later in a SearchContext
Config::modify()->set(Team::class, 'searchable_fields', ['WhatIsThis']);
$this->assertTrue($filterHeader->canFilterAnyColumns($gridField));
Copy link
Member Author

@GuySartorelli GuySartorelli Feb 11, 2025

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Need these getters for the match_any in search context.

@@ -3896,28 +3933,40 @@ public function searchableFields()
$rewrite = [];
foreach ($fields as $name => $specOrName) {
$identifier = (is_int($name)) ? $specOrName : $name;
$relObject = isset($specOrName['match_any']) ? null : $this->relObject($identifier);
Copy link
Member Author

Choose a reason for hiding this comment

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

This was only being called if the spec is an array - but that means if you're trying to search against a field that isn't valid, you only get an exception if you're using complex config! Otherwise you have to filter to get the exception, which isn't very friendly.

Note that it is possible for this to return null

@GuySartorelli GuySartorelli marked this pull request as ready for review February 12, 2025 00:10
Copy link
Member

@emteknetnz emteknetnz left a comment

Choose a reason for hiding this comment

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

I followed the example in the developer-docs PR and end up with this somewhat suboptimal UX - the validation errors showed on blur after making no changes to the fields, just clicking between them

I had a "MyDataObject" in a "MyModelAdmin", with things auto-scaffolded

image

I only had this PR installed - am I missing something?

@GuySartorelli
Copy link
Member Author

GuySartorelli commented Feb 12, 2025

Was the front-end errors on blur the only thing you encountered? I suspect you'll find that exact same thing happens with that field type when any other filter is used even without this PR (i.e. it's a pre-existing problem). I haven't touched JS for this PR.

It doesn't prevent you submitting the form with or without values in those fields.

@emteknetnz
Copy link
Member

emteknetnz commented Feb 12, 2025

OK, looks like it's CurrencyField that has the onblur issue, also does it with other filters e.g. GreaterThanFilter, there may be others where their default value is considered invalid. Could you please raise a bug issue for this? You probably have more context than me about why it's breaking like it is

I also tried the WithinRangeFilter on an Int field and that mostly worked, there were no onblur validation issues, and the gridfield did filter, however after filtering and opening up the filter again, then values I had put in were no longer there.

@GuySartorelli
Copy link
Member Author

GuySartorelli commented Feb 12, 2025

Could you please raise a bug issue for this?

Done (silverstripe/silverstripe-admin#1899) - though I haven't looked at all into a) what causes that or b) what the desired UX should be.

I also tried the WithinRangeFilter on an Int field and that mostly worked, there were no onblur validation issues, and the gridfield did filter, however after filtering and opening up the filter again, then values I had put in were no longer there.

Ahh yup I missed a fix that was originally included in my larger set of PRs. Raised #11609 since this is a bug that exists in CMS 5. Will need to be merged, then merged up, and I'll rebase on top.

This will be used with handling WithinRangeFilter in searchable fields
so that if only one of the two ends of a range is filled in, the other
can be automatically populated.
@GuySartorelli GuySartorelli force-pushed the pulls/6.0/withinrangefilter branch from b81437c to 0c31c3f Compare February 13, 2025 03:30
@GuySartorelli
Copy link
Member Author

CI failures are being fixed in #11615 and aren't related to this PR

Copy link
Member

@emteknetnz emteknetnz left a comment

Choose a reason for hiding this comment

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

Tested locally, work good with Date and Int data types

With Currency, which I is what is listed in as an example in the docs, seems very broken e.g. validates default values as invalid, clear button doesn't set to a valid value, the actual filtering doesn't seem to work. I'm not sure how much of that is an existing issue, or how much of it relates to this PR. Could you have a look and let me know if it should be fixed here or in another issue

@GuySartorelli
Copy link
Member Author

the actual filtering doesn't seem to work

Can you please be more specific about that? That sounds like something I need to fix in this PR but I can't without details about what you're doing, what you're expecting, and what's actually happening.

@emteknetnz
Copy link
Member

OK it's specific to this PR with the WithinRangeFilter, not prior to this PR with the GreatThanFilter

Filtering a currency value between two currency values just doesn't work i.e. no results returned

<?php

use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Filters\WithinRangeFilter;

class MyDataObject extends DataObject
{
    private static $table_name = 'MyDataObject';

    private static $db = [
        'Title' => 'Varchar',
        'MyCurrency' => 'Currency',
    ];

    private static array $searchable_fields = [
        'MyCurrency' => [
            'filter' => WithinRangeFilter::class,
        ],
    ];
}

And create ModelAdmin to manage the model

@GuySartorelli
Copy link
Member Author

GuySartorelli commented Feb 17, 2025

Seems to work fine for me?

Expand to see the exact PHP I used for the model and admin
<?php

use SilverStripe\Admin\ModelAdmin;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Filters\WithinRangeFilter;

class MyDataObject extends DataObject
{
    private static $table_name = 'MyDataObject';

    private static $db = [
        'Title' => 'Varchar',
        'MyCurrency' => 'Currency',
    ];

    private static array $summary_fields = [
        'Title',
        'MyCurrency',
    ];

    private static array $searchable_fields = [
        'MyCurrency' => [
            'filter' => WithinRangeFilter::class,
        ],
    ];
}

class MyModelAdmin extends ModelAdmin
{
    private static string $url_segment = 'my-admin';

    private static string $menu_title = 'my admin';

    private static array $managed_models = [
        MyDataObject::class,
    ];
}
2025-02-17.15-56-07.mp4

What sort of values are you trying to add? Are you adding the $ sign before the values, or something like that?

@emteknetnz
Copy link
Member

Ah right, it'll work if I remove the leading $ from the filter values. This is very confusing as the filter values start off with leading $

@GuySartorelli
Copy link
Member Author

Turns out the values weren't being passed through the form first, which meant form fields like CurrencyField didn't get a chance to update the value - so the DB query was trying to check if $1.23 matched 1.23 and uhhhh no it doesn't lol.

Added a new commit to pass values through the form first, with a fallback to the original filter value if somehow there's no form field (which shouldn't ever happen but would have been the previous behaviour anyway)

Copy link
Member

@emteknetnz emteknetnz left a comment

Choose a reason for hiding this comment

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

It's better now, though if I leave the filter values unchanged i.e. $0.00 and $0.00, then it will filter out values. When the values are left in their default state they should do any filtering

Have talked offline, best solution is probably to swap out CurrencyField for TextField in the method that scaffolds search fields. Have a look at other fields that should probably also have this to prevent formatting or validation that prevents partial searches e.g. MoneyField, EmailField, UrlField, IpField, etc

@GuySartorelli
Copy link
Member Author

GuySartorelli commented Feb 17, 2025

swap out CurrencyField for TextField

I've gone for NumericField since the value should never be e.g. "seven dollars" but instead 7.00

Updated DBEmail and DBUrl to use TextField. DBIp already uses that field.

I've left enums alone - they have concrete values, so a dropdown makes sense for them in general (and withinrange probably doesn't make sense for them so they're not really in scope anyway)

To use DBMoney you'd have to do something like this to use it (at which point you're not actually dealing with MoneyField directly anyway):

<?php

use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Filters\WithinRangeFilter;

class MyDataObject extends DataObject
{
    private static $table_name = 'MyDataObject';

    private static $db = [
        'Title' => 'Varchar',
        'MyMoney' => 'Money',
    ];

    private static array $summary_fields = [
        'Title',
        'MyMoney',
    ];

    private static array $searchable_fields = [
        'MyMoney.Currency',
        'MyMoney.Amount' => [
            'filter' => WithinRangeFilter::class,
        ],
    ];
}

And that only works for building the form - the default search context can't handle that syntax because it expects . syntax to be used with relations, not composite fields. So tl;dr handling DBMoney isn't in scope for this and has its own pre-existing challenges in relation to filtering that go waaay beyond the scope of this card which is dealing with within range filter.

@GuySartorelli GuySartorelli force-pushed the pulls/6.0/withinrangefilter branch from b61def9 to 95097c9 Compare February 17, 2025 23:56
This is especially helpful with date ranges, e.g. all records edited
within a certain range, but can be used out of the box with basically
any numeric, date, or time-based fields.
Some form fields such as CurrencyField manipulate the value to give more
pure values, e.g. removing a currency symbol before the actual value.
@GuySartorelli GuySartorelli force-pushed the pulls/6.0/withinrangefilter branch from 95097c9 to d047b6c Compare February 18, 2025 00:09
@emteknetnz emteknetnz merged commit 4972b0d into silverstripe:6.0 Feb 18, 2025
12 checks passed
@emteknetnz emteknetnz deleted the pulls/6.0/withinrangefilter branch February 18, 2025 01:36
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.

2 participants