mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-07-07 01:41:39 +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() {
|
connect() {
|
||||||
|
|
||||||
|
let dropdownParent = "body";
|
||||||
|
if (this.element.closest('.modal')) {
|
||||||
|
dropdownParent = null
|
||||||
|
}
|
||||||
|
|
||||||
let settings = {
|
let settings = {
|
||||||
persistent: false,
|
persistent: false,
|
||||||
create: true,
|
create: true,
|
||||||
|
|
@ -42,7 +47,7 @@ export default class extends Controller {
|
||||||
selectOnTab: true,
|
selectOnTab: true,
|
||||||
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
|
//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',
|
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
|
||||||
dropdownParent: 'body',
|
dropdownParent: dropdownParent,
|
||||||
render: {
|
render: {
|
||||||
item: (data, escape) => {
|
item: (data, escape) => {
|
||||||
return '<span>' + escape(data.label) + '</span>';
|
return '<span>' + escape(data.label) + '</span>';
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,19 @@ export default class extends Controller {
|
||||||
|
|
||||||
connect() {
|
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 = {
|
let settings = {
|
||||||
allowEmptyOption: true,
|
allowEmptyOption: true,
|
||||||
plugins: ['dropdown_input'],
|
plugins: ['dropdown_input'],
|
||||||
searchField: ["name", "description", "category", "footprint"],
|
searchField: ["name", "description", "category", "footprint"],
|
||||||
valueField: "id",
|
valueField: "id",
|
||||||
labelField: "name",
|
labelField: "name",
|
||||||
dropdownParent: 'body',
|
dropdownParent: dropdownParent,
|
||||||
preload: "focus",
|
preload: "focus",
|
||||||
render: {
|
render: {
|
||||||
item: (data, escape) => {
|
item: (data, escape) => {
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,17 @@ export default class extends Controller {
|
||||||
this._emptyMessage = this.element.getAttribute('title');
|
this._emptyMessage = this.element.getAttribute('title');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dropdownParent = "body";
|
||||||
|
if (this.element.closest('.modal')) {
|
||||||
|
dropdownParent = null
|
||||||
|
}
|
||||||
|
|
||||||
let settings = {
|
let settings = {
|
||||||
plugins: ["clear_button"],
|
plugins: ["clear_button"],
|
||||||
allowEmptyOption: true,
|
allowEmptyOption: true,
|
||||||
selectOnTab: true,
|
selectOnTab: true,
|
||||||
maxOptions: null,
|
maxOptions: null,
|
||||||
dropdownParent: 'body',
|
dropdownParent: dropdownParent,
|
||||||
|
|
||||||
render: {
|
render: {
|
||||||
item: this.renderItem.bind(this),
|
item: this.renderItem.bind(this),
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,15 @@ export default class extends Controller {
|
||||||
_tomSelect;
|
_tomSelect;
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
|
let dropdownParent = "body";
|
||||||
|
if (this.element.closest('.modal')) {
|
||||||
|
dropdownParent = null
|
||||||
|
}
|
||||||
|
|
||||||
this._tomSelect = new TomSelect(this.element, {
|
this._tomSelect = new TomSelect(this.element, {
|
||||||
maxItems: 1000,
|
maxItems: 1000,
|
||||||
allowEmptyOption: true,
|
allowEmptyOption: true,
|
||||||
dropdownParent: 'body',
|
dropdownParent: dropdownParent,
|
||||||
plugins: ['remove_button'],
|
plugins: ['remove_button'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@ export default class extends Controller {
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
|
|
||||||
|
let dropdownParent = "body";
|
||||||
|
if (this.element.closest('.modal')) {
|
||||||
|
dropdownParent = null
|
||||||
|
}
|
||||||
|
|
||||||
let settings = {
|
let settings = {
|
||||||
persistent: false,
|
persistent: false,
|
||||||
create: true,
|
create: true,
|
||||||
|
|
@ -50,7 +55,7 @@ export default class extends Controller {
|
||||||
valueField: 'text',
|
valueField: 'text',
|
||||||
searchField: 'text',
|
searchField: 'text',
|
||||||
orderField: 'text',
|
orderField: 'text',
|
||||||
dropdownParent: 'body',
|
dropdownParent: dropdownParent,
|
||||||
|
|
||||||
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
|
//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',
|
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 allowAdd = this.element.getAttribute("data-allow-add") === "true";
|
||||||
const addHint = this.element.getAttribute("data-add-hint") ?? "";
|
const addHint = this.element.getAttribute("data-add-hint") ?? "";
|
||||||
|
|
||||||
|
let dropdownParent = "body";
|
||||||
|
if (this.element.closest('.modal')) {
|
||||||
|
dropdownParent = null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
let settings = {
|
let settings = {
|
||||||
|
|
@ -54,7 +57,7 @@ export default class extends Controller {
|
||||||
maxItems: 1,
|
maxItems: 1,
|
||||||
delimiter: "$$VERY_LONG_DELIMITER_THAT_SHOULD_NEVER_APPEAR$$",
|
delimiter: "$$VERY_LONG_DELIMITER_THAT_SHOULD_NEVER_APPEAR$$",
|
||||||
splitOn: null,
|
splitOn: null,
|
||||||
dropdownParent: 'body',
|
dropdownParent: dropdownParent,
|
||||||
|
|
||||||
searchField: [
|
searchField: [
|
||||||
{field: "text", weight : 2},
|
{field: "text", weight : 2},
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,11 @@ export default class extends Controller {
|
||||||
_tomSelect;
|
_tomSelect;
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
|
let dropdownParent = "body";
|
||||||
|
if (this.element.closest('.modal')) {
|
||||||
|
dropdownParent = null
|
||||||
|
}
|
||||||
|
|
||||||
let settings = {
|
let settings = {
|
||||||
plugins: {
|
plugins: {
|
||||||
remove_button:{},
|
remove_button:{},
|
||||||
|
|
@ -43,7 +48,7 @@ export default class extends Controller {
|
||||||
selectOnTab: true,
|
selectOnTab: true,
|
||||||
createOnBlur: true,
|
createOnBlur: true,
|
||||||
create: true,
|
create: true,
|
||||||
dropdownParent: 'body',
|
dropdownParent: dropdownParent,
|
||||||
};
|
};
|
||||||
|
|
||||||
if(this.element.dataset.autocomplete) {
|
if(this.element.dataset.autocomplete) {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ use App\Exceptions\InvalidRegexException;
|
||||||
use App\Form\Filters\PartFilterType;
|
use App\Form\Filters\PartFilterType;
|
||||||
use App\Services\Parts\PartsTableActionHandler;
|
use App\Services\Parts\PartsTableActionHandler;
|
||||||
use App\Services\Trees\NodesListBuilder;
|
use App\Services\Trees\NodesListBuilder;
|
||||||
|
use App\Settings\BehaviorSettings\SidebarSettings;
|
||||||
use App\Settings\BehaviorSettings\TableSettings;
|
use App\Settings\BehaviorSettings\TableSettings;
|
||||||
use Doctrine\DBAL\Exception\DriverException;
|
use Doctrine\DBAL\Exception\DriverException;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
@ -56,11 +57,21 @@ class PartListsController extends AbstractController
|
||||||
private readonly NodesListBuilder $nodesListBuilder,
|
private readonly NodesListBuilder $nodesListBuilder,
|
||||||
private readonly DataTableFactory $dataTableFactory,
|
private readonly DataTableFactory $dataTableFactory,
|
||||||
private readonly TranslatorInterface $translator,
|
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'])]
|
#[Route(path: '/table/action', name: 'table_action', methods: ['POST'])]
|
||||||
public function tableAction(Request $request, PartsTableActionHandler $actionHandler): Response
|
public function tableAction(Request $request, PartsTableActionHandler $actionHandler): Response
|
||||||
{
|
{
|
||||||
|
|
@ -203,7 +214,7 @@ class PartListsController extends AbstractController
|
||||||
return $this->showListWithFilter($request,
|
return $this->showListWithFilter($request,
|
||||||
'parts/lists/category_list.html.twig',
|
'parts/lists/category_list.html.twig',
|
||||||
function (PartFilter $filter) use ($category) {
|
function (PartFilter $filter) use ($category) {
|
||||||
$filter->category->setOperator('INCLUDING_CHILDREN')->setValue($category);
|
$filter->category->setOperator($this->getFilterOperator())->setValue($category);
|
||||||
}, function (FormInterface $filterForm) {
|
}, function (FormInterface $filterForm) {
|
||||||
$this->disableFormFieldAfterCreation($filterForm->get('category')->get('value'));
|
$this->disableFormFieldAfterCreation($filterForm->get('category')->get('value'));
|
||||||
}, [
|
}, [
|
||||||
|
|
@ -221,7 +232,7 @@ class PartListsController extends AbstractController
|
||||||
return $this->showListWithFilter($request,
|
return $this->showListWithFilter($request,
|
||||||
'parts/lists/footprint_list.html.twig',
|
'parts/lists/footprint_list.html.twig',
|
||||||
function (PartFilter $filter) use ($footprint) {
|
function (PartFilter $filter) use ($footprint) {
|
||||||
$filter->footprint->setOperator('INCLUDING_CHILDREN')->setValue($footprint);
|
$filter->footprint->setOperator($this->getFilterOperator())->setValue($footprint);
|
||||||
}, function (FormInterface $filterForm) {
|
}, function (FormInterface $filterForm) {
|
||||||
$this->disableFormFieldAfterCreation($filterForm->get('footprint')->get('value'));
|
$this->disableFormFieldAfterCreation($filterForm->get('footprint')->get('value'));
|
||||||
}, [
|
}, [
|
||||||
|
|
@ -239,7 +250,7 @@ class PartListsController extends AbstractController
|
||||||
return $this->showListWithFilter($request,
|
return $this->showListWithFilter($request,
|
||||||
'parts/lists/manufacturer_list.html.twig',
|
'parts/lists/manufacturer_list.html.twig',
|
||||||
function (PartFilter $filter) use ($manufacturer) {
|
function (PartFilter $filter) use ($manufacturer) {
|
||||||
$filter->manufacturer->setOperator('INCLUDING_CHILDREN')->setValue($manufacturer);
|
$filter->manufacturer->setOperator($this->getFilterOperator())->setValue($manufacturer);
|
||||||
}, function (FormInterface $filterForm) {
|
}, function (FormInterface $filterForm) {
|
||||||
$this->disableFormFieldAfterCreation($filterForm->get('manufacturer')->get('value'));
|
$this->disableFormFieldAfterCreation($filterForm->get('manufacturer')->get('value'));
|
||||||
}, [
|
}, [
|
||||||
|
|
@ -257,7 +268,7 @@ class PartListsController extends AbstractController
|
||||||
return $this->showListWithFilter($request,
|
return $this->showListWithFilter($request,
|
||||||
'parts/lists/store_location_list.html.twig',
|
'parts/lists/store_location_list.html.twig',
|
||||||
function (PartFilter $filter) use ($storelocation) {
|
function (PartFilter $filter) use ($storelocation) {
|
||||||
$filter->storelocation->setOperator('INCLUDING_CHILDREN')->setValue($storelocation);
|
$filter->storelocation->setOperator($this->getFilterOperator())->setValue($storelocation);
|
||||||
}, function (FormInterface $filterForm) {
|
}, function (FormInterface $filterForm) {
|
||||||
$this->disableFormFieldAfterCreation($filterForm->get('storelocation')->get('value'));
|
$this->disableFormFieldAfterCreation($filterForm->get('storelocation')->get('value'));
|
||||||
}, [
|
}, [
|
||||||
|
|
@ -275,7 +286,7 @@ class PartListsController extends AbstractController
|
||||||
return $this->showListWithFilter($request,
|
return $this->showListWithFilter($request,
|
||||||
'parts/lists/supplier_list.html.twig',
|
'parts/lists/supplier_list.html.twig',
|
||||||
function (PartFilter $filter) use ($supplier) {
|
function (PartFilter $filter) use ($supplier) {
|
||||||
$filter->supplier->setOperator('INCLUDING_CHILDREN')->setValue($supplier);
|
$filter->supplier->setOperator($this->getFilterOperator())->setValue($supplier);
|
||||||
}, function (FormInterface $filterForm) {
|
}, function (FormInterface $filterForm) {
|
||||||
$this->disableFormFieldAfterCreation($filterForm->get('supplier')->get('value'));
|
$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
|
//The CONTAINS, LIKE, STARTS and ENDS operators use the LIKE operator, but we have to build the value string differently
|
||||||
$like_value = null;
|
$like_value = null;
|
||||||
|
$escaped_value = str_replace(['%', '_'], ['\%', '\_'], $this->value);
|
||||||
if ($this->operator === 'LIKE') {
|
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') {
|
} elseif ($this->operator === 'STARTS') {
|
||||||
$like_value = $this->value . '%';
|
$like_value = $escaped_value . '%';
|
||||||
} elseif ($this->operator === 'ENDS') {
|
} elseif ($this->operator === 'ENDS') {
|
||||||
$like_value = '%' . $this->value;
|
$like_value = '%' . $escaped_value;
|
||||||
} elseif ($this->operator === 'CONTAINS') {
|
} elseif ($this->operator === 'CONTAINS') {
|
||||||
$like_value = '%' . $this->value . '%';
|
$like_value = '%' . $escaped_value . '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($like_value !== null) {
|
if ($like_value !== null) {
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,8 @@ class PartSearchFilter implements FilterInterface
|
||||||
if ($this->regex) {
|
if ($this->regex) {
|
||||||
$queryBuilder->setParameter('search_query', $this->keyword);
|
$queryBuilder->setParameter('search_query', $this->keyword);
|
||||||
} else {
|
} else {
|
||||||
|
//Escape % and _ characters in the keyword
|
||||||
|
$this->keyword = str_replace(['%', '_'], ['\%', '\_'], $this->keyword);
|
||||||
$queryBuilder->setParameter('search_query', '%' . $this->keyword . '%');
|
$queryBuilder->setParameter('search_query', '%' . $this->keyword . '%');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@ class ILike extends FunctionNode
|
||||||
{
|
{
|
||||||
$platform = $sqlWalker->getConnection()->getDatabasePlatform();
|
$platform = $sqlWalker->getConnection()->getDatabasePlatform();
|
||||||
|
|
||||||
//
|
|
||||||
if ($platform instanceof AbstractMySQLPlatform || $platform instanceof SQLitePlatform) {
|
if ($platform instanceof AbstractMySQLPlatform || $platform instanceof SQLitePlatform) {
|
||||||
$operator = 'LIKE';
|
$operator = 'LIKE';
|
||||||
} elseif ($platform instanceof PostgreSQLPlatform) {
|
} 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.');
|
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);
|
$attachment = $this->convertFile($image, $image_type);
|
||||||
|
|
||||||
$attachments_grouped[$attachment->getName()][] = $attachment;
|
$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) . ')');
|
$attachment->setName($attachment->getName() . ' (' . (count($attachments_grouped[$attachment->getName()]) + 1) . ')');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,7 +236,7 @@ final class DTOtoEntityConverter
|
||||||
$attachment = $this->convertFile($datasheet, $datasheet_type);
|
$attachment = $this->convertFile($datasheet, $datasheet_type);
|
||||||
|
|
||||||
$attachments_grouped[$attachment->getName()][] = $attachment;
|
$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()])) . ')');
|
$attachment->setName($attachment->getName() . ' (' . (count($attachments_grouped[$attachment->getName()])) . ')');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,9 @@ use App\Services\Parts\PartLotWithdrawAddHelper;
|
||||||
/**
|
/**
|
||||||
* @see \App\Tests\Services\ProjectSystem\ProjectBuildHelperTest
|
* @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
|
public function getMaximumBuildableCount(Project $project): int
|
||||||
{
|
{
|
||||||
|
$bom_entries = $project->getBomEntries();
|
||||||
|
if ($bom_entries->isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
$maximum_buildable_count = PHP_INT_MAX;
|
$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)
|
//Skip BOM entries without a part (as we can not determine that)
|
||||||
if (!$bom_entry->isPartBomEntry()) {
|
if (!$bom_entry->isPartBomEntry()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
//The maximum buildable count for the whole project is the minimum of all BOM entries
|
//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));
|
$maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $maximum_buildable_count;
|
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.
|
* 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
|
* 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"))]
|
#[SettingsParameter(label: new TM("settings.behavior.sidebar.rootNodeRedirectsToNewEntity"))]
|
||||||
public bool $rootNodeRedirectsToNewEntity = false;
|
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
|
public function formatBytes(int $bytes, int $precision = 2): string
|
||||||
{
|
{
|
||||||
$size = ['B','kB','MB','GB','TB','PB','EB','ZB','YB'];
|
$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
|
//We use the real (10 based) SI prefix here
|
||||||
return sprintf("%.{$precision}f", $bytes / (1000 ** $factor)) . ' ' . @$size[$factor];
|
return sprintf("%.{$precision}f", $bytes / (1000 ** $factor)) . ' ' . @$size[$factor];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block card_content %}
|
{% 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 %}
|
{% import "components/projects.macro.html.twig" as project_macros %}
|
||||||
|
|
||||||
{% if project.status is not empty and project.status != "in_production" %}
|
{% if project.status is not empty and project.status != "in_production" %}
|
||||||
|
|
@ -17,8 +18,10 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="alert {% if can_build %}alert-success{% else %}alert-danger{% endif %}" role="alert">
|
<div class="alert {% if can_build %}alert-success{% elseif bom_empty%}alert-warning{% else %}alert-danger{% endif %}" role="alert">
|
||||||
{% if not can_build %}
|
{% 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>
|
<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>
|
<b>{% trans with {"%number_of_builds%": number_of_builds} %}project.builds.following_bom_entries_miss_instock_n{% endtrans %}</b>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
{% import "components/projects.macro.html.twig" as project_macros %}
|
||||||
|
|
||||||
|
|
@ -8,8 +9,10 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="alert mt-2 {% if can_build %}alert-success{% else %}alert-danger{% endif %}" role="alert">
|
<div class="alert mt-2 {% if can_build %}alert-success{% elseif bom_empty%}alert-warning{% else %}alert-danger{% endif %}" role="alert">
|
||||||
{% if not can_build %}
|
{% 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>
|
<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>
|
<b>{% trans %}project.builds.following_bom_entries_miss_instock{% endtrans %}</b>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
@ -19,7 +22,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h5><i class="fa-solid fa-circle-check fa-fw"></i> {% trans %}project.builds.build_possible{% endtrans %}</h5>
|
<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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -27,7 +30,7 @@
|
||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<div class="input-group mb-3">
|
<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) }}">
|
<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>
|
<button class="btn btn-outline-secondary" type="submit" id="button-addon2">{% trans %}project.build.btn_build{% endtrans %}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -114,4 +114,22 @@ class ProjectBuildHelperTest extends WebTestCase
|
||||||
$this->assertSame(0, $this->service->getMaximumBuildableCount($project));
|
$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">
|
<unit id="Ej2znKK" name="settings.system.localization.language_menu_entries.description">
|
||||||
<segment state="translated">
|
<segment state="translated">
|
||||||
<source>settings.system.localization.language_menu_entries.description</source>
|
<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>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
</file>
|
</file>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue