mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 01:25:55 +02:00
Merge branch 'master' into api
This commit is contained in:
commit
85f3ba6aaa
35 changed files with 1219 additions and 878 deletions
|
@ -28,7 +28,7 @@
|
|||
PassEnv APP_ENV APP_DEBUG APP_SECRET
|
||||
PassEnv TRUSTED_PROXIES TRUSTED_HOSTS LOCK_DSN
|
||||
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 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
|
||||
|
|
3
.env
3
.env
|
@ -44,6 +44,9 @@ DEFAULT_URI="https://partdb.changeme.invalid/"
|
|||
# Leave this empty, to make all change reasons optional
|
||||
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
|
||||
###################################################################################
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
1.7.0-dev
|
||||
1.7.0
|
||||
|
|
|
@ -65,8 +65,13 @@ export default class extends Controller {
|
|||
localStorage.setItem( this.getStateSaveKey(), JSON.stringify(data) );
|
||||
}
|
||||
|
||||
stateLoadCallback(settings) {
|
||||
const data = JSON.parse( localStorage.getItem(this.getStateSaveKey()) );
|
||||
stateLoadCallback() {
|
||||
const json = localStorage.getItem(this.getStateSaveKey());
|
||||
if(json === null || json === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = JSON.parse(json);
|
||||
|
||||
if (data) {
|
||||
//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
|
||||
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 = {
|
||||
colReorder: true,
|
||||
responsive: true,
|
||||
|
@ -221,4 +239,16 @@ export default class extends Controller {
|
|||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -91,7 +91,7 @@ th.select-checkbox {
|
|||
/** Fix datatables select-checkbox position */
|
||||
table.dataTable tr.selected td.select-checkbox:after
|
||||
{
|
||||
margin-top: -25px !important;
|
||||
margin-top: -20px !important;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -47,7 +47,8 @@
|
|||
method: config.method,
|
||||
data: {
|
||||
_dt: config.name,
|
||||
_init: true
|
||||
_init: true,
|
||||
order: config.initial_order ?? undefined,
|
||||
}
|
||||
}).done(function(data) {
|
||||
var baseState;
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
"phpstan/phpstan-strict-rules": "^1.5",
|
||||
"phpstan/phpstan-symfony": "^1.1.7",
|
||||
"psalm/plugin-symfony": "^v5.0.1",
|
||||
"rector/rector": "^0.17.0",
|
||||
"rector/rector": "^0.18.0",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"symfony/browser-kit": "6.3.*",
|
||||
"symfony/css-selector": "6.3.*",
|
||||
|
@ -108,7 +108,7 @@
|
|||
"symfony/phpunit-bridge": "6.3.*",
|
||||
"symfony/stopwatch": "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"
|
||||
},
|
||||
"suggest": {
|
||||
|
|
440
composer.lock
generated
440
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -4,6 +4,9 @@ framework:
|
|||
csrf_protection: 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
|
||||
# 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
|
||||
|
|
|
@ -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.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
|
||||
|
|
|
@ -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_delete`: Delete operation of a existing 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
|
||||
* `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`
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
|
|
@ -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:
|
||||
|
||||
* `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,
|
||||
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_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`).
|
||||
|
||||
**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
|
||||
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/).
|
||||
|
|
|
@ -73,3 +73,10 @@ See the configuration reference for more information.
|
|||
## 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
|
||||
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`.
|
68
package.json
68
package.json
|
@ -30,38 +30,38 @@
|
|||
"build": "encore production --progress"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ckeditor/ckeditor5-alignment": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-autoformat": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-basic-styles": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-block-quote": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-code-block": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-dev-utils": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13",
|
||||
"@ckeditor/ckeditor5-editor-classic": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-essentials": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-find-and-replace": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-font": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-heading": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-highlight": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-horizontal-line": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-html-embed": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-html-support": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-image": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-indent": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-link": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-list": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-markdown-gfm": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-media-embed": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-paragraph": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-paste-from-office": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-remove-format": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-source-editing": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-special-characters": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-table": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-theme-lark": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-upload": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-watchdog": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-word-count": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-alignment": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-autoformat": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-basic-styles": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-block-quote": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-code-block": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-dev-translations": "^38.4.0",
|
||||
"@ckeditor/ckeditor5-dev-utils": "^38.4.0",
|
||||
"@ckeditor/ckeditor5-editor-classic": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-essentials": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-find-and-replace": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-font": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-heading": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-highlight": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-horizontal-line": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-html-embed": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-html-support": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-image": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-indent": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-link": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-list": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-markdown-gfm": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-media-embed": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-paragraph": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-paste-from-office": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-remove-format": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-source-editing": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-special-characters": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-table": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-theme-lark": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-upload": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-watchdog": "^39.0.1",
|
||||
"@ckeditor/ckeditor5-word-count": "^39.0.1",
|
||||
"@jbtronics/bs-treeview": "^1.0.1",
|
||||
"@zxcvbn-ts/core": "^3.0.2",
|
||||
"@zxcvbn-ts/language-common": "^3.0.3",
|
||||
|
@ -81,13 +81,13 @@
|
|||
"datatables.net-responsive-bs5": "^2.2.3",
|
||||
"datatables.net-select-bs5": "^1.2.7",
|
||||
"dompurify": "^3.0.3",
|
||||
"emoji.json": "^14.0.0",
|
||||
"emoji.json": "^15.0.0",
|
||||
"exports-loader": "^3.0.0",
|
||||
"html5-qrcode": "^2.2.1",
|
||||
"json-formatter-js": "^2.3.4",
|
||||
"jszip": "^3.2.0",
|
||||
"katex": "^0.16.0",
|
||||
"marked": "^5.1.0",
|
||||
"marked": "^7.0.4",
|
||||
"marked-gfm-heading-id": "^3.0.4",
|
||||
"marked-mangle": "^1.0.1",
|
||||
"pdfmake": "^0.2.2",
|
||||
|
|
|
@ -49,6 +49,8 @@ class HomepageController extends AbstractController
|
|||
public function homepage(Request $request, GitVersionInfo $versionInfo, EntityManagerInterface $entityManager,
|
||||
UpdateAvailableManager $updateAvailableManager): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('HAS_ACCESS_PERMISSIONS');
|
||||
|
||||
if ($this->isGranted('@tools.lastActivity')) {
|
||||
$table = $this->dataTable->createFromType(
|
||||
LogDataTable::class,
|
||||
|
|
|
@ -160,7 +160,7 @@ class PartController extends AbstractController
|
|||
public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator,
|
||||
AttachmentSubmitHandler $attachmentSubmitHandler, ProjectBuildPartHelper $projectBuildPartHelper,
|
||||
#[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) {
|
||||
|
|
|
@ -46,6 +46,7 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
|
|||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
#[Route(path: '/user')]
|
||||
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}/', 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)
|
||||
$permissionSchemaUpdater->userUpgradeSchemaRecursively($entity);
|
||||
|
@ -108,7 +110,7 @@ class UserController extends BaseAdminController
|
|||
|
||||
$this->addFlash('success', 'user.edit.reset_success');
|
||||
} else {
|
||||
$this->addFlash('danger', 'csfr_invalid');
|
||||
$this->addFlash('error', 'csfr_invalid');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -120,6 +122,13 @@ class UserController extends BaseAdminController
|
|||
|
||||
$permissionPresetsHelper->applyPreset($entity, $preset);
|
||||
|
||||
//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');
|
||||
|
@ -128,7 +137,10 @@ class UserController extends BaseAdminController
|
|||
return $this->redirectToRoute('user_edit', ['id' => $entity->getID()]);
|
||||
}
|
||||
|
||||
$this->addFlash('danger', 'csfr_invalid');
|
||||
|
||||
} else {
|
||||
$this->addFlash('error', 'csfr_invalid');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->_edit($entity, $request, $em, $timestamp);
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -62,6 +62,15 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface
|
|||
'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, [
|
||||
'label' => 'part.info.withdraw_modal.comment',
|
||||
'help' => 'part.info.withdraw_modal.comment.hint',
|
||||
|
@ -124,6 +133,7 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface
|
|||
}
|
||||
|
||||
$forms['comment']->setData($data->getComment());
|
||||
$forms['dontCheckQuantity']->setData($data->isDontCheckQuantity());
|
||||
$forms['addBuildsToBuildsPart']->setData($data->getAddBuildsToBuildsPart());
|
||||
if (isset($forms['buildsPartLot'])) {
|
||||
$forms['buildsPartLot']->setData($data->getBuildsPartLot());
|
||||
|
@ -150,6 +160,8 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface
|
|||
}
|
||||
|
||||
$data->setComment($forms['comment']->getData());
|
||||
$data->setDontCheckQuantity($forms['dontCheckQuantity']->getData());
|
||||
|
||||
if (isset($forms['buildsPartLot'])) {
|
||||
$lot = $forms['buildsPartLot']->getData();
|
||||
if (!$lot) { //When the user selected "Create new lot", create a new lot
|
||||
|
|
|
@ -47,6 +47,8 @@ final class ProjectBuildRequest
|
|||
|
||||
private bool $add_build_to_builds_part = false;
|
||||
|
||||
private bool $dont_check_quantity = false;
|
||||
|
||||
/**
|
||||
* @param Project $project The project that should be build
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
45
src/Security/Voter/HasAccessPermissionsVoter.php
Normal file
45
src/Security/Voter/HasAccessPermissionsVoter.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -265,8 +265,7 @@ class PKDatastructureImporter
|
|||
{
|
||||
$count = $this->importElementsWithCategory($data, Storelocation::class, 'storagelocation');
|
||||
|
||||
//Footprints have both attachments and images
|
||||
$this->importAttachments($data, 'storagelocationimage', Storelocation::class, 'footprint_id', StorelocationAttachment::class);
|
||||
$this->importAttachments($data, 'storagelocationimage', Storelocation::class, 'storageLocation_id', StorelocationAttachment::class);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Services\System;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shivas\VersioningBundle\Service\VersionManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
@ -41,7 +43,8 @@ class UpdateAvailableManager
|
|||
|
||||
public function __construct(private readonly HttpClientInterface $httpClient,
|
||||
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,6 +110,7 @@ class UpdateAvailableManager
|
|||
|
||||
return $this->updateCache->get(self::CACHE_KEY, function (ItemInterface $item) {
|
||||
$item->expiresAfter(self::CACHE_TTL);
|
||||
try {
|
||||
$response = $this->httpClient->request('GET', self::API_URL);
|
||||
$result = $response->toArray();
|
||||
$tag_name = $result['tag_name'];
|
||||
|
@ -118,6 +122,22 @@ class UpdateAvailableManager
|
|||
'version' => $version,
|
||||
'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'
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
{
|
||||
$cache = new ConfigCache($this->cache_file, $this->kernel_debug_enabled);
|
||||
|
|
|
@ -69,12 +69,12 @@ class ValidProjectBuildRequestValidator extends ConstraintValidator
|
|||
->addViolation();
|
||||
}
|
||||
|
||||
if ($withdraw_sum > $needed_amount) {
|
||||
if ($withdraw_sum > $needed_amount && $value->isDontCheckQuantity() === false) {
|
||||
$this->buildViolationForLot($lot, 'validator.project_build.lot_bigger_than_needed')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if ($withdraw_sum < $needed_amount) {
|
||||
if ($withdraw_sum < $needed_amount && $value->isDontCheckQuantity() === false) {
|
||||
$this->buildViolationForLot($lot, 'validator.project_build.lot_smaller_than_needed')
|
||||
->addViolation();
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@
|
|||
{# <span id="select_count"></span> #}
|
||||
|
||||
<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="badge bg-primary">{% trans with {'%count%': '<span ' ~ stimulus_target('elements/datatables/parts', 'selectCount') ~ '></span>'} %}part_list.action.part_count{% endtrans %}</span>
|
||||
</span>
|
||||
|
@ -75,7 +77,7 @@
|
|||
{# This is left empty, as this will be filled by Javascript #}
|
||||
</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>
|
||||
|
||||
|
|
|
@ -74,6 +74,9 @@
|
|||
</table>
|
||||
|
||||
{{ form_row(form.comment) }}
|
||||
<div {{ stimulus_controller('pages/dont_check_quantity_checkbox') }}>
|
||||
{{ form_row(form.dontCheckQuantity) }}
|
||||
</div>
|
||||
|
||||
{{ form_row(form.addBuildsToBuildsPart) }}
|
||||
{% if form.buildsPartLot is defined %}
|
||||
|
|
|
@ -31,15 +31,12 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
|||
|
||||
class PermissionManagerTest extends WebTestCase
|
||||
{
|
||||
protected $user_withoutGroup;
|
||||
protected ?User $user_withoutGroup = null;
|
||||
|
||||
protected $user;
|
||||
protected $group;
|
||||
protected ?User $user = null;
|
||||
protected ?Group $group = null;
|
||||
|
||||
/**
|
||||
* @var PermissionManager
|
||||
*/
|
||||
protected $service;
|
||||
protected ?PermissionManager $service = null;
|
||||
|
||||
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, '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));
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11575,5 +11575,29 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
|
|||
<target>Quelle</target>
|
||||
</segment>
|
||||
</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>
|
||||
</xliff>
|
||||
|
|
|
@ -731,10 +731,10 @@
|
|||
</notes>
|
||||
<segment>
|
||||
<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>!
|
||||
<br>
|
||||
The user will have to set up all two-factor authentication methods again and print new backup codes! <br><br>
|
||||
<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>
|
||||
<target>This will disable <b>all active two-factor authentication methods of the user</b> and delete the <b>backup codes</b>!
|
||||
<br>
|
||||
The user will have to set up all two-factor authentication methods again and print new backup codes! <br><br>
|
||||
<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>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="02HvwiS" name="user.edit.tfa.disable_tfa.btn">
|
||||
|
@ -11326,67 +11326,67 @@ Element 3</target>
|
|||
</segment>
|
||||
</unit>
|
||||
<unit id="DGczoY6" name="tfa_u2f.add_key.registration_error">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<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>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ie0Ca0l" name="log.target_type.none">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>log.target_type.none</source>
|
||||
<target>None</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="R2nX4ip" name="ui.darkmode.light">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>ui.darkmode.light</source>
|
||||
<target>Light</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="3NHpuW3" name="ui.darkmode.dark">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>ui.darkmode.dark</source>
|
||||
<target>Dark</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="4TGOK5_" name="ui.darkmode.auto">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>ui.darkmode.auto</source>
|
||||
<target>Auto (decide based on system settings)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="9N0N8aL" name="label_generator.no_lines_given">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>label_generator.no_lines_given</source>
|
||||
<target>No text content given! The labels will remain empty.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="RdFvZsb" name="user.password_strength.very_weak">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>user.password_strength.very_weak</source>
|
||||
<target>Very weak</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="IBjmblZ" name="user.password_strength.weak">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>user.password_strength.weak</source>
|
||||
<target>Weak</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="qSm_ID0" name="user.password_strength.medium">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>user.password_strength.medium</source>
|
||||
<target>Medium</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="aWAaADS" name="user.password_strength.strong">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>user.password_strength.strong</source>
|
||||
<target>Strong</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Wa9CStW" name="user.password_strength.very_strong">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>user.password_strength.very_strong</source>
|
||||
<target>Very strong</target>
|
||||
</segment>
|
||||
|
@ -11580,25 +11580,25 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
|||
</segment>
|
||||
</unit>
|
||||
<unit id="21tIbM2" name="update_manager.new_version_available.title">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>update_manager.new_version_available.title</source>
|
||||
<target>New version available</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id=".c.eoDV" name="update_manager.new_version_available.text">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>update_manager.new_version_available.text</source>
|
||||
<target>A new version of Part-DB is available. Check it out here</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<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>
|
||||
<target>Only administrators can see this message.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="IFkvJpC" name="perm.system.show_available_updates">
|
||||
<segment>
|
||||
<segment state="translated">
|
||||
<source>perm.system.show_available_updates</source>
|
||||
<target>Show available Part-DB updates</target>
|
||||
</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>
|
||||
</segment>
|
||||
</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>
|
||||
</xliff>
|
||||
|
|
|
@ -1860,6 +1860,20 @@
|
|||
<target>库存</target>
|
||||
</segment>
|
||||
</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">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">Part-DB1\templates\Parts\info\_order_infos.html.twig:5</note>
|
||||
|
@ -1880,6 +1894,16 @@
|
|||
<target>元件编号</target>
|
||||
</segment>
|
||||
</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">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">Part-DB1\templates\Parts\info\_part_lots.html.twig:24</note>
|
||||
|
|
|
@ -24,7 +24,7 @@ var Encore = require('@symfony/webpack-encore');
|
|||
const zlib = require('zlib');
|
||||
const CompressionPlugin = require("compression-webpack-plugin");
|
||||
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' );
|
||||
|
||||
// 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
|
||||
.autoProvidejQuery()
|
||||
|
||||
.addPlugin( new CKEditorWebpackPlugin( {
|
||||
.addPlugin( new CKEditorTranslationsPlugin( {
|
||||
// See https://ckeditor.com/docs/ckeditor5/latest/features/ui-language.html
|
||||
language: 'en',
|
||||
addMainLanguageTranslationsToAllAssets: true,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue