mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-03-16 20:28:19 +01:00
Compare commits
24 Commits
v2.8.1
...
update-kic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7217ea8521 | ||
|
|
343c078b7d | ||
|
|
37b98adc6e | ||
|
|
4f12fd7390 | ||
|
|
13b98cc0b1 | ||
|
|
7f8f5990a7 | ||
|
|
bcbbb1ecb9 | ||
|
|
8727d83097 | ||
|
|
70919d953a | ||
|
|
a722608ae8 | ||
|
|
12a760d27e | ||
|
|
b8d1414403 | ||
|
|
463d7b89f6 | ||
|
|
6e4d252617 | ||
|
|
3ed27f6c0f | ||
|
|
0d58262e19 | ||
|
|
db8881621c | ||
|
|
ceda91488c | ||
|
|
e84bae2807 | ||
|
|
e8d90487d2 | ||
|
|
598cf3ed80 | ||
|
|
30e3bc3153 | ||
|
|
f95a58087b | ||
|
|
83608fffcf |
5
.env
5
.env
@@ -71,6 +71,11 @@ DISABLE_WEB_UPDATES=1
|
||||
# Restoring backups is a destructive operation that could overwrite your database.
|
||||
DISABLE_BACKUP_RESTORE=1
|
||||
|
||||
# Disable backup download from the Update Manager UI (0=enabled, 1=disabled).
|
||||
# Backups contain sensitive data including password hashes and secrets.
|
||||
# When enabled, users must confirm their password before downloading.
|
||||
DISABLE_BACKUP_DOWNLOAD=1
|
||||
|
||||
###################################################################################
|
||||
# SAML Single sign on-settings
|
||||
###################################################################################
|
||||
|
||||
@@ -45,6 +45,7 @@ export default class extends Controller {
|
||||
maxItems: 1,
|
||||
createOnBlur: true,
|
||||
selectOnTab: true,
|
||||
clearAfterSelect: 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,
|
||||
|
||||
@@ -23,6 +23,8 @@ export default class extends Controller {
|
||||
valueField: "id",
|
||||
labelField: "name",
|
||||
dropdownParent: dropdownParent,
|
||||
selectOnTab: true,
|
||||
clearAfterSelect: true,
|
||||
preload: "focus",
|
||||
render: {
|
||||
item: (data, escape) => {
|
||||
|
||||
@@ -49,6 +49,7 @@ export default class extends Controller {
|
||||
selectOnTab: true,
|
||||
maxOptions: null,
|
||||
dropdownParent: dropdownParent,
|
||||
clearAfterSelect: true,
|
||||
|
||||
render: {
|
||||
item: this.renderItem.bind(this),
|
||||
|
||||
@@ -35,6 +35,8 @@ export default class extends Controller {
|
||||
maxItems: 1000,
|
||||
allowEmptyOption: true,
|
||||
dropdownParent: dropdownParent,
|
||||
selectOnTab: true,
|
||||
clearAfterSelect: true,
|
||||
plugins: ['remove_button'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import {Controller} from "@hotwired/stimulus";
|
||||
import {default as TreeController} from "./tree_controller";
|
||||
import {EVENT_INITIALIZED} from "@jbtronics/bs-treeview";
|
||||
|
||||
export default class extends TreeController {
|
||||
static targets = [ "tree", 'sourceText' ];
|
||||
@@ -40,6 +41,8 @@ export default class extends TreeController {
|
||||
//Check if we have a saved mode
|
||||
const stored_mode = localStorage.getItem(this._storage_key);
|
||||
|
||||
this._frame = this.element.dataset.frame || "content"; //By default, navigate in the content frame, if a frame is defined
|
||||
|
||||
//Use stored mode if possible, otherwise use default
|
||||
if(stored_mode) {
|
||||
try {
|
||||
@@ -55,6 +58,39 @@ export default class extends TreeController {
|
||||
|
||||
//Register an event listener which checks if the tree needs to be updated
|
||||
document.addEventListener('turbo:render', this.doUpdateIfNeeded.bind(this));
|
||||
|
||||
//Register an event listener, to check if we end up on a page we can highlight in the tree, if so then higlight it
|
||||
document.addEventListener('turbo:load', this._onTurboLoad.bind(this));
|
||||
//On initial page load the tree is not available yet, so do another check after the tree is initialized
|
||||
this.treeTarget.addEventListener(EVENT_INITIALIZED, (event) => {
|
||||
this.selectNodeWithURL(document.location)
|
||||
});
|
||||
}
|
||||
|
||||
_onTurboLoad(event) {
|
||||
this.selectNodeWithURL(event.detail.url);
|
||||
}
|
||||
|
||||
selectNodeWithURL(url) {
|
||||
//Get path from url
|
||||
const path = new URL(url).pathname;
|
||||
|
||||
if (!this._tree) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Unselect all nodes
|
||||
this._tree.unselectAll({silent: true, ignorePreventUnselect: true});
|
||||
|
||||
//Try to find a node with this path as data-path
|
||||
const nodes = this._tree.findNodes(path, "href");
|
||||
if (nodes.length !== 1) {
|
||||
return; //We can only work with exactly one node, if there are multiple nodes with the same path, we cannot know which one to select, so we do nothing
|
||||
}
|
||||
const node = nodes[0];
|
||||
|
||||
node.setSelected(true, {ignorePreventUnselect: true, silent: true});
|
||||
this._tree.revealNode(node);
|
||||
}
|
||||
|
||||
doUpdateIfNeeded()
|
||||
|
||||
@@ -56,6 +56,7 @@ export default class extends Controller {
|
||||
searchField: 'text',
|
||||
orderField: 'text',
|
||||
dropdownParent: dropdownParent,
|
||||
clearAfterSelect: 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',
|
||||
|
||||
@@ -58,6 +58,7 @@ export default class extends Controller {
|
||||
delimiter: "$$VERY_LONG_DELIMITER_THAT_SHOULD_NEVER_APPEAR$$",
|
||||
splitOn: null,
|
||||
dropdownParent: dropdownParent,
|
||||
clearAfterSelect: true,
|
||||
|
||||
searchField: [
|
||||
{field: "text", weight : 2},
|
||||
|
||||
@@ -49,6 +49,7 @@ export default class extends Controller {
|
||||
createOnBlur: true,
|
||||
create: true,
|
||||
dropdownParent: dropdownParent,
|
||||
clearAfterSelect: true,
|
||||
};
|
||||
|
||||
if(this.element.dataset.autocomplete) {
|
||||
|
||||
@@ -39,6 +39,8 @@ export default class extends Controller {
|
||||
*/
|
||||
_tree = null;
|
||||
|
||||
_frame = "frame";
|
||||
|
||||
connect() {
|
||||
const treeElement = this.treeTarget;
|
||||
if (!treeElement) {
|
||||
@@ -48,6 +50,7 @@ export default class extends Controller {
|
||||
|
||||
this._url = this.element.dataset.treeUrl;
|
||||
this._data = this.element.dataset.treeData;
|
||||
this._frame = this.element.dataset.frame || "content"; //By default, navigate in the content frame, if a frame is defined
|
||||
|
||||
if(this.element.dataset.treeShowTags === "true") {
|
||||
this._showTags = true;
|
||||
@@ -99,8 +102,7 @@ export default class extends Controller {
|
||||
onNodeSelected: (event) => {
|
||||
const node = event.detail.node;
|
||||
if (node.href) {
|
||||
window.Turbo.visit(node.href, {action: "advance"});
|
||||
this._registerURLWatcher(node);
|
||||
window.Turbo.visit(node.href, {action: "advance", frame: this._frame});
|
||||
}
|
||||
},
|
||||
}, [BS5Theme, BS53Theme, FAIconTheme]);
|
||||
@@ -110,41 +112,12 @@ export default class extends Controller {
|
||||
const treeView = event.detail.treeView;
|
||||
treeView.revealNode(treeView.getSelected());
|
||||
|
||||
//Add the url watcher to all selected nodes
|
||||
for (const node of treeView.getSelected()) {
|
||||
this._registerURLWatcher(node);
|
||||
}
|
||||
|
||||
//Add contextmenu event listener to the tree, which allows us to open the links in a new tab with a right click
|
||||
treeView.getTreeElement().addEventListener("contextmenu", this._onContextMenu.bind(this));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
_registerURLWatcher(node)
|
||||
{
|
||||
//Register a watcher for a location change, which will unselect the node, if the location changes
|
||||
const desired_url = node.href;
|
||||
|
||||
//Ensure that the node is unselected, if the location changes
|
||||
const unselectNode = () => {
|
||||
//Parse url so we can properly compare them
|
||||
const desired = new URL(node.href, window.location.origin);
|
||||
|
||||
//We only compare the pathname, because the hash and parameters should not matter
|
||||
if(window.location.pathname !== desired.pathname) {
|
||||
//The ignore parameter is important here, otherwise the node will not be unselected
|
||||
node.setSelected(false, {silent: true, ignorePreventUnselect: true});
|
||||
|
||||
//Unregister the watcher
|
||||
document.removeEventListener('turbo:load', unselectNode);
|
||||
}
|
||||
};
|
||||
|
||||
//Register the watcher via hotwire turbo
|
||||
//We must just load to have the new url in window.location
|
||||
document.addEventListener('turbo:load', unselectNode);
|
||||
}
|
||||
|
||||
_onContextMenu(event)
|
||||
{
|
||||
@@ -198,4 +171,4 @@ export default class extends Controller {
|
||||
return myResolve(this._data);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ export default class extends Controller
|
||||
searchField: "name",
|
||||
//labelField: "name",
|
||||
valueField: "name",
|
||||
clearAfterSelect: true,
|
||||
onItemAdd: this.onItemAdd.bind(this),
|
||||
render: {
|
||||
option: (data, escape) => {
|
||||
@@ -136,4 +137,4 @@ export default class extends Controller
|
||||
//Destroy the TomSelect instance
|
||||
this._tomSelect.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +177,11 @@
|
||||
"allow-contrib": false,
|
||||
"require": "7.4.*",
|
||||
"docker": true
|
||||
},
|
||||
"phpstan/extension-installer": {
|
||||
"ignore" : [
|
||||
"ekino/phpstan-banned-code"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
382
composer.lock
generated
382
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,11 @@ twig:
|
||||
saml_enabled: '%partdb.saml.enabled%'
|
||||
part_preview_generator: '@App\Services\Attachments\PartPreviewGenerator'
|
||||
|
||||
# Bootstrap grid classes used for horizontal form layouts
|
||||
col_label: 'col-sm-3 col-lg-2' # The column classes for form labels
|
||||
col_input: 'col-sm-9 col-lg-10' # The column classes for form input fields
|
||||
offset_label: 'offset-sm-3 offset-lg-2' # Offset classes for elements that should be aligned with the input fields (e.g., submit buttons)
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
||||
|
||||
@@ -144,6 +144,18 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
|
||||
* `ALLOW_EMAIL_PW_RESET`: Set this value to true, if you want to allow users to reset their password via an email
|
||||
notification. You have to configure the mail provider first before via the MAILER_DSN setting.
|
||||
|
||||
### Update manager settings
|
||||
* `DISABLE_WEB_UPDATES` (default `1`): Set this to 0 to enable web-based updates. When enabled, you can perform updates
|
||||
via the web interface in the update manager. This is disabled by default for security reasons, as it can be a risk if
|
||||
not used carefully. You can still use the CLI commands to perform updates, even when web updates are disabled.
|
||||
* `DISABLE_BACKUP_RESTORE` (default `1`): Set this to 0 to enable backup restore via the web interface. When enabled, you can
|
||||
restore backups via the web interface in the update manager. This is disabled by default for security reasons, as it can
|
||||
be a risk if not used carefully. You can still use the CLI commands to perform backup restores, even when web-based
|
||||
backup restore is disabled.
|
||||
* `DISABLE_BACKUP_DOWNLOAD` (default `1`): Set this to 0 to enable backup download via the web interface. When enabled, you can download backups via the web interface
|
||||
in the update manager. This is disabled by default for security reasons, as it can be a risk if not used carefully, as
|
||||
the downloads contain sensitive data like password hashes or secrets.
|
||||
|
||||
### Table related settings
|
||||
|
||||
* `TABLE_DEFAULT_PAGE_SIZE`: The default page size for tables. This is the number of rows which are shown per page. Set
|
||||
|
||||
73
migrations/Version20260307204859.php
Normal file
73
migrations/Version20260307204859.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Migration\AbstractMultiPlatformMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260307204859 extends AbstractMultiPlatformMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Increase the length of the vendor_barcode field in part_lots to 1000 characters and update the index accordingly';
|
||||
}
|
||||
|
||||
public function mySQLUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE part_lots DROP INDEX part_lots_idx_barcode, ADD INDEX part_lots_idx_barcode (vendor_barcode(100))');
|
||||
$this->addSql('ALTER TABLE part_lots CHANGE vendor_barcode vendor_barcode LONGTEXT DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function mySQLDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE part_lots DROP INDEX part_lots_idx_barcode, ADD INDEX part_lots_idx_barcode (vendor_barcode)');
|
||||
$this->addSql('ALTER TABLE part_lots CHANGE vendor_barcode vendor_barcode VARCHAR(255) DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function sqLiteUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__part_lots AS SELECT id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added, vendor_barcode, last_stocktake_at FROM part_lots');
|
||||
$this->addSql('DROP TABLE part_lots');
|
||||
$this->addSql('CREATE TABLE part_lots (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_store_location INTEGER DEFAULT NULL, id_part INTEGER NOT NULL, id_owner INTEGER DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, expiration_date DATETIME DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, vendor_barcode CLOB DEFAULT NULL, last_stocktake_at DATETIME DEFAULT NULL, CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES storelocations (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES parts (id) ON UPDATE NO ACTION ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO part_lots (id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added, vendor_barcode, last_stocktake_at) SELECT id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added, vendor_barcode, last_stocktake_at FROM __temp__part_lots');
|
||||
$this->addSql('DROP TABLE __temp__part_lots');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)');
|
||||
$this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)');
|
||||
$this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)');
|
||||
$this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)');
|
||||
}
|
||||
|
||||
public function sqLiteDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__part_lots AS SELECT id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_stocktake_at, last_modified, datetime_added, id_store_location, id_part, id_owner FROM part_lots');
|
||||
$this->addSql('DROP TABLE part_lots');
|
||||
$this->addSql('CREATE TABLE part_lots (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, expiration_date DATETIME DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, vendor_barcode VARCHAR(255) DEFAULT NULL, last_stocktake_at DATETIME DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, id_store_location INTEGER DEFAULT NULL, id_part INTEGER NOT NULL, id_owner INTEGER DEFAULT NULL, CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES "storelocations" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO part_lots (id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_stocktake_at, last_modified, datetime_added, id_store_location, id_part, id_owner) SELECT id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_stocktake_at, last_modified, datetime_added, id_store_location, id_part, id_owner FROM __temp__part_lots');
|
||||
$this->addSql('DROP TABLE __temp__part_lots');
|
||||
$this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)');
|
||||
$this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)');
|
||||
$this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)');
|
||||
}
|
||||
|
||||
public function postgreSQLUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX part_lots_idx_barcode');
|
||||
$this->addSql('ALTER TABLE part_lots ALTER vendor_barcode TYPE TEXT');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)');
|
||||
}
|
||||
|
||||
public function postgreSQLDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX part_lots_idx_barcode');
|
||||
$this->addSql('ALTER TABLE part_lots ALTER vendor_barcode TYPE VARCHAR(255)');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)');
|
||||
}
|
||||
}
|
||||
86
phpstan.banned_code.neon
Normal file
86
phpstan.banned_code.neon
Normal file
@@ -0,0 +1,86 @@
|
||||
# Manually configure ekino/phpstan-banned-code to detect usage of echo, eval, die/exit, print, shell execution and a set of functions that should not be used in production code.
|
||||
|
||||
parametersSchema:
|
||||
banned_code: structure([
|
||||
nodes: listOf(structure([
|
||||
type: string()
|
||||
functions: schema(listOf(string()), nullable())
|
||||
]))
|
||||
use_from_tests: bool()
|
||||
non_ignorable: bool()
|
||||
])
|
||||
|
||||
parameters:
|
||||
banned_code:
|
||||
nodes:
|
||||
# enable detection of echo
|
||||
-
|
||||
type: Stmt_Echo
|
||||
functions: null
|
||||
|
||||
# enable detection of eval
|
||||
-
|
||||
type: Expr_Eval
|
||||
functions: null
|
||||
|
||||
# enable detection of die/exit
|
||||
-
|
||||
type: Expr_Exit
|
||||
functions: null
|
||||
|
||||
# enable detection of a set of functions
|
||||
-
|
||||
type: Expr_FuncCall
|
||||
functions:
|
||||
- dd
|
||||
- debug_backtrace
|
||||
- dump
|
||||
- exec
|
||||
- passthru
|
||||
- phpinfo
|
||||
- print_r
|
||||
- proc_open
|
||||
- shell_exec
|
||||
- system
|
||||
- var_dump
|
||||
|
||||
# enable detection of print statements
|
||||
-
|
||||
type: Expr_Print
|
||||
functions: null
|
||||
|
||||
# enable detection of shell execution by backticks
|
||||
-
|
||||
type: Expr_ShellExec
|
||||
functions: null
|
||||
|
||||
# enable detection of empty()
|
||||
#-
|
||||
# type: Expr_Empty
|
||||
# functions: null
|
||||
|
||||
# enable detection of `use Tests\Foo\Bar` in a non-test file
|
||||
use_from_tests: true
|
||||
|
||||
# when true, errors cannot be excluded
|
||||
non_ignorable: false
|
||||
|
||||
services:
|
||||
-
|
||||
class: Ekino\PHPStanBannedCode\Rules\BannedNodesRule
|
||||
tags:
|
||||
- phpstan.rules.rule
|
||||
arguments:
|
||||
- '%banned_code.nodes%'
|
||||
|
||||
-
|
||||
class: Ekino\PHPStanBannedCode\Rules\BannedUseTestRule
|
||||
tags:
|
||||
- phpstan.rules.rule
|
||||
arguments:
|
||||
- '%banned_code.use_from_tests%'
|
||||
|
||||
-
|
||||
class: Ekino\PHPStanBannedCode\Rules\BannedNodesErrorBuilder
|
||||
arguments:
|
||||
- '%banned_code.non_ignorable%'
|
||||
@@ -1,3 +1,6 @@
|
||||
includes:
|
||||
- phpstan.banned_code.neon
|
||||
|
||||
parameters:
|
||||
|
||||
level: 5
|
||||
@@ -6,9 +9,6 @@ parameters:
|
||||
- src
|
||||
# - tests
|
||||
|
||||
banned_code:
|
||||
non_ignorable: false # Allow to ignore some banned code
|
||||
|
||||
excludePaths:
|
||||
- src/DataTables/Adapter/*
|
||||
- src/Configuration/*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated on Tue Mar 3 14:26:21 UTC 2026
|
||||
# Generated on Mon Mar 9 04:23:25 UTC 2026
|
||||
# This file contains all footprints available in the offical KiCAD library
|
||||
Audio_Module:Reverb_BTDR-1H
|
||||
Audio_Module:Reverb_BTDR-1V
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated on Tue Mar 3 14:27:05 UTC 2026
|
||||
# Generated on Mon Mar 9 04:24:12 UTC 2026
|
||||
# This file contains all symbols available in the offical KiCAD library
|
||||
4xxx:14528
|
||||
4xxx:14529
|
||||
@@ -22324,6 +22324,7 @@ Transistor_FET:PSMN5R2-60YL
|
||||
Transistor_FET:QM6006D
|
||||
Transistor_FET:QM6015D
|
||||
Transistor_FET:Q_Dual_NMOS_G1S2G2D2S1D1
|
||||
Transistor_FET:Q_Dual_NMOS_PMOS_G1S2G2D2S1D1
|
||||
Transistor_FET:Q_Dual_NMOS_S1G1D2S2G2D1
|
||||
Transistor_FET:Q_Dual_NMOS_S1G1S2G2D2D1
|
||||
Transistor_FET:Q_Dual_NMOS_S1G1S2G2D2D2D1D1
|
||||
|
||||
@@ -290,6 +290,23 @@ final class PartController extends AbstractController
|
||||
$this->addFlash('warning', t("part.create_from_info_provider.no_category_yet"));
|
||||
}
|
||||
|
||||
$lotAmount = $request->query->get('lotAmount');
|
||||
$lotName = $request->query->get('lotName');
|
||||
$lotUserBarcode = $request->query->get('lotUserBarcode');
|
||||
|
||||
if ($lotAmount !== null || $lotName !== null || $lotUserBarcode !== null) {
|
||||
$partLot = new PartLot();
|
||||
$partLot->setAmount($lotAmount !== null ? (float)$lotAmount : 0);
|
||||
$partLot->setDescription($lotName !== null ? (string)$lotName : '');
|
||||
$partLot->setUserBarcode($lotUserBarcode !== null ? (string)$lotUserBarcode : '');
|
||||
|
||||
$new_part->addPartLot($partLot);
|
||||
|
||||
$this->addFlash('notice', t('part.create_from_info_provider.lot_filled_from_barcode'));
|
||||
|
||||
}
|
||||
|
||||
|
||||
return $this->renderPartForm('new', $request, $new_part, [
|
||||
'info_provider_dto' => $dto,
|
||||
]);
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace App\Controller;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Events\SecurityEvent;
|
||||
use App\Events\SecurityEvents;
|
||||
use App\Form\Security\LoginFormType;
|
||||
use App\Services\UserSystem\PasswordResetManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Gregwar\CaptchaBundle\Type\CaptchaType;
|
||||
@@ -61,7 +62,12 @@ class SecurityController extends AbstractController
|
||||
// last username entered by the user
|
||||
$lastUsername = $authenticationUtils->getLastUsername();
|
||||
|
||||
$form = $this->createForm(LoginFormType::class, [
|
||||
'_username' => $lastUsername,
|
||||
]);
|
||||
|
||||
return $this->render('security/login.html.twig', [
|
||||
'form' => $form,
|
||||
'last_username' => $lastUsername,
|
||||
'error' => $error,
|
||||
]);
|
||||
|
||||
@@ -23,16 +23,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Services\System\BackupManager;
|
||||
use App\Services\System\InstallationTypeDetector;
|
||||
use App\Services\System\UpdateChecker;
|
||||
use App\Services\System\UpdateExecutor;
|
||||
use Shivas\VersioningBundle\Service\VersionManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
/**
|
||||
@@ -49,10 +54,14 @@ class UpdateManagerController extends AbstractController
|
||||
private readonly UpdateExecutor $updateExecutor,
|
||||
private readonly VersionManagerInterface $versionManager,
|
||||
private readonly BackupManager $backupManager,
|
||||
private readonly InstallationTypeDetector $installationTypeDetector,
|
||||
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||
#[Autowire(env: 'bool:DISABLE_WEB_UPDATES')]
|
||||
private readonly bool $webUpdatesDisabled = false,
|
||||
#[Autowire(env: 'bool:DISABLE_BACKUP_RESTORE')]
|
||||
private readonly bool $backupRestoreDisabled = false,
|
||||
#[Autowire(env: 'bool:DISABLE_BACKUP_DOWNLOAD')]
|
||||
private readonly bool $backupDownloadDisabled = false,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -76,6 +85,16 @@ class UpdateManagerController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if backup download is disabled and throw exception if so.
|
||||
*/
|
||||
private function denyIfBackupDownloadDisabled(): void
|
||||
{
|
||||
if ($this->backupDownloadDisabled) {
|
||||
throw new AccessDeniedHttpException('Backup download is disabled by server configuration.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main update manager page.
|
||||
*/
|
||||
@@ -101,6 +120,8 @@ class UpdateManagerController extends AbstractController
|
||||
'backups' => $this->backupManager->getBackups(),
|
||||
'web_updates_disabled' => $this->webUpdatesDisabled,
|
||||
'backup_restore_disabled' => $this->backupRestoreDisabled,
|
||||
'backup_download_disabled' => $this->backupDownloadDisabled,
|
||||
'is_docker' => $this->installationTypeDetector->isDocker(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -206,6 +227,7 @@ class UpdateManagerController extends AbstractController
|
||||
#[Route('/start', name: 'admin_update_manager_start', methods: ['POST'])]
|
||||
public function startUpdate(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
||||
$this->denyIfWebUpdatesDisabled();
|
||||
|
||||
@@ -314,12 +336,126 @@ class UpdateManagerController extends AbstractController
|
||||
return $this->json($details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a manual backup.
|
||||
*/
|
||||
#[Route('/backup', name: 'admin_update_manager_backup', methods: ['POST'])]
|
||||
public function createBackup(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
||||
|
||||
if (!$this->isCsrfTokenValid('update_manager_backup', $request->request->get('_token'))) {
|
||||
$this->addFlash('error', 'Invalid CSRF token.');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
if ($this->updateExecutor->isLocked()) {
|
||||
$this->addFlash('error', 'Cannot create backup while an update is in progress.');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->backupManager->createBackup(null, 'manual');
|
||||
$this->addFlash('success', 'update_manager.backup.created');
|
||||
} catch (\Exception $e) {
|
||||
$this->addFlash('error', 'Backup failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a backup file.
|
||||
*/
|
||||
#[Route('/backup/delete', name: 'admin_update_manager_backup_delete', methods: ['POST'])]
|
||||
public function deleteBackup(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
||||
|
||||
if (!$this->isCsrfTokenValid('update_manager_delete', $request->request->get('_token'))) {
|
||||
$this->addFlash('error', 'Invalid CSRF token.');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
$filename = $request->request->get('filename');
|
||||
if ($filename && $this->backupManager->deleteBackup($filename)) {
|
||||
$this->addFlash('success', 'update_manager.backup.deleted');
|
||||
} else {
|
||||
$this->addFlash('error', 'update_manager.backup.delete_error');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an update log file.
|
||||
*/
|
||||
#[Route('/log/delete', name: 'admin_update_manager_log_delete', methods: ['POST'])]
|
||||
public function deleteLog(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
||||
|
||||
if (!$this->isCsrfTokenValid('update_manager_delete', $request->request->get('_token'))) {
|
||||
$this->addFlash('error', 'Invalid CSRF token.');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
$filename = $request->request->get('filename');
|
||||
if ($filename && $this->updateExecutor->deleteLog($filename)) {
|
||||
$this->addFlash('success', 'update_manager.log.deleted');
|
||||
} else {
|
||||
$this->addFlash('error', 'update_manager.log.delete_error');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a backup file.
|
||||
* Requires password confirmation as backups contain sensitive data (password hashes, secrets, etc.).
|
||||
*/
|
||||
#[Route('/backup/download', name: 'admin_update_manager_backup_download', methods: ['POST'])]
|
||||
public function downloadBackup(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
||||
$this->denyIfBackupDownloadDisabled();
|
||||
|
||||
if (!$this->isCsrfTokenValid('update_manager_download', $request->request->get('_token'))) {
|
||||
$this->addFlash('error', 'Invalid CSRF token.');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
// Verify password
|
||||
$password = $request->request->get('password', '');
|
||||
$user = $this->getUser();
|
||||
if (!$user instanceof User || !$this->passwordHasher->isPasswordValid($user, $password)) {
|
||||
$this->addFlash('error', 'update_manager.backup.download.invalid_password');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
$filename = $request->request->get('filename', '');
|
||||
$details = $this->backupManager->getBackupDetails($filename);
|
||||
if (!$details) {
|
||||
throw $this->createNotFoundException('Backup not found');
|
||||
}
|
||||
|
||||
$response = new BinaryFileResponse($details['path']);
|
||||
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $details['file']);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore from a backup.
|
||||
*/
|
||||
#[Route('/restore', name: 'admin_update_manager_restore', methods: ['POST'])]
|
||||
public function restore(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
||||
$this->denyIfBackupRestoreDisabled();
|
||||
|
||||
|
||||
@@ -23,11 +23,13 @@ declare(strict_types=1);
|
||||
namespace App\DataTables;
|
||||
|
||||
use App\DataTables\Column\EntityColumn;
|
||||
use App\DataTables\Column\EnumColumn;
|
||||
use App\DataTables\Column\LocaleDateTimeColumn;
|
||||
use App\DataTables\Column\MarkdownColumn;
|
||||
use App\DataTables\Helpers\PartDataTableHelper;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\ManufacturingStatus;
|
||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||
use App\Services\ElementTypeNameGenerator;
|
||||
use App\Services\EntityURLGenerator;
|
||||
@@ -145,6 +147,19 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||
'orderField' => 'NATSORT(manufacturer.name)',
|
||||
])
|
||||
|
||||
->add('manufacturing_status', EnumColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.manufacturingStatus'),
|
||||
'data' => static fn(ProjectBOMEntry $context): ?ManufacturingStatus => $context->getPart()?->getManufacturingStatus(),
|
||||
'class' => ManufacturingStatus::class,
|
||||
'render' => function (?ManufacturingStatus $status, ProjectBOMEntry $context): string {
|
||||
if ($status === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->translator->trans($status->toTranslationKey());
|
||||
},
|
||||
])
|
||||
|
||||
->add('mountnames', TextColumn::class, [
|
||||
'label' => 'project.bom.mountnames',
|
||||
'render' => function ($value, ProjectBOMEntry $context) {
|
||||
|
||||
@@ -66,7 +66,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
#[ORM\Table(name: 'part_lots')]
|
||||
#[ORM\Index(columns: ['instock_unknown', 'expiration_date', 'id_part'], name: 'part_lots_idx_instock_un_expiration_id_part')]
|
||||
#[ORM\Index(columns: ['needs_refill'], name: 'part_lots_idx_needs_refill')]
|
||||
#[ORM\Index(columns: ['vendor_barcode'], name: 'part_lots_idx_barcode')]
|
||||
#[ORM\Index(name: 'part_lots_idx_barcode', columns: ['vendor_barcode'], options: ['lengths' => [100]])]
|
||||
#[ValidPartLot]
|
||||
#[UniqueEntity(['user_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')]
|
||||
#[ApiResource(
|
||||
@@ -81,7 +81,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
denormalizationContext: ['groups' => ['part_lot:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
|
||||
)]
|
||||
#[ApiFilter(PropertyFilter::class)]
|
||||
#[ApiFilter(LikeFilter::class, properties: ["description", "comment"])]
|
||||
#[ApiFilter(LikeFilter::class, properties: ["description", "comment", "user_barcode"])]
|
||||
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
||||
#[ApiFilter(BooleanFilter::class, properties: ['instock_unknown', 'needs_refill'])]
|
||||
#[ApiFilter(RangeFilter::class, properties: ['amount'])]
|
||||
@@ -166,9 +166,8 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
|
||||
/**
|
||||
* @var string|null The content of the barcode of this part lot (e.g. a barcode on the package put by the vendor)
|
||||
*/
|
||||
#[ORM\Column(name: "vendor_barcode", type: Types::STRING, nullable: true)]
|
||||
#[ORM\Column(name: "vendor_barcode", type: Types::TEXT, nullable: true)]
|
||||
#[Groups(['part_lot:read', 'part_lot:write'])]
|
||||
#[Length(max: 255)]
|
||||
protected ?string $user_barcode = null;
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,7 +45,7 @@ final class TogglePasswordTypeExtension extends AbstractTypeExtension
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'toggle' => false,
|
||||
'toggle' => true,
|
||||
'hidden_label' => new TranslatableMessage('password_toggle.hide'),
|
||||
'visible_label' => new TranslatableMessage('password_toggle.show'),
|
||||
'hidden_icon' => 'Default',
|
||||
|
||||
83
src/Form/Security/LoginFormType.php
Normal file
83
src/Form/Security/LoginFormType.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 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\Form\Security;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
use function Symfony\Component\Translation\t;
|
||||
|
||||
class LoginFormType extends AbstractType
|
||||
{
|
||||
public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('_username', TextType::class, [
|
||||
'label' => t('login.username.label'),
|
||||
'attr' => [
|
||||
'autofocus' => 'autofocus',
|
||||
'autocomplete' => 'username',
|
||||
'placeholder' => t('login.username.placeholder'),
|
||||
]
|
||||
])
|
||||
->add('_password', PasswordType::class, [
|
||||
'label' => t('login.password.label'),
|
||||
'attr' => [
|
||||
'autocomplete' => 'current-password',
|
||||
'placeholder' => t('login.password.placeholder'),
|
||||
]
|
||||
])
|
||||
->add('_remember_me', CheckboxType::class, [
|
||||
'label' => t('login.rememberme'),
|
||||
'required' => false,
|
||||
])
|
||||
->add('submit', \Symfony\Component\Form\Extension\Core\Type\SubmitType::class, [
|
||||
'label' => t('login.btn'),
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
// This ensures CSRF protection is active for the login
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_csrf_token',
|
||||
'csrf_token_id' => 'authenticate',
|
||||
'attr' => [
|
||||
'data-turbo' => 'false', // Disable Turbo for the login form to ensure proper redirection after login
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
// This removes the "login_form_" prefix from field names
|
||||
// so that Security can find "_username" directly.
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,8 @@ class StructuralElementDenormalizer implements DenormalizerInterface, Denormaliz
|
||||
|
||||
private const ALREADY_CALLED = 'STRUCTURAL_DENORMALIZER_ALREADY_CALLED';
|
||||
|
||||
private const PARENT_ELEMENT = 'STRUCTURAL_DENORMALIZER_PARENT_ELEMENT';
|
||||
|
||||
private array $object_cache = [];
|
||||
|
||||
public function __construct(
|
||||
@@ -89,37 +91,59 @@ class StructuralElementDenormalizer implements DenormalizerInterface, Denormaliz
|
||||
|
||||
$context[self::ALREADY_CALLED][] = $data;
|
||||
|
||||
//In the first step, denormalize without children
|
||||
$context_without_children = $context;
|
||||
$context_without_children['groups'] = array_filter(
|
||||
$context_without_children['groups'] ?? [],
|
||||
static fn($group) => $group !== 'include_children',
|
||||
);
|
||||
//Also unset any parent element, to avoid infinite loops. We will set the parent element in the next step, when we denormalize the children
|
||||
unset($context_without_children[self::PARENT_ELEMENT]);
|
||||
/** @var AbstractStructuralDBElement $entity */
|
||||
$entity = $this->denormalizer->denormalize($data, $type, $format, $context_without_children);
|
||||
|
||||
/** @var AbstractStructuralDBElement $deserialized_entity */
|
||||
$deserialized_entity = $this->denormalizer->denormalize($data, $type, $format, $context);
|
||||
//Assign the parent element to the denormalized entity, so it can be used in the denormalization of the children (e.g. for path generation)
|
||||
if (isset($context[self::PARENT_ELEMENT]) && $context[self::PARENT_ELEMENT] instanceof $entity && $entity->getID() === null) {
|
||||
$entity->setParent($context[self::PARENT_ELEMENT]);
|
||||
}
|
||||
|
||||
//Check if we already have the entity in the database (via path)
|
||||
/** @var StructuralDBElementRepository<T> $repo */
|
||||
$repo = $this->entityManager->getRepository($type);
|
||||
|
||||
$path = $deserialized_entity->getFullPath(AbstractStructuralDBElement::PATH_DELIMITER_ARROW);
|
||||
$path = $entity->getFullPath(AbstractStructuralDBElement::PATH_DELIMITER_ARROW);
|
||||
$db_elements = $repo->getEntityByPath($path, AbstractStructuralDBElement::PATH_DELIMITER_ARROW);
|
||||
if ($db_elements !== []) {
|
||||
//We already have the entity in the database, so we can return it
|
||||
return end($db_elements);
|
||||
$entity = end($db_elements);
|
||||
}
|
||||
|
||||
|
||||
//Check if we have created the entity in this request before (so we don't create multiple entities for the same path)
|
||||
//Entities get saved in the cache by type and path
|
||||
//We use a different cache for this then the objects created by a string value (saved in repo). However, that should not be a problem
|
||||
//unless the user data has mixed structure between json data and a string path
|
||||
//unless the user data has mixed structure between JSON data and a string path
|
||||
if (isset($this->object_cache[$type][$path])) {
|
||||
return $this->object_cache[$type][$path];
|
||||
$entity = $this->object_cache[$type][$path];
|
||||
} else {
|
||||
//Save the entity in the cache
|
||||
$this->object_cache[$type][$path] = $entity;
|
||||
}
|
||||
|
||||
//Save the entity in the cache
|
||||
$this->object_cache[$type][$path] = $deserialized_entity;
|
||||
//In the next step we can denormalize the children, and add our children to the entity.
|
||||
if (in_array('include_children', $context['groups'], true) && isset($data['children']) && is_array($data['children'])) {
|
||||
foreach ($data['children'] as $child_data) {
|
||||
$child_entity = $this->denormalize($child_data, $type, $format, array_merge($context, [self::PARENT_ELEMENT => $entity]));
|
||||
if ($child_entity !== null && !$entity->getChildren()->contains($child_entity)) {
|
||||
$entity->addChild($child_entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//We don't have the entity in the database, so we have to persist it
|
||||
$this->entityManager->persist($deserialized_entity);
|
||||
$this->entityManager->persist($entity);
|
||||
|
||||
return $deserialized_entity;
|
||||
return $entity;
|
||||
}
|
||||
|
||||
public function getSupportedTypes(?string $format): array
|
||||
|
||||
@@ -202,6 +202,7 @@ class KiCadHelper
|
||||
"exclude_from_bom" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromBom() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromBom() ?? false),
|
||||
"exclude_from_board" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromBoard() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromBoard() ?? false),
|
||||
"exclude_from_sim" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromSim() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromSim() ?? false),
|
||||
"description" => $part->getDescription(),
|
||||
"fields" => []
|
||||
];
|
||||
|
||||
|
||||
@@ -219,11 +219,6 @@ class EntityImporter
|
||||
$entities = [$entities];
|
||||
}
|
||||
|
||||
//The serializer has only set the children attributes. We also have to change the parent value (the real value in DB)
|
||||
if ($entities[0] instanceof AbstractStructuralDBElement) {
|
||||
$this->correctParentEntites($entities, null);
|
||||
}
|
||||
|
||||
//Set the parent of the imported elements to the given options
|
||||
foreach ($entities as $entity) {
|
||||
if ($entity instanceof AbstractStructuralDBElement) {
|
||||
@@ -297,6 +292,14 @@ class EntityImporter
|
||||
return $resolver;
|
||||
}
|
||||
|
||||
private function persistRecursively(AbstractStructuralDBElement $entity): void
|
||||
{
|
||||
$this->em->persist($entity);
|
||||
foreach ($entity->getChildren() as $child) {
|
||||
$this->persistRecursively($child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method deserializes the given file and writes the entities to the database (and flush the db).
|
||||
* The imported elements will be checked (validated) before written to database.
|
||||
@@ -322,7 +325,11 @@ class EntityImporter
|
||||
|
||||
//Iterate over each $entity write it to DB (the invalid entities were already filtered out).
|
||||
foreach ($entities as $entity) {
|
||||
$this->em->persist($entity);
|
||||
if ($entity instanceof AbstractStructuralDBElement) {
|
||||
$this->persistRecursively($entity);
|
||||
} else {
|
||||
$this->em->persist($entity);
|
||||
}
|
||||
}
|
||||
|
||||
//Save changes to database, when no error happened, or we should continue on error.
|
||||
@@ -400,7 +407,7 @@ class EntityImporter
|
||||
*
|
||||
* @param File $file The Excel file to convert
|
||||
* @param string $delimiter The CSV delimiter to use
|
||||
*
|
||||
*
|
||||
* @return string The CSV data as string
|
||||
*/
|
||||
protected function convertExcelToCsv(File $file, string $delimiter = ';'): string
|
||||
@@ -421,7 +428,7 @@ class EntityImporter
|
||||
]);
|
||||
|
||||
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
|
||||
|
||||
|
||||
for ($row = 1; $row <= $highestRow; $row++) {
|
||||
$rowData = [];
|
||||
|
||||
@@ -431,7 +438,7 @@ class EntityImporter
|
||||
try {
|
||||
$cellValue = $worksheet->getCell("{$col}{$row}")->getCalculatedValue();
|
||||
$rowData[] = $cellValue ?? '';
|
||||
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('Error reading cell value', [
|
||||
'cell' => "{$col}{$row}",
|
||||
@@ -484,21 +491,4 @@ class EntityImporter
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This functions corrects the parent setting based on the children value of the parent.
|
||||
*
|
||||
* @param iterable $entities the list of entities that should be fixed
|
||||
* @param AbstractStructuralDBElement|null $parent the parent, to which the entity should be set
|
||||
*/
|
||||
protected function correctParentEntites(iterable $entities, ?AbstractStructuralDBElement $parent = null): void
|
||||
{
|
||||
foreach ($entities as $entity) {
|
||||
/** @var AbstractStructuralDBElement $entity */
|
||||
$entity->setParent($parent);
|
||||
//Do the same for the children of entity
|
||||
$this->correctParentEntites($entity->getChildren(), $entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,8 @@ final readonly class BarcodeScanResultHandler
|
||||
throw InfoProviderNotActiveException::fromProvider($provider);
|
||||
}
|
||||
|
||||
return $this->urlGenerator->generate('info_providers_create_part', ['providerKey' => $infos['providerKey'], 'providerId' => $infos['providerId']]);
|
||||
//So far we can just copy over our provider info array to the URL parameters:
|
||||
return $this->urlGenerator->generate('info_providers_create_part', $infos);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,7 +147,7 @@ final readonly class BarcodeScanResultHandler
|
||||
|
||||
if ($barcodeScan instanceof AmazonBarcodeScanResult) {
|
||||
return $this->em->getRepository(Part::class)->getPartByProviderInfo($barcodeScan->asin)
|
||||
?? $this->em->getRepository(Part::class)->getPartBySPN($barcodeScan->asin);
|
||||
?? $this->em->getRepository(Part::class)->getPartBySPN($barcodeScan->asin);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -246,7 +247,7 @@ final readonly class BarcodeScanResultHandler
|
||||
* Returns null if no provider information could be extracted from the scan result, or if the scan result type is unknown and cannot be handled by this function.
|
||||
* It is not necessarily checked that the provider is active, or that the result actually exists on the provider side.
|
||||
* @param BarcodeScanResultInterface $scanResult
|
||||
* @return array{providerKey: string, providerId: string}|null
|
||||
* @return array{providerKey: string, providerId: string, lotAmount?: float|int, lotName?: string, lotUserBarcode?: string}|null
|
||||
* @throws InfoProviderNotActiveException If the scan result contains information for a provider which is currently not active in the system
|
||||
*/
|
||||
public function getCreateInfos(BarcodeScanResultInterface $scanResult): ?array
|
||||
@@ -256,6 +257,9 @@ final readonly class BarcodeScanResultHandler
|
||||
return [
|
||||
'providerKey' => 'lcsc',
|
||||
'providerId' => $scanResult->lcscCode,
|
||||
'lotAmount' => $scanResult->quantity,
|
||||
'lotName' => $scanResult->orderNumber ?? $scanResult->pickBatchNumber,
|
||||
'lotUserBarcode' => $scanResult->rawInput,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -276,7 +280,7 @@ final readonly class BarcodeScanResultHandler
|
||||
|
||||
/**
|
||||
* @param EIGP114BarcodeScanResult $scanResult
|
||||
* @return array{providerKey: string, providerId: string}|null
|
||||
* * @return array{providerKey: string, providerId: string, lotAmount?: float|int, lotName?: string, lotUserBarcode?: string}|null
|
||||
*/
|
||||
private function getCreationInfoForEIGP114(EIGP114BarcodeScanResult $scanResult): ?array
|
||||
{
|
||||
@@ -285,23 +289,26 @@ final readonly class BarcodeScanResultHandler
|
||||
// Mouser: use supplierPartNumber -> search provider -> provider_id
|
||||
if ($vendor === 'mouser' && $scanResult->supplierPartNumber !== null
|
||||
) {
|
||||
// Search Mouser using the MPN
|
||||
$dtos = $this->infoRetriever->searchByKeyword(
|
||||
keyword: $scanResult->supplierPartNumber,
|
||||
providers: ["mouser"]
|
||||
);
|
||||
// Search Mouser using the MPN
|
||||
$dtos = $this->infoRetriever->searchByKeyword(
|
||||
keyword: $scanResult->supplierPartNumber,
|
||||
providers: ["mouser"]
|
||||
);
|
||||
|
||||
// If there are results, provider_id is MouserPartNumber (per MouserProvider.php)
|
||||
$best = $dtos[0] ?? null;
|
||||
// If there are results, provider_id is MouserPartNumber (per MouserProvider.php)
|
||||
$best = $dtos[0] ?? null;
|
||||
|
||||
if ($best !== null) {
|
||||
return [
|
||||
'providerKey' => 'mouser',
|
||||
'providerId' => $best->provider_id,
|
||||
];
|
||||
}
|
||||
if ($best !== null) {
|
||||
return [
|
||||
'providerKey' => 'mouser',
|
||||
'providerId' => $best->provider_id,
|
||||
'lotAmount' => $scanResult->quantity,
|
||||
'lotName' => $scanResult->customerPO,
|
||||
'lotUserBarcode' => $scanResult->rawInput,
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Digi-Key: supplierPartNumber directly
|
||||
@@ -309,6 +316,9 @@ final readonly class BarcodeScanResultHandler
|
||||
return [
|
||||
'providerKey' => 'digikey',
|
||||
'providerId' => $scanResult->supplierPartNumber ?? throw new \RuntimeException('Digikey barcode does not contain required supplier part number'),
|
||||
'lotAmount' => $scanResult->quantity,
|
||||
'lotName' => $scanResult->digikeyInvoiceNumber ?? $scanResult->digikeySalesOrderNumber ?? $scanResult->customerPO,
|
||||
'lotUserBarcode' => $scanResult->rawInput,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -317,6 +327,9 @@ final readonly class BarcodeScanResultHandler
|
||||
return [
|
||||
'providerKey' => 'element14',
|
||||
'providerId' => $scanResult->supplierPartNumber ?? throw new \RuntimeException('Element14 barcode does not contain required supplier part number'),
|
||||
'lotAmount' => $scanResult->quantity,
|
||||
'lotName' => $scanResult->customerPO,
|
||||
'lotUserBarcode' => $scanResult->rawInput,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ readonly class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
||||
*
|
||||
* @param array<string, string> $data The fields of the EIGP114 barcode, where the key is the field name and the value is the field content
|
||||
*/
|
||||
public function __construct(public array $data)
|
||||
public function __construct(public array $data, public readonly ?string $rawInput = null)
|
||||
{
|
||||
//IDs per EIGP 114.2018
|
||||
$this->shipDate = $data['6D'] ?? null;
|
||||
@@ -271,6 +271,8 @@ readonly class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
||||
*/
|
||||
public static function parseFormat06Code(string $input): self
|
||||
{
|
||||
$rawInput = $input;
|
||||
|
||||
//Ensure that the input is a valid format06 code
|
||||
if (!self::isFormat06Code($input)) {
|
||||
throw new \InvalidArgumentException("The given input is not a valid format06 code");
|
||||
@@ -306,7 +308,7 @@ readonly class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
||||
$results[$key] = $fieldValue;
|
||||
}
|
||||
|
||||
return new self($results);
|
||||
return new self($results, $rawInput);
|
||||
}
|
||||
|
||||
public function getDecodedForInfoMode(): array
|
||||
|
||||
@@ -114,10 +114,12 @@ final class BarcodeProvider implements PlaceholderProviderInterface
|
||||
return '<b>IPN Barcode ERROR!</b>: '.$e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function getDefaultPriority(): int
|
||||
{
|
||||
//This provider should be checked before all others, so that nothing is delegated for part lots
|
||||
return 1000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,14 +327,14 @@ readonly class BackupManager
|
||||
*/
|
||||
private function restoreDatabaseFromBackup(string $tempDir): void
|
||||
{
|
||||
// Get database connection params from Doctrine
|
||||
$connection = $this->entityManager->getConnection();
|
||||
$params = $connection->getParams();
|
||||
$platform = $connection->getDatabasePlatform();
|
||||
|
||||
// Check for SQL dump (MySQL/PostgreSQL)
|
||||
$sqlFile = $tempDir . '/database.sql';
|
||||
if (file_exists($sqlFile)) {
|
||||
// Import SQL using mysql/psql command directly
|
||||
// First, get database connection params from Doctrine
|
||||
$connection = $this->entityManager->getConnection();
|
||||
$params = $connection->getParams();
|
||||
$platform = $connection->getDatabasePlatform();
|
||||
|
||||
if ($platform instanceof AbstractMySQLPlatform) {
|
||||
// Use mysql command to import - need to use shell to handle input redirection
|
||||
@@ -403,7 +403,8 @@ readonly class BackupManager
|
||||
// Check for SQLite database file
|
||||
$sqliteFile = $tempDir . '/var/app.db';
|
||||
if (file_exists($sqliteFile)) {
|
||||
$targetDb = $this->projectDir . '/var/app.db';
|
||||
// Use the actual configured SQLite path from Doctrine, not a hardcoded path
|
||||
$targetDb = $params['path'] ?? $this->projectDir . '/var/app.db';
|
||||
$this->filesystem->copy($sqliteFile, $targetDb, true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -207,6 +207,79 @@ class UpdateExecutor
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset PHP OPcache for the web server process.
|
||||
*
|
||||
* OPcache in PHP-FPM is separate from CLI. After updating code files,
|
||||
* PHP-FPM may still serve stale cached bytecode, causing constructor
|
||||
* mismatches and 500 errors. This method creates a temporary PHP script
|
||||
* in the public directory, invokes it via HTTP to reset OPcache in the
|
||||
* web server context, then removes the script.
|
||||
*
|
||||
* @return bool Whether OPcache was successfully reset
|
||||
*/
|
||||
private function resetOpcache(): bool
|
||||
{
|
||||
$token = bin2hex(random_bytes(16));
|
||||
$resetScript = $this->project_dir . '/public/_opcache_reset_' . $token . '.php';
|
||||
|
||||
try {
|
||||
// Create a temporary PHP script that resets OPcache
|
||||
$scriptContent = '<?php '
|
||||
. 'if (function_exists("opcache_reset")) { opcache_reset(); echo "OK"; } '
|
||||
. 'else { echo "NO_OPCACHE"; } '
|
||||
. '@unlink(__FILE__);';
|
||||
|
||||
$this->filesystem->dumpFile($resetScript, $scriptContent);
|
||||
|
||||
// Try to invoke it via HTTP on localhost
|
||||
$urls = [
|
||||
'http://127.0.0.1/_opcache_reset_' . $token . '.php',
|
||||
'http://localhost/_opcache_reset_' . $token . '.php',
|
||||
];
|
||||
|
||||
$success = false;
|
||||
foreach ($urls as $url) {
|
||||
try {
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 5,
|
||||
'ignore_errors' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$response = @file_get_contents($url, false, $context);
|
||||
if ($response === 'OK') {
|
||||
$this->logger->info('OPcache reset via ' . $url);
|
||||
$success = true;
|
||||
break;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Try next URL
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$success) {
|
||||
$this->logger->info('OPcache reset via HTTP not available, trying CLI fallback');
|
||||
// CLI opcache_reset() only affects CLI, but try anyway
|
||||
if (function_exists('opcache_reset')) {
|
||||
opcache_reset();
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('OPcache reset failed: ' . $e->getMessage());
|
||||
return false;
|
||||
} finally {
|
||||
// Ensure the temp script is removed
|
||||
if (file_exists($resetScript)) {
|
||||
@unlink($resetScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that we can perform an update.
|
||||
*
|
||||
@@ -434,12 +507,20 @@ class UpdateExecutor
|
||||
], 'Warmup cache', 120);
|
||||
$log('cache_warmup', 'Warmed up application cache', true, microtime(true) - $stepStart);
|
||||
|
||||
// Step 13: Disable maintenance mode
|
||||
// Step 13: Reset OPcache (if available)
|
||||
$stepStart = microtime(true);
|
||||
$opcacheResult = $this->resetOpcache();
|
||||
$log('opcache_reset', $opcacheResult
|
||||
? 'Reset PHP OPcache for web server'
|
||||
: 'OPcache reset skipped (not available or not needed)',
|
||||
true, microtime(true) - $stepStart);
|
||||
|
||||
// Step 14: Disable maintenance mode
|
||||
$stepStart = microtime(true);
|
||||
$this->disableMaintenanceMode();
|
||||
$log('maintenance_off', 'Disabled maintenance mode', true, microtime(true) - $stepStart);
|
||||
|
||||
// Step 14: Release lock
|
||||
// Step 15: Release lock
|
||||
$stepStart = microtime(true);
|
||||
$this->releaseLock();
|
||||
|
||||
@@ -494,6 +575,9 @@ class UpdateExecutor
|
||||
], 'Clear cache after rollback', 120);
|
||||
$log('rollback_cache', 'Cleared cache after rollback', true);
|
||||
|
||||
// Reset OPcache after rollback
|
||||
$this->resetOpcache();
|
||||
|
||||
} catch (\Exception $rollbackError) {
|
||||
$log('rollback_failed', 'Rollback failed: ' . $rollbackError->getMessage(), false);
|
||||
}
|
||||
@@ -602,6 +686,33 @@ class UpdateExecutor
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete a specific update log file.
|
||||
*/
|
||||
public function deleteLog(string $filename): bool
|
||||
{
|
||||
// Validate filename pattern for security
|
||||
if (!preg_match('/^update-[\w.\-]+\.log$/', $filename)) {
|
||||
$this->logger->warning('Attempted to delete invalid log filename: ' . $filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
$logPath = $this->project_dir . '/' . self::UPDATE_LOG_DIR . '/' . basename($filename);
|
||||
|
||||
if (!file_exists($logPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->filesystem->remove($logPath);
|
||||
$this->logger->info('Deleted update log: ' . $filename);
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Failed to delete update log: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore from a backup file with maintenance mode and cache clearing.
|
||||
*
|
||||
@@ -682,12 +793,17 @@ class UpdateExecutor
|
||||
$this->runCommand(['php', 'bin/console', 'cache:warmup'], 'Warm up cache');
|
||||
$log('cache_warmup', 'Warmed up application cache', true, microtime(true) - $stepStart);
|
||||
|
||||
// Step 6: Disable maintenance mode
|
||||
// Step 6: Reset OPcache
|
||||
$stepStart = microtime(true);
|
||||
$this->resetOpcache();
|
||||
$log('opcache_reset', 'Reset PHP OPcache', true, microtime(true) - $stepStart);
|
||||
|
||||
// Step 7: Disable maintenance mode
|
||||
$stepStart = microtime(true);
|
||||
$this->disableMaintenanceMode();
|
||||
$log('maintenance_off', 'Disabled maintenance mode', true, microtime(true) - $stepStart);
|
||||
|
||||
// Step 7: Release lock
|
||||
// Step 8: Release lock
|
||||
$this->releaseLock();
|
||||
|
||||
$totalDuration = microtime(true) - $startTime;
|
||||
@@ -817,7 +933,7 @@ class UpdateExecutor
|
||||
'create_backup' => $createBackup,
|
||||
'started_at' => (new \DateTime())->format('c'),
|
||||
'current_step' => 0,
|
||||
'total_steps' => 14,
|
||||
'total_steps' => 15,
|
||||
'step_name' => 'initializing',
|
||||
'step_message' => 'Starting update process...',
|
||||
'steps' => [],
|
||||
@@ -890,7 +1006,7 @@ class UpdateExecutor
|
||||
bool $createBackup = true,
|
||||
?callable $onProgress = null
|
||||
): array {
|
||||
$totalSteps = 12;
|
||||
$totalSteps = 13;
|
||||
$currentStep = 0;
|
||||
|
||||
$updateProgress = function (string $stepName, string $message, bool $success = true) use (&$currentStep, $totalSteps, $targetVersion, $createBackup): void {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ entity.id) }}">
|
||||
<div class="form-group">
|
||||
<div class=""></div>
|
||||
<div class="col-sm offset-sm-3 ps-2">
|
||||
<div class="{{ col_input }} {{ offset_label }} ps-1">
|
||||
{% set delete_disabled = (not is_granted("delete", entity)) or (entity.group is defined and entity.id == 1) or entity == app.user %}
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-danger" {% if delete_disabled %}disabled{% endif %}>{% trans %}entity.delete{% endtrans %}</button>
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% if entity.parent is defined %}
|
||||
<div class="ms-2 custom-control custom-checkbox custom-control-inline">
|
||||
<div class="ms-1 custom-control custom-checkbox custom-control-inline">
|
||||
<input type="checkbox" class="form-check-input" id="recursive" name="delete_recursive">
|
||||
<label class="form-check-label" for="recursive">{% trans %}entity.delete.recursive{% endtrans %}</label>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="row mb-2">
|
||||
<div class="offset-3 col">
|
||||
<div class="{{ offset_label }} {{ col_input }}">
|
||||
<a class="btn btn-info {% if not is_granted('create', entity) %}disabled{% endif %}" href="{{ entity_url(entity, 'clone') }}">{% trans %}entity.duplicate{% endtrans %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
</div>
|
||||
|
||||
<div class="row mt-2">
|
||||
<div class="offset-sm-3 col-sm">
|
||||
<div class="{{ offset_label }} col-sm">
|
||||
<button type="submit" class="btn btn-primary">{% trans %}export.btn{% endtrans %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
<div class="btn-group">
|
||||
{{ form_widget(form.save) }}
|
||||
<button type="button" class="btn {% if entity.id is not null %}btn-primary{% else %}btn-success{% endif %} dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
@@ -186,7 +186,7 @@
|
||||
|
||||
<div id="mass_creation" class="tab-pane fade">
|
||||
<div class="row">
|
||||
<p class="text-muted offset-sm-3 col-sm-9">{% trans %}mass_creation.help{% endtrans %}</p>
|
||||
<p class="text-muted {{ offset_label }} {{ col_input }}">{% trans %}mass_creation.help{% endtrans %}</p>
|
||||
</div>
|
||||
{{ form(mass_creation_form) }}
|
||||
</div>
|
||||
|
||||
@@ -40,14 +40,12 @@
|
||||
<div class="tab-pane" id="eda">
|
||||
{{ form_row(form.eda_info.reference_prefix) }}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
{{ form_row(form.eda_info.visibility) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_row(form.eda_info.visibility) }}
|
||||
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
{{ form_widget(form.eda_info.exclude_from_bom) }}
|
||||
{{ form_widget(form.eda_info.exclude_from_board) }}
|
||||
{{ form_widget(form.eda_info.exclude_from_sim) }}
|
||||
@@ -55,7 +53,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
<h6>{% trans %}eda_info.kicad_section.title{% endtrans %}:</h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
{{ form_row(form.iso_code) }}
|
||||
{% if entity.isoCode %}
|
||||
<div class="mt-0 mb-3">
|
||||
<span class="form-text text-muted offset-3 col-9">
|
||||
<span class="form-text text-muted {{ offset_label }} {{ col_input }}">
|
||||
<b>{% trans %}currency.iso_code.caption{% endtrans %}:</b> {{ entity.isoCode }}
|
||||
</span>
|
||||
<span class="form-text text-muted offset-3 col-9">
|
||||
<span class="form-text text-muted {{ offset_label }} {{ col_input }}">
|
||||
<b>{% trans %}currency.symbol.caption{% endtrans %}:</b> {{ entity.isoCode | currency_symbol }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
{{ form_row(form.exchange_rate) }}
|
||||
{% if entity.inverseExchangeRate %}
|
||||
<p class="form-text text-muted offset-3 col-9">
|
||||
<p class="form-text text-muted {{ offset_label }} {{ col_input }}">
|
||||
{{ '1'|format_currency(vars.base_currency()) }} = {{ entity.inverseExchangeRate.tofloat | format_currency(entity.isoCode, {fraction_digit: 5}) }}<br>
|
||||
{{ '1'|format_currency(entity.isoCode) }} = {{ entity.exchangeRate.tofloat | format_currency(vars.base_currency(), {fraction_digit: 5}) }}
|
||||
</p>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{% block additional_panes %}
|
||||
<div class="tab-pane" id="eda">
|
||||
<div class="row">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
<h6>{% trans %}eda_info.kicad_section.title{% endtrans %}:</h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
{{ form_row(form.options.supported_element) }}
|
||||
<div class="mb-2 row">
|
||||
{{ form_label(form.options.width) }}
|
||||
<div class="col-sm-9">
|
||||
<div class="{{ col_input }}">
|
||||
<div class="input-group">
|
||||
{{ form_widget(form.options.width) }}
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
{{ form_row(form.status) }}
|
||||
{% if entity.id %}
|
||||
<div class="mb-2 row">
|
||||
<label class="col-form-label col-sm-3">{% trans %}project.edit.associated_build_part{% endtrans %}</label>
|
||||
<div class="col-sm-9">
|
||||
<label class="col-form-label {{ col_label }}">{% trans %}project.edit.associated_build_part{% endtrans %}</label>
|
||||
<div class="{{ col_input }}">
|
||||
{% if entity.buildPart %}
|
||||
<span class="form-control-static"><a href="{{ entity_url(entity.buildPart) }}">{{ entity.buildPart.name }}</a></span>
|
||||
{% else %}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{% block additional_controls %}
|
||||
{% if entity.id %}
|
||||
<div class="row form-group">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<div class="{{ offset_label }} {{ col_input }}">
|
||||
{{ dropdown.profile_dropdown('storelocation', entity.id) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{% extends "main_card.html.twig" %}
|
||||
|
||||
{% import "helper.twig" as helper %}
|
||||
|
||||
{% block title %}Part-DB {% trans %}update_manager.title{% endtrans %}{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
@@ -7,60 +9,60 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
<div>
|
||||
<div>
|
||||
|
||||
{# Maintenance Mode Warning #}
|
||||
{% if is_maintenance %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="fas fa-tools me-2"></i>
|
||||
<strong>{% trans %}update_manager.maintenance_mode_active{% endtrans %}</strong>
|
||||
{% if maintenance_info.reason is defined %}
|
||||
- {{ maintenance_info.reason }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Maintenance Mode Warning #}
|
||||
{% if is_maintenance %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="fas fa-tools me-2"></i>
|
||||
<strong>{% trans %}update_manager.maintenance_mode_active{% endtrans %}</strong>
|
||||
{% if maintenance_info.reason is defined %}
|
||||
- {{ maintenance_info.reason }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Lock Warning #}
|
||||
{% if is_locked %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="fas fa-lock me-2"></i>
|
||||
<strong>{% trans %}update_manager.update_in_progress{% endtrans %}</strong>
|
||||
{% if lock_info.started_at is defined %}
|
||||
({% trans %}update_manager.started_at{% endtrans %}: {{ lock_info.started_at }})
|
||||
{% endif %}
|
||||
<a href="{{ path('admin_update_manager_progress') }}" class="alert-link ms-2">
|
||||
{% trans %}update_manager.view_progress{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Lock Warning #}
|
||||
{% if is_locked %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="fas fa-lock me-2"></i>
|
||||
<strong>{% trans %}update_manager.update_in_progress{% endtrans %}</strong>
|
||||
{% if lock_info.started_at is defined %}
|
||||
({% trans %}update_manager.started_at{% endtrans %}: {{ lock_info.started_at }})
|
||||
{% endif %}
|
||||
<a href="{{ path('admin_update_manager_progress') }}" class="alert-link ms-2">
|
||||
{% trans %}update_manager.view_progress{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Web Updates Disabled Warning #}
|
||||
{% if web_updates_disabled %}
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<i class="fas fa-ban me-2"></i>
|
||||
<strong>{% trans %}update_manager.web_updates_disabled{% endtrans %}</strong>
|
||||
<p class="mb-0 mt-2">{% trans %}update_manager.web_updates_disabled_hint{% endtrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Web Updates Disabled Warning #}
|
||||
{% if web_updates_disabled %}
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<i class="fas fa-ban me-2"></i>
|
||||
<strong>{% trans %}update_manager.web_updates_disabled{% endtrans %}</strong>
|
||||
<p class="mb-0 mt-2">{% trans %}update_manager.web_updates_disabled_hint{% endtrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Backup Restore Disabled Warning #}
|
||||
{% if backup_restore_disabled %}
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<i class="fas fa-ban me-2"></i>
|
||||
<strong>{% trans %}update_manager.backup_restore_disabled{% endtrans %}</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Backup Restore Disabled Warning #}
|
||||
{% if backup_restore_disabled %}
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<i class="fas fa-ban me-2"></i>
|
||||
<strong>{% trans %}update_manager.backup_restore_disabled{% endtrans %}</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
{# Current Version Card #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>{% trans %}update_manager.current_installation{% endtrans %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
<div class="row">
|
||||
{# Current Version Card #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>{% trans %}update_manager.current_installation{% endtrans %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" style="width: 40%">{% trans %}update_manager.version{% endtrans %}</th>
|
||||
<td>
|
||||
@@ -100,153 +102,159 @@
|
||||
<tr>
|
||||
<th scope="row">{% trans %}update_manager.auto_update_supported{% endtrans %}</th>
|
||||
<td>
|
||||
{% if status.can_auto_update %}
|
||||
<span class="badge bg-success">
|
||||
<i class="fas fa-check me-1"></i>{% trans %}Yes{% endtrans %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">
|
||||
<i class="fas fa-times me-1"></i>{% trans %}No{% endtrans %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{{ helper.boolean_badge(status.can_auto_update) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<form action="{{ path('admin_update_manager_refresh') }}" method="post" class="d-inline">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_refresh') }}">
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-sync-alt me-1"></i> {% trans %}update_manager.refresh{% endtrans %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<tr>
|
||||
<th scope="row">{% trans %}update_manager.web_updates_allowed{% endtrans %}</th>
|
||||
<td>{{ helper.boolean_badge(not web_updates_disabled) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans %}update_manager.backup_restore_allowed{% endtrans %}</th>
|
||||
<td>{{ helper.boolean_badge(not backup_restore_disabled) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans %}update_manager.backup_download_allowed{% endtrans %}</th>
|
||||
<td>{{ helper.boolean_badge(not backup_download_disabled) }}</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<form action="{{ path('admin_update_manager_refresh') }}" method="post" class="d-inline">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_refresh') }}">
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-sync-alt me-1"></i> {% trans %}update_manager.refresh{% endtrans %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Latest Version / Update Card #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100 {{ status.update_available ? 'border-success' : '' }}">
|
||||
<div class="card-header {{ status.update_available ? 'bg-success text-white' : '' }}">
|
||||
{% if status.update_available %}
|
||||
<i class="fas fa-gift me-2"></i>{% trans %}update_manager.new_version_available.title{% endtrans %}
|
||||
{% else %}
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans %}update_manager.latest_release{% endtrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if status.latest_version %}
|
||||
<div class="text-center mb-3">
|
||||
{# Latest Version / Update Card #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100 {{ status.update_available ? 'border-success' : '' }}">
|
||||
<div class="card-header {{ status.update_available ? 'bg-success text-white' : '' }}">
|
||||
{% if status.update_available %}
|
||||
<i class="fas fa-gift me-2"></i>{% trans %}update_manager.new_version_available.title{% endtrans %}
|
||||
{% else %}
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans %}update_manager.latest_release{% endtrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if status.latest_version %}
|
||||
<div class="text-center mb-3">
|
||||
<span class="badge bg-{{ status.update_available ? 'success' : 'primary' }} fs-4 px-4 py-2">
|
||||
{{ status.latest_tag }}
|
||||
</span>
|
||||
{% if not status.update_available %}
|
||||
<p class="text-success mt-2 mb-0">
|
||||
<i class="fas fa-check-circle me-1"></i>
|
||||
{% trans %}update_manager.already_up_to_date{% endtrans %}
|
||||
{% if not status.update_available %}
|
||||
<p class="text-success mt-2 mb-0">
|
||||
<i class="fas fa-check-circle me-1"></i>
|
||||
{% trans %}update_manager.already_up_to_date{% endtrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if status.update_available and status.can_auto_update and validation.valid and not web_updates_disabled %}
|
||||
<form action="{{ path('admin_update_manager_start') }}" method="post"
|
||||
data-controller="update-confirm"
|
||||
data-update-confirm-is-downgrade-value="false"
|
||||
data-update-confirm-target-version-value="{{ status.latest_tag }}"
|
||||
data-update-confirm-confirm-update-value="{{ 'update_manager.confirm_update'|trans }}"
|
||||
data-update-confirm-confirm-downgrade-value="{{ 'update_manager.confirm_downgrade'|trans }}"
|
||||
data-update-confirm-downgrade-warning-value="{{ 'update_manager.downgrade_removes_update_manager'|trans }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_start') }}">
|
||||
<input type="hidden" name="version" value="{{ status.latest_tag }}">
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-success btn-lg">
|
||||
<i class="fas fa-download me-2"></i>
|
||||
{% trans %}update_manager.update_to{% endtrans %} {{ status.latest_tag }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" type="checkbox" name="backup" value="1" id="create-backup" checked>
|
||||
<label class="form-check-label" for="create-backup">
|
||||
<i class="fas fa-database me-1"></i> {% trans %}update_manager.create_backup{% endtrans %}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if status.published_at %}
|
||||
<p class="text-muted small mt-3 mb-0">
|
||||
<i class="fas fa-calendar me-1"></i>
|
||||
{% trans %}update_manager.released{% endtrans %}: {{ status.published_at|date('Y-m-d') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if status.update_available and status.can_auto_update and validation.valid and not web_updates_disabled %}
|
||||
<form action="{{ path('admin_update_manager_start') }}" method="post"
|
||||
data-controller="update-confirm"
|
||||
data-update-confirm-is-downgrade-value="false"
|
||||
data-update-confirm-target-version-value="{{ status.latest_tag }}"
|
||||
data-update-confirm-confirm-update-value="{{ 'update_manager.confirm_update'|trans }}"
|
||||
data-update-confirm-confirm-downgrade-value="{{ 'update_manager.confirm_downgrade'|trans }}"
|
||||
data-update-confirm-downgrade-warning-value="{{ 'update_manager.downgrade_removes_update_manager'|trans }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_start') }}">
|
||||
<input type="hidden" name="version" value="{{ status.latest_tag }}">
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-success btn-lg">
|
||||
<i class="fas fa-download me-2"></i>
|
||||
{% trans %}update_manager.update_to{% endtrans %} {{ status.latest_tag }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" type="checkbox" name="backup" value="1" id="create-backup" checked>
|
||||
<label class="form-check-label" for="create-backup">
|
||||
<i class="fas fa-database me-1"></i> {% trans %}update_manager.create_backup{% endtrans %}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-question-circle fa-3x mb-3"></i>
|
||||
<p>{% trans %}update_manager.could_not_fetch_releases{% endtrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if status.published_at %}
|
||||
<p class="text-muted small mt-3 mb-0">
|
||||
<i class="fas fa-calendar me-1"></i>
|
||||
{% trans %}update_manager.released{% endtrans %}: {{ status.published_at|date('Y-m-d') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-question-circle fa-3x mb-3"></i>
|
||||
<p>{% trans %}update_manager.could_not_fetch_releases{% endtrans %}</p>
|
||||
</div>
|
||||
{% if status.latest_tag %}
|
||||
<div class="card-footer">
|
||||
<a href="{{ path('admin_update_manager_release', {tag: status.latest_tag}) }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans %}update_manager.view_release_notes{% endtrans %}
|
||||
</a>
|
||||
{% if status.release_url %}
|
||||
<a href="{{ status.release_url }}" class="btn btn-outline-secondary btn-sm" target="_blank">
|
||||
<i class="fab fa-github me-1"></i> GitHub
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if status.latest_tag %}
|
||||
<div class="card-footer">
|
||||
<a href="{{ path('admin_update_manager_release', {tag: status.latest_tag}) }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans %}update_manager.view_release_notes{% endtrans %}
|
||||
</a>
|
||||
{% if status.release_url %}
|
||||
<a href="{{ status.release_url }}" class="btn btn-outline-secondary btn-sm" target="_blank">
|
||||
<i class="fab fa-github me-1"></i> GitHub
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Validation Issues #}
|
||||
{% if not validation.valid %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>{% trans %}update_manager.validation_issues{% endtrans %}
|
||||
</h6>
|
||||
<ul class="mb-0">
|
||||
{% for error in validation.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Validation Issues #}
|
||||
{% if not validation.valid %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>{% trans %}update_manager.validation_issues{% endtrans %}
|
||||
</h6>
|
||||
<ul class="mb-0">
|
||||
{% for error in validation.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Non-auto-update installations info #}
|
||||
{% if not status.can_auto_update %}
|
||||
<div class="alert alert-secondary">
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-info-circle me-2"></i>{% trans%}update_manager.cant_auto_update{% endtrans%}: {{ status.installation.type_name }}
|
||||
</h6>
|
||||
<p class="mb-0">{{ status.installation.update_instructions }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Non-auto-update installations info #}
|
||||
{% if not status.can_auto_update %}
|
||||
<div class="alert alert-secondary">
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-info-circle me-2"></i>{% trans%}update_manager.cant_auto_update{% endtrans%}: {{ status.installation.type_name }}
|
||||
</h6>
|
||||
<p class="mb-0">{{ status.installation.update_instructions }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
{# Available Versions #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-tags me-2"></i>{% trans %}update_manager.available_versions{% endtrans %}
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
<div class="row">
|
||||
{# Available Versions #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-tags me-2"></i>{% trans %}update_manager.available_versions{% endtrans %}
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
<tr>
|
||||
<th>{% trans %}update_manager.version{% endtrans %}</th>
|
||||
<th>{% trans %}update_manager.released{% endtrans %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for release in all_releases %}
|
||||
<tr{% if release.version == status.current_version %} class="table-active"{% endif %}>
|
||||
<td>
|
||||
@@ -280,8 +288,8 @@
|
||||
<input type="hidden" name="version" value="{{ release.tag }}">
|
||||
<input type="hidden" name="backup" value="1">
|
||||
<button type="submit"
|
||||
class="btn btn-{{ release.version > status.current_version ? 'outline-success' : 'outline-warning' }}"
|
||||
title="{% trans %}update_manager.switch_to{% endtrans %} {{ release.tag }}">
|
||||
class="btn btn-{{ release.version > status.current_version ? 'outline-success' : 'outline-warning' }}"
|
||||
title="{% trans %}update_manager.switch_to{% endtrans %} {{ release.tag }}">
|
||||
{% if release.version > status.current_version %}
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
{% else %}
|
||||
@@ -300,54 +308,69 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Update History & Backups #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#logs-tab" type="button">
|
||||
<i class="fas fa-history me-1"></i>{% trans %}update_manager.update_logs{% endtrans %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#backups-tab" type="button">
|
||||
<i class="fas fa-archive me-1"></i>{% trans %}update_manager.backups{% endtrans %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="logs-tab">
|
||||
<div class="table-responsive" style="max-height: 350px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
{# Update History & Backups #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#logs-tab" type="button">
|
||||
<i class="fas fa-history me-1"></i>{% trans %}update_manager.update_logs{% endtrans %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#backups-tab" type="button">
|
||||
<i class="fas fa-archive me-1"></i>{% trans %}update_manager.backups{% endtrans %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="logs-tab">
|
||||
<div class="table-responsive" style="max-height: 350px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
<tr>
|
||||
<th>{% trans %}update_manager.date{% endtrans %}</th>
|
||||
<th>{% trans %}update_manager.log_file{% endtrans %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in update_logs %}
|
||||
<tr>
|
||||
<td class="text-muted small">
|
||||
{{ log.date|date('Y-m-d H:i') }}
|
||||
</td>
|
||||
<td><code class="small">{{ log.file }}</code></td>
|
||||
<td>
|
||||
<a href="{{ path('admin_update_manager_log', {filename: log.file}) }}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="{{ path('admin_update_manager_log', {filename: log.file}) }}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans %}update_manager.view_log{% endtrans %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if is_granted('@system.manage_updates') %}
|
||||
<form action="{{ path('admin_update_manager_log_delete') }}" method="post" class="d-inline"
|
||||
data-turbo-confirm="{% trans %}update_manager.log.delete.confirm{% endtrans %}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_delete') }}">
|
||||
<input type="hidden" name="filename" value="{{ log.file }}">
|
||||
<button type="submit"
|
||||
class="btn btn-outline-danger"
|
||||
title="{% trans %}update_manager.delete{% endtrans %}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
@@ -357,22 +380,39 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="backups-tab">
|
||||
<div class="table-responsive" style="max-height: 350px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
<div class="tab-pane fade" id="backups-tab">
|
||||
{% if is_granted('@system.manage_updates') and not is_locked %}
|
||||
<div class="p-2 border-bottom">
|
||||
<form action="{{ path('admin_update_manager_backup') }}" method="post" class="d-inline"
|
||||
data-turbo-confirm="{% trans %}update_manager.backup.create.confirm{% endtrans %}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_backup') }}">
|
||||
<button type="submit" class="btn btn-sm btn-success">
|
||||
<i class="fas fa-plus me-1"></i>{% trans %}update_manager.backup.create{% endtrans %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if is_docker %}
|
||||
<div class="alert alert-info alert-sm m-2 mb-0 py-2 small">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans %}update_manager.backup.docker_warning{% endtrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="table-responsive" style="max-height: 350px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
<tr>
|
||||
<th>{% trans %}update_manager.date{% endtrans %}</th>
|
||||
<th>{% trans %}update_manager.file{% endtrans %}</th>
|
||||
<th>{% trans %}update_manager.size{% endtrans %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for backup in backups %}
|
||||
<tr>
|
||||
<td class="text-muted small">
|
||||
@@ -383,23 +423,89 @@
|
||||
{{ (backup.size / 1024 / 1024)|number_format(1) }} MB
|
||||
</td>
|
||||
<td class="text-end">
|
||||
{% if status.can_auto_update and validation.valid and not backup_restore_disabled %}
|
||||
<form action="{{ path('admin_update_manager_restore') }}" method="post" class="d-inline"
|
||||
data-controller="backup-restore"
|
||||
data-backup-restore-filename-value="{{ backup.file }}"
|
||||
data-backup-restore-date-value="{{ backup.date|date('Y-m-d H:i') }}"
|
||||
data-backup-restore-confirm-title-value="{{ 'update_manager.restore_confirm_title'|trans }}"
|
||||
data-backup-restore-confirm-message-value="{{ 'update_manager.restore_confirm_message'|trans }}"
|
||||
data-backup-restore-confirm-warning-value="{{ 'update_manager.restore_confirm_warning'|trans }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_restore') }}">
|
||||
<input type="hidden" name="filename" value="{{ backup.file }}">
|
||||
<input type="hidden" name="restore_database" value="1">
|
||||
<button type="submit"
|
||||
class="btn btn-sm btn-outline-warning"
|
||||
title="{% trans %}update_manager.restore_backup{% endtrans %}">
|
||||
<i class="fas fa-undo"></i>
|
||||
<div class="btn-group btn-group-sm">
|
||||
{% if not backup_download_disabled and is_granted('@system.manage_updates') %}
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#downloadBackupModal-{{ loop.index }}"
|
||||
title="{% trans %}update_manager.backup.download{% endtrans %}">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if not backup_restore_disabled and is_granted('@system.manage_updates') %}
|
||||
<form action="{{ path('admin_update_manager_restore') }}" method="post" class="d-inline"
|
||||
{{ stimulus_controller('backup-restore') }}
|
||||
data-backup-restore-filename-value="{{ backup.file }}"
|
||||
data-backup-restore-date-value="{{ backup.date|date('Y-m-d H:i') }}"
|
||||
data-backup-restore-confirm-title-value="{{ 'update_manager.restore_confirm_title'|trans }}"
|
||||
data-backup-restore-confirm-message-value="{{ 'update_manager.restore_confirm_message'|trans }}"
|
||||
data-backup-restore-confirm-warning-value="{{ 'update_manager.restore_confirm_warning'|trans }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_restore') }}">
|
||||
<input type="hidden" name="filename" value="{{ backup.file }}">
|
||||
<input type="hidden" name="restore_database" value="1">
|
||||
<button type="submit"
|
||||
class="btn btn-outline-warning"
|
||||
title="{% trans %}update_manager.restore_backup{% endtrans %}">
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if is_granted('@system.manage_updates') %}
|
||||
<form action="{{ path('admin_update_manager_backup_delete') }}" method="post" class="d-inline"
|
||||
data-turbo-confirm="{% trans %}update_manager.backup.delete.confirm{% endtrans %}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_delete') }}">
|
||||
<input type="hidden" name="filename" value="{{ backup.file }}">
|
||||
<button type="submit"
|
||||
class="btn btn-outline-danger"
|
||||
title="{% trans %}update_manager.delete{% endtrans %}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not backup_download_disabled and is_granted('@system.manage_updates') %}
|
||||
{# Per-backup download modal - no inline JS needed, CSP compatible with Turbo #}
|
||||
<div class="modal fade text-start" id="downloadBackupModal-{{ loop.index }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form action="{{ path('admin_update_manager_backup_download') }}" method="post" data-turbo="false">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-download me-2"></i>{% trans %}update_manager.backup.download{% endtrans %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>
|
||||
{% trans %}update_manager.backup.download.security_warning{% endtrans %}
|
||||
</div>
|
||||
<p class="text-muted small mb-3">{{ backup.file }}</p>
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_download') }}">
|
||||
<input type="hidden" name="filename" value="{{ backup.file }}">
|
||||
<input type="hidden" name="username" autocomplete="username">
|
||||
<div class="mb-3">
|
||||
<label for="downloadPassword-{{ loop.index }}" class="form-label">
|
||||
{% trans %}update_manager.backup.download.password_label{% endtrans %}
|
||||
</label>
|
||||
<input type="password" class="form-control" id="downloadPassword-{{ loop.index }}"
|
||||
name="password" required autocomplete="current-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{% trans %}modal.cancel{% endtrans %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-download me-1"></i>{% trans %}update_manager.backup.download{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -410,8 +516,9 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -419,5 +526,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
<div class="tab-pane" id="password">
|
||||
{% if entity.samlUser %}
|
||||
<div class="offset-3 mb-3 col-9">
|
||||
<div class="mb-3 {{ offset_label }} {{ col_input }}">
|
||||
<span class="badge badge-warning bg-warning"><i class="fa-solid fa-house-user"></i> {% trans %}user.saml_user{% endtrans %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -60,7 +60,7 @@
|
||||
{{ form_row(form.disabled) }}
|
||||
|
||||
{% if entity.id is not null %}
|
||||
<div class="offset-3 mb-3">
|
||||
<div class="{{ offset_label }} {{ col_input }} mb-3">
|
||||
<hr>
|
||||
<h6>{% trans %}user.edit.tfa.caption{% endtrans %}</h6>
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{{ form_row(filterForm.discard) }}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
<button type="button" class="btn btn-danger" {{ stimulus_action('helpers/form_cleanup', 'clearAll') }}>{% trans %}filter.clear_filters{% endtrans %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
<div class="">
|
||||
{{ form_row(form.mountnames) }}
|
||||
<div class="row mb-2">
|
||||
<label class="col-form-label col-sm-3">{% trans %}project.bom.price{% endtrans %}</label>
|
||||
<div class="col-sm-9">
|
||||
<label class="col-form-label {{ col_label }}">{% trans %}project.bom.price{% endtrans %}</label>
|
||||
<div class="{{ col_input }}">
|
||||
<div class="input-group">
|
||||
{{ form_widget(form.price) }}
|
||||
{{ form_widget(form.priceCurrency) }}
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
|
||||
|
||||
{% block form_label_class -%}
|
||||
col-sm-3
|
||||
{{ col_label }}
|
||||
{%- endblock form_label_class %}
|
||||
|
||||
{% block form_group_class -%}
|
||||
col-sm-9
|
||||
{{ col_input }}
|
||||
{%- endblock form_group_class %}
|
||||
|
||||
{% block si_unit_widget %}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{% macro boolean(value) %}
|
||||
{% if value %}
|
||||
{% trans %}bool.true{% endtrans %}
|
||||
{% trans %}Yes{% endtrans %}
|
||||
{% else %}
|
||||
{% trans %}bool.false{% endtrans %}
|
||||
{% trans %}No{% endtrans %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
|
||||
{% macro bool_icon(bool) %}
|
||||
{% if bool %}
|
||||
<i class="fas fa-check-circle fa-fw" title="{% trans %}Yes{% endtrans %}"></i>
|
||||
<i class="fas fa-check fa-fw" title="{% trans %}Yes{% endtrans %}"></i>
|
||||
{% else %}
|
||||
<i class="fas fa-times-circle fa-fw" title="{% trans %}No{% endtrans %}"></i>
|
||||
<i class="fas fa-times fa-fw" title="{% trans %}No{% endtrans %}"></i>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
{% if value %}
|
||||
{% set class = class ~ ' bg-success' %}
|
||||
{% else %}
|
||||
{% set class = class ~ ' bg-danger' %}
|
||||
{% set class = class ~ ' bg-secondary' %}
|
||||
{% endif %}
|
||||
|
||||
<span class="{{ class }}">{{ _self.bool_icon(value) }} {{ _self.boolean(value) }}</span>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
<p class="text-muted offset-3">{% trans %}info_providers.from_url.help{% endtrans %}</p>
|
||||
<p class="text-muted {{ offset_label }}">{% trans %}info_providers.from_url.help{% endtrans %}</p>
|
||||
|
||||
{{ form_start(form) }}
|
||||
{{ form_row(form.url) }}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{{ form_row(form.providers) }}
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
<a href="{{ path('info_providers_list') }}">{% trans %}info_providers.search.info_providers_list{% endtrans %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{% block card_title %}<i class="fa-solid fa-gear fa-fw"></i> {% trans %}info_providers.settings.title{% endtrans %}: <b>{{ info_provider_info.name }}</b>{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
<div class="offset-sm-3">
|
||||
<div class="{{ offset_label }}">
|
||||
<h3>
|
||||
{% if info_provider_info.url is defined %}
|
||||
<a href="{{ info_provider_info.url }}" class="link-external" target="_blank" rel="nofollow">{{ info_provider_info.name }}</a>
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
{{ form_start(form) }}
|
||||
<div class="row">
|
||||
<div class="offset-sm-3 col mb-3 ps-2">
|
||||
<div class="{{ offset_label }} col mb-3 ps-2">
|
||||
<b>{{ form_help(form) }}</b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
{{ form_row(form.options.supported_element) }}
|
||||
<div class="mb-2 row">
|
||||
{{ form_label(form.options.width) }}
|
||||
<div class="col-sm-9">
|
||||
<div class="{{ col_input }}">
|
||||
<div class="input-group">
|
||||
{{ form_widget(form.options.width) }}
|
||||
|
||||
@@ -59,8 +59,8 @@
|
||||
|
||||
<div class="tab-pane" id="profiles" role="tabpanel" aria-labelledby="profiles-tab">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">{% trans %}label_generator.selected_profile{% endtrans %}</label>
|
||||
<div class="col-sm-9">
|
||||
<label class="{{ col_label }} col-form-label">{% trans %}label_generator.selected_profile{% endtrans %}</label>
|
||||
<div class="{{ col_input }}">
|
||||
<span class="form-control-plaintext">{{ profile.name ?? '-' }}
|
||||
{% if profile and is_granted("edit", profile) %}
|
||||
<a href="{{ entity_url(profile, 'edit') }}" title="{% trans %}label_generator.edit_profile{% endtrans %}"
|
||||
@@ -71,7 +71,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<div class="{{ offset_label }} {{ col_input }}">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-info dropdown-toggle" type="button" id="loadProfilesButton"
|
||||
{% if not is_granted("@labels.create_labels") %}disabled{% endif %}
|
||||
@@ -97,7 +97,7 @@
|
||||
|
||||
{% if is_granted("@labels.read_profiles") %}
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<div class="{{ offset_label }} {{ col_input }}">
|
||||
<a class="btn btn-link" href="{{ path('label_profile_new') }}">{% trans %}label_generator.edit_profiles{% endtrans %}</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,7 +108,7 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<div class="{{ offset_label }} {{ col_input }}">
|
||||
<div class="input-group">
|
||||
{{ form_widget(form.save_profile_name) }}
|
||||
{{ form_widget(form.save_profile) }}
|
||||
@@ -124,7 +124,7 @@
|
||||
{{ form_end(form) }}
|
||||
{% if pdf_data %}
|
||||
<div class="row">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
<a data-turbo="false" class="btn btn-secondary" href="#" {{ stimulus_controller('pages/label_download_btn')}} {{ stimulus_action('pages/label_download_btn', 'download')}} download="{{ filename ?? '' }}">
|
||||
{% trans %}label_generator.download{% endtrans %}
|
||||
</a>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<div class="">
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<div class="{{ offset_label }} {{ col_input }}">
|
||||
<div class="img-thumbnail" style="max-width: 600px;">
|
||||
<div id="reader-box" {{ stimulus_controller('pages/barcode_scan') }}></div>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
{{ form_row(filterForm.discard) }}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
<button type="button" class="btn btn-danger" {{ stimulus_action('helpers/form_cleanup', 'clearAll') }}>{% trans %}filter.clear_filters{% endtrans %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{{ form_row(form.eda_info.visibility) }}
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
{{ form_widget(form.eda_info.exclude_from_bom) }}
|
||||
{{ form_widget(form.eda_info.exclude_from_board) }}
|
||||
{{ form_widget(form.eda_info.exclude_from_sim) }}
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
<h6>{% trans %}eda_info.kicad_section.title{% endtrans %}:</h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{ form_row(form.name) }}
|
||||
{% if part.category is not null and part.category.partnameHint is not empty %}
|
||||
<div class="row">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
<p class="form-text help-text"><b>{% trans %}part.edit.name.category_hint{% endtrans %}:</b> {{ part.category.partnameHint }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
|
||||
{% set id = 'collapse_' ~ random() %}
|
||||
|
||||
<a class="btn btn-link offset-sm-3 btn-sm" data-bs-toggle="collapse" href="#{{ id }}" role="button" aria-expanded="false" aria-controls="{{ id }}">
|
||||
<a class="btn btn-link {{ offset_label }} btn-sm" data-bs-toggle="collapse" href="#{{ id }}" role="button" aria-expanded="false" aria-controls="{{ id }}">
|
||||
{% trans %}part_lot.edit.advanced{% endtrans %}
|
||||
</a>
|
||||
<div class="collapse" id="{{ id }}">
|
||||
@@ -142,7 +142,7 @@
|
||||
|
||||
<div class="mb-2 row">
|
||||
{{ form_label(form.file) }}
|
||||
<div class="col-sm-9">
|
||||
<div class="{{ col_input }}">
|
||||
{{ form_widget(form.file) }}
|
||||
{{ form_errors(form.file) }}
|
||||
<small class="text-muted">{% trans %}attachment.max_file_size{% endtrans %}: {{ max_upload_size | format_bytes }}</small>
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
<div class="btn-group">
|
||||
{{ form_widget(form.save) }}
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
<p class="text-muted offset-sm-3">
|
||||
<p class="text-muted {{ offset_label }}">
|
||||
{% trans %}parts.import.help{% endtrans %}<br>
|
||||
{% trans with {'%link%': 'https://docs.part-db.de/usage/import_export.html'} %}parts.import.help_documentation{% endtrans %}
|
||||
</p>
|
||||
|
||||
@@ -13,39 +13,39 @@
|
||||
<input type="hidden" name="_redirect" value="{{ uri_without_host(app.request) }}">
|
||||
|
||||
<div class="row mb-2">
|
||||
<label class="col-form-label col-sm-3">
|
||||
<label class="col-form-label {{ col_label }}">
|
||||
{% trans %}part.info.stocktake_modal.expected_amount{% endtrans %}
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="{{ col_input }}">
|
||||
<span id="stocktake-modal-expected-amount" class="form-control-plaintext">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<label class="col-form-label col-sm-3">
|
||||
<label class="col-form-label {{ col_label }}">
|
||||
{% trans %}part.info.stocktake_modal.actual_amount{% endtrans %}
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="{{ col_input }}">
|
||||
<input type="number" required class="form-control" min="0" step="{{ (part.partUnit and not part.partUnit.integer) ? 'any' : '1' }}" name="actual_amount" value="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-2">
|
||||
<label class="col-form-label col-sm-3">
|
||||
<label class="col-form-label {{ col_label }}">
|
||||
{% trans %}part.info.withdraw_modal.comment{% endtrans %}
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="{{ col_input }}">
|
||||
<input type="text" class="form-control" name="comment" value="">
|
||||
<div class="form-text">{% trans %}part.info.withdraw_modal.comment.hint{% endtrans %}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<label class="col-form-label col-sm-3">
|
||||
<label class="col-form-label {{ col_label }}">
|
||||
{% trans %}part.info.withdraw_modal.timestamp{% endtrans %}
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="{{ col_input }}">
|
||||
{# The timestamp must be between a year ago and 1 hour in the future #}
|
||||
<input type="datetime-local" class="form-control" name="timestamp" value=""
|
||||
max="{{ "+10mins"|date('Y-m-d\\TH:i') }}" min="{{ "-1year"|date('Y-m-d\\TH:i') }}">
|
||||
|
||||
@@ -18,17 +18,17 @@
|
||||
<input type="hidden" name="_redirect" value="{{ uri_without_host(app.request) }}">
|
||||
|
||||
<div class="row mb-2">
|
||||
<label class="col-form-label col-sm-3">
|
||||
<label class="col-form-label {{ col_label }}">
|
||||
{% trans %}part.info.withdraw_modal.amount{% endtrans %}
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="{{ col_input }}">
|
||||
<input type="number" required class="form-control" min="0" step="{{ (part.partUnit and not part.partUnit.integer) ? 'any' : '1' }}" name="amount" value="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2 d-none" id="withdraw-modal-move-to">
|
||||
<label class="col-form-label col-sm-3">{% trans %}part.info.withdraw_modal.move_to{% endtrans %}</label>
|
||||
<div class="col-sm-9">
|
||||
<label class="col-form-label {{ col_label }}">{% trans %}part.info.withdraw_modal.move_to{% endtrans %}</label>
|
||||
<div class="{{ col_input }}">
|
||||
{% for lots in part.partLots|filter(l => l.instockUnknown == false) %}
|
||||
|
||||
<div class="form-check">
|
||||
@@ -42,20 +42,20 @@
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<label class="col-form-label col-sm-3">
|
||||
<label class="col-form-label {{ col_label }}">
|
||||
{% trans %}part.info.withdraw_modal.comment{% endtrans %}
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="{{ col_input }}">
|
||||
<input type="text" class="form-control" name="comment" value="" {% if event_comment_needed('part_stock_operation') %}required{% endif %}>
|
||||
<div id="emailHelp" class="form-text">{% trans %}part.info.withdraw_modal.comment.hint{% endtrans %}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<label class="col-form-label col-sm-3">
|
||||
<label class="col-form-label {{ col_label }}">
|
||||
{% trans %}part.info.withdraw_modal.timestamp{% endtrans %}
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="{{ col_input }}">
|
||||
{# The timestamp must be between a year ago and 1 hour in the future #}
|
||||
<input type="datetime-local" class="form-control" name="timestamp" value=""
|
||||
max="{{ "+10mins"|date('Y-m-d\\TH:i') }}" min="{{ "-1year"|date('Y-m-d\\TH:i') }}">
|
||||
@@ -64,7 +64,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
{# The timestamp must be between a year ago and 1 hour in the future #}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="delete_lot_if_empty" value="true" id="withdraw_modal_delete_if_empty">
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
{{ form_row(filterForm.discard) }}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
<button type="button" class="btn btn-danger" {{ stimulus_action('helpers/form_cleanup', 'clearAll') }}>{% trans %}filter.clear_filters{% endtrans %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
{% if displayTrustedOption %}
|
||||
<div class="form-group row mt-3">
|
||||
<div class="offset-3">
|
||||
<div class="{{ offset_label }} {{ col_input }}">
|
||||
<div class="custom-checkbox custom-control ms-2">
|
||||
<input id="_trusted" class="form-check-input" type="checkbox" name="{{ trustedParameterName }}" />
|
||||
<label class="form-check-label" for="_trusted">{% trans %}tfa.code.trusted_pc{% endtrans %}</label>
|
||||
@@ -48,11 +48,11 @@
|
||||
|
||||
{% block submit_btn %}
|
||||
<div class="form-group-row">
|
||||
<div class="offset-3">
|
||||
<div class="{{ offset_label }} {{ col_input }}">
|
||||
<button type="submit" class="btn btn-primary" value="{{ "login"|trans({}, 'SchebTwoFactorBundle') }}">{% trans %}login.btn{% endtrans %}</button>
|
||||
<a class="ms-2" href="{{ logoutPath }}">{% trans %}user.logout{% endtrans %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -20,55 +20,25 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
<form action="{{ path('login') }}" method="post" data-turbo="false" class="form-horizontal">
|
||||
|
||||
<input type="hidden" name="_csrf_token" data-controller="csrf-protection" value="{{ csrf_token('authenticate') }}">
|
||||
{% if saml_enabled %}
|
||||
<div class="{{ offset_label }} {{ col_input }}">
|
||||
<a class="btn btn-secondary" href="{{ path('saml_login') }}"><i class="fa-solid fa-house-user"></i> {% trans %}login.sso_saml_login{% endtrans %}</a>
|
||||
|
||||
<input type="hidden" name="_target_path" value="{{ app.request.query.get('_target_path') }}" />
|
||||
|
||||
{% if saml_enabled %}
|
||||
<div class="col-md-9 offset-md-3 col-lg-10 offset-lg-2">
|
||||
<a class="btn btn-secondary" href="{{ path('saml_login') }}"><i class="fa-solid fa-house-user"></i> {% trans %}login.sso_saml_login{% endtrans %}</a>
|
||||
|
||||
<p class="text-muted">{% trans %}login.local_login_hint{% endtrans %}</p>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label col-md-3 col-lg-2">{% trans %}login.username.label{% endtrans %}</label>
|
||||
<div class="col-md-9 col-lg-10">
|
||||
<input type="text" class="form-control" name="_username" value="{{ last_username }}"
|
||||
placeholder="{% trans %}login.username.placeholder{% endtrans %}" autocomplete="username">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label col-md-3 col-lg-2">{% trans %}login.password.label{% endtrans %}</label>
|
||||
<div class="col-md-9 col-lg-10">
|
||||
<input type="password" class="form-control" placeholder="{% trans %}login.password.placeholder{% endtrans %}" name="_password"
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
<p class="text-muted">{% trans %}login.local_login_hint{% endtrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-md-9 offset-md-3 col-lg-10 offset-lg-2">
|
||||
<div class="custom-control custom-checkbox custom-control-inline">
|
||||
<input class="form-check-input" name="_remember_me" id="remember_me" type="checkbox">
|
||||
<label class="form-check-label" for="remember_me">
|
||||
{% trans %}login.rememberme{% endtrans %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ form_start(form) }}
|
||||
|
||||
<div class="form-group row mt-3">
|
||||
<div class="col-md-9 offset-md-3 col-lg-10 offset-lg-2">
|
||||
<button type="submit" class="btn btn-primary">{% trans %}login.btn{% endtrans %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{ form_row(form._username) }}
|
||||
{{ form_row(form._password) }}
|
||||
{{ form_row(form._remember_me) }}
|
||||
{{ form_row(form.submit) }}
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% if allow_email_pw_reset %}
|
||||
<a class="offset-sm-2" href="{{ path('pw_reset_request') }}">{% trans %}pw_reset.password_forget{% endtrans %}</a>
|
||||
<a class="{{ offset_label }}" href="{{ path('pw_reset_request') }}">{% trans %}pw_reset.password_forget{% endtrans %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -38,12 +38,12 @@
|
||||
|
||||
{% if section_widget.vars.embedded_settings_metadata is defined %} {# Check if we have nested embedded settings or not #}
|
||||
<fieldset>
|
||||
<legend class="offset-3">
|
||||
<legend class="{{ offset_label }}">
|
||||
<i class="fa-solid {{ settings_icon(settings_object)|default('fa-sliders') }} fa-fw"></i>
|
||||
{{ (section_widget.vars.label ?? section_widget.vars.name|humanize)|trans }}
|
||||
</legend>
|
||||
<div class="row">
|
||||
<div class="offset-sm-3 col mb-3 ps-2">
|
||||
<div class="{{ offset_label }} col mb-3 ps-2">
|
||||
<b>{{ form_help(section_widget) }}</b>
|
||||
{{ form_errors(section_widget) }}
|
||||
</div>
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
|
||||
{{ form_start(google_form, { 'attr': google_form_attr}) }}
|
||||
{% if not tfa_google.enabled %}
|
||||
<div class="offset-sm-3">
|
||||
<div class="{{ offset_label }}">
|
||||
<h6>{% trans %}tfa_google.disabled_message{% endtrans %}</h6>
|
||||
</div>
|
||||
|
||||
<div class="offset-sm-3 row">
|
||||
<div class="{{ offset_label }} row">
|
||||
<div class="col-sm-3">
|
||||
<img width="100%" class="img-fluid bg-white p-2" alt="{{ tfa_google.qrContent }}" src="{{ barcode_svg(tfa_google.qrContent) | data_uri("image/svg+xml") }}">
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="offset-sm-3">
|
||||
<div class="{{ offset_label }}">
|
||||
<button class="btn btn-link" type="button" data-bs-toggle="collapse" data-bs-target="#manualSetupCollapse" aria-expanded="false" aria-controls="manualSetupCollapse">
|
||||
{% trans %}tfa_google.manual_setup{% endtrans %}
|
||||
</button>
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
{{ form_row(google_form.google_confirmation) }}
|
||||
{% else %}
|
||||
<div class="offset-sm-3">
|
||||
<div class="{{ offset_label }}">
|
||||
<h6>{% trans %}tfa_google.enabled_message{% endtrans %}</h6>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -81,7 +81,7 @@
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tfa-backup" role="tabpanel" aria-labelledby="backup-tab">
|
||||
{% if user.backupCodes is empty %}
|
||||
<div class="offset-sm-3">
|
||||
<div class="{{ offset_label }}">
|
||||
<h6>{% trans %}tfa_backup.disabled{% endtrans %}</h6>
|
||||
<span>{% trans %}tfa_backup.explanation{% endtrans %}</span>
|
||||
</div>
|
||||
@@ -89,19 +89,19 @@
|
||||
{% set backup_form_attr = { 'data-delete-form': true, 'data-controller': 'elements--delete-btn', 'data-action': 'submit->elements--delete-btn#submit',
|
||||
'data-delete-title': 'tfa_backup.reset_codes.confirm_title' | trans, 'data-delete-message': 'tfa_backup.reset_codes.confirm_message' | trans} %}
|
||||
{{ form_start(backup_form, { 'attr': backup_form_attr}) }}
|
||||
<div class="offset-sm-3">
|
||||
<div class="{{ offset_label }}">
|
||||
<h6>{% trans %}tfa_backup.enabled{% endtrans %}</h6>
|
||||
<span>{% trans %}tfa_backup.explanation{% endtrans %}</span>
|
||||
</div>
|
||||
<div class="offset-sm-3 mt-2">
|
||||
<div class="{{ offset_label }} mt-2">
|
||||
<p class="mb-0"><b>{% trans %}tfa_backup.remaining_tokens{% endtrans %}:</b> {{ user.backupCodes | length }}</p>
|
||||
<p><b>{% trans %}tfa_backup.generation_date{% endtrans %}:</b> {{ user.backupCodesGenerationDate | format_datetime }}</p>
|
||||
</div>
|
||||
<div class="offset-sm-3">
|
||||
<div class="{{ offset_label }}">
|
||||
<a href="{{ path('show_backup_codes') }}" target="_blank" data-turbo="false" class="btn btn-primary">{% trans %}tfa_backup.show_codes{% endtrans %}</a>
|
||||
</div>
|
||||
|
||||
<div class="offset-sm-3 mt-2">
|
||||
<div class="{{ offset_label }} mt-2">
|
||||
{{ form_widget(backup_form.reset_codes) }}
|
||||
</div>
|
||||
{{ form_end(backup_form) }}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{{ form_row(settings_form.showEmailOnProfile) }}
|
||||
{{ form_row(settings_form.avatar_file) }}
|
||||
<div class="mb-3 row {% if user.masterPictureAttachment is null %}d-none{% endif %}">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<div class="{{ offset_label }} {{ col_input }}">
|
||||
{% if user.masterPictureAttachment %}
|
||||
<img src="{{ attachment_thumbnail(user.masterPictureAttachment, 'thumbnail_md') }}" alt="avatar" class="rounded" style="height: 75px;">
|
||||
{% endif %}
|
||||
|
||||
@@ -47,6 +47,32 @@ final class PartLotsEndpointTest extends CrudEndpointTestCase
|
||||
$this->_testGetItem(2);
|
||||
}
|
||||
|
||||
public function testFilterByUserBarcode(): void
|
||||
{
|
||||
$response = self::createAuthenticatedClient()->request('GET', '/api/part_lots?user_barcode=lot2_vendor_barcode');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertJsonContains([
|
||||
'hydra:totalItems' => 1,
|
||||
]);
|
||||
|
||||
$json = $response->toArray();
|
||||
self::assertSame('/api/part_lots/2', $json['hydra:member'][0]['@id']);
|
||||
}
|
||||
|
||||
public function testFilterByUserBarcodeUsingWildcard(): void
|
||||
{
|
||||
$response = self::createAuthenticatedClient()->request('GET', '/api/part_lots?user_barcode=lot2_%');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertJsonContains([
|
||||
'hydra:totalItems' => 1,
|
||||
]);
|
||||
|
||||
$json = $response->toArray();
|
||||
self::assertSame('/api/part_lots/2', $json['hydra:member'][0]['@id']);
|
||||
}
|
||||
|
||||
public function testCreateItem(): void
|
||||
{
|
||||
$this->_testPostItem([
|
||||
|
||||
@@ -121,6 +121,7 @@ final class KiCadApiControllerTest extends WebTestCase
|
||||
'exclude_from_bom' => 'False',
|
||||
'exclude_from_board' => 'True',
|
||||
'exclude_from_sim' => 'False',
|
||||
'description' => '',
|
||||
'fields' =>
|
||||
array(
|
||||
'footprint' =>
|
||||
@@ -203,6 +204,7 @@ final class KiCadApiControllerTest extends WebTestCase
|
||||
'exclude_from_bom' => 'False',
|
||||
'exclude_from_board' => 'True',
|
||||
'exclude_from_sim' => 'False',
|
||||
'description' => '',
|
||||
'fields' =>
|
||||
array (
|
||||
'footprint' =>
|
||||
@@ -318,4 +320,4 @@ final class KiCadApiControllerTest extends WebTestCase
|
||||
self::assertResponseStatusCodeSame(304);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
381
tests/Controller/UpdateManagerControllerTest.php
Normal file
381
tests/Controller/UpdateManagerControllerTest.php
Normal file
@@ -0,0 +1,381 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2024 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\Tests\Controller;
|
||||
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Services\System\BackupManager;
|
||||
use App\Services\System\UpdateExecutor;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
#[Group("slow")]
|
||||
#[Group("DB")]
|
||||
final class UpdateManagerControllerTest extends WebTestCase
|
||||
{
|
||||
private function loginAsAdmin($client): void
|
||||
{
|
||||
$entityManager = $client->getContainer()->get('doctrine')->getManager();
|
||||
$userRepository = $entityManager->getRepository(User::class);
|
||||
$user = $userRepository->findOneBy(['name' => 'admin']);
|
||||
|
||||
if (!$user) {
|
||||
$this->markTestSkipped('Admin user not found');
|
||||
}
|
||||
|
||||
$client->loginUser($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a CSRF token from the rendered update manager page.
|
||||
*/
|
||||
private function getCsrfTokenFromPage($crawler, string $formAction): string
|
||||
{
|
||||
$form = $crawler->filter('form[action*="' . $formAction . '"]');
|
||||
if ($form->count() === 0) {
|
||||
$this->fail('Form with action containing "' . $formAction . '" not found on page');
|
||||
}
|
||||
|
||||
return $form->filter('input[name="_token"]')->attr('value');
|
||||
}
|
||||
|
||||
// ---- Authentication tests ----
|
||||
|
||||
public function testIndexPageRequiresAuth(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('GET', '/en/system/update-manager');
|
||||
|
||||
// Should deny access (401 with HTTP Basic auth in test env)
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
public function testIndexPageAccessibleByAdmin(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
$client->request('GET', '/en/system/update-manager');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
// ---- Backup creation tests ----
|
||||
|
||||
public function testCreateBackupRequiresCsrf(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/backup', [
|
||||
'_token' => 'invalid',
|
||||
]);
|
||||
|
||||
// Should redirect with error flash
|
||||
$this->assertResponseRedirects();
|
||||
}
|
||||
|
||||
public function testCreateBackupWithValidCsrf(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// Load the page and extract CSRF token from the backup form
|
||||
$crawler = $client->request('GET', '/en/system/update-manager');
|
||||
$csrfToken = $this->getCsrfTokenFromPage($crawler, 'backup');
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/backup', [
|
||||
'_token' => $csrfToken,
|
||||
]);
|
||||
|
||||
$this->assertResponseRedirects();
|
||||
|
||||
// Clean up: delete the backup that was just created
|
||||
$backupManager = $client->getContainer()->get(BackupManager::class);
|
||||
$backups = $backupManager->getBackups();
|
||||
foreach ($backups as $backup) {
|
||||
if (str_contains($backup['file'], 'manual')) {
|
||||
$backupManager->deleteBackup($backup['file']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testCreateBackupBlockedWhenLocked(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// Load the page first to get CSRF token before locking
|
||||
$crawler = $client->request('GET', '/en/system/update-manager');
|
||||
$csrfToken = $this->getCsrfTokenFromPage($crawler, 'backup');
|
||||
|
||||
// Acquire lock to simulate update in progress
|
||||
$updateExecutor = $client->getContainer()->get(UpdateExecutor::class);
|
||||
$updateExecutor->acquireLock();
|
||||
|
||||
try {
|
||||
$client->request('POST', '/en/system/update-manager/backup', [
|
||||
'_token' => $csrfToken,
|
||||
]);
|
||||
|
||||
$this->assertResponseRedirects();
|
||||
} finally {
|
||||
// Always release lock
|
||||
$updateExecutor->releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Backup deletion tests ----
|
||||
|
||||
public function testDeleteBackupRequiresCsrf(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/backup/delete', [
|
||||
'_token' => 'invalid',
|
||||
'filename' => 'test.zip',
|
||||
]);
|
||||
|
||||
$this->assertResponseRedirects();
|
||||
}
|
||||
|
||||
public function testDeleteBackupWithValidCsrf(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// Create a temporary backup file so the page shows the delete form
|
||||
$backupManager = $client->getContainer()->get(BackupManager::class);
|
||||
$backupDir = $backupManager->getBackupDir();
|
||||
if (!is_dir($backupDir)) {
|
||||
mkdir($backupDir, 0755, true);
|
||||
}
|
||||
$testFile = 'test-delete-' . uniqid() . '.zip';
|
||||
file_put_contents($backupDir . '/' . $testFile, 'test');
|
||||
|
||||
// Load the page and extract CSRF token from the delete form
|
||||
$crawler = $client->request('GET', '/en/system/update-manager');
|
||||
$csrfToken = $this->getCsrfTokenFromPage($crawler, 'backup/delete');
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/backup/delete', [
|
||||
'_token' => $csrfToken,
|
||||
'filename' => $testFile,
|
||||
]);
|
||||
|
||||
$this->assertResponseRedirects();
|
||||
$this->assertFileDoesNotExist($backupDir . '/' . $testFile);
|
||||
}
|
||||
|
||||
// ---- Log deletion tests ----
|
||||
|
||||
public function testDeleteLogRequiresCsrf(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/log/delete', [
|
||||
'_token' => 'invalid',
|
||||
'filename' => 'test.log',
|
||||
]);
|
||||
|
||||
$this->assertResponseRedirects();
|
||||
}
|
||||
|
||||
public function testDeleteLogWithValidCsrf(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// Create a temporary log file so the page shows the delete form
|
||||
$projectDir = $client->getContainer()->getParameter('kernel.project_dir');
|
||||
$logDir = $projectDir . '/var/log/updates';
|
||||
if (!is_dir($logDir)) {
|
||||
mkdir($logDir, 0755, true);
|
||||
}
|
||||
$testFile = 'update-test-delete-' . uniqid() . '.log';
|
||||
file_put_contents($logDir . '/' . $testFile, 'test log content');
|
||||
|
||||
// Load the page and extract CSRF token from the log delete form
|
||||
$crawler = $client->request('GET', '/en/system/update-manager');
|
||||
$csrfToken = $this->getCsrfTokenFromPage($crawler, 'log/delete');
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/log/delete', [
|
||||
'_token' => $csrfToken,
|
||||
'filename' => $testFile,
|
||||
]);
|
||||
|
||||
$this->assertResponseRedirects();
|
||||
$this->assertFileDoesNotExist($logDir . '/' . $testFile);
|
||||
}
|
||||
|
||||
// ---- Backup download tests ----
|
||||
|
||||
public function testDownloadBackupBlockedByDefault(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// DISABLE_BACKUP_DOWNLOAD=1 is the default in .env, so this should return 403
|
||||
$client->request('POST', '/en/system/update-manager/backup/download', [
|
||||
'_token' => 'any',
|
||||
'filename' => 'test.zip',
|
||||
'password' => 'test',
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
public function testDownloadBackupRequiresPost(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// GET returns 404 since no GET route exists for this path
|
||||
$client->request('GET', '/en/system/update-manager/backup/download');
|
||||
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
public function testDownloadBackupRequiresAuth(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/backup/download', [
|
||||
'_token' => 'any',
|
||||
'filename' => 'test.zip',
|
||||
'password' => 'test',
|
||||
]);
|
||||
|
||||
// Should deny access (401 with HTTP Basic auth in test env)
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
// ---- Backup details tests ----
|
||||
|
||||
public function testBackupDetailsReturns404ForNonExistent(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
$client->request('GET', '/en/system/update-manager/backup/nonexistent.zip');
|
||||
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
// ---- Restore tests ----
|
||||
|
||||
public function testRestoreBlockedWhenDisabled(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// DISABLE_BACKUP_RESTORE=1 is the default in .env, so this should return 403
|
||||
$client->request('POST', '/en/system/update-manager/restore', [
|
||||
'_token' => 'invalid',
|
||||
'filename' => 'test.zip',
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
public function testRestoreRequiresAuth(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/restore', [
|
||||
'_token' => 'invalid',
|
||||
'filename' => 'test.zip',
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
// ---- Start update tests ----
|
||||
|
||||
public function testStartUpdateRequiresAuth(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/start', [
|
||||
'_token' => 'invalid',
|
||||
'version' => 'v1.0.0',
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
public function testStartUpdateBlockedWhenWebUpdatesDisabled(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// DISABLE_WEB_UPDATES=1 is the default in .env
|
||||
$client->request('POST', '/en/system/update-manager/start', [
|
||||
'_token' => 'invalid',
|
||||
'version' => 'v1.0.0',
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
// ---- Status and progress tests ----
|
||||
|
||||
public function testStatusEndpointRequiresAuth(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('GET', '/en/system/update-manager/status');
|
||||
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
public function testStatusEndpointAccessibleByAdmin(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
$client->request('GET', '/en/system/update-manager/status');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testProgressStatusEndpointRequiresAuth(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('GET', '/en/system/update-manager/progress/status');
|
||||
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
public function testProgressStatusEndpointAccessibleByAdmin(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
$client->request('GET', '/en/system/update-manager/progress/status');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
}
|
||||
@@ -85,4 +85,41 @@ final class StructuralElementDenormalizerTest extends WebTestCase
|
||||
$result2 = $this->service->denormalize($data, Category::class, 'json', ['groups' => ['import']]);
|
||||
$this->assertSame($result, $result2);
|
||||
}
|
||||
|
||||
public function testDenormalizeViaChildren(): void
|
||||
{
|
||||
$data = ['name' => 'Node',
|
||||
'children' => [
|
||||
['name' => 'A', 'children' => [['name' => '1'], ['name' => '2']]],
|
||||
['name' => 'B', 'children' => [['name' => '1'], ['name' => '2']]],
|
||||
['name' => 'C', 'children' => [['name' => '1'], ['name' => '2'], ['name' => '3']]],
|
||||
]
|
||||
];
|
||||
|
||||
$result = $this->service->denormalize($data, Category::class, 'json', ['groups' => ['import', 'include_children']]);
|
||||
$this->assertInstanceOf(Category::class, $result);
|
||||
|
||||
$this->assertCount(3, $result->getChildren());
|
||||
$this->assertSame('A', $result->getChildren()[0]->getName());
|
||||
$this->assertSame('B', $result->getChildren()[1]->getName());
|
||||
$this->assertSame('C', $result->getChildren()[2]->getName());
|
||||
//Parents should be set correctly
|
||||
$this->assertSame($result, $result->getChildren()[0]->getParent());
|
||||
$this->assertSame($result, $result->getChildren()[1]->getParent());
|
||||
$this->assertSame($result, $result->getChildren()[2]->getParent());
|
||||
|
||||
$this->assertCount(2, $result->getChildren()[0]->getChildren());
|
||||
$this->assertSame('1', $result->getChildren()[0]->getChildren()[0]->getName());
|
||||
$this->assertSame('2', $result->getChildren()[0]->getChildren()[1]->getName());
|
||||
//Parents should be set correctly
|
||||
$this->assertSame($result->getChildren()[0], $result->getChildren()[0]->getChildren()[0]->getParent());
|
||||
$this->assertSame($result->getChildren()[0], $result->getChildren()[0]->getChildren()[1]->getParent());
|
||||
|
||||
$this->assertCount(2, $result->getChildren()[1]->getChildren());
|
||||
$this->assertSame('1', $result->getChildren()[1]->getChildren()[0]->getName());
|
||||
$this->assertSame('2', $result->getChildren()[1]->getChildren()[1]->getName());
|
||||
//Must be different instances than the children of A, because we create new elements for the same path, if we don't have them in the DB
|
||||
$this->assertNotSame($result->getChildren()[0]->getChildren()[0], $result->getChildren()[1]->getChildren()[0]);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,8 @@ final class BarcodeScanHelperTest extends WebTestCase
|
||||
yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 2,BarcodeSourceType::USER_DEFINED),
|
||||
'lot2_vendor_barcode'];
|
||||
|
||||
|
||||
$input = "[)>\x1E06\x1DP596-777A1-ND\x1D1PXAF4444\x1DQ3\x1D10D1452\x1D1TBF1103\x1D4LUS\x1E\x04";
|
||||
$eigp114Result = new EIGP114BarcodeScanResult([
|
||||
'P' => '596-777A1-ND',
|
||||
'1P' => 'XAF4444',
|
||||
@@ -122,9 +124,9 @@ final class BarcodeScanHelperTest extends WebTestCase
|
||||
'10D' => '1452',
|
||||
'1T' => 'BF1103',
|
||||
'4L' => 'US',
|
||||
]);
|
||||
], $input);
|
||||
|
||||
yield [$eigp114Result, "[)>\x1E06\x1DP596-777A1-ND\x1D1PXAF4444\x1DQ3\x1D10D1452\x1D1TBF1103\x1D4LUS\x1E\x04"];
|
||||
yield [$eigp114Result, $input];
|
||||
|
||||
$lcscInput = '{pc:C138033,pm:RC0402FR-071ML,qty:10}';
|
||||
$lcscResult = new LCSCBarcodeScanResult(
|
||||
|
||||
@@ -82,6 +82,16 @@ final class BackupManagerTest extends KernelTestCase
|
||||
$this->assertSame('2.6.0', $matches[2]);
|
||||
}
|
||||
|
||||
public function testDeleteBackupReturnsFalseForNonExistentFile(): void
|
||||
{
|
||||
$this->assertFalse($this->backupManager->deleteBackup('non-existent.zip'));
|
||||
}
|
||||
|
||||
public function testDeleteBackupReturnsFalseForNonZipFile(): void
|
||||
{
|
||||
$this->assertFalse($this->backupManager->deleteBackup('not-a-zip.txt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test version parsing with different filename formats.
|
||||
*/
|
||||
|
||||
@@ -139,6 +139,38 @@ final class UpdateExecutorTest extends KernelTestCase
|
||||
$this->assertFalse($this->updateExecutor->isLocked());
|
||||
}
|
||||
|
||||
public function testDeleteLogRejectsInvalidFilename(): void
|
||||
{
|
||||
// Path traversal attempts should be rejected
|
||||
$this->assertFalse($this->updateExecutor->deleteLog('../../../etc/passwd'));
|
||||
$this->assertFalse($this->updateExecutor->deleteLog('malicious.txt'));
|
||||
$this->assertFalse($this->updateExecutor->deleteLog(''));
|
||||
// Must start with "update-"
|
||||
$this->assertFalse($this->updateExecutor->deleteLog('backup-v1.0.0.log'));
|
||||
}
|
||||
|
||||
public function testDeleteLogReturnsFalseForNonExistentFile(): void
|
||||
{
|
||||
$this->assertFalse($this->updateExecutor->deleteLog('update-nonexistent-file.log'));
|
||||
}
|
||||
|
||||
public function testDeleteLogDeletesExistingFile(): void
|
||||
{
|
||||
// Create a temporary log file in the update logs directory
|
||||
$projectDir = self::getContainer()->getParameter('kernel.project_dir');
|
||||
$logDir = $projectDir . '/var/log/updates';
|
||||
|
||||
if (!is_dir($logDir)) {
|
||||
mkdir($logDir, 0755, true);
|
||||
}
|
||||
|
||||
$testFile = 'update-test-delete-' . uniqid() . '.log';
|
||||
file_put_contents($logDir . '/' . $testFile, 'test log content');
|
||||
|
||||
$this->assertTrue($this->updateExecutor->deleteLog($testFile));
|
||||
$this->assertFileDoesNotExist($logDir . '/' . $testFile);
|
||||
}
|
||||
|
||||
public function testEnableAndDisableMaintenanceMode(): void
|
||||
{
|
||||
// First, ensure maintenance mode is off
|
||||
|
||||
@@ -12489,6 +12489,102 @@ Buerklin-API-Authentication-Server:
|
||||
<target>Backup Wiederherstellungen wurden durch Serverkonfiguration deaktiviert.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="oAb35wU" name="update_manager.backup.create">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.create</source>
|
||||
<target>Backup erstellen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ms26oI0" name="update_manager.backup.create.confirm">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.create.confirm</source>
|
||||
<target>Jetzt ein vollständiges Backup erstellen? Dies kann einen Moment dauern.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="H9y0eLa" name="update_manager.backup.created">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.created</source>
|
||||
<target>Backup erfolgreich erstellt.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bMhXPVB" name="update_manager.backup.delete.confirm">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.delete.confirm</source>
|
||||
<target>Sind Sie sicher, dass Sie dieses Backup löschen möchten?</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="8tw67c_" name="update_manager.backup.deleted">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.deleted</source>
|
||||
<target>Backup erfolgreich gelöscht.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BzBBuqk" name="update_manager.backup.delete_error">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.delete_error</source>
|
||||
<target>Backup konnte nicht gelöscht werden.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="2olmcSs" name="update_manager.log.delete.confirm">
|
||||
<segment state="translated">
|
||||
<source>update_manager.log.delete.confirm</source>
|
||||
<target>Sind Sie sicher, dass Sie dieses Protokoll löschen möchten?</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id=".ZrVHpp" name="update_manager.log.deleted">
|
||||
<segment state="translated">
|
||||
<source>update_manager.log.deleted</source>
|
||||
<target>Protokoll erfolgreich gelöscht.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="P2JI5Yw" name="update_manager.log.delete_error">
|
||||
<segment state="translated">
|
||||
<source>update_manager.log.delete_error</source>
|
||||
<target>Protokoll konnte nicht gelöscht werden.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Yos9FWk" name="update_manager.view_log">
|
||||
<segment state="translated">
|
||||
<source>update_manager.view_log</source>
|
||||
<target>Protokoll anzeigen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="B9uA2va" name="update_manager.delete">
|
||||
<segment state="translated">
|
||||
<source>update_manager.delete</source>
|
||||
<target>Löschen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ZtgvnXB" name="update_manager.backup.download">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download</source>
|
||||
<target>Backup herunterladen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="wxtmrnP" name="update_manager.backup.download.password_label">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download.password_label</source>
|
||||
<target>Bestätigen Sie Ihr Passwort zum Herunterladen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="MIlTTgL" name="update_manager.backup.download.security_warning">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download.security_warning</source>
|
||||
<target>Backups enthalten sensible Daten, einschließlich Passwort-Hashes und Geheimnisse. Bitte bestätigen Sie Ihr Passwort, um mit dem Download fortzufahren.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kZPHBRt" name="update_manager.backup.download.invalid_password">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download.invalid_password</source>
|
||||
<target>Ungültiges Passwort. Backup-Download verweigert.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="AZOjnE0" name="update_manager.backup.docker_warning">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.docker_warning</source>
|
||||
<target>Docker-Installation erkannt. Backups werden in var/backups/ gespeichert, was kein persistentes Volume ist. Verwenden Sie die Download-Schaltfläche, um Backups extern zu speichern, oder binden Sie var/backups/ als Volume in Ihrer docker-compose.yml ein.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kHKChQB" name="settings.ips.conrad">
|
||||
<segment state="translated">
|
||||
<source>settings.ips.conrad</source>
|
||||
@@ -12819,5 +12915,35 @@ Buerklin-API-Authentication-Server:
|
||||
<target>Als Klartext anzeigen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Ehsj93c" name="modal.cancel">
|
||||
<segment state="translated">
|
||||
<source>modal.cancel</source>
|
||||
<target>Abbrechen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="jdpoFf2" name="update_manager.web_updates_allowed">
|
||||
<segment state="translated">
|
||||
<source>update_manager.web_updates_allowed</source>
|
||||
<target>Web-Updates erlaubt</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bdWa7is" name="update_manager.backup_restore_allowed">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup_restore_allowed</source>
|
||||
<target>Backup-Wiederherstellung erlaubt</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kllGQEN" name="update_manager.backup_download_allowed">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup_download_allowed</source>
|
||||
<target>Backup-Download erlaubt</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="b8JxfcX" name="part.create_from_info_provider.lot_filled_from_barcode">
|
||||
<segment state="translated">
|
||||
<source>part.create_from_info_provider.lot_filled_from_barcode</source>
|
||||
<target>[Part_lot] aus Barcode erstellt: Bitte überprüfen Sie, ob die Daten korrekt und gewünscht sind.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -12491,6 +12491,102 @@ Buerklin-API Authentication server:
|
||||
<target>Backup restore is disabled by server configuration.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="oAb35wU" name="update_manager.backup.create">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.create</source>
|
||||
<target>Create Backup</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ms26oI0" name="update_manager.backup.create.confirm">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.create.confirm</source>
|
||||
<target>Create a full backup now? This may take a moment.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="H9y0eLa" name="update_manager.backup.created">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.created</source>
|
||||
<target>Backup created successfully.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bMhXPVB" name="update_manager.backup.delete.confirm">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.delete.confirm</source>
|
||||
<target>Are you sure you want to delete this backup?</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="8tw67c_" name="update_manager.backup.deleted">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.deleted</source>
|
||||
<target>Backup deleted successfully.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BzBBuqk" name="update_manager.backup.delete_error">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.delete_error</source>
|
||||
<target>Failed to delete backup.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="2olmcSs" name="update_manager.log.delete.confirm">
|
||||
<segment state="translated">
|
||||
<source>update_manager.log.delete.confirm</source>
|
||||
<target>Are you sure you want to delete this log?</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id=".ZrVHpp" name="update_manager.log.deleted">
|
||||
<segment state="translated">
|
||||
<source>update_manager.log.deleted</source>
|
||||
<target>Log deleted successfully.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="P2JI5Yw" name="update_manager.log.delete_error">
|
||||
<segment state="translated">
|
||||
<source>update_manager.log.delete_error</source>
|
||||
<target>Failed to delete log.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Yos9FWk" name="update_manager.view_log">
|
||||
<segment state="translated">
|
||||
<source>update_manager.view_log</source>
|
||||
<target>View log</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="B9uA2va" name="update_manager.delete">
|
||||
<segment state="translated">
|
||||
<source>update_manager.delete</source>
|
||||
<target>Delete</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ZtgvnXB" name="update_manager.backup.download">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download</source>
|
||||
<target>Download backup</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="wxtmrnP" name="update_manager.backup.download.password_label">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download.password_label</source>
|
||||
<target>Confirm your password to download</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="MIlTTgL" name="update_manager.backup.download.security_warning">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download.security_warning</source>
|
||||
<target>Backups contain sensitive data including password hashes and secrets. Please confirm your password to proceed with the download.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kZPHBRt" name="update_manager.backup.download.invalid_password">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download.invalid_password</source>
|
||||
<target>Invalid password. Backup download denied.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="AZOjnE0" name="update_manager.backup.docker_warning">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.docker_warning</source>
|
||||
<target>Docker installation detected. Backups are stored in var/backups/ which is not a persistent volume. Use the download button to save backups externally, or mount var/backups/ as a volume in your docker-compose.yml.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kHKChQB" name="settings.ips.conrad">
|
||||
<segment state="translated">
|
||||
<source>settings.ips.conrad</source>
|
||||
@@ -12821,5 +12917,35 @@ Buerklin-API Authentication server:
|
||||
<target>View as plain text</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Ehsj93c" name="modal.cancel">
|
||||
<segment state="translated">
|
||||
<source>modal.cancel</source>
|
||||
<target>Cancel</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="jdpoFf2" name="update_manager.web_updates_allowed">
|
||||
<segment state="translated">
|
||||
<source>update_manager.web_updates_allowed</source>
|
||||
<target>Web updates allowed</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bdWa7is" name="update_manager.backup_restore_allowed">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup_restore_allowed</source>
|
||||
<target>Backup restore allowed</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kllGQEN" name="update_manager.backup_download_allowed">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup_download_allowed</source>
|
||||
<target>Backup download allowed</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="b8JxfcX" name="part.create_from_info_provider.lot_filled_from_barcode">
|
||||
<segment state="translated">
|
||||
<source>part.create_from_info_provider.lot_filled_from_barcode</source>
|
||||
<target>[Part_lot] created from barcode: Please check if the data is correct and desired.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -247,5 +247,11 @@
|
||||
<target>Es existiert bereits eine Übersetzung für diesen Typ und Sprache!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="zT_j_oQ" name="validator.invalid_gtin">
|
||||
<segment state="translated">
|
||||
<source>validator.invalid_gtin</source>
|
||||
<target>Dies ist keine gültige GTIN / EAN!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
</xliff>
|
||||
|
||||
242
yarn.lock
242
yarn.lock
@@ -141,10 +141,10 @@
|
||||
regexpu-core "^6.3.1"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/helper-define-polyfill-provider@^0.6.6":
|
||||
version "0.6.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz#714dfe33d8bd710f556df59953720f6eeb6c1a14"
|
||||
integrity sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==
|
||||
"@babel/helper-define-polyfill-provider@^0.6.7":
|
||||
version "0.6.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.7.tgz#8d01cba97de419115ad3497573a476db15dc6c6a"
|
||||
integrity sha512-6Fqi8MtQ/PweQ9xvux65emkLQ83uB+qAVtfHkC9UodyHMIZdxNI01HjLCLUtybElp2KY2XNE0nOgyP1E1vXw9w==
|
||||
dependencies:
|
||||
"@babel/helper-compilation-targets" "^7.28.6"
|
||||
"@babel/helper-plugin-utils" "^7.28.6"
|
||||
@@ -1859,9 +1859,9 @@
|
||||
integrity sha512-GZ7cijxEZ6Ig71u7rD6LHaRv/wcE/hNsc+nEfiWOkLNqUgLOwo5MNGWOy5ZV9ZUDSiQx1no7YxjTNnT4O6//cQ==
|
||||
|
||||
"@jbtronics/bs-treeview@^1.0.1":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@jbtronics/bs-treeview/-/bs-treeview-1.0.6.tgz#7fe126a2ca4716c824d97ab6d1a5f2417750445a"
|
||||
integrity sha512-fLY2tnbDYO4kCjpmGQyfvoHN8x72Rk5p+tTgVjKJUbiJHHhXt0yIW+l5P83CwYtPD5EwS7kMKc8RLuU1xA2bpA==
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@jbtronics/bs-treeview/-/bs-treeview-1.0.7.tgz#42a5ea40ce1bfe6cffbc1b811dc4e32dd8d0273a"
|
||||
integrity sha512-AvEdkQNkNvh9+yGGHto8ABBsicEzFjLtSSbl61c9D0yq+RrIsrwTpz/H3RmDhvdtdteywQRItVuS18XOc+0p2A==
|
||||
|
||||
"@jest/pattern@30.0.1":
|
||||
version "30.0.1"
|
||||
@@ -2156,9 +2156,9 @@
|
||||
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
|
||||
|
||||
"@types/node@*":
|
||||
version "25.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.3.3.tgz#605862544ee7ffd7a936bcbf0135a14012f1e549"
|
||||
integrity sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==
|
||||
version "25.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.3.5.tgz#beccb5915561f7a9970ace547ad44d6cdbf39b46"
|
||||
integrity sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==
|
||||
dependencies:
|
||||
undici-types "~7.18.0"
|
||||
|
||||
@@ -2556,35 +2556,35 @@ available-typed-arrays@^1.0.7:
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
"babel-loader@^9.1.3 || ^10.0.0":
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-10.0.0.tgz#b9743714c0e1e084b3e4adef3cd5faee33089977"
|
||||
integrity sha512-z8jt+EdS61AMw22nSfoNJAZ0vrtmhPRVi6ghL3rCeRZI8cdNYFiV5xeV3HbE7rlZZNmGH8BVccwWt8/ED0QOHA==
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-10.1.0.tgz#3ac62a9a47dba6b1be77fb24764ec5784981fbbe"
|
||||
integrity sha512-5HTUZa013O4SWEYlJDHexrqSIYkWatfA9w/ZZQa7V2nMc0dRWkfu/0pmioC7XMYm8M7Z/3+q42NWj6e+fAT0MQ==
|
||||
dependencies:
|
||||
find-up "^5.0.0"
|
||||
|
||||
babel-plugin-polyfill-corejs2@^0.4.15:
|
||||
version "0.4.15"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz#808fa349686eea4741807cfaaa2aa3aa57ce120a"
|
||||
integrity sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==
|
||||
version "0.4.16"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.16.tgz#a1321145f6cde738b0a412616b6bcf77f143ab36"
|
||||
integrity sha512-xaVwwSfebXf0ooE11BJovZYKhFjIvQo7TsyVpETuIeH2JHv0k/T6Y5j22pPTvqYqmpkxdlPAJlyJ0tfOJAoMxw==
|
||||
dependencies:
|
||||
"@babel/compat-data" "^7.28.6"
|
||||
"@babel/helper-define-polyfill-provider" "^0.6.6"
|
||||
"@babel/helper-define-polyfill-provider" "^0.6.7"
|
||||
semver "^6.3.1"
|
||||
|
||||
babel-plugin-polyfill-corejs3@^0.14.0:
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz#65b06cda48d6e447e1e926681f5a247c6ae2b9cf"
|
||||
integrity sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==
|
||||
version "0.14.1"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.1.tgz#75fb533a1c23c0a976f189cba1d035199705b8ad"
|
||||
integrity sha512-ENp89vM9Pw4kv/koBb5N2f9bDZsR0hpf3BdPMOg/pkS3pwO4dzNnQZVXtBbeyAadgm865DmQG2jMMLqmZXvuCw==
|
||||
dependencies:
|
||||
"@babel/helper-define-polyfill-provider" "^0.6.6"
|
||||
"@babel/helper-define-polyfill-provider" "^0.6.7"
|
||||
core-js-compat "^3.48.0"
|
||||
|
||||
babel-plugin-polyfill-regenerator@^0.6.6:
|
||||
version "0.6.6"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz#69f5dd263cab933c42fe5ea05e83443b374bd4bf"
|
||||
integrity sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==
|
||||
version "0.6.7"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.7.tgz#eca723d67ef87b798881ad00546db1b6dd72e1ef"
|
||||
integrity sha512-OTYbUlSwXhNgr4g6efMZgsO8//jA61P7ZbRX3iTT53VON8l+WQS8IAUEVo4a4cWknrg2W8Cj4gQhRYNCJ8GkAA==
|
||||
dependencies:
|
||||
"@babel/helper-define-polyfill-provider" "^0.6.6"
|
||||
"@babel/helper-define-polyfill-provider" "^0.6.7"
|
||||
|
||||
bail@^2.0.0:
|
||||
version "2.0.2"
|
||||
@@ -2692,7 +2692,7 @@ browser-stdout@1.3.1:
|
||||
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
|
||||
integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
|
||||
|
||||
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.27.0, browserslist@^4.28.1:
|
||||
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.28.1:
|
||||
version "4.28.1"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95"
|
||||
integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==
|
||||
@@ -2789,9 +2789,9 @@ caniuse-api@^3.0.0:
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001759:
|
||||
version "1.0.30001776"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz#3c64d006348a2e92037aa4302345129806a42d24"
|
||||
integrity sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==
|
||||
version "1.0.30001777"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz#028f21e4b2718d138b55e692583e6810ccf60691"
|
||||
integrity sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==
|
||||
|
||||
ccount@^2.0.0:
|
||||
version "2.0.1"
|
||||
@@ -3220,12 +3220,12 @@ css-tree@^2.3.1:
|
||||
source-map-js "^1.0.1"
|
||||
|
||||
css-tree@^3.0.1:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-3.1.0.tgz#7aabc035f4e66b5c86f54570d55e05b1346eb0fd"
|
||||
integrity sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-3.2.1.tgz#86cac7011561272b30e6b1e042ba6ce047aa7518"
|
||||
integrity sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==
|
||||
dependencies:
|
||||
mdn-data "2.12.2"
|
||||
source-map-js "^1.0.1"
|
||||
mdn-data "2.27.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
css-tree@~2.2.0:
|
||||
version "2.2.1"
|
||||
@@ -3281,41 +3281,41 @@ cssnano-preset-default@^6.1.2:
|
||||
postcss-svgo "^6.0.3"
|
||||
postcss-unique-selectors "^6.0.4"
|
||||
|
||||
cssnano-preset-default@^7.0.10:
|
||||
version "7.0.10"
|
||||
resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-7.0.10.tgz#4fb6ee962c0852a03084e8c7a4b60fb0e2db45e0"
|
||||
integrity sha512-6ZBjW0Lf1K1Z+0OKUAUpEN62tSXmYChXWi2NAA0afxEVsj9a+MbcB1l5qel6BHJHmULai2fCGRthCeKSFbScpA==
|
||||
cssnano-preset-default@^7.0.11:
|
||||
version "7.0.11"
|
||||
resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-7.0.11.tgz#ea81661d0e8fe59b752560cca4a9f2fac763e92c"
|
||||
integrity sha512-waWlAMuCakP7//UCY+JPrQS1z0OSLeOXk2sKWJximKWGupVxre50bzPlvpbUwZIDylhf/ptf0Pk+Yf7C+hoa3g==
|
||||
dependencies:
|
||||
browserslist "^4.27.0"
|
||||
browserslist "^4.28.1"
|
||||
css-declaration-sorter "^7.2.0"
|
||||
cssnano-utils "^5.0.1"
|
||||
postcss-calc "^10.1.1"
|
||||
postcss-colormin "^7.0.5"
|
||||
postcss-convert-values "^7.0.8"
|
||||
postcss-discard-comments "^7.0.5"
|
||||
postcss-colormin "^7.0.6"
|
||||
postcss-convert-values "^7.0.9"
|
||||
postcss-discard-comments "^7.0.6"
|
||||
postcss-discard-duplicates "^7.0.2"
|
||||
postcss-discard-empty "^7.0.1"
|
||||
postcss-discard-overridden "^7.0.1"
|
||||
postcss-merge-longhand "^7.0.5"
|
||||
postcss-merge-rules "^7.0.7"
|
||||
postcss-merge-rules "^7.0.8"
|
||||
postcss-minify-font-values "^7.0.1"
|
||||
postcss-minify-gradients "^7.0.1"
|
||||
postcss-minify-params "^7.0.5"
|
||||
postcss-minify-selectors "^7.0.5"
|
||||
postcss-minify-params "^7.0.6"
|
||||
postcss-minify-selectors "^7.0.6"
|
||||
postcss-normalize-charset "^7.0.1"
|
||||
postcss-normalize-display-values "^7.0.1"
|
||||
postcss-normalize-positions "^7.0.1"
|
||||
postcss-normalize-repeat-style "^7.0.1"
|
||||
postcss-normalize-string "^7.0.1"
|
||||
postcss-normalize-timing-functions "^7.0.1"
|
||||
postcss-normalize-unicode "^7.0.5"
|
||||
postcss-normalize-unicode "^7.0.6"
|
||||
postcss-normalize-url "^7.0.1"
|
||||
postcss-normalize-whitespace "^7.0.1"
|
||||
postcss-ordered-values "^7.0.2"
|
||||
postcss-reduce-initial "^7.0.5"
|
||||
postcss-reduce-initial "^7.0.6"
|
||||
postcss-reduce-transforms "^7.0.1"
|
||||
postcss-svgo "^7.1.0"
|
||||
postcss-unique-selectors "^7.0.4"
|
||||
postcss-svgo "^7.1.1"
|
||||
postcss-unique-selectors "^7.0.5"
|
||||
|
||||
cssnano-utils@^4.0.2:
|
||||
version "4.0.2"
|
||||
@@ -3336,11 +3336,11 @@ cssnano@^6.0.3:
|
||||
lilconfig "^3.1.1"
|
||||
|
||||
cssnano@^7.0.4:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-7.1.2.tgz#a8a533a8f509d74b2d22e73d80ec1294f65fdc70"
|
||||
integrity sha512-HYOPBsNvoiFeR1eghKD5C3ASm64v9YVyJB4Ivnl2gqKoQYvjjN/G0rztvKQq8OxocUtC6sjqY8jwYngIB4AByA==
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-7.1.3.tgz#2a542bb8d62b6bee9e23e455ba2e507fd102f611"
|
||||
integrity sha512-mLFHQAzyapMVFLiJIn7Ef4C2UCEvtlTlbyILR6B5ZsUAV3D/Pa761R5uC1YPhyBkRd3eqaDm2ncaNrD7R4mTRg==
|
||||
dependencies:
|
||||
cssnano-preset-default "^7.0.10"
|
||||
cssnano-preset-default "^7.0.11"
|
||||
lilconfig "^3.1.3"
|
||||
|
||||
csso@^5.0.5:
|
||||
@@ -3636,9 +3636,9 @@ domhandler@^5.0.2, domhandler@^5.0.3:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
dompurify@^3.0.3:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.1.tgz#c7e1ddebfe3301eacd6c0c12a4af284936dbbb86"
|
||||
integrity sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.2.tgz#58c515d0f8508b8749452a028aa589ad80b36325"
|
||||
integrity sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==
|
||||
optionalDependencies:
|
||||
"@types/trusted-types" "^2.0.7"
|
||||
|
||||
@@ -4956,9 +4956,9 @@ jszip@^3.2.0:
|
||||
setimmediate "^1.0.5"
|
||||
|
||||
katex@^0.16.0:
|
||||
version "0.16.33"
|
||||
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.33.tgz#5cd5af2ddc1132fe6a710ae6604ec1f19fca9e91"
|
||||
integrity sha512-q3N5u+1sY9Bu7T4nlXoiRBXWfwSefNGoKeOwekV+gw0cAXQlz2Ww6BLcmBxVDeXBMUDQv6fK5bcNaJLxob3ZQA==
|
||||
version "0.16.37"
|
||||
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.37.tgz#aae346d30ebfde946c915405a48099d71ab9b149"
|
||||
integrity sha512-TIGjO2cCGYono+uUzgkE7RFF329mLLWGuHUlSr6cwIVj9O8f0VQZ783rsanmJpFUo32vvtj7XT04NGRPh+SZFg==
|
||||
dependencies:
|
||||
commander "^8.3.0"
|
||||
|
||||
@@ -5266,10 +5266,10 @@ mdn-data@2.0.30:
|
||||
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc"
|
||||
integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==
|
||||
|
||||
mdn-data@2.12.2:
|
||||
version "2.12.2"
|
||||
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.12.2.tgz#9ae6c41a9e65adf61318b32bff7b64fbfb13f8cf"
|
||||
integrity sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==
|
||||
mdn-data@2.27.1:
|
||||
version "2.27.1"
|
||||
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.27.1.tgz#e37b9c50880b75366c4d40ac63d9bbcacdb61f0e"
|
||||
integrity sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==
|
||||
|
||||
merge-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
@@ -6033,12 +6033,12 @@ postcss-colormin@^6.1.0:
|
||||
colord "^2.9.3"
|
||||
postcss-value-parser "^4.2.0"
|
||||
|
||||
postcss-colormin@^7.0.5:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-7.0.5.tgz#0c7526289ab3f0daf96a376fd7430fae7258d5cf"
|
||||
integrity sha512-ekIBP/nwzRWhEMmIxHHbXHcMdzd1HIUzBECaj5KEdLz9DVP2HzT065sEhvOx1dkLjYW7jyD0CngThx6bpFi2fA==
|
||||
postcss-colormin@^7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-7.0.6.tgz#8f1bcfaa6f4959a872824f3b5bd4e1278bf35e45"
|
||||
integrity sha512-oXM2mdx6IBTRm39797QguYzVEWzbdlFiMNfq88fCCN1Wepw3CYmJ/1/Ifa/KjWo+j5ZURDl2NTldLJIw51IeNQ==
|
||||
dependencies:
|
||||
browserslist "^4.27.0"
|
||||
browserslist "^4.28.1"
|
||||
caniuse-api "^3.0.0"
|
||||
colord "^2.9.3"
|
||||
postcss-value-parser "^4.2.0"
|
||||
@@ -6051,12 +6051,12 @@ postcss-convert-values@^6.1.0:
|
||||
browserslist "^4.23.0"
|
||||
postcss-value-parser "^4.2.0"
|
||||
|
||||
postcss-convert-values@^7.0.8:
|
||||
version "7.0.8"
|
||||
resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-7.0.8.tgz#0c599dc29891d47d7b4d6db399c402cf3ba8efc3"
|
||||
integrity sha512-+XNKuPfkHTCEo499VzLMYn94TiL3r9YqRE3Ty+jP7UX4qjewUONey1t7CG21lrlTLN07GtGM8MqFVp86D4uKJg==
|
||||
postcss-convert-values@^7.0.9:
|
||||
version "7.0.9"
|
||||
resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-7.0.9.tgz#6ada5c2c480f1ddbd4c886339025a916ecc8ff01"
|
||||
integrity sha512-l6uATQATZaCa0bckHV+r6dLXfWtUBKXxO3jK+AtxxJJtgMPD+VhhPCCx51I4/5w8U5uHV67g3w7PXj+V3wlMlg==
|
||||
dependencies:
|
||||
browserslist "^4.27.0"
|
||||
browserslist "^4.28.1"
|
||||
postcss-value-parser "^4.2.0"
|
||||
|
||||
postcss-discard-comments@^6.0.2:
|
||||
@@ -6064,12 +6064,12 @@ postcss-discard-comments@^6.0.2:
|
||||
resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz#e768dcfdc33e0216380623652b0a4f69f4678b6c"
|
||||
integrity sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==
|
||||
|
||||
postcss-discard-comments@^7.0.5:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-7.0.5.tgz#0a95aa4d229a021bc441861d4773d57145ee15dd"
|
||||
integrity sha512-IR2Eja8WfYgN5n32vEGSctVQ1+JARfu4UH8M7bgGh1bC+xI/obsPJXaBpQF7MAByvgwZinhpHpdrmXtvVVlKcQ==
|
||||
postcss-discard-comments@^7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-7.0.6.tgz#4e9c696a83391d90b3ffa4485ac144e555db443c"
|
||||
integrity sha512-Sq+Fzj1Eg5/CPf1ERb0wS1Im5cvE2gDXCE+si4HCn1sf+jpQZxDI4DXEp8t77B/ImzDceWE2ebJQFXdqZ6GRJw==
|
||||
dependencies:
|
||||
postcss-selector-parser "^7.1.0"
|
||||
postcss-selector-parser "^7.1.1"
|
||||
|
||||
postcss-discard-duplicates@^6.0.3:
|
||||
version "6.0.3"
|
||||
@@ -6154,15 +6154,15 @@ postcss-merge-rules@^6.1.1:
|
||||
cssnano-utils "^4.0.2"
|
||||
postcss-selector-parser "^6.0.16"
|
||||
|
||||
postcss-merge-rules@^7.0.7:
|
||||
version "7.0.7"
|
||||
resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-7.0.7.tgz#f49537e5029ce0e655c2f31fdb205f14575c7334"
|
||||
integrity sha512-njWJrd/Ms6XViwowaaCc+/vqhPG3SmXn725AGrnl+BgTuRPEacjiLEaGq16J6XirMJbtKkTwnt67SS+e2WGoew==
|
||||
postcss-merge-rules@^7.0.8:
|
||||
version "7.0.8"
|
||||
resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-7.0.8.tgz#d63ce875b9f7880ca4aa89d9ae3eaa3657215f82"
|
||||
integrity sha512-BOR1iAM8jnr7zoQSlpeBmCsWV5Uudi/+5j7k05D0O/WP3+OFMPD86c1j/20xiuRtyt45bhxw/7hnhZNhW2mNFA==
|
||||
dependencies:
|
||||
browserslist "^4.27.0"
|
||||
browserslist "^4.28.1"
|
||||
caniuse-api "^3.0.0"
|
||||
cssnano-utils "^5.0.1"
|
||||
postcss-selector-parser "^7.1.0"
|
||||
postcss-selector-parser "^7.1.1"
|
||||
|
||||
postcss-minify-font-values@^6.1.0:
|
||||
version "6.1.0"
|
||||
@@ -6205,12 +6205,12 @@ postcss-minify-params@^6.1.0:
|
||||
cssnano-utils "^4.0.2"
|
||||
postcss-value-parser "^4.2.0"
|
||||
|
||||
postcss-minify-params@^7.0.5:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-7.0.5.tgz#4a0d15e312252e41d0c8504227d43538e3f607a2"
|
||||
integrity sha512-FGK9ky02h6Ighn3UihsyeAH5XmLEE2MSGH5Tc4tXMFtEDx7B+zTG6hD/+/cT+fbF7PbYojsmmWjyTwFwW1JKQQ==
|
||||
postcss-minify-params@^7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-7.0.6.tgz#ca0df1bd4eaa70ee7a4ee17f393d275988f44657"
|
||||
integrity sha512-YOn02gC68JijlaXVuKvFSCvQOhTpblkcfDre2hb/Aaa58r2BIaK4AtE/cyZf2wV7YKAG+UlP9DT+By0ry1E4VQ==
|
||||
dependencies:
|
||||
browserslist "^4.27.0"
|
||||
browserslist "^4.28.1"
|
||||
cssnano-utils "^5.0.1"
|
||||
postcss-value-parser "^4.2.0"
|
||||
|
||||
@@ -6221,13 +6221,13 @@ postcss-minify-selectors@^6.0.4:
|
||||
dependencies:
|
||||
postcss-selector-parser "^6.0.16"
|
||||
|
||||
postcss-minify-selectors@^7.0.5:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-7.0.5.tgz#d8c89eeeb208705ab4127a464d1f54a3bc22cae3"
|
||||
integrity sha512-x2/IvofHcdIrAm9Q+p06ZD1h6FPcQ32WtCRVodJLDR+WMn8EVHI1kvLxZuGKz/9EY5nAmI6lIQIrpo4tBy5+ug==
|
||||
postcss-minify-selectors@^7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-7.0.6.tgz#1e0240e1fa3372d81d3f0586591f1e8d2ae21e16"
|
||||
integrity sha512-lIbC0jy3AAwDxEgciZlBullDiMBeBCT+fz5G8RcA9MWqh/hfUkpOI3vNDUNEZHgokaoiv0juB9Y8fGcON7rU/A==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
postcss-selector-parser "^7.1.0"
|
||||
postcss-selector-parser "^7.1.1"
|
||||
|
||||
postcss-mixins@^9.0.2:
|
||||
version "9.0.4"
|
||||
@@ -6364,12 +6364,12 @@ postcss-normalize-unicode@^6.1.0:
|
||||
browserslist "^4.23.0"
|
||||
postcss-value-parser "^4.2.0"
|
||||
|
||||
postcss-normalize-unicode@^7.0.5:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.5.tgz#d47a3cc40529d7eeb18d7f7a8a215c38c54455cd"
|
||||
integrity sha512-X6BBwiRxVaFHrb2WyBMddIeB5HBjJcAaUHyhLrM2FsxSq5TFqcHSsK7Zu1otag+o0ZphQGJewGH1tAyrD0zX1Q==
|
||||
postcss-normalize-unicode@^7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.6.tgz#6935d6baf7f7374a34c216a7fe13229acd1073f2"
|
||||
integrity sha512-z6bwTV84YW6ZvvNoaNLuzRW4/uWxDKYI1iIDrzk6D2YTL7hICApy+Q1LP6vBEsljX8FM7YSuV9qI79XESd4ddQ==
|
||||
dependencies:
|
||||
browserslist "^4.27.0"
|
||||
browserslist "^4.28.1"
|
||||
postcss-value-parser "^4.2.0"
|
||||
|
||||
postcss-normalize-url@^6.0.2:
|
||||
@@ -6424,12 +6424,12 @@ postcss-reduce-initial@^6.1.0:
|
||||
browserslist "^4.23.0"
|
||||
caniuse-api "^3.0.0"
|
||||
|
||||
postcss-reduce-initial@^7.0.5:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-7.0.5.tgz#cf74bb747dfa003cd3d5372081f6157e6d8e1545"
|
||||
integrity sha512-RHagHLidG8hTZcnr4FpyMB2jtgd/OcyAazjMhoy5qmWJOx1uxKh4ntk0Pb46ajKM0rkf32lRH4C8c9qQiPR6IA==
|
||||
postcss-reduce-initial@^7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-7.0.6.tgz#fa3af45e60cd04d9a3d29315eb97c82b7b447ead"
|
||||
integrity sha512-G6ZyK68AmrPdMB6wyeA37ejnnRG2S8xinJrZJnOv+IaRKf6koPAVbQsiC7MfkmXaGmF1UO+QCijb27wfpxuRNg==
|
||||
dependencies:
|
||||
browserslist "^4.27.0"
|
||||
browserslist "^4.28.1"
|
||||
caniuse-api "^3.0.0"
|
||||
|
||||
postcss-reduce-transforms@^6.0.2:
|
||||
@@ -6454,7 +6454,7 @@ postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss-selector-parser@^7.0.0, postcss-selector-parser@^7.1.0:
|
||||
postcss-selector-parser@^7.0.0, postcss-selector-parser@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz#e75d2e0d843f620e5df69076166f4e16f891cb9f"
|
||||
integrity sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==
|
||||
@@ -6475,13 +6475,13 @@ postcss-svgo@^6.0.3:
|
||||
postcss-value-parser "^4.2.0"
|
||||
svgo "^3.2.0"
|
||||
|
||||
postcss-svgo@^7.1.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-7.1.0.tgz#7eb6764a643ac2699bf56eef6d2676d428ed4542"
|
||||
integrity sha512-KnAlfmhtoLz6IuU3Sij2ycusNs4jPW+QoFE5kuuUOK8awR6tMxZQrs5Ey3BUz7nFCzT3eqyFgqkyrHiaU2xx3w==
|
||||
postcss-svgo@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-7.1.1.tgz#14b90fd2a1b1f27bcb2d0ef0444f954237e7883c"
|
||||
integrity sha512-zU9H9oEDrUFKa0JB7w+IYL7Qs9ey1mZyjhbf0KLxwJDdDRtoPvCmaEfknzqfHj44QS9VD6c5sJnBAVYTLRg/Sg==
|
||||
dependencies:
|
||||
postcss-value-parser "^4.2.0"
|
||||
svgo "^4.0.0"
|
||||
svgo "^4.0.1"
|
||||
|
||||
postcss-unique-selectors@^6.0.4:
|
||||
version "6.0.4"
|
||||
@@ -6490,12 +6490,12 @@ postcss-unique-selectors@^6.0.4:
|
||||
dependencies:
|
||||
postcss-selector-parser "^6.0.16"
|
||||
|
||||
postcss-unique-selectors@^7.0.4:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-7.0.4.tgz#625ad1c808bdf322fab6c027ae8d4f2637140995"
|
||||
integrity sha512-pmlZjsmEAG7cHd7uK3ZiNSW6otSZ13RHuZ/4cDN/bVglS5EpF2r2oxY99SuOHa8m7AWoBCelTS3JPpzsIs8skQ==
|
||||
postcss-unique-selectors@^7.0.5:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-7.0.5.tgz#a7dd5652c95f459176e5f135c021473e4ee58874"
|
||||
integrity sha512-3QoYmEt4qg/rUWDn6Tc8+ZVPmbp4G1hXDtCNWDx0st8SjtCbRcxRXDDM1QrEiXGG3A45zscSJFb4QH90LViyxg==
|
||||
dependencies:
|
||||
postcss-selector-parser "^7.1.0"
|
||||
postcss-selector-parser "^7.1.1"
|
||||
|
||||
postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
||||
version "4.2.0"
|
||||
@@ -7294,12 +7294,12 @@ stylehacks@^6.1.1:
|
||||
postcss-selector-parser "^6.0.16"
|
||||
|
||||
stylehacks@^7.0.5:
|
||||
version "7.0.7"
|
||||
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-7.0.7.tgz#12b0dd1eceee4d564aae6da0632804ef0004a5be"
|
||||
integrity sha512-bJkD0JkEtbRrMFtwgpJyBbFIwfDDONQ1Ov3sDLZQP8HuJ73kBOyx66H4bOcAbVWmnfLdvQ0AJwXxOMkpujcO6g==
|
||||
version "7.0.8"
|
||||
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-7.0.8.tgz#cb5d00bb1779a30c4d408a7d576c016c88b36491"
|
||||
integrity sha512-I3f053GBLIiS5Fg6OMFhq/c+yW+5Hc2+1fgq7gElDMMSqwlRb3tBf2ef6ucLStYRpId4q//bQO1FjcyNyy4yDQ==
|
||||
dependencies:
|
||||
browserslist "^4.27.0"
|
||||
postcss-selector-parser "^7.1.0"
|
||||
browserslist "^4.28.1"
|
||||
postcss-selector-parser "^7.1.1"
|
||||
|
||||
sugarss@^4.0.1:
|
||||
version "4.0.1"
|
||||
@@ -7352,7 +7352,7 @@ svgo@^3.2.0:
|
||||
picocolors "^1.0.0"
|
||||
sax "^1.5.0"
|
||||
|
||||
svgo@^4.0.0:
|
||||
svgo@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/svgo/-/svgo-4.0.1.tgz#c82dacd04ee9f1d55cd4e0b7f9a214c86670e3ee"
|
||||
integrity sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==
|
||||
|
||||
Reference in New Issue
Block a user