Merge remote-tracking branch 'upstream/master'

This commit is contained in:
d-buchmann 2025-08-13 12:17:02 +02:00
commit a8a72343ac
15 changed files with 1384 additions and 1126 deletions

View file

@ -47,6 +47,7 @@
PassEnv PROVIDER_REICHELT_ENABLED PROVIDER_REICHELT_CURRENCY PROVIDER_REICHELT_COUNTRY PROVIDER_REICHELT_LANGUAGE PROVIDER_REICHELT_INCLUDE_VAT PassEnv PROVIDER_REICHELT_ENABLED PROVIDER_REICHELT_CURRENCY PROVIDER_REICHELT_COUNTRY PROVIDER_REICHELT_LANGUAGE PROVIDER_REICHELT_INCLUDE_VAT
PassEnv PROVIDER_POLLIN_ENABLED PassEnv PROVIDER_POLLIN_ENABLED
PassEnv EDA_KICAD_CATEGORY_DEPTH PassEnv EDA_KICAD_CATEGORY_DEPTH
PassEnv SHOW_PART_IMAGE_OVERLAY
# For most configuration files from conf-available/, which are # For most configuration files from conf-available/, which are
# enabled or disabled at a global level, it is possible to # enabled or disabled at a global level, it is possible to

3
.env
View file

@ -164,6 +164,9 @@ FIXER_API_KEY=CHANGEME
# When this is empty the content of config/banner.md is used as banner # When this is empty the content of config/banner.md is used as banner
BANNER="" BANNER=""
# Enable the part image overlay which shows name and filename of the picture
SHOW_PART_IMAGE_OVERLAY=1
APP_ENV=prod APP_ENV=prod
APP_SECRET=a03498528f5a5fc089273ec9ae5b2849 APP_SECRET=a03498528f5a5fc089273ec9ae5b2849

View file

@ -1 +1 @@
2.0.0-dev 1.17.2

View file

