Move options from Settings to localStorage

This commit is contained in:
buchmann 2026-06-26 11:54:45 +02:00
parent 3702c079b2
commit 2a1c985715
7 changed files with 63 additions and 112 deletions

View file

@ -37,7 +37,6 @@ use App\Form\Filters\PartFilterType;
use App\Services\Parts\PartsTableActionHandler;
use App\Services\Trees\NodesListBuilder;
use App\Settings\BehaviorSettings\SidebarSettings;
use App\Settings\BehaviorSettings\SearchSettings;
use App\Settings\BehaviorSettings\TableSettings;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\ORM\EntityManagerInterface;
@ -60,7 +59,6 @@ class PartListsController extends AbstractController
private readonly TranslatorInterface $translator,
private readonly TableSettings $tableSettings,
private readonly SidebarSettings $sidebarSettings,
private readonly SearchSettings $searchSettings,
)
{
}
@ -317,7 +315,7 @@ class PartListsController extends AbstractController
private function searchRequestToFilter(Request $request): PartSearchFilter
{
$filter = new PartSearchFilter($request->query->get('keyword', ''), $this->searchSettings);
$filter = new PartSearchFilter($request->query->get('keyword', ''));
//As an unchecked checkbox is not set in the query, the default value for all bools have to be false (which is the default argument value)!
$filter->setName($request->query->getBoolean('name'));
@ -336,6 +334,8 @@ class PartListsController extends AbstractController
$filter->setRegex($request->query->getBoolean('regex'));
$filter->setExtensive($request->query->getBoolean('extensive'));
$filter->setWildcard($request->query->getBoolean('wildcard'));
return $filter;
}

View file

@ -26,13 +26,18 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Query\Parameter;
use Doctrine\DBAL\ParameterType;
use App\Settings\BehaviorSettings\SearchSettings;
class PartSearchFilter implements FilterInterface
{
/** @var boolean Whether to use regex for searching */
protected bool $regex = false;
/** @var boolean Whether to use extensive matching for searching */
protected bool $extensive = false;
/** @var boolean Whether to use wildcards for searching */
protected bool $wildcard = false;
/** @var bool Use name field for searching */
protected bool $name = true;
@ -78,9 +83,7 @@ class PartSearchFilter implements FilterInterface
public function __construct(
/** @var string The string to query for */
protected string $keyword,
/** @var SearchSettings The settings that control how the search operates */
private readonly SearchSettings $searchSettings,
protected string $keyword
) {
}
@ -138,11 +141,11 @@ class PartSearchFilter implements FilterInterface
$search_dbId = $is_numeric && (bool)$this->dbId;
$tokens = [];
if ($this->searchSettings->enableAdvancedSearch) {
if ($this->extensive) {
//Transform keyword and trim excess spaces
$this->keyword = trim(str_replace('+', ' ', $this->keyword));
//Split keyword on spaces, but limit token count (default is 3)
$tokens = explode(' ', $this->keyword, $this->searchSettings->searchTokenLimit);
//Split keyword on spaces, but limit token count to 5
$tokens = explode(' ', $this->keyword, 5);
//Throw away array elements which are null or have zero length
$tokens = array_filter($tokens, fn($x) => (strlen($x) > 0));
}
@ -176,7 +179,7 @@ class PartSearchFilter implements FilterInterface
//Add a new expression and parameter set to the query for each token
foreach ($tokens as $i => $token) {
//Conditionally escape % and _ characters
if ($this->searchSettings->escapeSQLWildcards)
if (!$this->wildcard)
$token = str_replace(['%', '_'], ['\%', '\_'], $token);
//Convert the fields to search to a list of expressions
@ -240,6 +243,30 @@ class PartSearchFilter implements FilterInterface
return $this;
}
public function isExtensive(): bool
{
return $this->extensive;
}
public function setExtensive(bool $extensive): PartSearchFilter
{
$this->extensive = $extensive;
return $this;
}
public function isWildcard(): bool
{
return $this->wildcard;
}
public function setWildcard(bool $wildcard): PartSearchFilter
{
$this->wildcard = $wildcard;
return $this;
}
public function isName(): bool
{
return $this->name;

View file

@ -44,7 +44,4 @@ class BehaviorSettings
#[EmbeddedSettings]
public ?KeybindingsSettings $keybindings = null;
#[EmbeddedSettings]
public ?SearchSettings $search = null;
}

View file

@ -1,74 +0,0 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Settings\BehaviorSettings;
use App\Settings\SettingsIcon;
use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
use Jbtronics\SettingsBundle\Settings\Settings;
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Symfony\Component\Translation\TranslatableMessage as TM;
use Symfony\Component\Validator\Constraints as Assert;
#[Settings(name: "search", label: new TM("settings.behavior.search"))]
#[SettingsIcon('fa-magnifying-glass')]
class SearchSettings
{
/**
* Whether to enable advanced search
* @var bool
*/
#[SettingsParameter(
label: new TM("settings.behavior.search.enable_advanced_search"),
description: new TM("settings.behavior.search.enable_advanced_search.help"),
envVar: "bool:ENABLE_ADVANCED_SEARCH",
envVarMode: EnvVarMode::OVERWRITE
)]
public bool $enableAdvancedSearch = false;
/**
* Defines the maximum number of tokens the keyword can be split into
* @var int
*/
#[SettingsParameter(
label: new TM("settings.behavior.search.token_limit"),
description: new TM("settings.behavior.search.token_limit.help"),
envVar: "int:SEARCH_TOKEN_LIMIT",
envVarMode: EnvVarMode::OVERWRITE,
formOptions: ['attr' => ['min' => 2, 'max' => 5]],
)]
#[Assert\Range(min: 2, max: 5)]
public int $searchTokenLimit = 3;
/**
* Whether to escape sql wildcards
* @var bool
*/
#[SettingsParameter(
label: new TM("settings.behavior.search.escape_sql_wildcards"),
description: new TM("settings.behavior.search.escape_sql_wildcards.help"),
envVar: "bool:ESCAPE_SQL_WILDCARDS",
envVarMode: EnvVarMode::OVERWRITE
)]
public bool $escapeSQLWildcards = true;
}

View file

@ -1,4 +1,4 @@
{% macro settings_drodown(show_label_instead_icon = true) %}
{% macro settings_dropdown(show_label_instead_icon = true) %}
<div class="dropdown">
<button class="btn dropdown-toggle my-2" type="button" id="navbar-search-options" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-bs-auto-close="true">
@ -70,6 +70,14 @@
<input type="checkbox" class="form-check-input" id="regex" name="regex" value="1" {{ stimulus_controller('elements/localStorage_checkbox') }}>
<label for="regex" class="form-check-label justify-content-start">{% trans %}search.regexmatching{% endtrans %}</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="extensive" name="extensive" value="0" {{ stimulus_controller('elements/localStorage_checkbox') }}>
<label for="extensive" class="form-check-label justify-content-start">{% trans %}search.extensivematching{% endtrans %}</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="wildcard" name="wildcard" value="0" {{ stimulus_controller('elements/localStorage_checkbox') }}>
<label for="wildcard" class="form-check-label justify-content-start">{% trans %}search.permitwildcards{% endtrans %}</label>
</div>
</div>
</div>
</div>
@ -85,7 +93,7 @@
{# Show the options left in navbar #}
{% if is_navbar %}
{{ _self.settings_drodown(is_navbar) }}
{{ _self.settings_dropdown(is_navbar) }}
{% endif %}
<div {{ stimulus_controller('elements/part_search') }}
@ -103,7 +111,7 @@
{# And right in the standalone mode #}
{% if not is_navbar %}
{{ _self.settings_drodown(is_navbar) }}
{{ _self.settings_dropdown(is_navbar) }}
{% endif %}
</form>
{% endmacro %}

View file

@ -69,6 +69,14 @@
<input type="checkbox" class="form-check-input" disabled {% if searchFilter.regex %}checked{% endif %}>
<label for="regex" class="form-check-label justify-content-start">{% trans %}search.regexmatching{% endtrans %}</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" disabled {% if searchFilter.extensive %}checked{% endif %}>
<label for="extensive" class="form-check-label justify-content-start">{% trans %}search.extensivematching{% endtrans %}</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" disabled {% if searchFilter.extensive %}checked{% endif %}>
<label for="wildcard" class="form-check-label justify-content-start">{% trans %}search.permitwildcards{% endtrans %}</label>
</div>
</div>
</div>
</div>

View file

@ -22,7 +22,6 @@
namespace App\Tests\DataTables\Filters;
use App\DataTables\Filters\PartSearchFilter;
use App\Settings\BehaviorSettings\SearchSettings;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\ParameterType;
use Doctrine\ORM\Query\Expr;
@ -34,22 +33,10 @@ use PHPUnit\Framework\TestCase;
final class PartSearchFilterTest extends TestCase
{
private function makeSearchSettings(
bool $enableAdvancedSearch = false,
int $searchTokenLimit = 3,
bool $escapeSQLWildcards = true,
): SearchSettings {
$settings = $this->createMock(SearchSettings::class);
$settings->enableAdvancedSearch = $enableAdvancedSearch;
$settings->searchTokenLimit = $searchTokenLimit;
$settings->escapeSQLWildcards = $escapeSQLWildcards;
return $settings;
}
public function testApplyEnforcesNoResultsWhenKeywordEmpty(): void
{
$filter = new PartSearchFilter('', $this->makeSearchSettings());
$filter = new PartSearchFilter('');
$qb = $this->createMock(QueryBuilder::class);
$qb->expects($this->once())
@ -63,7 +50,7 @@ final class PartSearchFilterTest extends TestCase
public function testApplyEnforcesNoResultsWhenNothingToSearchForAndNoExactIdSearch(): void
{
$filter = (new PartSearchFilter('foo', $this->makeSearchSettings()))
$filter = (new PartSearchFilter('foo'))
->setName(false)
->setCategory(false)
->setDescription(false)
@ -90,7 +77,7 @@ final class PartSearchFilterTest extends TestCase
public function testApplyUsesRegexExpressionAndRawParameterWhenRegexEnabled(): void
{
$filter = (new PartSearchFilter('foo.*bar', $this->makeSearchSettings()))
$filter = (new PartSearchFilter('foo.*bar'))
->setRegex(true);
$expr = $this->createStub(Expr::class);
@ -123,9 +110,7 @@ final class PartSearchFilterTest extends TestCase
public function testApplyEscapesSqlWildcardsAndWrapsLikeParameterWhenRegexDisabled(): void
{
$filter = (new PartSearchFilter('10%_off', $this->makeSearchSettings(escapeSQLWildcards: true)))
->setRegex(false);
$filter = (new PartSearchFilter('10%_off'));
$expr = $this->createMock(Expr::class);
$expr->method('orX')->willReturn(new Orx());
@ -157,7 +142,7 @@ final class PartSearchFilterTest extends TestCase
public function testApplyAddsExactIdExpressionWhenDbIdSearchEnabledAndKeywordNumeric(): void
{
$filter = (new PartSearchFilter('123', $this->makeSearchSettings()))
$filter = (new PartSearchFilter('123'))
->setDbId(true);
$expr = $this->createMock(Expr::class);
@ -209,7 +194,7 @@ final class PartSearchFilterTest extends TestCase
public function testApplyDoesNotAddExactIdExpressionWhenKeywordNotNumeric(): void
{
$filter = (new PartSearchFilter('123abc', $this->makeSearchSettings()))
$filter = (new PartSearchFilter('123abc'))
->setDbId(true);
$expr = $this->createMock(Expr::class);