Merge branch 'master' into api

This commit is contained in:
Jan Böhmer 2023-08-26 22:15:12 +02:00
commit 85f3ba6aaa
35 changed files with 1219 additions and 878 deletions

View file

@ -28,7 +28,7 @@
PassEnv APP_ENV APP_DEBUG APP_SECRET PassEnv APP_ENV APP_DEBUG APP_SECRET
PassEnv TRUSTED_PROXIES TRUSTED_HOSTS LOCK_DSN PassEnv TRUSTED_PROXIES TRUSTED_HOSTS LOCK_DSN
PassEnv DATABASE_URL ENFORCE_CHANGE_COMMENTS_FOR PassEnv DATABASE_URL ENFORCE_CHANGE_COMMENTS_FOR
PassEnv DEFAULT_LANG DEFAULT_TIMEZONE BASE_CURRENCY INSTANCE_NAME ALLOW_ATTACHMENT_DOWNLOADS USE_GRAVATAR MAX_ATTACHMENT_FILE_SIZE DEFAULT_URI PassEnv DEFAULT_LANG DEFAULT_TIMEZONE BASE_CURRENCY INSTANCE_NAME ALLOW_ATTACHMENT_DOWNLOADS USE_GRAVATAR MAX_ATTACHMENT_FILE_SIZE DEFAULT_URI CHECK_FOR_UPDATES
PassEnv MAILER_DSN ALLOW_EMAIL_PW_RESET EMAIL_SENDER_EMAIL EMAIL_SENDER_NAME PassEnv MAILER_DSN ALLOW_EMAIL_PW_RESET EMAIL_SENDER_EMAIL EMAIL_SENDER_NAME
PassEnv HISTORY_SAVE_CHANGED_FIELDS HISTORY_SAVE_CHANGED_DATA HISTORY_SAVE_REMOVED_DATA HISTORY_SAVE_NEW_DATA PassEnv HISTORY_SAVE_CHANGED_FIELDS HISTORY_SAVE_CHANGED_DATA HISTORY_SAVE_REMOVED_DATA HISTORY_SAVE_NEW_DATA
PassEnv ERROR_PAGE_ADMIN_EMAIL ERROR_PAGE_SHOW_HELP PassEnv ERROR_PAGE_ADMIN_EMAIL ERROR_PAGE_SHOW_HELP

3
.env
View file

@ -44,6 +44,9 @@ DEFAULT_URI="https://partdb.changeme.invalid/"
# Leave this empty, to make all change reasons optional # Leave this empty, to make all change reasons optional
ENFORCE_CHANGE_COMMENTS_FOR="" ENFORCE_CHANGE_COMMENTS_FOR=""
# Disable that if you do not want that Part-DB connects to GitHub to check for available updates, or if your server can not connect to the internet
CHECK_FOR_UPDATES=1
################################################################################### ###################################################################################
# Email settings # Email settings
################################################################################### ###################################################################################

View file

@ -1 +1 @@
1.7.0-dev 1.7.0

View file

@ -65,8 +65,13 @@ export default class extends Controller {
localStorage.setItem( this.getStateSaveKey(), JSON.stringify(data) ); localStorage.setItem( this.getStateSaveKey(), JSON.stringify(data) );
} }
stateLoadCallback(settings) { stateLoadCallback() {
const data = JSON.parse( localStorage.getItem(this.getStateSaveKey()) ); const json = localStorage.getItem(this.getStateSaveKey());
if(json === null || json === undefined) {
return null;
}
const data = JSON.parse(json);
if (data) { if (data) {
//Do not save the start value (current page), as we want to always start at the first page on a page reload //Do not save the start value (current page), as we want to always start at the first page on a page reload
@ -90,6 +95,19 @@ export default class extends Controller {
//Add url info, as the one available in the history is not enough, as Turbo may have not changed it yet //Add url info, as the one available in the history is not enough, as Turbo may have not changed it yet
settings.url = this.element.dataset.dtUrl; settings.url = this.element.dataset.dtUrl;
//Add initial_order info to the settings, so that the order on the initial page load is the one saved in the state
const saved_state = this.stateLoadCallback();
if (saved_state !== null) {
const raw_order = saved_state.order;
settings.initial_order = raw_order.map((order) => {
return {
column: order[0],
dir: order[1]
}
});
}
let options = { let options = {
colReorder: true, colReorder: true,
responsive: true, responsive: true,
@ -221,4 +239,16 @@ export default class extends Controller {
return this.element.dataset.select ?? false; return this.element.dataset.select ?? false;
} }
invertSelection() {
//Do nothing if the datatable is not selectable
if(!this.isSelectable()) {
return;
}
//Invert the selected rows on the datatable
const selected_rows = this._dt.rows({selected: true});
this._dt.rows().select();
selected_rows.deselect();
}
} }

View file

@ -0,0 +1,65 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 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/>.
*/
import {Controller} from "@hotwired/stimulus";
/**
* This controller is used on a checkbox, which toggles the max value of all number input fields
*/
export default class extends Controller {
_checkbox;
getCheckbox() {
if (this._checkbox) {
return this._checkbox;
}
//Find the checkbox inside the controller element
this._checkbox = this.element.querySelector('input[type="checkbox"]');
return this._checkbox;
}
connect() {
//Add event listener to the checkbox
this.getCheckbox().addEventListener('change', this.toggleInputLimits.bind(this));
}
toggleInputLimits() {
//Find all input fields with the data-toggle-input-limits-target="max"
const inputFields = document.querySelectorAll("input[type='number']");
inputFields.forEach((inputField) => {
//Ensure that the input field has either a max or a data-max attribute
if (!inputField.hasAttribute('max') && !inputField.hasAttribute('data-max')) {
return;
}
//If the checkbox is checked, rename the max attribute to data-max
if (this.getCheckbox().checked) {
inputField.setAttribute('data-max', inputField.getAttribute('max'));
inputField.removeAttribute('max');
} else {
//If the checkbox is not checked, rename the data-max attribute back to max
inputField.setAttribute('max', inputField.getAttribute('data-max'));
inputField.removeAttribute('data-max');
}
});
}
}

View file

@ -91,7 +91,7 @@ th.select-checkbox {
/** Fix datatables select-checkbox position */ /** Fix datatables select-checkbox position */
table.dataTable tr.selected td.select-checkbox:after table.dataTable tr.selected td.select-checkbox:after
{ {
margin-top: -25px !important; margin-top: -20px !important;
} }

View file

@ -47,7 +47,8 @@
method: config.method, method: config.method,
data: { data: {
_dt: config.name, _dt: config.name,
_init: true _init: true,
order: config.initial_order ?? undefined,
} }
}).done(function(data) { }).done(function(data) {
var baseState; var baseState;

View file

@ -99,7 +99,7 @@
"phpstan/phpstan-strict-rules": "^1.5", "phpstan/phpstan-strict-rules": "^1.5",
"phpstan/phpstan-symfony": "^1.1.7", "phpstan/phpstan-symfony": "^1.1.7",
"psalm/plugin-symfony": "^v5.0.1", "psalm/plugin-symfony": "^v5.0.1",
"rector/rector": "^0.17.0", "rector/rector": "^0.18.0",
"roave/security-advisories": "dev-latest", "roave/security-advisories": "dev-latest",
"symfony/browser-kit": "6.3.*", "symfony/browser-kit": "6.3.*",
"symfony/css-selector": "6.3.*", "symfony/css-selector": "6.3.*",
@ -108,7 +108,7 @@
"symfony/phpunit-bridge": "6.3.*", "symfony/phpunit-bridge": "6.3.*",
"symfony/stopwatch": "6.3.*", "symfony/stopwatch": "6.3.*",
"symfony/web-profiler-bundle": "6.3.*", "symfony/web-profiler-bundle": "6.3.*",
"symplify/easy-coding-standard": "^11.0", "symplify/easy-coding-standard": "^12.0",
"vimeo/psalm": "^5.6.0" "vimeo/psalm": "^5.6.0"
}, },
"suggest": { "suggest": {

440
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,9 @@ framework:
csrf_protection: true csrf_protection: true
handle_all_throwables: true handle_all_throwables: true
# We set this header by ourself, so we can disable it here
disallow_search_engine_index: false
# Must be set to true, to enable the change of HTTP method via _method parameter, otherwise our delete routines does not work anymore # Must be set to true, to enable the change of HTTP method via _method parameter, otherwise our delete routines does not work anymore
# TODO: Rework delete routines to work without _method parameter as it is not recommended anymore (see https://github.com/symfony/symfony/issues/45278) # TODO: Rework delete routines to work without _method parameter as it is not recommended anymore (see https://github.com/symfony/symfony/issues/45278)
http_method_override: true http_method_override: true

View file

@ -23,7 +23,7 @@ parameters:
partdb.users.use_gravatar: '%env(bool:USE_GRAVATAR)%' # Set to false, if no Gravatar images should be used for user profiles. partdb.users.use_gravatar: '%env(bool:USE_GRAVATAR)%' # Set to false, if no Gravatar images should be used for user profiles.
partdb.users.email_pw_reset: '%env(bool:ALLOW_EMAIL_PW_RESET)%' # Config if users are able, to reset their password by email. By default this enabled, when a mail server is configured. partdb.users.email_pw_reset: '%env(bool:ALLOW_EMAIL_PW_RESET)%' # Config if users are able, to reset their password by email. By default this enabled, when a mail server is configured.
partdb.check_for_updates: true # Set to false, if Part-DB should not contact the GitHub API to check for updates partdb.check_for_updates: '%env(bool:CHECK_FOR_UPDATES)' # Set to false, if Part-DB should not contact the GitHub API to check for updates
###################################################################################################################### ######################################################################################################################
# Mail settings # Mail settings

View file

@ -36,6 +36,7 @@ The following configuration options can only be changed by the server administra
* `datastructure_edit`: Edit operation of a existing datastructure (e.g. category, manufacturer, ...) * `datastructure_edit`: Edit operation of a existing datastructure (e.g. category, manufacturer, ...)
* `datastructure_delete`: Delete operation of a existing datastructure (e.g. category, manufacturer, ...) * `datastructure_delete`: Delete operation of a existing datastructure (e.g. category, manufacturer, ...)
* `datastructure_create`: Creation of a new datastructure (e.g. category, manufacturer, ...) * `datastructure_create`: Creation of a new datastructure (e.g. category, manufacturer, ...)
* `CHECK_FOR_UPDATES` (default `1`): Set this to 0, if you do not want Part-DB to connect to GitHub to check for new versions, or if your server can not connect to the internet.
### E-Mail settings ### E-Mail settings
* `MAILER_DSN`: You can configure the mail provider which should be used for email delivery (see https://symfony.com/doc/current/components/mailer.html for full documentation). If you just want to use an SMTP mail account, you can use the following syntax `MAILER_DSN=smtp://user:password@smtp.mailserver.invalid:587` * `MAILER_DSN`: You can configure the mail provider which should be used for email delivery (see https://symfony.com/doc/current/components/mailer.html for full documentation). If you just want to use an SMTP mail account, you can use the following syntax `MAILER_DSN=smtp://user:password@smtp.mailserver.invalid:587`

View file

@ -101,7 +101,7 @@ The basic configuration of Part-DB is done by a `.env.local` file in the main di
cp .env .env.local cp .env .env.local
``` ```
In your `.env.local` you can configure Part-DB according to your wishes. A full list of configuration options can be found [here]({% link configuration.md %}. In your `.env.local` you can configure Part-DB according to your wishes. A full list of configuration options can be found [here](../configuration.md).
Other configuration options like the default language or default currency can be found in `config/parameters.yaml`. Other configuration options like the default language or default currency can be found in `config/parameters.yaml`.
Please check that the `partdb.default_currency` value in `config/parameters.yaml` matches your mainly used currency, as this can not be changed after creating price informations. Please check that the `partdb.default_currency` value in `config/parameters.yaml` matches your mainly used currency, as this can not be changed after creating price informations.

View file

@ -77,13 +77,15 @@ Part-DB caches the search results internally, so if you have searched for a part
Following env configuration options are available: Following env configuration options are available:
* `PROVIDER_OCTOPART_CLIENT_ID`: The client ID you got from Nexar (mandatory) * `PROVIDER_OCTOPART_CLIENT_ID`: The client ID you got from Nexar (mandatory)
* `PROVIDER_OCTOPART_CLIENT_SECRET`: The client secret you got from Nexar (mandatory) * `PROVIDER_OCTOPART_SECRET`: The client secret you got from Nexar (mandatory)
* `PROVIDER_OCTOPART_CURRENCY`: The currency you want to get prices in if available (optional, 3 letter ISO-code, default: `EUR`). If an offer is only available in a certain currency, * `PROVIDER_OCTOPART_CURRENCY`: The currency you want to get prices in if available (optional, 3 letter ISO-code, default: `EUR`). If an offer is only available in a certain currency,
Part-DB will save the prices in their native currency, and you can use Part-DB currency conversion feature to convert it to your preferred currency. Part-DB will save the prices in their native currency, and you can use Part-DB currency conversion feature to convert it to your preferred currency.
* `PROVIDER_OCOTPART_COUNTRY`: The country you want to get prices in if available (optional, 2 letter ISO-code, default: `DE`). To get correct prices, you have to set this and the currency setting to the correct value. * `PROVIDER_OCOTPART_COUNTRY`: The country you want to get prices in if available (optional, 2 letter ISO-code, default: `DE`). To get correct prices, you have to set this and the currency setting to the correct value.
* `PROVIDER_OCTOPART_SEARCH_LIMIT`: The maximum number of results to return per search (optional, default: `10`). This affects how quickly your monthly limit is used up. * `PROVIDER_OCTOPART_SEARCH_LIMIT`: The maximum number of results to return per search (optional, default: `10`). This affects how quickly your monthly limit is used up.
* `PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS`: If set to `true`, only offers from [authorized sellers](https://octopart.com/authorized) will be returned (optional, default: `false`). * `PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS`: If set to `true`, only offers from [authorized sellers](https://octopart.com/authorized) will be returned (optional, default: `false`).
**Attention**: If you change the octopart clientID after you have already used the provider, you have to remove the OAuth token in the Part-DB database. Remove the entry in the table `oauth_tokens` with the name `ip_octopart_oauth`.
### Digi-Key ### Digi-Key
The Digi-Key provider uses the [Digi-Key API](https://developer.digikey.com/) to search for parts and getting shopping information from [Digi-Key](https://www.digikey.com/). The Digi-Key provider uses the [Digi-Key API](https://developer.digikey.com/) to search for parts and getting shopping information from [Digi-Key](https://www.digikey.com/).
To use it you have to create an account at Digi-Key and get an API key on the [Digi-Key API page](https://developer.digikey.com/). To use it you have to create an account at Digi-Key and get an API key on the [Digi-Key API page](https://developer.digikey.com/).

View file

@ -73,3 +73,10 @@ See the configuration reference for more information.
## Personal stocks and stock locations ## Personal stocks and stock locations
For makerspaces and universities with a lot of users, where each user can have his own stock, which only he should be able to access, you can assign For makerspaces and universities with a lot of users, where each user can have his own stock, which only he should be able to access, you can assign
the user as "owner" of a part lot. This way, only him is allowed to add or remove parts from this lot. the user as "owner" of a part lot. This way, only him is allowed to add or remove parts from this lot.
## Update notfications
Part-DB can show you a notification that there is a newer version than currently installed available. The notification will be shown on the homepage and the server info page.
It is only be shown to users which has the `Show available Part-DB updates` permission.
For the notification to work, Part-DB queries the GitHub API every 2 days to check for new releases. No data is sent to GitHub besides the metadata required for the connection (so the public IP address of your computer running Part-DB).
If you don't want Part-DB to query the GitHub API, or if your server can not reach the internet, you can disable the update notifications by setting the `CHECK_FOR_UPDATES` option to `false`.

View file

@ -30,38 +30,38 @@
"build": "encore production --progress" "build": "encore production --progress"
}, },
"dependencies": { "dependencies": {
"@ckeditor/ckeditor5-alignment": "^38.0.1", "@ckeditor/ckeditor5-alignment": "^39.0.1",
"@ckeditor/ckeditor5-autoformat": "^38.0.1", "@ckeditor/ckeditor5-autoformat": "^39.0.1",
"@ckeditor/ckeditor5-basic-styles": "^38.0.1", "@ckeditor/ckeditor5-basic-styles": "^39.0.1",
"@ckeditor/ckeditor5-block-quote": "^38.0.1", "@ckeditor/ckeditor5-block-quote": "^39.0.1",
"@ckeditor/ckeditor5-code-block": "^38.0.1", "@ckeditor/ckeditor5-code-block": "^39.0.1",
"@ckeditor/ckeditor5-dev-utils": "^38.0.1", "@ckeditor/ckeditor5-dev-translations": "^38.4.0",
"@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13", "@ckeditor/ckeditor5-dev-utils": "^38.4.0",
"@ckeditor/ckeditor5-editor-classic": "^38.0.1", "@ckeditor/ckeditor5-editor-classic": "^39.0.1",
"@ckeditor/ckeditor5-essentials": "^38.0.1", "@ckeditor/ckeditor5-essentials": "^39.0.1",
"@ckeditor/ckeditor5-find-and-replace": "^38.0.1", "@ckeditor/ckeditor5-find-and-replace": "^39.0.1",
"@ckeditor/ckeditor5-font": "^38.0.1", "@ckeditor/ckeditor5-font": "^39.0.1",
"@ckeditor/ckeditor5-heading": "^38.0.1", "@ckeditor/ckeditor5-heading": "^39.0.1",
"@ckeditor/ckeditor5-highlight": "^38.0.1", "@ckeditor/ckeditor5-highlight": "^39.0.1",
"@ckeditor/ckeditor5-horizontal-line": "^38.0.1", "@ckeditor/ckeditor5-horizontal-line": "^39.0.1",
"@ckeditor/ckeditor5-html-embed": "^38.0.1", "@ckeditor/ckeditor5-html-embed": "^39.0.1",
"@ckeditor/ckeditor5-html-support": "^38.0.1", "@ckeditor/ckeditor5-html-support": "^39.0.1",
"@ckeditor/ckeditor5-image": "^38.0.1", "@ckeditor/ckeditor5-image": "^39.0.1",
"@ckeditor/ckeditor5-indent": "^38.0.1", "@ckeditor/ckeditor5-indent": "^39.0.1",
"@ckeditor/ckeditor5-link": "^38.0.1", "@ckeditor/ckeditor5-link": "^39.0.1",
"@ckeditor/ckeditor5-list": "^38.0.1", "@ckeditor/ckeditor5-list": "^39.0.1",
"@ckeditor/ckeditor5-markdown-gfm": "^38.0.1", "@ckeditor/ckeditor5-markdown-gfm": "^39.0.1",
"@ckeditor/ckeditor5-media-embed": "^38.0.1", "@ckeditor/ckeditor5-media-embed": "^39.0.1",
"@ckeditor/ckeditor5-paragraph": "^38.0.1", "@ckeditor/ckeditor5-paragraph": "^39.0.1",
"@ckeditor/ckeditor5-paste-from-office": "^38.0.1", "@ckeditor/ckeditor5-paste-from-office": "^39.0.1",
"@ckeditor/ckeditor5-remove-format": "^38.0.1", "@ckeditor/ckeditor5-remove-format": "^39.0.1",
"@ckeditor/ckeditor5-source-editing": "^38.0.1", "@ckeditor/ckeditor5-source-editing": "^39.0.1",
"@ckeditor/ckeditor5-special-characters": "^38.0.1", "@ckeditor/ckeditor5-special-characters": "^39.0.1",
"@ckeditor/ckeditor5-table": "^38.0.1", "@ckeditor/ckeditor5-table": "^39.0.1",
"@ckeditor/ckeditor5-theme-lark": "^38.0.1", "@ckeditor/ckeditor5-theme-lark": "^39.0.1",
"@ckeditor/ckeditor5-upload": "^38.0.1", "@ckeditor/ckeditor5-upload": "^39.0.1",
"@ckeditor/ckeditor5-watchdog": "^38.0.1", "@ckeditor/ckeditor5-watchdog": "^39.0.1",
"@ckeditor/ckeditor5-word-count": "^38.0.1", "@ckeditor/ckeditor5-word-count": "^39.0.1",
"@jbtronics/bs-treeview": "^1.0.1", "@jbtronics/bs-treeview": "^1.0.1",
"@zxcvbn-ts/core": "^3.0.2", "@zxcvbn-ts/core": "^3.0.2",
"@zxcvbn-ts/language-common": "^3.0.3", "@zxcvbn-ts/language-common": "^3.0.3",
@ -81,13 +81,13 @@
"datatables.net-responsive-bs5": "^2.2.3", "datatables.net-responsive-bs5": "^2.2.3",
"datatables.net-select-bs5": "^1.2.7", "datatables.net-select-bs5": "^1.2.7",
"dompurify": "^3.0.3", "dompurify": "^3.0.3",
"emoji.json": "^14.0.0", "emoji.json": "^15.0.0",
"exports-loader": "^3.0.0", "exports-loader": "^3.0.0",
"html5-qrcode": "^2.2.1", "html5-qrcode": "^2.2.1",
"json-formatter-js": "^2.3.4", "json-formatter-js": "^2.3.4",
"jszip": "^3.2.0", "jszip": "^3.2.0",
"katex": "^0.16.0", "katex": "^0.16.0",
"marked": "^5.1.0", "marked": "^7.0.4",
"marked-gfm-heading-id": "^3.0.4", "marked-gfm-heading-id": "^3.0.4",
"marked-mangle": "^1.0.1", "marked-mangle": "^1.0.1",
"pdfmake": "^0.2.2", "pdfmake": "^0.2.2",

View file

@ -49,6 +49,8 @@ class HomepageController extends AbstractController
public function homepage(Request $request, GitVersionInfo $versionInfo, EntityManagerInterface $entityManager, public function homepage(Request $request, GitVersionInfo $versionInfo, EntityManagerInterface $entityManager,
UpdateAvailableManager $updateAvailableManager): Response UpdateAvailableManager $updateAvailableManager): Response
{ {
$this->denyAccessUnlessGranted('HAS_ACCESS_PERMISSIONS');
if ($this->isGranted('@tools.lastActivity')) { if ($this->isGranted('@tools.lastActivity')) {
$table = $this->dataTable->createFromType( $table = $this->dataTable->createFromType(
LogDataTable::class, LogDataTable::class,

View file

@ -160,7 +160,7 @@ class PartController extends AbstractController
public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator, public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator,
AttachmentSubmitHandler $attachmentSubmitHandler, ProjectBuildPartHelper $projectBuildPartHelper, AttachmentSubmitHandler $attachmentSubmitHandler, ProjectBuildPartHelper $projectBuildPartHelper,
#[MapEntity(mapping: ['id' => 'id'])] ?Part $part = null, #[MapEntity(mapping: ['id' => 'id'])] ?Part $part = null,
#[MapEntity(mapping: ['id' => 'project_id'])] ?Project $project = null): Response #[MapEntity(mapping: ['project_id' => 'id'])] ?Project $project = null): Response
{ {
if ($part instanceof Part) { if ($part instanceof Part) {

View file

@ -46,6 +46,7 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
#[Route(path: '/user')] #[Route(path: '/user')]
class UserController extends BaseAdminController class UserController extends BaseAdminController
@ -79,7 +80,8 @@ class UserController extends BaseAdminController
*/ */
#[Route(path: '/{id}/edit/{timestamp}', requirements: ['id' => '\d+'], name: 'user_edit')] #[Route(path: '/{id}/edit/{timestamp}', requirements: ['id' => '\d+'], name: 'user_edit')]
#[Route(path: '/{id}/', requirements: ['id' => '\d+'])] #[Route(path: '/{id}/', requirements: ['id' => '\d+'])]
public function edit(User $entity, Request $request, EntityManagerInterface $em, PermissionPresetsHelper $permissionPresetsHelper, PermissionSchemaUpdater $permissionSchemaUpdater, ?string $timestamp = null): Response public function edit(User $entity, Request $request, EntityManagerInterface $em, PermissionPresetsHelper $permissionPresetsHelper,
PermissionSchemaUpdater $permissionSchemaUpdater, ValidatorInterface $validator, ?string $timestamp = null): Response
{ {
//Do an upgrade of the permission schema if needed (so the user can see the permissions a user get on next request (even if it was not done yet) //Do an upgrade of the permission schema if needed (so the user can see the permissions a user get on next request (even if it was not done yet)
$permissionSchemaUpdater->userUpgradeSchemaRecursively($entity); $permissionSchemaUpdater->userUpgradeSchemaRecursively($entity);
@ -108,7 +110,7 @@ class UserController extends BaseAdminController
$this->addFlash('success', 'user.edit.reset_success'); $this->addFlash('success', 'user.edit.reset_success');
} else { } else {
$this->addFlash('danger', 'csfr_invalid'); $this->addFlash('error', 'csfr_invalid');
} }
} }
@ -120,15 +122,25 @@ class UserController extends BaseAdminController
$permissionPresetsHelper->applyPreset($entity, $preset); $permissionPresetsHelper->applyPreset($entity, $preset);
$em->flush(); //Ensure that the user is valid after applying the preset
$errors = $validator->validate($entity);
if (count($errors) > 0) {
$this->addFlash('error', 'validator.noLockout');
//Refresh the entity to remove the changes
$em->refresh($entity);
} else {
$em->flush();
$this->addFlash('success', 'user.edit.permission_success'); $this->addFlash('success', 'user.edit.permission_success');
//We need to stop the execution here, or our permissions changes will be overwritten by the form values //We need to stop the execution here, or our permissions changes will be overwritten by the form values
return $this->redirectToRoute('user_edit', ['id' => $entity->getID()]); return $this->redirectToRoute('user_edit', ['id' => $entity->getID()]);
}
} else {
$this->addFlash('error', 'csfr_invalid');
} }
$this->addFlash('danger', 'csfr_invalid');
} }
return $this->_edit($entity, $request, $em, $timestamp); return $this->_edit($entity, $request, $em, $timestamp);

View file

@ -0,0 +1,55 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 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\EventListener;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
#[AsEventListener]
class DisallowSearchEngineIndexingRequestListener
{
private const HEADER_NAME = 'X-Robots-Tag';
private readonly bool $enabled;
public function __construct(#[Autowire(param: 'partdb.demo_mode')] bool $demo_mode)
{
// Disable this listener in demo mode
$this->enabled = !$demo_mode;
}
public function __invoke(ResponseEvent $event): void
{
//Skip if disabled
if (!$this->enabled) {
return;
}
if (!$event->getResponse()->headers->has(self::HEADER_NAME)) {
$event->getResponse()->headers->set(self::HEADER_NAME, 'noindex');
}
}
}

View file

@ -62,6 +62,15 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface
'disabled' => !$this->security->isGranted('@parts_stock.withdraw'), 'disabled' => !$this->security->isGranted('@parts_stock.withdraw'),
]); ]);
$builder->add('dontCheckQuantity', CheckboxType::class, [
'label' => 'project.build.dont_check_quantity',
'help' => 'project.build.dont_check_quantity.help',
'required' => false,
'attr' => [
'data-controller' => 'pages--dont-check-quantity-checkbox'
]
]);
$builder->add('comment', TextType::class, [ $builder->add('comment', TextType::class, [
'label' => 'part.info.withdraw_modal.comment', 'label' => 'part.info.withdraw_modal.comment',
'help' => 'part.info.withdraw_modal.comment.hint', 'help' => 'part.info.withdraw_modal.comment.hint',
@ -124,6 +133,7 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface
} }
$forms['comment']->setData($data->getComment()); $forms['comment']->setData($data->getComment());
$forms['dontCheckQuantity']->setData($data->isDontCheckQuantity());
$forms['addBuildsToBuildsPart']->setData($data->getAddBuildsToBuildsPart()); $forms['addBuildsToBuildsPart']->setData($data->getAddBuildsToBuildsPart());
if (isset($forms['buildsPartLot'])) { if (isset($forms['buildsPartLot'])) {
$forms['buildsPartLot']->setData($data->getBuildsPartLot()); $forms['buildsPartLot']->setData($data->getBuildsPartLot());
@ -150,6 +160,8 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface
} }
$data->setComment($forms['comment']->getData()); $data->setComment($forms['comment']->getData());
$data->setDontCheckQuantity($forms['dontCheckQuantity']->getData());
if (isset($forms['buildsPartLot'])) { if (isset($forms['buildsPartLot'])) {
$lot = $forms['buildsPartLot']->getData(); $lot = $forms['buildsPartLot']->getData();
if (!$lot) { //When the user selected "Create new lot", create a new lot if (!$lot) { //When the user selected "Create new lot", create a new lot

View file

@ -47,6 +47,8 @@ final class ProjectBuildRequest
private bool $add_build_to_builds_part = false; private bool $add_build_to_builds_part = false;
private bool $dont_check_quantity = false;
/** /**
* @param Project $project The project that should be build * @param Project $project The project that should be build
* @param int $number_of_builds The number of builds that should be created * @param int $number_of_builds The number of builds that should be created
@ -283,4 +285,26 @@ final class ProjectBuildRequest
{ {
return $this->number_of_builds; return $this->number_of_builds;
} }
/**
* If Set to true, the given withdraw amounts are used without any checks for requirements.
* @return bool
*/
public function isDontCheckQuantity(): bool
{
return $this->dont_check_quantity;
}
/**
* Set to true, the given withdraw amounts are used without any checks for requirements.
* @param bool $dont_check_quantity
* @return $this
*/
public function setDontCheckQuantity(bool $dont_check_quantity): ProjectBuildRequest
{
$this->dont_check_quantity = $dont_check_quantity;
return $this;
}
} }

View file

@ -0,0 +1,45 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 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\Security\Voter;
use App\Entity\UserSystem\User;
/**
* This voter implements a virtual role, which can be used if the user has any permission set to allowed.
* We use this to restrict access to the homepage.
*/
class HasAccessPermissionsVoter extends ExtendedVoter
{
public const ROLE = "HAS_ACCESS_PERMISSIONS";
protected function voteOnUser(string $attribute, $subject, User $user): bool
{
return $this->resolver->hasAnyPermissionSetToAllowInherited($user);
}
protected function supports(string $attribute, mixed $subject): bool
{
return $attribute === self::ROLE;
}
}

View file

@ -265,8 +265,7 @@ class PKDatastructureImporter
{ {
$count = $this->importElementsWithCategory($data, Storelocation::class, 'storagelocation'); $count = $this->importElementsWithCategory($data, Storelocation::class, 'storagelocation');
//Footprints have both attachments and images $this->importAttachments($data, 'storagelocationimage', Storelocation::class, 'storageLocation_id', StorelocationAttachment::class);
$this->importAttachments($data, 'storagelocationimage', Storelocation::class, 'footprint_id', StorelocationAttachment::class);
return $count; return $count;
} }

View file

@ -23,7 +23,9 @@ declare(strict_types=1);
namespace App\Services\System; namespace App\Services\System;
use Psr\Log\LoggerInterface;
use Shivas\VersioningBundle\Service\VersionManagerInterface; use Shivas\VersioningBundle\Service\VersionManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
@ -41,7 +43,8 @@ class UpdateAvailableManager
public function __construct(private readonly HttpClientInterface $httpClient, public function __construct(private readonly HttpClientInterface $httpClient,
private readonly CacheInterface $updateCache, private readonly VersionManagerInterface $versionManager, private readonly CacheInterface $updateCache, private readonly VersionManagerInterface $versionManager,
private readonly bool $check_for_updates) private readonly bool $check_for_updates, private readonly LoggerInterface $logger,
#[Autowire(param: 'kernel.debug')] private readonly bool $is_dev_mode)
{ {
} }
@ -107,17 +110,34 @@ class UpdateAvailableManager
return $this->updateCache->get(self::CACHE_KEY, function (ItemInterface $item) { return $this->updateCache->get(self::CACHE_KEY, function (ItemInterface $item) {
$item->expiresAfter(self::CACHE_TTL); $item->expiresAfter(self::CACHE_TTL);
$response = $this->httpClient->request('GET', self::API_URL); try {
$result = $response->toArray(); $response = $this->httpClient->request('GET', self::API_URL);
$tag_name = $result['tag_name']; $result = $response->toArray();
$tag_name = $result['tag_name'];
// Remove the leading 'v' from the tag name // Remove the leading 'v' from the tag name
$version = substr($tag_name, 1); $version = substr($tag_name, 1);
return [ return [
'version' => $version, 'version' => $version,
'url' => $result['html_url'], 'url' => $result['html_url'],
]; ];
} catch (\Exception $e) {
//When we are in dev mode, throw the exception, otherwise just silently log it
if ($this->is_dev_mode) {
throw $e;
}
//In the case of an error, try it again after half of the cache time
$item->expiresAfter(self::CACHE_TTL / 2);
$this->logger->error('Checking for updates failed: ' . $e->getMessage());
return [
'version' => '0.0.1',
'url' => 'update-checking-error'
];
}
}); });
} }
} }

View file

@ -271,6 +271,27 @@ class PermissionManager
} }
} }
/**
* This function checks if the given user has any permission set to allow, either directly or inherited.
* @param User $user
* @return bool
*/
public function hasAnyPermissionSetToAllowInherited(User $user): bool
{
//Iterate over all permissions
foreach ($this->permission_structure['perms'] as $perm_key => $permission) {
//Iterate over all operations of the permission
foreach ($permission['operations'] as $op_key => $op) {
//Check if the user has the permission set to allow
if ($this->inherit($user, $perm_key, $op_key) === true) {
return true;
}
}
}
return false;
}
protected function generatePermissionStructure() protected function generatePermissionStructure()
{ {
$cache = new ConfigCache($this->cache_file, $this->kernel_debug_enabled); $cache = new ConfigCache($this->cache_file, $this->kernel_debug_enabled);

View file

@ -69,12 +69,12 @@ class ValidProjectBuildRequestValidator extends ConstraintValidator
->addViolation(); ->addViolation();
} }
if ($withdraw_sum > $needed_amount) { if ($withdraw_sum > $needed_amount && $value->isDontCheckQuantity() === false) {
$this->buildViolationForLot($lot, 'validator.project_build.lot_bigger_than_needed') $this->buildViolationForLot($lot, 'validator.project_build.lot_bigger_than_needed')
->addViolation(); ->addViolation();
} }
if ($withdraw_sum < $needed_amount) { if ($withdraw_sum < $needed_amount && $value->isDontCheckQuantity() === false) {
$this->buildViolationForLot($lot, 'validator.project_build.lot_smaller_than_needed') $this->buildViolationForLot($lot, 'validator.project_build.lot_smaller_than_needed')
->addViolation(); ->addViolation();
} }

View file

@ -32,6 +32,8 @@
{# <span id="select_count"></span> #} {# <span id="select_count"></span> #}
<div class="input-group"> <div class="input-group">
<button class="btn btn-outline-secondary" type="button" {{ stimulus_action('elements/datatables/parts', 'invertSelection')}}
title="{% trans %}part_list.action.invert_selection{% endtrans %}" ><i class="fa-solid fa-arrow-right-arrow-left"></i></button>
<span class="input-group-text"> <span class="input-group-text">
<span class="badge bg-primary">{% trans with {'%count%': '<span ' ~ stimulus_target('elements/datatables/parts', 'selectCount') ~ '></span>'} %}part_list.action.part_count{% endtrans %}</span> <span class="badge bg-primary">{% trans with {'%count%': '<span ' ~ stimulus_target('elements/datatables/parts', 'selectCount') ~ '></span>'} %}part_list.action.part_count{% endtrans %}</span>
</span> </span>
@ -75,7 +77,7 @@
{# This is left empty, as this will be filled by Javascript #} {# This is left empty, as this will be filled by Javascript #}
</select> </select>
<button type="submit" class="btn btn-secondary">{% trans %}part_list.action.submit{% endtrans %}</button> <button type="submit" class="btn btn-primary">{% trans %}part_list.action.submit{% endtrans %}</button>
</div> </div>
</div> </div>

View file

@ -74,6 +74,9 @@
</table> </table>
{{ form_row(form.comment) }} {{ form_row(form.comment) }}
<div {{ stimulus_controller('pages/dont_check_quantity_checkbox') }}>
{{ form_row(form.dontCheckQuantity) }}
</div>
{{ form_row(form.addBuildsToBuildsPart) }} {{ form_row(form.addBuildsToBuildsPart) }}
{% if form.buildsPartLot is defined %} {% if form.buildsPartLot is defined %}

View file

@ -31,15 +31,12 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class PermissionManagerTest extends WebTestCase class PermissionManagerTest extends WebTestCase
{ {
protected $user_withoutGroup; protected ?User $user_withoutGroup = null;
protected $user; protected ?User $user = null;
protected $group; protected ?Group $group = null;
/** protected ?PermissionManager $service = null;
* @var PermissionManager
*/
protected $service;
protected function setUp(): void protected function setUp(): void
{ {
@ -294,4 +291,34 @@ class PermissionManagerTest extends WebTestCase
$this->assertTrue($this->service->dontInherit($user, 'parts', 'edit')); $this->assertTrue($this->service->dontInherit($user, 'parts', 'edit'));
$this->assertTrue($this->service->dontInherit($user, 'categories', 'read')); $this->assertTrue($this->service->dontInherit($user, 'categories', 'read'));
} }
public function testHasAnyPermissionSetToAllowInherited(): void
{
//For empty user this should return false
$user = new User();
$this->assertFalse($this->service->hasAnyPermissionSetToAllowInherited($user));
//If all permissions are set to false this should return false
$this->service->setAllPermissions($user, false);
$this->assertFalse($this->service->hasAnyPermissionSetToAllowInherited($user));
//If all permissions are set to null this should return false
$this->service->setAllPermissions($user, null);
$this->assertFalse($this->service->hasAnyPermissionSetToAllowInherited($user));
//If all permissions are set to true this should return true
$this->service->setAllPermissions($user, true);
$this->assertTrue($this->service->hasAnyPermissionSetToAllowInherited($user));
//The test data should return true
$this->assertTrue($this->service->hasAnyPermissionSetToAllowInherited($this->user));
$this->assertTrue($this->service->hasAnyPermissionSetToAllowInherited($this->user_withoutGroup));
//Create a user with a group
$user = new User();
$user->setGroup($this->group);
//This should return true
$this->assertTrue($this->service->hasAnyPermissionSetToAllowInherited($user));
}
} }

View file

@ -11575,5 +11575,29 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<target>Quelle</target> <target>Quelle</target>
</segment> </segment>
</unit> </unit>
<unit id="21tIbM2" name="update_manager.new_version_available.title">
<segment state="translated">
<source>update_manager.new_version_available.title</source>
<target>Neue Version verfügbar</target>
</segment>
</unit>
<unit id=".c.eoDV" name="update_manager.new_version_available.text">
<segment state="translated">
<source>update_manager.new_version_available.text</source>
<target>Eine neue Version von Part-DB ist verfügbar. Mehr Informationen gibt es hier</target>
</segment>
</unit>
<unit id="KOFGqJw" name="update_manager.new_version_available.only_administrators_can_see">
<segment state="translated">
<source>update_manager.new_version_available.only_administrators_can_see</source>
<target>Nur Administratoren können diese Nachricht sehen.</target>
</segment>
</unit>
<unit id="IFkvJpC" name="perm.system.show_available_updates">
<segment state="translated">
<source>perm.system.show_available_updates</source>
<target>Verfügbare Part-DB Updates anzeigen</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -731,10 +731,10 @@
</notes> </notes>
<segment> <segment>
<source>user.edit.tfa.disable_tfa_message</source> <source>user.edit.tfa.disable_tfa_message</source>
<target><![CDATA[This will disable <b>all active two-factor authentication methods of the user</b> and delete the <b>backup codes</b>! <target>This will disable &lt;b&gt;all active two-factor authentication methods of the user&lt;/b&gt; and delete the &lt;b&gt;backup codes&lt;/b&gt;!
<br> &lt;br&gt;
The user will have to set up all two-factor authentication methods again and print new backup codes! <br><br> The user will have to set up all two-factor authentication methods again and print new backup codes! &lt;br&gt;&lt;br&gt;
<b>Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!</b>]]></target> &lt;b&gt;Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!&lt;/b&gt;</target>
</segment> </segment>
</unit> </unit>
<unit id="02HvwiS" name="user.edit.tfa.disable_tfa.btn"> <unit id="02HvwiS" name="user.edit.tfa.disable_tfa.btn">
@ -11326,67 +11326,67 @@ Element 3</target>
</segment> </segment>
</unit> </unit>
<unit id="DGczoY6" name="tfa_u2f.add_key.registration_error"> <unit id="DGczoY6" name="tfa_u2f.add_key.registration_error">
<segment> <segment state="translated">
<source>tfa_u2f.add_key.registration_error</source> <source>tfa_u2f.add_key.registration_error</source>
<target>An error occurred during the registration of the security key. Try again or use another security key!</target> <target>An error occurred during the registration of the security key. Try again or use another security key!</target>
</segment> </segment>
</unit> </unit>
<unit id="ie0Ca0l" name="log.target_type.none"> <unit id="ie0Ca0l" name="log.target_type.none">
<segment> <segment state="translated">
<source>log.target_type.none</source> <source>log.target_type.none</source>
<target>None</target> <target>None</target>
</segment> </segment>
</unit> </unit>
<unit id="R2nX4ip" name="ui.darkmode.light"> <unit id="R2nX4ip" name="ui.darkmode.light">
<segment> <segment state="translated">
<source>ui.darkmode.light</source> <source>ui.darkmode.light</source>
<target>Light</target> <target>Light</target>
</segment> </segment>
</unit> </unit>
<unit id="3NHpuW3" name="ui.darkmode.dark"> <unit id="3NHpuW3" name="ui.darkmode.dark">
<segment> <segment state="translated">
<source>ui.darkmode.dark</source> <source>ui.darkmode.dark</source>
<target>Dark</target> <target>Dark</target>
</segment> </segment>
</unit> </unit>
<unit id="4TGOK5_" name="ui.darkmode.auto"> <unit id="4TGOK5_" name="ui.darkmode.auto">
<segment> <segment state="translated">
<source>ui.darkmode.auto</source> <source>ui.darkmode.auto</source>
<target>Auto (decide based on system settings)</target> <target>Auto (decide based on system settings)</target>
</segment> </segment>
</unit> </unit>
<unit id="9N0N8aL" name="label_generator.no_lines_given"> <unit id="9N0N8aL" name="label_generator.no_lines_given">
<segment> <segment state="translated">
<source>label_generator.no_lines_given</source> <source>label_generator.no_lines_given</source>
<target>No text content given! The labels will remain empty.</target> <target>No text content given! The labels will remain empty.</target>
</segment> </segment>
</unit> </unit>
<unit id="RdFvZsb" name="user.password_strength.very_weak"> <unit id="RdFvZsb" name="user.password_strength.very_weak">
<segment> <segment state="translated">
<source>user.password_strength.very_weak</source> <source>user.password_strength.very_weak</source>
<target>Very weak</target> <target>Very weak</target>
</segment> </segment>
</unit> </unit>
<unit id="IBjmblZ" name="user.password_strength.weak"> <unit id="IBjmblZ" name="user.password_strength.weak">
<segment> <segment state="translated">
<source>user.password_strength.weak</source> <source>user.password_strength.weak</source>
<target>Weak</target> <target>Weak</target>
</segment> </segment>
</unit> </unit>
<unit id="qSm_ID0" name="user.password_strength.medium"> <unit id="qSm_ID0" name="user.password_strength.medium">
<segment> <segment state="translated">
<source>user.password_strength.medium</source> <source>user.password_strength.medium</source>
<target>Medium</target> <target>Medium</target>
</segment> </segment>
</unit> </unit>
<unit id="aWAaADS" name="user.password_strength.strong"> <unit id="aWAaADS" name="user.password_strength.strong">
<segment> <segment state="translated">
<source>user.password_strength.strong</source> <source>user.password_strength.strong</source>
<target>Strong</target> <target>Strong</target>
</segment> </segment>
</unit> </unit>
<unit id="Wa9CStW" name="user.password_strength.very_strong"> <unit id="Wa9CStW" name="user.password_strength.very_strong">
<segment> <segment state="translated">
<source>user.password_strength.very_strong</source> <source>user.password_strength.very_strong</source>
<target>Very strong</target> <target>Very strong</target>
</segment> </segment>
@ -11580,25 +11580,25 @@ Please note, that you can not impersonate a disabled user. If you try you will g
</segment> </segment>
</unit> </unit>
<unit id="21tIbM2" name="update_manager.new_version_available.title"> <unit id="21tIbM2" name="update_manager.new_version_available.title">
<segment> <segment state="translated">
<source>update_manager.new_version_available.title</source> <source>update_manager.new_version_available.title</source>
<target>New version available</target> <target>New version available</target>
</segment> </segment>
</unit> </unit>
<unit id=".c.eoDV" name="update_manager.new_version_available.text"> <unit id=".c.eoDV" name="update_manager.new_version_available.text">
<segment> <segment state="translated">
<source>update_manager.new_version_available.text</source> <source>update_manager.new_version_available.text</source>
<target>A new version of Part-DB is available. Check it out here</target> <target>A new version of Part-DB is available. Check it out here</target>
</segment> </segment>
</unit> </unit>
<unit id="KOFGqJw" name="update_manager.new_version_available.only_administrators_can_see"> <unit id="KOFGqJw" name="update_manager.new_version_available.only_administrators_can_see">
<segment> <segment state="translated">
<source>update_manager.new_version_available.only_administrators_can_see</source> <source>update_manager.new_version_available.only_administrators_can_see</source>
<target>Only administrators can see this message.</target> <target>Only administrators can see this message.</target>
</segment> </segment>
</unit> </unit>
<unit id="IFkvJpC" name="perm.system.show_available_updates"> <unit id="IFkvJpC" name="perm.system.show_available_updates">
<segment> <segment state="translated">
<source>perm.system.show_available_updates</source> <source>perm.system.show_available_updates</source>
<target>Show available Part-DB updates</target> <target>Show available Part-DB updates</target>
</segment> </segment>
@ -11729,5 +11729,23 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<target>Back to user settings</target> <target>Back to user settings</target>
</segment> </segment>
</unit> </unit>
<unit id="VVpmfIj" name="project.build.dont_check_quantity">
<segment state="translated">
<source>project.build.dont_check_quantity</source>
<target>Do not check quantities</target>
</segment>
</unit>
<unit id="AzYSIiX" name="project.build.dont_check_quantity.help">
<segment state="translated">
<source>project.build.dont_check_quantity.help</source>
<target>If this option is selected, the given withdraw quantities are used as given, no matter if more or less parts are actually required to build this project.</target>
</segment>
</unit>
<unit id="tfOeMsC" name="part_list.action.invert_selection">
<segment state="translated">
<source>part_list.action.invert_selection</source>
<target>Invert selection</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -1860,6 +1860,20 @@
<target>库存</target> <target>库存</target>
</segment> </segment>
</unit> </unit>
<unit id="t2TNwOq" name="Unknown">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\Parts\info\_extended_infos.html.twig:13</note>
<note category="file-source" priority="1">Part-DB1\templates\Parts\info\_extended_infos.html.twig:28</note>
<note category="file-source" priority="1">Part-DB1\templates\Parts\info\_extended_infos.html.twig:50</note>
<note priority="1">Part-DB1\templates\Parts\info\_extended_infos.html.twig:13</note>
<note priority="1">Part-DB1\templates\Parts\info\_extended_infos.html.twig:28</note>
<note priority="1">Part-DB1\templates\Parts\info\_extended_infos.html.twig:50</note>
</notes>
<segment state="translated">
<source>Unknown</source>
<target>未知</target>
</segment>
</unit>
<unit id="3tdTZVD" name="part.supplier.name"> <unit id="3tdTZVD" name="part.supplier.name">
<notes> <notes>
<note category="file-source" priority="1">Part-DB1\templates\Parts\info\_order_infos.html.twig:5</note> <note category="file-source" priority="1">Part-DB1\templates\Parts\info\_order_infos.html.twig:5</note>
@ -1880,6 +1894,16 @@
<target>元件编号</target> <target>元件编号</target>
</segment> </segment>
</unit> </unit>
<unit id="qNZM46r" name="delete.caption">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\Parts\info\_order_infos.html.twig:72</note>
<note priority="1">Part-DB1\templates\Parts\info\_order_infos.html.twig:72</note>
</notes>
<segment state="translated">
<source>delete.caption</source>
<target>删除</target>
</segment>
</unit>
<unit id="upshmqD" name="part_lots.location_unknown"> <unit id="upshmqD" name="part_lots.location_unknown">
<notes> <notes>
<note category="file-source" priority="1">Part-DB1\templates\Parts\info\_part_lots.html.twig:24</note> <note category="file-source" priority="1">Part-DB1\templates\Parts\info\_part_lots.html.twig:24</note>

View file

@ -24,7 +24,7 @@ var Encore = require('@symfony/webpack-encore');
const zlib = require('zlib'); const zlib = require('zlib');
const CompressionPlugin = require("compression-webpack-plugin"); const CompressionPlugin = require("compression-webpack-plugin");
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const CKEditorWebpackPlugin = require( '@ckeditor/ckeditor5-dev-webpack-plugin' ); const { CKEditorTranslationsPlugin } = require( '@ckeditor/ckeditor5-dev-translations' );
const { styles } = require( '@ckeditor/ckeditor5-dev-utils' ); const { styles } = require( '@ckeditor/ckeditor5-dev-utils' );
// Manually configure the runtime environment if not already configured yet by the "encore" command. // Manually configure the runtime environment if not already configured yet by the "encore" command.
@ -120,7 +120,7 @@ Encore
// uncomment if you're having problems with a jQuery plugin // uncomment if you're having problems with a jQuery plugin
.autoProvidejQuery() .autoProvidejQuery()
.addPlugin( new CKEditorWebpackPlugin( { .addPlugin( new CKEditorTranslationsPlugin( {
// See https://ckeditor.com/docs/ckeditor5/latest/features/ui-language.html // See https://ckeditor.com/docs/ckeditor5/latest/features/ui-language.html
language: 'en', language: 'en',
addMainLanguageTranslationsToAllAssets: true, addMainLanguageTranslationsToAllAssets: true,

1059
yarn.lock

File diff suppressed because it is too large Load diff