@ -33,7 +33,10 @@ export default class extends Controller {
{ {
let value = ""; let value = "";
if (this.unitValue) { if (this.unitValue) {
value = "\\mathrm{" + this.inputTarget.value + "}"; //Escape percentage signs
value = this.inputTarget.value.replace(/%/g, '\\%');
value = "\\mathrm{" + value + "}";
} else { } else {
value = this.inputTarget.value; value = this.inputTarget.value;
} }

View file

@ -85,7 +85,9 @@ export default class extends Controller
tmp += '<span>' + katex.renderToString(data.symbol) + '</span>' tmp += '<span>' + katex.renderToString(data.symbol) + '</span>'
} }
if (data.unit) { if (data.unit) {
tmp += '<span class="ms-2">' + katex.renderToString('[' + data.unit + ']') + '</span>' let unit = data.unit.replace(/%/g, '\\%');
unit = "\\mathrm{" + unit + "}";
tmp += '<span class="ms-2">' + katex.renderToString('[' + unit + ']') + '</span>'
} }

1638
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@ twig:
available_themes: '%partdb.available_themes%' available_themes: '%partdb.available_themes%'
saml_enabled: '%partdb.saml.enabled%' saml_enabled: '%partdb.saml.enabled%'
part_preview_generator: '@App\Services\Attachments\PartPreviewGenerator' part_preview_generator: '@App\Services\Attachments\PartPreviewGenerator'
img_overlay: '%partdb.show_part_image_overlay%'
when@test: when@test:
twig: twig:

View file

@ -48,6 +48,7 @@ parameters:
# Miscellaneous # Miscellaneous
###################################################################################################################### ######################################################################################################################
partdb.demo_mode: '%env(bool:DEMO_MODE)%' # If set to true, all potentially dangerous things are disabled (like changing passwords of the own user) partdb.demo_mode: '%env(bool:DEMO_MODE)%' # If set to true, all potentially dangerous things are disabled (like changing passwords of the own user)
partdb.show_part_image_overlay: '%env(bool:SHOW_PART_IMAGE_OVERLAY)%' # If set to false, the filename overlay of the part image will be disabled
# Set the themes from which the user can choose from in the settings. # Set the themes from which the user can choose from in the settings.
# Themes commented here by default, are not really usable, because of display problems. Enable them at your own risk! # Themes commented here by default, are not really usable, because of display problems. Enable them at your own risk!

View file

@ -95,6 +95,8 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
particularly for securing and protecting various aspects of your application. It's a secret key that is used for particularly for securing and protecting various aspects of your application. It's a secret key that is used for
cryptographic operations and security measures (session management, CSRF protection, etc..). Therefore this cryptographic operations and security measures (session management, CSRF protection, etc..). Therefore this
value should be handled as confidential data and not shared publicly. value should be handled as confidential data and not shared publicly.
* `SHOW_PART_IMAGE_OVERLAY`: Set to 0 to disable the part image overlay, which appears if you hover over an image in the
part image gallery
### E-Mail settings ### E-Mail settings

View file

@ -217,7 +217,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
$str = ''; $str = '';
$bracket_opened = false; $bracket_opened = false;
if ($this->value_typical) { if ($this->value_typical !== null) {
$str .= $this->getValueTypicalWithUnit($latex_formatted); $str .= $this->getValueTypicalWithUnit($latex_formatted);
if ($this->value_min || $this->value_max) { if ($this->value_min || $this->value_max) {
$bracket_opened = true; $bracket_opened = true;
@ -225,11 +225,11 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
} }
} }
if ($this->value_max && $this->value_min) { if ($this->value_max !== null && $this->value_min !== null) {
$str .= $this->getValueMinWithUnit($latex_formatted).' ... '.$this->getValueMaxWithUnit($latex_formatted); $str .= $this->getValueMinWithUnit($latex_formatted).' ... '.$this->getValueMaxWithUnit($latex_formatted);
} elseif ($this->value_max) { } elseif ($this->value_max !== null) {
$str .= 'max. '.$this->getValueMaxWithUnit($latex_formatted); $str .= 'max. '.$this->getValueMaxWithUnit($latex_formatted);
} elseif ($this->value_min) { } elseif ($this->value_min !== null) {
$str .= 'min. '.$this->getValueMinWithUnit($latex_formatted); $str .= 'min. '.$this->getValueMinWithUnit($latex_formatted);
} }
@ -449,7 +449,10 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
if (!$with_latex) { if (!$with_latex) {
$unit = $this->unit; $unit = $this->unit;
} else { } else {
$unit = '$\mathrm{'.$this->unit.'}$'; //Escape the percentage sign for convenience (as latex uses it as comment and it is often used in units)
$escaped = preg_replace('/\\\\?%/', "\\\\%", $this->unit);
$unit = '$\mathrm{'.$escaped.'}$';
} }
return $str.' '.$unit; return $str.' '.$unit;

View file

@ -112,12 +112,12 @@ class AttachmentURLGenerator
/** /**
* Returns a URL to a thumbnail of the attachment file. * Returns a URL to a thumbnail of the attachment file.
* For external files the original URL is returned. * For external files the original URL is returned.
* @return string|null The URL or null if the attachment file is not existing * @return string|null The URL or null if the attachment file is not existing or is invalid
*/ */
public function getThumbnailURL(Attachment $attachment, string $filter_name = 'thumbnail_sm'): ?string public function getThumbnailURL(Attachment $attachment, string $filter_name = 'thumbnail_sm'): ?string
{ {
if (!$attachment->isPicture()) { if (!$attachment->isPicture()) {
throw new InvalidArgumentException('Thumbnail creation only works for picture attachments!'); return null;
} }
if (!$attachment->hasInternal()){ if (!$attachment->hasInternal()){

View file

@ -241,6 +241,49 @@ class KiCadHelper
$result["fields"]["Part-DB IPN"] = $this->createField($part->getIpn()); $result["fields"]["Part-DB IPN"] = $this->createField($part->getIpn());
} }
// Add supplier information from orderdetails (include obsolete orderdetails)
if ($part->getOrderdetails(false)->count() > 0) {
$supplierCounts = [];
foreach ($part->getOrderdetails(false) as $orderdetail) {
if ($orderdetail->getSupplier() !== null && $orderdetail->getSupplierPartNr() !== '') {
$supplierName = $orderdetail->getSupplier()->getName();
$supplierName .= " SPN"; // Append "SPN" to the supplier name to indicate Supplier Part Number
if (!isset($supplierCounts[$supplierName])) {
$supplierCounts[$supplierName] = 0;
}
$supplierCounts[$supplierName]++;
// Create field name with sequential number if more than one from same supplier (e.g. "Mouser", "Mouser 2", etc.)
$fieldName = $supplierCounts[$supplierName] > 1
? $supplierName . ' ' . $supplierCounts[$supplierName]
: $supplierName;
$result["fields"][$fieldName] = $this->createField($orderdetail->getSupplierPartNr());
}
}
}
//Add fields for KiCost:
if ($part->getManufacturer() !== null) {
$result["fields"]["manf"] = $this->createField($part->getManufacturer()->getName());
}
if ($part->getManufacturerProductNumber() !== "") {
$result['fields']['manf#'] = $this->createField($part->getManufacturerProductNumber());
}
//For each supplier, add a field with the supplier name and the supplier part number for KiCost
if ($part->getOrderdetails(false)->count() > 0) {
foreach ($part->getOrderdetails(false) as $orderdetail) {
if ($orderdetail->getSupplier() !== null && $orderdetail->getSupplierPartNr() !== '') {
$fieldName = mb_strtolower($orderdetail->getSupplier()->getName()) . '#';
$result["fields"][$fieldName] = $this->createField($orderdetail->getSupplierPartNr());
}
}
}
return $result; return $result;
} }

View file

@ -38,7 +38,6 @@ use App\Repository\StructuralDBElementRepository;
use App\Services\Cache\ElementCacheTagGenerator; use App\Services\Cache\ElementCacheTagGenerator;
use App\Services\Cache\UserCacheKeyGenerator; use App\Services\Cache\UserCacheKeyGenerator;
use App\Services\EntityURLGenerator; use App\Services\EntityURLGenerator;
use App\Settings\BehaviorSettings\SidebarSettings;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException; use InvalidArgumentException;
use RecursiveIteratorIterator; use RecursiveIteratorIterator;
@ -54,10 +53,6 @@ use function count;
*/ */
class TreeViewGenerator class TreeViewGenerator
{ {
private readonly bool $rootNodeExpandedByDefault;
private readonly bool $rootNodeEnabled;
public function __construct( public function __construct(
protected EntityURLGenerator $urlGenerator, protected EntityURLGenerator $urlGenerator,
protected EntityManagerInterface $em, protected EntityManagerInterface $em,
@ -66,10 +61,11 @@ class TreeViewGenerator
protected UserCacheKeyGenerator $keyGenerator, protected UserCacheKeyGenerator $keyGenerator,
protected TranslatorInterface $translator, protected TranslatorInterface $translator,
private readonly UrlGeneratorInterface $router, private readonly UrlGeneratorInterface $router,
private readonly SidebarSettings $sidebarSettings, protected bool $rootNodeExpandedByDefault,
protected bool $rootNodeEnabled,
//TODO: Make this configurable in the future
protected bool $rootNodeRedirectsToNewEntity = false,
) { ) {
$this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled;
$this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded;
} }
/** /**
@ -179,10 +175,7 @@ class TreeViewGenerator
} }
if (($mode === 'list_parts_root' || $mode === 'devices') && $this->rootNodeEnabled) { if (($mode === 'list_parts_root' || $mode === 'devices') && $this->rootNodeEnabled) {
//We show the root node as a link to the list of all parts $root_node = new TreeViewNode($this->entityClassToRootNodeString($class), $this->entityClassToRootNodeHref($class), $generic);
$show_all_parts_url = $this->router->generate('parts_show_all');
$root_node = new TreeViewNode($this->entityClassToRootNodeString($class), $show_all_parts_url, $generic);
$root_node->setExpanded($this->rootNodeExpandedByDefault); $root_node->setExpanded($this->rootNodeExpandedByDefault);
$root_node->setIcon($this->entityClassToRootNodeIcon($class)); $root_node->setIcon($this->entityClassToRootNodeIcon($class));
@ -192,6 +185,27 @@ class TreeViewGenerator
return array_merge($head, $generic); return array_merge($head, $generic);
} }
protected function entityClassToRootNodeHref(string $class): ?string
{
//If the root node should redirect to the new entity page, we return the URL for the new entity.
if ($this->rootNodeRedirectsToNewEntity) {
return match ($class) {
Category::class => $this->router->generate('category_new'),
StorageLocation::class => $this->router->generate('store_location_new'),
Footprint::class => $this->router->generate('footprint_new'),
Manufacturer::class => $this->router->generate('manufacturer_new'),
Supplier::class => $this->router->generate('supplier_new'),
Project::class => $this->router->generate('project_new'),
default => null,
};
}
return match ($class) {
Project::class => $this->router->generate('project_new'),
default => $this->router->generate('parts_show_all')
};
}
protected function entityClassToRootNodeString(string $class): string protected function entityClassToRootNodeString(string $class): string
{ {
return match ($class) { return match ($class) {

View file

@ -13,6 +13,7 @@
<div class="carousel-item {% if loop.first %}active{% endif %}"> <div class="carousel-item {% if loop.first %}active{% endif %}">
<a href="{{ entity_url(pic, 'file_view') }}" data-turbo="false" target="_blank" rel="noopener"> <a href="{{ entity_url(pic, 'file_view') }}" data-turbo="false" target="_blank" rel="noopener">
<img class="d-block w-100 img-fluid img-thumbnail bg-light part-info-image" src="{{ entity_url(pic, 'file_view') }}" alt=""> <img class="d-block w-100 img-fluid img-thumbnail bg-light part-info-image" src="{{ entity_url(pic, 'file_view') }}" alt="">
{% if img_overlay %}
<div class="mask"></div> <div class="mask"></div>
<div class="carousel-caption-hover"> <div class="carousel-caption-hover">
<div class="carousel-caption text-white"> <div class="carousel-caption text-white">
@ -21,6 +22,7 @@
<div>{{ entity_type_label(pic.element) }}</div> <div>{{ entity_type_label(pic.element) }}</div>
</div> </div>
</div> </div>
{% endif %}
</a> </a>
</div> </div>
{% endfor %} {% endfor %}

751
yarn.lock

File diff suppressed because it is too large Load diff