mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-03-04 14:29:35 +00:00
Compare commits
6 commits
7a1a458abe
...
56f82a7587
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56f82a7587 | ||
|
|
4c30cab7c1 | ||
|
|
1c8ca6c0a2 | ||
|
|
5dbe4ba00b | ||
|
|
377feaf566 | ||
|
|
05839a549c |
19 changed files with 160 additions and 42 deletions
|
|
@ -34,6 +34,11 @@ export default class extends Controller {
|
|||
|
||||
connect() {
|
||||
|
||||
let dropdownParent = "body";
|
||||
if (this.element.closest('.modal')) {
|
||||
dropdownParent = null
|
||||
}
|
||||
|
||||
let settings = {
|
||||
persistent: false,
|
||||
create: true,
|
||||
|
|
@ -42,7 +47,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: 'body',
|
||||
dropdownParent: dropdownParent,
|
||||
render: {
|
||||
item: (data, escape) => {
|
||||
return '<span>' + escape(data.label) + '</span>';
|
||||
|
|
|
|||
|
|
@ -10,13 +10,19 @@ 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: 'body',
|
||||
dropdownParent: dropdownParent,
|
||||
preload: "focus",
|
||||
render: {
|
||||
item: (data, escape) => {
|
||||
|
|
|
|||
|
|
@ -38,13 +38,17 @@ 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: 'body',
|
||||
dropdownParent: dropdownParent,
|
||||
|
||||
render: {
|
||||
item: this.renderItem.bind(this),
|
||||
|
|
|
|||
|
|
@ -26,10 +26,15 @@ 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: 'body',
|
||||
dropdownParent: dropdownParent,
|
||||
plugins: ['remove_button'],
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ export default class extends Controller {
|
|||
|
||||
connect() {
|
||||
|
||||
let dropdownParent = "body";
|
||||
if (this.element.closest('.modal')) {
|
||||
dropdownParent = null
|
||||
}
|
||||
|
||||
let settings = {
|
||||
persistent: false,
|
||||
create: true,
|
||||
|
|
@ -50,7 +55,7 @@ export default class extends Controller {
|
|||
valueField: 'text',
|
||||
searchField: 'text',
|
||||
orderField: 'text',
|
||||
dropdownParent: 'body',
|
||||
dropdownParent: dropdownParent,
|
||||
|
||||
//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',
|
||||
|
|
|
|||
|
|
@ -40,7 +40,10 @@ 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 = {
|
||||
|
|
@ -54,7 +57,7 @@ export default class extends Controller {
|
|||
maxItems: 1,
|
||||
delimiter: "$$VERY_LONG_DELIMITER_THAT_SHOULD_NEVER_APPEAR$$",
|
||||
splitOn: null,
|
||||
dropdownParent: 'body',
|
||||
dropdownParent: dropdownParent,
|
||||
|
||||
searchField: [
|
||||
{field: "text", weight : 2},
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ export default class extends Controller {
|
|||
_tomSelect;
|
||||
|
||||
connect() {
|
||||
let dropdownParent = "body";
|
||||
if (this.element.closest('.modal')) {
|
||||
dropdownParent = null
|
||||
}
|
||||
|
||||
let settings = {
|
||||
plugins: {
|
||||
remove_button:{},
|
||||
|
|
@ -43,7 +48,7 @@ export default class extends Controller {
|
|||
selectOnTab: true,
|
||||
createOnBlur: true,
|
||||
create: true,
|
||||
dropdownParent: 'body',
|
||||
dropdownParent: dropdownParent,
|
||||
};
|
||||
|
||||
if(this.element.dataset.autocomplete) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ 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;
|
||||
|
|
@ -56,11 +57,21 @@ class PartListsController extends AbstractController
|
|||
private readonly NodesListBuilder $nodesListBuilder,
|
||||
private readonly DataTableFactory $dataTableFactory,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly TableSettings $tableSettings
|
||||
private readonly TableSettings $tableSettings,
|
||||
private readonly SidebarSettings $sidebarSettings,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
|
|
@ -203,7 +214,7 @@ class PartListsController extends AbstractController
|
|||
return $this->showListWithFilter($request,
|
||||
'parts/lists/category_list.html.twig',
|
||||
function (PartFilter $filter) use ($category) {
|
||||
$filter->category->setOperator('INCLUDING_CHILDREN')->setValue($category);
|
||||
$filter->category->setOperator($this->getFilterOperator())->setValue($category);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('category')->get('value'));
|
||||
}, [
|
||||
|
|
@ -221,7 +232,7 @@ class PartListsController extends AbstractController
|
|||
return $this->showListWithFilter($request,
|
||||
'parts/lists/footprint_list.html.twig',
|
||||
function (PartFilter $filter) use ($footprint) {
|
||||
$filter->footprint->setOperator('INCLUDING_CHILDREN')->setValue($footprint);
|
||||
$filter->footprint->setOperator($this->getFilterOperator())->setValue($footprint);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('footprint')->get('value'));
|
||||
}, [
|
||||
|
|
@ -239,7 +250,7 @@ class PartListsController extends AbstractController
|
|||
return $this->showListWithFilter($request,
|
||||
'parts/lists/manufacturer_list.html.twig',
|
||||
function (PartFilter $filter) use ($manufacturer) {
|
||||
$filter->manufacturer->setOperator('INCLUDING_CHILDREN')->setValue($manufacturer);
|
||||
$filter->manufacturer->setOperator($this->getFilterOperator())->setValue($manufacturer);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('manufacturer')->get('value'));
|
||||
}, [
|
||||
|
|
@ -257,7 +268,7 @@ class PartListsController extends AbstractController
|
|||
return $this->showListWithFilter($request,
|
||||
'parts/lists/store_location_list.html.twig',
|
||||
function (PartFilter $filter) use ($storelocation) {
|
||||
$filter->storelocation->setOperator('INCLUDING_CHILDREN')->setValue($storelocation);
|
||||
$filter->storelocation->setOperator($this->getFilterOperator())->setValue($storelocation);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('storelocation')->get('value'));
|
||||
}, [
|
||||
|
|
@ -275,7 +286,7 @@ class PartListsController extends AbstractController
|
|||
return $this->showListWithFilter($request,
|
||||
'parts/lists/supplier_list.html.twig',
|
||||
function (PartFilter $filter) use ($supplier) {
|
||||
$filter->supplier->setOperator('INCLUDING_CHILDREN')->setValue($supplier);
|
||||
$filter->supplier->setOperator($this->getFilterOperator())->setValue($supplier);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('supplier')->get('value'));
|
||||
}, [
|
||||
|
|
|
|||
|
|
@ -96,14 +96,15 @@ 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;
|
||||
$like_value = $this->value; //Here we do not escape anything, as the user may provide % and _ wildcards
|
||||
} elseif ($this->operator === 'STARTS') {
|
||||
$like_value = $this->value . '%';
|
||||
$like_value = $escaped_value . '%';
|
||||
} elseif ($this->operator === 'ENDS') {
|
||||
$like_value = '%' . $this->value;
|
||||
$like_value = '%' . $escaped_value;
|
||||
} elseif ($this->operator === 'CONTAINS') {
|
||||
$like_value = '%' . $this->value . '%';
|
||||
$like_value = '%' . $escaped_value . '%';
|
||||
}
|
||||
|
||||
if ($like_value !== null) {
|
||||
|
|
|
|||
|
|
@ -144,6 +144,8 @@ 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 . '%');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ class ILike extends FunctionNode
|
|||
{
|
||||
$platform = $sqlWalker->getConnection()->getDatabasePlatform();
|
||||
|
||||
//
|
||||
if ($platform instanceof AbstractMySQLPlatform || $platform instanceof SQLitePlatform) {
|
||||
$operator = 'LIKE';
|
||||
} elseif ($platform instanceof PostgreSQLPlatform) {
|
||||
|
|
@ -66,6 +65,12 @@ class ILike extends FunctionNode
|
|||
throw new \RuntimeException('Platform ' . gettype($platform) . ' does not support case insensitive like expressions.');
|
||||
}
|
||||
|
||||
return '(' . $this->value->dispatch($sqlWalker) . ' ' . $operator . ' ' . $this->expr->dispatch($sqlWalker) . ')';
|
||||
$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 . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ use App\Services\Parts\PartLotWithdrawAddHelper;
|
|||
/**
|
||||
* @see \App\Tests\Services\ProjectSystem\ProjectBuildHelperTest
|
||||
*/
|
||||
class ProjectBuildHelper
|
||||
final readonly class ProjectBuildHelper
|
||||
{
|
||||
public function __construct(private readonly PartLotWithdrawAddHelper $withdraw_add_helper)
|
||||
public function __construct(private PartLotWithdrawAddHelper $withdraw_add_helper)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -63,20 +63,37 @@ 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 ($project->getBomEntries() as $bom_entry) {
|
||||
foreach ($bom_entries 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given project can be built with the current stock.
|
||||
* This means that the maximum buildable count is greater or equal than the requested $number_of_projects
|
||||
|
|
|
|||
|
|
@ -73,4 +73,11 @@ 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = floor((strlen((string) $bytes) - 1) / 3);
|
||||
$factor = (int) floor((strlen((string) $bytes) - 1) / 3);
|
||||
//We use the real (10 based) SI prefix here
|
||||
return sprintf("%.{$precision}f", $bytes / (1000 ** $factor)) . ' ' . @$size[$factor];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
{% set can_build = buildHelper.projectBuildable(project, number_of_builds) %}
|
||||
{% set bom_empty = project.bomEntries | length == 0 %}
|
||||
{% set can_build = not bom_empty and 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" %}
|
||||
|
|
@ -17,8 +18,10 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="alert {% if can_build %}alert-success{% else %}alert-danger{% endif %}" role="alert">
|
||||
{% if not can_build %}
|
||||
<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 %}
|
||||
<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>
|
||||
|
|
@ -37,4 +40,4 @@
|
|||
{% include 'projects/build/_form.html.twig' %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{% set can_build = buildHelper.projectBuildable(project) %}
|
||||
{% set bom_empty = project.bomEntries | length == 0 %}
|
||||
{% set can_build = not bom_empty and buildHelper.projectBuildable(project) %}
|
||||
|
||||
{% import "components/projects.macro.html.twig" as project_macros %}
|
||||
|
||||
|
|
@ -8,8 +9,10 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="alert mt-2 {% if can_build %}alert-success{% else %}alert-danger{% endif %}" role="alert">
|
||||
{% if not can_build %}
|
||||
<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 %}
|
||||
<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>
|
||||
|
|
@ -19,7 +22,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.maximumBuildableCount(project)} %}project.builds.number_of_builds_possible{% endtrans %}</span>
|
||||
<span>{% trans with {"%max_builds%": buildHelper.maximumBuildableCountAsString(project)} %}project.builds.number_of_builds_possible{% endtrans %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
|
@ -27,7 +30,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>
|
||||
<input type="number" min="1" class="form-control" placeholder="{% trans %}project.builds.number_of_builds{% endtrans %}" name="n" required value="1">
|
||||
<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>
|
||||
|
|
@ -37,4 +40,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 %}
|
||||
|
|
|
|||
|
|
@ -114,4 +114,22 @@ 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));
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14223,7 +14223,25 @@ 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>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>
|
||||
<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>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue