mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-07-04 16:31:34 +00:00
Compare commits
61 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
523b07ef2f | ||
|
|
ffe2f8004b | ||
|
|
7244803937 | ||
|
|
214646caa6 | ||
|
|
ffcfdb793f | ||
|
|
e03eda84c5 | ||
|
|
4d3e2e28a5 | ||
|
|
8ed14ca708 | ||
|
|
34257332ff | ||
|
|
2503e63bb2 | ||
|
|
0eba7121aa | ||
|
|
188444b30f | ||
|
|
cd87b59c15 | ||
|
|
b3895c1e91 | ||
|
|
23e22b19e2 | ||
|
|
9c919a9be9 | ||
|
|
eb7da91c44 | ||
|
|
b8fc5d4ace | ||
|
|
fe0809230b | ||
|
|
9d4dabbd20 | ||
|
|
5e18ae2874 | ||
|
|
3e725dd2ec | ||
|
|
ec80115d0a | ||
|
|
b60887c71d | ||
|
|
9f686c88fe | ||
|
|
36244ec63f | ||
|
|
f45e3a9ef8 | ||
|
|
a46f1713fe | ||
|
|
22f23d9c82 | ||
|
|
b4cf5b57fa | ||
|
|
3491559e9f | ||
|
|
b83fc73e18 | ||
|
|
8c88df4ecf | ||
|
|
176d5ad2b6 | ||
|
|
99e56c4b1d | ||
|
|
a489380f49 | ||
|
|
9127bcf25e | ||
|
|
c3af73daae | ||
|
|
7e90f6d707 | ||
|
|
a793bc32c7 | ||
|
|
e642dbe060 | ||
|
|
8ba3139617 | ||
|
|
b62f47ba05 | ||
|
|
0cd83f0322 | ||
|
|
02726fdf69 | ||
|
|
98df91d785 | ||
|
|
bffb2c1d70 | ||
|
|
d44ce85d89 | ||
|
|
12f4a3606e | ||
|
|
192c5fcaa3 | ||
|
|
63d507b2f3 | ||
|
|
ef7e6d6f3b | ||
|
|
350e5a0245 | ||
|
|
704f7e7645 | ||
|
|
589f420b79 | ||
|
|
dfbdac7688 | ||
|
|
11b41ee66a | ||
|
|
c9dd27712c | ||
|
|
8421636b1c | ||
|
|
b357ee196c | ||
|
|
0c5f8dc9fd |
127 changed files with 9508 additions and 5695 deletions
|
|
@ -51,6 +51,15 @@
|
||||||
# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
|
# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
|
||||||
header ?Permissions-Policy "browsing-topics=()"
|
header ?Permissions-Policy "browsing-topics=()"
|
||||||
|
|
||||||
|
# Set a strict CSP and nosniff for all static assets not handled by PHP.
|
||||||
|
# ? means "set only if not already present", so PHP responses carrying a Nelmio CSP are left untouched.
|
||||||
|
header ?Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;"
|
||||||
|
header ?X-Content-Type-Options "nosniff"
|
||||||
|
|
||||||
|
# SVG files get a slightly different CSP because they can embed resources and must not be framed.
|
||||||
|
@svg path *.svg *.svg.gz *.svg.br
|
||||||
|
header @svg Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; sandbox;"
|
||||||
|
|
||||||
# Prevent PHP execution in the media upload directory
|
# Prevent PHP execution in the media upload directory
|
||||||
@php_in_media path_regexp (?i)^/media/.*\.(php[3-8]?|phar|phtml|pht|phps)$
|
@php_in_media path_regexp (?i)^/media/.*\.(php[3-8]?|phar|phtml|pht|phps)$
|
||||||
respond @php_in_media 403
|
respond @php_in_media 403
|
||||||
|
|
|
||||||
10
.env
10
.env
|
|
@ -149,6 +149,16 @@ DISABLE_YEAR2038_BUG_CHECK=0
|
||||||
#TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
#TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||||
#TRUSTED_HOSTS='^(localhost|example\.com)$'
|
#TRUSTED_HOSTS='^(localhost|example\.com)$'
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
|
# Logging settings
|
||||||
|
###################################################################################
|
||||||
|
|
||||||
|
# The minimum level a deprecation notice must have to be written to the var/log/<env>_deprecations.log file.
|
||||||
|
# Deprecation notices are logged with level "info", so this disables the deprecation log by default.
|
||||||
|
# Set to debug to log all deprecation notices
|
||||||
|
DEPRECATION_LOG_LEVEL=emergency
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
###> symfony/lock ###
|
###> symfony/lock ###
|
||||||
# Choose one of the stores below
|
# Choose one of the stores below
|
||||||
|
|
|
||||||
2
.github/workflows/assets_artifact_build.yml
vendored
2
.github/workflows/assets_artifact_build.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
||||||
APP_ENV: prod
|
APP_ENV: prod
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
|
|
|
||||||
2
.github/workflows/docker_build.yml
vendored
2
.github/workflows/docker_build.yml
vendored
|
|
@ -32,7 +32,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
-
|
-
|
||||||
name: Docker meta
|
name: Docker meta
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
|
|
|
||||||
2
.github/workflows/docker_frankenphp.yml
vendored
2
.github/workflows/docker_frankenphp.yml
vendored
|
|
@ -32,7 +32,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
-
|
-
|
||||||
name: Docker meta
|
name: Docker meta
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
|
|
|
||||||
2
.github/workflows/static_analysis.yml
vendored
2
.github/workflows/static_analysis.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
|
|
|
||||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
|
|
@ -46,7 +46,7 @@ jobs:
|
||||||
if: matrix.db-type == 'postgres'
|
if: matrix.db-type == 'postgres'
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
|
|
@ -129,7 +129,7 @@ jobs:
|
||||||
run: ./bin/phpunit --coverage-clover=coverage.xml
|
run: ./bin/phpunit --coverage-clover=coverage.xml
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v6
|
uses: codecov/codecov-action@v7
|
||||||
with:
|
with:
|
||||||
env_vars: PHP_VERSION,DB_TYPE
|
env_vars: PHP_VERSION,DB_TYPE
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ RUN a2dissite 000-default.conf && \
|
||||||
a2enmod proxy_fcgi setenvif && \
|
a2enmod proxy_fcgi setenvif && \
|
||||||
a2enconf php${PHP_VERSION}-fpm && \
|
a2enconf php${PHP_VERSION}-fpm && \
|
||||||
a2enconf docker-php && \
|
a2enconf docker-php && \
|
||||||
a2enmod rewrite
|
a2enmod rewrite headers
|
||||||
|
|
||||||
# Install composer and yarn dependencies for Part-DB
|
# Install composer and yarn dependencies for Part-DB
|
||||||
USER www-data
|
USER www-data
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
2.12.1
|
2.13.1
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@
|
||||||
|
|
||||||
import {Controller} from "@hotwired/stimulus";
|
import {Controller} from "@hotwired/stimulus";
|
||||||
import {visit} from "@hotwired/turbo";
|
import {visit} from "@hotwired/turbo";
|
||||||
import * as bootbox from "bootbox";
|
import {ConfirmSwal} from "../../helpers/swal";
|
||||||
import "../../css/components/bootbox_extensions.css";
|
|
||||||
import "../../css/components/dirty_form.css";
|
import "../../css/components/dirty_form.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -207,11 +206,10 @@ export default class extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
_confirmNavigation(onConfirm) {
|
_confirmNavigation(onConfirm) {
|
||||||
bootbox.confirm({
|
ConfirmSwal.fire({
|
||||||
title: this.confirmTitleValue,
|
titleText: this.confirmTitleValue,
|
||||||
message: this.confirmMessageValue,
|
text: this.confirmMessageValue,
|
||||||
callback: (result) => { if (result) onConfirm(); }
|
}).then(({isConfirmed}) => { if (isConfirmed) onConfirm(); });
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleLinkClick(event) {
|
_handleLinkClick(event) {
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export default class extends Controller {
|
||||||
|
|
||||||
//Make the state persistent over reloads
|
//Make the state persistent over reloads
|
||||||
if(localStorage.getItem(STORAGE_KEY) === 'true') {
|
if(localStorage.getItem(STORAGE_KEY) === 'true') {
|
||||||
sidebarHide();
|
this.hideSidebar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@
|
||||||
|
|
||||||
import {Controller} from "@hotwired/stimulus";
|
import {Controller} from "@hotwired/stimulus";
|
||||||
|
|
||||||
import * as bootbox from "bootbox";
|
import {AlertSwal, ConfirmSwal} from "../../helpers/swal";
|
||||||
import "../../css/components/bootbox_extensions.css";
|
|
||||||
import accept from "attr-accept";
|
import accept from "attr-accept";
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
|
|
@ -62,7 +61,7 @@ export default class extends Controller {
|
||||||
|
|
||||||
if(!prototype) {
|
if(!prototype) {
|
||||||
console.warn("Prototype is not set, we cannot create a new element. This is most likely due to missing permissions.");
|
console.warn("Prototype is not set, we cannot create a new element. This is most likely due to missing permissions.");
|
||||||
bootbox.alert("You do not have the permissions to create a new element. (No protoype element is set)");
|
AlertSwal.fire({"text": "You do not have the permissions to create a new element. (No protoype element is set)"});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,8 +225,10 @@ export default class extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.deleteMessageValue) {
|
if(this.deleteMessageValue) {
|
||||||
bootbox.confirm(this.deleteMessageValue, (result) => {
|
ConfirmSwal.fire({
|
||||||
if (result) {
|
text: this.deleteMessageValue,
|
||||||
|
}).then(({isConfirmed}) => {
|
||||||
|
if (isConfirmed) {
|
||||||
del();
|
del();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,7 @@ import 'datatables.net-colreorder-bs5';
|
||||||
import 'datatables.net-responsive-bs5';
|
import 'datatables.net-responsive-bs5';
|
||||||
import '../../../js/lib/datatables';
|
import '../../../js/lib/datatables';
|
||||||
|
|
||||||
//import 'datatables.net-select-bs5';
|
import 'datatables.net-select-bs5';
|
||||||
//Use the local version containing the fix for the select extension
|
|
||||||
import '../../../js/lib/dataTables.select.mjs';
|
|
||||||
|
|
||||||
|
|
||||||
const EVENT_DT_LOADED = 'dt:loaded';
|
const EVENT_DT_LOADED = 'dt:loaded';
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
import DatatablesController from "./datatables_controller.js";
|
import DatatablesController from "./datatables_controller.js";
|
||||||
import TomSelect from "tom-select";
|
import TomSelect from "tom-select";
|
||||||
|
|
||||||
import * as bootbox from "bootbox";
|
import {ConfirmSwal} from "../../../helpers/swal";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the datatables controller for parts lists
|
* This is the datatables controller for parts lists
|
||||||
|
|
@ -146,15 +146,17 @@ export default class extends DatatablesController {
|
||||||
bubbles: true, //This line is important, otherwise Turbo will not receive the event
|
bubbles: true, //This line is important, otherwise Turbo will not receive the event
|
||||||
});
|
});
|
||||||
|
|
||||||
const confirm = bootbox.confirm({
|
ConfirmSwal.fire({
|
||||||
message: message, title: title, callback: function (result) {
|
titleText: title,
|
||||||
//If the dialog was confirmed, then submit the form.
|
text: message,
|
||||||
if (result) {
|
icon: "warning"
|
||||||
that._confirmed = true;
|
}).then(({isConfirmed}) => {
|
||||||
form.dispatchEvent(that._our_event);
|
//If the dialog was confirmed, then submit the form.
|
||||||
} else {
|
if (isConfirmed) {
|
||||||
that._confirmed = false;
|
that._confirmed = true;
|
||||||
}
|
form.dispatchEvent(that._our_event);
|
||||||
|
} else {
|
||||||
|
that._confirmed = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@
|
||||||
|
|
||||||
import {Controller} from "@hotwired/stimulus";
|
import {Controller} from "@hotwired/stimulus";
|
||||||
|
|
||||||
import * as bootbox from "bootbox";
|
import {ConfirmSwal} from "../../helpers/swal";
|
||||||
import "../../css/components/bootbox_extensions.css";
|
|
||||||
|
|
||||||
export default class extends Controller
|
export default class extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -48,31 +47,32 @@ export default class extends Controller
|
||||||
const submitter = event.submitter;
|
const submitter = event.submitter;
|
||||||
const that = this;
|
const that = this;
|
||||||
|
|
||||||
const confirm = bootbox.confirm({
|
ConfirmSwal.fire({
|
||||||
message: message, title: title, callback: function (result) {
|
titleText: title,
|
||||||
//If the dialog was confirmed, then submit the form.
|
html: message, //Message contains a <br> tag and no user injectable HTML
|
||||||
if (result) {
|
}).then(({isConfirmed}) => {
|
||||||
//Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form
|
//If the dialog was confirmed, then submit the form.
|
||||||
that._confirmed = true;
|
if (isConfirmed) {
|
||||||
|
//Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form
|
||||||
|
that._confirmed = true;
|
||||||
|
|
||||||
//Create a submit button in the form and click it to submit the form
|
//Create a submit button in the form and click it to submit the form
|
||||||
//Before a submit event was dispatched, but this caused weird issues on Firefox causing the delete request being posted twice (and the second time was returning 404). See https://github.com/Part-DB/Part-DB-server/issues/273
|
//Before a submit event was dispatched, but this caused weird issues on Firefox causing the delete request being posted twice (and the second time was returning 404). See https://github.com/Part-DB/Part-DB-server/issues/273
|
||||||
const submit_btn = document.createElement('button');
|
const submit_btn = document.createElement('button');
|
||||||
submit_btn.type = 'submit';
|
submit_btn.type = 'submit';
|
||||||
submit_btn.style.display = 'none';
|
submit_btn.style.display = 'none';
|
||||||
|
|
||||||
//If the clicked button has a value, set it on the submit button
|
//If the clicked button has a value, set it on the submit button
|
||||||
if (submitter.value) {
|
if (submitter.value) {
|
||||||
submit_btn.value = submitter.value;
|
submit_btn.value = submitter.value;
|
||||||
}
|
|
||||||
if (submitter.name) {
|
|
||||||
submit_btn.name = submitter.name;
|
|
||||||
}
|
|
||||||
form.appendChild(submit_btn);
|
|
||||||
submit_btn.click();
|
|
||||||
} else {
|
|
||||||
that._confirmed = false;
|
|
||||||
}
|
}
|
||||||
|
if (submitter.name) {
|
||||||
|
submit_btn.name = submitter.name;
|
||||||
|
}
|
||||||
|
form.appendChild(submit_btn);
|
||||||
|
submit_btn.click();
|
||||||
|
} else {
|
||||||
|
that._confirmed = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@
|
||||||
|
|
||||||
import {Controller} from "@hotwired/stimulus";
|
import {Controller} from "@hotwired/stimulus";
|
||||||
|
|
||||||
import * as bootbox from "bootbox";
|
import {ConfirmSwal} from "../../helpers/swal";
|
||||||
import "../../css/components/bootbox_extensions.css";
|
|
||||||
|
|
||||||
export default class extends Controller
|
export default class extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -53,19 +52,18 @@ export default class extends Controller
|
||||||
|
|
||||||
const that = this;
|
const that = this;
|
||||||
|
|
||||||
bootbox.confirm({
|
ConfirmSwal.fire({
|
||||||
title: this.titleValue,
|
titleText: this.titleValue,
|
||||||
message: this.messageValue,
|
text: this.messageValue,
|
||||||
callback: (result) => {
|
}).then(({isConfirmed}) => {
|
||||||
if (result) {
|
if (isConfirmed) {
|
||||||
//Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form
|
//Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form
|
||||||
that._confirmed = true;
|
that._confirmed = true;
|
||||||
|
|
||||||
//Click the link
|
//Click the link
|
||||||
that.element.click();
|
that.element.click();
|
||||||
} else {
|
} else {
|
||||||
that._confirmed = false;
|
that._confirmed = false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,14 @@
|
||||||
|
|
||||||
|
|
||||||
import {Controller} from "@hotwired/stimulus";
|
import {Controller} from "@hotwired/stimulus";
|
||||||
import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core';
|
import { ZxcvbnFactory } from '@zxcvbn-ts/core';
|
||||||
import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
|
import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
|
||||||
import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en';
|
import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en';
|
||||||
import * as zxcvbnDePackage from '@zxcvbn-ts/language-de';
|
import * as zxcvbnDePackage from '@zxcvbn-ts/language-de';
|
||||||
import * as zxcvbnFrPackage from '@zxcvbn-ts/language-fr';
|
import * as zxcvbnFrPackage from '@zxcvbn-ts/language-fr';
|
||||||
import * as zxcvbnJaPackage from '@zxcvbn-ts/language-ja';
|
import * as zxcvbnJaPackage from '@zxcvbn-ts/language-ja';
|
||||||
|
import * as zxcvbnItPackage from '@zxcvbn-ts/language-it';
|
||||||
|
import * as zxcvbnPlPackage from '@zxcvbn-ts/language-pl';
|
||||||
import {trans} from '../../translator.js';
|
import {trans} from '../../translator.js';
|
||||||
|
|
||||||
/* stimulusFetch: 'lazy' */
|
/* stimulusFetch: 'lazy' */
|
||||||
|
|
@ -34,6 +36,8 @@ export default class extends Controller {
|
||||||
|
|
||||||
static targets = ["badge", "warning"]
|
static targets = ["badge", "warning"]
|
||||||
|
|
||||||
|
_zxcvbnFactory;
|
||||||
|
|
||||||
_getTranslations() {
|
_getTranslations() {
|
||||||
//Get the current locale
|
//Get the current locale
|
||||||
const locale = document.documentElement.lang;
|
const locale = document.documentElement.lang;
|
||||||
|
|
@ -43,6 +47,10 @@ export default class extends Controller {
|
||||||
return zxcvbnFrPackage.translations;
|
return zxcvbnFrPackage.translations;
|
||||||
} else if (locale.includes('ja')) {
|
} else if (locale.includes('ja')) {
|
||||||
return zxcvbnJaPackage.translations;
|
return zxcvbnJaPackage.translations;
|
||||||
|
} else if (locale.includes('it')) {
|
||||||
|
return zxcvbnItPackage.translations;
|
||||||
|
} else if (locale.includes('pl')) {
|
||||||
|
return zxcvbnPlPackage.translations;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Fallback to english
|
//Fallback to english
|
||||||
|
|
@ -56,34 +64,39 @@ export default class extends Controller {
|
||||||
//Configure zxcvbn
|
//Configure zxcvbn
|
||||||
const options = {
|
const options = {
|
||||||
graphs: zxcvbnCommonPackage.adjacencyGraphs,
|
graphs: zxcvbnCommonPackage.adjacencyGraphs,
|
||||||
|
useLevenshtein: true,
|
||||||
dictionary: {
|
dictionary: {
|
||||||
...zxcvbnCommonPackage.dictionary,
|
...zxcvbnCommonPackage.dictionary,
|
||||||
// We could use the english dictionary here too, but it is very big. So we just use the common words
|
// We could use the english dictionary here too, but it is very big. So we just use the common words
|
||||||
//...zxcvbnEnPackage.dictionary,
|
...zxcvbnEnPackage.dictionary,
|
||||||
|
...zxcvbnDePackage.dictionary,
|
||||||
|
|
||||||
|
"partdb": ['part-db', 'partdb', 'part_db', 'part-db-symfony', 'partdb-symfony', 'part_db_symfony'],
|
||||||
},
|
},
|
||||||
translations: this._getTranslations(),
|
translations: this._getTranslations(),
|
||||||
};
|
};
|
||||||
zxcvbnOptions.setOptions(options);
|
|
||||||
|
this._zxcvbnFactory = new ZxcvbnFactory(options);
|
||||||
|
|
||||||
//Add event listener to the password input field
|
//Add event listener to the password input field
|
||||||
this._passwordInput.addEventListener('input', this._onPasswordInput.bind(this));
|
this._passwordInput.addEventListener('input', this._onPasswordInput.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPasswordInput() {
|
async _onPasswordInput() {
|
||||||
//Retrieve the password
|
//Retrieve the password
|
||||||
const password = this._passwordInput.value;
|
const password = this._passwordInput.value;
|
||||||
|
|
||||||
//Estimate the password strength
|
//Estimate the password strength
|
||||||
const result = zxcvbn(password);
|
const result = await this._zxcvbnFactory.checkAsync(password);
|
||||||
|
|
||||||
//Update the badge
|
//Update the badge
|
||||||
this.badgeTarget.parentElement.classList.remove("d-none");
|
this.badgeTarget.parentElement.classList.remove("d-none");
|
||||||
this._setBadgeToLevel(result.score);
|
this._setBadgeToLevel(result.score, result.crackTimes.onlineNoThrottlingXPerSecond.display);
|
||||||
|
|
||||||
this.warningTarget.innerHTML = result.feedback.warning;
|
this.warningTarget.innerHTML = result.feedback.warning;
|
||||||
}
|
}
|
||||||
|
|
||||||
_setBadgeToLevel(level) {
|
_setBadgeToLevel(level, time = null) {
|
||||||
let text, classes;
|
let text, classes;
|
||||||
|
|
||||||
switch (level) {
|
switch (level) {
|
||||||
|
|
@ -118,5 +131,11 @@ export default class extends Controller {
|
||||||
//Re-add the classes
|
//Re-add the classes
|
||||||
this.badgeTarget.classList.add("badge");
|
this.badgeTarget.classList.add("badge");
|
||||||
this.badgeTarget.classList.add(...classes.split(" "));
|
this.badgeTarget.classList.add(...classes.split(" "));
|
||||||
|
|
||||||
|
if (time) {
|
||||||
|
this.badgeTarget.setAttribute("title", trans("user.password_strength.crack_time", {"%time%": time}));
|
||||||
|
} else {
|
||||||
|
this.badgeTarget.removeAttribute("title");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Controller} from "@hotwired/stimulus";
|
import {Controller} from "@hotwired/stimulus";
|
||||||
import * as bootbox from "bootbox";
|
import {AlertSwal} from "../../helpers/swal";
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
|
|
||||||
|
|
@ -35,12 +35,12 @@ export default class extends Controller {
|
||||||
const part_distance = document.getElementById('reel_part_distance').value;
|
const part_distance = document.getElementById('reel_part_distance').value;
|
||||||
|
|
||||||
if (dia_inner == "" || dia_outer == "" || tape_thickness == "") {
|
if (dia_inner == "" || dia_outer == "" || tape_thickness == "") {
|
||||||
bootbox.alert(this.errorMissingValuesValue);
|
AlertSwal.fire({title: this.errorMissingValuesValue});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dia_outer**dia_outer < dia_inner**dia_inner) {
|
if (dia_outer**dia_outer < dia_inner**dia_inner) {
|
||||||
bootbox.alert(this.errorOuterGreaterInnerValue);
|
AlertSwal.fire({title: this.errorOuterGreaterInnerValue});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,12 +61,12 @@ export default class extends Controller {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts_per_meter = 1 / (part_distance / 1000);
|
const parts_per_meter = 1 / (part_distance / 1000);
|
||||||
|
|
||||||
document.getElementById('result_parts_per_meter').textContent = parts_per_meter.toFixed(2) + ' 1/m';
|
document.getElementById('result_parts_per_meter').textContent = parts_per_meter.toFixed(2) + ' 1/m';
|
||||||
|
|
||||||
var parts_amount = (length/1000) * parts_per_meter;
|
const parts_amount = (length / 1000) * parts_per_meter;
|
||||||
|
|
||||||
document.getElementById('result_amount').textContent = Math.floor(parts_amount);
|
document.getElementById('result_amount').textContent = Math.floor(parts_amount).toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
50
assets/css/components/swal.css
Normal file
50
assets/css/components/swal.css
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Respect the dark mode of Bootstrap 5 set via data-bs-theme="dark" on the <html> element. This is done by overriding the CSS variables of the bootstrap-5 theme of SweetAlert2.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html[data-bs-theme="dark"] [data-swal2-theme='bootstrap-5'] {
|
||||||
|
/* POPUP */
|
||||||
|
--swal2-background: #212529;
|
||||||
|
--swal2-color: #fff;
|
||||||
|
--swal2-border: 1px solid #495057;
|
||||||
|
|
||||||
|
/* INPUT */
|
||||||
|
--swal2-input-background: #2b3035;
|
||||||
|
--swal2-input-border: 1px solid #495057;
|
||||||
|
--swal2-input-focus-border: 1px solid #86b7fe;
|
||||||
|
--swal2-input-focus-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.075), 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||||
|
|
||||||
|
/* VALIDATION MESSAGE */
|
||||||
|
--swal2-validation-message-background: #2c0b0e;
|
||||||
|
--swal2-validation-message-color: #ea868f;
|
||||||
|
|
||||||
|
/* FOOTER */
|
||||||
|
--swal2-footer-border-color: #495057;
|
||||||
|
--swal2-footer-background: #343a40;
|
||||||
|
--swal2-footer-color: #adb5bd;
|
||||||
|
|
||||||
|
/* CLOSE BUTTON */
|
||||||
|
--swal2-close-button-color: #fff;
|
||||||
|
|
||||||
|
/* TOASTS */
|
||||||
|
--swal2-toast-border: 1px solid #495057;
|
||||||
|
}
|
||||||
44
assets/helpers/swal.js
Normal file
44
assets/helpers/swal.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* 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 Swal from 'sweetalert2';
|
||||||
|
import 'sweetalert2/themes/bootstrap-5.css';
|
||||||
|
import '../css/components/swal.css'
|
||||||
|
import { trans } from '../translator';
|
||||||
|
|
||||||
|
const BaseSwal = Swal.mixin({
|
||||||
|
position: "top",
|
||||||
|
theme: "bootstrap-5",
|
||||||
|
confirmButtonText: trans('dialog.btn.ok'),
|
||||||
|
cancelButtonText: trans('dialog.btn.cancel'),
|
||||||
|
denyButtonText: trans('dialog.btn.deny'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ConfirmSwal = BaseSwal.mixin({
|
||||||
|
showCancelButton: true,
|
||||||
|
showCloseButton: true,
|
||||||
|
icon: "warning",
|
||||||
|
});
|
||||||
|
|
||||||
|
const AlertSwal = BaseSwal.mixin({
|
||||||
|
showCloseButton: true,
|
||||||
|
icon: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
export { ConfirmSwal, AlertSwal, BaseSwal, BaseSwal as default,};
|
||||||
|
|
@ -30,21 +30,21 @@ import '../css/app/images.css';
|
||||||
// start the Stimulus application
|
// start the Stimulus application
|
||||||
import '../stimulus_bootstrap';
|
import '../stimulus_bootstrap';
|
||||||
|
|
||||||
// Need jQuery? Install it with "yarn add jquery", then uncomment to require it.
|
import $ from 'jquery';
|
||||||
const $ = require('jquery');
|
|
||||||
|
|
||||||
//Only include javascript
|
//Only include javascript
|
||||||
import '@fortawesome/fontawesome-free/css/all.css'
|
import '@fortawesome/fontawesome-free/css/all.css'
|
||||||
|
|
||||||
require('bootstrap');
|
import 'bootstrap';
|
||||||
|
|
||||||
import "./error_handler";
|
import "./error_handler";
|
||||||
import "./tab_remember";
|
import "./tab_remember";
|
||||||
import "./register_events";
|
import "./register_events";
|
||||||
import "./tristate_checkboxes";
|
import "./tristate_checkboxes";
|
||||||
|
|
||||||
//Define jquery globally
|
// Expose jQuery globally so legacy plugins and Bootstrap's jQuery integration
|
||||||
global.$ = global.jQuery = require("jquery");
|
// can find it on window at runtime.
|
||||||
|
global.$ = global.jQuery = $;
|
||||||
|
|
||||||
//Use the local WASM file for the ZXing library
|
//Use the local WASM file for the ZXing library
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as bootbox from "bootbox";
|
import Swal from "../helpers/swal";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this class is imported the user is shown an error dialog if he calls an page via Turbo and an error is responded.
|
* If this class is imported the user is shown an error dialog if he calls an page via Turbo and an error is responded.
|
||||||
|
|
@ -40,21 +40,6 @@ class ErrorHandlerHelper {
|
||||||
_showAlert(statusText, statusCode, location, responseHTML)
|
_showAlert(statusText, statusCode, location, responseHTML)
|
||||||
{
|
{
|
||||||
const httpStatusToText = {
|
const httpStatusToText = {
|
||||||
'200': 'OK',
|
|
||||||
'201': 'Created',
|
|
||||||
'202': 'Accepted',
|
|
||||||
'203': 'Non-Authoritative Information',
|
|
||||||
'204': 'No Content',
|
|
||||||
'205': 'Reset Content',
|
|
||||||
'206': 'Partial Content',
|
|
||||||
'300': 'Multiple Choices',
|
|
||||||
'301': 'Moved Permanently',
|
|
||||||
'302': 'Found',
|
|
||||||
'303': 'See Other',
|
|
||||||
'304': 'Not Modified',
|
|
||||||
'305': 'Use Proxy',
|
|
||||||
'306': 'Unused',
|
|
||||||
'307': 'Temporary Redirect',
|
|
||||||
'400': 'Bad Request',
|
'400': 'Bad Request',
|
||||||
'401': 'Unauthorized',
|
'401': 'Unauthorized',
|
||||||
'402': 'Payment Required',
|
'402': 'Payment Required',
|
||||||
|
|
@ -83,49 +68,67 @@ class ErrorHandlerHelper {
|
||||||
'505': 'HTTP Version Not Supported',
|
'505': 'HTTP Version Not Supported',
|
||||||
};
|
};
|
||||||
|
|
||||||
//If the statusText is empty, we use the status code as text
|
const userFriendlyMessages = {
|
||||||
if (!statusText) {
|
'400': 'The request was invalid or malformed.',
|
||||||
statusText = httpStatusToText[statusCode];
|
'401': 'You need to log in to access this resource.',
|
||||||
}
|
'403': 'You don\'t have permission to access this resource.',
|
||||||
|
'404': 'The requested page or resource could not be found.',
|
||||||
//Create error text
|
'408': 'The request timed out. Please check your connection and try again.',
|
||||||
const title = statusText + ' (Status ' + statusCode + ')';
|
'409': 'There was a conflict with the current state of the resource.',
|
||||||
|
'429': 'Too many requests sent. Please wait a moment and try again.',
|
||||||
let trimString = function (string, length) {
|
'500': 'An internal server error occurred. This is not your fault.',
|
||||||
return string.length > length ?
|
'502': 'The server received an invalid response from an upstream service.',
|
||||||
string.substring(0, length) + '...' :
|
'503': 'The service is temporarily unavailable. Please try again later.',
|
||||||
string;
|
'504': 'The server did not respond in time. Please try again later.',
|
||||||
};
|
};
|
||||||
|
|
||||||
const short_location = trimString(location, 50);
|
if (!statusText) {
|
||||||
|
statusText = httpStatusToText[String(statusCode)] ?? 'Unknown Error';
|
||||||
|
}
|
||||||
|
|
||||||
const alert = bootbox.alert(
|
const title = `${statusText} <small class="text-muted fs-6">(HTTP ${statusCode})</small>`;
|
||||||
{
|
const friendlyMsg = userFriendlyMessages[String(statusCode)]
|
||||||
size: 'large',
|
?? 'An unexpected error occurred. Please try again or contact the administrator.';
|
||||||
message: function() {
|
|
||||||
let url = location;
|
|
||||||
let msg = `Error calling <a href="${url}">${short_location}</a>.<br>`;
|
|
||||||
msg += '<b>Try to reload the page or contact the administrator if this error persists.</b>';
|
|
||||||
|
|
||||||
msg += '<br><br><a class=\"btn btn-outline-secondary mb-2\" data-bs-toggle=\"collapse\" href=\"#iframe_div\" >' + 'View details' + "</a>";
|
const short_location = location.length > 80
|
||||||
msg += "<div class=\" collapse\" id='iframe_div'><iframe height='512' width='100%' id='error-iframe'></iframe></div>";
|
? location.substring(0, 80) + '…'
|
||||||
|
: location;
|
||||||
|
|
||||||
return msg;
|
const msg = `
|
||||||
},
|
<p class="mb-3">${friendlyMsg}</p>
|
||||||
title: title,
|
<p class="text-muted small mb-3">If this error keeps happening, please contact your administrator.</p>
|
||||||
callback: function () {
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#swal-error-details" aria-expanded="false">
|
||||||
//Remove blur
|
<i class="fas fa-code me-1"></i>Technical details
|
||||||
$('#content').removeClass('loading-content');
|
</button>
|
||||||
}
|
<div class="collapse mt-2" id="swal-error-details">
|
||||||
|
<iframe height="400" width="100%" id="error-iframe" style="border:1px solid var(--bs-border-color);border-radius:var(--bs-border-radius);"></iframe>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
});
|
const footer = `<span class="text-muted small">Error while loading: <a href="${location}" class="text-muted text-decoration-none" style="opacity:0.7;">${short_location}</a></span>`;
|
||||||
|
|
||||||
alert.init(function (){
|
Swal.fire({
|
||||||
var dstFrame = document.getElementById('error-iframe');
|
icon: 'error',
|
||||||
//@ts-ignore
|
title: title,
|
||||||
var dstDoc = dstFrame.contentDocument || dstFrame.contentWindow.document;
|
html: msg,
|
||||||
dstDoc.write(responseHTML)
|
footer: footer,
|
||||||
dstDoc.close();
|
width: '90%',
|
||||||
|
confirmButtonText: '<i class="fas fa-rotate-right me-1"></i>Reload page',
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonText: 'Close',
|
||||||
|
showCloseButton: true,
|
||||||
|
reverseButtons: true,
|
||||||
|
didOpen: () => {
|
||||||
|
const dstFrame = document.getElementById('error-iframe');
|
||||||
|
//@ts-ignore
|
||||||
|
const dstDoc = dstFrame.contentDocument || dstFrame.contentWindow.document;
|
||||||
|
dstDoc.write(responseHTML);
|
||||||
|
dstDoc.close();
|
||||||
|
},
|
||||||
|
}).then((result) => {
|
||||||
|
document.getElementById('content').classList.remove('loading-content');
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -19,9 +19,8 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import {Dropdown} from "bootstrap";
|
import {Dropdown, Modal, Tooltip} from "bootstrap";
|
||||||
import ClipboardJS from "clipboard";
|
import ClipboardJS from "clipboard";
|
||||||
import {Modal} from "bootstrap";
|
|
||||||
|
|
||||||
class RegisterEventHelper {
|
class RegisterEventHelper {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -40,8 +39,6 @@ class RegisterEventHelper {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.registerModalDropRemovalOnFormSubmit();
|
this.registerModalDropRemovalOnFormSubmit();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerModalDropRemovalOnFormSubmit() {
|
registerModalDropRemovalOnFormSubmit() {
|
||||||
|
|
@ -83,11 +80,17 @@ class RegisterEventHelper {
|
||||||
|
|
||||||
registerTooltips() {
|
registerTooltips() {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
$(".tooltip").remove();
|
document.querySelectorAll('.tooltip').forEach(el => el.remove());
|
||||||
|
|
||||||
//Exclude dropdown buttons from tooltips, otherwise we run into endless errors from bootstrap (bootstrap.esm.js:614 Bootstrap doesn't allow more than one instance per element. Bound instance: bs.dropdown.)
|
//Exclude dropdown buttons from tooltips, otherwise we run into endless errors from bootstrap (bootstrap.esm.js:614 Bootstrap doesn't allow more than one instance per element. Bound instance: bs.dropdown.)
|
||||||
$('a[title], label[title], button[title]:not([data-bs-toggle="dropdown"]), p[title], span[title], h6[title], h3[title], i[title], small[title]')
|
const tooltipSelector = 'a[title], label[title], button[title]:not([data-bs-toggle="dropdown"]), p[title], span[title], h6[title], h3[title], i[title], small[title]';
|
||||||
//@ts-ignore
|
document.querySelectorAll(tooltipSelector).forEach(el => {
|
||||||
.tooltip("hide").tooltip({container: "body", placement: "auto", boundary: 'window'});
|
const existing = Tooltip.getInstance(el);
|
||||||
|
if (existing) {
|
||||||
|
existing.dispose();
|
||||||
|
}
|
||||||
|
new Tooltip(el, {container: 'body', placement: 'auto', boundary: 'window'});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
this.registerLoadHandler(handler);
|
this.registerLoadHandler(handler);
|
||||||
|
|
@ -95,241 +98,239 @@ class RegisterEventHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
registerSpecialCharInput() {
|
registerSpecialCharInput() {
|
||||||
this.registerLoadHandler(() => {
|
const keydownHandler = function(event) {
|
||||||
//@ts-ignore
|
let use_special_char = event.altKey;
|
||||||
$("input[type=text], input[type=search]").unbind("keydown").keydown(function (event) {
|
|
||||||
let use_special_char = event.altKey;
|
|
||||||
|
|
||||||
let greek_char = "";
|
|
||||||
if (use_special_char){
|
|
||||||
//Use the key property to determine the greek letter (as it is independent of the keyboard layout)
|
|
||||||
switch(event.key) {
|
|
||||||
//Greek letters
|
|
||||||
case "a": //Alpha (lowercase)
|
|
||||||
greek_char = "\u03B1";
|
|
||||||
break;
|
|
||||||
case "A": //Alpha (uppercase)
|
|
||||||
greek_char = "\u0391";
|
|
||||||
break;
|
|
||||||
case "b": //Beta (lowercase)
|
|
||||||
greek_char = "\u03B2";
|
|
||||||
break;
|
|
||||||
case "B": //Beta (uppercase)
|
|
||||||
greek_char = "\u0392";
|
|
||||||
break;
|
|
||||||
case "g": //Gamma (lowercase)
|
|
||||||
greek_char = "\u03B3";
|
|
||||||
break;
|
|
||||||
case "G": //Gamma (uppercase)
|
|
||||||
greek_char = "\u0393";
|
|
||||||
break;
|
|
||||||
case "d": //Delta (lowercase)
|
|
||||||
greek_char = "\u03B4";
|
|
||||||
break;
|
|
||||||
case "D": //Delta (uppercase)
|
|
||||||
greek_char = "\u0394";
|
|
||||||
break;
|
|
||||||
case "e": //Epsilon (lowercase)
|
|
||||||
greek_char = "\u03B5";
|
|
||||||
break;
|
|
||||||
case "E": //Epsilon (uppercase)
|
|
||||||
greek_char = "\u0395";
|
|
||||||
break;
|
|
||||||
case "z": //Zeta (lowercase)
|
|
||||||
greek_char = "\u03B6";
|
|
||||||
break;
|
|
||||||
case "Z": //Zeta (uppercase)
|
|
||||||
greek_char = "\u0396";
|
|
||||||
break;
|
|
||||||
case "h": //Eta (lowercase)
|
|
||||||
greek_char = "\u03B7";
|
|
||||||
break;
|
|
||||||
case "H": //Eta (uppercase)
|
|
||||||
greek_char = "\u0397";
|
|
||||||
break;
|
|
||||||
case "q": //Theta (lowercase)
|
|
||||||
greek_char = "\u03B8";
|
|
||||||
break;
|
|
||||||
case "Q": //Theta (uppercase)
|
|
||||||
greek_char = "\u0398";
|
|
||||||
break;
|
|
||||||
case "i": //Iota (lowercase)
|
|
||||||
greek_char = "\u03B9";
|
|
||||||
break;
|
|
||||||
case "I": //Iota (uppercase)
|
|
||||||
greek_char = "\u0399";
|
|
||||||
break;
|
|
||||||
case "k": //Kappa (lowercase)
|
|
||||||
greek_char = "\u03BA";
|
|
||||||
break;
|
|
||||||
case "K": //Kappa (uppercase)
|
|
||||||
greek_char = "\u039A";
|
|
||||||
break;
|
|
||||||
case "l": //Lambda (lowercase)
|
|
||||||
greek_char = "\u03BB";
|
|
||||||
break;
|
|
||||||
case "L": //Lambda (uppercase)
|
|
||||||
greek_char = "\u039B";
|
|
||||||
break;
|
|
||||||
case "m": //Mu (lowercase)
|
|
||||||
greek_char = "\u03BC";
|
|
||||||
break;
|
|
||||||
case "M": //Mu (uppercase)
|
|
||||||
greek_char = "\u039C";
|
|
||||||
break;
|
|
||||||
case "n": //Nu (lowercase)
|
|
||||||
greek_char = "\u03BD";
|
|
||||||
break;
|
|
||||||
case "N": //Nu (uppercase)
|
|
||||||
greek_char = "\u039D";
|
|
||||||
break;
|
|
||||||
case "x": //Xi (lowercase)
|
|
||||||
greek_char = "\u03BE";
|
|
||||||
break;
|
|
||||||
case "X": //Xi (uppercase)
|
|
||||||
greek_char = "\u039E";
|
|
||||||
break;
|
|
||||||
case "o": //Omicron (lowercase)
|
|
||||||
greek_char = "\u03BF";
|
|
||||||
break;
|
|
||||||
case "O": //Omicron (uppercase)
|
|
||||||
greek_char = "\u039F";
|
|
||||||
break;
|
|
||||||
case "p": //Pi (lowercase)
|
|
||||||
greek_char = "\u03C0";
|
|
||||||
break;
|
|
||||||
case "P": //Pi (uppercase)
|
|
||||||
greek_char = "\u03A0";
|
|
||||||
break;
|
|
||||||
case "r": //Rho (lowercase)
|
|
||||||
greek_char = "\u03C1";
|
|
||||||
break;
|
|
||||||
case "R": //Rho (uppercase)
|
|
||||||
greek_char = "\u03A1";
|
|
||||||
break;
|
|
||||||
case "s": //Sigma (lowercase)
|
|
||||||
greek_char = "\u03C3";
|
|
||||||
break;
|
|
||||||
case "S": //Sigma (uppercase)
|
|
||||||
greek_char = "\u03A3";
|
|
||||||
break;
|
|
||||||
case "t": //Tau (lowercase)
|
|
||||||
greek_char = "\u03C4";
|
|
||||||
break;
|
|
||||||
case "T": //Tau (uppercase)
|
|
||||||
greek_char = "\u03A4";
|
|
||||||
break;
|
|
||||||
case "u": //Upsilon (lowercase)
|
|
||||||
greek_char = "\u03C5";
|
|
||||||
break;
|
|
||||||
case "U": //Upsilon (uppercase)
|
|
||||||
greek_char = "\u03A5";
|
|
||||||
break;
|
|
||||||
case "f": //Phi (lowercase)
|
|
||||||
greek_char = "\u03C6";
|
|
||||||
break;
|
|
||||||
case "F": //Phi (uppercase)
|
|
||||||
greek_char = "\u03A6";
|
|
||||||
break;
|
|
||||||
case "c": //Chi (lowercase)
|
|
||||||
greek_char = "\u03C7";
|
|
||||||
break;
|
|
||||||
case "C": //Chi (uppercase)
|
|
||||||
greek_char = "\u03A7";
|
|
||||||
break;
|
|
||||||
case "y": //Psi (lowercase)
|
|
||||||
greek_char = "\u03C8";
|
|
||||||
break;
|
|
||||||
case "Y": //Psi (uppercase)
|
|
||||||
greek_char = "\u03A8";
|
|
||||||
break;
|
|
||||||
case "w": //Omega (lowercase)
|
|
||||||
greek_char = "\u03C9";
|
|
||||||
break;
|
|
||||||
case "W": //Omega (uppercase)
|
|
||||||
greek_char = "\u03A9";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Use keycodes for special characters as the shift char on the number keys are layout dependent
|
|
||||||
switch (event.keyCode) {
|
|
||||||
case 49: //1 key
|
|
||||||
//Product symbol on shift, sum on no shift
|
|
||||||
greek_char = event.shiftKey ? "\u220F" : "\u2211";
|
|
||||||
break;
|
|
||||||
case 50: //2 key
|
|
||||||
//Integral on no shift, partial derivative on shift
|
|
||||||
greek_char = event.shiftKey ? "\u2202" : "\u222B";
|
|
||||||
break;
|
|
||||||
case 51: //3 key
|
|
||||||
//Less than or equal on no shift, greater than or equal on shift
|
|
||||||
greek_char = event.shiftKey ? "\u2265" : "\u2264";
|
|
||||||
break;
|
|
||||||
case 52: //4 key
|
|
||||||
//Empty set on shift, infinity on no shift
|
|
||||||
greek_char = event.shiftKey ? "\u2205" : "\u221E";
|
|
||||||
break;
|
|
||||||
case 53: //5 key
|
|
||||||
//Not equal on shift, approx equal on no shift
|
|
||||||
greek_char = event.shiftKey ? "\u2260" : "\u2248";
|
|
||||||
break;
|
|
||||||
case 54: //6 key
|
|
||||||
//Element of on no shift, not element of on shift
|
|
||||||
greek_char = event.shiftKey ? "\u2209" : "\u2208";
|
|
||||||
break;
|
|
||||||
case 55: //7 key
|
|
||||||
//And on shift, or on no shift
|
|
||||||
greek_char = event.shiftKey ? "\u2227" : "\u2228";
|
|
||||||
break;
|
|
||||||
case 56: //8 key
|
|
||||||
//Proportional to on shift, angle on no shift
|
|
||||||
greek_char = event.shiftKey ? "\u221D" : "\u2220";
|
|
||||||
break;
|
|
||||||
case 57: //9 key
|
|
||||||
//Cube root on shift, square root on no shift
|
|
||||||
greek_char = event.shiftKey ? "\u221B" : "\u221A";
|
|
||||||
break;
|
|
||||||
case 48: //0 key
|
|
||||||
//Minus-Plus on shift, plus-minus on no shift
|
|
||||||
greek_char = event.shiftKey ? "\u2213" : "\u00B1";
|
|
||||||
break;
|
|
||||||
|
|
||||||
//Special characters
|
|
||||||
case 219: //hyphen (or ß on german layout)
|
|
||||||
//Copyright on no shift, TM on shift
|
|
||||||
greek_char = event.shiftKey ? "\u2122" : "\u00A9";
|
|
||||||
break;
|
|
||||||
case 191: //forward slash (or # on german layout)
|
|
||||||
//Generic currency on no shift, paragraph on shift
|
|
||||||
greek_char = event.shiftKey ? "\u00B6" : "\u00A4";
|
|
||||||
break;
|
|
||||||
|
|
||||||
//Currency symbols
|
|
||||||
case 192: //: or (ö on german layout)
|
|
||||||
//Euro on no shift, pound on shift
|
|
||||||
greek_char = event.shiftKey ? "\u00A3" : "\u20AC";
|
|
||||||
break;
|
|
||||||
case 221: //; or (ä on german layout)
|
|
||||||
//Yen on no shift, dollar on shift
|
|
||||||
greek_char = event.shiftKey ? "\u0024" : "\u00A5";
|
|
||||||
break;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if(greek_char=="") return;
|
|
||||||
|
|
||||||
let $txt = $(this);
|
|
||||||
//@ts-ignore
|
|
||||||
let caretPos = $txt[0].selectionStart;
|
|
||||||
let textAreaTxt = $txt.val().toString();
|
|
||||||
$txt.val(textAreaTxt.substring(0, caretPos) + greek_char + textAreaTxt.substring(caretPos) );
|
|
||||||
|
|
||||||
|
let greek_char = "";
|
||||||
|
if (use_special_char){
|
||||||
|
//Use the key property to determine the greek letter (as it is independent of the keyboard layout)
|
||||||
|
switch(event.key) {
|
||||||
|
//Greek letters
|
||||||
|
case "a": //Alpha (lowercase)
|
||||||
|
greek_char = "α";
|
||||||
|
break;
|
||||||
|
case "A": //Alpha (uppercase)
|
||||||
|
greek_char = "Α";
|
||||||
|
break;
|
||||||
|
case "b": //Beta (lowercase)
|
||||||
|
greek_char = "β";
|
||||||
|
break;
|
||||||
|
case "B": //Beta (uppercase)
|
||||||
|
greek_char = "Β";
|
||||||
|
break;
|
||||||
|
case "g": //Gamma (lowercase)
|
||||||
|
greek_char = "γ";
|
||||||
|
break;
|
||||||
|
case "G": //Gamma (uppercase)
|
||||||
|
greek_char = "Γ";
|
||||||
|
break;
|
||||||
|
case "d": //Delta (lowercase)
|
||||||
|
greek_char = "δ";
|
||||||
|
break;
|
||||||
|
case "D": //Delta (uppercase)
|
||||||
|
greek_char = "Δ";
|
||||||
|
break;
|
||||||
|
case "e": //Epsilon (lowercase)
|
||||||
|
greek_char = "ε";
|
||||||
|
break;
|
||||||
|
case "E": //Epsilon (uppercase)
|
||||||
|
greek_char = "Ε";
|
||||||
|
break;
|
||||||
|
case "z": //Zeta (lowercase)
|
||||||
|
greek_char = "ζ";
|
||||||
|
break;
|
||||||
|
case "Z": //Zeta (uppercase)
|
||||||
|
greek_char = "Ζ";
|
||||||
|
break;
|
||||||
|
case "h": //Eta (lowercase)
|
||||||
|
greek_char = "η";
|
||||||
|
break;
|
||||||
|
case "H": //Eta (uppercase)
|
||||||
|
greek_char = "Η";
|
||||||
|
break;
|
||||||
|
case "q": //Theta (lowercase)
|
||||||
|
greek_char = "θ";
|
||||||
|
break;
|
||||||
|
case "Q": //Theta (uppercase)
|
||||||
|
greek_char = "Θ";
|
||||||
|
break;
|
||||||
|
case "i": //Iota (lowercase)
|
||||||
|
greek_char = "ι";
|
||||||
|
break;
|
||||||
|
case "I": //Iota (uppercase)
|
||||||
|
greek_char = "Ι";
|
||||||
|
break;
|
||||||
|
case "k": //Kappa (lowercase)
|
||||||
|
greek_char = "κ";
|
||||||
|
break;
|
||||||
|
case "K": //Kappa (uppercase)
|
||||||
|
greek_char = "Κ";
|
||||||
|
break;
|
||||||
|
case "l": //Lambda (lowercase)
|
||||||
|
greek_char = "λ";
|
||||||
|
break;
|
||||||
|
case "L": //Lambda (uppercase)
|
||||||
|
greek_char = "Λ";
|
||||||
|
break;
|
||||||
|
case "m": //Mu (lowercase)
|
||||||
|
greek_char = "μ";
|
||||||
|
break;
|
||||||
|
case "M": //Mu (uppercase)
|
||||||
|
greek_char = "Μ";
|
||||||
|
break;
|
||||||
|
case "n": //Nu (lowercase)
|
||||||
|
greek_char = "ν";
|
||||||
|
break;
|
||||||
|
case "N": //Nu (uppercase)
|
||||||
|
greek_char = "Ν";
|
||||||
|
break;
|
||||||
|
case "x": //Xi (lowercase)
|
||||||
|
greek_char = "ξ";
|
||||||
|
break;
|
||||||
|
case "X": //Xi (uppercase)
|
||||||
|
greek_char = "Ξ";
|
||||||
|
break;
|
||||||
|
case "o": //Omicron (lowercase)
|
||||||
|
greek_char = "ο";
|
||||||
|
break;
|
||||||
|
case "O": //Omicron (uppercase)
|
||||||
|
greek_char = "Ο";
|
||||||
|
break;
|
||||||
|
case "p": //Pi (lowercase)
|
||||||
|
greek_char = "π";
|
||||||
|
break;
|
||||||
|
case "P": //Pi (uppercase)
|
||||||
|
greek_char = "Π";
|
||||||
|
break;
|
||||||
|
case "r": //Rho (lowercase)
|
||||||
|
greek_char = "ρ";
|
||||||
|
break;
|
||||||
|
case "R": //Rho (uppercase)
|
||||||
|
greek_char = "Ρ";
|
||||||
|
break;
|
||||||
|
case "s": //Sigma (lowercase)
|
||||||
|
greek_char = "σ";
|
||||||
|
break;
|
||||||
|
case "S": //Sigma (uppercase)
|
||||||
|
greek_char = "Σ";
|
||||||
|
break;
|
||||||
|
case "t": //Tau (lowercase)
|
||||||
|
greek_char = "τ";
|
||||||
|
break;
|
||||||
|
case "T": //Tau (uppercase)
|
||||||
|
greek_char = "Τ";
|
||||||
|
break;
|
||||||
|
case "u": //Upsilon (lowercase)
|
||||||
|
greek_char = "υ";
|
||||||
|
break;
|
||||||
|
case "U": //Upsilon (uppercase)
|
||||||
|
greek_char = "Υ";
|
||||||
|
break;
|
||||||
|
case "f": //Phi (lowercase)
|
||||||
|
greek_char = "φ";
|
||||||
|
break;
|
||||||
|
case "F": //Phi (uppercase)
|
||||||
|
greek_char = "Φ";
|
||||||
|
break;
|
||||||
|
case "c": //Chi (lowercase)
|
||||||
|
greek_char = "χ";
|
||||||
|
break;
|
||||||
|
case "C": //Chi (uppercase)
|
||||||
|
greek_char = "Χ";
|
||||||
|
break;
|
||||||
|
case "y": //Psi (lowercase)
|
||||||
|
greek_char = "ψ";
|
||||||
|
break;
|
||||||
|
case "Y": //Psi (uppercase)
|
||||||
|
greek_char = "Ψ";
|
||||||
|
break;
|
||||||
|
case "w": //Omega (lowercase)
|
||||||
|
greek_char = "ω";
|
||||||
|
break;
|
||||||
|
case "W": //Omega (uppercase)
|
||||||
|
greek_char = "Ω";
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Use keycodes for special characters as the shift char on the number keys are layout dependent
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case 49: //1 key
|
||||||
|
//Product symbol on shift, sum on no shift
|
||||||
|
greek_char = event.shiftKey ? "∏" : "∑";
|
||||||
|
break;
|
||||||
|
case 50: //2 key
|
||||||
|
//Integral on no shift, partial derivative on shift
|
||||||
|
greek_char = event.shiftKey ? "∂" : "∫";
|
||||||
|
break;
|
||||||
|
case 51: //3 key
|
||||||
|
//Less than or equal on no shift, greater than or equal on shift
|
||||||
|
greek_char = event.shiftKey ? "≥" : "≤";
|
||||||
|
break;
|
||||||
|
case 52: //4 key
|
||||||
|
//Empty set on shift, infinity on no shift
|
||||||
|
greek_char = event.shiftKey ? "∅" : "∞";
|
||||||
|
break;
|
||||||
|
case 53: //5 key
|
||||||
|
//Not equal on shift, approx equal on no shift
|
||||||
|
greek_char = event.shiftKey ? "≠" : "≈";
|
||||||
|
break;
|
||||||
|
case 54: //6 key
|
||||||
|
//Element of on no shift, not element of on shift
|
||||||
|
greek_char = event.shiftKey ? "∉" : "∈";
|
||||||
|
break;
|
||||||
|
case 55: //7 key
|
||||||
|
//And on shift, or on no shift
|
||||||
|
greek_char = event.shiftKey ? "∧" : "∨";
|
||||||
|
break;
|
||||||
|
case 56: //8 key
|
||||||
|
//Proportional to on shift, angle on no shift
|
||||||
|
greek_char = event.shiftKey ? "∝" : "∠";
|
||||||
|
break;
|
||||||
|
case 57: //9 key
|
||||||
|
//Cube root on shift, square root on no shift
|
||||||
|
greek_char = event.shiftKey ? "∛" : "√";
|
||||||
|
break;
|
||||||
|
case 48: //0 key
|
||||||
|
//Minus-Plus on shift, plus-minus on no shift
|
||||||
|
greek_char = event.shiftKey ? "∓" : "±";
|
||||||
|
break;
|
||||||
|
|
||||||
|
//Special characters
|
||||||
|
case 219: //hyphen (or ß on german layout)
|
||||||
|
//Copyright on no shift, TM on shift
|
||||||
|
greek_char = event.shiftKey ? "™" : "©";
|
||||||
|
break;
|
||||||
|
case 191: //forward slash (or # on german layout)
|
||||||
|
//Generic currency on no shift, paragraph on shift
|
||||||
|
greek_char = event.shiftKey ? "¶" : "¤";
|
||||||
|
break;
|
||||||
|
|
||||||
|
//Currency symbols
|
||||||
|
case 192: //: or (ö on german layout)
|
||||||
|
//Euro on no shift, pound on shift
|
||||||
|
greek_char = event.shiftKey ? "£" : "€";
|
||||||
|
break;
|
||||||
|
case 221: //; or (ä on german layout)
|
||||||
|
//Yen on no shift, dollar on shift
|
||||||
|
greek_char = event.shiftKey ? "$" : "¥";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(greek_char=="") return;
|
||||||
|
|
||||||
|
const txt = event.currentTarget;
|
||||||
|
const caretPos = txt.selectionStart;
|
||||||
|
const textAreaTxt = txt.value;
|
||||||
|
txt.value = textAreaTxt.substring(0, caretPos) + greek_char + textAreaTxt.substring(caretPos);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.registerLoadHandler(() => {
|
||||||
|
document.querySelectorAll('input[type=text], input[type=search]').forEach(input => {
|
||||||
|
input.removeEventListener('keydown', keydownHandler);
|
||||||
|
input.addEventListener('keydown', keydownHandler);
|
||||||
});
|
});
|
||||||
//@ts-ignore
|
});
|
||||||
this.greek_once = true;
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
20
assets/themes/brite.js
Normal file
20
assets/themes/brite.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "bootswatch/dist/brite/bootstrap.css";
|
||||||
|
|
@ -57,9 +57,10 @@
|
||||||
"scheb/2fa-trusted-device": "^v7.11.0",
|
"scheb/2fa-trusted-device": "^v7.11.0",
|
||||||
"shivas/versioning-bundle": "^4.0",
|
"shivas/versioning-bundle": "^4.0",
|
||||||
"spatie/db-dumper": "^3.3.1",
|
"spatie/db-dumper": "^3.3.1",
|
||||||
"symfony/ai-bundle": "^0.9.0",
|
"symfony/ai-bundle": "^0.10.0",
|
||||||
"symfony/ai-lm-studio-platform": "^0.9.0",
|
"symfony/ai-lm-studio-platform": "^v0.10.0",
|
||||||
"symfony/ai-open-router-platform": "^0.9.0",
|
"symfony/ai-ollama-platform": "^0.10.0",
|
||||||
|
"symfony/ai-open-router-platform": "^0.10.0",
|
||||||
"symfony/apache-pack": "^1.0",
|
"symfony/apache-pack": "^1.0",
|
||||||
"symfony/asset": "7.4.*",
|
"symfony/asset": "7.4.*",
|
||||||
"symfony/console": "7.4.*",
|
"symfony/console": "7.4.*",
|
||||||
|
|
@ -70,6 +71,7 @@
|
||||||
"symfony/flex": "^v2.3.1",
|
"symfony/flex": "^v2.3.1",
|
||||||
"symfony/form": "7.4.*",
|
"symfony/form": "7.4.*",
|
||||||
"symfony/framework-bundle": "7.4.*",
|
"symfony/framework-bundle": "7.4.*",
|
||||||
|
"symfony/html-sanitizer": "7.4.*",
|
||||||
"symfony/http-client": "7.4.*",
|
"symfony/http-client": "7.4.*",
|
||||||
"symfony/http-kernel": "7.4.*",
|
"symfony/http-kernel": "7.4.*",
|
||||||
"symfony/mailer": "7.4.*",
|
"symfony/mailer": "7.4.*",
|
||||||
|
|
|
||||||
1458
composer.lock
generated
1458
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -2,3 +2,4 @@ ai:
|
||||||
platform:
|
platform:
|
||||||
lmstudio:
|
lmstudio:
|
||||||
host_url: '%env(string:settings:ai_lmstudio:hostURL)%'
|
host_url: '%env(string:settings:ai_lmstudio:hostURL)%'
|
||||||
|
http_client: 'app.http_client.ai_lmstudio'
|
||||||
|
|
|
||||||
6
config/packages/ai_ollama_platform.yaml
Normal file
6
config/packages/ai_ollama_platform.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
ai:
|
||||||
|
platform:
|
||||||
|
ollama:
|
||||||
|
endpoint: '%env(string:settings:ai_ollama:endpoint)%'
|
||||||
|
api_key: '%env(string:settings:ai_ollama:apiKey)%'
|
||||||
|
http_client: 'app.http_client.ai_ollama'
|
||||||
|
|
@ -2,3 +2,4 @@ ai:
|
||||||
platform:
|
platform:
|
||||||
openrouter:
|
openrouter:
|
||||||
api_key: '%env(string:settings:ai_openrouter:apiKey)%'
|
api_key: '%env(string:settings:ai_openrouter:apiKey)%'
|
||||||
|
http_client: 'app.http_client.ai_openrouter'
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
services:
|
|
||||||
EasyCorp\EasyLog\EasyLogHandler:
|
|
||||||
public: false
|
|
||||||
arguments: ['%kernel.logs_dir%/%kernel.environment%.log']
|
|
||||||
|
|
||||||
#// FIXME: How to add this configuration automatically without messing up with the monolog configuration?
|
|
||||||
#monolog:
|
|
||||||
# handlers:
|
|
||||||
# buffered:
|
|
||||||
# type: buffer
|
|
||||||
# handler: easylog
|
|
||||||
# channels: ['!event']
|
|
||||||
# level: debug
|
|
||||||
# easylog:
|
|
||||||
# type: service
|
|
||||||
# id: EasyCorp\EasyLog\EasyLogHandler
|
|
||||||
|
|
@ -20,16 +20,16 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Symfony\Config\DoctrineConfig;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class extends the default doctrine ORM configuration to enable native lazy objects on PHP 8.4+.
|
* This file enables native lazy objects on PHP 8.4+.
|
||||||
* We have to do this in a PHP file, because the yaml file does not support conditionals on PHP version.
|
* We have to do this in a PHP file, because the yaml file does not support conditionals on PHP version.
|
||||||
|
*
|
||||||
|
* TODO: Remove this file when we drop support for PHP < 8.4
|
||||||
*/
|
*/
|
||||||
|
|
||||||
return static function(DoctrineConfig $doctrine) {
|
// On PHP 8.4+ we can use native lazy objects, which are much more efficient than proxies.
|
||||||
//On PHP 8.4+ we can use native lazy objects, which are much more efficient than proxies.
|
if (PHP_VERSION_ID >= 80400) {
|
||||||
if (PHP_VERSION_ID >= 80400) {
|
return ['doctrine' => ['orm' => ['enable_native_lazy_objects' => true]]];
|
||||||
$doctrine->orm()->enableNativeLazyObjects(true);
|
}
|
||||||
}
|
|
||||||
};
|
return [];
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ when@prod:
|
||||||
type: stream
|
type: stream
|
||||||
channels: [deprecation]
|
channels: [deprecation]
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%_deprecations.log"
|
path: "%kernel.logs_dir%/%kernel.environment%_deprecations.log"
|
||||||
|
level: "%env(DEPRECATION_LOG_LEVEL)%"
|
||||||
|
|
||||||
when@docker:
|
when@docker:
|
||||||
monolog:
|
monolog:
|
||||||
|
|
@ -75,3 +76,4 @@ when@docker:
|
||||||
type: stream
|
type: stream
|
||||||
channels: [deprecation]
|
channels: [deprecation]
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%_deprecations.log"
|
path: "%kernel.logs_dir%/%kernel.environment%_deprecations.log"
|
||||||
|
level: "%env(DEPRECATION_LOG_LEVEL)%"
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ parameters:
|
||||||
# Themes commented here by default, are not really usable, because of display problems. Enable them at your own risk!
|
# Themes commented here by default, are not really usable, because of display problems. Enable them at your own risk!
|
||||||
partdb.available_themes:
|
partdb.available_themes:
|
||||||
- bootstrap
|
- bootstrap
|
||||||
|
- brite
|
||||||
- cerulean
|
- cerulean
|
||||||
- cosmo
|
- cosmo
|
||||||
- cyborg
|
- cyborg
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* }
|
* }
|
||||||
* @psalm-type ServicesConfig = array{
|
* @psalm-type ServicesConfig = array{
|
||||||
* _defaults?: DefaultsType,
|
* _defaults?: DefaultsType,
|
||||||
* _instanceof?: InstanceofType,
|
* _instanceof?: array<class-string, InstanceofType>,
|
||||||
* ...<string, DefinitionType|AliasType|PrototypeType|StackType|ArgumentsType|null>
|
* ...<string, DefinitionType|AliasType|PrototypeType|StackType|ArgumentsType|null>
|
||||||
* }
|
* }
|
||||||
* @psalm-type ExtensionType = array<string, mixed>
|
* @psalm-type ExtensionType = array<string, mixed>
|
||||||
|
|
@ -653,7 +653,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* time_based_uuid_node?: scalar|Param|null,
|
* time_based_uuid_node?: scalar|Param|null,
|
||||||
* },
|
* },
|
||||||
* html_sanitizer?: bool|array{ // HtmlSanitizer configuration
|
* html_sanitizer?: bool|array{ // HtmlSanitizer configuration
|
||||||
* enabled?: bool|Param, // Default: false
|
* enabled?: bool|Param, // Default: true
|
||||||
* sanitizers?: array<string, array{ // Default: []
|
* sanitizers?: array<string, array{ // Default: []
|
||||||
* allow_safe_elements?: bool|Param, // Allows "safe" elements and attributes. // Default: false
|
* allow_safe_elements?: bool|Param, // Allows "safe" elements and attributes. // Default: false
|
||||||
* allow_static_elements?: bool|Param, // Allows all static elements and attributes from the W3C Sanitizer API standard. // Default: false
|
* allow_static_elements?: bool|Param, // Allows all static elements and attributes from the W3C Sanitizer API standard. // Default: false
|
||||||
|
|
@ -718,7 +718,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.
|
* servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.
|
||||||
* sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver
|
* sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver
|
||||||
* server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.
|
* server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.
|
||||||
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connexion.
|
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connection.
|
||||||
* sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.
|
* sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.
|
||||||
* sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.
|
* sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.
|
||||||
* sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.
|
* sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.
|
||||||
|
|
@ -769,7 +769,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.
|
* servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.
|
||||||
* sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver
|
* sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver
|
||||||
* server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.
|
* server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.
|
||||||
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connexion.
|
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connection.
|
||||||
* sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.
|
* sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.
|
||||||
* sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.
|
* sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.
|
||||||
* sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.
|
* sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.
|
||||||
|
|
@ -801,7 +801,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.
|
* servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.
|
||||||
* sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver
|
* sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver
|
||||||
* server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.
|
* server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.
|
||||||
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connexion.
|
* default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connection.
|
||||||
* sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.
|
* sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.
|
||||||
* sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.
|
* sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.
|
||||||
* sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.
|
* sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.
|
||||||
|
|
@ -2874,8 +2874,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* enable_translation?: bool|Param, // Enable translation for the system prompt // Default: false
|
* enable_translation?: bool|Param, // Enable translation for the system prompt // Default: false
|
||||||
* translation_domain?: string|Param, // The translation domain for the system prompt // Default: null
|
* translation_domain?: string|Param, // The translation domain for the system prompt // Default: null
|
||||||
* },
|
* },
|
||||||
* tools?: bool|array{
|
* tools?: bool|array{ // Tools are opt-in: set to true to inject all services tagged with "ai.tool", or configure an explicit list of tools. When the option is omitted (or set to null or false), no tools are registered.
|
||||||
* enabled?: bool|Param, // Default: true
|
* enabled?: bool|Param, // Default: false
|
||||||
* services?: list<string|array{ // Default: []
|
* services?: list<string|array{ // Default: []
|
||||||
* service?: string|Param,
|
* service?: string|Param,
|
||||||
* agent?: string|Param,
|
* agent?: string|Param,
|
||||||
|
|
@ -2886,6 +2886,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* },
|
* },
|
||||||
* keep_tool_messages?: bool|Param, // Keep tool messages in the conversation history // Default: false
|
* keep_tool_messages?: bool|Param, // Keep tool messages in the conversation history // Default: false
|
||||||
* include_sources?: bool|Param, // Include sources exposed by tools as part of the tool result metadata // Default: false
|
* include_sources?: bool|Param, // Include sources exposed by tools as part of the tool result metadata // Default: false
|
||||||
|
* max_tool_calls?: scalar|Param|null, // Maximum number of tool calls per agent call, null to disable // Default: 50
|
||||||
* fault_tolerant_toolbox?: bool|Param, // Continue the agent run even if a tool call fails // Default: true
|
* fault_tolerant_toolbox?: bool|Param, // Continue the agent run even if a tool call fails // Default: true
|
||||||
* speech?: bool|array{ // Speech (TTS/STT) decorator configuration
|
* speech?: bool|array{ // Speech (TTS/STT) decorator configuration
|
||||||
* enabled?: bool|Param, // Default: true
|
* enabled?: bool|Param, // Default: true
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,28 @@ services:
|
||||||
alias: 'doctrine.migrations.dependency_factory'
|
alias: 'doctrine.migrations.dependency_factory'
|
||||||
|
|
||||||
|
|
||||||
|
####################################################################################################################
|
||||||
|
# AI provider HTTP clients (with configurable timeouts)
|
||||||
|
####################################################################################################################
|
||||||
|
|
||||||
|
app.http_client.ai_ollama:
|
||||||
|
class: Symfony\Contracts\HttpClient\HttpClientInterface
|
||||||
|
factory: ['@http_client', 'withOptions']
|
||||||
|
arguments:
|
||||||
|
- { timeout: '%env(int:settings:ai_ollama:timeout)%' }
|
||||||
|
|
||||||
|
app.http_client.ai_lmstudio:
|
||||||
|
class: Symfony\Contracts\HttpClient\HttpClientInterface
|
||||||
|
factory: ['@http_client', 'withOptions']
|
||||||
|
arguments:
|
||||||
|
- { timeout: '%env(int:settings:ai_lmstudio:timeout)%' }
|
||||||
|
|
||||||
|
app.http_client.ai_openrouter:
|
||||||
|
class: Symfony\Contracts\HttpClient\HttpClientInterface
|
||||||
|
factory: ['@http_client', 'withOptions']
|
||||||
|
arguments:
|
||||||
|
- { timeout: '%env(int:settings:ai_openrouter:timeout)%' }
|
||||||
|
|
||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
# Email
|
# Email
|
||||||
####################################################################################################################
|
####################################################################################################################
|
||||||
|
|
|
||||||
|
|
@ -279,9 +279,13 @@ See the [information providers]({% link usage/information_provider_system.md %})
|
||||||
* `BANNER`: You can configure the text that should be shown as the banner on the homepage. Useful especially for docker
|
* `BANNER`: You can configure the text that should be shown as the banner on the homepage. Useful especially for docker
|
||||||
containers. In all other applications you can just change the `config/banner.md` file.
|
containers. In all other applications you can just change the `config/banner.md` file.
|
||||||
* `DISABLE_YEAR2038_BUG_CHECK` (env only): If set to `1`, the year 2038 bug check is disabled on 32-bit systems, and dates after
|
* `DISABLE_YEAR2038_BUG_CHECK` (env only): If set to `1`, the year 2038 bug check is disabled on 32-bit systems, and dates after
|
||||||
2038 are no longer forbidden. However this will lead to 500 error messages when rendering dates after 2038 as all current
|
2038 are no longer forbidden. However, this will lead to 500 error messages when rendering dates after 2038 as all current
|
||||||
32-bit PHP versions can not format these dates correctly. This setting is for the case that future PHP versions will
|
32-bit PHP versions can not format these dates correctly. This setting is for the case that future PHP versions will
|
||||||
handle this correctly on 32-bit systems. 64-bit systems are not affected by this bug, and the check is always disabled.
|
handle this correctly on 32-bit systems. 64-bit systems are not affected by this bug, and the check is always disabled.
|
||||||
|
* `DEPRECATION_LOG_LEVEL` (default `emergency`) (env only): In the `prod` and `docker` environments, PHP/Symfony
|
||||||
|
deprecation notices are written to their own `var/log/<env>_deprecations.log` file. This option sets the minimum log
|
||||||
|
level a deprecation notice must have to be written there. Since deprecation notices are logged with level `info`,
|
||||||
|
the default value of `emergency` effectively disables this dedicated deprecation log. Set it to `debug` to enable it.
|
||||||
|
|
||||||
## Banner
|
## Banner
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ sudo ln -s /etc/apache2/sites-available/partdb.conf /etc/apache2/sites-enabled/p
|
||||||
Configure apache to show pretty URL paths for Part-DB (`/label/dialog` instead of `/index.php/label/dialog`):
|
Configure apache to show pretty URL paths for Part-DB (`/label/dialog` instead of `/index.php/label/dialog`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo a2enmod rewrite
|
sudo a2enmod rewrite headers
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to access Part-DB via the IP-Address of the server, instead of the domain name, you have to remove the
|
If you want to access Part-DB via the IP-Address of the server, instead of the domain name, you have to remove the
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ server {
|
||||||
root /var/www/partdb/public;
|
root /var/www/partdb/public;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
# Headers are set here for static assets. PHP responses are served via the index.php location
|
||||||
|
# below and inherit neither of these headers, so Nelmio's PHP-side CSP is unaffected.
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
try_files $uri /index.php$is_args$args;
|
try_files $uri /index.php$is_args$args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,9 +62,11 @@ server {
|
||||||
return 403;
|
return 403;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Set Content-Security-Policy for svg files, to block embedded javascript in there
|
# SVG files get a slightly different CSP because they can embed resources and must not be framed.
|
||||||
|
# This regex location takes precedence over location /, so headers must be repeated here.
|
||||||
location ~* \.svg$ {
|
location ~* \.svg$ {
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';";
|
add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; sandbox;" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_log /var/log/nginx/parts.error.log;
|
error_log /var/log/nginx/parts.error.log;
|
||||||
|
|
|
||||||
|
|
@ -25,3 +25,10 @@ You need to supply an API key for OpenRouter to use it as an AI platform in Part
|
||||||
|
|
||||||
[LMStudio](https://lmstudio.ai/) is a local LLM hosting solution that allows you to run LLMs on your own hardware. You can use LMStudio to host your own LLM and connect it to Part-DB for AI features.
|
[LMStudio](https://lmstudio.ai/) is a local LLM hosting solution that allows you to run LLMs on your own hardware. You can use LMStudio to host your own LLM and connect it to Part-DB for AI features.
|
||||||
Currently only LMStudio without any authentication is supported. Supply your LMStudio instance URL (including the port) to use it as an AI platform in Part-DB.
|
Currently only LMStudio without any authentication is supported. Supply your LMStudio instance URL (including the port) to use it as an AI platform in Part-DB.
|
||||||
|
You have to set a model by hand, as suggestions currently do not work yet. Ensure the context length is suitable for your application.
|
||||||
|
|
||||||
|
### Ollama
|
||||||
|
|
||||||
|
[Ollama](https://ollama.com/) is another local LLM hosting solution that allows you to run LLMs on your own hardware. You can use Ollama to host your own LLM and connect it to Part-DB for AI features.
|
||||||
|
Supply your Ollama instance URL (including the port) and an optional API key for authentication to use it as an AI platform in Part-DB. The model selector should give you suggestions about available models.
|
||||||
|
Ensure the context length is suitable for your application.
|
||||||
|
|
|
||||||
22
package.json
22
package.json
|
|
@ -13,7 +13,7 @@
|
||||||
"bootstrap": "^5.1.3",
|
"bootstrap": "^5.1.3",
|
||||||
"core-js": "^3.38.0",
|
"core-js": "^3.38.0",
|
||||||
"intl-messageformat": "^10.5.11",
|
"intl-messageformat": "^10.5.11",
|
||||||
"jquery": "^3.5.1",
|
"jquery": "^4.0.0",
|
||||||
"popper.js": "^1.14.7",
|
"popper.js": "^1.14.7",
|
||||||
"regenerator-runtime": "^0.14.1",
|
"regenerator-runtime": "^0.14.1",
|
||||||
"webpack": "^5.74.0",
|
"webpack": "^5.74.0",
|
||||||
|
|
@ -38,20 +38,21 @@
|
||||||
"@algolia/autocomplete-theme-classic": "^1.17.0",
|
"@algolia/autocomplete-theme-classic": "^1.17.0",
|
||||||
"@jbtronics/bs-treeview": "^1.0.1",
|
"@jbtronics/bs-treeview": "^1.0.1",
|
||||||
"@part-db/html5-qrcode": "^4.0.0",
|
"@part-db/html5-qrcode": "^4.0.0",
|
||||||
"@zxcvbn-ts/core": "^3.0.2",
|
"@zxcvbn-ts/core": "^4.1.2",
|
||||||
"@zxcvbn-ts/language-common": "^3.0.3",
|
"@zxcvbn-ts/language-common": "^4.1.2",
|
||||||
"@zxcvbn-ts/language-de": "^3.0.1",
|
"@zxcvbn-ts/language-de": "^4.1.1",
|
||||||
"@zxcvbn-ts/language-en": "^3.0.1",
|
"@zxcvbn-ts/language-en": "^4.1.1",
|
||||||
"@zxcvbn-ts/language-fr": "^3.0.1",
|
"@zxcvbn-ts/language-fr": "^4.1.1",
|
||||||
"@zxcvbn-ts/language-ja": "^3.0.1",
|
"@zxcvbn-ts/language-it": "^4.1.1",
|
||||||
|
"@zxcvbn-ts/language-ja": "^4.1.1",
|
||||||
|
"@zxcvbn-ts/language-pl": "^4.1.1",
|
||||||
"attr-accept": "^2.2.5",
|
"attr-accept": "^2.2.5",
|
||||||
"barcode-detector": "^3.0.5",
|
"barcode-detector": "^3.0.5",
|
||||||
"bootbox": "^6.0.0",
|
|
||||||
"bootswatch": "^5.1.3",
|
"bootswatch": "^5.1.3",
|
||||||
"bs-custom-file-input": "^1.3.4",
|
"bs-custom-file-input": "^1.3.4",
|
||||||
"ckeditor5": "^48.0.0",
|
"ckeditor5": "^48.0.0",
|
||||||
"clipboard": "^2.0.4",
|
"clipboard": "^2.0.4",
|
||||||
"compression-webpack-plugin": "^11.1.0",
|
"compression-webpack-plugin": "^12.0.0",
|
||||||
"datatables.net": "^2.0.0",
|
"datatables.net": "^2.0.0",
|
||||||
"datatables.net-bs5": "^2.0.0",
|
"datatables.net-bs5": "^2.0.0",
|
||||||
"datatables.net-buttons-bs5": "^3.0.0",
|
"datatables.net-buttons-bs5": "^3.0.0",
|
||||||
|
|
@ -69,11 +70,12 @@
|
||||||
"marked-mangle": "^1.0.1",
|
"marked-mangle": "^1.0.1",
|
||||||
"pdfmake": "^0.3.7",
|
"pdfmake": "^0.3.7",
|
||||||
"stimulus-use": "^0.52.0",
|
"stimulus-use": "^0.52.0",
|
||||||
|
"sweetalert2": "^11.26.25",
|
||||||
"tom-select": "^2.1.0",
|
"tom-select": "^2.1.0",
|
||||||
"ts-loader": "^9.2.6",
|
"ts-loader": "^9.2.6",
|
||||||
"typescript": "^6.0.2"
|
"typescript": "^6.0.2"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"jquery": "^3.5.1"
|
"jquery": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,9 +119,14 @@ DirectoryIndex index.php
|
||||||
</IfModule>
|
</IfModule>
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
# Set Content-Security-Policy for svg files (and compressed variants), to block embedded javascript in there
|
|
||||||
<IfModule mod_headers.c>
|
<IfModule mod_headers.c>
|
||||||
|
# Set a strict CSP for all static assets not handled by PHP.
|
||||||
|
# PHP responses already carry their own CSP via NelmioSecurityBundle, so setifempty leaves those untouched.
|
||||||
|
Header always setifempty Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;"
|
||||||
|
Header always setifempty X-Content-Type-Options "nosniff"
|
||||||
|
|
||||||
|
# SVG files get a slightly different CSP because they can embed resources and must not be framed.
|
||||||
<FilesMatch "\.(svg|svg\.gz|svg\.br)$">
|
<FilesMatch "\.(svg|svg\.gz|svg\.br)$">
|
||||||
Header set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';"
|
Header always set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; sandbox;"
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated on Mon Jun 1 07:07:44 UTC 2026
|
# Generated on Mon Jun 22 07:31:48 UTC 2026
|
||||||
# This file contains all footprints available in the offical KiCAD library
|
# This file contains all footprints available in the offical KiCAD library
|
||||||
Audio_Module:Reverb_BTDR-1H
|
Audio_Module:Reverb_BTDR-1H
|
||||||
Audio_Module:Reverb_BTDR-1V
|
Audio_Module:Reverb_BTDR-1V
|
||||||
|
|
@ -8293,6 +8293,7 @@ Converter_DCDC:Converter_DCDC_Hamamatsu_C11204-1_THT
|
||||||
Converter_DCDC:Converter_DCDC_MeanWell_NID30_THT
|
Converter_DCDC:Converter_DCDC_MeanWell_NID30_THT
|
||||||
Converter_DCDC:Converter_DCDC_MeanWell_NID60_THT
|
Converter_DCDC:Converter_DCDC_MeanWell_NID60_THT
|
||||||
Converter_DCDC:Converter_DCDC_MeanWell_NSD10_THT
|
Converter_DCDC:Converter_DCDC_MeanWell_NSD10_THT
|
||||||
|
Converter_DCDC:Converter_DCDC_MeanWell_SMU02x-xxN_THT
|
||||||
Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxx3C_THT
|
Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxx3C_THT
|
||||||
Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxDC_THT
|
Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxDC_THT
|
||||||
Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxSC_THT
|
Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxSC_THT
|
||||||
|
|
@ -12012,7 +12013,6 @@ Package_DFN_QFN:WDFN-8-1EP_4x3mm_P0.65mm_EP2.4x1.8mm_ThermalVias
|
||||||
Package_DFN_QFN:WDFN-8-1EP_6x5mm_P1.27mm_EP3.4x4mm
|
Package_DFN_QFN:WDFN-8-1EP_6x5mm_P1.27mm_EP3.4x4mm
|
||||||
Package_DFN_QFN:WDFN-8-1EP_8x6mm_P1.27mm_EP6x4.8mm
|
Package_DFN_QFN:WDFN-8-1EP_8x6mm_P1.27mm_EP6x4.8mm
|
||||||
Package_DFN_QFN:WDFN-8-1EP_8x6mm_P1.27mm_EP6x4.8mm_ThermalVias
|
Package_DFN_QFN:WDFN-8-1EP_8x6mm_P1.27mm_EP6x4.8mm_ThermalVias
|
||||||
Package_DFN_QFN:WDFN-8_2x2mm_P0.5mm
|
|
||||||
Package_DFN_QFN:WFDFPN-8-1EP_3x2mm_P0.5mm_EP1.25x1.35mm
|
Package_DFN_QFN:WFDFPN-8-1EP_3x2mm_P0.5mm_EP1.25x1.35mm
|
||||||
Package_DFN_QFN:WQFN-14-1EP_2.5x2.5mm_P0.5mm_EP1.45x1.45mm
|
Package_DFN_QFN:WQFN-14-1EP_2.5x2.5mm_P0.5mm_EP1.45x1.45mm
|
||||||
Package_DFN_QFN:WQFN-14-1EP_2.5x2.5mm_P0.5mm_EP1.45x1.45mm_ThermalVias
|
Package_DFN_QFN:WQFN-14-1EP_2.5x2.5mm_P0.5mm_EP1.45x1.45mm_ThermalVias
|
||||||
|
|
@ -12358,7 +12358,6 @@ Package_DirectFET:DirectFET_SQ
|
||||||
Package_DirectFET:DirectFET_ST
|
Package_DirectFET:DirectFET_ST
|
||||||
Package_LCC:Analog_LCC-8_5x5mm_P1.27mm
|
Package_LCC:Analog_LCC-8_5x5mm_P1.27mm
|
||||||
Package_LCC:MO047AD_PLCC-52_19.1x19.1mm_P1.27mm
|
Package_LCC:MO047AD_PLCC-52_19.1x19.1mm_P1.27mm
|
||||||
Package_LCC:PLCC-20
|
|
||||||
Package_LCC:PLCC-20_9.0x9.0mm_P1.27mm
|
Package_LCC:PLCC-20_9.0x9.0mm_P1.27mm
|
||||||
Package_LCC:PLCC-20_SMD-Socket
|
Package_LCC:PLCC-20_SMD-Socket
|
||||||
Package_LCC:PLCC-20_THT-Socket
|
Package_LCC:PLCC-20_THT-Socket
|
||||||
|
|
@ -13032,8 +13031,6 @@ Package_SON:WSON-6-1EP_2x2mm_P0.65mm_EP1x1.6mm
|
||||||
Package_SON:WSON-6-1EP_2x2mm_P0.65mm_EP1x1.6mm_ThermalVias
|
Package_SON:WSON-6-1EP_2x2mm_P0.65mm_EP1x1.6mm_ThermalVias
|
||||||
Package_SON:WSON-6-1EP_3x3mm_P0.95mm
|
Package_SON:WSON-6-1EP_3x3mm_P0.95mm
|
||||||
Package_SON:WSON-6_1.5x1.5mm_P0.5mm
|
Package_SON:WSON-6_1.5x1.5mm_P0.5mm
|
||||||
Package_SON:WSON-8-1EP_2x2mm_P0.5mm_EP0.9x1.6mm
|
|
||||||
Package_SON:WSON-8-1EP_2x2mm_P0.5mm_EP0.9x1.6mm_ThermalVias
|
|
||||||
Package_SON:WSON-8-1EP_3x2.5mm_P0.5mm_EP1.2x1.5mm_PullBack
|
Package_SON:WSON-8-1EP_3x2.5mm_P0.5mm_EP1.2x1.5mm_PullBack
|
||||||
Package_SON:WSON-8-1EP_3x2.5mm_P0.5mm_EP1.2x1.5mm_PullBack_ThermalVias
|
Package_SON:WSON-8-1EP_3x2.5mm_P0.5mm_EP1.2x1.5mm_PullBack_ThermalVias
|
||||||
Package_SON:WSON-8-1EP_3x3mm_P0.5mm_EP1.2x2mm
|
Package_SON:WSON-8-1EP_3x3mm_P0.5mm_EP1.2x2mm
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated on Mon Jun 1 07:08:24 UTC 2026
|
# Generated on Mon Jun 22 07:32:26 UTC 2026
|
||||||
# This file contains all symbols available in the offical KiCAD library
|
# This file contains all symbols available in the offical KiCAD library
|
||||||
4xxx:14528
|
4xxx:14528
|
||||||
4xxx:14529
|
4xxx:14529
|
||||||
|
|
@ -4954,6 +4954,7 @@ Converter_DCDC:RPMH15-1.5
|
||||||
Converter_DCDC:RPMH24-1.5
|
Converter_DCDC:RPMH24-1.5
|
||||||
Converter_DCDC:RPMH3.3-1.5
|
Converter_DCDC:RPMH3.3-1.5
|
||||||
Converter_DCDC:RPMH5.0-1.5
|
Converter_DCDC:RPMH5.0-1.5
|
||||||
|
Converter_DCDC:SMU02L-24N
|
||||||
Converter_DCDC:TBA1-0310
|
Converter_DCDC:TBA1-0310
|
||||||
Converter_DCDC:TBA1-0311
|
Converter_DCDC:TBA1-0311
|
||||||
Converter_DCDC:TBA1-0510
|
Converter_DCDC:TBA1-0510
|
||||||
|
|
@ -5300,21 +5301,39 @@ Converter_DCDC:TMR-4812
|
||||||
Converter_DCDC:TMR-4821
|
Converter_DCDC:TMR-4821
|
||||||
Converter_DCDC:TMR-4822
|
Converter_DCDC:TMR-4822
|
||||||
Converter_DCDC:TMR-4823
|
Converter_DCDC:TMR-4823
|
||||||
|
Converter_DCDC:TMR10-1211WI
|
||||||
|
Converter_DCDC:TMR10-1212WI
|
||||||
|
Converter_DCDC:TMR10-1213WI
|
||||||
|
Converter_DCDC:TMR10-1215WI
|
||||||
|
Converter_DCDC:TMR10-1222WI
|
||||||
|
Converter_DCDC:TMR10-1223WI
|
||||||
Converter_DCDC:TMR10-2410WIR
|
Converter_DCDC:TMR10-2410WIR
|
||||||
|
Converter_DCDC:TMR10-2411WI
|
||||||
Converter_DCDC:TMR10-2411WIR
|
Converter_DCDC:TMR10-2411WIR
|
||||||
|
Converter_DCDC:TMR10-2412WI
|
||||||
Converter_DCDC:TMR10-2412WIR
|
Converter_DCDC:TMR10-2412WIR
|
||||||
|
Converter_DCDC:TMR10-2413WI
|
||||||
Converter_DCDC:TMR10-2413WIR
|
Converter_DCDC:TMR10-2413WIR
|
||||||
|
Converter_DCDC:TMR10-2415WI
|
||||||
Converter_DCDC:TMR10-2415WIR
|
Converter_DCDC:TMR10-2415WIR
|
||||||
Converter_DCDC:TMR10-2421WIR
|
Converter_DCDC:TMR10-2421WIR
|
||||||
|
Converter_DCDC:TMR10-2422WI
|
||||||
Converter_DCDC:TMR10-2422WIR
|
Converter_DCDC:TMR10-2422WIR
|
||||||
|
Converter_DCDC:TMR10-2423WI
|
||||||
Converter_DCDC:TMR10-2423WIR
|
Converter_DCDC:TMR10-2423WIR
|
||||||
Converter_DCDC:TMR10-4810WIR
|
Converter_DCDC:TMR10-4810WIR
|
||||||
|
Converter_DCDC:TMR10-4811WI
|
||||||
Converter_DCDC:TMR10-4811WIR
|
Converter_DCDC:TMR10-4811WIR
|
||||||
|
Converter_DCDC:TMR10-4812WI
|
||||||
Converter_DCDC:TMR10-4812WIR
|
Converter_DCDC:TMR10-4812WIR
|
||||||
|
Converter_DCDC:TMR10-4813WI
|
||||||
Converter_DCDC:TMR10-4813WIR
|
Converter_DCDC:TMR10-4813WIR
|
||||||
|
Converter_DCDC:TMR10-4815WI
|
||||||
Converter_DCDC:TMR10-4815WIR
|
Converter_DCDC:TMR10-4815WIR
|
||||||
Converter_DCDC:TMR10-4821WIR
|
Converter_DCDC:TMR10-4821WIR
|
||||||
|
Converter_DCDC:TMR10-4822WI
|
||||||
Converter_DCDC:TMR10-4822WIR
|
Converter_DCDC:TMR10-4822WIR
|
||||||
|
Converter_DCDC:TMR10-4823WI
|
||||||
Converter_DCDC:TMR10-4823WIR
|
Converter_DCDC:TMR10-4823WIR
|
||||||
Converter_DCDC:TMR10-7210WIR
|
Converter_DCDC:TMR10-7210WIR
|
||||||
Converter_DCDC:TMR10-7211WIR
|
Converter_DCDC:TMR10-7211WIR
|
||||||
|
|
@ -6124,6 +6143,7 @@ Device:SparkGap
|
||||||
Device:Speaker
|
Device:Speaker
|
||||||
Device:Speaker_Crystal
|
Device:Speaker_Crystal
|
||||||
Device:Speaker_Ultrasound
|
Device:Speaker_Ultrasound
|
||||||
|
Device:Thermal_Jumper
|
||||||
Device:Thermistor
|
Device:Thermistor
|
||||||
Device:Thermistor_NTC
|
Device:Thermistor_NTC
|
||||||
Device:Thermistor_NTC_3Wire
|
Device:Thermistor_NTC_3Wire
|
||||||
|
|
@ -14593,6 +14613,8 @@ MCU_Texas:LM4F111C4QR
|
||||||
MCU_Texas:LM4F111E5QR
|
MCU_Texas:LM4F111E5QR
|
||||||
MCU_Texas:LM4F111H5QR
|
MCU_Texas:LM4F111H5QR
|
||||||
MCU_Texas:MSP432E401Y
|
MCU_Texas:MSP432E401Y
|
||||||
|
MCU_Texas:MSPM0C110xSDDF
|
||||||
|
MCU_Texas:MSPM0C110xSDSG
|
||||||
MCU_Texas:TM4C1230C3PM
|
MCU_Texas:TM4C1230C3PM
|
||||||
MCU_Texas:TM4C1230D5PM
|
MCU_Texas:TM4C1230D5PM
|
||||||
MCU_Texas:TM4C1230E6PM
|
MCU_Texas:TM4C1230E6PM
|
||||||
|
|
@ -15115,6 +15137,7 @@ Memory_Flash:AM29F400Bx-xxEx
|
||||||
Memory_Flash:AM29F400Bx-xxSx
|
Memory_Flash:AM29F400Bx-xxSx
|
||||||
Memory_Flash:AM29PDL128G
|
Memory_Flash:AM29PDL128G
|
||||||
Memory_Flash:AT25DF041x-UxN-x
|
Memory_Flash:AT25DF041x-UxN-x
|
||||||
|
Memory_Flash:AT25SF041B-SSHD-X
|
||||||
Memory_Flash:AT25SF081-SSHD-X
|
Memory_Flash:AT25SF081-SSHD-X
|
||||||
Memory_Flash:AT25SF081-SSHF-X
|
Memory_Flash:AT25SF081-SSHF-X
|
||||||
Memory_Flash:AT25SF081-XMHD-X
|
Memory_Flash:AT25SF081-XMHD-X
|
||||||
|
|
@ -15670,6 +15693,7 @@ Power_Management:LM5069MM-1
|
||||||
Power_Management:LM5069MM-2
|
Power_Management:LM5069MM-2
|
||||||
Power_Management:LM66100DCK
|
Power_Management:LM66100DCK
|
||||||
Power_Management:LM74700
|
Power_Management:LM74700
|
||||||
|
Power_Management:LM74701-Q1
|
||||||
Power_Management:LMG3410
|
Power_Management:LMG3410
|
||||||
Power_Management:LMG5200
|
Power_Management:LMG5200
|
||||||
Power_Management:LT1641-1
|
Power_Management:LT1641-1
|
||||||
|
|
@ -15771,6 +15795,7 @@ Power_Management:TPS22810DBV
|
||||||
Power_Management:TPS22810DRV
|
Power_Management:TPS22810DRV
|
||||||
Power_Management:TPS22917DBV
|
Power_Management:TPS22917DBV
|
||||||
Power_Management:TPS22917LDBV
|
Power_Management:TPS22917LDBV
|
||||||
|
Power_Management:TPS22919DCK
|
||||||
Power_Management:TPS22929D
|
Power_Management:TPS22929D
|
||||||
Power_Management:TPS22993
|
Power_Management:TPS22993
|
||||||
Power_Management:TPS2412D
|
Power_Management:TPS2412D
|
||||||
|
|
@ -16480,6 +16505,7 @@ RF_Module:DWM3000
|
||||||
RF_Module:E18-MS1-PCB
|
RF_Module:E18-MS1-PCB
|
||||||
RF_Module:E73-2G4M04S-52810
|
RF_Module:E73-2G4M04S-52810
|
||||||
RF_Module:E73-2G4M04S-52832
|
RF_Module:E73-2G4M04S-52832
|
||||||
|
RF_Module:ESP-01
|
||||||
RF_Module:ESP-07
|
RF_Module:ESP-07
|
||||||
RF_Module:ESP-12E
|
RF_Module:ESP-12E
|
||||||
RF_Module:ESP-12F
|
RF_Module:ESP-12F
|
||||||
|
|
@ -16681,6 +16707,19 @@ Reference_Voltage:LM4040LP-4.1
|
||||||
Reference_Voltage:LM4040LP-5
|
Reference_Voltage:LM4040LP-5
|
||||||
Reference_Voltage:LM4040LP-8.2
|
Reference_Voltage:LM4040LP-8.2
|
||||||
Reference_Voltage:LM4041LP-ADJ
|
Reference_Voltage:LM4041LP-ADJ
|
||||||
|
Reference_Voltage:LM4050xEM3-2.1
|
||||||
|
Reference_Voltage:LM4050xEM3-2.5
|
||||||
|
Reference_Voltage:LM4050xEM3-3.0
|
||||||
|
Reference_Voltage:LM4050xEM3-3.3
|
||||||
|
Reference_Voltage:LM4050xEM3-4.1
|
||||||
|
Reference_Voltage:LM4050xEM3-5.0
|
||||||
|
Reference_Voltage:LM4050xEX3-2.1
|
||||||
|
Reference_Voltage:LM4050xEX3-2.5
|
||||||
|
Reference_Voltage:LM4050xEX3-3.3
|
||||||
|
Reference_Voltage:LM4050xEX3-4.1
|
||||||
|
Reference_Voltage:LM4050xEX3-5.0
|
||||||
|
Reference_Voltage:LM4051xEM3-1.2
|
||||||
|
Reference_Voltage:LM4051xEX3-1.2
|
||||||
Reference_Voltage:LM4125AIM5-2.5
|
Reference_Voltage:LM4125AIM5-2.5
|
||||||
Reference_Voltage:LM4125IM5-2.0
|
Reference_Voltage:LM4125IM5-2.0
|
||||||
Reference_Voltage:LM4125IM5-2.5
|
Reference_Voltage:LM4125IM5-2.5
|
||||||
|
|
@ -16828,7 +16867,6 @@ Reference_Voltage:MCP1501-25xCH
|
||||||
Reference_Voltage:MCP1501-25xRW
|
Reference_Voltage:MCP1501-25xRW
|
||||||
Reference_Voltage:MCP1501-25xSN
|
Reference_Voltage:MCP1501-25xSN
|
||||||
Reference_Voltage:MCP1501-30xCH
|
Reference_Voltage:MCP1501-30xCH
|
||||||
Reference_Voltage:MCP1501-30xRW
|
|
||||||
Reference_Voltage:MCP1501-30xSN
|
Reference_Voltage:MCP1501-30xSN
|
||||||
Reference_Voltage:MCP1501-33xCH
|
Reference_Voltage:MCP1501-33xCH
|
||||||
Reference_Voltage:MCP1501-33xRW
|
Reference_Voltage:MCP1501-33xRW
|
||||||
|
|
@ -19261,6 +19299,7 @@ Regulator_Switching:LT1373HVCN8
|
||||||
Regulator_Switching:LT1373HVCS8
|
Regulator_Switching:LT1373HVCS8
|
||||||
Regulator_Switching:LT1377CN8
|
Regulator_Switching:LT1377CN8
|
||||||
Regulator_Switching:LT1377CS8
|
Regulator_Switching:LT1377CS8
|
||||||
|
Regulator_Switching:LT1931
|
||||||
Regulator_Switching:LT1945
|
Regulator_Switching:LT1945
|
||||||
Regulator_Switching:LT3430
|
Regulator_Switching:LT3430
|
||||||
Regulator_Switching:LT3430-1
|
Regulator_Switching:LT3430-1
|
||||||
|
|
@ -20989,6 +21028,7 @@ Sensor_Temperature:MCP9501
|
||||||
Sensor_Temperature:MCP9502
|
Sensor_Temperature:MCP9502
|
||||||
Sensor_Temperature:MCP9503
|
Sensor_Temperature:MCP9503
|
||||||
Sensor_Temperature:MCP9504
|
Sensor_Temperature:MCP9504
|
||||||
|
Sensor_Temperature:MCP96xx01x-x-MX
|
||||||
Sensor_Temperature:MCP9700Ax-ELT
|
Sensor_Temperature:MCP9700Ax-ELT
|
||||||
Sensor_Temperature:MCP9700Ax-ETT
|
Sensor_Temperature:MCP9700Ax-ETT
|
||||||
Sensor_Temperature:MCP9700Ax-HLT
|
Sensor_Temperature:MCP9700Ax-HLT
|
||||||
|
|
@ -22479,6 +22519,7 @@ Transistor_FET_Other:Q_NMOS_Depletion_GDS
|
||||||
Transistor_FET_Other:Q_NMOS_Depletion_GSD
|
Transistor_FET_Other:Q_NMOS_Depletion_GSD
|
||||||
Transistor_FET_Other:Q_NMOS_Depletion_SDG
|
Transistor_FET_Other:Q_NMOS_Depletion_SDG
|
||||||
Transistor_FET_Other:Q_NMOS_Depletion_SGD
|
Transistor_FET_Other:Q_NMOS_Depletion_SGD
|
||||||
|
Transistor_FET_Other:SP010N70T8
|
||||||
Transistor_FET_Other:VNB35N07xx-E
|
Transistor_FET_Other:VNB35N07xx-E
|
||||||
Transistor_FET_Other:VNP10N07
|
Transistor_FET_Other:VNP10N07
|
||||||
Transistor_FET_Other:VNP35N07xx-E
|
Transistor_FET_Other:VNP35N07xx-E
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,8 @@ class AttachmentFileController extends AbstractController
|
||||||
//Set header content disposition, so that the file will be downloaded
|
//Set header content disposition, so that the file will be downloaded
|
||||||
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $attachment->getFilename());
|
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $attachment->getFilename());
|
||||||
|
|
||||||
|
$this->setAttachmentCSPHeaders($response);
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,6 +114,16 @@ class AttachmentFileController extends AbstractController
|
||||||
//Set header content disposition, so that the file will be downloaded
|
//Set header content disposition, so that the file will be downloaded
|
||||||
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE, $attachment->getFilename());
|
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE, $attachment->getFilename());
|
||||||
|
|
||||||
|
$this->setAttachmentCSPHeaders($response);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setAttachmentCSPHeaders(Response $response): Response
|
||||||
|
{
|
||||||
|
//Set an CSP that disallow to run any scripts, styles or images from the attachment render page, as it is not used anywhere else for now and can be a security risk if used without proper precautions, so it should be opt-in
|
||||||
|
$response->headers->set('Content-Security-Policy', "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;");
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use App\Entity\ProjectSystem\Project;
|
use App\Entity\ProjectSystem\Project;
|
||||||
use App\Entity\Parts\Category;
|
use App\Entity\Parts\Category;
|
||||||
|
|
@ -55,7 +56,7 @@ class TreeController extends AbstractController
|
||||||
|
|
||||||
#[Route(path: '/category/{id}', name: 'tree_category')]
|
#[Route(path: '/category/{id}', name: 'tree_category')]
|
||||||
#[Route(path: '/categories', name: 'tree_category_root')]
|
#[Route(path: '/categories', name: 'tree_category_root')]
|
||||||
public function categoryTree(?Category $category = null): JsonResponse
|
public function categoryTree(#[MapEntity(id: 'id')] ?Category $category = null): JsonResponse
|
||||||
{
|
{
|
||||||
if ($this->isGranted('@parts.read') && $this->isGranted('@categories.read')) {
|
if ($this->isGranted('@parts.read') && $this->isGranted('@categories.read')) {
|
||||||
$tree = $this->treeGenerator->getTreeView(Category::class, $category, 'list_parts_root');
|
$tree = $this->treeGenerator->getTreeView(Category::class, $category, 'list_parts_root');
|
||||||
|
|
@ -68,7 +69,7 @@ class TreeController extends AbstractController
|
||||||
|
|
||||||
#[Route(path: '/footprint/{id}', name: 'tree_footprint')]
|
#[Route(path: '/footprint/{id}', name: 'tree_footprint')]
|
||||||
#[Route(path: '/footprints', name: 'tree_footprint_root')]
|
#[Route(path: '/footprints', name: 'tree_footprint_root')]
|
||||||
public function footprintTree(?Footprint $footprint = null): JsonResponse
|
public function footprintTree(#[MapEntity(id: 'id')] ?Footprint $footprint = null): JsonResponse
|
||||||
{
|
{
|
||||||
if ($this->isGranted('@parts.read') && $this->isGranted('@footprints.read')) {
|
if ($this->isGranted('@parts.read') && $this->isGranted('@footprints.read')) {
|
||||||
$tree = $this->treeGenerator->getTreeView(Footprint::class, $footprint, 'list_parts_root');
|
$tree = $this->treeGenerator->getTreeView(Footprint::class, $footprint, 'list_parts_root');
|
||||||
|
|
@ -80,7 +81,7 @@ class TreeController extends AbstractController
|
||||||
|
|
||||||
#[Route(path: '/location/{id}', name: 'tree_location')]
|
#[Route(path: '/location/{id}', name: 'tree_location')]
|
||||||
#[Route(path: '/locations', name: 'tree_location_root')]
|
#[Route(path: '/locations', name: 'tree_location_root')]
|
||||||
public function locationTree(?StorageLocation $location = null): JsonResponse
|
public function locationTree(#[MapEntity(id: 'id')] ?StorageLocation $location = null): JsonResponse
|
||||||
{
|
{
|
||||||
if ($this->isGranted('@parts.read') && $this->isGranted('@storelocations.read')) {
|
if ($this->isGranted('@parts.read') && $this->isGranted('@storelocations.read')) {
|
||||||
$tree = $this->treeGenerator->getTreeView(StorageLocation::class, $location, 'list_parts_root');
|
$tree = $this->treeGenerator->getTreeView(StorageLocation::class, $location, 'list_parts_root');
|
||||||
|
|
@ -93,7 +94,7 @@ class TreeController extends AbstractController
|
||||||
|
|
||||||
#[Route(path: '/manufacturer/{id}', name: 'tree_manufacturer')]
|
#[Route(path: '/manufacturer/{id}', name: 'tree_manufacturer')]
|
||||||
#[Route(path: '/manufacturers', name: 'tree_manufacturer_root')]
|
#[Route(path: '/manufacturers', name: 'tree_manufacturer_root')]
|
||||||
public function manufacturerTree(?Manufacturer $manufacturer = null): JsonResponse
|
public function manufacturerTree(#[MapEntity(id: 'id')] ?Manufacturer $manufacturer = null): JsonResponse
|
||||||
{
|
{
|
||||||
if ($this->isGranted('@parts.read') && $this->isGranted('@manufacturers.read')) {
|
if ($this->isGranted('@parts.read') && $this->isGranted('@manufacturers.read')) {
|
||||||
$tree = $this->treeGenerator->getTreeView(Manufacturer::class, $manufacturer, 'list_parts_root');
|
$tree = $this->treeGenerator->getTreeView(Manufacturer::class, $manufacturer, 'list_parts_root');
|
||||||
|
|
@ -106,7 +107,7 @@ class TreeController extends AbstractController
|
||||||
|
|
||||||
#[Route(path: '/supplier/{id}', name: 'tree_supplier')]
|
#[Route(path: '/supplier/{id}', name: 'tree_supplier')]
|
||||||
#[Route(path: '/suppliers', name: 'tree_supplier_root')]
|
#[Route(path: '/suppliers', name: 'tree_supplier_root')]
|
||||||
public function supplierTree(?Supplier $supplier = null): JsonResponse
|
public function supplierTree(#[MapEntity(id: 'id')] ?Supplier $supplier = null): JsonResponse
|
||||||
{
|
{
|
||||||
if ($this->isGranted('@parts.read') && $this->isGranted('@suppliers.read')) {
|
if ($this->isGranted('@parts.read') && $this->isGranted('@suppliers.read')) {
|
||||||
$tree = $this->treeGenerator->getTreeView(Supplier::class, $supplier, 'list_parts_root');
|
$tree = $this->treeGenerator->getTreeView(Supplier::class, $supplier, 'list_parts_root');
|
||||||
|
|
@ -119,7 +120,7 @@ class TreeController extends AbstractController
|
||||||
|
|
||||||
#[Route(path: '/device/{id}', name: 'tree_device')]
|
#[Route(path: '/device/{id}', name: 'tree_device')]
|
||||||
#[Route(path: '/devices', name: 'tree_device_root')]
|
#[Route(path: '/devices', name: 'tree_device_root')]
|
||||||
public function deviceTree(?Project $device = null): JsonResponse
|
public function deviceTree(#[MapEntity(id: 'id')] ?Project $device = null): JsonResponse
|
||||||
{
|
{
|
||||||
if ($this->isGranted('@projects.read')) {
|
if ($this->isGranted('@projects.read')) {
|
||||||
$tree = $this->treeGenerator->getTreeView(Project::class, $device, 'devices');
|
$tree = $this->treeGenerator->getTreeView(Project::class, $device, 'devices');
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\DataTables;
|
namespace App\DataTables;
|
||||||
|
|
||||||
|
use App\DataTables\Column\HTMLColumn;
|
||||||
use App\DataTables\Column\LocaleDateTimeColumn;
|
use App\DataTables\Column\LocaleDateTimeColumn;
|
||||||
use App\DataTables\Column\PrettyBoolColumn;
|
use App\DataTables\Column\PrettyBoolColumn;
|
||||||
use App\DataTables\Column\RowClassColumn;
|
use App\DataTables\Column\RowClassColumn;
|
||||||
|
|
@ -40,14 +41,19 @@ use Omines\DataTablesBundle\DataTable;
|
||||||
use Omines\DataTablesBundle\DataTableTypeInterface;
|
use Omines\DataTablesBundle\DataTableTypeInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
final class AttachmentDataTable implements DataTableTypeInterface
|
final readonly class AttachmentDataTable implements DataTableTypeInterface
|
||||||
{
|
{
|
||||||
public function __construct(private readonly TranslatorInterface $translator, private readonly EntityURLGenerator $entityURLGenerator, private readonly AttachmentManager $attachmentHelper, private readonly AttachmentURLGenerator $attachmentURLGenerator, private readonly ElementTypeNameGenerator $elementTypeNameGenerator)
|
public function __construct(private TranslatorInterface $translator, private EntityURLGenerator $entityURLGenerator, private AttachmentManager $attachmentHelper, private AttachmentURLGenerator $attachmentURLGenerator, private ElementTypeNameGenerator $elementTypeNameGenerator)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configure(DataTable $dataTable, array $options): void
|
public function configure(DataTable $dataTable, array $options): void
|
||||||
{
|
{
|
||||||
|
/*************************************************************************************************************
|
||||||
|
* Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the
|
||||||
|
* HTMLColumn, if necessary
|
||||||
|
************************************************************************************************************/
|
||||||
|
|
||||||
$dataTable->add('dont_matter', RowClassColumn::class, [
|
$dataTable->add('dont_matter', RowClassColumn::class, [
|
||||||
'render' => function ($value, Attachment $context): string {
|
'render' => function ($value, Attachment $context): string {
|
||||||
//Mark attachments yellow which have an internal file linked that doesn't exist
|
//Mark attachments yellow which have an internal file linked that doesn't exist
|
||||||
|
|
@ -59,10 +65,10 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$dataTable->add('picture', TextColumn::class, [
|
$dataTable->add('picture', HTMLColumn::class, [
|
||||||
'label' => '',
|
'label' => '',
|
||||||
'className' => 'no-colvis',
|
'className' => 'no-colvis',
|
||||||
'render' => function ($value, Attachment $context): string {
|
'data' => function (Attachment $context): string {
|
||||||
if ($context->isPicture()
|
if ($context->isPicture()
|
||||||
&& $this->attachmentHelper->isInternalFileExisting($context)) {
|
&& $this->attachmentHelper->isInternalFileExisting($context)) {
|
||||||
|
|
||||||
|
|
@ -95,65 +101,65 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
||||||
'orderField' => 'NATSORT(attachment.name)',
|
'orderField' => 'NATSORT(attachment.name)',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$dataTable->add('attachment_type', TextColumn::class, [
|
$dataTable->add('attachment_type', HTMLColumn::class, [
|
||||||
'label' => 'attachment.table.type',
|
'label' => 'attachment.table.type',
|
||||||
'field' => 'attachment_type.name',
|
'field' => 'attachment_type.name',
|
||||||
'orderField' => 'NATSORT(attachment_type.name)',
|
'orderField' => 'NATSORT(attachment_type.name)',
|
||||||
'render' => fn($value, Attachment $context): string => sprintf(
|
'data' => fn(Attachment $context, $value): string => sprintf(
|
||||||
'<a href="%s">%s</a>',
|
'<a href="%s">%s</a>',
|
||||||
$this->entityURLGenerator->editURL($context->getAttachmentType()),
|
$this->entityURLGenerator->editURL($context->getAttachmentType()),
|
||||||
htmlspecialchars((string) $value)
|
htmlspecialchars((string) $value)
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$dataTable->add('element', TextColumn::class, [
|
$dataTable->add('element', HTMLColumn::class, [
|
||||||
'label' => 'attachment.table.element',
|
'label' => 'attachment.table.element',
|
||||||
//'propertyPath' => 'element.name',
|
//'propertyPath' => 'element.name',
|
||||||
'render' => fn($value, Attachment $context): string => sprintf(
|
'data' => fn(Attachment $context): string => sprintf(
|
||||||
'<a href="%s">%s</a>',
|
'<a href="%s">%s</a>',
|
||||||
$this->entityURLGenerator->infoURL($context->getElement()),
|
$this->entityURLGenerator->infoURL($context->getElement()),
|
||||||
$this->elementTypeNameGenerator->getTypeNameCombination($context->getElement(), true)
|
$this->elementTypeNameGenerator->getTypeNameCombination($context->getElement(), true)
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$dataTable->add('internal_link', TextColumn::class, [
|
$dataTable->add('internal_link', HTMLColumn::class, [
|
||||||
'label' => 'attachment.table.internal_file',
|
'label' => 'attachment.table.internal_file',
|
||||||
'propertyPath' => 'filename',
|
'propertyPath' => 'filename',
|
||||||
'orderField' => 'NATSORT(attachment.original_filename)',
|
'orderField' => 'NATSORT(attachment.original_filename)',
|
||||||
'render' => function ($value, Attachment $context) {
|
'data' => function (Attachment $context, $value) {
|
||||||
if ($this->attachmentHelper->isInternalFileExisting($context)) {
|
if ($this->attachmentHelper->isInternalFileExisting($context)) {
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'<a href="%s" target="_blank" data-no-ajax>%s</a>',
|
'<a href="%s" target="_blank" data-no-ajax>%s</a>',
|
||||||
$this->entityURLGenerator->viewURL($context),
|
$this->entityURLGenerator->viewURL($context),
|
||||||
htmlspecialchars($value)
|
htmlspecialchars((string) $value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $value;
|
return htmlspecialchars((string) $value);
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$dataTable->add('external_link', TextColumn::class, [
|
$dataTable->add('external_link', HTMLColumn::class, [
|
||||||
'label' => 'attachment.table.external_link',
|
'label' => 'attachment.table.external_link',
|
||||||
'propertyPath' => 'host',
|
'propertyPath' => 'host',
|
||||||
'orderField' => 'attachment.external_path',
|
'orderField' => 'attachment.external_path',
|
||||||
'render' => function ($value, Attachment $context) {
|
'data' => function (Attachment $context, $value) {
|
||||||
if ($context->hasExternal()) {
|
if ($context->hasExternal()) {
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'<a href="%s" class="link-external" title="%s" target="_blank" rel="noopener">%s</a>',
|
'<a href="%s" class="link-external" title="%s" target="_blank" rel="noopener">%s</a>',
|
||||||
htmlspecialchars((string) $context->getExternalPath()),
|
htmlspecialchars((string) $context->getExternalPath()),
|
||||||
htmlspecialchars((string) $context->getExternalPath()),
|
htmlspecialchars((string) $context->getExternalPath()),
|
||||||
htmlspecialchars($value),
|
htmlspecialchars((string) $value),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $value;
|
return htmlspecialchars((string) $value);
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$dataTable->add('filesize', TextColumn::class, [
|
$dataTable->add('filesize', HTMLColumn::class, [
|
||||||
'label' => $this->translator->trans('attachment.table.filesize'),
|
'label' => $this->translator->trans('attachment.table.filesize'),
|
||||||
'render' => function ($value, Attachment $context) {
|
'data' => function (Attachment $context) {
|
||||||
if (!$context->hasInternal()) {
|
if (!$context->hasInternal()) {
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'<span class="badge bg-primary">
|
'<span class="badge bg-primary">
|
||||||
|
|
@ -168,7 +174,7 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
||||||
'<span class="badge bg-secondary">
|
'<span class="badge bg-secondary">
|
||||||
<i class="fas fa-hdd fa-fw"></i> %s
|
<i class="fas fa-hdd fa-fw"></i> %s
|
||||||
</span>',
|
</span>',
|
||||||
$this->attachmentHelper->getHumanFileSize($context)
|
htmlspecialchars($this->attachmentHelper->getHumanFileSize($context))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ class EntityColumn extends AbstractColumn
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sprintf('<i>%s</i>', $value);
|
return sprintf('<i>%s</i>', htmlspecialchars($value));
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
* 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)
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
|
@ -16,31 +20,18 @@
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
* 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/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
namespace App\DataTables\Column;
|
||||||
|
|
||||||
.modal-body > .bootbox-close-button {
|
use Omines\DataTablesBundle\Column\TextColumn;
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.modal .bootbox-close-button {
|
|
||||||
font-weight: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.bootbox-close-button {
|
/**
|
||||||
padding: 0;
|
* A TextColumn whose value is always treated as raw HTML and therefore never passed through htmlspecialchars().
|
||||||
background-color: transparent;
|
* The value returned by the 'data' option must already contain properly escaped/sanitized HTML, as it is output as-is.
|
||||||
border: 0;
|
*/
|
||||||
-webkit-appearance: none;
|
class HTMLColumn extends TextColumn
|
||||||
}
|
{
|
||||||
|
public function isRaw(): bool
|
||||||
.bootbox-close-button {
|
{
|
||||||
/* float: right; */
|
return true;
|
||||||
font-size: 1.40625rem;
|
}
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1;
|
|
||||||
color: #000;
|
|
||||||
text-shadow: none;
|
|
||||||
opacity: .5;
|
|
||||||
}
|
}
|
||||||
|
|
@ -87,9 +87,9 @@ class IconLinkColumn extends AbstractColumn
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'<a class="btn btn-primary btn-sm %s" href="%s" title="%s"><i class="%s"></i></a>',
|
'<a class="btn btn-primary btn-sm %s" href="%s" title="%s"><i class="%s"></i></a>',
|
||||||
$disabled ? 'disabled' : '',
|
$disabled ? 'disabled' : '',
|
||||||
$href,
|
htmlspecialchars($href),
|
||||||
$title,
|
htmlspecialchars($title ?? ''),
|
||||||
$icon
|
htmlspecialchars($icon ?? '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@ declare(strict_types=1);
|
||||||
*/
|
*/
|
||||||
namespace App\DataTables;
|
namespace App\DataTables;
|
||||||
|
|
||||||
|
use App\DataTables\Column\HTMLColumn;
|
||||||
use App\DataTables\Column\RowClassColumn;
|
use App\DataTables\Column\RowClassColumn;
|
||||||
use Omines\DataTablesBundle\Adapter\ArrayAdapter;
|
use Omines\DataTablesBundle\Adapter\ArrayAdapter;
|
||||||
use Omines\DataTablesBundle\Column\TextColumn;
|
|
||||||
use Omines\DataTablesBundle\DataTable;
|
use Omines\DataTablesBundle\DataTable;
|
||||||
use Omines\DataTablesBundle\DataTableFactory;
|
use Omines\DataTablesBundle\DataTableFactory;
|
||||||
use Omines\DataTablesBundle\DataTableTypeInterface;
|
use Omines\DataTablesBundle\DataTableTypeInterface;
|
||||||
|
|
@ -32,7 +32,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
class ErrorDataTable implements DataTableTypeInterface
|
final readonly class ErrorDataTable implements DataTableTypeInterface
|
||||||
{
|
{
|
||||||
public function configureOptions(OptionsResolver $optionsResolver): void
|
public function configureOptions(OptionsResolver $optionsResolver): void
|
||||||
{
|
{
|
||||||
|
|
@ -49,6 +49,11 @@ class ErrorDataTable implements DataTableTypeInterface
|
||||||
|
|
||||||
public function configure(DataTable $dataTable, array $options): void
|
public function configure(DataTable $dataTable, array $options): void
|
||||||
{
|
{
|
||||||
|
/*************************************************************************************************************
|
||||||
|
* Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the
|
||||||
|
* HTMLColumn, if necessary
|
||||||
|
************************************************************************************************************/
|
||||||
|
|
||||||
$optionsResolver = new OptionsResolver();
|
$optionsResolver = new OptionsResolver();
|
||||||
$this->configureOptions($optionsResolver);
|
$this->configureOptions($optionsResolver);
|
||||||
$options = $optionsResolver->resolve($options);
|
$options = $optionsResolver->resolve($options);
|
||||||
|
|
@ -58,9 +63,9 @@ class ErrorDataTable implements DataTableTypeInterface
|
||||||
'render' => fn($value, $context): string => 'table-warning',
|
'render' => fn($value, $context): string => 'table-warning',
|
||||||
])
|
])
|
||||||
|
|
||||||
->add('error', TextColumn::class, [
|
->add('error', HTMLColumn::class, [
|
||||||
'label' => 'error_table.error',
|
'label' => 'error_table.error',
|
||||||
'render' => fn($value, $context): string => '<i class="fa-solid fa-triangle-exclamation fa-fw"></i> ' . $value,
|
'data' => fn($context, $value): string => '<i class="fa-solid fa-triangle-exclamation fa-fw"></i> ' . htmlspecialchars((string) $value),
|
||||||
])
|
])
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ class PartDataTableHelper
|
||||||
}
|
}
|
||||||
if ($context->getBuiltProject() instanceof Project) {
|
if ($context->getBuiltProject() instanceof Project) {
|
||||||
$icon = sprintf('<i class="fa-solid fa-box-archive fa-fw me-1" title="%s"></i>',
|
$icon = sprintf('<i class="fa-solid fa-box-archive fa-fw me-1" title="%s"></i>',
|
||||||
$this->translator->trans('part.info.projectBuildPart.hint').': '.$context->getBuiltProject()->getName());
|
$this->translator->trans('part.info.projectBuildPart.hint').': '.htmlspecialchars($context->getBuiltProject()->getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ namespace App\DataTables;
|
||||||
use App\DataTables\Column\EnumColumn;
|
use App\DataTables\Column\EnumColumn;
|
||||||
use App\Entity\LogSystem\LogTargetType;
|
use App\Entity\LogSystem\LogTargetType;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use App\DataTables\Column\HTMLColumn;
|
||||||
use App\DataTables\Column\IconLinkColumn;
|
use App\DataTables\Column\IconLinkColumn;
|
||||||
use App\DataTables\Column\LocaleDateTimeColumn;
|
use App\DataTables\Column\LocaleDateTimeColumn;
|
||||||
use App\DataTables\Column\LogEntryExtraColumn;
|
use App\DataTables\Column\LogEntryExtraColumn;
|
||||||
|
|
@ -59,7 +60,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
class LogDataTable implements DataTableTypeInterface
|
final readonly class LogDataTable implements DataTableTypeInterface
|
||||||
{
|
{
|
||||||
protected LogEntryRepository $logRepo;
|
protected LogEntryRepository $logRepo;
|
||||||
|
|
||||||
|
|
@ -95,6 +96,11 @@ class LogDataTable implements DataTableTypeInterface
|
||||||
|
|
||||||
public function configure(DataTable $dataTable, array $options): void
|
public function configure(DataTable $dataTable, array $options): void
|
||||||
{
|
{
|
||||||
|
/*************************************************************************************************************
|
||||||
|
* Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the
|
||||||
|
* HTMLColumn, if necessary
|
||||||
|
************************************************************************************************************/
|
||||||
|
|
||||||
$resolver = new OptionsResolver();
|
$resolver = new OptionsResolver();
|
||||||
$this->configureOptions($resolver);
|
$this->configureOptions($resolver);
|
||||||
$options = $resolver->resolve($options);
|
$options = $resolver->resolve($options);
|
||||||
|
|
@ -104,10 +110,10 @@ class LogDataTable implements DataTableTypeInterface
|
||||||
'render' => fn($value, AbstractLogEntry $context) => $this->logLevelHelper->logLevelToTableColorClass($context->getLevelString()),
|
'render' => fn($value, AbstractLogEntry $context) => $this->logLevelHelper->logLevelToTableColorClass($context->getLevelString()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$dataTable->add('symbol', TextColumn::class, [
|
$dataTable->add('symbol', HTMLColumn::class, [
|
||||||
'label' => '',
|
'label' => '',
|
||||||
'className' => 'no-colvis',
|
'className' => 'no-colvis',
|
||||||
'render' => fn($value, AbstractLogEntry $context): string => sprintf(
|
'data' => fn(AbstractLogEntry $context): string => sprintf(
|
||||||
'<i class="fas fa-fw %s" title="%s"></i>',
|
'<i class="fas fa-fw %s" title="%s"></i>',
|
||||||
$this->logLevelHelper->logLevelToIconClass($context->getLevelString()),
|
$this->logLevelHelper->logLevelToIconClass($context->getLevelString()),
|
||||||
$context->getLevelString()
|
$context->getLevelString()
|
||||||
|
|
@ -128,10 +134,10 @@ class LogDataTable implements DataTableTypeInterface
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$dataTable->add('type', TextColumn::class, [
|
$dataTable->add('type', HTMLColumn::class, [
|
||||||
'label' => 'log.type',
|
'label' => 'log.type',
|
||||||
'propertyPath' => 'type',
|
'propertyPath' => 'type',
|
||||||
'render' => function (string $value, AbstractLogEntry $context) {
|
'data' => function (AbstractLogEntry $context, string $value) {
|
||||||
$text = $this->translator->trans('log.type.'.$value);
|
$text = $this->translator->trans('log.type.'.$value);
|
||||||
|
|
||||||
if ($context instanceof PartStockChangedLogEntry) {
|
if ($context instanceof PartStockChangedLogEntry) {
|
||||||
|
|
@ -149,20 +155,20 @@ class LogDataTable implements DataTableTypeInterface
|
||||||
'label' => 'log.level',
|
'label' => 'log.level',
|
||||||
'visible' => 'system_log' === $options['mode'],
|
'visible' => 'system_log' === $options['mode'],
|
||||||
'propertyPath' => 'levelString',
|
'propertyPath' => 'levelString',
|
||||||
'render' => fn(string $value, AbstractLogEntry $context) => $this->translator->trans('log.level.'.$value),
|
'data' => fn(AbstractLogEntry $context, string $value) => $this->translator->trans('log.level.'.$value),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$dataTable->add('user', TextColumn::class, [
|
$dataTable->add('user', HTMLColumn::class, [
|
||||||
'label' => 'log.user',
|
'label' => 'log.user',
|
||||||
'orderField' => 'NATSORT(user.name)',
|
'orderField' => 'NATSORT(user.name)',
|
||||||
'render' => function ($value, AbstractLogEntry $context): string {
|
'data' => function (AbstractLogEntry $context): string {
|
||||||
$user = $context->getUser();
|
$user = $context->getUser();
|
||||||
|
|
||||||
//If user was deleted, show the info from the username field
|
//If user was deleted, show the info from the username field
|
||||||
if (!$user instanceof User) {
|
if (!$user instanceof User) {
|
||||||
if ($context->isCLIEntry()) {
|
if ($context->isCLIEntry()) {
|
||||||
return sprintf('%s [%s]',
|
return sprintf('%s [%s]',
|
||||||
htmlentities((string) $context->getCLIUsername()),
|
htmlspecialchars((string) $context->getCLIUsername()),
|
||||||
$this->translator->trans('log.cli_user')
|
$this->translator->trans('log.cli_user')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -170,7 +176,7 @@ class LogDataTable implements DataTableTypeInterface
|
||||||
//Else we just deal with a deleted user
|
//Else we just deal with a deleted user
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'@%s [%s]',
|
'@%s [%s]',
|
||||||
htmlentities($context->getUsername()),
|
htmlspecialchars($context->getUsername()),
|
||||||
$this->translator->trans('log.target_deleted'),
|
$this->translator->trans('log.target_deleted'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +188,7 @@ class LogDataTable implements DataTableTypeInterface
|
||||||
$img_url,
|
$img_url,
|
||||||
$this->userAvatarHelper->getAvatarMdURL($user),
|
$this->userAvatarHelper->getAvatarMdURL($user),
|
||||||
$this->urlGenerator->generate('user_info', ['id' => $user->getID()]),
|
$this->urlGenerator->generate('user_info', ['id' => $user->getID()]),
|
||||||
htmlentities($user->getFullName(true))
|
htmlspecialchars($user->getFullName(true))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
@ -194,7 +200,7 @@ class LogDataTable implements DataTableTypeInterface
|
||||||
'render' => function (LogTargetType $value, AbstractLogEntry $context) {
|
'render' => function (LogTargetType $value, AbstractLogEntry $context) {
|
||||||
$class = $value->toClass();
|
$class = $value->toClass();
|
||||||
if (null !== $class) {
|
if (null !== $class) {
|
||||||
return $this->elementTypeNameGenerator->getLocalizedTypeLabel($class);
|
return $this->elementTypeNameGenerator->typeLabel($class);
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
|
|
@ -216,9 +222,9 @@ class LogDataTable implements DataTableTypeInterface
|
||||||
'icon' => 'fas fa-fw fa-eye',
|
'icon' => 'fas fa-fw fa-eye',
|
||||||
'href' => function ($value, AbstractLogEntry $context) {
|
'href' => function ($value, AbstractLogEntry $context) {
|
||||||
if (
|
if (
|
||||||
|
$context instanceof CollectionElementDeleted ||
|
||||||
($context instanceof TimeTravelInterface
|
($context instanceof TimeTravelInterface
|
||||||
&& $context->hasOldDataInformation())
|
&& $context->hasOldDataInformation())
|
||||||
|| $context instanceof CollectionElementDeleted
|
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
$target = $this->logRepo->getTargetElement($context);
|
$target = $this->logRepo->getTargetElement($context);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ namespace App\DataTables;
|
||||||
use App\DataTables\Adapters\TwoStepORMAdapter;
|
use App\DataTables\Adapters\TwoStepORMAdapter;
|
||||||
use App\DataTables\Column\EntityColumn;
|
use App\DataTables\Column\EntityColumn;
|
||||||
use App\DataTables\Column\EnumColumn;
|
use App\DataTables\Column\EnumColumn;
|
||||||
|
use App\DataTables\Column\HTMLColumn;
|
||||||
use App\DataTables\Column\IconLinkColumn;
|
use App\DataTables\Column\IconLinkColumn;
|
||||||
use App\DataTables\Column\LocaleDateTimeColumn;
|
use App\DataTables\Column\LocaleDateTimeColumn;
|
||||||
use App\DataTables\Column\MarkdownColumn;
|
use App\DataTables\Column\MarkdownColumn;
|
||||||
|
|
@ -58,7 +59,7 @@ use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
final class PartsDataTable implements DataTableTypeInterface
|
final readonly class PartsDataTable implements DataTableTypeInterface
|
||||||
{
|
{
|
||||||
public const LENGTH_MENU = [[10, 25, 50, 100, 250, 500, -1], [10, 25, 50, 100, 250, 500, "All"]];
|
public const LENGTH_MENU = [[10, 25, 50, 100, 250, 500, -1], [10, 25, 50, 100, 250, 500, "All"]];
|
||||||
|
|
||||||
|
|
@ -94,6 +95,11 @@ final class PartsDataTable implements DataTableTypeInterface
|
||||||
* When adding columns here, add them also to PartTableColumns enum, to make them configurable in the settings!
|
* When adding columns here, add them also to PartTableColumns enum, to make them configurable in the settings!
|
||||||
*************************************************************************************************************/
|
*************************************************************************************************************/
|
||||||
|
|
||||||
|
/*************************************************************************************************************
|
||||||
|
* Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the
|
||||||
|
* HTMLColumn, if necessary
|
||||||
|
************************************************************************************************************/
|
||||||
|
|
||||||
$this->csh
|
$this->csh
|
||||||
//Color the table rows depending on the review and favorite status
|
//Color the table rows depending on the review and favorite status
|
||||||
->add('row_color', RowClassColumn::class, [
|
->add('row_color', RowClassColumn::class, [
|
||||||
|
|
@ -109,23 +115,23 @@ final class PartsDataTable implements DataTableTypeInterface
|
||||||
},
|
},
|
||||||
], visibility_configurable: false)
|
], visibility_configurable: false)
|
||||||
->add('select', SelectColumn::class, visibility_configurable: false)
|
->add('select', SelectColumn::class, visibility_configurable: false)
|
||||||
->add('picture', TextColumn::class, [
|
->add('picture', HTMLColumn::class, [
|
||||||
'label' => '',
|
'label' => '',
|
||||||
'className' => 'no-colvis',
|
'className' => 'no-colvis',
|
||||||
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderPicture($context),
|
'data' => fn(Part $context) => $this->partDataTableHelper->renderPicture($context),
|
||||||
], visibility_configurable: false)
|
], visibility_configurable: false)
|
||||||
->add('name', TextColumn::class, [
|
->add('name', HTMLColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.name'),
|
'label' => $this->translator->trans('part.table.name'),
|
||||||
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderName($context),
|
'data' => fn(Part $context) => $this->partDataTableHelper->renderName($context),
|
||||||
'orderField' => 'NATSORT(part.name)'
|
'orderField' => 'NATSORT(part.name)'
|
||||||
])
|
])
|
||||||
->add('si_value', TextColumn::class, [
|
->add('si_value', TextColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.si_value'),
|
'label' => $this->translator->trans('part.table.si_value'),
|
||||||
'render' => function ($value, Part $context): string {
|
'data' => function (Part $context): string {
|
||||||
$siValue = SiValueSort::sqliteSiValue($context->getName());
|
$siValue = SiValueSort::sqliteSiValue($context->getName());
|
||||||
if ($siValue !== null) {
|
if ($siValue !== null) {
|
||||||
//Output it as scientific number with a big E
|
//Output it as scientific number with a big E
|
||||||
return htmlspecialchars(sprintf('%G', $siValue));
|
return sprintf('%G', $siValue);
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
|
@ -156,38 +162,38 @@ final class PartsDataTable implements DataTableTypeInterface
|
||||||
'label' => $this->translator->trans('part.table.manufacturer'),
|
'label' => $this->translator->trans('part.table.manufacturer'),
|
||||||
'orderField' => 'NATSORT(_manufacturer.name)'
|
'orderField' => 'NATSORT(_manufacturer.name)'
|
||||||
])
|
])
|
||||||
->add('storelocation', TextColumn::class, [
|
->add('storelocation', HTMLColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.storeLocations'),
|
'label' => $this->translator->trans('part.table.storeLocations'),
|
||||||
//We need to use a aggregate function to get the first store location, as we have a one-to-many relation
|
//We need to use a aggregate function to get the first store location, as we have a one-to-many relation
|
||||||
'orderField' => 'NATSORT(MIN(_storelocations.name))',
|
'orderField' => 'NATSORT(MIN(_storelocations.name))',
|
||||||
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderStorageLocations($context),
|
'data' => fn(Part $context) => $this->partDataTableHelper->renderStorageLocations($context),
|
||||||
], alias: 'storage_location')
|
], alias: 'storage_location')
|
||||||
|
|
||||||
->add('amount', TextColumn::class, [
|
->add('amount', HTMLColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.amount'),
|
'label' => $this->translator->trans('part.table.amount'),
|
||||||
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderAmount($context),
|
'data' => fn(Part $context) => $this->partDataTableHelper->renderAmount($context),
|
||||||
'orderField' => 'amountSum'
|
'orderField' => 'amountSum'
|
||||||
])
|
])
|
||||||
->add('minamount', TextColumn::class, [
|
->add('minamount', TextColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.minamount'),
|
'label' => $this->translator->trans('part.table.minamount'),
|
||||||
'render' => fn($value, Part $context): string => htmlspecialchars($this->amountFormatter->format(
|
'data' => fn(Part $context, $value): string => $this->amountFormatter->format(
|
||||||
$value,
|
$value,
|
||||||
$context->getPartUnit()
|
$context->getPartUnit()
|
||||||
)),
|
),
|
||||||
])
|
])
|
||||||
->add('partUnit', TextColumn::class, [
|
->add('partUnit', TextColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.partUnit'),
|
'label' => $this->translator->trans('part.table.partUnit'),
|
||||||
'orderField' => 'NATSORT(_partUnit.name)',
|
'orderField' => 'NATSORT(_partUnit.name)',
|
||||||
'render' => function ($value, Part $context): string {
|
'data' => function (Part $context): string {
|
||||||
$partUnit = $context->getPartUnit();
|
$partUnit = $context->getPartUnit();
|
||||||
if ($partUnit === null) {
|
if ($partUnit === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$tmp = htmlspecialchars($partUnit->getName());
|
$tmp = $partUnit->getName();
|
||||||
|
|
||||||
if ($partUnit->getUnit()) {
|
if ($partUnit->getUnit()) {
|
||||||
$tmp .= ' (' . htmlspecialchars($partUnit->getUnit()) . ')';
|
$tmp .= ' (' . $partUnit->getUnit() . ')';
|
||||||
}
|
}
|
||||||
return $tmp;
|
return $tmp;
|
||||||
}
|
}
|
||||||
|
|
@ -195,14 +201,14 @@ final class PartsDataTable implements DataTableTypeInterface
|
||||||
->add('partCustomState', TextColumn::class, [
|
->add('partCustomState', TextColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.partCustomState'),
|
'label' => $this->translator->trans('part.table.partCustomState'),
|
||||||
'orderField' => 'NATSORT(_partCustomState.name)',
|
'orderField' => 'NATSORT(_partCustomState.name)',
|
||||||
'render' => function($value, Part $context): string {
|
'data' => function(Part $context): string {
|
||||||
$partCustomState = $context->getPartCustomState();
|
$partCustomState = $context->getPartCustomState();
|
||||||
|
|
||||||
if ($partCustomState === null) {
|
if ($partCustomState === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return htmlspecialchars($partCustomState->getName());
|
return $partCustomState->getName();
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
->add('addedDate', LocaleDateTimeColumn::class, [
|
->add('addedDate', LocaleDateTimeColumn::class, [
|
||||||
|
|
@ -248,25 +254,25 @@ final class PartsDataTable implements DataTableTypeInterface
|
||||||
])
|
])
|
||||||
->add('eda_reference', TextColumn::class, [
|
->add('eda_reference', TextColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.eda_reference'),
|
'label' => $this->translator->trans('part.table.eda_reference'),
|
||||||
'render' => static fn($value, Part $context) => htmlspecialchars($context->getEdaInfo()->getReferencePrefix() ?? ''),
|
'data' => static fn(Part $context) => $context->getEdaInfo()->getReferencePrefix() ?? '',
|
||||||
'orderField' => 'NATSORT(part.eda_info.reference_prefix)'
|
'orderField' => 'NATSORT(part.eda_info.reference_prefix)'
|
||||||
])
|
])
|
||||||
->add('eda_value', TextColumn::class, [
|
->add('eda_value', TextColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.eda_value'),
|
'label' => $this->translator->trans('part.table.eda_value'),
|
||||||
'render' => static fn($value, Part $context) => htmlspecialchars($context->getEdaInfo()->getValue() ?? ''),
|
'data' => static fn(Part $context) => $context->getEdaInfo()->getValue() ?? '',
|
||||||
'orderField' => 'NATSORT(part.eda_info.value)'
|
'orderField' => 'NATSORT(part.eda_info.value)'
|
||||||
])
|
])
|
||||||
->add('eda_status', TextColumn::class, [
|
->add('eda_status', HTMLColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.eda_status'),
|
'label' => $this->translator->trans('part.table.eda_status'),
|
||||||
'render' => fn($value, Part $context) => $this->partDataTableHelper->renderEdaStatus($context),
|
'data' => fn(Part $context) => $this->partDataTableHelper->renderEdaStatus($context),
|
||||||
'className' => 'text-center',
|
'className' => 'text-center',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
//Add a column to list the projects where the part is used, when the user has the permission to see the projects
|
//Add a column to list the projects where the part is used, when the user has the permission to see the projects
|
||||||
if ($this->security->isGranted('read', Project::class)) {
|
if ($this->security->isGranted('read', Project::class)) {
|
||||||
$this->csh->add('projects', TextColumn::class, [
|
$this->csh->add('projects', HTMLColumn::class, [
|
||||||
'label' => $this->translator->trans('project.labelp'),
|
'label' => $this->translator->trans('project.labelp'),
|
||||||
'render' => function ($value, Part $context): string {
|
'data' => function (Part $context): string {
|
||||||
//Only show the first 5 projects names
|
//Only show the first 5 projects names
|
||||||
$projects = $context->getProjects();
|
$projects = $context->getProjects();
|
||||||
$tmp = "";
|
$tmp = "";
|
||||||
|
|
@ -286,7 +292,7 @@ final class PartsDataTable implements DataTableTypeInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
return $tmp;
|
return $tmp;
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ namespace App\DataTables;
|
||||||
use App\DataTables\Adapters\TwoStepORMAdapter;
|
use App\DataTables\Adapters\TwoStepORMAdapter;
|
||||||
use App\DataTables\Column\EntityColumn;
|
use App\DataTables\Column\EntityColumn;
|
||||||
use App\DataTables\Column\EnumColumn;
|
use App\DataTables\Column\EnumColumn;
|
||||||
|
use App\DataTables\Column\HTMLColumn;
|
||||||
use App\DataTables\Column\LocaleDateTimeColumn;
|
use App\DataTables\Column\LocaleDateTimeColumn;
|
||||||
use App\DataTables\Column\MarkdownColumn;
|
use App\DataTables\Column\MarkdownColumn;
|
||||||
use App\DataTables\Helpers\PartDataTableHelper;
|
use App\DataTables\Helpers\PartDataTableHelper;
|
||||||
|
|
@ -48,7 +49,7 @@ use Omines\DataTablesBundle\DataTable;
|
||||||
use Omines\DataTablesBundle\DataTableTypeInterface;
|
use Omines\DataTablesBundle\DataTableTypeInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
final readonly class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected EntityURLGenerator $entityURLGenerator,
|
protected EntityURLGenerator $entityURLGenerator,
|
||||||
|
|
@ -63,17 +64,22 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||||
|
|
||||||
public function configure(DataTable $dataTable, array $options): void
|
public function configure(DataTable $dataTable, array $options): void
|
||||||
{
|
{
|
||||||
|
/*************************************************************************************************************
|
||||||
|
* Avoid using render, as it has no escaping, and is a potential security risk. Use data on TextColumn or the
|
||||||
|
* HTMLColumn, if necessary
|
||||||
|
************************************************************************************************************/
|
||||||
|
|
||||||
$dataTable
|
$dataTable
|
||||||
//->add('select', SelectColumn::class)
|
//->add('select', SelectColumn::class)
|
||||||
->add('picture', TextColumn::class, [
|
->add('picture', HTMLColumn::class, [
|
||||||
'label' => '',
|
'label' => '',
|
||||||
'className' => 'no-colvis',
|
'className' => 'no-colvis',
|
||||||
'render' => function ($value, ProjectBOMEntry $context) {
|
'data' => function (ProjectBOMEntry $context) {
|
||||||
if(!$context->getPart() instanceof Part) {
|
if(!$context->getPart() instanceof Part) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return $this->partDataTableHelper->renderPicture($context->getPart());
|
return $this->partDataTableHelper->renderPicture($context->getPart());
|
||||||
}
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
->add('id', TextColumn::class, [
|
->add('id', TextColumn::class, [
|
||||||
|
|
@ -85,27 +91,27 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||||
'label' => $this->translator->trans('project.bom.quantity'),
|
'label' => $this->translator->trans('project.bom.quantity'),
|
||||||
'className' => 'text-center',
|
'className' => 'text-center',
|
||||||
'orderField' => 'bom_entry.quantity',
|
'orderField' => 'bom_entry.quantity',
|
||||||
'render' => function ($value, ProjectBOMEntry $context): float|string {
|
'data' => function (ProjectBOMEntry $context): float|string {
|
||||||
//If we have a non-part entry, only show the rounded quantity
|
//If we have a non-part entry, only show the rounded quantity
|
||||||
if (!$context->getPart() instanceof Part) {
|
if (!$context->getPart() instanceof Part) {
|
||||||
return round($context->getQuantity());
|
return round($context->getQuantity());
|
||||||
}
|
}
|
||||||
//Otherwise use the unit of the part to format the quantity
|
//Otherwise use the unit of the part to format the quantity
|
||||||
return htmlspecialchars($this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit()));
|
return $this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit());
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
->add('partId', TextColumn::class, [
|
->add('partId', TextColumn::class, [
|
||||||
'label' => $this->translator->trans('project.bom.part_id'),
|
'label' => $this->translator->trans('project.bom.part_id'),
|
||||||
'visible' => true,
|
'visible' => true,
|
||||||
'orderField' => 'part.id',
|
'orderField' => 'part.id',
|
||||||
'render' => function ($value, ProjectBOMEntry $context) {
|
'data' => function (ProjectBOMEntry $context) {
|
||||||
return $context->getPart() instanceof Part ? (string) $context->getPart()->getId() : '';
|
return $context->getPart() instanceof Part ? (string) $context->getPart()->getId() : '';
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
->add('name', TextColumn::class, [
|
->add('name', HTMLColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.name'),
|
'label' => $this->translator->trans('part.table.name'),
|
||||||
'orderField' => 'NATSORT(part.name)',
|
'orderField' => 'NATSORT(part.name)',
|
||||||
'render' => function ($value, ProjectBOMEntry $context) {
|
'data' => function (ProjectBOMEntry $context) {
|
||||||
if(!$context->getPart() instanceof Part) {
|
if(!$context->getPart() instanceof Part) {
|
||||||
return htmlspecialchars((string) $context->getName());
|
return htmlspecialchars((string) $context->getName());
|
||||||
}
|
}
|
||||||
|
|
@ -123,11 +129,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||||
'label' => $this->translator->trans('part.table.ipn'),
|
'label' => $this->translator->trans('part.table.ipn'),
|
||||||
'orderField' => 'NATSORT(part.ipn)',
|
'orderField' => 'NATSORT(part.ipn)',
|
||||||
'visible' => false,
|
'visible' => false,
|
||||||
'render' => function ($value, ProjectBOMEntry $context) {
|
'data' => fn (ProjectBOMEntry $context) => $context->getPart()?->getIpn()
|
||||||
if($context->getPart() instanceof Part) {
|
|
||||||
return $context->getPart()->getIpn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
])
|
||||||
->add('description', MarkdownColumn::class, [
|
->add('description', MarkdownColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.description'),
|
'label' => $this->translator->trans('part.table.description'),
|
||||||
|
|
@ -172,9 +174,9 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
->add('mountnames', TextColumn::class, [
|
->add('mountnames', HTMLColumn::class, [
|
||||||
'label' => 'project.bom.mountnames',
|
'label' => 'project.bom.mountnames',
|
||||||
'render' => function ($value, ProjectBOMEntry $context) {
|
'data' => function (ProjectBOMEntry $context) {
|
||||||
$html = '';
|
$html = '';
|
||||||
|
|
||||||
foreach (explode(',', $context->getMountnames()) as $mountname) {
|
foreach (explode(',', $context->getMountnames()) as $mountname) {
|
||||||
|
|
@ -184,34 +186,34 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
->add('instockAmount', TextColumn::class, [
|
->add('instockAmount', HTMLColumn::class, [
|
||||||
'label' => 'project.bom.instockAmount',
|
'label' => 'project.bom.instockAmount',
|
||||||
'visible' => false,
|
'visible' => false,
|
||||||
'render' => function ($value, ProjectBOMEntry $context) {
|
'data' => function (ProjectBOMEntry $context) {
|
||||||
if ($context->getPart() !== null) {
|
if ($context->getPart() !== null) {
|
||||||
return $this->partDataTableHelper->renderAmount($context->getPart());
|
return $this->partDataTableHelper->renderAmount($context->getPart());
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
},
|
||||||
])
|
])
|
||||||
->add('storelocation', TextColumn::class, [
|
->add('storelocation', HTMLColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.storeLocations'),
|
'label' => $this->translator->trans('part.table.storeLocations'),
|
||||||
//We need to use a aggregate function to get the first store location, as we have a one-to-many relation
|
//We need to use a aggregate function to get the first store location, as we have a one-to-many relation
|
||||||
'orderField' => 'NATSORT(MIN(_storelocations.name))',
|
'orderField' => 'NATSORT(MIN(_storelocations.name))',
|
||||||
'visible' => false,
|
'visible' => false,
|
||||||
'render' => function ($value, ProjectBOMEntry $context) {
|
'data' => function (ProjectBOMEntry $context) {
|
||||||
if ($context->getPart() !== null) {
|
if ($context->getPart() !== null) {
|
||||||
return $this->partDataTableHelper->renderStorageLocations($context->getPart());
|
return $this->partDataTableHelper->renderStorageLocations($context->getPart());
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
},
|
||||||
])
|
])
|
||||||
->add('price', TextColumn::class, [
|
->add('price', TextColumn::class, [
|
||||||
'label' => 'project.bom.price',
|
'label' => 'project.bom.price',
|
||||||
'visible' => false,
|
'visible' => false,
|
||||||
'render' => function ($value, ProjectBOMEntry $context) {
|
'data' => function (ProjectBOMEntry $context) {
|
||||||
$price = $this->projectBuildHelper->getEntryUnitPrice($context);
|
$price = $this->projectBuildHelper->getEntryUnitPrice($context);
|
||||||
return $this->moneyFormatter->format($price->toScale(2, RoundingMode::Up)->toFloat(), null, 2, true);
|
return $this->moneyFormatter->format($price->toScale(2, RoundingMode::Up)->toFloat(), null, 2, true);
|
||||||
},
|
},
|
||||||
|
|
@ -219,7 +221,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||||
->add('ext_price', TextColumn::class, [
|
->add('ext_price', TextColumn::class, [
|
||||||
'label' => 'project.bom.ext_price',
|
'label' => 'project.bom.ext_price',
|
||||||
'visible' => false,
|
'visible' => false,
|
||||||
'render' => function ($value, ProjectBOMEntry $context) {
|
'data' => function (ProjectBOMEntry $context) {
|
||||||
$price = $this->projectBuildHelper->getEntryUnitPrice($context);
|
$price = $this->projectBuildHelper->getEntryUnitPrice($context);
|
||||||
return $this->moneyFormatter->format(
|
return $this->moneyFormatter->format(
|
||||||
$price->multipliedBy(BigDecimal::fromFloatShortest($context->getQuantity()))
|
$price->multipliedBy(BigDecimal::fromFloatShortest($context->getQuantity()))
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ use App\Doctrine\Functions\SiValueSort;
|
||||||
use App\Exceptions\InvalidRegexException;
|
use App\Exceptions\InvalidRegexException;
|
||||||
use Doctrine\DBAL\Driver\Connection;
|
use Doctrine\DBAL\Driver\Connection;
|
||||||
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
|
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
|
||||||
|
use Pdo\Sqlite;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This middleware is used to add the regexp operator to the SQLite platform.
|
* This middleware is used to add the regexp operator to the SQLite platform.
|
||||||
|
|
@ -44,17 +45,30 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
|
||||||
if ($params['driver'] === 'pdo_sqlite') {
|
if ($params['driver'] === 'pdo_sqlite') {
|
||||||
$native_connection = $connection->getNativeConnection();
|
$native_connection = $connection->getNativeConnection();
|
||||||
|
|
||||||
//Ensure that the function really exists on the connection, as it is marked as experimental according to PHP documentation
|
|
||||||
if($native_connection instanceof \PDO) {
|
if($native_connection instanceof \PDO) {
|
||||||
$native_connection->sqliteCreateFunction('REGEXP', self::regexp(...), 2, \PDO::SQLITE_DETERMINISTIC);
|
|
||||||
$native_connection->sqliteCreateFunction('FIELD', self::field(...), -1, \PDO::SQLITE_DETERMINISTIC);
|
|
||||||
$native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC);
|
|
||||||
|
|
||||||
//Create a new collation for natural sorting
|
//Use the new PDO::createFunction and PDO::createCollation methods if available (PHP 8.4+)
|
||||||
$native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...));
|
if (is_a($native_connection, Sqlite::class)) { #TODO: Remove this check when PHP 8.4 is the minimum requirement
|
||||||
|
$native_connection->createFunction('REGEXP', self::regexp(...), 2, Sqlite::DETERMINISTIC);
|
||||||
|
$native_connection->createFunction('FIELD', self::field(...), -1, Sqlite::DETERMINISTIC);
|
||||||
|
$native_connection->createFunction('FIELD2', self::field2(...), 2, Sqlite::DETERMINISTIC);
|
||||||
|
|
||||||
//Create a function for SI prefix value sorting
|
//Create a new collation for natural sorting
|
||||||
$native_connection->sqliteCreateFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, \PDO::SQLITE_DETERMINISTIC);
|
$native_connection->createCollation('NATURAL_CMP', strnatcmp(...));
|
||||||
|
|
||||||
|
//Create a function for SI prefix value sorting
|
||||||
|
$native_connection->createFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, Sqlite::DETERMINISTIC);
|
||||||
|
} else {
|
||||||
|
$native_connection->sqliteCreateFunction('REGEXP', self::regexp(...), 2, \PDO::SQLITE_DETERMINISTIC);
|
||||||
|
$native_connection->sqliteCreateFunction('FIELD', self::field(...), -1, \PDO::SQLITE_DETERMINISTIC);
|
||||||
|
$native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC);
|
||||||
|
|
||||||
|
//Create a new collation for natural sorting
|
||||||
|
$native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...));
|
||||||
|
|
||||||
|
//Create a function for SI prefix value sorting
|
||||||
|
$native_connection->sqliteCreateFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, \PDO::SQLITE_DETERMINISTIC);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,10 @@ class SetSQLModeMiddlewareDriver extends AbstractDriverMiddleware
|
||||||
{
|
{
|
||||||
//Only set this on MySQL connections, as other databases don't support this parameter
|
//Only set this on MySQL connections, as other databases don't support this parameter
|
||||||
if($params['driver'] === 'pdo_mysql') {
|
if($params['driver'] === 'pdo_mysql') {
|
||||||
//1002 is \PDO::MYSQL_ATTR_INIT_COMMAND constant value
|
//PDO::MYSQL_ATTR_INIT_COMMAND is deprecated since PHP 8.5 in favor of Pdo\Mysql::ATTR_INIT_COMMAND,
|
||||||
$params['driverOptions'][\PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode, \'ONLY_FULL_GROUP_BY\', \'\'))';
|
//but the Pdo\Mysql class only exists since PHP 8.4. Both constants have the same value (1002).
|
||||||
|
$initCommandAttr = class_exists(\Pdo\Mysql::class) ? \Pdo\Mysql::ATTR_INIT_COMMAND : \PDO::MYSQL_ATTR_INIT_COMMAND;
|
||||||
|
$params['driverOptions'][$initCommandAttr] = 'SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode, \'ONLY_FULL_GROUP_BY\', \'\'))';
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent::connect($params);
|
return parent::connect($params);
|
||||||
|
|
|
||||||
|
|
@ -65,21 +65,16 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||||
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
||||||
new Patch(security: 'is_granted("edit", object)'),
|
new Patch(security: 'is_granted("edit", object)'),
|
||||||
new Delete(security: 'is_granted("delete", object)'),
|
new Delete(security: 'is_granted("delete", object)'),
|
||||||
|
new GetCollection(
|
||||||
|
uriTemplate: '/attachment_types/{id}/children.{_format}',
|
||||||
|
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: AttachmentType::class)],
|
||||||
|
openapi: new Operation(summary: 'Retrieves the children elements of an attachment type.'),
|
||||||
|
security: 'is_granted("@attachment_types.read")'
|
||||||
|
),
|
||||||
],
|
],
|
||||||
normalizationContext: ['groups' => ['attachment_type:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
normalizationContext: ['groups' => ['attachment_type:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
||||||
denormalizationContext: ['groups' => ['attachment_type:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
denormalizationContext: ['groups' => ['attachment_type:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
||||||
)]
|
)]
|
||||||
#[ApiResource(
|
|
||||||
uriTemplate: '/attachment_types/{id}/children.{_format}',
|
|
||||||
operations: [
|
|
||||||
new GetCollection(openapi: new Operation(summary: 'Retrieves the children elements of an attachment type.'),
|
|
||||||
security: 'is_granted("@attachment_types.read")')
|
|
||||||
],
|
|
||||||
uriVariables: [
|
|
||||||
'id' => new Link(fromProperty: 'children', fromClass: AttachmentType::class)
|
|
||||||
],
|
|
||||||
normalizationContext: ['groups' => ['attachment_type:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
|
|
||||||
)]
|
|
||||||
#[ApiFilter(PropertyFilter::class)]
|
#[ApiFilter(PropertyFilter::class)]
|
||||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
|
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
|
||||||
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
||||||
|
|
|
||||||
|
|
@ -68,23 +68,16 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||||
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
||||||
new Patch(security: 'is_granted("edit", object)'),
|
new Patch(security: 'is_granted("edit", object)'),
|
||||||
new Delete(security: 'is_granted("delete", object)'),
|
new Delete(security: 'is_granted("delete", object)'),
|
||||||
|
new GetCollection(
|
||||||
|
uriTemplate: '/categories/{id}/children.{_format}',
|
||||||
|
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Category::class)],
|
||||||
|
openapi: new Operation(summary: 'Retrieves the children elements of a category.'),
|
||||||
|
security: 'is_granted("@categories.read")'
|
||||||
|
),
|
||||||
],
|
],
|
||||||
normalizationContext: ['groups' => ['category:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
normalizationContext: ['groups' => ['category:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
||||||
denormalizationContext: ['groups' => ['category:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
denormalizationContext: ['groups' => ['category:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
||||||
)]
|
)]
|
||||||
#[ApiResource(
|
|
||||||
uriTemplate: '/categories/{id}/children.{_format}',
|
|
||||||
operations: [
|
|
||||||
new GetCollection(
|
|
||||||
openapi: new Operation(summary: 'Retrieves the children elements of a category.'),
|
|
||||||
security: 'is_granted("@categories.read")'
|
|
||||||
)
|
|
||||||
],
|
|
||||||
uriVariables: [
|
|
||||||
'id' => new Link(fromProperty: 'children', fromClass: Category::class)
|
|
||||||
],
|
|
||||||
normalizationContext: ['groups' => ['category:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
|
|
||||||
)]
|
|
||||||
#[ApiFilter(PropertyFilter::class)]
|
#[ApiFilter(PropertyFilter::class)]
|
||||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
|
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
|
||||||
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
||||||
|
|
|
||||||
|
|
@ -67,23 +67,16 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||||
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
||||||
new Patch(security: 'is_granted("edit", object)'),
|
new Patch(security: 'is_granted("edit", object)'),
|
||||||
new Delete(security: 'is_granted("delete", object)'),
|
new Delete(security: 'is_granted("delete", object)'),
|
||||||
|
new GetCollection(
|
||||||
|
uriTemplate: '/footprints/{id}/children.{_format}',
|
||||||
|
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Footprint::class)],
|
||||||
|
openapi: new Operation(summary: 'Retrieves the children elements of a footprint.'),
|
||||||
|
security: 'is_granted("@footprints.read")'
|
||||||
|
),
|
||||||
],
|
],
|
||||||
normalizationContext: ['groups' => ['footprint:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
normalizationContext: ['groups' => ['footprint:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
||||||
denormalizationContext: ['groups' => ['footprint:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
denormalizationContext: ['groups' => ['footprint:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
||||||
)]
|
)]
|
||||||
#[ApiResource(
|
|
||||||
uriTemplate: '/footprints/{id}/children.{_format}',
|
|
||||||
operations: [
|
|
||||||
new GetCollection(
|
|
||||||
openapi: new Operation(summary: 'Retrieves the children elements of a footprint.'),
|
|
||||||
security: 'is_granted("@footprints.read")'
|
|
||||||
)
|
|
||||||
],
|
|
||||||
uriVariables: [
|
|
||||||
'id' => new Link(fromProperty: 'children', fromClass: Footprint::class)
|
|
||||||
],
|
|
||||||
normalizationContext: ['groups' => ['footprint:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
|
|
||||||
)]
|
|
||||||
#[ApiFilter(PropertyFilter::class)]
|
#[ApiFilter(PropertyFilter::class)]
|
||||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
|
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
|
||||||
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,11 @@ use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping\Column;
|
use Doctrine\ORM\Mapping\Column;
|
||||||
use Doctrine\ORM\Mapping\Embeddable;
|
use Doctrine\ORM\Mapping\Embeddable;
|
||||||
use Symfony\Component\Serializer\Annotation\Groups;
|
use Symfony\Component\Serializer\Annotation\Groups;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents a reference to a info provider inside a part.
|
* This class represents a reference to an info provider inside a part.
|
||||||
* @see \App\Tests\Entity\Parts\InfoProviderReferenceTest
|
* @see \App\Tests\Entity\Parts\InfoProviderReferenceTest
|
||||||
*/
|
*/
|
||||||
#[Embeddable]
|
#[Embeddable]
|
||||||
|
|
@ -157,4 +159,44 @@ class InfoProviderReference
|
||||||
$ref->last_updated = new \DateTimeImmutable();
|
$ref->last_updated = new \DateTimeImmutable();
|
||||||
return $ref;
|
return $ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a reference to an info provider based on the given parameters.
|
||||||
|
* @param string|null $provider_key
|
||||||
|
* @param string|null $provider_id
|
||||||
|
* @param string|null $provider_url
|
||||||
|
* @param \DateTimeImmutable|null $last_updated
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function create(?string $provider_key, ?string $provider_id, ?string $provider_url, ?\DateTimeImmutable $last_updated): self
|
||||||
|
{
|
||||||
|
$ref = new InfoProviderReference();
|
||||||
|
$ref->provider_key = $provider_key;
|
||||||
|
$ref->provider_id = $provider_id;
|
||||||
|
$ref->provider_url = $provider_url;
|
||||||
|
$ref->last_updated = $last_updated;
|
||||||
|
return $ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Assert\Callback()]
|
||||||
|
public function validate(ExecutionContextInterface $context, mixed $payload): void
|
||||||
|
{
|
||||||
|
if ($this->provider_key === null && $this->provider_id !== null) {
|
||||||
|
$context->buildViolation('info_providers.validation.provider_id_without_key')
|
||||||
|
->atPath('provider_key')
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->provider_key === null && $this->provider_url !== null) {
|
||||||
|
$context->buildViolation('info_providers.validation.provider_url_without_key')
|
||||||
|
->atPath('provider_url')
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->provider_key !== null && $this->provider_id === null) {
|
||||||
|
$context->buildViolation('info_providers.validation.provider_key_without_id')
|
||||||
|
->atPath('provider_id')
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,23 +66,16 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||||
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
||||||
new Patch(security: 'is_granted("edit", object)'),
|
new Patch(security: 'is_granted("edit", object)'),
|
||||||
new Delete(security: 'is_granted("delete", object)'),
|
new Delete(security: 'is_granted("delete", object)'),
|
||||||
|
new GetCollection(
|
||||||
|
uriTemplate: '/manufacturers/{id}/children.{_format}',
|
||||||
|
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Manufacturer::class)],
|
||||||
|
openapi: new Operation(summary: 'Retrieves the children elements of a manufacturer.'),
|
||||||
|
security: 'is_granted("@manufacturers.read")'
|
||||||
|
),
|
||||||
],
|
],
|
||||||
normalizationContext: ['groups' => ['manufacturer:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
normalizationContext: ['groups' => ['manufacturer:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
||||||
denormalizationContext: ['groups' => ['manufacturer:write', 'company:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
denormalizationContext: ['groups' => ['manufacturer:write', 'company:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
||||||
)]
|
)]
|
||||||
#[ApiResource(
|
|
||||||
uriTemplate: '/manufacturers/{id}/children.{_format}',
|
|
||||||
operations: [
|
|
||||||
new GetCollection(
|
|
||||||
openapi: new Operation(summary: 'Retrieves the children elements of a manufacturer.'),
|
|
||||||
security: 'is_granted("@manufacturers.read")'
|
|
||||||
)
|
|
||||||
],
|
|
||||||
uriVariables: [
|
|
||||||
'id' => new Link(fromProperty: 'children', fromClass: Manufacturer::class)
|
|
||||||
],
|
|
||||||
normalizationContext: ['groups' => ['manufacturer:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
|
|
||||||
)]
|
|
||||||
#[ApiFilter(PropertyFilter::class)]
|
#[ApiFilter(PropertyFilter::class)]
|
||||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
|
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
|
||||||
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
||||||
|
|
|
||||||
|
|
@ -71,23 +71,16 @@ use Symfony\Component\Validator\Constraints\Length;
|
||||||
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
||||||
new Patch(security: 'is_granted("edit", object)'),
|
new Patch(security: 'is_granted("edit", object)'),
|
||||||
new Delete(security: 'is_granted("delete", object)'),
|
new Delete(security: 'is_granted("delete", object)'),
|
||||||
|
new GetCollection(
|
||||||
|
uriTemplate: '/measurement_units/{id}/children.{_format}',
|
||||||
|
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: MeasurementUnit::class)],
|
||||||
|
openapi: new Operation(summary: 'Retrieves the children elements of a MeasurementUnit.'),
|
||||||
|
security: 'is_granted("@measurement_units.read")'
|
||||||
|
),
|
||||||
],
|
],
|
||||||
normalizationContext: ['groups' => ['measurement_unit:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
normalizationContext: ['groups' => ['measurement_unit:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
||||||
denormalizationContext: ['groups' => ['measurement_unit:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
denormalizationContext: ['groups' => ['measurement_unit:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
||||||
)]
|
)]
|
||||||
#[ApiResource(
|
|
||||||
uriTemplate: '/measurement_units/{id}/children.{_format}',
|
|
||||||
operations: [
|
|
||||||
new GetCollection(
|
|
||||||
openapi: new Operation(summary: 'Retrieves the children elements of a MeasurementUnit.'),
|
|
||||||
security: 'is_granted("@measurement_units.read")'
|
|
||||||
)
|
|
||||||
],
|
|
||||||
uriVariables: [
|
|
||||||
'id' => new Link(fromProperty: 'children', fromClass: MeasurementUnit::class)
|
|
||||||
],
|
|
||||||
normalizationContext: ['groups' => ['measurement_unit:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
|
|
||||||
)]
|
|
||||||
#[ApiFilter(PropertyFilter::class)]
|
#[ApiFilter(PropertyFilter::class)]
|
||||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "unit"])]
|
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "unit"])]
|
||||||
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ trait AdvancedPropertyTrait
|
||||||
*/
|
*/
|
||||||
#[ORM\Embedded(class: InfoProviderReference::class, columnPrefix: 'provider_reference_')]
|
#[ORM\Embedded(class: InfoProviderReference::class, columnPrefix: 'provider_reference_')]
|
||||||
#[Groups(['full', 'part:read'])]
|
#[Groups(['full', 'part:read'])]
|
||||||
|
#[Assert\Valid()]
|
||||||
protected InfoProviderReference $providerReference;
|
protected InfoProviderReference $providerReference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -67,23 +67,16 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||||
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
||||||
new Patch(security: 'is_granted("edit", object)'),
|
new Patch(security: 'is_granted("edit", object)'),
|
||||||
new Delete(security: 'is_granted("delete", object)'),
|
new Delete(security: 'is_granted("delete", object)'),
|
||||||
|
new GetCollection(
|
||||||
|
uriTemplate: '/storage_locations/{id}/children.{_format}',
|
||||||
|
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: StorageLocation::class)],
|
||||||
|
openapi: new Operation(summary: 'Retrieves the children elements of a storage location.'),
|
||||||
|
security: 'is_granted("@storelocations.read")'
|
||||||
|
),
|
||||||
],
|
],
|
||||||
normalizationContext: ['groups' => ['location:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
normalizationContext: ['groups' => ['location:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
||||||
denormalizationContext: ['groups' => ['location:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
denormalizationContext: ['groups' => ['location:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
||||||
)]
|
)]
|
||||||
#[ApiResource(
|
|
||||||
uriTemplate: '/storage_locations/{id}/children.{_format}',
|
|
||||||
operations: [
|
|
||||||
new GetCollection(
|
|
||||||
openapi: new Operation(summary: 'Retrieves the children elements of a storage location.'),
|
|
||||||
security: 'is_granted("@storelocations.read")'
|
|
||||||
)
|
|
||||||
],
|
|
||||||
uriVariables: [
|
|
||||||
'id' => new Link(fromProperty: 'children', fromClass: Manufacturer::class)
|
|
||||||
],
|
|
||||||
normalizationContext: ['groups' => ['location:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
|
|
||||||
)]
|
|
||||||
#[ApiFilter(PropertyFilter::class)]
|
#[ApiFilter(PropertyFilter::class)]
|
||||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
|
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
|
||||||
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
||||||
|
|
|
||||||
|
|
@ -71,21 +71,16 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||||
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
||||||
new Patch(security: 'is_granted("edit", object)'),
|
new Patch(security: 'is_granted("edit", object)'),
|
||||||
new Delete(security: 'is_granted("delete", object)'),
|
new Delete(security: 'is_granted("delete", object)'),
|
||||||
|
new GetCollection(
|
||||||
|
uriTemplate: '/suppliers/{id}/children.{_format}',
|
||||||
|
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Supplier::class)],
|
||||||
|
openapi: new Operation(summary: 'Retrieves the children elements of a supplier.'),
|
||||||
|
security: 'is_granted("@manufacturers.read")'
|
||||||
|
),
|
||||||
],
|
],
|
||||||
normalizationContext: ['groups' => ['supplier:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
normalizationContext: ['groups' => ['supplier:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
||||||
denormalizationContext: ['groups' => ['supplier:write', 'company:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
denormalizationContext: ['groups' => ['supplier:write', 'company:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
||||||
)]
|
)]
|
||||||
#[ApiResource(
|
|
||||||
uriTemplate: '/suppliers/{id}/children.{_format}',
|
|
||||||
operations: [new GetCollection(
|
|
||||||
openapi: new Operation(summary: 'Retrieves the children elements of a supplier.'),
|
|
||||||
security: 'is_granted("@manufacturers.read")'
|
|
||||||
)],
|
|
||||||
uriVariables: [
|
|
||||||
'id' => new Link(fromProperty: 'children', fromClass: Supplier::class)
|
|
||||||
],
|
|
||||||
normalizationContext: ['groups' => ['supplier:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
|
|
||||||
)]
|
|
||||||
#[ApiFilter(PropertyFilter::class)]
|
#[ApiFilter(PropertyFilter::class)]
|
||||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
|
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
|
||||||
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
||||||
|
|
|
||||||
|
|
@ -71,23 +71,16 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||||
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
||||||
new Patch(security: 'is_granted("edit", object)'),
|
new Patch(security: 'is_granted("edit", object)'),
|
||||||
new Delete(security: 'is_granted("delete", object)'),
|
new Delete(security: 'is_granted("delete", object)'),
|
||||||
|
new GetCollection(
|
||||||
|
uriTemplate: '/currencies/{id}/children.{_format}',
|
||||||
|
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Currency::class)],
|
||||||
|
openapi: new Operation(summary: 'Retrieves the children elements of a currency.'),
|
||||||
|
security: 'is_granted("@currencies.read")'
|
||||||
|
),
|
||||||
],
|
],
|
||||||
normalizationContext: ['groups' => ['currency:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
normalizationContext: ['groups' => ['currency:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
||||||
denormalizationContext: ['groups' => ['currency:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
denormalizationContext: ['groups' => ['currency:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
||||||
)]
|
)]
|
||||||
#[ApiResource(
|
|
||||||
uriTemplate: '/currencies/{id}/children.{_format}',
|
|
||||||
operations: [
|
|
||||||
new GetCollection(
|
|
||||||
openapi: new Operation(summary: 'Retrieves the children elements of a currency.'),
|
|
||||||
security: 'is_granted("@currencies.read")'
|
|
||||||
)
|
|
||||||
],
|
|
||||||
uriVariables: [
|
|
||||||
'id' => new Link(fromProperty: 'children', fromClass: Currency::class)
|
|
||||||
],
|
|
||||||
normalizationContext: ['groups' => ['currency:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
|
|
||||||
)]
|
|
||||||
#[ApiFilter(PropertyFilter::class)]
|
#[ApiFilter(PropertyFilter::class)]
|
||||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "iso_code"])]
|
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "iso_code"])]
|
||||||
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
||||||
|
|
|
||||||
|
|
@ -71,23 +71,17 @@ use Symfony\Component\Validator\Constraints\Length;
|
||||||
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
||||||
new Patch(security: 'is_granted("edit", object)'),
|
new Patch(security: 'is_granted("edit", object)'),
|
||||||
new Delete(security: 'is_granted("delete", object)'),
|
new Delete(security: 'is_granted("delete", object)'),
|
||||||
|
new GetCollection(
|
||||||
|
uriTemplate: '/parts/{id}/orderdetails.{_format}',
|
||||||
|
uriVariables: ['id' => new Link(toProperty: 'part', fromClass: Part::class)],
|
||||||
|
normalizationContext: ['groups' => ['orderdetail:read', 'pricedetail:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
||||||
|
openapi: new Operation(summary: 'Retrieves the orderdetails of a part.'),
|
||||||
|
security: 'is_granted("@parts.read")'
|
||||||
|
),
|
||||||
],
|
],
|
||||||
normalizationContext: ['groups' => ['orderdetail:read', 'orderdetail:read:standalone', 'api:basic:read', 'pricedetail:read'], 'openapi_definition_name' => 'Read'],
|
normalizationContext: ['groups' => ['orderdetail:read', 'orderdetail:read:standalone', 'api:basic:read', 'pricedetail:read'], 'openapi_definition_name' => 'Read'],
|
||||||
denormalizationContext: ['groups' => ['orderdetail:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
|
denormalizationContext: ['groups' => ['orderdetail:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
|
||||||
)]
|
)]
|
||||||
#[ApiResource(
|
|
||||||
uriTemplate: '/parts/{id}/orderdetails.{_format}',
|
|
||||||
operations: [
|
|
||||||
new GetCollection(
|
|
||||||
openapi: new Operation(summary: 'Retrieves the orderdetails of a part.'),
|
|
||||||
security: 'is_granted("@parts.read")'
|
|
||||||
)
|
|
||||||
],
|
|
||||||
uriVariables: [
|
|
||||||
'id' => new Link(toProperty: 'part', fromClass: Part::class)
|
|
||||||
],
|
|
||||||
normalizationContext: ['groups' => ['orderdetail:read', 'pricedetail:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
|
|
||||||
)]
|
|
||||||
#[ApiFilter(PropertyFilter::class)]
|
#[ApiFilter(PropertyFilter::class)]
|
||||||
#[ApiFilter(PropertyFilter::class)]
|
#[ApiFilter(PropertyFilter::class)]
|
||||||
#[ApiFilter(LikeFilter::class, properties: ["supplierpartnr", "supplier_product_url"])]
|
#[ApiFilter(LikeFilter::class, properties: ["supplierpartnr", "supplier_product_url"])]
|
||||||
|
|
|
||||||
|
|
@ -66,23 +66,16 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
||||||
new Patch(security: 'is_granted("edit", object)'),
|
new Patch(security: 'is_granted("edit", object)'),
|
||||||
new Delete(security: 'is_granted("delete", object)'),
|
new Delete(security: 'is_granted("delete", object)'),
|
||||||
|
new GetCollection(
|
||||||
|
uriTemplate: '/projects/{id}/children.{_format}',
|
||||||
|
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: Project::class)],
|
||||||
|
openapi: new Operation(summary: 'Retrieves the children elements of a project.'),
|
||||||
|
security: 'is_granted("@projects.read")'
|
||||||
|
),
|
||||||
],
|
],
|
||||||
normalizationContext: ['groups' => ['project:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
normalizationContext: ['groups' => ['project:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
||||||
denormalizationContext: ['groups' => ['project:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
denormalizationContext: ['groups' => ['project:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
||||||
)]
|
)]
|
||||||
#[ApiResource(
|
|
||||||
uriTemplate: '/projects/{id}/children.{_format}',
|
|
||||||
operations: [
|
|
||||||
new GetCollection(
|
|
||||||
openapi: new Operation(summary: 'Retrieves the children elements of a project.'),
|
|
||||||
security: 'is_granted("@projects.read")'
|
|
||||||
)
|
|
||||||
],
|
|
||||||
uriVariables: [
|
|
||||||
'id' => new Link(fromProperty: 'children', fromClass: Project::class)
|
|
||||||
],
|
|
||||||
normalizationContext: ['groups' => ['project:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
|
|
||||||
)]
|
|
||||||
#[ApiFilter(PropertyFilter::class)]
|
#[ApiFilter(PropertyFilter::class)]
|
||||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
|
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
|
||||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])]
|
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])]
|
||||||
|
|
@ -117,7 +110,7 @@ class Project extends AbstractStructuralDBElement
|
||||||
/**
|
/**
|
||||||
* @var string|null The current status of the project
|
* @var string|null The current status of the project
|
||||||
*/
|
*/
|
||||||
#[Assert\Choice(['draft', 'planning', 'in_production', 'finished', 'archived'])]
|
#[Assert\Choice(choices: ['draft', 'planning', 'in_production', 'finished', 'archived'])]
|
||||||
#[Groups(['extended', 'full', 'project:read', 'project:write', 'import'])]
|
#[Groups(['extended', 'full', 'project:read', 'project:write', 'import'])]
|
||||||
#[ORM\Column(type: Types::STRING, length: 64, nullable: true)]
|
#[ORM\Column(type: Types::STRING, length: 64, nullable: true)]
|
||||||
protected ?string $status = null;
|
protected ?string $status = null;
|
||||||
|
|
|
||||||
|
|
@ -63,23 +63,16 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
new Post(uriTemplate: '/project_bom_entries.{_format}', securityPostDenormalize: 'is_granted("create", object)',),
|
new Post(uriTemplate: '/project_bom_entries.{_format}', securityPostDenormalize: 'is_granted("create", object)',),
|
||||||
new Patch(uriTemplate: '/project_bom_entries/{id}.{_format}', security: 'is_granted("edit", object)',),
|
new Patch(uriTemplate: '/project_bom_entries/{id}.{_format}', security: 'is_granted("edit", object)',),
|
||||||
new Delete(uriTemplate: '/project_bom_entries/{id}.{_format}', security: 'is_granted("delete", object)',),
|
new Delete(uriTemplate: '/project_bom_entries/{id}.{_format}', security: 'is_granted("delete", object)',),
|
||||||
|
new GetCollection(
|
||||||
|
uriTemplate: '/projects/{id}/bom.{_format}',
|
||||||
|
uriVariables: ['id' => new Link(fromProperty: 'bom_entries', fromClass: Project::class)],
|
||||||
|
openapi: new Operation(summary: 'Retrieves the BOM entries of the given project.'),
|
||||||
|
security: 'is_granted("@projects.read")'
|
||||||
|
),
|
||||||
],
|
],
|
||||||
normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
||||||
denormalizationContext: ['groups' => ['bom_entry:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
|
denormalizationContext: ['groups' => ['bom_entry:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
|
||||||
)]
|
)]
|
||||||
#[ApiResource(
|
|
||||||
uriTemplate: '/projects/{id}/bom.{_format}',
|
|
||||||
operations: [
|
|
||||||
new GetCollection(
|
|
||||||
openapi: new Operation(summary: 'Retrieves the BOM entries of the given project.'),
|
|
||||||
security: 'is_granted("@projects.read")'
|
|
||||||
)
|
|
||||||
],
|
|
||||||
uriVariables: [
|
|
||||||
'id' => new Link(fromProperty: 'bom_entries', fromClass: Project::class)
|
|
||||||
],
|
|
||||||
normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
|
|
||||||
)]
|
|
||||||
#[ApiFilter(PropertyFilter::class)]
|
#[ApiFilter(PropertyFilter::class)]
|
||||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", 'mountnames'])]
|
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", 'mountnames'])]
|
||||||
#[ApiFilter(RangeFilter::class, properties: ['quantity'])]
|
#[ApiFilter(RangeFilter::class, properties: ['quantity'])]
|
||||||
|
|
|
||||||
79
src/Form/Extension/DateTimeModelTimezoneExtension.php
Normal file
79
src/Form/Extension/DateTimeModelTimezoneExtension.php
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form\Extension;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\AbstractTypeExtension;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\Form\FormEvent;
|
||||||
|
use Symfony\Component\Form\FormEvents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catches timezone mismatches between a DateTimeInterface model value and the effective
|
||||||
|
* model_timezone configured on the field.
|
||||||
|
*
|
||||||
|
* Doctrine's UTCDateTimeImmutableType always returns UTC DateTimeImmutable objects, so any
|
||||||
|
* date/datetime field that omits `model_timezone: 'UTC'` will silently corrupt stored values
|
||||||
|
* (the transformer treats the UTC instant as if it were in the user's local timezone).
|
||||||
|
* This extension throws a \LogicException early so the mistake is caught at development time.
|
||||||
|
*/
|
||||||
|
class DateTimeModelTimezoneExtension extends AbstractTypeExtension
|
||||||
|
{
|
||||||
|
public static function getExtendedTypes(): iterable
|
||||||
|
{
|
||||||
|
return [DateTimeType::class, DateType::class];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void {
|
||||||
|
$data = $event->getData();
|
||||||
|
|
||||||
|
if (!$data instanceof \DateTimeInterface) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the effective model timezone: explicit option or the PHP default set at build time.
|
||||||
|
// This mirrors what BaseDateTimeTransformer does in its constructor.
|
||||||
|
$modelTimezone = $options['model_timezone'] ?? date_default_timezone_get();
|
||||||
|
|
||||||
|
$dataOffset = $data->getTimezone()->getOffset($data);
|
||||||
|
$modelOffset = (new \DateTimeZone($modelTimezone))->getOffset($data);
|
||||||
|
|
||||||
|
if ($dataOffset !== $modelOffset) {
|
||||||
|
throw new \LogicException(sprintf(
|
||||||
|
'Form field "%s" received a %s with timezone "%s" (UTC offset %+d s), '
|
||||||
|
. 'but the effective model_timezone is "%s" (UTC offset %+d s). '
|
||||||
|
. 'Set the "model_timezone" option to match the timezone of your data source.',
|
||||||
|
$event->getForm()->getName(),
|
||||||
|
get_debug_type($data),
|
||||||
|
$data->getTimezone()->getName(),
|
||||||
|
$dataOffset,
|
||||||
|
$modelTimezone,
|
||||||
|
$modelOffset
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/Form/InfoProviderSystem/InfoProviderReferenceType.php
Normal file
113
src/Form/InfoProviderSystem/InfoProviderReferenceType.php
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\Form\InfoProviderSystem;
|
||||||
|
|
||||||
|
use App\Entity\Parts\InfoProviderReference;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\DataMapperInterface;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\UrlType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class InfoProviderReferenceType extends AbstractType implements DataMapperInterface
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->setDataMapper($this)
|
||||||
|
->add('provider_key', ProviderSelectType::class, [
|
||||||
|
'label' => 'info_providers.provider_key',
|
||||||
|
'input' => 'string',
|
||||||
|
'multiple' => false,
|
||||||
|
'required' => false,
|
||||||
|
'only_active' => false,
|
||||||
|
])
|
||||||
|
->add('provider_id', TextType::class, [
|
||||||
|
'label' => 'info_providers.provider_id',
|
||||||
|
'required' => false,
|
||||||
|
])
|
||||||
|
->add('provider_url', UrlType::class, [
|
||||||
|
'label' => 'info_providers.provider_url',
|
||||||
|
'required' => false,
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => InfoProviderReference::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function mapDataToForms(mixed $viewData, \Traversable $forms): void
|
||||||
|
{
|
||||||
|
if ($viewData === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$viewData instanceof InfoProviderReference) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var FormInterface[] $forms */
|
||||||
|
$forms = iterator_to_array($forms);
|
||||||
|
|
||||||
|
$forms['provider_key']->setData($viewData->getProviderKey());
|
||||||
|
$forms['provider_id']->setData($viewData->getProviderId());
|
||||||
|
$forms['provider_url']->setData($viewData->getProviderUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mapFormsToData(\Traversable $forms, mixed &$viewData): void
|
||||||
|
{
|
||||||
|
/** @var FormInterface[] $forms */
|
||||||
|
$forms = iterator_to_array($forms);
|
||||||
|
|
||||||
|
$providerKey = $forms['provider_key']->getData();
|
||||||
|
$providerId = $forms['provider_id']->getData();
|
||||||
|
$providerUrl = $forms['provider_url']->getData();
|
||||||
|
|
||||||
|
if ($viewData === null) {
|
||||||
|
$viewData = InfoProviderReference::noProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$viewData instanceof InfoProviderReference) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$oldDate = $viewData->getLastUpdated();
|
||||||
|
|
||||||
|
//If all fields are empty, we set the view data to a new instance without provider information
|
||||||
|
if ($providerKey === null && $providerId === null && $providerUrl === null) {
|
||||||
|
$viewData = InfoProviderReference::noProvider();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$viewData = InfoProviderReference::create($providerKey, $providerId, $providerUrl, $oldDate);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,12 +31,12 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\OptionsResolver\Options;
|
use Symfony\Component\OptionsResolver\Options;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
use Symfony\Component\Translation\StaticMessage;
|
use Symfony\Component\Translation\StaticMessage;
|
||||||
|
use Symfony\Component\Translation\TranslatableMessage;
|
||||||
|
|
||||||
class ProviderSelectType extends AbstractType
|
class ProviderSelectType extends AbstractType
|
||||||
{
|
{
|
||||||
public function __construct(private readonly ProviderRegistry $providerRegistry)
|
public function __construct(private readonly ProviderRegistry $providerRegistry)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getParent(): string
|
public function getParent(): string
|
||||||
|
|
@ -46,17 +46,22 @@ class ProviderSelectType extends AbstractType
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver): void
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
{
|
{
|
||||||
$providers = $this->providerRegistry->getActiveProviders();
|
|
||||||
|
|
||||||
$resolver->setDefault('input', 'object');
|
$resolver->setDefault('input', 'object');
|
||||||
$resolver->setAllowedTypes('input', 'string');
|
$resolver->setAllowedTypes('input', 'string');
|
||||||
//Either the form returns the provider objects or their keys
|
//Either the form returns the provider objects or their keys
|
||||||
$resolver->setAllowedValues('input', ['object', 'string']);
|
$resolver->setAllowedValues('input', ['object', 'string']);
|
||||||
$resolver->setDefault('multiple', true);
|
$resolver->setDefault('multiple', true);
|
||||||
|
|
||||||
$resolver->setDefault('choices', function (Options $options) use ($providers) {
|
//Only show active providers in the list, or also inactive ones
|
||||||
|
$resolver->setDefault('only_active', true);
|
||||||
|
$resolver->setAllowedTypes('only_active', 'bool');
|
||||||
|
|
||||||
|
|
||||||
|
$resolver->setDefault('choices', function (Options $options) {
|
||||||
|
$providers = $options['only_active'] ? $this->providerRegistry->getActiveProviders() : $this->providerRegistry->getProviders();
|
||||||
|
|
||||||
if ('object' === $options['input']) {
|
if ('object' === $options['input']) {
|
||||||
return $this->providerRegistry->getActiveProviders();
|
return $providers;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tmp = [];
|
$tmp = [];
|
||||||
|
|
@ -69,20 +74,35 @@ class ProviderSelectType extends AbstractType
|
||||||
});
|
});
|
||||||
|
|
||||||
//The choice_label and choice_value only needs to be set if we want the objects
|
//The choice_label and choice_value only needs to be set if we want the objects
|
||||||
$resolver->setDefault('choice_label', function (Options $options){
|
$resolver->setDefault('choice_label', function (Options $options) {
|
||||||
if ('object' === $options['input']) {
|
if ('object' === $options['input']) {
|
||||||
return ChoiceList::label($this, static fn (?InfoProviderInterface $choice) => new StaticMessage($choice?->getProviderInfo()['name']));
|
return ChoiceList::label($this, static fn(?InfoProviderInterface $choice
|
||||||
|
) => new StaticMessage($choice?->getProviderInfo()['name']));
|
||||||
}
|
}
|
||||||
|
|
||||||
return static fn ($choice, $key, $value) => new StaticMessage($key);
|
return static fn($choice, $key, $value) => new StaticMessage($key);
|
||||||
});
|
});
|
||||||
$resolver->setDefault('choice_value', function (Options $options) {
|
$resolver->setDefault('choice_value', function (Options $options) {
|
||||||
if ('object' === $options['input']) {
|
if ('object' === $options['input']) {
|
||||||
return ChoiceList::value($this, static fn(?InfoProviderInterface $choice) => $choice?->getProviderKey());
|
return ChoiceList::value($this,
|
||||||
|
static fn(?InfoProviderInterface $choice) => $choice?->getProviderKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
$resolver->setDefault('group_by', function (Options $options) {
|
||||||
|
//Do not show groups when only active providers are shown, because then all providers are active and the group would be useless
|
||||||
|
if ($options['only_active']) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return function ($choice, $key, string $value) {
|
||||||
|
if ($this->providerRegistry->getProviderByKey($value)->isActive()) {
|
||||||
|
return new TranslatableMessage('info_providers.providers_list.active');
|
||||||
|
}
|
||||||
|
return new TranslatableMessage('info_providers.providers_list.disabled');
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ use App\Entity\Parts\Part;
|
||||||
use App\Entity\Parts\PartCustomState;
|
use App\Entity\Parts\PartCustomState;
|
||||||
use App\Entity\PriceInformations\Orderdetail;
|
use App\Entity\PriceInformations\Orderdetail;
|
||||||
use App\Form\AttachmentFormType;
|
use App\Form\AttachmentFormType;
|
||||||
|
use App\Form\InfoProviderSystem\InfoProviderReferenceType;
|
||||||
use App\Form\ParameterType;
|
use App\Form\ParameterType;
|
||||||
use App\Form\Part\EDA\EDAPartInfoType;
|
use App\Form\Part\EDA\EDAPartInfoType;
|
||||||
use App\Form\Type\MasterPictureAttachmentType;
|
use App\Form\Type\MasterPictureAttachmentType;
|
||||||
|
|
@ -225,6 +226,10 @@ class PartBaseType extends AbstractType
|
||||||
'empty_data' => null,
|
'empty_data' => null,
|
||||||
'label' => 'part.gtin',
|
'label' => 'part.gtin',
|
||||||
])
|
])
|
||||||
|
->add('providerReference', InfoProviderReferenceType::class, [
|
||||||
|
'label' => false,
|
||||||
|
'required' => false,
|
||||||
|
])
|
||||||
;
|
;
|
||||||
|
|
||||||
//Comment section
|
//Comment section
|
||||||
|
|
|
||||||
|
|
@ -115,8 +115,10 @@ class PartLotType extends AbstractType
|
||||||
$builder->add('last_stocktake_at', DateTimeType::class, [
|
$builder->add('last_stocktake_at', DateTimeType::class, [
|
||||||
'label' => 'part_lot.edit.last_stocktake_at',
|
'label' => 'part_lot.edit.last_stocktake_at',
|
||||||
'widget' => 'single_text',
|
'widget' => 'single_text',
|
||||||
|
'model_timezone' => 'UTC', // The database stores the datetime in UTC, so we need to set the model timezone to UTC
|
||||||
'disabled' => !$this->security->isGranted('@parts_stock.stocktake'),
|
'disabled' => !$this->security->isGranted('@parts_stock.stocktake'),
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'with_seconds' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ class AttachmentTypeType extends AbstractType
|
||||||
return StructuralEntityType::class;
|
return StructuralEntityType::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
{
|
{
|
||||||
$resolver->define('attachment_filter_class')->allowedTypes('null', 'string')->default(null);
|
$resolver->define('attachment_filter_class')->allowedTypes('null', 'string')->default(null);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,10 @@ final class ProjectBuildRequest
|
||||||
$remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry);
|
$remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry);
|
||||||
foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) {
|
foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) {
|
||||||
//If the lot has instock use it for the build
|
//If the lot has instock use it for the build
|
||||||
$this->withdraw_amounts[$lot->getID()] = min($remaining_amount, $lot->getAmount());
|
$id = $lot->getID() ?? throw new \RuntimeException("Part lot needs to have an ID!");
|
||||||
$remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]);
|
|
||||||
|
$this->withdraw_amounts[$id] = min($remaining_amount, $lot->getAmount());
|
||||||
|
$remaining_amount -= max(0, $this->withdraw_amounts[$id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -176,6 +178,10 @@ final class ProjectBuildRequest
|
||||||
{
|
{
|
||||||
$lot_id = $lot instanceof PartLot ? $lot->getID() : $lot;
|
$lot_id = $lot instanceof PartLot ? $lot->getID() : $lot;
|
||||||
|
|
||||||
|
if ($lot_id === null) {
|
||||||
|
throw new \InvalidArgumentException('The given lot must have an ID!');
|
||||||
|
}
|
||||||
|
|
||||||
if (! array_key_exists($lot_id, $this->withdraw_amounts)) {
|
if (! array_key_exists($lot_id, $this->withdraw_amounts)) {
|
||||||
throw new \InvalidArgumentException('The given lot is not in the withdraw amounts array!');
|
throw new \InvalidArgumentException('The given lot is not in the withdraw amounts array!');
|
||||||
}
|
}
|
||||||
|
|
@ -192,10 +198,12 @@ final class ProjectBuildRequest
|
||||||
{
|
{
|
||||||
if ($lot instanceof PartLot) {
|
if ($lot instanceof PartLot) {
|
||||||
$lot_id = $lot->getID();
|
$lot_id = $lot->getID();
|
||||||
} elseif (is_int($lot)) {
|
|
||||||
$lot_id = $lot;
|
|
||||||
} else {
|
} else {
|
||||||
throw new \InvalidArgumentException('The given lot must be an instance of PartLot or an ID of a PartLot!');
|
$lot_id = $lot;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($lot_id === null) {
|
||||||
|
throw new \InvalidArgumentException('The given lot must have an ID!');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->withdraw_amounts[$lot_id] = $amount;
|
$this->withdraw_amounts[$lot_id] = $amount;
|
||||||
|
|
@ -296,7 +304,7 @@ final class ProjectBuildRequest
|
||||||
* @param bool $dont_check_quantity
|
* @param bool $dont_check_quantity
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function setDontCheckQuantity(bool $dont_check_quantity): ProjectBuildRequest
|
public function setDontCheckQuantity(bool $dont_check_quantity): self
|
||||||
{
|
{
|
||||||
$this->dont_check_quantity = $dont_check_quantity;
|
$this->dont_check_quantity = $dont_check_quantity;
|
||||||
return $this;
|
return $this;
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,6 @@ class DBElementRepository extends EntityRepository
|
||||||
{
|
{
|
||||||
$reflection = new ReflectionClass($element::class);
|
$reflection = new ReflectionClass($element::class);
|
||||||
$property = $reflection->getProperty($field);
|
$property = $reflection->getProperty($field);
|
||||||
$property->setAccessible(true);
|
|
||||||
$property->setValue($element, $new_value);
|
$property->setValue($element, $new_value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||||
namespace App\Services\AI;
|
namespace App\Services\AI;
|
||||||
|
|
||||||
use App\Settings\AISettings\LMStudioSettings;
|
use App\Settings\AISettings\LMStudioSettings;
|
||||||
|
use App\Settings\AISettings\OllamaSettings;
|
||||||
use App\Settings\AISettings\OpenRouterSettings;
|
use App\Settings\AISettings\OpenRouterSettings;
|
||||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
@ -32,6 +33,7 @@ enum AIPlatforms: string implements TranslatableInterface
|
||||||
{
|
{
|
||||||
case OPENROUTER = 'openrouter';
|
case OPENROUTER = 'openrouter';
|
||||||
case LMSTUDIO = 'lmstudio';
|
case LMSTUDIO = 'lmstudio';
|
||||||
|
case OLLAMA = 'ollama';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name attribute of the service tag for this platform, which is used to register the platform in the AIPlatformRegistry
|
* Returns the name attribute of the service tag for this platform, which is used to register the platform in the AIPlatformRegistry
|
||||||
|
|
@ -52,6 +54,7 @@ enum AIPlatforms: string implements TranslatableInterface
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
self::LMSTUDIO => LMStudioSettings::class,
|
self::LMSTUDIO => LMStudioSettings::class,
|
||||||
self::OPENROUTER => OpenRouterSettings::class,
|
self::OPENROUTER => OpenRouterSettings::class,
|
||||||
|
self::OLLAMA => OllamaSettings::class,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -543,8 +543,10 @@ class AttachmentSubmitHandler
|
||||||
return $attachment;
|
return $attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$guessed_mime_type = $this->mimeTypes->guessMimeType($path);
|
||||||
|
|
||||||
//Check if the file is an SVG
|
//Check if the file is an SVG
|
||||||
if ($attachment->getExtension() === "svg") {
|
if ($guessed_mime_type === "image/svg+xml" || $attachment->getExtension() === "svg") {
|
||||||
$this->SVGSanitizer->sanitizeFile($path);
|
$this->SVGSanitizer->sanitizeFile($path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,13 +29,13 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for validating BOM import data with comprehensive validation rules
|
* Service for validating BOM import data with comprehensive validation rules
|
||||||
* and user-friendly error messages.
|
* and user-friendly error messages. The results are not HTML safe, and must be escaped before display!
|
||||||
*/
|
*/
|
||||||
class BOMValidationService
|
readonly class BOMValidationService
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
private readonly TranslatorInterface $translator
|
private TranslatorInterface $translator
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,6 @@ trait PKImportHelperTrait
|
||||||
|
|
||||||
$reflectionClass = new \ReflectionClass($entity);
|
$reflectionClass = new \ReflectionClass($entity);
|
||||||
$property = $reflectionClass->getProperty('addedDate');
|
$property = $reflectionClass->getProperty('addedDate');
|
||||||
$property->setAccessible(true);
|
|
||||||
$property->setValue($entity, $date);
|
$property->setValue($entity, $date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ final class DTOJsonSchemaConverter
|
||||||
public function getJSONSchema(): array
|
public function getJSONSchema(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => 'clock',
|
'name' => 'part_detail',
|
||||||
'strict' => true,
|
'strict' => true,
|
||||||
'schema' => [
|
'schema' => [
|
||||||
'type' => 'object',
|
'type' => 'object',
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,10 @@ final class AIWebProvider implements InfoProviderInterface
|
||||||
try {
|
try {
|
||||||
$aiPlatform = $this->AIPlatformRegistry->getPlatform($this->settings->platform ?? throw new \RuntimeException('No AI platform selected') );
|
$aiPlatform = $this->AIPlatformRegistry->getPlatform($this->settings->platform ?? throw new \RuntimeException('No AI platform selected') );
|
||||||
|
|
||||||
|
// AI inference can take much longer than PHP's default max_execution_time (typically 30s).
|
||||||
|
// The HTTP client timeout already enforces the configured limit; disable PHP's constraint here.
|
||||||
|
set_time_limit(0);
|
||||||
|
|
||||||
//'openai/gpt-5-mini'
|
//'openai/gpt-5-mini'
|
||||||
$result = $aiPlatform->invoke($this->settings->model ?? throw new \RuntimeException('No model selected'), $input, [
|
$result = $aiPlatform->invoke($this->settings->model ?? throw new \RuntimeException('No model selected'), $input, [
|
||||||
'response_format' => [
|
'response_format' => [
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,14 @@ class AISettings
|
||||||
{
|
{
|
||||||
use SettingsTrait;
|
use SettingsTrait;
|
||||||
|
|
||||||
|
public const TIMEOUT_LIMIT = 600;
|
||||||
|
|
||||||
#[EmbeddedSettings]
|
#[EmbeddedSettings]
|
||||||
public ?OpenRouterSettings $openRouter = null;
|
public ?OpenRouterSettings $openRouter = null;
|
||||||
|
|
||||||
#[EmbeddedSettings]
|
#[EmbeddedSettings]
|
||||||
public ?LMStudioSettings $lmstudio = null;
|
public ?LMStudioSettings $lmstudio = null;
|
||||||
|
|
||||||
|
#[EmbeddedSettings]
|
||||||
|
public ?OllamaSettings $ollama = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,16 +23,17 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Settings\AISettings;
|
namespace App\Settings\AISettings;
|
||||||
|
|
||||||
use App\Form\Type\APIKeyType;
|
|
||||||
use App\Services\AI\AIPlatformSettingsInterface;
|
use App\Services\AI\AIPlatformSettingsInterface;
|
||||||
use App\Settings\SettingsIcon;
|
use App\Settings\SettingsIcon;
|
||||||
use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
|
use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
|
||||||
use Jbtronics\SettingsBundle\Settings\Settings;
|
use Jbtronics\SettingsBundle\Settings\Settings;
|
||||||
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
|
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
|
||||||
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
|
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\UrlType;
|
use Symfony\Component\Form\Extension\Core\Type\UrlType;
|
||||||
use Symfony\Component\Translation\StaticMessage;
|
use Symfony\Component\Translation\StaticMessage;
|
||||||
use Symfony\Component\Translation\TranslatableMessage as TM;
|
use Symfony\Component\Translation\TranslatableMessage as TM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
#[Settings(name: 'ai_lmstudio', label: new TM("settings.ai.lmstudio"))]
|
#[Settings(name: 'ai_lmstudio', label: new TM("settings.ai.lmstudio"))]
|
||||||
#[SettingsIcon("fa-robot")]
|
#[SettingsIcon("fa-robot")]
|
||||||
|
|
@ -46,6 +47,14 @@ class LMStudioSettings implements AIPlatformSettingsInterface
|
||||||
envVar: "AI_LMSTUDIO_HOSTURL", envVarMode: EnvVarMode::OVERWRITE)]
|
envVar: "AI_LMSTUDIO_HOSTURL", envVarMode: EnvVarMode::OVERWRITE)]
|
||||||
public ?string $hostURL = null;
|
public ?string $hostURL = null;
|
||||||
|
|
||||||
|
#[SettingsParameter(label: new TM("settings.ai.timeout"),
|
||||||
|
description: new TM("settings.ai.timeout.help"),
|
||||||
|
formType: NumberType::class,
|
||||||
|
formOptions: ["scale" => 0, "attr" => ["min" => 1]],
|
||||||
|
)]
|
||||||
|
#[Assert\Range(min: 1, max: AISettings::TIMEOUT_LIMIT)]
|
||||||
|
public int $timeout = 180;
|
||||||
|
|
||||||
public function isAIPlatformEnabled(): bool
|
public function isAIPlatformEnabled(): bool
|
||||||
{
|
{
|
||||||
return $this->hostURL !== null && $this->hostURL !== "";
|
return $this->hostURL !== null && $this->hostURL !== "";
|
||||||
|
|
|
||||||
68
src/Settings/AISettings/OllamaSettings.php
Normal file
68
src/Settings/AISettings/OllamaSettings.php
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\Settings\AISettings;
|
||||||
|
|
||||||
|
use App\Form\Type\APIKeyType;
|
||||||
|
use App\Services\AI\AIPlatformSettingsInterface;
|
||||||
|
use App\Settings\SettingsIcon;
|
||||||
|
use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
|
||||||
|
use Jbtronics\SettingsBundle\Settings\Settings;
|
||||||
|
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
|
||||||
|
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\UrlType;
|
||||||
|
use Symfony\Component\Translation\StaticMessage;
|
||||||
|
use Symfony\Component\Translation\TranslatableMessage as TM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
#[Settings(name: 'ai_ollama', label: new TM("settings.ai.ollama"))]
|
||||||
|
#[SettingsIcon("fa-robot")]
|
||||||
|
class OllamaSettings implements AIPlatformSettingsInterface
|
||||||
|
{
|
||||||
|
use SettingsTrait;
|
||||||
|
|
||||||
|
#[SettingsParameter(label: new TM("settings.ai.ollama.endpoint"),
|
||||||
|
formType: UrlType::class,
|
||||||
|
formOptions: ["attr" => ["placeholder" => new StaticMessage("http://localhost:11434")]],
|
||||||
|
envVar: "AI_OLLAMA_ENDPOINT", envVarMode: EnvVarMode::OVERWRITE)]
|
||||||
|
public ?string $endpoint = null;
|
||||||
|
|
||||||
|
#[SettingsParameter(label: new TM("settings.ai.ollama.apiKey"),
|
||||||
|
formType: APIKeyType::class,
|
||||||
|
envVar: "AI_OLLAMA_API_KEY", envVarMode: EnvVarMode::OVERWRITE)]
|
||||||
|
public ?string $apiKey = null;
|
||||||
|
|
||||||
|
#[SettingsParameter(label: new TM("settings.ai.timeout"),
|
||||||
|
description: new TM("settings.ai.timeout.help"),
|
||||||
|
formType: NumberType::class,
|
||||||
|
formOptions: ["scale" => 0, "attr" => ["min" => 1]]
|
||||||
|
)]
|
||||||
|
#[Assert\Range(min: 1, max: AISettings::TIMEOUT_LIMIT)]
|
||||||
|
public int $timeout = 180;
|
||||||
|
|
||||||
|
public function isAIPlatformEnabled(): bool
|
||||||
|
{
|
||||||
|
return $this->endpoint !== null && $this->endpoint !== "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,9 @@ use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
|
||||||
use Jbtronics\SettingsBundle\Settings\Settings;
|
use Jbtronics\SettingsBundle\Settings\Settings;
|
||||||
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
|
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
|
||||||
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
|
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||||
use Symfony\Component\Translation\TranslatableMessage as TM;
|
use Symfony\Component\Translation\TranslatableMessage as TM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
#[Settings(name: 'ai_openrouter', label: new TM("settings.ai.openrouter"), description: "settings.ai.openrouter.help")]
|
#[Settings(name: 'ai_openrouter', label: new TM("settings.ai.openrouter"), description: "settings.ai.openrouter.help")]
|
||||||
#[SettingsIcon("fa-robot")]
|
#[SettingsIcon("fa-robot")]
|
||||||
|
|
@ -43,6 +45,14 @@ class OpenRouterSettings implements AIPlatformSettingsInterface
|
||||||
formOptions: ["help_html" => true], envVar: "AI_OPENROUTER_KEY", envVarMode: EnvVarMode::OVERWRITE)]
|
formOptions: ["help_html" => true], envVar: "AI_OPENROUTER_KEY", envVarMode: EnvVarMode::OVERWRITE)]
|
||||||
public ?string $apiKey = null;
|
public ?string $apiKey = null;
|
||||||
|
|
||||||
|
#[SettingsParameter(label: new TM("settings.ai.timeout"),
|
||||||
|
description: new TM("settings.ai.timeout.help"),
|
||||||
|
formType: NumberType::class,
|
||||||
|
formOptions: ["scale" => 0, "attr" => ["min" => 1]],
|
||||||
|
envVar: "int:AI_OPENROUTER_TIMEOUT", envVarMode: EnvVarMode::OVERWRITE)]
|
||||||
|
#[Assert\Range(min: 1, max: AISettings::TIMEOUT_LIMIT)]
|
||||||
|
public int $timeout = 90;
|
||||||
|
|
||||||
public function isAIPlatformEnabled(): bool
|
public function isAIPlatformEnabled(): bool
|
||||||
{
|
{
|
||||||
return $this->apiKey !== null && $this->apiKey !== "";
|
return $this->apiKey !== null && $this->apiKey !== "";
|
||||||
|
|
|
||||||
12
symfony.lock
12
symfony.lock
|
|
@ -411,6 +411,18 @@
|
||||||
"config/packages/ai_lm_studio_platform.yaml"
|
"config/packages/ai_lm_studio_platform.yaml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"symfony/ai-ollama-platform": {
|
||||||
|
"version": "0.10",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "0.1",
|
||||||
|
"ref": "2f0ac0a8bc59c4e46b47a962a3ad7fe8104457d6"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/ai_ollama_platform.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
"symfony/ai-open-router-platform": {
|
"symfony/ai-open-router-platform": {
|
||||||
"version": "0.8",
|
"version": "0.8",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
<b>{% trans %}log.collection_deleted.deleted{% endtrans %}</b>:
|
<b>{% trans %}log.collection_deleted.deleted{% endtrans %}</b>:
|
||||||
{{ entity_type_label(entry.deletedElementClass) }} #{{ entry.deletedElementID }}
|
{{ type_label(entry.deletedElementClass) }} #{{ entry.deletedElementID }}
|
||||||
{% if entry.oldName is not empty %}
|
{% if entry.oldName is not empty %}
|
||||||
({{ entry.oldName }})
|
({{ entry.oldName }})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans %}log.target{% endtrans %}</td>
|
<td>{% trans %}log.target{% endtrans %}</td>
|
||||||
<td>{{ target_html|raw }}</td>
|
<td>{{ target_html|sanitize_html }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
@ -111,7 +111,7 @@
|
||||||
{% elseif log_entry is instanceof('App\\Entity\\LogSystem\\CollectionElementDeleted') %}
|
{% elseif log_entry is instanceof('App\\Entity\\LogSystem\\CollectionElementDeleted') %}
|
||||||
{% include "log_system/details/_extra_collection_element_deleted.html.twig" %}
|
{% include "log_system/details/_extra_collection_element_deleted.html.twig" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ extra_html | raw }}
|
{{ extra_html | sanitize_html }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -15,3 +15,24 @@
|
||||||
{{ form_row(form.partUnit) }}
|
{{ form_row(form.partUnit) }}
|
||||||
{{ form_row(form.partCustomState) }}
|
{{ form_row(form.partCustomState) }}
|
||||||
{{ form_row(form.gtin) }}
|
{{ form_row(form.gtin) }}
|
||||||
|
|
||||||
|
<div class="{{ offset_label }} {{ col_input }} ps-1">
|
||||||
|
<div class="accordion" id="accordionProviderReference">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#collapseProviderReference" aria-expanded="true" aria-controls="collapseProviderReference">
|
||||||
|
<span>{% trans %}part.edit.provider_reference{% endtrans %}</span>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseProviderReference" class="accordion-collapse collapse" data-bs-parent="#accordionProviderReference">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
{% trans %}part.edit.provider_reference.warning{% endtrans %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ form_widget(form.providerReference) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
<a href="{{ part.providerReference.providerUrl }}" rel="noopener">
|
<a href="{{ part.providerReference.providerUrl }}" rel="noopener">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span title="{{ part.providerReference.providerKey }}">{{ info_provider_label(part.providerReference.providerKey)|default(part.providerReference.providerKey) }}</span>: {{ part.providerReference.providerId }}
|
<span title="{{ part.providerReference.providerKey }}">{{ info_provider_label(part.providerReference.providerKey)|default(part.providerReference.providerKey) }}</span>: {{ part.providerReference.providerId }}
|
||||||
<span> ({{ part.providerReference.lastUpdated | format_datetime() }})</span>
|
<span> ({{ part.providerReference.lastUpdated ? (part.providerReference.lastUpdated | format_datetime()) : ("part.info_provider_reference.updated_never"|trans) }})</span>
|
||||||
{% if part.providerReference.providerUrl %}
|
{% if part.providerReference.providerUrl %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<div class="carousel-caption text-white">
|
<div class="carousel-caption text-white">
|
||||||
<div><b>{{ pic.name }}</b></div>
|
<div><b>{{ pic.name }}</b></div>
|
||||||
<div>{% if pic.filename %}({{ pic.filename }}) {% endif %}</div>
|
<div>{% if pic.filename %}({{ pic.filename }}) {% endif %}</div>
|
||||||
<div>{{ entity_type_label(pic.element) }}</div>
|
<div>{{ type_label(pic.element) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
<i>({{ timeTravel | format_datetime('short') }})</i>
|
<i>({{ timeTravel | format_datetime('short') }})</i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.projectBuildPart %}
|
{% if part.projectBuildPart %}
|
||||||
(<i>{{ entity_type_label(part.builtProject) }}</i>: <a class="text-white" href="{{ entity_url(part.builtProject) }}">{{ part.builtProject.name }}</a>)
|
(<i>{{ type_label(part.builtProject) }}</i>: <a class="text-white" href="{{ entity_url(part.builtProject) }}">{{ part.builtProject.name }}</a>)
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
<span class="float-end">
|
<span class="float-end">
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
<p class="mb-2">{% trans %}project.bom_import.validation.errors.description{% endtrans %}</p>
|
<p class="mb-2">{% trans %}project.bom_import.validation.errors.description{% endtrans %}</p>
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
{% for error in validation_result.errors %}
|
{% for error in validation_result.errors %}
|
||||||
<li>{{ error|raw }}</li>
|
<li>{{ error }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
<p class="mb-2">{% trans %}project.bom_import.validation.warnings.description{% endtrans %}</p>
|
<p class="mb-2">{% trans %}project.bom_import.validation.warnings.description{% endtrans %}</p>
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
{% for warning in validation_result.warnings %}
|
{% for warning in validation_result.warnings %}
|
||||||
<li>{{ warning|raw }}</li>
|
<li>{{ warning }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
<h4><i class="fa-solid fa-info-circle fa-fw"></i> {% trans %}project.bom_import.validation.info.title{% endtrans %}</h4>
|
<h4><i class="fa-solid fa-info-circle fa-fw"></i> {% trans %}project.bom_import.validation.info.title{% endtrans %}</h4>
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
{% for info in validation_result.info %}
|
{% for info in validation_result.info %}
|
||||||
<li>{{ info|raw }}</li>
|
<li>{{ info }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -139,21 +139,21 @@
|
||||||
{% if line_result.errors is not empty %}
|
{% if line_result.errors is not empty %}
|
||||||
<div class="text-danger">
|
<div class="text-danger">
|
||||||
{% for error in line_result.errors %}
|
{% for error in line_result.errors %}
|
||||||
<div><i class="fa-solid fa-exclamation-triangle fa-fw"></i> {{ error|raw }}</div>
|
<div><i class="fa-solid fa-exclamation-triangle fa-fw"></i> {{ error }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if line_result.warnings is not empty %}
|
{% if line_result.warnings is not empty %}
|
||||||
<div class="text-warning">
|
<div class="text-warning">
|
||||||
{% for warning in line_result.warnings %}
|
{% for warning in line_result.warnings %}
|
||||||
<div><i class="fa-solid fa-exclamation-circle fa-fw"></i> {{ warning|raw }}</div>
|
<div><i class="fa-solid fa-exclamation-circle fa-fw"></i> {{ warning }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if line_result.info is not empty %}
|
{% if line_result.info is not empty %}
|
||||||
<div class="text-info">
|
<div class="text-info">
|
||||||
{% for info in line_result.info %}
|
{% for info in line_result.info %}
|
||||||
<div><i class="fa-solid fa-info-circle fa-fw"></i> {{ info|raw }}</div>
|
<div><i class="fa-solid fa-info-circle fa-fw"></i> {{ info }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,8 @@
|
||||||
<script nonce="{{ csp_nonce('script') }}">
|
<script nonce="{{ csp_nonce('script') }}">
|
||||||
// Function to initialize the field mapping page
|
// Function to initialize the field mapping page
|
||||||
function initializeFieldMapping() {
|
function initializeFieldMapping() {
|
||||||
const suggestions = {{ suggested_mapping|json_encode|raw }};
|
const suggestions = JSON.parse("{{ suggested_mapping|json_encode|escape('js')}}");
|
||||||
const fieldNameMapping = {{ field_name_mapping|json_encode|raw }};
|
const fieldNameMapping = JSON.parse("{{ field_name_mapping|json_encode|escape('js') }}");
|
||||||
|
|
||||||
Object.keys(suggestions).forEach(function(field) {
|
Object.keys(suggestions).forEach(function(field) {
|
||||||
// Use the sanitized field name from the server-side mapping
|
// Use the sanitized field name from the server-side mapping
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ use App\Doctrine\Functions\SiValueSort;
|
||||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||||
use Doctrine\DBAL\Platforms\SQLitePlatform;
|
use Doctrine\DBAL\Platforms\SQLitePlatform;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
|
||||||
final class SiValueSortTest extends AbstractDoctrineFunctionTestCase
|
final class SiValueSortTest extends AbstractDoctrineFunctionTestCase
|
||||||
{
|
{
|
||||||
|
|
@ -71,9 +72,7 @@ final class SiValueSortTest extends AbstractDoctrineFunctionTestCase
|
||||||
$this->assertSame('SI_VALUE(part_name)', $sql);
|
$this->assertSame('SI_VALUE(part_name)', $sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[DataProvider('sqliteSiValueProvider')]
|
||||||
* @dataProvider sqliteSiValueProvider
|
|
||||||
*/
|
|
||||||
public function testSqliteSiValue(?string $input, ?float $expected): void
|
public function testSqliteSiValue(?string $input, ?float $expected): void
|
||||||
{
|
{
|
||||||
$result = SiValueSort::sqliteSiValue($input);
|
$result = SiValueSort::sqliteSiValue($input);
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,6 @@ final class AttachmentTest extends TestCase
|
||||||
{
|
{
|
||||||
$reflection = new ReflectionClass($object);
|
$reflection = new ReflectionClass($object);
|
||||||
$reflection_property = $reflection->getProperty($property);
|
$reflection_property = $reflection->getProperty($property);
|
||||||
$reflection_property->setAccessible(true);
|
|
||||||
$reflection_property->setValue($object, $value);
|
$reflection_property->setValue($object, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,12 @@ final class ProjectBuildRequestTest extends TestCase
|
||||||
|
|
||||||
$part2->setName('Part 2');
|
$part2->setName('Part 2');
|
||||||
$part2->setPartUnit($float_unit);
|
$part2->setPartUnit($float_unit);
|
||||||
$this->lot2 = new PartLot();
|
$this->lot2 = new class extends PartLot {
|
||||||
|
public function getID(): ?int
|
||||||
|
{
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
};;
|
||||||
$part2->addPartLot($this->lot2);
|
$part2->addPartLot($this->lot2);
|
||||||
$this->lot2->setAmount(2.5);
|
$this->lot2->setAmount(2.5);
|
||||||
$this->lot2->setDescription('Lot 2');
|
$this->lot2->setDescription('Lot 2');
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue