Compare commits

..

No commits in common. "56f82a75875f20b21f28e626c0c999b584154577" and "7a1a458abec88aff4de71f3e3cdae27bf078b43e" have entirely different histories.

19 changed files with 42 additions and 160 deletions

View file

@ -34,11 +34,6 @@ export default class extends Controller {
connect() {
let dropdownParent = "body";
if (this.element.closest('.modal')) {
dropdownParent = null
}
let settings = {
persistent: false,
create: true,
@ -47,7 +42,7 @@ export default class extends Controller {
selectOnTab: true,
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
dropdownParent: dropdownParent,
dropdownParent: 'body',
render: {
item: (data, escape) => {
return '<span>' + escape(data.label) + '</span>';

View file

@ -10,19 +10,13 @@ export default class extends Controller {
connect() {
//Check if tomselect is inside an modal and do not attach the dropdown to body in that case (as it breaks the modal)
let dropdownParent = "body";
if (this.element.closest('.modal')) {
dropdownParent = null
}
let settings = {
allowEmptyOption: true,
plugins: ['dropdown_input'],
searchField: ["name", "description", "category", "footprint"],
valueField: "id",
labelField: "name",
dropdownParent: dropdownParent,
dropdownParent: 'body',
preload: "focus",
render: {
item: (data, escape) => {

View file

@ -38,17 +38,13 @@ export default class extends Controller {
this._emptyMessage = this.element.getAttribute('title');
}
let dropdownParent = "body";
if (this.element.closest('.modal')) {
dropdownParent = null
}
let settings = {
plugins: ["clear_button"],
allowEmptyOption: true,
selectOnTab: true,
maxOptions: null,
dropdownParent: dropdownParent,
dropdownParent: 'body',
render: {
item: this.renderItem.bind(this),

View file

@ -26,15 +26,10 @@ export default class extends Controller {
_tomSelect;
connect() {
let dropdownParent = "body";
if (this.element.closest('.modal')) {
dropdownParent = null
}
this._tomSelect = new TomSelect(this.element, {
maxItems: 1000,
allowEmptyOption: true,
dropdownParent: dropdownParent,
dropdownParent: 'body',
plugins: ['remove_button'],
});
}

View file

@ -40,11 +40,6 @@ export default class extends Controller {
connect() {
let dropdownParent = "body";
if (this.element.closest('.modal')) {
dropdownParent = null
}
let settings = {
persistent: false,
create: true,
@ -55,7 +50,7 @@ export default class extends Controller {
valueField: 'text',
searchField: 'text',
orderField: 'text',
dropdownParent: dropdownParent,
dropdownParent: 'body',
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',

View file

@ -40,10 +40,7 @@ export default class extends Controller {
const allowAdd = this.element.getAttribute("data-allow-add") === "true";
const addHint = this.element.getAttribute("data-add-hint") ?? "";
let dropdownParent = "body";
if (this.element.closest('.modal')) {
dropdownParent = null
}
let settings = {
@ -57,7 +54,7 @@ export default class extends Controller {
maxItems: 1,
delimiter: "$$VERY_LONG_DELIMITER_THAT_SHOULD_NEVER_APPEAR$$",
splitOn: null,
dropdownParent: dropdownParent,
dropdownParent: 'body',
searchField: [
{field: "text", weight : 2},

View file

@ -33,11 +33,6 @@ export default class extends Controller {
_tomSelect;
connect() {
let dropdownParent = "body";
if (this.element.closest('.modal')) {
dropdownParent = null
}
let settings = {
plugins: {
remove_button:{},
@ -48,7 +43,7 @@ export default class extends Controller {
selectOnTab: true,
createOnBlur: true,
create: true,
dropdownParent: dropdownParent,
dropdownParent: 'body',
};
if(this.element.dataset.autocomplete) {

View file

@ -36,7 +36,6 @@ use App\Exceptions\InvalidRegexException;
use App\Form\Filters\PartFilterType;
use App\Services\Parts\PartsTableActionHandler;
use App\Services\Trees\NodesListBuilder;
use App\Settings\BehaviorSettings\SidebarSettings;
use App\Settings\BehaviorSettings\TableSettings;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\ORM\EntityManagerInterface;
@ -57,21 +56,11 @@ class PartListsController extends AbstractController
private readonly NodesListBuilder $nodesListBuilder,
private readonly DataTableFactory $dataTableFactory,
private readonly TranslatorInterface $translator,
private readonly TableSettings $tableSettings,
private readonly SidebarSettings $sidebarSettings,
private readonly TableSettings $tableSettings
)
{
}
/**
* Gets the filter operator to use by default (INCLUDING_CHILDREN or =)
* @return string
*/
private function getFilterOperator(): string
{
return $this->sidebarSettings->dataStructureNodesTableIncludeChildren ? 'INCLUDING_CHILDREN' : '=';
}
#[Route(path: '/table/action', name: 'table_action', methods: ['POST'])]
public function tableAction(Request $request, PartsTableActionHandler $actionHandler): Response
{
@ -214,7 +203,7 @@ class PartListsController extends AbstractController
return $this->showListWithFilter($request,
'parts/lists/category_list.html.twig',
function (PartFilter $filter) use ($category) {
$filter->category->setOperator($this->getFilterOperator())->setValue($category);
$filter->category->setOperator('INCLUDING_CHILDREN')->setValue($category);
}, function (FormInterface $filterForm) {
$this->disableFormFieldAfterCreation($filterForm->get('category')->get('value'));
}, [
@ -232,7 +221,7 @@ class PartListsController extends AbstractController
return $this->showListWithFilter($request,
'parts/lists/footprint_list.html.twig',
function (PartFilter $filter) use ($footprint) {
$filter->footprint->setOperator($this->getFilterOperator())->setValue($footprint);
$filter->footprint->setOperator('INCLUDING_CHILDREN')->setValue($footprint);
}, function (FormInterface $filterForm) {
$this->disableFormFieldAfterCreation($filterForm->get('footprint')->get('value'));
}, [
@ -250,7 +239,7 @@ class PartListsController extends AbstractController
return $this->showListWithFilter($request,
'parts/lists/manufacturer_list.html.twig',
function (PartFilter $filter) use ($manufacturer) {
$filter->manufacturer->setOperator($this->getFilterOperator())->setValue($manufacturer);
$filter->manufacturer->setOperator('INCLUDING_CHILDREN')->setValue($manufacturer);
}, function (FormInterface $filterForm) {
$this->disableFormFieldAfterCreation($filterForm->get('manufacturer')->get('value'));
}, [
@ -268,7 +257,7 @@ class PartListsController extends AbstractController
return $this->showListWithFilter($request,
'parts/lists/store_location_list.html.twig',
function (PartFilter $filter) use ($storelocation) {
$filter->storelocation->setOperator($this->getFilterOperator())->setValue($storelocation);
$filter->storelocation->setOperator('INCLUDING_CHILDREN')->setValue($storelocation);
}, function (FormInterface $filterForm) {
$this->disableFormFieldAfterCreation($filterForm->get('storelocation')->get('value'));
}, [
@ -286,7 +275,7 @@ class PartListsController extends AbstractController
return $this->showListWithFilter($request,
'parts/lists/supplier_list.html.twig',
function (PartFilter $filter) use ($supplier) {
$filter->supplier->setOperator($this->getFilterOperator())->setValue($supplier);
$filter->supplier->setOperator('INCLUDING_CHILDREN')->setValue($supplier);
}, function (FormInterface $filterForm) {
$this->disableFormFieldAfterCreation($filterForm->get('supplier')->get('value'));
}, [

View file

@ -96,15 +96,14 @@ class TextConstraint extends AbstractConstraint
//The CONTAINS, LIKE, STARTS and ENDS operators use the LIKE operator, but we have to build the value string differently
$like_value = null;
$escaped_value = str_replace(['%', '_'], ['\%', '\_'], $this->value);
if ($this->operator === 'LIKE') {
$like_value = $this->value; //Here we do not escape anything, as the user may provide % and _ wildcards
$like_value = $this->value;
} elseif ($this->operator === 'STARTS') {
$like_value = $escaped_value . '%';
$like_value = $this->value . '%';
} elseif ($this->operator === 'ENDS') {
$like_value = '%' . $escaped_value;
$like_value = '%' . $this->value;
} elseif ($this->operator === 'CONTAINS') {
$like_value = '%' . $escaped_value . '%';
$like_value = '%' . $this->value . '%';
}
if ($like_value !== null) {

View file

@ -144,8 +144,6 @@ class PartSearchFilter implements FilterInterface
if ($this->regex) {
$queryBuilder->setParameter('search_query', $this->keyword);
} else {
//Escape % and _ characters in the keyword
$this->keyword = str_replace(['%', '_'], ['\%', '\_'], $this->keyword);
$queryBuilder->setParameter('search_query', '%' . $this->keyword . '%');
}
}

View file

@ -56,6 +56,7 @@ class ILike extends FunctionNode
{
$platform = $sqlWalker->getConnection()->getDatabasePlatform();
//
if ($platform instanceof AbstractMySQLPlatform || $platform instanceof SQLitePlatform) {
$operator = 'LIKE';
} elseif ($platform instanceof PostgreSQLPlatform) {
@ -65,12 +66,6 @@ class ILike extends FunctionNode
throw new \RuntimeException('Platform ' . gettype($platform) . ' does not support case insensitive like expressions.');
}
$escape = "";
if ($platform instanceof SQLitePlatform) {
//SQLite needs ESCAPE explicitly defined backslash as escape character
$escape = " ESCAPE '\\'";
}
return '(' . $this->value->dispatch($sqlWalker) . ' ' . $operator . ' ' . $this->expr->dispatch($sqlWalker) . $escape . ')';
return '(' . $this->value->dispatch($sqlWalker) . ' ' . $operator . ' ' . $this->expr->dispatch($sqlWalker) . ')';
}
}
}

View file

@ -221,7 +221,7 @@ final class DTOtoEntityConverter
$attachment = $this->convertFile($image, $image_type);
$attachments_grouped[$attachment->getName()][] = $attachment;
if (count($attachments_grouped[$attachment->getName()]) > 1) {
if (count($attachments_grouped[$attachment->getName()] ?? []) > 1) {
$attachment->setName($attachment->getName() . ' (' . (count($attachments_grouped[$attachment->getName()]) + 1) . ')');
}
@ -236,7 +236,7 @@ final class DTOtoEntityConverter
$attachment = $this->convertFile($datasheet, $datasheet_type);
$attachments_grouped[$attachment->getName()][] = $attachment;
if (count($attachments_grouped[$attachment->getName()]) > 1) {
if (count($attachments_grouped[$attachment->getName()] ?? []) > 1) {
$attachment->setName($attachment->getName() . ' (' . (count($attachments_grouped[$attachment->getName()])) . ')');
}
@ -357,4 +357,4 @@ final class DTOtoEntityConverter
return $tmp;
}
}
}

View file

@ -31,9 +31,9 @@ use App\Services\Parts\PartLotWithdrawAddHelper;
/**
* @see \App\Tests\Services\ProjectSystem\ProjectBuildHelperTest
*/
final readonly class ProjectBuildHelper
class ProjectBuildHelper
{
public function __construct(private PartLotWithdrawAddHelper $withdraw_add_helper)
public function __construct(private readonly PartLotWithdrawAddHelper $withdraw_add_helper)
{
}
@ -63,35 +63,18 @@ final readonly class ProjectBuildHelper
*/
public function getMaximumBuildableCount(Project $project): int
{
$bom_entries = $project->getBomEntries();
if ($bom_entries->isEmpty()) {
return 0;
}
$maximum_buildable_count = PHP_INT_MAX;
foreach ($bom_entries as $bom_entry) {
foreach ($project->getBomEntries() as $bom_entry) {
//Skip BOM entries without a part (as we can not determine that)
if (!$bom_entry->isPartBomEntry()) {
continue;
}
//The maximum buildable count for the whole project is the minimum of all BOM entries
$maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry));
}
return $maximum_buildable_count;
}
/**
* Returns the maximum buildable amount of the given project as string, based on the stock of the used parts in the BOM.
* If the maximum buildable count is infinite, the string '∞' is returned.
* @param Project $project
* @return string
*/
public function getMaximumBuildableCountAsString(Project $project): string
{
$max_count = $this->getMaximumBuildableCount($project);
if ($max_count === PHP_INT_MAX) {
return '∞';
}
return (string) $max_count;
return $maximum_buildable_count;
}
/**

View file

@ -73,11 +73,4 @@ class SidebarSettings
*/
#[SettingsParameter(label: new TM("settings.behavior.sidebar.rootNodeRedirectsToNewEntity"))]
public bool $rootNodeRedirectsToNewEntity = false;
/**
* @var bool Whether to include child nodes in the data structure nodes table, or only show the selected node's parts.
*/
#[SettingsParameter(label: new TM("settings.behavior.sidebar.data_structure_nodes_table_include_children"),
description: new TM("settings.behavior.sidebar.data_structure_nodes_table_include_children.help"))]
public bool $dataStructureNodesTableIncludeChildren = true;
}
}

View file

@ -82,7 +82,7 @@ final class FormatExtension extends AbstractExtension
public function formatBytes(int $bytes, int $precision = 2): string
{
$size = ['B','kB','MB','GB','TB','PB','EB','ZB','YB'];
$factor = (int) floor((strlen((string) $bytes) - 1) / 3);
$factor = floor((strlen((string) $bytes) - 1) / 3);
//We use the real (10 based) SI prefix here
return sprintf("%.{$precision}f", $bytes / (1000 ** $factor)) . ' ' . @$size[$factor];
}

View file

@ -8,8 +8,7 @@
{% endblock %}
{% block card_content %}
{% set bom_empty = project.bomEntries | length == 0 %}
{% set can_build = not bom_empty and buildHelper.projectBuildable(project, number_of_builds) %}
{% set can_build = buildHelper.projectBuildable(project, number_of_builds) %}
{% import "components/projects.macro.html.twig" as project_macros %}
{% if project.status is not empty and project.status != "in_production" %}
@ -18,10 +17,8 @@
</div>
{% endif %}
<div class="alert {% if can_build %}alert-success{% elseif bom_empty%}alert-warning{% else %}alert-danger{% endif %}" role="alert">
{% if bom_empty %}
<h5><i class="fa-solid fa-circle-exclamation fa-fw"></i> {% trans %}project.builds.no_bom_entries{% endtrans %}</h5>
{% elseif not can_build %}
<div class="alert {% if can_build %}alert-success{% else %}alert-danger{% endif %}" role="alert">
{% if not can_build %}
<h5><i class="fa-solid fa-circle-exclamation fa-fw"></i> {% trans %}project.builds.build_not_possible{% endtrans %}</h5>
<b>{% trans with {"%number_of_builds%": number_of_builds} %}project.builds.following_bom_entries_miss_instock_n{% endtrans %}</b>
<ul>
@ -40,4 +37,4 @@
{% include 'projects/build/_form.html.twig' %}
{% endblock %}
{% endblock %}

View file

@ -1,5 +1,4 @@
{% set bom_empty = project.bomEntries | length == 0 %}
{% set can_build = not bom_empty and buildHelper.projectBuildable(project) %}
{% set can_build = buildHelper.projectBuildable(project) %}
{% import "components/projects.macro.html.twig" as project_macros %}
@ -9,10 +8,8 @@
</div>
{% endif %}
<div class="alert mt-2 {% if can_build %}alert-success{% elseif bom_empty%}alert-warning{% else %}alert-danger{% endif %}" role="alert">
{% if bom_empty %}
<h5><i class="fa-solid fa-circle-exclamation fa-fw"></i> {% trans %}project.builds.no_bom_entries{% endtrans %}</h5>
{% elseif not can_build %}
<div class="alert mt-2 {% if can_build %}alert-success{% else %}alert-danger{% endif %}" role="alert">
{% if not can_build %}
<h5><i class="fa-solid fa-circle-exclamation fa-fw"></i> {% trans %}project.builds.build_not_possible{% endtrans %}</h5>
<b>{% trans %}project.builds.following_bom_entries_miss_instock{% endtrans %}</b>
<ul>
@ -22,7 +19,7 @@
</ul>
{% else %}
<h5><i class="fa-solid fa-circle-check fa-fw"></i> {% trans %}project.builds.build_possible{% endtrans %}</h5>
<span>{% trans with {"%max_builds%": buildHelper.maximumBuildableCountAsString(project)} %}project.builds.number_of_builds_possible{% endtrans %}</span>
<span>{% trans with {"%max_builds%": buildHelper.maximumBuildableCount(project)} %}project.builds.number_of_builds_possible{% endtrans %}</span>
{% endif %}
</div>
@ -30,7 +27,7 @@
<div class="row mt-2">
<div class="col-4">
<div class="input-group mb-3">
<input type="number" min="1" class="form-control" placeholder="{% trans %}project.builds.number_of_builds{% endtrans %}" name="n" required value="1">
<input type="number" min="1" class="form-control" placeholder="{% trans %}project.builds.number_of_builds{% endtrans %}" name="n" required>
<input type="hidden" name="_redirect" value="{{ uri_without_host(app.request) }}">
<button class="btn btn-outline-secondary" type="submit" id="button-addon2">{% trans %}project.build.btn_build{% endtrans %}</button>
</div>
@ -40,4 +37,4 @@
{% if project.buildPart %}
<p><b>{% trans %}project.builds.no_stocked_builds{% endtrans %}:</b> <a href="{{ entity_url(project.buildPart) }}">{{ project.buildPart.amountSum }}</a></p>
{% endif %}
{% endif %}

View file

@ -114,22 +114,4 @@ class ProjectBuildHelperTest extends WebTestCase
$this->assertSame(0, $this->service->getMaximumBuildableCount($project));
}
public function testGetMaximumBuildableCountEmpty(): void
{
$project = new Project();
$this->assertSame(0, $this->service->getMaximumBuildableCount($project));
}
public function testGetMaximumBuildableCountAsString(): void
{
$project = new Project();
$bom_entry1 = new ProjectBOMEntry();
$bom_entry1->setName("Test");
$project->addBomEntry($bom_entry1);
$this->assertSame('∞', $this->service->getMaximumBuildableCountAsString($project));
}
}

View file

@ -14223,25 +14223,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="Ej2znKK" name="settings.system.localization.language_menu_entries.description">
<segment state="translated">
<source>settings.system.localization.language_menu_entries.description</source>
<target><![CDATA[The languages to show in the language drop-down menu. Order can be changed via drag & drop. Leave empty to show all available languages.]]></target>
</segment>
</unit>
<unit id="xIZ_mEX" name="project.builds.no_bom_entries">
<segment>
<source>project.builds.no_bom_entries</source>
<target>Project has no BOM entries</target>
</segment>
</unit>
<unit id="pCKgjIr" name="settings.behavior.sidebar.data_structure_nodes_table_include_children">
<segment>
<source>settings.behavior.sidebar.data_structure_nodes_table_include_children</source>
<target>Tables should include children nodes by default</target>
</segment>
</unit>
<unit id="KJQHfwV" name="settings.behavior.sidebar.data_structure_nodes_table_include_children.help">
<segment>
<source>settings.behavior.sidebar.data_structure_nodes_table_include_children.help</source>
<target>If checked, the part tables for categories, footprints, etc. should include all parts of child categories. If not checked, only parts that strictly belong to the clicked node are shown.</target>
<target>The languages to show in the language drop-down menu. Order can be changed via drag &amp; drop. Leave empty to show all available languages.</target>
</segment>
</unit>
</file>