diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index c80b3ccc..7b055653 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -43,7 +43,7 @@ jobs: run: ./bin/console lint:xliff translations - name: Check dependencies for security - uses: symfonycorp/security-checker-action@v2 + uses: symfonycorp/security-checker-action@v3 - name: Check doctrine mapping run: ./bin/console doctrine:schema:validate --skip-sync -vvv --no-interaction diff --git a/assets/controllers/elements/collection_type_controller.js b/assets/controllers/elements/collection_type_controller.js index 4c804534..607984a2 100644 --- a/assets/controllers/elements/collection_type_controller.js +++ b/assets/controllers/elements/collection_type_controller.js @@ -103,12 +103,20 @@ export default class extends Controller { } deleteElement(event) { - bootbox.confirm(this.deleteMessageValue, (result) => { - if(result) { - const target = event.target; - //Remove the row element from the table - target.closest("tr").remove(); - } - }); + const del = () => { + const target = event.target; + //Remove the row element from the table + target.closest("tr").remove(); + } + + if(this.deleteMessageValue) { + bootbox.confirm(this.deleteMessageValue, (result) => { + if (result) { + del(); + } + }); + } else { + del(); + } } } \ No newline at end of file diff --git a/assets/controllers/elements/datatables/datatables_controller.js b/assets/controllers/elements/datatables/datatables_controller.js index 25763570..a2dc45fe 100644 --- a/assets/controllers/elements/datatables/datatables_controller.js +++ b/assets/controllers/elements/datatables/datatables_controller.js @@ -4,8 +4,8 @@ import {Controller} from "@hotwired/stimulus"; import 'datatables.net-bs5/css/dataTables.bootstrap5.css' import 'datatables.net-buttons-bs5/css/buttons.bootstrap5.css' import 'datatables.net-fixedheader-bs5/css/fixedHeader.bootstrap5.css' -import 'datatables.net-select-bs5/css/select.bootstrap5.css' import 'datatables.net-responsive-bs5/css/responsive.bootstrap5.css'; +import 'datatables.net-select-bs5/css/select.bootstrap5.css'; //JS import 'datatables.net-bs5'; @@ -23,9 +23,33 @@ export default class extends Controller { static targets = ['dt']; + static values = { + stateSaveTag: String + }; + /** The datatable instance associated with this controller instance */ _dt; + getStateSaveKey() { + let key = 'dt_state_' + + if(this.stateSaveTagValue) { //If a tag is provided, use it to store the state + key += this.stateSaveTagValue; + } else { //Otherwise generate one from the current url + key += window.location.pathname; + } + + return key; + } + + stateSaveCallback(settings, data) { + localStorage.setItem( this.getStateSaveKey(), JSON.stringify(data) ); + } + + stateLoadCallback(settings) { + return JSON.parse( localStorage.getItem(this.getStateSaveKey()) ); + } + connect() { //$($.fn.DataTable.tables()).DataTable().fixedHeader.disable(); //$($.fn.DataTable.tables()).DataTable().destroy(); @@ -39,23 +63,36 @@ export default class extends Controller { settings.url = this.element.dataset.dtUrl; + let options = { + colReorder: true, + responsive: true, + fixedHeader: { + header: $(window).width() >= 768, //Only enable fixedHeaders on devices with big screen. Fixes scrolling issues on smartphones. + headerOffset: $("#navbar").height() + }, + buttons: [{ + "extend": 'colvis', + 'className': 'mr-2 btn-light', + 'columns': ':not(.no-colvis)', + "text": "" + }], + + + rowCallback: this._rowCallback.bind(this), + stateSave: true, + stateSaveCallback: this.stateSaveCallback.bind(this), + stateLoadCallback: this.stateLoadCallback.bind(this), + }; + + if(this.isSelectable()) { + options.select = { + style: 'multi+shift', + selector: 'td.select-checkbox' + }; + } + //@ts-ignore - const promise = $(this.dtTarget).initDataTables(settings, - { - colReorder: true, - responsive: true, - fixedHeader: { - header: $(window).width() >= 768, //Only enable fixedHeaders on devices with big screen. Fixes scrolling issues on smartphones. - headerOffset: $("#navbar").height() - }, - buttons: [{ - "extend": 'colvis', - 'className': 'mr-2 btn-light', - "text": "" - }], - select: this.isSelectable(), - rowCallback: this._rowCallback.bind(this), - }) + const promise = $(this.dtTarget).initDataTables(settings, options) //Register error handler .catch(err => { console.error("Error initializing datatables: " + err); @@ -78,46 +115,6 @@ export default class extends Controller { promise.then(this._afterLoaded.bind(this)); - - //Register links. - /*promise.then(function() { - - //Set the correct title in the table. - let title = $('#part-card-header-src'); - $('#part-card-header').html(title.html()); - $(document).trigger('ajaxUI:dt_loaded'); - - - if($table.data('part_table')) { - //@ts-ignore - $('#dt').on( 'select.dt deselect.dt', function ( e, dt, items ) { - let selected_elements = dt.rows({selected: true}); - let count = selected_elements.count(); - - if(count > 0) { - $('#select_panel').removeClass('d-none'); - } else { - $('#select_panel').addClass('d-none'); - } - - $('#select_count').text(count); - - let selected_ids_string = selected_elements.data().map(function(value, index) { - return value['id']; } - ).join(","); - - $('#select_ids').val(selected_ids_string); - - } ); - } - - //Attach event listener to update links after new page selection: - $('#dt').on('draw.dt column-visibility.dt', function() { - //ajaxUI.registerLinks(); - $(document).trigger('ajaxUI:dt_loaded'); - }); - });*/ - console.debug('Datatables inited.'); } diff --git a/assets/controllers/elements/select_multiple_controller.js b/assets/controllers/elements/select_multiple_controller.js new file mode 100644 index 00000000..25291b3e --- /dev/null +++ b/assets/controllers/elements/select_multiple_controller.js @@ -0,0 +1,15 @@ +import {Controller} from "@hotwired/stimulus"; +import TomSelect from "tom-select"; + +export default class extends Controller { + _tomSelect; + + connect() { + this._tomSelect = new TomSelect(this.element, { + maxItems: 1000, + allowEmptyOption: true, + plugins: ['remove_button'], + }); + } + +} \ No newline at end of file diff --git a/assets/controllers/elements/selectpicker_controller.js b/assets/controllers/elements/selectpicker_controller.js index 182a2e35..aac08719 100644 --- a/assets/controllers/elements/selectpicker_controller.js +++ b/assets/controllers/elements/selectpicker_controller.js @@ -2,7 +2,7 @@ const bootstrap = window.bootstrap = require('bootstrap'); // without this boots require('bootstrap-select/js/bootstrap-select'); // we have to manually require the working js file import {Controller} from "@hotwired/stimulus"; -import "bootstrap-select/dist/css/bootstrap-select.css"; +import "../../css/lib/boostrap-select.css"; import "../../css/selectpicker_extensions.css"; export default class extends Controller { diff --git a/assets/controllers/elements/tagsinput_controller.js b/assets/controllers/elements/tagsinput_controller.js index 4454089f..88f1626a 100644 --- a/assets/controllers/elements/tagsinput_controller.js +++ b/assets/controllers/elements/tagsinput_controller.js @@ -8,7 +8,6 @@ export default class extends Controller { _tomSelect; connect() { - let settings = { plugins: { remove_button:{ diff --git a/assets/controllers/filters/number_constraint_controller.js b/assets/controllers/filters/number_constraint_controller.js new file mode 100644 index 00000000..2a432426 --- /dev/null +++ b/assets/controllers/filters/number_constraint_controller.js @@ -0,0 +1,26 @@ +import {Controller} from "@hotwired/stimulus"; + +export default class extends Controller { + + static targets = ["operator", "thingsToHide"]; + + connect() { + this.update(); + } + + /** + * Updates the visibility state of the value2 input, based on the operator selection. + */ + update() + { + const two_element_values = [ + "BETWEEN", + 'RANGE_IN_RANGE', + 'RANGE_INTERSECT_RANGE' + ]; + + for (const thingToHide of this.thingsToHideTargets) { + thingToHide.classList.toggle("d-none", !two_element_values.includes(this.operatorTarget.value)); + } + } +} \ No newline at end of file diff --git a/assets/controllers/helpers/form_cleanup_controller.js b/assets/controllers/helpers/form_cleanup_controller.js new file mode 100644 index 00000000..22f4fdb5 --- /dev/null +++ b/assets/controllers/helpers/form_cleanup_controller.js @@ -0,0 +1,48 @@ +import {Controller} from "@hotwired/stimulus"; + +/** + * Purpose of this controller is to clean up the form before it is finally submitted. This means empty fields get disabled, so they are not submitted. + * This is especially useful for GET forms, to prevent very long URLs + */ +export default class extends Controller { + + /** + * Call during the submit event of the form. This will disable all empty fields, so they are not submitted. + * @param event + */ + submit(event) { + /** Find the form this event belongs to */ + /** @type {HTMLFormElement} */ + const form = event.target.closest('form'); + + for(const element of form.elements) { + if(! element.value) { + element.disabled = true; + } + + //Workaround for tristate checkboxes which use a hidden field to store the value + if ((element.type === 'hidden' || element.type === 'checkbox') && element.value === 'indeterminate') { + element.disabled = true; + } + } + } + + /** + * Submits the form with all form elements disabled, so they are not submitted. This is useful for GET forms, to reset the form to not filled state. + * @param event + */ + clearAll(event) + { + const form = event.target.closest('form'); + for(const element of form.elements) { + // Do not clear elements with data-no-clear attribute + if(element.dataset.noClear) { + continue; + } + + element.disabled = true; + } + + form.submit(); + } +} \ No newline at end of file diff --git a/assets/controllers/pages/latex_preview_controller.js b/assets/controllers/pages/latex_preview_controller.js new file mode 100644 index 00000000..8c1279f1 --- /dev/null +++ b/assets/controllers/pages/latex_preview_controller.js @@ -0,0 +1,21 @@ +import {Controller} from "@hotwired/stimulus"; +import katex from "katex"; +import "katex/dist/katex.css"; + +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + static targets = ["input", "preview"]; + + updatePreview() + { + katex.render(this.inputTarget.value, this.previewTarget, { + throwOnError: false, + }); + } + + connect() + { + this.updatePreview(); + this.inputTarget.addEventListener('input', this.updatePreview.bind(this)); + } +} \ No newline at end of file diff --git a/assets/controllers/pages/parameters_autocomplete_controller.js b/assets/controllers/pages/parameters_autocomplete_controller.js new file mode 100644 index 00000000..fce678ca --- /dev/null +++ b/assets/controllers/pages/parameters_autocomplete_controller.js @@ -0,0 +1,99 @@ +import {Controller} from "@hotwired/stimulus"; +import TomSelect from "tom-select"; +import katex from "katex"; +import "katex/dist/katex.css"; + +/* stimulusFetch: 'lazy' */ +export default class extends Controller +{ + static values = { + url: String, + } + + static targets = ["name", "symbol", "unit"] + + onItemAdd(value, item) { + //Retrieve the unit and symbol from the item + const symbol = item.dataset.symbol; + const unit = item.dataset.unit; + + if (this.symbolTarget && symbol !== undefined) { + this.symbolTarget.value = symbol; + //Trigger input event to update the preview + this.symbolTarget.dispatchEvent(new Event('input')); + } + if (this.unitTarget && unit !== undefined) { + this.unitTarget.value = unit; + //Trigger input event to update the preview + this.unitTarget.dispatchEvent(new Event('input')); + } + } + + connect() { + const settings = { + plugins: { + clear_button:{} + }, + persistent: false, + maxItems: 1, + //This a an ugly solution to disable the delimiter parsing of the TomSelect plugin + delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING', + createOnBlur: true, + create: true, + searchField: "name", + //labelField: "name", + valueField: "name", + onItemAdd: this.onItemAdd.bind(this), + render: { + option: (data, escape) => { + let tmp = '
' + + '' + escape(data.name) + '
'; + + if (data.symbol) { + tmp += '' + katex.renderToString(data.symbol) + '' + } + if (data.unit) { + tmp += '' + katex.renderToString('[' + data.unit + ']') + '' + } + + + //+ '' + escape(data.unit) + '' + tmp += '
'; + + return tmp; + }, + item: (data, escape) => { + //We use the item to transfert data to the onItemAdd function using data attributes + const element = document.createElement('div'); + element.innerText = data.name; + if(data.unit !== undefined) { + element.dataset.unit = data.unit; + } + if (data.symbol !== undefined) { + element.dataset.symbol = data.symbol; + } + + return element.outerHTML; + } + } + }; + + if(this.urlValue) { + const base_url = this.urlValue; + settings.load = (query, callback) => { + const url = base_url.replace('__QUERY__', encodeURIComponent(query)); + + fetch(url) + .then(response => response.json()) + .then(json => { + //const data = json.map(x => {return {"value": x, "text": x}}); + callback(json); + }).catch(()=>{ + callback(); + }); + } + } + + this._tomSelect = new TomSelect(this.nameTarget, settings); + } +} \ No newline at end of file diff --git a/assets/css/app.css b/assets/css/app.css index 01eecf10..33891bc8 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -754,6 +754,12 @@ div.dataTables_wrapper div.dataTables_info { display: inline-flex; } +/** Fix datatables select-checkbox position */ + +table.dataTable tr.selected td.select-checkbox:after, table.dataTable tr.selected th.select-checkbox:after { + margin-top: -28px !important; +} + /****************************************** * Typeahead menu *******************************************/ diff --git a/assets/css/lib/boostrap-select.css b/assets/css/lib/boostrap-select.css new file mode 100644 index 00000000..7ae65947 --- /dev/null +++ b/assets/css/lib/boostrap-select.css @@ -0,0 +1,491 @@ +/*! + * Bootstrap-select v1.14.0-beta3 (https://developer.snapappointments.com/bootstrap-select) + * + * Copyright 2012-2022 SnapAppointments, LLC + * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) + */ + +/** + * Modified by Jan Böhmer 2022, to improve styling with BS5: + * - Keep border around selectpicker in a form-control + */ + +@-webkit-keyframes bs-notify-fadeOut { + 0% { + opacity: 0.9; + } + 100% { + opacity: 0; + } +} +@-o-keyframes bs-notify-fadeOut { + 0% { + opacity: 0.9; + } + 100% { + opacity: 0; + } +} +@keyframes bs-notify-fadeOut { + 0% { + opacity: 0.9; + } + 100% { + opacity: 0; + } +} +select.bs-select-hidden, +.bootstrap-select > select.bs-select-hidden, +select.selectpicker { + display: none !important; +} +.bootstrap-select { + width: 220px; + vertical-align: middle; +} +.bootstrap-select > .dropdown-toggle { + position: relative; + width: 100%; + text-align: right; + white-space: nowrap; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} +.bootstrap-select > .dropdown-toggle:after { + margin-top: -1px; +} +.bootstrap-select > .dropdown-toggle.bs-placeholder, +.bootstrap-select > .dropdown-toggle.bs-placeholder:hover, +.bootstrap-select > .dropdown-toggle.bs-placeholder:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder:active { + color: #999; +} +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary:hover, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary:hover, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success:hover, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger:hover, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info:hover, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark:hover, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary:active, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary:active, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success:active, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger:active, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info:active, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark:active { + color: rgba(255, 255, 255, 0.5); +} +.bootstrap-select > select { + position: absolute !important; + bottom: 0; + left: 50%; + display: block !important; + width: 0.5px !important; + height: 100% !important; + padding: 0 !important; + opacity: 0 !important; + border: none; + z-index: 0 !important; +} +.bootstrap-select > select.mobile-device { + top: 0; + left: 0; + display: block !important; + width: 100% !important; + z-index: 2 !important; +} +.has-error .bootstrap-select .dropdown-toggle, +.error .bootstrap-select .dropdown-toggle, +.bootstrap-select.is-invalid .dropdown-toggle, +.was-validated .bootstrap-select select:invalid + .dropdown-toggle { + border-color: #b94a48; +} +.bootstrap-select.is-valid .dropdown-toggle, +.was-validated .bootstrap-select select:valid + .dropdown-toggle { + border-color: #28a745; +} +.bootstrap-select.fit-width { + width: auto !important; +} +.bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) { + width: 220px; +} +.bootstrap-select > select.mobile-device:focus + .dropdown-toggle, +.bootstrap-select .dropdown-toggle:focus { + outline: thin dotted #333333 !important; + outline: 5px auto -webkit-focus-ring-color !important; + outline-offset: -2px; +} +.bootstrap-select.form-control { + margin-bottom: 0; + padding: 0; + /*border: none;*/ + height: auto; +} +:not(.input-group) > .bootstrap-select.form-control:not([class*="col-"]) { + width: 100%; +} +.bootstrap-select.form-control.input-group-btn { + float: none; + z-index: auto; +} +.form-inline .bootstrap-select, +.form-inline .bootstrap-select.form-control:not([class*="col-"]) { + width: auto; +} +.bootstrap-select:not(.input-group-btn), +.bootstrap-select[class*="col-"] { + float: none; + display: inline-block; + margin-left: 0; +} +.bootstrap-select.dropdown-menu-right, +.bootstrap-select[class*="col-"].dropdown-menu-right, +.row .bootstrap-select[class*="col-"].dropdown-menu-right { + float: right; +} +.form-inline .bootstrap-select, +.form-horizontal .bootstrap-select, +.form-group .bootstrap-select { + margin-bottom: 0; +} +.form-group-lg .bootstrap-select.form-control, +.form-group-sm .bootstrap-select.form-control { + padding: 0; +} +.form-group-lg .bootstrap-select.form-control .dropdown-toggle, +.form-group-sm .bootstrap-select.form-control .dropdown-toggle { + height: 100%; + font-size: inherit; + line-height: inherit; + border-radius: inherit; +} +.bootstrap-select.form-control-sm .dropdown-toggle, +.bootstrap-select.form-control-lg .dropdown-toggle { + font-size: inherit; + line-height: inherit; + border-radius: inherit; +} +.bootstrap-select.form-control-sm .dropdown-toggle { + padding: 0.25rem 0.5rem; +} +.bootstrap-select.form-control-lg .dropdown-toggle { + padding: 0.5rem 1rem; +} +.form-inline .bootstrap-select .form-control { + width: 100%; +} +.bootstrap-select.disabled, +.bootstrap-select > .disabled { + cursor: not-allowed; +} +.bootstrap-select.disabled:focus, +.bootstrap-select > .disabled:focus { + outline: none !important; +} +.bootstrap-select.bs-container { + position: absolute; + top: 0; + left: 0; + height: 0 !important; + padding: 0 !important; +} +.bootstrap-select.bs-container .dropdown-menu { + z-index: 1060; +} +.bootstrap-select .dropdown-toggle .filter-option { + position: static; + top: 0; + left: 0; + float: left; + height: 100%; + width: 100%; + text-align: left; + overflow: hidden; + -webkit-box-flex: 0; + -webkit-flex: 0 1 auto; + -ms-flex: 0 1 auto; + flex: 0 1 auto; +} +.bs3.bootstrap-select .dropdown-toggle .filter-option { + padding-right: inherit; +} +.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option { + position: absolute; + padding-top: inherit; + padding-bottom: inherit; + padding-left: inherit; + float: none; +} +.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option .filter-option-inner { + padding-right: inherit; +} +.bootstrap-select .dropdown-toggle .filter-option-inner-inner { + overflow: hidden; +} +.bootstrap-select .dropdown-toggle .filter-expand { + width: 0 !important; + float: left; + opacity: 0 !important; + overflow: hidden; +} +.bootstrap-select .dropdown-toggle .caret { + position: absolute; + top: 50%; + right: 12px; + margin-top: -2px; + vertical-align: middle; +} +.bootstrap-select .dropdown-toggle .bs-select-clear-selected { + position: relative; + display: block; + margin-right: 5px; + text-align: center; +} +.bs3.bootstrap-select .dropdown-toggle .bs-select-clear-selected { + padding-right: inherit; +} +.bootstrap-select .dropdown-toggle .bs-select-clear-selected span { + position: relative; + top: -webkit-calc(((-1em / 1.5) + 1ex) / 2); + top: calc(((-1em / 1.5) + 1ex) / 2); + pointer-events: none; +} +.bs3.bootstrap-select .dropdown-toggle .bs-select-clear-selected span { + top: auto; +} +.bootstrap-select .dropdown-toggle.bs-placeholder .bs-select-clear-selected { + display: none; +} +.input-group .bootstrap-select.form-control .dropdown-toggle { + border-radius: inherit; +} +.bootstrap-select[class*="col-"] .dropdown-toggle { + width: 100%; +} +.bootstrap-select .dropdown-menu { + min-width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.bootstrap-select .dropdown-menu > .inner:focus { + outline: none !important; +} +.bootstrap-select .dropdown-menu.inner { + position: static; + float: none; + border: 0; + padding: 0; + margin: 0; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; +} +.bootstrap-select .dropdown-menu li { + position: relative; +} +.bootstrap-select .dropdown-menu li.active small { + color: rgba(255, 255, 255, 0.5) !important; +} +.bootstrap-select .dropdown-menu li.disabled a { + cursor: not-allowed; +} +.bootstrap-select .dropdown-menu li a { + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.bootstrap-select .dropdown-menu li a.opt { + position: relative; + padding-left: 2.25em; +} +.bootstrap-select .dropdown-menu li a span.check-mark { + display: none; +} +.bootstrap-select .dropdown-menu li a span.text { + display: inline-block; +} +.bootstrap-select .dropdown-menu li small { + padding-left: 0.5em; +} +.bootstrap-select .dropdown-menu .notify { + position: absolute; + bottom: 5px; + width: 96%; + margin: 0 2%; + min-height: 26px; + padding: 3px 5px; + background: #f5f5f5; + border: 1px solid #e3e3e3; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + pointer-events: none; + opacity: 0.9; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.bootstrap-select .dropdown-menu .notify.fadeOut { + -webkit-animation: 300ms linear 750ms forwards bs-notify-fadeOut; + -o-animation: 300ms linear 750ms forwards bs-notify-fadeOut; + animation: 300ms linear 750ms forwards bs-notify-fadeOut; +} +.bootstrap-select .no-results { + padding: 3px; + background: #f5f5f5; + margin: 0 5px; + white-space: nowrap; +} +.bootstrap-select.fit-width .dropdown-toggle .filter-option { + position: static; + display: inline; + padding: 0; +} +.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner, +.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner-inner { + display: inline; +} +.bootstrap-select.fit-width .dropdown-toggle .bs-caret:before { + content: '\00a0'; +} +.bootstrap-select.fit-width .dropdown-toggle .caret { + position: static; + top: auto; + margin-top: -1px; +} +.bootstrap-select.show-tick .dropdown-menu .selected span.check-mark { + position: absolute; + display: inline-block; + right: 15px; + top: 5px; +} +.bootstrap-select.show-tick .dropdown-menu li a span.text { + margin-right: 34px; +} +.bootstrap-select .bs-ok-default:after { + content: ''; + display: block; + width: 0.5em; + height: 1em; + border-style: solid; + border-width: 0 0.26em 0.26em 0; + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + -o-transform: rotate(45deg); + transform: rotate(45deg); +} +.bootstrap-select.show-menu-arrow.open > .dropdown-toggle, +.bootstrap-select.show-menu-arrow.show > .dropdown-toggle { + z-index: 1061; +} +.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:before { + content: ''; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid rgba(204, 204, 204, 0.2); + position: absolute; + bottom: -4px; + left: 9px; + display: none; +} +.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:after { + content: ''; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid white; + position: absolute; + bottom: -4px; + left: 10px; + display: none; +} +.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:before { + bottom: auto; + top: -4px; + border-top: 7px solid rgba(204, 204, 204, 0.2); + border-bottom: 0; +} +.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:after { + bottom: auto; + top: -4px; + border-top: 6px solid white; + border-bottom: 0; +} +.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:before { + right: 12px; + left: auto; +} +.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:after { + right: 13px; + left: auto; +} +.bootstrap-select.show-menu-arrow.open > .dropdown-toggle .filter-option:before, +.bootstrap-select.show-menu-arrow.show > .dropdown-toggle .filter-option:before, +.bootstrap-select.show-menu-arrow.open > .dropdown-toggle .filter-option:after, +.bootstrap-select.show-menu-arrow.show > .dropdown-toggle .filter-option:after { + display: block; +} +.bs-searchbox, +.bs-actionsbox, +.bs-donebutton { + padding: 4px 8px; +} +.bs-actionsbox { + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.bs-actionsbox .btn-group { + display: block; +} +.bs-actionsbox .btn-group button { + width: 50%; +} +.bs-donebutton { + float: left; + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.bs-donebutton .btn-group { + display: block; +} +.bs-donebutton .btn-group button { + width: 100%; +} +.bs-searchbox + .bs-actionsbox { + padding: 0 8px 4px; +} +.bs-searchbox .form-control { + margin-bottom: 0; + width: 100%; + float: none; +} +/*# sourceMappingURL=bootstrap-select.css.map */ \ No newline at end of file diff --git a/assets/js/error_handler.js b/assets/js/error_handler.js index 573a0915..a8c5c683 100644 --- a/assets/js/error_handler.js +++ b/assets/js/error_handler.js @@ -10,6 +10,61 @@ class ErrorHandlerHelper { const content = document.getElementById('content'); content.addEventListener('turbo:before-fetch-response', (event) => this.handleError(event)); + + $(document).ajaxError(this.handleJqueryErrror.bind(this)); + } + + _showAlert(statusText, statusCode, location, responseHTML) + { + //Create error text + const title = statusText + ' (Status ' + statusCode + ')'; + + let trimString = function (string, length) { + return string.length > length ? + string.substring(0, length) + '...' : + string; + }; + + const short_location = trimString(location, 50); + + const alert = bootbox.alert( + { + size: 'large', + message: function() { + let url = location; + let msg = `Error calling ${short_location}.
`; + msg += 'Try to reload the page or contact the administrator if this error persists.'; + + msg += '

' + 'View details' + ""; + msg += "
"; + + return msg; + }, + title: title, + callback: function () { + //Remove blur + $('#content').removeClass('loading-content'); + } + + }); + + alert.init(function (){ + var dstFrame = document.getElementById('error-iframe'); + //@ts-ignore + var dstDoc = dstFrame.contentDocument || dstFrame.contentWindow.document; + dstDoc.write(responseHTML) + dstDoc.close(); + }); + } + + handleJqueryErrror(event, jqXHR, ajaxSettings, thrownError) + { + //Ignore status 422 as this means a symfony validation error occured and we need to show it to user. This is no (unexpected) error. + if (jqXHR.status === 422) { + return; + } + + this._showAlert(jqXHR.statusText, jqXHR.status, ajaxSettings.url, jqXHR.responseText); } handleError(event) { @@ -27,52 +82,10 @@ class ErrorHandlerHelper { } if(fetchResponse.failed) { - //Create error text - let title = response.statusText + ' (Status ' + response.status + ')'; - - /** - switch(response.status) { - case 500: - title = 'Internal Server Error!'; - break; - case 404: - title = "Site not found!"; - break; - case 403: - title = "Permission denied!"; - break; - } **/ - - const alert = bootbox.alert( - { - size: 'large', - message: function() { - let url = fetchResponse.location.toString(); - let msg = `Error calling ${url}. `; - msg += 'Try to reload the page or contact the administrator if this error persists.' - - msg += '

' + 'View details' + ""; - msg += "
"; - - return msg; - }, - title: title, - callback: function () { - //Remove blur - $('#content').removeClass('loading-content'); - } - - }); - - //@ts-ignore - alert.init(function (){ - response.text().then( (html) => { - var dstFrame = document.getElementById('error-iframe'); - //@ts-ignore - var dstDoc = dstFrame.contentDocument || dstFrame.contentWindow.document; - dstDoc.write(html) - dstDoc.close(); - }); + response.text().then(responseHTML => { + this._showAlert(response.statusText, response.status, fetchResponse.location.toString(), responseHTML); + }).catch(err => { + this._showAlert(response.statusText, response.status, fetchResponse.location.toString(), '
' + err + '
'); }); } } diff --git a/assets/js/lib/TristateCheckbox.js b/assets/js/lib/TristateCheckbox.js new file mode 100644 index 00000000..c8ceed0e --- /dev/null +++ b/assets/js/lib/TristateCheckbox.js @@ -0,0 +1,207 @@ +const DEFAULT_OPTIONS = { + true: "true", + false: "false", + null: "indeterminate", +}; + +/** + * A simple tristate checkbox + */ +export default class TristateCheckbox { + + static instances = new Map(); + + /** + * + * @type {null|boolean} + * @private + */ + _state = false; + + /** + * The element representing the checkbox. + * @type {HTMLInputElement} + * @private + */ + _element = null; + + /** + * The hidden input element representing the value of the checkbox + * @type {HTMLInputElement} + * @private + */ + _hiddenInput = null; + + /** + * The values of the checkbox. + * @type {{null: string, true: string, false: string}} + * @private + */ + _options = DEFAULT_OPTIONS; + + /** + * Retrieve the instance of the TristateCheckbox for the given element if already existing, otherwise a new one is created. + * @param element + * @param options + * @return {any} + */ + static getInstance(element, options = {}) + { + if(!TristateCheckbox.instances.has(element)) { + TristateCheckbox.instances.set(element, new TristateCheckbox(element, options)); + } + + return TristateCheckbox.instances.get(element); + } + + /** + * @param {HTMLElement} element + */ + constructor(element, options = {}) + { + if(!element instanceof HTMLInputElement || !(element.tagName === 'INPUT' && element.type === 'checkbox')) { + throw new Error("The given element is not an input checkbox"); + } + + //Apply options + this._options = Object.assign(this._options, options); + + this._element = element; + + //Set the state of our element to the value of the passed input value + this._parseInitialState(); + + //Create a hidden input field to store the value of the checkbox, because this will be always be submitted in the form + this._hiddenInput = document.createElement('input'); + this._hiddenInput.type = 'hidden'; + this._hiddenInput.name = this._element.name; + this._hiddenInput.value = this._element.value; + + //Insert the hidden input field after the checkbox and remove the checkbox from form submission (by removing the name property) + element.after(this._hiddenInput); + this._element.removeAttribute('name'); + + //Do a refresh to set the correct styling of the checkbox + this._refresh(); + + this._element.addEventListener('click', this.click.bind(this)); + } + + /** + * Parse the attributes of the checkbox and set the correct state. + * @private + */ + _parseInitialState() + { + if(this._element.hasAttribute('value')) { + this._state = this._stringToState(this._element.getAttribute('value')); + return; + } + + if(this._element.checked) { + this._state = true; + return; + } + + if(this._element.indeterminate) { + this._state = null; + return; + } + + this._state = false; + } + + _refresh() + { + this._element.indeterminate = this._state === null; + this._element.checked = this._state === true; + //Set the value field of the checkbox and the hidden input to correct value + this._element.value = this._stateToString(this._state); + this._hiddenInput.value = this._stateToString(this._state); + } + + + /** + * Returns the current state of the checkbox. True if checked, false if unchecked, null if indeterminate. + * @return {boolean|null} + */ + get state() { + return this._state; + } + + /** + * Sets the state of the checkbox. True if checked, false if unchecked, null if indeterminate. + * @param state + */ + set state(state) { + this._state = state; + this._refresh(); + } + + /** + * Returns the current state of the checkbox as string, according to the options. + * @return {string} + */ + get stateString() { + return this._stateToString(this._state); + } + + set stateString(string) { + this.state = this._stringToState(string); + this._refresh(); + } + + /** + * @param {boolean|null} state + * @return string + * @private + */ + _stateToString(state) + { + if (this.state === null) { + return this._options.null; + } else if (this.state === true) { + return this._options.true; + } else if (this.state === false) { + return this._options.false; + } + + throw new Error("Invalid state " + state); + } + + /** + * Converts a string to a state according to the options. + * @param string + * @param throwError + * @return {null|boolean} + * @private + */ + _stringToState(string, throwError = true) + { + if (string === this._options.true) { + return true; + } else if (string === this._options.false) { + return false; + } else if (string === this._options.null) { + return null; + } + + if(throwError) { + throw new Error("Invalid state string " + string); + } else { + return null; + } + } + + click() + { + switch (this._state) { + case true: this._state = false; break; + case false: this._state = null; break; + default: this._state = true; break; + } + + this._refresh(); + } + +} \ No newline at end of file diff --git a/assets/js/lib/jquery.tristate.js b/assets/js/lib/jquery.tristate.js deleted file mode 100644 index c1a85d29..00000000 --- a/assets/js/lib/jquery.tristate.js +++ /dev/null @@ -1,213 +0,0 @@ -/*jslint devel: true, bitwise: true, regexp: true, browser: true, confusion: true, unparam: true, eqeq: true, white: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */ -/*globals jQuery */ - -/*! - * Tristate v1.2.1 - * - * Copyright (c) 2013-2017 Martijn W. van der Lee - * Licensed under the MIT. - */ -/* Based on work by: - * Chris Coyier (http://css-tricks.com/indeterminate-checkboxes/) - * - * Tristate checkbox with support features - * pseudo selectors - * val() overwrite - */ - -;(function($, undefined) { - 'use strict'; - - var pluginName = 'tristate', - defaults = { - 'change': undefined, - 'checked': undefined, - 'indeterminate': undefined, - 'init': undefined, - 'reverse': false, - 'state': undefined, - 'unchecked': undefined, - 'value': undefined // one-way only! - }, - valFunction = $.fn.val; - - function Plugin(element, options) { - if($(element).is(':checkbox')) { - this.element = $(element); - this.settings = $.extend( {}, defaults, options ); - this._create(); - } - } - - $.extend(Plugin.prototype, { - _create: function() { - var that = this, - state; - - // Fix for #1 - if (window.navigator.userAgent.indexOf('Trident') >= 0) { - this.element.click(function(e) { - that._change.call(that, e); - that.element.closest('form').change(); - }); - } else { - this.element.change(function(e) { - that._change.call(that, e); - }); - } - - this.settings.checked = this.element.attr('checkedvalue') || this.settings.checked; - this.settings.unchecked = this.element.attr('uncheckedvalue') || this.settings.unchecked; - this.settings.indeterminate = this.element.attr('indeterminatevalue') || this.settings.indeterminate; - - // Initially, set state based on option state or attributes - if (typeof this.settings.state === 'undefined') { - this.settings.state = typeof this.element.attr('indeterminate') !== 'undefined'? null : this.element.is(':checked'); - } - - // If value specified, overwrite with value - if (typeof this.settings.value !== 'undefined') { - state = this._parseValue(this.settings.value); - if (typeof state !== 'undefined') { - this.settings.state = state; - } - } - - this._refresh(this.settings.init); - - return this; - }, - - _change: function(e) { - if (e.isTrigger || !e.hasOwnProperty('which')) { - e.preventDefault(); - } - - switch (this.settings.state) { - case true: this.settings.state = (this.settings.reverse ? false : null); break; - case false: this.settings.state = (this.settings.reverse ? null : true); break; - default: this.settings.state = (this.settings.reverse ? true : false); break; - } - - this._refresh(this.settings.change); - }, - - _refresh: function(callback) { - var value = this.value(); - - this.element.data("vanderlee." + pluginName, value); - - this.element[this.settings.state === null ? 'attr' : 'removeAttr']('indeterminate', 'indeterminate'); - this.element.prop('indeterminate', this.settings.state === null); - this.element.get(0).indeterminate = this.settings.state === null; - - this.element[this.settings.state === true ? 'attr' : 'removeAttr']('checked', true); - this.element.prop('checked', this.settings.state === true); - - if ($.isFunction(callback)) { - callback.call(this.element, this.settings.state, this.value()); - } - }, - - state: function(value) { - if (typeof value === 'undefined') { - return this.settings.state; - } else if (value === true || value === false || value === null) { - this.settings.state = value; - - this._refresh(this.settings.change); - } - return this; - }, - - _parseValue: function(value) { - if (value === this.settings.checked) { - return true; - } else if (value === this.settings.unchecked) { - return false; - } else if (value === this.settings.indeterminate) { - return null; - } - }, - - value: function(value) { - if (typeof value === 'undefined') { - var value; - switch (this.settings.state) { - case true: - value = this.settings.checked; - break; - - case false: - value = this.settings.unchecked; - break; - - case null: - value = this.settings.indeterminate; - break; - } - return typeof value === 'undefined'? this.element.attr('value') : value; - } else { - var state = this._parseValue(value); - if (typeof state !== 'undefined') { - this.settings.state = state; - this._refresh(this.settings.change); - } - } - } - }); - - $.fn[pluginName] = function (options, value) { - var result = this; - - this.each(function() { - if (!$.data(this, "plugin.vanderlee." + pluginName)) { - $.data(this, "plugin.vanderlee." + pluginName, new Plugin(this, options)); - } else if (typeof options === 'string') { - if (typeof value === 'undefined') { - result = $(this).data("plugin.vanderlee." + pluginName)[options](); - return false; - } else { - $(this).data("plugin.vanderlee." + pluginName)[options](value); - } - } - }); - - return result; - }; - - // Overwrite fn.val - $.fn.val = function(value) { - var data = this.data("vanderlee." + pluginName); - if (typeof data === 'undefined') { - if (typeof value === 'undefined') { - return valFunction.call(this); - } else { - return valFunction.call(this, value); - } - } else { - if (typeof value === 'undefined') { - return data; - } else { - this.data("vanderlee." + pluginName, value); - return this; - } - } - }; - - // :indeterminate pseudo selector - $.expr.filters.indeterminate = function(element) { - var $element = $(element); - return typeof $element.data("vanderlee." + pluginName) !== 'undefined' && $element.prop('indeterminate'); - }; - - // :determinate pseudo selector - $.expr.filters.determinate = function(element) { - return !($.expr.filters.indeterminate(element)); - }; - - // :tristate selector - $.expr.filters.tristate = function(element) { - return typeof $(element).data("vanderlee." + pluginName) !== 'undefined'; - }; -})(jQuery); diff --git a/assets/js/tab_remember.js b/assets/js/tab_remember.js index b0697393..78309742 100644 --- a/assets/js/tab_remember.js +++ b/assets/js/tab_remember.js @@ -55,6 +55,7 @@ class TabRememberHelper { while(parent) { //Invoker can either be a button or a element let tabInvoker = document.querySelector("button[data-content='#" + parent.id + "']") + ?? document.querySelector("button[data-bs-target='#" + parent.id + "']") ?? document.querySelector("a[href='#" + parent.id + "']"); Tab.getOrCreateInstance(tabInvoker).show(); diff --git a/assets/js/tristate_checkboxes.js b/assets/js/tristate_checkboxes.js index 5ec16b19..c59a3e22 100644 --- a/assets/js/tristate_checkboxes.js +++ b/assets/js/tristate_checkboxes.js @@ -1,49 +1,37 @@ 'use strict'; -import "./lib/jquery.tristate" +import TristateCheckbox from "./lib/TristateCheckbox"; class TristateHelper { constructor() { this.registerTriStateCheckboxes(); - this.registerSubmitHandler(); - } - - registerSubmitHandler() { - document.addEventListener("turbo:submit-start", (e) => { - var form = e.detail.formSubmission.formElement; - var formData = e.detail.formSubmission.formData; - - var $tristate_checkboxes = $('.tristate:checkbox', form); - - //Iterate over each tristate checkbox in the form and set formData to the correct value - $tristate_checkboxes.each(function() { - var $checkbox = $(this); - var state = $checkbox.tristate('state'); - - formData.set($checkbox.attr('name'), state); - }); - }); } registerTriStateCheckboxes() { //Initialize tristate checkboxes and if needed the multicheckbox functionality const listener = () => { - $(".tristate").tristate( { - checked: "true", - unchecked: "false", - indeterminate: "indeterminate", + + const tristates = document.querySelectorAll("input.tristate"); + + tristates.forEach(tristate => { + TristateCheckbox.getInstance(tristate); }); - $('.permission_multicheckbox:checkbox').change(function() { - //Find the other checkboxes in this row, and change their value - var $row = $(this).parents('tr'); - //@ts-ignore - var new_state = $(this).tristate('state'); + //Register multi checkboxes in permission tables + const multicheckboxes = document.querySelectorAll("input.permission_multicheckbox"); + multicheckboxes.forEach(multicheckbox => { + multicheckbox.addEventListener("change", (event) => { + const newValue = TristateCheckbox.getInstance(event.target).state; + const row = event.target.closest("tr"); - //@ts-ignore - $('.tristate:checkbox', $row).tristate('state', new_state); + //Find all tristate checkboxes in the same row and set their state to the new value + const tristateCheckboxes = row.querySelectorAll("input.tristate"); + tristateCheckboxes.forEach(tristateCheckbox => { + TristateCheckbox.getInstance(tristateCheckbox).state = newValue; + }); + }); }); } diff --git a/composer.lock b/composer.lock index 430d25ac..e0713e1f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "186080614c26d1b307fd99823f281e22", + "content-hash": "fae8a3a2fff35f26444bf1c3ceb771b3", "packages": [ { "name": "beberlei/assert", @@ -507,26 +507,27 @@ }, { "name": "doctrine/collections", - "version": "1.6.8", + "version": "1.7.2", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "1958a744696c6bb3bb0d28db2611dc11610e78af" + "reference": "3fe77330f5591108bbf1315da7377a7e704ed8a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/1958a744696c6bb3bb0d28db2611dc11610e78af", - "reference": "1958a744696c6bb3bb0d28db2611dc11610e78af", + "url": "https://api.github.com/repos/doctrine/collections/zipball/3fe77330f5591108bbf1315da7377a7e704ed8a0", + "reference": "3fe77330f5591108bbf1315da7377a7e704ed8a0", "shasum": "" }, "require": { + "doctrine/deprecations": "^0.5.3 || ^1", "php": "^7.1.3 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9.0", - "phpstan/phpstan": "^0.12", + "doctrine/coding-standard": "^9.0 || ^10.0", + "phpstan/phpstan": "^1.4.8", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.1.5", - "vimeo/psalm": "^4.2.1" + "vimeo/psalm": "^4.22" }, "type": "library", "autoload": { @@ -570,22 +571,22 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/1.6.8" + "source": "https://github.com/doctrine/collections/tree/1.7.2" }, - "time": "2021-08-10T18:51:53+00:00" + "time": "2022-08-27T16:08:58+00:00" }, { "name": "doctrine/common", - "version": "3.3.0", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "c824e95d4c83b7102d8bc60595445a6f7d540f96" + "reference": "e09556bbdf95b8420e649162b19ae9da2d1a80f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/c824e95d4c83b7102d8bc60595445a6f7d540f96", - "reference": "c824e95d4c83b7102d8bc60595445a6f7d540f96", + "url": "https://api.github.com/repos/doctrine/common/zipball/e09556bbdf95b8420e649162b19ae9da2d1a80f3", + "reference": "e09556bbdf95b8420e649162b19ae9da2d1a80f3", "shasum": "" }, "require": { @@ -594,6 +595,7 @@ }, "require-dev": { "doctrine/coding-standard": "^9.0", + "doctrine/collections": "^1", "phpstan/phpstan": "^1.4.1", "phpstan/phpstan-phpunit": "^1", "phpunit/phpunit": "^7.5.20 || ^8.5 || ^9.0", @@ -604,7 +606,7 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\Common\\": "lib/Doctrine/Common" + "Doctrine\\Common\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -646,7 +648,7 @@ ], "support": { "issues": "https://github.com/doctrine/common/issues", - "source": "https://github.com/doctrine/common/tree/3.3.0" + "source": "https://github.com/doctrine/common/tree/3.4.0" }, "funding": [ { @@ -662,7 +664,7 @@ "type": "tidelift" } ], - "time": "2022-02-05T18:28:51+00:00" + "time": "2022-08-23T19:46:56+00:00" }, { "name": "doctrine/dbal", @@ -1110,28 +1112,28 @@ }, { "name": "doctrine/inflector", - "version": "2.0.4", + "version": "2.0.5", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "8b7ff3e4b7de6b2c84da85637b59fd2880ecaa89" + "reference": "ade2b3bbfb776f27f0558e26eed43b5d9fe1b392" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/8b7ff3e4b7de6b2c84da85637b59fd2880ecaa89", - "reference": "8b7ff3e4b7de6b2c84da85637b59fd2880ecaa89", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/ade2b3bbfb776f27f0558e26eed43b5d9fe1b392", + "reference": "ade2b3bbfb776f27f0558e26eed43b5d9fe1b392", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^8.2", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", - "vimeo/psalm": "^4.10" + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25" }, "type": "library", "autoload": { @@ -1181,7 +1183,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.4" + "source": "https://github.com/doctrine/inflector/tree/2.0.5" }, "funding": [ { @@ -1197,7 +1199,7 @@ "type": "tidelift" } ], - "time": "2021-10-22T20:16:43+00:00" + "time": "2022-09-07T09:01:28+00:00" }, { "name": "doctrine/instantiator", @@ -2877,16 +2879,16 @@ }, { "name": "masterminds/html5", - "version": "2.7.5", + "version": "2.7.6", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "f640ac1bdddff06ea333a920c95bbad8872429ab" + "reference": "897eb517a343a2281f11bc5556d6548db7d93947" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f640ac1bdddff06ea333a920c95bbad8872429ab", - "reference": "f640ac1bdddff06ea333a920c95bbad8872429ab", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/897eb517a343a2281f11bc5556d6548db7d93947", + "reference": "897eb517a343a2281f11bc5556d6548db7d93947", "shasum": "" }, "require": { @@ -2940,9 +2942,9 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.7.5" + "source": "https://github.com/Masterminds/html5-php/tree/2.7.6" }, - "time": "2021-07-01T14:25:37+00:00" + "time": "2022-08-18T16:18:26+00:00" }, { "name": "monolog/monolog", @@ -3116,16 +3118,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.14.0", + "version": "v4.15.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1" + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/34bea19b6e03d8153165d8f30bba4c3be86184c1", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", "shasum": "" }, "require": { @@ -3166,9 +3168,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1" }, - "time": "2022-05-31T20:59:12+00:00" + "time": "2022-09-04T07:30:47+00:00" }, { "name": "nikolaposa/version", @@ -4014,16 +4016,16 @@ }, { "name": "php-translation/symfony-bundle", - "version": "0.12.6", + "version": "0.12.7", "source": { "type": "git", "url": "https://github.com/php-translation/symfony-bundle.git", - "reference": "1a83e47666ebebc27bbf4e57d91c7f0064056a05" + "reference": "123dfd27f5fb330d2ea5e5519565eae97f91839e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-translation/symfony-bundle/zipball/1a83e47666ebebc27bbf4e57d91c7f0064056a05", - "reference": "1a83e47666ebebc27bbf4e57d91c7f0064056a05", + "url": "https://api.github.com/repos/php-translation/symfony-bundle/zipball/123dfd27f5fb330d2ea5e5519565eae97f91839e", + "reference": "123dfd27f5fb330d2ea5e5519565eae97f91839e", "shasum": "" }, "require": { @@ -4085,9 +4087,9 @@ ], "support": { "issues": "https://github.com/php-translation/symfony-bundle/issues", - "source": "https://github.com/php-translation/symfony-bundle/tree/0.12.6" + "source": "https://github.com/php-translation/symfony-bundle/tree/0.12.7" }, - "time": "2022-04-06T11:17:06+00:00" + "time": "2022-09-02T15:33:16+00:00" }, { "name": "php-translation/symfony-storage", @@ -4914,16 +4916,16 @@ }, { "name": "s9e/text-formatter", - "version": "2.11.3", + "version": "2.11.4", "source": { "type": "git", "url": "https://github.com/s9e/TextFormatter.git", - "reference": "e94e1823714da475a9df50c5319e29c2bb91c591" + "reference": "a7f31582f97abe17ae6b7c95a198dc0d48e7d1a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/s9e/TextFormatter/zipball/e94e1823714da475a9df50c5319e29c2bb91c591", - "reference": "e94e1823714da475a9df50c5319e29c2bb91c591", + "url": "https://api.github.com/repos/s9e/TextFormatter/zipball/a7f31582f97abe17ae6b7c95a198dc0d48e7d1a1", + "reference": "a7f31582f97abe17ae6b7c95a198dc0d48e7d1a1", "shasum": "" }, "require": { @@ -4950,7 +4952,7 @@ }, "type": "library", "extra": { - "version": "2.11.3" + "version": "2.11.4" }, "autoload": { "psr-4": { @@ -4982,9 +4984,9 @@ ], "support": { "issues": "https://github.com/s9e/TextFormatter/issues", - "source": "https://github.com/s9e/TextFormatter/tree/2.11.3" + "source": "https://github.com/s9e/TextFormatter/tree/2.11.4" }, - "time": "2022-06-13T00:31:16+00:00" + "time": "2022-08-17T21:44:18+00:00" }, { "name": "sabberworm/php-css-parser", @@ -5256,16 +5258,16 @@ }, { "name": "sensio/framework-extra-bundle", - "version": "v6.2.6", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/sensiolabs/SensioFrameworkExtraBundle.git", - "reference": "6bd976c99ef3f78e31c9490a10ba6dd8901076eb" + "reference": "bb962f8aed09e60b0942545f6e4842ffeee4aafd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sensiolabs/SensioFrameworkExtraBundle/zipball/6bd976c99ef3f78e31c9490a10ba6dd8901076eb", - "reference": "6bd976c99ef3f78e31c9490a10ba6dd8901076eb", + "url": "https://api.github.com/repos/sensiolabs/SensioFrameworkExtraBundle/zipball/bb962f8aed09e60b0942545f6e4842ffeee4aafd", + "reference": "bb962f8aed09e60b0942545f6e4842ffeee4aafd", "shasum": "" }, "require": { @@ -5328,9 +5330,9 @@ ], "support": { "issues": "https://github.com/sensiolabs/SensioFrameworkExtraBundle/issues", - "source": "https://github.com/sensiolabs/SensioFrameworkExtraBundle/tree/v6.2.6" + "source": "https://github.com/sensiolabs/SensioFrameworkExtraBundle/tree/v6.2.8" }, - "time": "2022-01-14T11:51:13+00:00" + "time": "2022-09-05T16:44:56+00:00" }, { "name": "shivas/versioning-bundle", @@ -5824,16 +5826,16 @@ }, { "name": "symfony/console", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "535846c7ee6bc4dd027ca0d93220601456734b10" + "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/535846c7ee6bc4dd027ca0d93220601456734b10", - "reference": "535846c7ee6bc4dd027ca0d93220601456734b10", + "url": "https://api.github.com/repos/symfony/console/zipball/c072aa8f724c3af64e2c7a96b796a4863d24dba1", + "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1", "shasum": "" }, "require": { @@ -5903,7 +5905,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.11" + "source": "https://github.com/symfony/console/tree/v5.4.12" }, "funding": [ { @@ -5919,7 +5921,7 @@ "type": "tidelift" } ], - "time": "2022-07-22T10:42:43+00:00" + "time": "2022-08-17T13:18:05+00:00" }, { "name": "symfony/css-selector", @@ -6631,16 +6633,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "6699fb0228d1bc35b12aed6dd5e7455457609ddd" + "reference": "2d67c1f9a1937406a9be3171b4b22250c0a11447" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/6699fb0228d1bc35b12aed6dd5e7455457609ddd", - "reference": "6699fb0228d1bc35b12aed6dd5e7455457609ddd", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/2d67c1f9a1937406a9be3171b4b22250c0a11447", + "reference": "2d67c1f9a1937406a9be3171b4b22250c0a11447", "shasum": "" }, "require": { @@ -6675,7 +6677,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.11" + "source": "https://github.com/symfony/filesystem/tree/v5.4.12" }, "funding": [ { @@ -6691,7 +6693,7 @@ "type": "tidelift" } ], - "time": "2022-07-20T13:00:38+00:00" + "time": "2022-08-02T13:48:16+00:00" }, { "name": "symfony/finder", @@ -6823,16 +6825,16 @@ }, { "name": "symfony/form", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "1c156d7093cce68600604f155cb51065e897d7fa" + "reference": "d8c5cc929f8dc7a58b710c9474dd7a0173006017" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/1c156d7093cce68600604f155cb51065e897d7fa", - "reference": "1c156d7093cce68600604f155cb51065e897d7fa", + "url": "https://api.github.com/repos/symfony/form/zipball/d8c5cc929f8dc7a58b710c9474dd7a0173006017", + "reference": "d8c5cc929f8dc7a58b710c9474dd7a0173006017", "shasum": "" }, "require": { @@ -6906,7 +6908,7 @@ "description": "Allows to easily create, process and reuse HTML forms", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/form/tree/v5.4.11" + "source": "https://github.com/symfony/form/tree/v5.4.12" }, "funding": [ { @@ -6922,20 +6924,20 @@ "type": "tidelift" } ], - "time": "2022-07-20T13:00:38+00:00" + "time": "2022-08-05T13:13:10+00:00" }, { "name": "symfony/framework-bundle", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "a0660b602357d5c2ceaac1c9f80c5820bbff803d" + "reference": "49f8fe5d39b7513a3f26898788885dbe66b0d910" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/a0660b602357d5c2ceaac1c9f80c5820bbff803d", - "reference": "a0660b602357d5c2ceaac1c9f80c5820bbff803d", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/49f8fe5d39b7513a3f26898788885dbe66b0d910", + "reference": "49f8fe5d39b7513a3f26898788885dbe66b0d910", "shasum": "" }, "require": { @@ -7057,7 +7059,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v5.4.11" + "source": "https://github.com/symfony/framework-bundle/tree/v5.4.12" }, "funding": [ { @@ -7073,20 +7075,20 @@ "type": "tidelift" } ], - "time": "2022-07-20T13:00:38+00:00" + "time": "2022-08-26T10:32:10+00:00" }, { "name": "symfony/http-client", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "5c5c37eb2a276d8d7d669dd76688aa1606ee78fb" + "reference": "6a057be154824487fd5e6b65ab83899e0c5ac550" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/5c5c37eb2a276d8d7d669dd76688aa1606ee78fb", - "reference": "5c5c37eb2a276d8d7d669dd76688aa1606ee78fb", + "url": "https://api.github.com/repos/symfony/http-client/zipball/6a057be154824487fd5e6b65ab83899e0c5ac550", + "reference": "6a057be154824487fd5e6b65ab83899e0c5ac550", "shasum": "" }, "require": { @@ -7144,7 +7146,7 @@ "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-client/tree/v5.4.11" + "source": "https://github.com/symfony/http-client/tree/v5.4.12" }, "funding": [ { @@ -7160,7 +7162,7 @@ "type": "tidelift" } ], - "time": "2022-07-28T13:33:28+00:00" + "time": "2022-08-02T15:52:22+00:00" }, { "name": "symfony/http-client-contracts", @@ -7242,16 +7244,16 @@ }, { "name": "symfony/http-foundation", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "0a5868e0999e9d47859ba3d918548ff6943e6389" + "reference": "f4bfe9611b113b15d98a43da68ec9b5a00d56791" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/0a5868e0999e9d47859ba3d918548ff6943e6389", - "reference": "0a5868e0999e9d47859ba3d918548ff6943e6389", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f4bfe9611b113b15d98a43da68ec9b5a00d56791", + "reference": "f4bfe9611b113b15d98a43da68ec9b5a00d56791", "shasum": "" }, "require": { @@ -7263,8 +7265,11 @@ "require-dev": { "predis/predis": "~1.0", "symfony/cache": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/mime": "^4.4|^5.0|^6.0" + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", + "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/rate-limiter": "^5.2|^6.0" }, "suggest": { "symfony/mime": "To use the file extension guesser" @@ -7295,7 +7300,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.4.11" + "source": "https://github.com/symfony/http-foundation/tree/v5.4.12" }, "funding": [ { @@ -7311,20 +7316,20 @@ "type": "tidelift" } ], - "time": "2022-07-20T13:00:38+00:00" + "time": "2022-08-19T07:33:17+00:00" }, { "name": "symfony/http-kernel", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "4fd590a2ef3f62560dbbf6cea511995dd77321ee" + "reference": "37f660fa3bcd78fe4893ce23ebe934618ec099be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4fd590a2ef3f62560dbbf6cea511995dd77321ee", - "reference": "4fd590a2ef3f62560dbbf6cea511995dd77321ee", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/37f660fa3bcd78fe4893ce23ebe934618ec099be", + "reference": "37f660fa3bcd78fe4893ce23ebe934618ec099be", "shasum": "" }, "require": { @@ -7407,7 +7412,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v5.4.11" + "source": "https://github.com/symfony/http-kernel/tree/v5.4.12" }, "funding": [ { @@ -7423,7 +7428,7 @@ "type": "tidelift" } ], - "time": "2022-07-29T12:30:22+00:00" + "time": "2022-08-26T14:40:40+00:00" }, { "name": "symfony/intl", @@ -7515,16 +7520,16 @@ }, { "name": "symfony/mailer", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "9f34f71ec05cef8a0d434988476ee9fd32075a6c" + "reference": "076043af11e58b20a68d2fd93f59cdbc6e8fdd00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/9f34f71ec05cef8a0d434988476ee9fd32075a6c", - "reference": "9f34f71ec05cef8a0d434988476ee9fd32075a6c", + "url": "https://api.github.com/repos/symfony/mailer/zipball/076043af11e58b20a68d2fd93f59cdbc6e8fdd00", + "reference": "076043af11e58b20a68d2fd93f59cdbc6e8fdd00", "shasum": "" }, "require": { @@ -7571,7 +7576,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v5.4.11" + "source": "https://github.com/symfony/mailer/tree/v5.4.12" }, "funding": [ { @@ -7587,20 +7592,20 @@ "type": "tidelift" } ], - "time": "2022-07-24T16:05:20+00:00" + "time": "2022-08-03T05:17:26+00:00" }, { "name": "symfony/mime", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "3cd175cdcdb6db2e589e837dd46aff41027d9830" + "reference": "03876e9c5a36f5b45e7d9a381edda5421eff8a90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/3cd175cdcdb6db2e589e837dd46aff41027d9830", - "reference": "3cd175cdcdb6db2e589e837dd46aff41027d9830", + "url": "https://api.github.com/repos/symfony/mime/zipball/03876e9c5a36f5b45e7d9a381edda5421eff8a90", + "reference": "03876e9c5a36f5b45e7d9a381edda5421eff8a90", "shasum": "" }, "require": { @@ -7654,7 +7659,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v5.4.11" + "source": "https://github.com/symfony/mime/tree/v5.4.12" }, "funding": [ { @@ -7670,7 +7675,7 @@ "type": "tidelift" } ], - "time": "2022-07-20T11:34:24+00:00" + "time": "2022-08-19T14:24:03+00:00" }, { "name": "symfony/monolog-bridge", @@ -9604,16 +9609,16 @@ }, { "name": "symfony/security-http", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "447f8b5313f17b6a1297df6a9d0fc36fb555de4d" + "reference": "3ca3eb2a866a4a5adaf0a952d2d7db7208da378b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/447f8b5313f17b6a1297df6a9d0fc36fb555de4d", - "reference": "447f8b5313f17b6a1297df6a9d0fc36fb555de4d", + "url": "https://api.github.com/repos/symfony/security-http/zipball/3ca3eb2a866a4a5adaf0a952d2d7db7208da378b", + "reference": "3ca3eb2a866a4a5adaf0a952d2d7db7208da378b", "shasum": "" }, "require": { @@ -9669,7 +9674,7 @@ "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-http/tree/v5.4.11" + "source": "https://github.com/symfony/security-http/tree/v5.4.12" }, "funding": [ { @@ -9685,20 +9690,20 @@ "type": "tidelift" } ], - "time": "2022-07-29T07:37:50+00:00" + "time": "2022-08-23T10:55:18+00:00" }, { "name": "symfony/serializer", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "412e2a242a380267f3ddf281047b8720d2ad9b08" + "reference": "776fa3010f62b97a7119757a66596a654cd244d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/412e2a242a380267f3ddf281047b8720d2ad9b08", - "reference": "412e2a242a380267f3ddf281047b8720d2ad9b08", + "url": "https://api.github.com/repos/symfony/serializer/zipball/776fa3010f62b97a7119757a66596a654cd244d4", + "reference": "776fa3010f62b97a7119757a66596a654cd244d4", "shasum": "" }, "require": { @@ -9772,7 +9777,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v5.4.11" + "source": "https://github.com/symfony/serializer/tree/v5.4.12" }, "funding": [ { @@ -9788,7 +9793,7 @@ "type": "tidelift" } ], - "time": "2022-07-28T13:33:28+00:00" + "time": "2022-08-26T10:32:10+00:00" }, { "name": "symfony/service-contracts", @@ -9937,16 +9942,16 @@ }, { "name": "symfony/string", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "5eb661e49ad389e4ae2b6e4df8d783a8a6548322" + "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/5eb661e49ad389e4ae2b6e4df8d783a8a6548322", - "reference": "5eb661e49ad389e4ae2b6e4df8d783a8a6548322", + "url": "https://api.github.com/repos/symfony/string/zipball/2fc515e512d721bf31ea76bd02fe23ada4640058", + "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058", "shasum": "" }, "require": { @@ -10003,7 +10008,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.11" + "source": "https://github.com/symfony/string/tree/v5.4.12" }, "funding": [ { @@ -10019,7 +10024,7 @@ "type": "tidelift" } ], - "time": "2022-07-24T16:15:25+00:00" + "time": "2022-08-12T17:03:11+00:00" }, { "name": "symfony/templating", @@ -10091,16 +10096,16 @@ }, { "name": "symfony/translation", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "7a1a8f6bbff269f434a83343a0a5d36a4f8cfa21" + "reference": "42ecc77eb4f229ce2df702a648ec93b8478d76ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/7a1a8f6bbff269f434a83343a0a5d36a4f8cfa21", - "reference": "7a1a8f6bbff269f434a83343a0a5d36a4f8cfa21", + "url": "https://api.github.com/repos/symfony/translation/zipball/42ecc77eb4f229ce2df702a648ec93b8478d76ae", + "reference": "42ecc77eb4f229ce2df702a648ec93b8478d76ae", "shasum": "" }, "require": { @@ -10168,7 +10173,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v5.4.11" + "source": "https://github.com/symfony/translation/tree/v5.4.12" }, "funding": [ { @@ -10184,7 +10189,7 @@ "type": "tidelift" } ], - "time": "2022-07-20T13:00:38+00:00" + "time": "2022-08-02T15:52:22+00:00" }, { "name": "symfony/translation-contracts", @@ -10266,16 +10271,16 @@ }, { "name": "symfony/twig-bridge", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "63b8a50d48c9fe3d04e77307d4f1771dd848baa8" + "reference": "94c3b38514c953e3e84719c96d4e578a01ca1819" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/63b8a50d48c9fe3d04e77307d4f1771dd848baa8", - "reference": "63b8a50d48c9fe3d04e77307d4f1771dd848baa8", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/94c3b38514c953e3e84719c96d4e578a01ca1819", + "reference": "94c3b38514c953e3e84719c96d4e578a01ca1819", "shasum": "" }, "require": { @@ -10367,7 +10372,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v5.4.11" + "source": "https://github.com/symfony/twig-bridge/tree/v5.4.12" }, "funding": [ { @@ -10383,7 +10388,7 @@ "type": "tidelift" } ], - "time": "2022-07-20T13:00:38+00:00" + "time": "2022-08-03T13:09:21+00:00" }, { "name": "symfony/twig-bundle", @@ -10476,7 +10481,7 @@ }, { "name": "symfony/ux-turbo", - "version": "v2.3.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/ux-turbo.git", @@ -10554,7 +10559,7 @@ "turbo-stream" ], "support": { - "source": "https://github.com/symfony/ux-turbo/tree/v2.3.0" + "source": "https://github.com/symfony/ux-turbo/tree/v2.4.0" }, "funding": [ { @@ -10574,16 +10579,16 @@ }, { "name": "symfony/validator", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "d6457034ba8a4ea6703e5607829a337b66a53ce8" + "reference": "38bc4d83d01b800f1fa5acaceb5ff77490b8f768" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/d6457034ba8a4ea6703e5607829a337b66a53ce8", - "reference": "d6457034ba8a4ea6703e5607829a337b66a53ce8", + "url": "https://api.github.com/repos/symfony/validator/zipball/38bc4d83d01b800f1fa5acaceb5ff77490b8f768", + "reference": "38bc4d83d01b800f1fa5acaceb5ff77490b8f768", "shasum": "" }, "require": { @@ -10667,7 +10672,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v5.4.11" + "source": "https://github.com/symfony/validator/tree/v5.4.12" }, "funding": [ { @@ -10683,7 +10688,7 @@ "type": "tidelift" } ], - "time": "2022-07-20T13:00:38+00:00" + "time": "2022-08-09T11:54:29+00:00" }, { "name": "symfony/var-dumper", @@ -11009,16 +11014,16 @@ }, { "name": "symfony/yaml", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "05d4ea560f3402c6c116afd99fdc66e60eda227e" + "reference": "7a3aa21ac8ab1a96cc6de5bbcab4bc9fc943b18c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/05d4ea560f3402c6c116afd99fdc66e60eda227e", - "reference": "05d4ea560f3402c6c116afd99fdc66e60eda227e", + "url": "https://api.github.com/repos/symfony/yaml/zipball/7a3aa21ac8ab1a96cc6de5bbcab4bc9fc943b18c", + "reference": "7a3aa21ac8ab1a96cc6de5bbcab4bc9fc943b18c", "shasum": "" }, "require": { @@ -11064,7 +11069,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.4.11" + "source": "https://github.com/symfony/yaml/tree/v5.4.12" }, "funding": [ { @@ -11080,7 +11085,7 @@ "type": "tidelift" } ], - "time": "2022-06-27T16:58:25+00:00" + "time": "2022-08-02T15:52:22+00:00" }, { "name": "tecnickcom/tc-lib-barcode", @@ -13085,16 +13090,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.8.2", + "version": "1.8.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "c53312ecc575caf07b0e90dee43883fdf90ca67c" + "reference": "f6598a5ff12ca4499a836815e08b4d77a2ddeb20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c53312ecc575caf07b0e90dee43883fdf90ca67c", - "reference": "c53312ecc575caf07b0e90dee43883fdf90ca67c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f6598a5ff12ca4499a836815e08b4d77a2ddeb20", + "reference": "f6598a5ff12ca4499a836815e08b4d77a2ddeb20", "shasum": "" }, "require": { @@ -13118,9 +13123,13 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.8.2" + "source": "https://github.com/phpstan/phpstan/tree/1.8.5" }, "funding": [ { @@ -13131,34 +13140,30 @@ "url": "https://github.com/phpstan", "type": "github" }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, { "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", "type": "tidelift" } ], - "time": "2022-07-20T09:57:31+00:00" + "time": "2022-09-07T16:05:32+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "1.3.12", + "version": "1.3.13", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "c5ec462889f3bcee32be57ff26f775295836c173" + "reference": "62a3b4252d502f0ead9c145055947b38b8568498" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/c5ec462889f3bcee32be57ff26f775295836c173", - "reference": "c5ec462889f3bcee32be57ff26f775295836c173", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/62a3b4252d502f0ead9c145055947b38b8568498", + "reference": "62a3b4252d502f0ead9c145055947b38b8568498", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.8.0" + "phpstan/phpstan": "^1.8.3" }, "conflict": { "doctrine/collections": "<1.0", @@ -13176,6 +13181,7 @@ "doctrine/mongodb-odm": "^1.3 || ^2.1", "doctrine/orm": "^2.11.0", "doctrine/persistence": "^1.3.8 || ^2.2.1", + "gedmo/doctrine-extensions": "^3.8", "nesbot/carbon": "^2.49", "nikic/php-parser": "^4.13.2", "php-parallel-lint/php-parallel-lint": "^1.2", @@ -13206,22 +13212,22 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.12" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.13" }, - "time": "2022-08-08T18:06:58+00:00" + "time": "2022-09-06T14:54:00+00:00" }, { "name": "phpstan/phpstan-symfony", - "version": "1.2.9", + "version": "1.2.13", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "f4cb3b8915d3656e780f305f01c86b70ff933272" + "reference": "016e441a19a2af79ca0c60920ba0d61747b4e855" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/f4cb3b8915d3656e780f305f01c86b70ff933272", - "reference": "f4cb3b8915d3656e780f305f01c86b70ff933272", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/016e441a19a2af79ca0c60920ba0d61747b4e855", + "reference": "016e441a19a2af79ca0c60920ba0d61747b4e855", "shasum": "" }, "require": { @@ -13239,15 +13245,15 @@ "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^9.5", "psr/container": "1.0 || 1.1.1", - "symfony/config": "^4.2 || ^5.0", - "symfony/console": "^4.0 || ^5.0", - "symfony/dependency-injection": "^4.0 || ^5.0", - "symfony/form": "^4.0 || ^5.0", - "symfony/framework-bundle": "^4.4 || ^5.0", - "symfony/http-foundation": "^5.1", - "symfony/messenger": "^4.2 || ^5.0", + "symfony/config": "^5.4 || ^6.1", + "symfony/console": "^5.4 || ^6.1", + "symfony/dependency-injection": "^5.4 || ^6.1", + "symfony/form": "^5.4 || ^6.1", + "symfony/framework-bundle": "^5.4 || ^6.1", + "symfony/http-foundation": "^5.4 || ^6.1", + "symfony/messenger": "^5.4", "symfony/polyfill-php80": "^1.24", - "symfony/serializer": "^4.0 || ^5.0" + "symfony/serializer": "^5.4" }, "type": "phpstan-extension", "extra": { @@ -13277,9 +13283,9 @@ "description": "Symfony Framework extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/1.2.9" + "source": "https://github.com/phpstan/phpstan-symfony/tree/1.2.13" }, - "time": "2022-08-05T20:13:38+00:00" + "time": "2022-08-28T13:34:45+00:00" }, { "name": "psalm/plugin-symfony", @@ -13348,22 +13354,23 @@ }, { "name": "roave/security-advisories", - "version": "dev-master", + "version": "dev-latest", "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "773292d413a97c357a0b49635afd5fdb1d4f314a" + "reference": "6d260392fad173d6ee6e3a93c875d9327db1109b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/773292d413a97c357a0b49635afd5fdb1d4f314a", - "reference": "773292d413a97c357a0b49635afd5fdb1d4f314a", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/6d260392fad173d6ee6e3a93c875d9327db1109b", + "reference": "6d260392fad173d6ee6e3a93c875d9327db1109b", "shasum": "" }, "conflict": { "3f/pygmentize": "<1.2", "admidio/admidio": "<4.1.9", "adodb/adodb-php": "<=5.20.20|>=5.21,<=5.21.3", + "aheinze/cockpit": "<=2.2.1", "akaunting/akaunting": "<2.1.13", "alextselegidis/easyappointments": "<=1.4.3", "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", @@ -13407,6 +13414,7 @@ "codeception/codeception": "<3.1.3|>=4,<4.1.22", "codeigniter/framework": "<=3.0.6", "codeigniter4/framework": "<4.1.9", + "codeigniter4/shield": "= 1.0.0-beta", "codiad/codiad": "<=2.8.4", "composer/composer": "<1.10.26|>=2-alpha.1,<2.2.12|>=2.3,<2.3.5", "concrete5/concrete5": "<9", @@ -13491,8 +13499,9 @@ "gaoming13/wechat-php-sdk": "<=1.10.2", "genix/cms": "<=1.1.11", "getgrav/grav": "<1.7.34", - "getkirby/cms": "<3.5.8", + "getkirby/cms": "<3.5.8.1|>=3.6,<3.6.6.1|>=3.7,<3.7.4", "getkirby/panel": "<2.5.14", + "getkirby/starterkit": "<=3.7.0.2", "gilacms/gila": "<=1.11.4", "globalpayments/php-sdk": "<2", "google/protobuf": "<3.15", @@ -13543,7 +13552,6 @@ "laminas/laminas-http": "<2.14.2", "laravel/fortify": "<1.11.1", "laravel/framework": "<6.20.42|>=7,<7.30.6|>=8,<8.75", - "laravel/laravel": "<=9.1.8", "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10", "latte/latte": "<2.10.8", "lavalite/cms": "<=5.8", @@ -13586,6 +13594,7 @@ "nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6", "nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13", "nilsteampassnet/teampass": "<=2.1.27.36", + "notrinos/notrinos-erp": "<=0.7", "noumo/easyii": "<=0.9", "nukeviet/nukeviet": "<4.5.2", "nystudio107/craft-seomatic": "<3.4.12", @@ -13631,14 +13640,14 @@ "pimcore/data-hub": "<1.2.4", "pimcore/pimcore": "<10.4.4", "pocketmine/bedrock-protocol": "<8.0.2", - "pocketmine/pocketmine-mp": ">= 4.0.0-BETA5, < 4.4.2|<4.2.10", + "pocketmine/pocketmine-mp": "<4.7.2|>= 4.0.0-BETA5, < 4.4.2", "pressbooks/pressbooks": "<5.18", "prestashop/autoupgrade": ">=4,<4.10.1", "prestashop/blockwishlist": ">=2,<2.1.1", "prestashop/contactform": ">1.0.1,<4.3", "prestashop/gamification": "<2.3.2", "prestashop/prestashop": ">=1.6.0.10,<1.7.8.7", - "prestashop/productcomments": ">=4,<4.2.1", + "prestashop/productcomments": "<5.0.2", "prestashop/ps_emailsubscription": "<2.6.1", "prestashop/ps_facetedsearch": "<3.4.1", "prestashop/ps_linklist": "<3.1", @@ -13689,7 +13698,7 @@ "simplito/elliptic-php": "<1.0.6", "slim/slim": "<2.6", "smarty/smarty": "<3.1.45|>=4,<4.1.1", - "snipe/snipe-it": "<=6.0.2|>= 6.0.0-RC-1, <= 6.0.0-RC-5", + "snipe/snipe-it": "<6.0.10|>= 6.0.0-RC-1, <= 6.0.0-RC-5", "socalnick/scn-social-auth": "<1.15.2", "socialiteproviders/steam": "<1.1", "spipu/html2pdf": "<5.2.4", @@ -13827,6 +13836,7 @@ "zfr/zfr-oauth2-server-module": "<0.1.2", "zoujingli/thinkadmin": "<6.0.22" }, + "default-branch": true, "type": "metapackage", "notification-url": "https://packagist.org/downloads/", "license": [ @@ -13859,7 +13869,7 @@ "type": "tidelift" } ], - "time": "2022-08-12T16:04:45+00:00" + "time": "2022-08-31T22:04:18+00:00" }, { "name": "sebastian/diff", @@ -14080,16 +14090,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "0b900ca5576ecd59e08c76127e616667cfe427a7" + "reference": "291c1e92281a09152dda089f782e23dedd34bd4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/0b900ca5576ecd59e08c76127e616667cfe427a7", - "reference": "0b900ca5576ecd59e08c76127e616667cfe427a7", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/291c1e92281a09152dda089f782e23dedd34bd4f", + "reference": "291c1e92281a09152dda089f782e23dedd34bd4f", "shasum": "" }, "require": { @@ -14135,7 +14145,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v5.4.11" + "source": "https://github.com/symfony/dom-crawler/tree/v5.4.12" }, "funding": [ { @@ -14151,7 +14161,7 @@ "type": "tidelift" } ], - "time": "2022-06-27T16:58:25+00:00" + "time": "2022-08-03T13:09:21+00:00" }, { "name": "symfony/maker-bundle", @@ -14409,16 +14419,16 @@ }, { "name": "symplify/easy-coding-standard", - "version": "11.1.4", + "version": "11.1.9", "source": { "type": "git", "url": "https://github.com/symplify/easy-coding-standard.git", - "reference": "d70ff73140ef96b1faa04c93fc57b2b1e9d6d8bd" + "reference": "65341819f0f518b5f424a57736e9b0e9c5a9202f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symplify/easy-coding-standard/zipball/d70ff73140ef96b1faa04c93fc57b2b1e9d6d8bd", - "reference": "d70ff73140ef96b1faa04c93fc57b2b1e9d6d8bd", + "url": "https://api.github.com/repos/symplify/easy-coding-standard/zipball/65341819f0f518b5f424a57736e9b0e9c5a9202f", + "reference": "65341819f0f518b5f424a57736e9b0e9c5a9202f", "shasum": "" }, "require": { @@ -14448,7 +14458,7 @@ ], "description": "Prefixed scoped version of ECS package", "support": { - "source": "https://github.com/symplify/easy-coding-standard/tree/11.1.4" + "source": "https://github.com/symplify/easy-coding-standard/tree/11.1.9" }, "funding": [ { @@ -14460,20 +14470,20 @@ "type": "github" } ], - "time": "2022-08-13T19:37:11+00:00" + "time": "2022-09-02T10:10:26+00:00" }, { "name": "vimeo/psalm", - "version": "4.26.0", + "version": "4.27.0", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "6998fabb2bf528b65777bf9941920888d23c03ac" + "reference": "faf106e717c37b8c81721845dba9de3d8deed8ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/6998fabb2bf528b65777bf9941920888d23c03ac", - "reference": "6998fabb2bf528b65777bf9941920888d23c03ac", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/faf106e717c37b8c81721845dba9de3d8deed8ff", + "reference": "faf106e717c37b8c81721845dba9de3d8deed8ff", "shasum": "" }, "require": { @@ -14565,9 +14575,9 @@ ], "support": { "issues": "https://github.com/vimeo/psalm/issues", - "source": "https://github.com/vimeo/psalm/tree/4.26.0" + "source": "https://github.com/vimeo/psalm/tree/4.27.0" }, - "time": "2022-07-31T13:10:26+00:00" + "time": "2022-08-31T13:47:09+00:00" }, { "name": "webmozart/path-util", diff --git a/config/packages/datatables.yaml b/config/packages/datatables.yaml index 18fbb18d..7f85df10 100644 --- a/config/packages/datatables.yaml +++ b/config/packages/datatables.yaml @@ -3,6 +3,9 @@ datatables: language_from_cdn: false + # Set to none, as we override the bundle mechanism with our own custom one + persist_state: none + # Set options, as documented at https://datatables.net/reference/option/ options: lengthMenu : [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]] diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 8f9d1586..44ea77f3 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -32,4 +32,5 @@ doctrine: dql: string_functions: - regexp: DoctrineExtensions\Query\Mysql\Regexp \ No newline at end of file + regexp: DoctrineExtensions\Query\Mysql\Regexp + ifnull: DoctrineExtensions\Query\Mysql\IfNull \ No newline at end of file diff --git a/config/packages/nelmio_security.yaml b/config/packages/nelmio_security.yaml index 24e0a50d..d97b3983 100644 --- a/config/packages/nelmio_security.yaml +++ b/config/packages/nelmio_security.yaml @@ -58,3 +58,9 @@ nelmio_security: - 'data:' block-all-mixed-content: true # defaults to false, blocks HTTP content over HTTPS transport # upgrade-insecure-requests: true # defaults to false, upgrades HTTP requests to HTTPS transport + +when@dev: + # disables the Content-Security-Policy header + nelmio_security: + csp: + enabled: false \ No newline at end of file diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 5fdd5bec..0c4492af 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -1,6 +1,6 @@ twig: default_path: '%kernel.project_dir%/templates' - form_themes: ['bootstrap_5_horizontal_layout.html.twig', 'Form/extendedBootstrap4_layout.html.twig', 'Form/permissionLayout.html.twig' ] + form_themes: ['bootstrap_5_horizontal_layout.html.twig', 'Form/extendedBootstrap4_layout.html.twig', 'Form/permissionLayout.html.twig', 'Form/FilterTypesLayout.html.twig'] paths: '%kernel.project_dir%/assets/css': css diff --git a/src/Controller/AttachmentFileController.php b/src/Controller/AttachmentFileController.php index 9d9ba2f1..34501024 100644 --- a/src/Controller/AttachmentFileController.php +++ b/src/Controller/AttachmentFileController.php @@ -43,9 +43,15 @@ declare(strict_types=1); namespace App\Controller; use App\DataTables\AttachmentDataTable; +use App\DataTables\Filters\AttachmentFilter; +use App\DataTables\Filters\PartFilter; +use App\DataTables\PartsDataTable; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\PartAttachment; +use App\Form\Filters\AttachmentFilterType; +use App\Form\Filters\PartFilterType; use App\Services\Attachments\AttachmentManager; +use App\Services\Trees\NodesListBuilder; use Omines\DataTablesBundle\DataTableFactory; use RuntimeException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -123,11 +129,19 @@ class AttachmentFileController extends AbstractController * * @return JsonResponse|Response */ - public function attachmentsTable(DataTableFactory $dataTable, Request $request) + public function attachmentsTable(Request $request, DataTableFactory $dataTableFactory, NodesListBuilder $nodesListBuilder) { $this->denyAccessUnlessGranted('read', new PartAttachment()); - $table = $dataTable->createFromType(AttachmentDataTable::class) + $formRequest = clone $request; + $formRequest->setMethod('GET'); + $filter = new AttachmentFilter($nodesListBuilder); + + $filterForm = $this->createForm(AttachmentFilterType::class, $filter, ['method' => 'GET']); + + $filterForm->handleRequest($formRequest); + + $table = $dataTableFactory->createFromType(AttachmentDataTable::class, ['filter' => $filter]) ->handleRequest($request); if ($table->isCallback()) { @@ -136,6 +150,7 @@ class AttachmentFileController extends AbstractController return $this->render('attachment_list.html.twig', [ 'datatable' => $table, + 'filterForm' => $filterForm->createView(), ]); } } diff --git a/src/Controller/LogController.php b/src/Controller/LogController.php index 991cd0e4..c4c81df8 100644 --- a/src/Controller/LogController.php +++ b/src/Controller/LogController.php @@ -42,6 +42,7 @@ declare(strict_types=1); namespace App\Controller; +use App\DataTables\Filters\LogFilter; use App\DataTables\LogDataTable; use App\Entity\Base\AbstractDBElement; use App\Entity\LogSystem\AbstractLogEntry; @@ -49,6 +50,7 @@ use App\Entity\LogSystem\CollectionElementDeleted; use App\Entity\LogSystem\ElementCreatedLogEntry; use App\Entity\LogSystem\ElementDeletedLogEntry; use App\Entity\LogSystem\ElementEditedLogEntry; +use App\Form\Filters\LogFilterType; use App\Services\LogSystem\EventUndoHelper; use App\Services\LogSystem\TimeTravel; use Doctrine\ORM\EntityManagerInterface; @@ -86,7 +88,17 @@ class LogController extends AbstractController { $this->denyAccessUnlessGranted('@system.show_logs'); - $table = $dataTable->createFromType(LogDataTable::class) + $formRequest = clone $request; + $formRequest->setMethod('GET'); + $filter = new LogFilter(); + + $filterForm = $this->createForm(LogFilterType::class, $filter, ['method' => 'GET']); + + $filterForm->handleRequest($formRequest); + + $table = $dataTable->createFromType(LogDataTable::class, [ + 'filter' => $filter, + ]) ->handleRequest($request); if ($table->isCallback()) { @@ -95,6 +107,7 @@ class LogController extends AbstractController return $this->render('LogSystem/log_list.html.twig', [ 'datatable' => $table, + 'filterForm' => $filterForm->createView(), ]); } diff --git a/src/Controller/PartListsController.php b/src/Controller/PartListsController.php index 32719591..3c883551 100644 --- a/src/Controller/PartListsController.php +++ b/src/Controller/PartListsController.php @@ -42,16 +42,21 @@ declare(strict_types=1); namespace App\Controller; +use App\DataTables\Filters\PartFilter; +use App\DataTables\Filters\PartSearchFilter; use App\DataTables\PartsDataTable; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Storelocation; use App\Entity\Parts\Supplier; +use App\Form\Filters\PartFilterType; use App\Services\Parts\PartsTableActionHandler; +use App\Services\Trees\NodesListBuilder; use Doctrine\ORM\EntityManagerInterface; use Omines\DataTablesBundle\DataTableFactory; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -60,10 +65,14 @@ use Symfony\Component\Routing\Annotation\Route; class PartListsController extends AbstractController { private $entityManager; + private $nodesListBuilder; + private $dataTableFactory; - public function __construct(EntityManagerInterface $entityManager) + public function __construct(EntityManagerInterface $entityManager, NodesListBuilder $nodesListBuilder, DataTableFactory $dataTableFactory) { $this->entityManager = $entityManager; + $this->nodesListBuilder = $nodesListBuilder; + $this->dataTableFactory = $dataTableFactory; } /** @@ -98,24 +107,81 @@ class PartListsController extends AbstractController } /** - * @Route("/category/{id}/parts", name="part_list_category") - * - * @return JsonResponse|Response + * Disable the given form interface after creation of the form by removing and reattaching the form. + * @param FormInterface $form + * @return void */ - public function showCategory(Category $category, Request $request, DataTableFactory $dataTable) + private function disableFormFieldAfterCreation(FormInterface $form, bool $disabled = true): void { - $table = $dataTable->createFromType(PartsDataTable::class, ['category' => $category]) + $attrs = $form->getConfig()->getOptions(); + $attrs['disabled'] = $disabled; + + $parent = $form->getParent(); + if ($parent === null) { + throw new \RuntimeException('This function can only be used on form fields that are children of another form!'); + } + + $parent->remove($form->getName()); + $parent->add($form->getName(), get_class($form->getConfig()->getType()->getInnerType()), $attrs); + } + + /** + * Common implementation for the part list pages. + * @param Request $request The request to parse + * @param string $template The template that should be rendered + * @param callable|null $filter_changer A function that is called with the filter object as parameter. This function can be used to customize the filter + * @param callable|null $form_changer A function that is called with the form object as parameter. This function can be used to customize the form + * @param array $additonal_template_vars Any additional template variables that should be passed to the template + * @param array $additional_table_vars Any additional variables that should be passed to the table creation + * @return Response + */ + protected function showListWithFilter(Request $request, string $template, ?callable $filter_changer = null, ?callable $form_changer = null, array $additonal_template_vars = [], array $additional_table_vars = []): Response + { + $formRequest = clone $request; + $formRequest->setMethod('GET'); + $filter = new PartFilter($this->nodesListBuilder); + if($filter_changer !== null){ + $filter_changer($filter); + } + + $filterForm = $this->createForm(PartFilterType::class, $filter, ['method' => 'GET']); + if($form_changer !== null) { + $form_changer($filterForm); + } + + $filterForm->handleRequest($formRequest); + + $table = $this->dataTableFactory->createFromType(PartsDataTable::class, array_merge(['filter' => $filter], $additional_table_vars)) ->handleRequest($request); if ($table->isCallback()) { return $table->getResponse(); } - return $this->render('Parts/lists/category_list.html.twig', [ + return $this->render($template, array_merge([ 'datatable' => $table, - 'entity' => $category, - 'repo' => $this->entityManager->getRepository(Category::class), - ]); + 'filterForm' => $filterForm->createView(), + ], $additonal_template_vars)); + } + + /** + * @Route("/category/{id}/parts", name="part_list_category") + * + * @return JsonResponse|Response + */ + public function showCategory(Category $category, Request $request) + { + return $this->showListWithFilter($request, + 'Parts/lists/category_list.html.twig', + function (PartFilter $filter) use ($category) { + $filter->getCategory()->setOperator('INCLUDING_CHILDREN')->setValue($category); + }, function (FormInterface $filterForm) { + $this->disableFormFieldAfterCreation($filterForm->get('category')->get('value')); + }, [ + 'entity' => $category, + 'repo' => $this->entityManager->getRepository(Category::class), + ] + ); } /** @@ -123,20 +189,19 @@ class PartListsController extends AbstractController * * @return JsonResponse|Response */ - public function showFootprint(Footprint $footprint, Request $request, DataTableFactory $dataTable) + public function showFootprint(Footprint $footprint, Request $request) { - $table = $dataTable->createFromType(PartsDataTable::class, ['footprint' => $footprint]) - ->handleRequest($request); - - if ($table->isCallback()) { - return $table->getResponse(); - } - - return $this->render('Parts/lists/footprint_list.html.twig', [ - 'datatable' => $table, - 'entity' => $footprint, - 'repo' => $this->entityManager->getRepository(Footprint::class), - ]); + return $this->showListWithFilter($request, + 'Parts/lists/footprint_list.html.twig', + function (PartFilter $filter) use ($footprint) { + $filter->getFootprint()->setOperator('INCLUDING_CHILDREN')->setValue($footprint); + }, function (FormInterface $filterForm) { + $this->disableFormFieldAfterCreation($filterForm->get('footprint')->get('value')); + }, [ + 'entity' => $footprint, + 'repo' => $this->entityManager->getRepository(Footprint::class), + ] + ); } /** @@ -144,20 +209,19 @@ class PartListsController extends AbstractController * * @return JsonResponse|Response */ - public function showManufacturer(Manufacturer $manufacturer, Request $request, DataTableFactory $dataTable) + public function showManufacturer(Manufacturer $manufacturer, Request $request) { - $table = $dataTable->createFromType(PartsDataTable::class, ['manufacturer' => $manufacturer]) - ->handleRequest($request); - - if ($table->isCallback()) { - return $table->getResponse(); - } - - return $this->render('Parts/lists/manufacturer_list.html.twig', [ - 'datatable' => $table, - 'entity' => $manufacturer, - 'repo' => $this->entityManager->getRepository(Manufacturer::class), - ]); + return $this->showListWithFilter($request, + 'Parts/lists/manufacturer_list.html.twig', + function (PartFilter $filter) use ($manufacturer) { + $filter->getManufacturer()->setOperator('INCLUDING_CHILDREN')->setValue($manufacturer); + }, function (FormInterface $filterForm) { + $this->disableFormFieldAfterCreation($filterForm->get('manufacturer')->get('value')); + }, [ + 'entity' => $manufacturer, + 'repo' => $this->entityManager->getRepository(Manufacturer::class), + ] + ); } /** @@ -165,20 +229,19 @@ class PartListsController extends AbstractController * * @return JsonResponse|Response */ - public function showStorelocation(Storelocation $storelocation, Request $request, DataTableFactory $dataTable) + public function showStorelocation(Storelocation $storelocation, Request $request) { - $table = $dataTable->createFromType(PartsDataTable::class, ['storelocation' => $storelocation]) - ->handleRequest($request); - - if ($table->isCallback()) { - return $table->getResponse(); - } - - return $this->render('Parts/lists/store_location_list.html.twig', [ - 'datatable' => $table, - 'entity' => $storelocation, - 'repo' => $this->entityManager->getRepository(Storelocation::class), - ]); + return $this->showListWithFilter($request, + 'Parts/lists/store_location_list.html.twig', + function (PartFilter $filter) use ($storelocation) { + $filter->getStorelocation()->setOperator('INCLUDING_CHILDREN')->setValue($storelocation); + }, function (FormInterface $filterForm) { + $this->disableFormFieldAfterCreation($filterForm->get('storelocation')->get('value')); + }, [ + 'entity' => $storelocation, + 'repo' => $this->entityManager->getRepository(Storelocation::class), + ] + ); } /** @@ -186,20 +249,19 @@ class PartListsController extends AbstractController * * @return JsonResponse|Response */ - public function showSupplier(Supplier $supplier, Request $request, DataTableFactory $dataTable) + public function showSupplier(Supplier $supplier, Request $request) { - $table = $dataTable->createFromType(PartsDataTable::class, ['supplier' => $supplier]) - ->handleRequest($request); - - if ($table->isCallback()) { - return $table->getResponse(); - } - - return $this->render('Parts/lists/supplier_list.html.twig', [ - 'datatable' => $table, - 'entity' => $supplier, - 'repo' => $this->entityManager->getRepository(Supplier::class), - ]); + return $this->showListWithFilter($request, + 'Parts/lists/supplier_list.html.twig', + function (PartFilter $filter) use ($supplier) { + $filter->getSupplier()->setOperator('INCLUDING_CHILDREN')->setValue($supplier); + }, function (FormInterface $filterForm) { + $this->disableFormFieldAfterCreation($filterForm->get('supplier')->get('value')); + }, [ + 'entity' => $supplier, + 'repo' => $this->entityManager->getRepository(Supplier::class), + ] + ); } /** @@ -210,17 +272,37 @@ class PartListsController extends AbstractController public function showTag(string $tag, Request $request, DataTableFactory $dataTable) { $tag = trim($tag); - $table = $dataTable->createFromType(PartsDataTable::class, ['tag' => $tag]) - ->handleRequest($request); - if ($table->isCallback()) { - return $table->getResponse(); - } + return $this->showListWithFilter($request, + 'Parts/lists/tags_list.html.twig', + function (PartFilter $filter) use ($tag) { + $filter->getTags()->setOperator('ANY')->setValue($tag); + }, function (FormInterface $filterForm) { + $this->disableFormFieldAfterCreation($filterForm->get('tags')->get('value')); + }, [ + 'tag' => $tag, + ] + ); + } - return $this->render('Parts/lists/tags_list.html.twig', [ - 'tag' => $tag, - 'datatable' => $table, - ]); + private function searchRequestToFilter(Request $request): PartSearchFilter + { + $filter = new PartSearchFilter($request->query->get('keyword', '')); + + $filter->setName($request->query->getBoolean('name', true)); + $filter->setCategory($request->query->getBoolean('category', true)); + $filter->setDescription($request->query->getBoolean('description', true)); + $filter->setTags($request->query->getBoolean('tags', true)); + $filter->setStorelocation($request->query->getBoolean('storelocation', true)); + $filter->setComment($request->query->getBoolean('comment', true)); + $filter->setOrdernr($request->query->getBoolean('ordernr', true)); + $filter->setSupplier($request->query->getBoolean('supplier', false)); + $filter->setManufacturer($request->query->getBoolean('manufacturer', false)); + $filter->setFootprint($request->query->getBoolean('footprint', false)); + + $filter->setRegex($request->query->getBoolean('regex', false)); + + return $filter; } /** @@ -230,35 +312,20 @@ class PartListsController extends AbstractController */ public function showSearch(Request $request, DataTableFactory $dataTable) { - $search = $request->query->get('keyword', ''); - $search_options = [ - 'name' => $request->query->getBoolean('name'), - 'description' => $request->query->getBoolean('description'), - 'comment' => $request->query->getBoolean('comment'), - 'category' => $request->query->getBoolean('category'), - 'store_location' => $request->query->getBoolean('storelocation'), - 'supplier' => $request->query->getBoolean('supplier'), - 'ordernr' => $request->query->getBoolean('ordernr'), - 'manufacturer' => $request->query->getBoolean('manufacturer'), - 'footprint' => $request->query->getBoolean('footprint'), - 'tags' => $request->query->getBoolean('tags'), - 'regex' => $request->query->getBoolean('regex'), - ]; + $searchFilter = $this->searchRequestToFilter($request); - $table = $dataTable->createFromType(PartsDataTable::class, [ - 'search' => $search, - 'search_options' => $search_options, - ]) - ->handleRequest($request); - - if ($table->isCallback()) { - return $table->getResponse(); - } - - return $this->render('Parts/lists/search_list.html.twig', [ - 'datatable' => $table, - 'keyword' => $search, - ]); + return $this->showListWithFilter($request, + 'Parts/lists/search_list.html.twig', + null, + null, + [ + 'keyword' => $searchFilter->getKeyword(), + 'searchFilter' => $searchFilter, + ], + [ + 'search' => $searchFilter, + ] + ); } /** @@ -268,13 +335,6 @@ class PartListsController extends AbstractController */ public function showAll(Request $request, DataTableFactory $dataTable) { - $table = $dataTable->createFromType(PartsDataTable::class) - ->handleRequest($request); - - if ($table->isCallback()) { - return $table->getResponse(); - } - - return $this->render('Parts/lists/all_list.html.twig', ['datatable' => $table]); + return $this->showListWithFilter($request,'Parts/lists/all_list.html.twig'); } } diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index f69f137e..77edae80 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -42,9 +42,22 @@ declare(strict_types=1); namespace App\Controller; +use App\Entity\Parameters\AttachmentTypeParameter; +use App\Entity\Parameters\CategoryParameter; +use App\Entity\Parameters\DeviceParameter; +use App\Entity\Parameters\FootprintParameter; +use App\Entity\Parameters\GroupParameter; +use App\Entity\Parameters\ManufacturerParameter; +use App\Entity\Parameters\MeasurementUnitParameter; +use App\Entity\Parameters\PartParameter; +use App\Entity\Parameters\StorelocationParameter; +use App\Entity\Parameters\SupplierParameter; +use App\Entity\PriceInformations\Currency; +use App\Repository\ParameterRepository; use App\Services\Attachments\AttachmentURLGenerator; use App\Services\Attachments\BuiltinAttachmentsFinder; use App\Services\TagFinder; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Asset\Packages; use Symfony\Component\HttpFoundation\JsonResponse; @@ -99,6 +112,58 @@ class TypeaheadController extends AbstractController return new JsonResponse($data, 200, [], true); } + /** + * This functions map the parameter type to the class, so we can access its repository + * @param string $type + * @return class-string + */ + private function typeToParameterClass(string $type): string + { + switch ($type) { + case 'category': + return CategoryParameter::class; + case 'part': + return PartParameter::class; + case 'device': + return DeviceParameter::class; + case 'footprint': + return FootprintParameter::class; + case 'manufacturer': + return ManufacturerParameter::class; + case 'storelocation': + return StorelocationParameter::class; + case 'supplier': + return SupplierParameter::class; + case 'attachment_type': + return AttachmentTypeParameter::class; + case 'group': + return GroupParameter::class; + case 'measurement_unit': + return MeasurementUnitParameter::class; + case 'currency': + return Currency::class; + + default: + throw new \InvalidArgumentException('Invalid parameter type: '.$type); + } + } + + /** + * @Route("/parameters/{type}/search/{query}", name="typeahead_parameters", requirements={"type" = ".+"}) + * @param string $query + * @return JsonResponse + */ + public function parameters(string $type, EntityManagerInterface $entityManager, string $query = ""): JsonResponse + { + $class = $this->typeToParameterClass($type); + /** @var ParameterRepository $repository */ + $repository = $entityManager->getRepository($class); + + $data = $repository->autocompleteParamName($query); + + return new JsonResponse($data); + } + /** * @Route("/tags/search/{query}", name="typeahead_tags", requirements={"query"= ".+"}) */ diff --git a/src/DataTables/AttachmentDataTable.php b/src/DataTables/AttachmentDataTable.php index 3a113ccf..122d3abe 100644 --- a/src/DataTables/AttachmentDataTable.php +++ b/src/DataTables/AttachmentDataTable.php @@ -43,14 +43,16 @@ declare(strict_types=1); namespace App\DataTables; use App\DataTables\Column\LocaleDateTimeColumn; +use App\DataTables\Column\PrettyBoolColumn; +use App\DataTables\Filters\AttachmentFilter; use App\Entity\Attachments\Attachment; use App\Services\Attachments\AttachmentManager; use App\Services\Attachments\AttachmentURLGenerator; use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; use Doctrine\ORM\QueryBuilder; +use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; -use Omines\DataTablesBundle\Column\BoolColumn; use Omines\DataTablesBundle\Column\TextColumn; use Omines\DataTablesBundle\DataTable; use Omines\DataTablesBundle\DataTableTypeInterface; @@ -79,6 +81,7 @@ final class AttachmentDataTable implements DataTableTypeInterface { $dataTable->add('picture', TextColumn::class, [ 'label' => '', + 'className' => 'no-colvis', 'render' => function ($value, Attachment $context) { if ($context->isPicture() && !$context->isExternal() @@ -166,7 +169,7 @@ final class AttachmentDataTable implements DataTableTypeInterface } return sprintf( - ' + ' %s ', $this->translator->trans('attachment.file_not_found') @@ -184,37 +187,25 @@ final class AttachmentDataTable implements DataTableTypeInterface 'visible' => false, ]); - $dataTable->add('show_in_table', BoolColumn::class, [ + $dataTable->add('show_in_table', PrettyBoolColumn::class, [ 'label' => 'attachment.edit.show_in_table', - 'trueValue' => $this->translator->trans('true'), - 'falseValue' => $this->translator->trans('false'), - 'nullValue' => '', 'visible' => false, ]); - $dataTable->add('isPicture', BoolColumn::class, [ + $dataTable->add('isPicture', PrettyBoolColumn::class, [ 'label' => 'attachment.edit.isPicture', - 'trueValue' => $this->translator->trans('true'), - 'falseValue' => $this->translator->trans('false'), - 'nullValue' => '', 'visible' => false, 'propertyPath' => 'picture', ]); - $dataTable->add('is3DModel', BoolColumn::class, [ + $dataTable->add('is3DModel', PrettyBoolColumn::class, [ 'label' => 'attachment.edit.is3DModel', - 'trueValue' => $this->translator->trans('true'), - 'falseValue' => $this->translator->trans('false'), - 'nullValue' => '', 'visible' => false, 'propertyPath' => '3dmodel', ]); - $dataTable->add('isBuiltin', BoolColumn::class, [ + $dataTable->add('isBuiltin', PrettyBoolColumn::class, [ 'label' => 'attachment.edit.isBuiltin', - 'trueValue' => $this->translator->trans('true'), - 'falseValue' => $this->translator->trans('false'), - 'nullValue' => '', 'visible' => false, 'propertyPath' => 'builtin', ]); @@ -224,6 +215,12 @@ final class AttachmentDataTable implements DataTableTypeInterface 'query' => function (QueryBuilder $builder): void { $this->getQuery($builder); }, + 'criteria' => [ + function (QueryBuilder $builder) use ($options): void { + $this->buildCriteria($builder, $options); + }, + new SearchCriteriaProvider(), + ], ]); } @@ -236,4 +233,18 @@ final class AttachmentDataTable implements DataTableTypeInterface ->leftJoin('attachment.attachment_type', 'attachment_type'); //->leftJoin('attachment.element', 'element'); } + + private function buildCriteria(QueryBuilder $builder, array $options): void + { + //We do the most stuff here in the filter class + if (isset($options['filter'])) { + if(!$options['filter'] instanceof AttachmentFilter) { + throw new \Exception('filter must be an instance of AttachmentFilter!'); + } + + $filter = $options['filter']; + $filter->apply($builder); + } + + } } diff --git a/src/DataTables/Column/LocaleDateTimeColumn.php b/src/DataTables/Column/LocaleDateTimeColumn.php index f9103f86..c380bada 100644 --- a/src/DataTables/Column/LocaleDateTimeColumn.php +++ b/src/DataTables/Column/LocaleDateTimeColumn.php @@ -58,7 +58,7 @@ class LocaleDateTimeColumn extends AbstractColumn { /** * @param $value - * @return bool|mixed|string + * @return string * @throws Exception */ public function normalize($value): string diff --git a/src/DataTables/Column/PrettyBoolColumn.php b/src/DataTables/Column/PrettyBoolColumn.php new file mode 100644 index 00000000..da6af1c4 --- /dev/null +++ b/src/DataTables/Column/PrettyBoolColumn.php @@ -0,0 +1,49 @@ +translator = $translator; + } + + public function normalize($value): ?bool + { + if (null === $value) { + return null; + } + + return (bool) $value; + } + + public function render($value, $context) + { + if ($value === true) { + return ' ' + . $this->translator->trans('bool.true') + . ''; + } + + if ($value === false) { + return ' ' + . $this->translator->trans('bool.false') + . ''; + } + + if ($value === null) { + return ' ' + . $this->translator->trans('bool.unknown') + . ''; + } + + throw new \RuntimeException('Unexpected value!'); + } +} \ No newline at end of file diff --git a/src/DataTables/Column/SIUnitNumberColumn.php b/src/DataTables/Column/SIUnitNumberColumn.php new file mode 100644 index 00000000..77d2a197 --- /dev/null +++ b/src/DataTables/Column/SIUnitNumberColumn.php @@ -0,0 +1,37 @@ +formatter = $formatter; + } + + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + $resolver->setDefault('precision', 2); + $resolver->setDefault('unit', ''); + + return $this; + } + + public function normalize($value) + { + //Ignore null values + if ($value === null) { + return ''; + } + + return $this->formatter->format((float) $value, $this->options['unit'], $this->options['precision']); + } +} \ No newline at end of file diff --git a/src/DataTables/Column/SelectColumn.php b/src/DataTables/Column/SelectColumn.php new file mode 100644 index 00000000..b4dd17e8 --- /dev/null +++ b/src/DataTables/Column/SelectColumn.php @@ -0,0 +1,38 @@ +setDefaults([ + 'label' => '', + 'orderable' => false, + 'searchable' => false, + 'className' => 'select-checkbox no-colvis', + 'visible' => true, + ]); + + return $this; + } + + public function normalize($value) + { + return $value; + } + + public function render($value, $context) + { + //Return empty string, as it this column is filled by datatables on client side + return ''; + } +} \ No newline at end of file diff --git a/src/DataTables/Column/TagsColumn.php b/src/DataTables/Column/TagsColumn.php index b0364cd7..d02dc1f9 100644 --- a/src/DataTables/Column/TagsColumn.php +++ b/src/DataTables/Column/TagsColumn.php @@ -79,7 +79,7 @@ class TagsColumn extends AbstractColumn break; } $html .= sprintf( - '%s', + '%s', $this->urlGenerator->generate('part_list_tags', ['tag' => $tag]), htmlspecialchars($tag) ); diff --git a/src/DataTables/Filters/AttachmentFilter.php b/src/DataTables/Filters/AttachmentFilter.php new file mode 100644 index 00000000..bbdadca2 --- /dev/null +++ b/src/DataTables/Filters/AttachmentFilter.php @@ -0,0 +1,121 @@ +dbId = new IntConstraint('attachment.id'); + $this->name = new TextConstraint('attachment.name'); + $this->targetType = new InstanceOfConstraint('attachment'); + $this->attachmentType = new EntityConstraint($nodesListBuilder, AttachmentType::class, 'attachment.attachment_type'); + $this->lastModified = new DateTimeConstraint('attachment.lastModified'); + $this->addedDate = new DateTimeConstraint('attachment.addedDate'); + $this->showInTable = new BooleanConstraint('attachment.show_in_table'); + } + + public function apply(QueryBuilder $queryBuilder): void + { + $this->applyAllChildFilters($queryBuilder); + } + + /** + * @return NumberConstraint + */ + public function getDbId(): NumberConstraint + { + return $this->dbId; + } + + /** + * @return TextConstraint + */ + public function getName(): TextConstraint + { + return $this->name; + } + + /** + * @return DateTimeConstraint + */ + public function getLastModified(): DateTimeConstraint + { + return $this->lastModified; + } + + /** + * @return DateTimeConstraint + */ + public function getAddedDate(): DateTimeConstraint + { + return $this->addedDate; + } + + + /** + * @return BooleanConstraint + */ + public function getShowInTable(): BooleanConstraint + { + return $this->showInTable; + } + + + /** + * @return EntityConstraint + */ + public function getAttachmentType(): EntityConstraint + { + return $this->attachmentType; + } + + /** + * @return InstanceOfConstraint + */ + public function getTargetType(): InstanceOfConstraint + { + return $this->targetType; + } + + + + + + +} \ No newline at end of file diff --git a/src/DataTables/Filters/CompoundFilterTrait.php b/src/DataTables/Filters/CompoundFilterTrait.php new file mode 100644 index 00000000..540e291e --- /dev/null +++ b/src/DataTables/Filters/CompoundFilterTrait.php @@ -0,0 +1,57 @@ + $filter_object + * @return FilterInterface[] + */ + protected function findAllChildFilters(): array + { + $filters = []; + $reflection = new \ReflectionClass($this); + + foreach ($reflection->getProperties() as $property) { + //Set property to accessible (otherwise we run into problems on PHP < 8.1) + $property->setAccessible(true); + + $value = $property->getValue($this); + //We only want filters (objects implementing FilterInterface) + if($value instanceof FilterInterface) { + $filters[$property->getName()] = $value; + } + + //Add filters in collections + if ($value instanceof Collection) { + foreach ($value as $key => $filter) { + if($filter instanceof FilterInterface) { + $filters[$property->getName() . '.' . (string) $key] = $filter; + } + } + } + } + return $filters; + } + + /** + * Applies all children filters that are declared as property of this filter using reflection. + * @param QueryBuilder $queryBuilder + * @return void + */ + protected function applyAllChildFilters(QueryBuilder $queryBuilder): void + { + //Retrieve all child filters and apply them + $filters = $this->findAllChildFilters(); + + foreach ($filters as $filter) { + $filter->apply($queryBuilder); + } + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/AbstractConstraint.php b/src/DataTables/Filters/Constraints/AbstractConstraint.php new file mode 100644 index 00000000..0af33ce5 --- /dev/null +++ b/src/DataTables/Filters/Constraints/AbstractConstraint.php @@ -0,0 +1,34 @@ +property = $property; + $this->identifier = $identifier ?? $this->generateParameterIdentifier($property); + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/BooleanConstraint.php b/src/DataTables/Filters/Constraints/BooleanConstraint.php new file mode 100644 index 00000000..c72c7c6e --- /dev/null +++ b/src/DataTables/Filters/Constraints/BooleanConstraint.php @@ -0,0 +1,53 @@ +value = $default_value; + } + + /** + * Gets the value of this constraint. Null means "don't filter", true means "filter for true", false means "filter for false". + * @return bool|null + */ + public function getValue(): ?bool + { + return $this->value; + } + + /** + * Sets the value of this constraint. Null means "don't filter", true means "filter for true", false means "filter for false". + * @param bool|null $value + */ + public function setValue(?bool $value): void + { + $this->value = $value; + } + + public function isEnabled(): bool + { + return $this->value !== null; + } + + + public function apply(QueryBuilder $queryBuilder): void + { + //Do not apply a filter if value is null (filter is set to ignore) + if(!$this->isEnabled()) { + return; + } + + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, '=', $this->value); + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/ChoiceConstraint.php b/src/DataTables/Filters/Constraints/ChoiceConstraint.php new file mode 100644 index 00000000..63737cc0 --- /dev/null +++ b/src/DataTables/Filters/Constraints/ChoiceConstraint.php @@ -0,0 +1,84 @@ +value; + } + + /** + * @param string[]|int[] $value + * @return ChoiceConstraint + */ + public function setValue(array $value): ChoiceConstraint + { + $this->value = $value; + return $this; + } + + /** + * @return string + */ + public function getOperator(): string + { + return $this->operator; + } + + /** + * @param string $operator + * @return ChoiceConstraint + */ + public function setOperator(string $operator): ChoiceConstraint + { + $this->operator = $operator; + return $this; + } + + + + public function isEnabled(): bool + { + return !empty($this->operator); + } + + public function apply(QueryBuilder $queryBuilder): void + { + //If no value is provided then we do not apply a filter + if (!$this->isEnabled()) { + return; + } + + //Ensure we have an valid operator + if(!in_array($this->operator, self::ALLOWED_OPERATOR_VALUES, true)) { + throw new \RuntimeException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES)); + } + + if ($this->operator === 'ANY') { + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'IN', $this->value); + } elseif ($this->operator === 'NONE') { + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'NOT IN', $this->value); + } else { + throw new \RuntimeException('Unknown operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES)); + } + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/DateTimeConstraint.php b/src/DataTables/Filters/Constraints/DateTimeConstraint.php new file mode 100644 index 00000000..4cc3f50f --- /dev/null +++ b/src/DataTables/Filters/Constraints/DateTimeConstraint.php @@ -0,0 +1,10 @@ + The class to use for the comparison + */ + protected $class; + + /** + * @var string|null The operator to use + */ + protected $operator; + + /** + * @var T The value to compare to + */ + protected $value; + + /** + * @param NodesListBuilder|null $nodesListBuilder + * @param class-string $class + * @param string $property + * @param string|null $identifier + * @param null $value + * @param string $operator + */ + public function __construct(?NodesListBuilder $nodesListBuilder, string $class, string $property, string $identifier = null, $value = null, string $operator = '') + { + $this->nodesListBuilder = $nodesListBuilder; + $this->class = $class; + + if ($nodesListBuilder === null && $this->isStructural()) { + throw new \InvalidArgumentException('NodesListBuilder must be provided for structural entities'); + } + + parent::__construct($property, $identifier); + $this->value = $value; + $this->operator = $operator; + } + + public function getClass(): string + { + return $this->class; + } + + /** + * @return string|null + */ + public function getOperator(): ?string + { + return $this->operator; + } + + /** + * @param string|null $operator + */ + public function setOperator(?string $operator): self + { + $this->operator = $operator; + return $this; + } + + /** + * @return T|null + */ + public function getValue(): ?AbstractDBElement + { + return $this->value; + } + + /** + * @param T|null $value + */ + public function setValue(?AbstractDBElement $value): void + { + if (!$value instanceof $this->class) { + throw new \InvalidArgumentException('The value must be an instance of ' . $this->class); + } + + $this->value = $value; + } + + /** + * Checks whether the constraints apply to a structural type or not + * @return bool + */ + public function isStructural(): bool + { + return is_subclass_of($this->class, AbstractStructuralDBElement::class); + } + + /** + * Returns a list of operators which are allowed with the given class. + * @return string[] + */ + public function getAllowedOperatorValues(): array + { + //Base operators are allowed for everything + $tmp = self::ALLOWED_OPERATOR_VALUES_BASE; + + if ($this->isStructural()) { + $tmp = array_merge($tmp, self::ALLOWED_OPERATOR_VALUES_STRUCTURAL); + } + + return $tmp; + } + + public function isEnabled(): bool + { + return !empty($this->operator); + } + + public function apply(QueryBuilder $queryBuilder): void + { + //If no value is provided then we do not apply a filter + if (!$this->isEnabled()) { + return; + } + + //Ensure we have an valid operator + if(!in_array($this->operator, $this->getAllowedOperatorValues(), true)) { + throw new \RuntimeException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', $this->getAllowedOperatorValues())); + } + + //We need to handle null values differently, as they can not be compared with == or != + if ($this->value === null) { + if($this->operator === '=' || $this->operator === 'INCLUDING_CHILDREN') { + $queryBuilder->andWhere(sprintf("%s IS NULL", $this->property)); + return; + } + + if ($this->operator === '!=' || $this->operator === 'EXCLUDING_CHILDREN') { + $queryBuilder->andWhere(sprintf("%s IS NOT NULL", $this->property)); + return; + } + + throw new \RuntimeException('Unknown operator '. $this->operator . ' provided. Valid operators are '. implode(', ', $this->getAllowedOperatorValues())); + } + + if($this->operator === '=' || $this->operator === '!=') { + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, $this->operator, $this->value); + return; + } + + //Otherwise retrieve the children list and apply the operator to it + if($this->isStructural()) { + $list = $this->nodesListBuilder->getChildrenFlatList($this->value); + //Add the element itself to the list + $list[] = $this->value; + + if ($this->operator === 'INCLUDING_CHILDREN') { + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'IN', $list); + return; + } + + if ($this->operator === 'EXCLUDING_CHILDREN') { + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'NOT IN', $list); + return; + } + } else { + throw new \RuntimeException('Cannot apply operator '. $this->operator . ' to non-structural type'); + } + + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/FilterTrait.php b/src/DataTables/Filters/Constraints/FilterTrait.php new file mode 100644 index 00000000..5371a48d --- /dev/null +++ b/src/DataTables/Filters/Constraints/FilterTrait.php @@ -0,0 +1,65 @@ +useHaving = $value; + return $this; + } + + /** + * Checks if the given input is an aggregateFunction like COUNT(part.partsLot) or so + * @return bool + */ + protected function isAggregateFunctionString(string $input): bool + { + return preg_match('/^[a-zA-Z]+\(.*\)$/', $input) === 1; + } + + /** + * Generates a parameter identifier that can be used for the given property. It gives random results, to be unique, so you have to cache it. + * @param string $property + * @return string + */ + protected function generateParameterIdentifier(string $property): string + { + //Replace all special characters with underscores + $property = preg_replace('/[^a-zA-Z0-9_]/', '_', $property); + //Add a random number to the end of the property name for uniqueness + return $property . '_' . uniqid("", false); + } + + /** + * Adds a simple constraint in the form of (property OPERATOR value) (e.g. "part.name = :name") to the given query builder. + * @param QueryBuilder $queryBuilder + * @param string $property + * @param string $comparison_operator + * @param mixed $value + * @return void + */ + protected function addSimpleAndConstraint(QueryBuilder $queryBuilder, string $property, string $parameterIdentifier, string $comparison_operator, $value): void + { + if ($comparison_operator === 'IN' || $comparison_operator === 'NOT IN') { + $expression = sprintf("%s %s (:%s)", $property, $comparison_operator, $parameterIdentifier); + } else { + $expression = sprintf("%s %s :%s", $property, $comparison_operator, $parameterIdentifier); + } + + if($this->useHaving || $this->isAggregateFunctionString($property)) { //If the property is an aggregate function, we have to use the "having" instead of the "where" + $queryBuilder->andHaving($expression); + } else { + $queryBuilder->andWhere($expression); + } + + $queryBuilder->setParameter($parameterIdentifier, $value); + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/InstanceOfConstraint.php b/src/DataTables/Filters/Constraints/InstanceOfConstraint.php new file mode 100644 index 00000000..c3f11e4a --- /dev/null +++ b/src/DataTables/Filters/Constraints/InstanceOfConstraint.php @@ -0,0 +1,96 @@ +value; + } + + /** + * @param string[] $value + * @return $this + */ + public function setValue(array $value): self + { + $this->value = $value; + return $this; + } + + /** + * @return string + */ + public function getOperator(): string + { + return $this->operator; + } + + /** + * @param string $operator + * @return $this + */ + public function setOperator(string $operator): self + { + $this->operator = $operator; + return $this; + } + + + + public function isEnabled(): bool + { + return !empty($this->operator); + } + + public function apply(QueryBuilder $queryBuilder): void + { + //If no value is provided then we do not apply a filter + if (!$this->isEnabled()) { + return; + } + + //Ensure we have an valid operator + if(!in_array($this->operator, self::ALLOWED_OPERATOR_VALUES, true)) { + throw new \RuntimeException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES)); + } + + $expressions = []; + + if ($this->operator === 'ANY' || $this->operator === 'NONE') { + foreach($this->value as $value) { + //We cannnot use an paramater here, as this is the only way to pass the FCQN to the query (via binded params, we would need to use ClassMetaData). See: https://github.com/doctrine/orm/issues/4462 + $expressions[] = ($queryBuilder->expr()->isInstanceOf($this->property, $value)); + } + + if($this->operator === 'ANY') { + $queryBuilder->andWhere($queryBuilder->expr()->orX(...$expressions)); + } else { //NONE + $queryBuilder->andWhere($queryBuilder->expr()->not($queryBuilder->expr()->orX(...$expressions))); + } + } else { + throw new \RuntimeException('Unknown operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES)); + } + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/IntConstraint.php b/src/DataTables/Filters/Constraints/IntConstraint.php new file mode 100644 index 00000000..2df3864e --- /dev/null +++ b/src/DataTables/Filters/Constraints/IntConstraint.php @@ -0,0 +1,20 @@ +value1 !== null) { + $this->value1 = (int) $this->value1; + } + if($this->value2 !== null) { + $this->value2 = (int) $this->value2; + } + + parent::apply($queryBuilder); + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/NumberConstraint.php b/src/DataTables/Filters/Constraints/NumberConstraint.php new file mode 100644 index 00000000..bacd289a --- /dev/null +++ b/src/DataTables/Filters/Constraints/NumberConstraint.php @@ -0,0 +1,117 @@ +', '<=', '>=', 'BETWEEN']; + + + /** + * The value1 used for comparison (this is the main one used for all mono-value comparisons) + * @var float|null|int|\DateTimeInterface + */ + protected $value1; + + /** + * The second value used when operator is RANGE; this is the upper bound of the range + * @var float|null|int|\DateTimeInterface + */ + protected $value2; + + /** + * @var string The operator to use + */ + protected $operator; + + /** + * @return float|int|null|\DateTimeInterface + */ + public function getValue1() + { + return $this->value1; + } + + /** + * @param float|int|\DateTimeInterface|null $value1 + */ + public function setValue1($value1): void + { + $this->value1 = $value1; + } + + /** + * @return float|int|null + */ + public function getValue2() + { + return $this->value2; + } + + /** + * @param float|int|null $value2 + */ + public function setValue2($value2): void + { + $this->value2 = $value2; + } + + /** + * @return string + */ + public function getOperator(): string + { + return $this->operator; + } + + /** + * @param string $operator + */ + public function setOperator(?string $operator): void + { + $this->operator = $operator; + } + + + public function __construct(string $property, string $identifier = null, $value1 = null, string $operator = null, $value2 = null) + { + parent::__construct($property, $identifier); + $this->value1 = $value1; + $this->value2 = $value2; + $this->operator = $operator; + } + + public function isEnabled(): bool + { + return $this->value1 !== null + && !empty($this->operator); + } + + public function apply(QueryBuilder $queryBuilder): void + { + //If no value is provided then we do not apply a filter + if (!$this->isEnabled()) { + return; + } + + //Ensure we have an valid operator + if(!in_array($this->operator, self::ALLOWED_OPERATOR_VALUES, true)) { + throw new \RuntimeException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES)); + } + + if ($this->operator !== 'BETWEEN') { + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, $this->operator, $this->value1); + } else { + if ($this->value2 === null) { + throw new RuntimeException("Cannot use operator BETWEEN without value2!"); + } + + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '1', '>=', $this->value1); + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '2', '<=', $this->value2); + } + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/Part/ParameterConstraint.php b/src/DataTables/Filters/Constraints/Part/ParameterConstraint.php new file mode 100644 index 00000000..58509546 --- /dev/null +++ b/src/DataTables/Filters/Constraints/Part/ParameterConstraint.php @@ -0,0 +1,160 @@ +alias = uniqid('param_', false); + + $this->value_text = new TextConstraint($this->alias . '.value_text'); + $this->value = new ParameterValueConstraint($this->alias ); + } + + public function isEnabled(): bool + { + return true; + } + + public function apply(QueryBuilder $queryBuilder): void + { + //Create a new qb to build the subquery + $subqb = new QueryBuilder($queryBuilder->getEntityManager()); + + + + $subqb->select('COUNT(' . $this->alias . ')') + ->from(PartParameter::class, $this->alias) + ->where($this->alias . '.element = part'); + + if (!empty($this->name)) { + $paramName = $this->generateParameterIdentifier('params.name'); + $subqb->andWhere($this->alias . '.name = :' . $paramName); + $queryBuilder->setParameter($paramName, $this->name); + } + + if (!empty($this->symbol)) { + $paramName = $this->generateParameterIdentifier('params.symbol'); + $subqb->andWhere($this->alias . '.symbol = :' . $paramName); + $queryBuilder->setParameter($paramName, $this->symbol); + } + + if (!empty($this->unit)) { + $paramName = $this->generateParameterIdentifier('params.unit'); + $subqb->andWhere($this->alias . '.unit = :' . $paramName); + $queryBuilder->setParameter($paramName, $this->unit); + } + + //Apply all subfilters + $this->value_text->apply($subqb); + $this->value->apply($subqb); + + //Copy all parameters from the subquery to the main query + //We can not use setParameters here, as this would override the exiting paramaters in queryBuilder + foreach ($subqb->getParameters() as $parameter) { + $queryBuilder->setParameter($parameter->getName(), $parameter->getValue()); + } + + $queryBuilder->andWhere('(' . $subqb->getDQL() . ') > 0'); + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param string $name + * @return ParameterConstraint + */ + public function setName(string $name): ParameterConstraint + { + $this->name = $name; + return $this; + } + + /** + * @return string + */ + public function getSymbol(): string + { + return $this->symbol; + } + + /** + * @param string $symbol + * @return ParameterConstraint + */ + public function setSymbol(string $symbol): ParameterConstraint + { + $this->symbol = $symbol; + return $this; + } + + /** + * @return string + */ + public function getUnit(): string + { + return $this->unit; + } + + /** + * @param string $unit + * @return ParameterConstraint + */ + public function setUnit(string $unit): ParameterConstraint + { + $this->unit = $unit; + return $this; + } + + /** + * @return TextConstraint + */ + public function getValueText(): TextConstraint + { + return $this->value_text; + } + + /** + * @return ParameterValueConstraint + */ + public function getValue(): ParameterValueConstraint + { + return $this->value; + } + + +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/Part/ParameterValueConstraint.php b/src/DataTables/Filters/Constraints/Part/ParameterValueConstraint.php new file mode 100644 index 00000000..27b91965 --- /dev/null +++ b/src/DataTables/Filters/Constraints/Part/ParameterValueConstraint.php @@ -0,0 +1,130 @@ +', '<=', '>=', 'BETWEEN', + //Additional operators + 'IN_RANGE', 'NOT_IN_RANGE', 'GREATER_THAN_RANGE', 'GREATER_EQUAL_RANGE', 'LESS_THAN_RANGE', 'LESS_EQUAL_RANGE', 'RANGE_IN_RANGE', 'RANGE_INTERSECT_RANGE']; + + /** + * @param string $alias The alias which is used in the sub query of ParameterConstraint + */ + public function __construct(string $alias) { + $this->alias = $alias; + + parent::__construct($alias . '.value_typical'); + } + + public function apply(QueryBuilder $queryBuilder): void + { + //Skip if not enabled + if(!$this->isEnabled()) { + return; + } + + $paramName1 = $this->generateParameterIdentifier('value1'); + $paramName2 = $this->generateParameterIdentifier('value2'); + + if ($this->operator === 'IN_RANGE') { + + $queryBuilder->andWhere( + "({$this->alias}.value_min <= :{$paramName1} AND {$this->alias}.value_max >= :{$paramName1}) OR + ({$this->alias}.value_typical = :{$paramName1})" + ); + + $queryBuilder->setParameter($paramName1, $this->value1); + + return; + } + + if ($this->operator === 'NOT_IN_RANGE') { + + $queryBuilder->andWhere( + "({$this->alias}.value_min > :{$paramName1} OR {$this->alias}.value_max < :{$paramName1}) AND + ({$this->alias}.value_typical IS NULL OR {$this->alias}.value_typical != :{$paramName1})" + ); + + $queryBuilder->setParameter($paramName1, $this->value1); + + return; + } + + if ($this->operator === 'GREATER_THAN_RANGE') { + $queryBuilder->andWhere( + "{$this->alias}.value_max < :{$paramName1} OR {$this->alias}.value_typical < :{$paramName1}" + ); + + $queryBuilder->setParameter($paramName1, $this->value1); + + return; + } + + if ($this->operator === 'GREATER_EQUAL_RANGE') { + $queryBuilder->andWhere( + "{$this->alias}.value_max <= :{$paramName1} OR {$this->alias}.value_typical <= :{$paramName1}" + ); + + $queryBuilder->setParameter($paramName1, $this->value1); + + return; + } + + if ($this->operator === 'LESS_THAN_RANGE') { + $queryBuilder->andWhere( + "{$this->alias}.value_min > :{$paramName1} OR {$this->alias}.value_typical > :{$paramName1}" + ); + + $queryBuilder->setParameter($paramName1, $this->value1); + + return; + } + + if ($this->operator === 'LESS_EQUAL_RANGE') { + $queryBuilder->andWhere( + "{$this->alias}.value_min >= :{$paramName1} OR {$this->alias}.value_typical >= :{$paramName1}" + ); + + $queryBuilder->setParameter($paramName1, $this->value1); + + return; + } + + // This operator means the constraint range must lie completely within the parameter value range + if ($this->operator === 'RANGE_IN_RANGE') { + $queryBuilder->andWhere( + "({$this->alias}.value_min <= :{$paramName1} AND {$this->alias}.value_max >= :{$paramName2}) OR + ({$this->alias}.value_typical >= :{$paramName1} AND {$this->alias}.value_typical <= :{$paramName2})" + ); + + $queryBuilder->setParameter($paramName1, $this->value1); + $queryBuilder->setParameter($paramName2, $this->value2); + + return; + } + + if ($this->operator === 'RANGE_INTERSECT_RANGE') { + $queryBuilder->andWhere( + //The ORs are important here!! + "({$this->alias}.value_min <= :{$paramName1} OR {$this->alias}.value_min <= :{$paramName2}) OR + ({$this->alias}.value_max >= :{$paramName1} OR {$this->alias}.value_max >= :{$paramName2}) OR + ({$this->alias}.value_typical >= :{$paramName1} AND {$this->alias}.value_typical <= :{$paramName2})" + ); + + $queryBuilder->setParameter($paramName1, $this->value1); + $queryBuilder->setParameter($paramName2, $this->value2); + + return; + } + + + //For all other cases use the default implementation + parent::apply($queryBuilder); + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/Part/TagsConstraint.php b/src/DataTables/Filters/Constraints/Part/TagsConstraint.php new file mode 100644 index 00000000..ecfb6274 --- /dev/null +++ b/src/DataTables/Filters/Constraints/Part/TagsConstraint.php @@ -0,0 +1,137 @@ +value = $value; + $this->operator = $operator; + } + + /** + * @return string + */ + public function getOperator(): ?string + { + return $this->operator; + } + + /** + * @param string $operator + */ + public function setOperator(?string $operator): self + { + $this->operator = $operator; + return $this; + } + + /** + * @return string + */ + public function getValue(): string + { + return $this->value; + } + + /** + * @param string $value + */ + public function setValue(string $value): self + { + $this->value = $value; + return $this; + } + + public function isEnabled(): bool + { + return $this->value !== null + && !empty($this->operator); + } + + /** + * Returns a list of tags based on the comma separated tags list + * @return string[] + */ + public function getTags(): array + { + return explode(',', trim($this->value, ',')); + } + + /** + * Builds an expression to query for a single tag + * @param QueryBuilder $queryBuilder + * @param string $tag + * @return Expr\Orx + */ + protected function getExpressionForTag(QueryBuilder $queryBuilder, string $tag): Expr\Orx + { + $tag_identifier_prefix = uniqid($this->identifier . '_', false); + + $expr = $queryBuilder->expr(); + + $tmp = $expr->orX( + $expr->like($this->property, ':' . $tag_identifier_prefix . '_1'), + $expr->like($this->property, ':' . $tag_identifier_prefix . '_2'), + $expr->like($this->property, ':' . $tag_identifier_prefix . '_3'), + $expr->eq($this->property, ':' . $tag_identifier_prefix . '_4'), + ); + + //Set the parameters for the LIKE expression, in each variation of the tag (so with a comma, at the end, at the beginning, and on both ends, and equaling the tag) + $queryBuilder->setParameter($tag_identifier_prefix . '_1', '%,' . $tag . ',%'); + $queryBuilder->setParameter($tag_identifier_prefix . '_2', '%,' . $tag); + $queryBuilder->setParameter($tag_identifier_prefix . '_3', $tag . ',%'); + $queryBuilder->setParameter($tag_identifier_prefix . '_4', $tag); + + return $tmp; + } + + public function apply(QueryBuilder $queryBuilder): void + { + if(!$this->isEnabled()) { + return; + } + + if(!in_array($this->operator, self::ALLOWED_OPERATOR_VALUES, true)) { + throw new \RuntimeException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES)); + } + + $tagsExpressions = []; + foreach ($this->getTags() as $tag) { + $tagsExpressions[] = $this->getExpressionForTag($queryBuilder, $tag); + } + + if ($this->operator === 'ANY') { + $queryBuilder->andWhere($queryBuilder->expr()->orX(...$tagsExpressions)); + return; + } + + if ($this->operator === 'ALL') { + $queryBuilder->andWhere($queryBuilder->expr()->andX(...$tagsExpressions)); + return; + } + + if ($this->operator === 'NONE') { + $queryBuilder->andWhere($queryBuilder->expr()->not($queryBuilder->expr()->orX(...$tagsExpressions))); + return; + } + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/TextConstraint.php b/src/DataTables/Filters/Constraints/TextConstraint.php new file mode 100644 index 00000000..3ce5c7eb --- /dev/null +++ b/src/DataTables/Filters/Constraints/TextConstraint.php @@ -0,0 +1,109 @@ +value = $value; + $this->operator = $operator; + } + + /** + * @return string + */ + public function getOperator(): ?string + { + return $this->operator; + } + + /** + * @param string $operator + */ + public function setOperator(?string $operator): self + { + $this->operator = $operator; + return $this; + } + + /** + * @return string + */ + public function getValue(): string + { + return $this->value; + } + + /** + * @param string $value + */ + public function setValue(string $value): self + { + $this->value = $value; + return $this; + } + + public function isEnabled(): bool + { + return $this->value !== null + && !empty($this->operator); + } + + public function apply(QueryBuilder $queryBuilder): void + { + if(!$this->isEnabled()) { + return; + } + + if(!in_array($this->operator, self::ALLOWED_OPERATOR_VALUES, true)) { + throw new \RuntimeException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES)); + } + + //Equal and not equal can be handled easily + if($this->operator === '=' || $this->operator === '!=') { + + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, $this->operator, $this->value); + return; + } + + //The CONTAINS, LIKE, STARTS and ENDS operators use the LIKE operator but we have to build the value string differently + $like_value = null; + if ($this->operator === 'LIKE') { + $like_value = $this->value; + } else if ($this->operator === 'STARTS') { + $like_value = $this->value . '%'; + } else if ($this->operator === 'ENDS') { + $like_value = '%' . $this->value; + } else if ($this->operator === 'CONTAINS') { + $like_value = '%' . $this->value . '%'; + } + + if ($like_value !== null) { + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'LIKE', $like_value); + return; + } + + //Regex is only supported on MySQL and needs a special function + if ($this->operator === 'REGEX') { + $queryBuilder->andWhere(sprintf('REGEXP(%s, :%s) = 1', $this->property, $this->identifier)); + $queryBuilder->setParameter($this->identifier, $this->value); + } + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/FilterInterface.php b/src/DataTables/Filters/FilterInterface.php new file mode 100644 index 00000000..0a13a4aa --- /dev/null +++ b/src/DataTables/Filters/FilterInterface.php @@ -0,0 +1,16 @@ +timestamp = new DateTimeConstraint('log.timestamp'); + $this->dbId = new IntConstraint('log.id'); + $this->level = new ChoiceConstraint('log.level'); + $this->eventType = new InstanceOfConstraint('log'); + $this->user = new EntityConstraint(null, User::class, 'log.user'); + + $this->targetType = new ChoiceConstraint('log.target_type'); + $this->targetId = new IntConstraint('log.target_id'); + } + + public function apply(QueryBuilder $queryBuilder): void + { + $this->applyAllChildFilters($queryBuilder); + } + + /** + * @return DateTimeConstraint + */ + public function getTimestamp(): DateTimeConstraint + { + return $this->timestamp; + } + + /** + * @return IntConstraint|NumberConstraint + */ + public function getDbId() + { + return $this->dbId; + } + + /** + * @return ChoiceConstraint + */ + public function getLevel(): ChoiceConstraint + { + return $this->level; + } + + /** + * @return InstanceOfConstraint + */ + public function getEventType(): InstanceOfConstraint + { + return $this->eventType; + } + + /** + * @return ChoiceConstraint + */ + public function getTargetType(): ChoiceConstraint + { + return $this->targetType; + } + + /** + * @return IntConstraint + */ + public function getTargetId(): IntConstraint + { + return $this->targetId; + } + + public function getUser(): EntityConstraint + { + return $this->user; + } + + +} \ No newline at end of file diff --git a/src/DataTables/Filters/PartFilter.php b/src/DataTables/Filters/PartFilter.php new file mode 100644 index 00000000..2d3e6087 --- /dev/null +++ b/src/DataTables/Filters/PartFilter.php @@ -0,0 +1,427 @@ + */ + protected $parameters; + + /** @var IntConstraint */ + protected $parametersCount; + + public function __construct(NodesListBuilder $nodesListBuilder) + { + $this->name = new TextConstraint('part.name'); + $this->description = new TextConstraint('part.description'); + $this->comment = new TextConstraint('part.comment'); + $this->category = new EntityConstraint($nodesListBuilder, Category::class, 'part.category'); + $this->footprint = new EntityConstraint($nodesListBuilder, Footprint::class, 'part.footprint'); + $this->tags = new TagsConstraint('part.tags'); + + $this->favorite = new BooleanConstraint('part.favorite'); + $this->needsReview = new BooleanConstraint('part.needs_review'); + $this->measurementUnit = new EntityConstraint($nodesListBuilder, MeasurementUnit::class, 'part.partUnit'); + $this->mass = new NumberConstraint('part.mass'); + $this->dbId = new IntConstraint('part.id'); + $this->addedDate = new DateTimeConstraint('part.addedDate'); + $this->lastModified = new DateTimeConstraint('part.lastModified'); + + $this->minAmount = new NumberConstraint('part.minamount'); + /* We have to use an IntConstraint here because otherwise we get just an empty result list when applying the filter + This seems to be related to the fact, that PDO does not have an float parameter type and using string type does not work in this situation (at least in SQLite) + TODO: Find a better solution here + */ + $this->amountSum = new IntConstraint('amountSum'); + $this->lotCount = new IntConstraint('COUNT(partLots)'); + + $this->storelocation = new EntityConstraint($nodesListBuilder, Storelocation::class, 'partLots.storage_location'); + $this->lotNeedsRefill = new BooleanConstraint('partLots.needs_refill'); + $this->lotUnknownAmount = new BooleanConstraint('partLots.instock_unknown'); + $this->lotExpirationDate = new DateTimeConstraint('partLots.expiration_date'); + $this->lotDescription = new TextConstraint('partLots.description'); + + $this->manufacturer = new EntityConstraint($nodesListBuilder, Manufacturer::class, 'part.manufacturer'); + $this->manufacturer_product_number = new TextConstraint('part.manufacturer_product_number'); + $this->manufacturer_product_url = new TextConstraint('part.manufacturer_product_url'); + $this->manufacturing_status = new ChoiceConstraint('part.manufacturing_status'); + + $this->attachmentsCount = new IntConstraint('COUNT(attachments)'); + $this->attachmentType = new EntityConstraint($nodesListBuilder, AttachmentType::class, 'attachments.attachment_type'); + $this->attachmentName = new TextConstraint('attachments.name'); + + $this->supplier = new EntityConstraint($nodesListBuilder, Supplier::class, 'orderdetails.supplier'); + $this->orderdetailsCount = new IntConstraint('COUNT(orderdetails)'); + $this->obsolete = new BooleanConstraint('orderdetails.obsolete'); + + $this->parameters = new ArrayCollection(); + $this->parametersCount = new IntConstraint('COUNT(parameters)'); + } + + public function apply(QueryBuilder $queryBuilder): void + { + $this->applyAllChildFilters($queryBuilder); + } + + + /** + * @return BooleanConstraint|false + */ + public function getFavorite() + { + return $this->favorite; + } + + /** + * @return BooleanConstraint + */ + public function getNeedsReview(): BooleanConstraint + { + return $this->needsReview; + } + + public function getMass(): NumberConstraint + { + return $this->mass; + } + + public function getName(): TextConstraint + { + return $this->name; + } + + public function getDescription(): TextConstraint + { + return $this->description; + } + + /** + * @return DateTimeConstraint + */ + public function getLastModified(): DateTimeConstraint + { + return $this->lastModified; + } + + /** + * @return DateTimeConstraint + */ + public function getAddedDate(): DateTimeConstraint + { + return $this->addedDate; + } + + public function getCategory(): EntityConstraint + { + return $this->category; + } + + /** + * @return EntityConstraint + */ + public function getFootprint(): EntityConstraint + { + return $this->footprint; + } + + /** + * @return EntityConstraint + */ + public function getManufacturer(): EntityConstraint + { + return $this->manufacturer; + } + + /** + * @return EntityConstraint + */ + public function getSupplier(): EntityConstraint + { + return $this->supplier; + } + + /** + * @return EntityConstraint + */ + public function getStorelocation(): EntityConstraint + { + return $this->storelocation; + } + + /** + * @return EntityConstraint + */ + public function getMeasurementUnit(): EntityConstraint + { + return $this->measurementUnit; + } + + /** + * @return NumberConstraint + */ + public function getDbId(): NumberConstraint + { + return $this->dbId; + } + + /** + * @return TextConstraint + */ + public function getComment(): TextConstraint + { + return $this->comment; + } + + /** + * @return NumberConstraint + */ + public function getMinAmount(): NumberConstraint + { + return $this->minAmount; + } + + /** + * @return TextConstraint + */ + public function getManufacturerProductUrl(): TextConstraint + { + return $this->manufacturer_product_url; + } + + /** + * @return TextConstraint + */ + public function getManufacturerProductNumber(): TextConstraint + { + return $this->manufacturer_product_number; + } + + public function getLotCount(): NumberConstraint + { + return $this->lotCount; + } + + /** + * @return TagsConstraint + */ + public function getTags(): TagsConstraint + { + return $this->tags; + } + + /** + * @return IntConstraint + */ + public function getOrderdetailsCount(): IntConstraint + { + return $this->orderdetailsCount; + } + + /** + * @return IntConstraint + */ + public function getAttachmentsCount(): IntConstraint + { + return $this->attachmentsCount; + } + + /** + * @return BooleanConstraint + */ + public function getLotNeedsRefill(): BooleanConstraint + { + return $this->lotNeedsRefill; + } + + /** + * @return BooleanConstraint + */ + public function getLotUnknownAmount(): BooleanConstraint + { + return $this->lotUnknownAmount; + } + + /** + * @return DateTimeConstraint + */ + public function getLotExpirationDate(): DateTimeConstraint + { + return $this->lotExpirationDate; + } + + /** + * @return EntityConstraint + */ + public function getAttachmentType(): EntityConstraint + { + return $this->attachmentType; + } + + /** + * @return TextConstraint + */ + public function getAttachmentName(): TextConstraint + { + return $this->attachmentName; + } + + public function getManufacturingStatus(): ChoiceConstraint + { + return $this->manufacturing_status; + } + + public function getAmountSum(): NumberConstraint + { + return $this->amountSum; + } + + /** + * @return ArrayCollection + */ + public function getParameters(): ArrayCollection + { + return $this->parameters; + } + + public function getParametersCount(): IntConstraint + { + return $this->parametersCount; + } + + /** + * @return TextConstraint + */ + public function getLotDescription(): TextConstraint + { + return $this->lotDescription; + } + + /** + * @return BooleanConstraint + */ + public function getObsolete(): BooleanConstraint + { + return $this->obsolete; + } + + + + +} diff --git a/src/DataTables/Filters/PartSearchFilter.php b/src/DataTables/Filters/PartSearchFilter.php new file mode 100644 index 00000000..01538a16 --- /dev/null +++ b/src/DataTables/Filters/PartSearchFilter.php @@ -0,0 +1,359 @@ +keyword = $query; + } + + protected function getFieldsToSearch(): array + { + $fields_to_search = []; + + if($this->name) { + $fields_to_search[] = 'part.name'; + } + if($this->category) { + $fields_to_search[] = 'category.name'; + } + if($this->description) { + $fields_to_search[] = 'part.description'; + } + if($this->tags) { + $fields_to_search[] = 'part.tags'; + } + if($this->storelocation) { + $fields_to_search[] = 'storelocations.name'; + } + if($this->ordernr) { + $fields_to_search[] = 'orderdetails.supplierpartnr'; + } + if($this->mpn) { + $fields_to_search[] = 'part.manufacturer_product_url'; + } + if($this->supplier) { + $fields_to_search[] = 'suppliers.name'; + } + if($this->manufacturer) { + $fields_to_search[] = 'manufacturer.name'; + } + if($this->footprint) { + $fields_to_search[] = 'footprint.name'; + } + + return $fields_to_search; + } + + public function apply(QueryBuilder $queryBuilder): void + { + $fields_to_search = $this->getFieldsToSearch(); + + //If we have nothing to search for, do nothing + if (empty($fields_to_search) || empty($this->keyword)) { + return; + } + + //Convert the fields to search to a list of expressions + $expressions = array_map(function (string $field) { + if ($this->regex) { + return sprintf("REGEXP(%s, :search_query) = 1", $field); + } + + return sprintf("%s LIKE :search_query", $field); + }, $fields_to_search); + + //Add Or concatation of the expressions to our query + $queryBuilder->andWhere( + $queryBuilder->expr()->orX(...$expressions) + ); + + //For regex we pass the query as is, for like we add % to the start and end as wildcards + if ($this->regex) { + $queryBuilder->setParameter('search_query', $this->keyword); + } else { + $queryBuilder->setParameter('search_query', '%' . $this->keyword . '%'); + } + } + + /** + * @return string + */ + public function getKeyword(): string + { + return $this->keyword; + } + + /** + * @param string $keyword + * @return PartSearchFilter + */ + public function setKeyword(string $keyword): PartSearchFilter + { + $this->keyword = $keyword; + return $this; + } + + /** + * @return bool + */ + public function isRegex(): bool + { + return $this->regex; + } + + /** + * @param bool $regex + * @return PartSearchFilter + */ + public function setRegex(bool $regex): PartSearchFilter + { + $this->regex = $regex; + return $this; + } + + /** + * @return bool + */ + public function isName(): bool + { + return $this->name; + } + + /** + * @param bool $name + * @return PartSearchFilter + */ + public function setName(bool $name): PartSearchFilter + { + $this->name = $name; + return $this; + } + + /** + * @return bool + */ + public function isCategory(): bool + { + return $this->category; + } + + /** + * @param bool $category + * @return PartSearchFilter + */ + public function setCategory(bool $category): PartSearchFilter + { + $this->category = $category; + return $this; + } + + /** + * @return bool + */ + public function isDescription(): bool + { + return $this->description; + } + + /** + * @param bool $description + * @return PartSearchFilter + */ + public function setDescription(bool $description): PartSearchFilter + { + $this->description = $description; + return $this; + } + + /** + * @return bool + */ + public function isTags(): bool + { + return $this->tags; + } + + /** + * @param bool $tags + * @return PartSearchFilter + */ + public function setTags(bool $tags): PartSearchFilter + { + $this->tags = $tags; + return $this; + } + + /** + * @return bool + */ + public function isStorelocation(): bool + { + return $this->storelocation; + } + + /** + * @param bool $storelocation + * @return PartSearchFilter + */ + public function setStorelocation(bool $storelocation): PartSearchFilter + { + $this->storelocation = $storelocation; + return $this; + } + + /** + * @return bool + */ + public function isOrdernr(): bool + { + return $this->ordernr; + } + + /** + * @param bool $ordernr + * @return PartSearchFilter + */ + public function setOrdernr(bool $ordernr): PartSearchFilter + { + $this->ordernr = $ordernr; + return $this; + } + + /** + * @return bool + */ + public function isMpn(): bool + { + return $this->mpn; + } + + /** + * @param bool $mpn + * @return PartSearchFilter + */ + public function setMpn(bool $mpn): PartSearchFilter + { + $this->mpn = $mpn; + return $this; + } + + /** + * @return bool + */ + public function isSupplier(): bool + { + return $this->supplier; + } + + /** + * @param bool $supplier + * @return PartSearchFilter + */ + public function setSupplier(bool $supplier): PartSearchFilter + { + $this->supplier = $supplier; + return $this; + } + + /** + * @return bool + */ + public function isManufacturer(): bool + { + return $this->manufacturer; + } + + /** + * @param bool $manufacturer + * @return PartSearchFilter + */ + public function setManufacturer(bool $manufacturer): PartSearchFilter + { + $this->manufacturer = $manufacturer; + return $this; + } + + /** + * @return bool + */ + public function isFootprint(): bool + { + return $this->footprint; + } + + /** + * @param bool $footprint + * @return PartSearchFilter + */ + public function setFootprint(bool $footprint): PartSearchFilter + { + $this->footprint = $footprint; + return $this; + } + + /** + * @return bool + */ + public function isComment(): bool + { + return $this->comment; + } + + /** + * @param bool $comment + * @return PartSearchFilter + */ + public function setComment(bool $comment): PartSearchFilter + { + $this->comment = $comment; + return $this; + } + + +} \ No newline at end of file diff --git a/src/DataTables/LogDataTable.php b/src/DataTables/LogDataTable.php index 3b91c5f2..e675d681 100644 --- a/src/DataTables/LogDataTable.php +++ b/src/DataTables/LogDataTable.php @@ -47,6 +47,8 @@ use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\LogEntryExtraColumn; use App\DataTables\Column\LogEntryTargetColumn; use App\DataTables\Column\RevertLogColumn; +use App\DataTables\Filters\AttachmentFilter; +use App\DataTables\Filters\LogFilter; use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\TimeTravelInterface; use App\Entity\LogSystem\AbstractLogEntry; @@ -61,6 +63,7 @@ use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; +use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; use Omines\DataTablesBundle\Column\TextColumn; use Omines\DataTablesBundle\DataTable; @@ -97,10 +100,12 @@ class LogDataTable implements DataTableTypeInterface $optionsResolver->setDefaults([ 'mode' => 'system_log', 'filter_elements' => [], + 'filter' => null, ]); $optionsResolver->setAllowedTypes('filter_elements', ['array', 'object']); $optionsResolver->setAllowedTypes('mode', 'string'); + $optionsResolver->setAllowedTypes('filter', ['null', LogFilter::class]); $optionsResolver->setNormalizer('filter_elements', static function (Options $options, $value) { if (!is_array($value)) { @@ -121,6 +126,7 @@ class LogDataTable implements DataTableTypeInterface $dataTable->add('symbol', TextColumn::class, [ 'label' => '', + 'className' => 'no-colvis', 'render' => static function ($value, AbstractLogEntry $context) { switch ($context->getLevelString()) { case LogLevel::DEBUG: @@ -191,8 +197,8 @@ class LogDataTable implements DataTableTypeInterface 'label' => $this->translator->trans('log.level'), 'visible' => 'system_log' === $options['mode'], 'propertyPath' => 'levelString', - 'render' => static function (string $value, AbstractLogEntry $context) { - return $value; + 'render' => function (string $value, AbstractLogEntry $context) { + return $this->translator->trans('log.level.'.$value); }, ]); @@ -270,9 +276,24 @@ class LogDataTable implements DataTableTypeInterface 'query' => function (QueryBuilder $builder) use ($options): void { $this->getQuery($builder, $options); }, + 'criteria' => [ + function (QueryBuilder $builder) use ($options): void { + $this->buildCriteria($builder, $options); + }, + new SearchCriteriaProvider(), + ], ]); } + private function buildCriteria(QueryBuilder $builder, array $options): void + { + if (!empty($options['filter'])) { + $filter = $options['filter']; + $filter->apply($builder); + } + + } + protected function getQuery(QueryBuilder $builder, array $options): void { $builder->distinct()->select('log') @@ -280,6 +301,7 @@ class LogDataTable implements DataTableTypeInterface ->from(AbstractLogEntry::class, 'log') ->leftJoin('log.user', 'user'); + /* Do this here as we don't want to show up the global count of all log entries in the footer line, with these modes */ if ('last_activity' === $options['mode']) { $builder->where('log INSTANCE OF '.ElementCreatedLogEntry::class) ->orWhere('log INSTANCE OF '.ElementDeletedLogEntry::class) diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index db81b348..e9be7f6a 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -47,11 +47,17 @@ use App\DataTables\Column\IconLinkColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; use App\DataTables\Column\PartAttachmentsColumn; +use App\DataTables\Column\PrettyBoolColumn; +use App\DataTables\Column\SelectColumn; +use App\DataTables\Column\SIUnitNumberColumn; use App\DataTables\Column\TagsColumn; +use App\DataTables\Filters\PartFilter; +use App\DataTables\Filters\PartSearchFilter; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; use App\Entity\Parts\Storelocation; use App\Entity\Parts\Supplier; use App\Services\AmountFormatter; @@ -101,48 +107,12 @@ final class PartsDataTable implements DataTableTypeInterface public function configureOptions(OptionsResolver $optionsResolver): void { $optionsResolver->setDefaults([ - 'category' => null, - 'footprint' => null, - 'manufacturer' => null, - 'storelocation' => null, - 'supplier' => null, - 'tag' => null, - 'search' => null, + 'filter' => null, + 'search' => null ]); - $optionsResolver->setAllowedTypes('category', ['null', Category::class]); - $optionsResolver->setAllowedTypes('footprint', ['null', Footprint::class]); - $optionsResolver->setAllowedTypes('manufacturer', ['null', Manufacturer::class]); - $optionsResolver->setAllowedTypes('supplier', ['null', Supplier::class]); - $optionsResolver->setAllowedTypes('tag', ['null', 'string']); - $optionsResolver->setAllowedTypes('search', ['null', 'string']); - - //Configure search options - $optionsResolver->setDefault('search_options', static function (OptionsResolver $resolver): void { - $resolver->setDefaults([ - 'name' => true, - 'category' => true, - 'description' => true, - 'store_location' => true, - 'comment' => true, - 'ordernr' => true, - 'supplier' => false, - 'manufacturer' => false, - 'footprint' => false, - 'tags' => false, - 'regex' => false, - ]); - $resolver->setAllowedTypes('name', 'bool'); - $resolver->setAllowedTypes('category', 'bool'); - $resolver->setAllowedTypes('description', 'bool'); - $resolver->setAllowedTypes('store_location', 'bool'); - $resolver->setAllowedTypes('comment', 'bool'); - $resolver->setAllowedTypes('supplier', 'bool'); - $resolver->setAllowedTypes('manufacturer', 'bool'); - $resolver->setAllowedTypes('footprint', 'bool'); - $resolver->setAllowedTypes('tags', 'bool'); - $resolver->setAllowedTypes('regex', 'bool'); - }); + $optionsResolver->setAllowedTypes('filter', [PartFilter::class, 'null']); + $optionsResolver->setAllowedTypes('search', [PartSearchFilter::class, 'null']); } public function configure(DataTable $dataTable, array $options): void @@ -152,8 +122,10 @@ final class PartsDataTable implements DataTableTypeInterface $options = $resolver->resolve($options); $dataTable + ->add('select', SelectColumn::class) ->add('picture', TextColumn::class, [ 'label' => '', + 'className' => 'no-colvis', 'render' => function ($value, Part $context) { $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($context); if (null === $preview_attachment) { @@ -230,6 +202,7 @@ final class PartsDataTable implements DataTableTypeInterface return $this->amountFormatter->format($amount, $context->getPartUnit()); }, + 'orderField' => 'amountSum' ]) ->add('minamount', TextColumn::class, [ 'label' => $this->translator->trans('part.table.minamount'), @@ -251,18 +224,12 @@ final class PartsDataTable implements DataTableTypeInterface 'label' => $this->translator->trans('part.table.lastModified'), 'visible' => false, ]) - ->add('needs_review', BoolColumn::class, [ + ->add('needs_review', PrettyBoolColumn::class, [ 'label' => $this->translator->trans('part.table.needsReview'), - 'trueValue' => $this->translator->trans('true'), - 'falseValue' => $this->translator->trans('false'), - 'nullValue' => '', 'visible' => false, ]) - ->add('favorite', BoolColumn::class, [ + ->add('favorite', PrettyBoolColumn::class, [ 'label' => $this->translator->trans('part.table.favorite'), - 'trueValue' => $this->translator->trans('true'), - 'falseValue' => $this->translator->trans('false'), - 'nullValue' => '', 'visible' => false, ]) ->add('manufacturing_status', MapColumn::class, [ @@ -282,9 +249,10 @@ final class PartsDataTable implements DataTableTypeInterface 'label' => $this->translator->trans('part.table.mpn'), 'visible' => false, ]) - ->add('mass', TextColumn::class, [ + ->add('mass', SIUnitNumberColumn::class, [ 'label' => $this->translator->trans('part.table.mass'), 'visible' => false, + 'unit' => 'g' ]) ->add('tags', TagsColumn::class, [ 'label' => $this->translator->trans('part.table.tags'), @@ -324,6 +292,7 @@ final class PartsDataTable implements DataTableTypeInterface private function getQuery(QueryBuilder $builder): void { + $builder->distinct()->select('part') ->addSelect('category') ->addSelect('footprint') @@ -335,6 +304,16 @@ final class PartsDataTable implements DataTableTypeInterface ->addSelect('orderdetails') ->addSelect('attachments') ->addSelect('storelocations') + //Calculate amount sum using a subquery, so we can filter and sort by it + ->addSelect( + '( + SELECT IFNULL(SUM(partLot.amount), 0.0) + FROM '. PartLot::class. ' partLot + WHERE partLot.part = part.id + AND partLot.instock_unknown = false + AND (partLot.expiration_date IS NULL OR partLot.expiration_date > CURRENT_DATE()) + ) AS HIDDEN amountSum' + ) ->from(Part::class, 'part') ->leftJoin('part.category', 'category') ->leftJoin('part.master_picture_attachment', 'master_picture_attachment') @@ -346,144 +325,26 @@ final class PartsDataTable implements DataTableTypeInterface ->leftJoin('part.orderdetails', 'orderdetails') ->leftJoin('orderdetails.supplier', 'suppliers') ->leftJoin('part.attachments', 'attachments') - ->leftJoin('part.partUnit', 'partUnit'); + ->leftJoin('part.partUnit', 'partUnit') + ->leftJoin('part.parameters', 'parameters') + + ->groupBy('part') + ; } private function buildCriteria(QueryBuilder $builder, array $options): void { - if (isset($options['category'])) { - $category = $options['category']; - $list = $this->treeBuilder->typeToNodesList(Category::class, $category); - $list[] = $category; - - $builder->andWhere('part.category IN (:cid)')->setParameter('cid', $list); + //Apply the search criterias first + if ($options['search'] instanceof PartSearchFilter) { + $search = $options['search']; + $search->apply($builder); } - if (isset($options['footprint'])) { - $category = $options['footprint']; - $list = $this->treeBuilder->typeToNodesList(Footprint::class, $category); - $list[] = $category; - - $builder->andWhere('part.footprint IN (:cid)')->setParameter('cid', $list); + //We do the most stuff here in the filter class + if ($options['filter'] instanceof PartFilter) { + $filter = $options['filter']; + $filter->apply($builder); } - if (isset($options['manufacturer'])) { - $category = $options['manufacturer']; - $list = $this->treeBuilder->typeToNodesList(Manufacturer::class, $category); - $list[] = $category; - - $builder->andWhere('part.manufacturer IN (:cid)')->setParameter('cid', $list); - } - - if (isset($options['storelocation'])) { - $location = $options['storelocation']; - $list = $this->treeBuilder->typeToNodesList(Storelocation::class, $location); - $list[] = $location; - - $builder->andWhere('partLots.storage_location IN (:cid)')->setParameter('cid', $list); - } - - if (isset($options['supplier'])) { - $supplier = $options['supplier']; - $list = $this->treeBuilder->typeToNodesList(Supplier::class, $supplier); - $list[] = $supplier; - - $builder->andWhere('orderdetails.supplier IN (:cid)')->setParameter('cid', $list); - } - - if (isset($options['tag'])) { - $builder->andWhere('part.tags LIKE :tag')->setParameter('tag', $options['tag']); - } - - if (!empty($options['search'])) { - if (!$options['search_options']['regex']) { - //Dont show results, if no things are selected - $builder->andWhere('0=1'); - $defined = false; - if ($options['search_options']['name']) { - $builder->orWhere('part.name LIKE :search'); - $defined = true; - } - if ($options['search_options']['description']) { - $builder->orWhere('part.description LIKE :search'); - $defined = true; - } - if ($options['search_options']['comment']) { - $builder->orWhere('part.comment LIKE :search'); - $defined = true; - } - if ($options['search_options']['category']) { - $builder->orWhere('category.name LIKE :search'); - $defined = true; - } - if ($options['search_options']['manufacturer']) { - $builder->orWhere('manufacturer.name LIKE :search'); - $defined = true; - } - if ($options['search_options']['footprint']) { - $builder->orWhere('footprint.name LIKE :search'); - $defined = true; - } - if ($options['search_options']['tags']) { - $builder->orWhere('part.tags LIKE :search'); - $defined = true; - } - if ($options['search_options']['store_location']) { - $builder->orWhere('storelocations.name LIKE :search'); - $defined = true; - } - if ($options['search_options']['supplier']) { - $builder->orWhere('suppliers.name LIKE :search'); - $defined = true; - } - - if ($defined) { - $builder->setParameter('search', '%'.$options['search'].'%'); - } - } else { //Use REGEX - $builder->andWhere('0=1'); - $defined = false; - if ($options['search_options']['name']) { - $builder->orWhere('REGEXP(part.name, :search) = 1'); - $defined = true; - } - if ($options['search_options']['description']) { - $builder->orWhere('REGEXP(part.description, :search) = 1'); - $defined = true; - } - if ($options['search_options']['comment']) { - $builder->orWhere('REGEXP(part.comment, :search) = 1'); - $defined = true; - } - if ($options['search_options']['category']) { - $builder->orWhere('REGEXP(category.name, :search) = 1'); - $defined = true; - } - if ($options['search_options']['manufacturer']) { - $builder->orWhere('REGEXP(manufacturer.name, :search) = 1'); - $defined = true; - } - if ($options['search_options']['footprint']) { - $builder->orWhere('REGEXP(footprint.name, :search) = 1'); - $defined = true; - } - if ($options['search_options']['tags']) { - $builder->orWhere('REGEXP(part.tags, :search) = 1'); - $defined = true; - } - if ($options['search_options']['store_location']) { - $builder->orWhere('REGEXP(storelocations.name, :search) = 1'); - $defined = true; - } - if ($options['search_options']['supplier']) { - $builder->orWhere('REGEXP(suppliers.name, :search) = 1'); - $defined = true; - } - - if ($defined) { - $builder->setParameter('search', $options['search']); - } - } - } } } diff --git a/src/Doctrine/SQLiteRegexExtension.php b/src/Doctrine/SQLiteRegexExtension.php new file mode 100644 index 00000000..fe24ae9a --- /dev/null +++ b/src/Doctrine/SQLiteRegexExtension.php @@ -0,0 +1,40 @@ +getConnection(); + + //We only execute this on SQLite databases + if ($connection->getDatabasePlatform() instanceof SqlitePlatform) { + $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 && method_exists($native_connection, 'sqliteCreateFunction' )) { + $native_connection->sqliteCreateFunction('REGEXP', function ($pattern, $value) { + return (false !== mb_ereg($pattern, $value)) ? 1 : 0; + }); + } + } + } + + public function getSubscribedEvents() + { + return[ + Events::postConnect + ]; + } +} \ No newline at end of file diff --git a/src/Doctrine/SetSQLMode/SetSQLModeMiddlewareDriver.php b/src/Doctrine/SetSQLMode/SetSQLModeMiddlewareDriver.php new file mode 100644 index 00000000..ac36827c --- /dev/null +++ b/src/Doctrine/SetSQLMode/SetSQLModeMiddlewareDriver.php @@ -0,0 +1,24 @@ +getDatabasePlatform() instanceof AbstractMySQLPlatform) { + //1002 is \PDO::MYSQL_ATTR_INIT_COMMAND constant value + $params['driverOptions'][1002] = 'SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode, \'ONLY_FULL_GROUP_BY\', \'\'))'; + } + + return parent::connect($params); + } +} \ No newline at end of file diff --git a/src/Doctrine/SetSQLMode/SetSQLModeMiddlewareWrapper.php b/src/Doctrine/SetSQLMode/SetSQLModeMiddlewareWrapper.php new file mode 100644 index 00000000..4dd32875 --- /dev/null +++ b/src/Doctrine/SetSQLMode/SetSQLModeMiddlewareWrapper.php @@ -0,0 +1,18 @@ +orderdetails; - foreach ($orderdetails as $key => $details) { - if ($details->getObsolete()) { - unset($orderdetails[$key]); - } - } - - return $orderdetails; + return $this->orderdetails->filter(function (Orderdetail $orderdetail) { + return ! $orderdetail->getObsolete(); + }); } return $this->orderdetails; diff --git a/src/Form/Filters/AttachmentFilterType.php b/src/Form/Filters/AttachmentFilterType.php new file mode 100644 index 00000000..788e57b5 --- /dev/null +++ b/src/Form/Filters/AttachmentFilterType.php @@ -0,0 +1,102 @@ +setDefaults([ + 'compound' => true, + 'data_class' => AttachmentFilter::class, + 'csrf_protection' => false, + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->add('dbId', NumberConstraintType::class, [ + 'label' => 'part.filter.dbId', + 'min' => 1, + 'step' => 1, + ]); + + $builder->add('name', TextConstraintType::class, [ + 'label' => 'attachment.edit.name', + ]); + + $builder->add('targetType', InstanceOfConstraintType::class, [ + 'label' => 'attachment.table.element_type', + 'choices' => [ + 'part.label' => PartAttachment::class, + 'attachment_type.label' => AttachmentTypeAttachment::class, + 'category.label' => CategoryAttachment::class, + 'currency.label' => CurrencyAttachment::class, + 'device.label' => DeviceAttachment::class, + 'footprint.label' => FootprintAttachment::class, + 'group.label' => GroupAttachment::class, + 'label_profile.label' => LabelAttachment::class, + 'manufacturer.label' => Manufacturer::class, + 'measurement_unit.label' => MeasurementUnit::class, + 'storelocation.label' => StorelocationAttachment::class, + 'supplier.label' => SupplierAttachment::class, + 'user.label' => UserAttachment::class, + ] + ]); + + $builder->add('attachmentType', StructuralEntityConstraintType::class, [ + 'label' => 'attachment.attachment_type', + 'entity_class' => AttachmentType::class + ]); + + $builder->add('showInTable', BooleanConstraintType::class, [ + 'label' => 'attachment.edit.show_in_table' + ]); + + $builder->add('lastModified', DateTimeConstraintType::class, [ + 'label' => 'lastModified' + ]); + + $builder->add('addedDate', DateTimeConstraintType::class, [ + 'label' => 'createdAt' + ]); + + $builder->add('submit', SubmitType::class, [ + 'label' => 'filter.submit', + ]); + + $builder->add('discard', ResetType::class, [ + 'label' => 'filter.discard', + ]); + } +} \ No newline at end of file diff --git a/src/Form/Filters/Constraints/BooleanConstraintType.php b/src/Form/Filters/Constraints/BooleanConstraintType.php new file mode 100644 index 00000000..0ad27372 --- /dev/null +++ b/src/Form/Filters/Constraints/BooleanConstraintType.php @@ -0,0 +1,28 @@ +setDefaults([ + 'compound' => true, + 'data_class' => BooleanConstraint::class, + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('value', TriStateCheckboxType::class, [ + 'label' => $options['label'], + 'required' => false, + ]); + } +} \ No newline at end of file diff --git a/src/Form/Filters/Constraints/ChoiceConstraintType.php b/src/Form/Filters/Constraints/ChoiceConstraintType.php new file mode 100644 index 00000000..c9e3320b --- /dev/null +++ b/src/Form/Filters/Constraints/ChoiceConstraintType.php @@ -0,0 +1,48 @@ +setRequired('choices'); + $resolver->setAllowedTypes('choices', 'array'); + + $resolver->setDefaults([ + 'compound' => true, + 'data_class' => ChoiceConstraint::class, + ]); + + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $choices = [ + '' => '', + 'filter.choice_constraint.operator.ANY' => 'ANY', + 'filter.choice_constraint.operator.NONE' => 'NONE', + ]; + + $builder->add('operator', ChoiceType::class, [ + 'choices' => $choices, + 'required' => false, + ]); + + $builder->add('value', ChoiceType::class, [ + 'choices' => $options['choices'], + 'required' => false, + 'multiple' => true, + 'attr' => [ + 'data-controller' => 'elements--select-multiple', + ] + ]); + } + +} \ No newline at end of file diff --git a/src/Form/Filters/Constraints/DateTimeConstraintType.php b/src/Form/Filters/Constraints/DateTimeConstraintType.php new file mode 100644 index 00000000..96a3b940 --- /dev/null +++ b/src/Form/Filters/Constraints/DateTimeConstraintType.php @@ -0,0 +1,77 @@ +setDefaults([ + 'compound' => true, + 'data_class' => DateTimeConstraint::class, + 'text_suffix' => '', // An suffix which is attached as text-append to the input group. This can for example be used for units + + 'value1_options' => [], // Options for the first value input + 'value2_options' => [], // Options for the second value input + 'input_type' => DateTimeType::class, + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $choices = [ + '' => '', + '=' => '=', + '!=' => '!=', + '<' => '<', + '>' => '>', + '<=' => '<=', + '>=' => '>=', + 'filter.number_constraint.value.operator.BETWEEN' => 'BETWEEN', + ]; + + $builder->add('value1', $options['input_type'], array_merge_recursive([ + 'label' => 'filter.datetime_constraint.value1', + 'attr' => [ + 'placeholder' => 'filter.datetime_constraint.value1', + ], + 'required' => false, + 'html5' => true, + 'widget' => 'single_text', + ], $options['value1_options'])); + + $builder->add('value2', $options['input_type'], array_merge_recursive([ + 'label' => 'filter.datetime_constraint.value2', + 'attr' => [ + 'placeholder' => 'filter.datetime_constraint.value2', + ], + 'required' => false, + 'html5' => true, + 'widget' => 'single_text', + ], $options['value2_options'])); + + $builder->add('operator', ChoiceType::class, [ + 'label' => 'filter.datetime_constraint.operator', + 'choices' => $choices, + 'required' => false, + ]); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + parent::buildView($view, $form, $options); + + $view->vars['text_suffix'] = $options['text_suffix']; + } +} \ No newline at end of file diff --git a/src/Form/Filters/Constraints/InstanceOfConstraintType.php b/src/Form/Filters/Constraints/InstanceOfConstraintType.php new file mode 100644 index 00000000..666ae563 --- /dev/null +++ b/src/Form/Filters/Constraints/InstanceOfConstraintType.php @@ -0,0 +1,30 @@ +em = $entityManager; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('data_class', InstanceOfConstraint::class); + } + + public function getParent() + { + return ChoiceConstraintType::class; + } +} \ No newline at end of file diff --git a/src/Form/Filters/Constraints/NumberConstraintType.php b/src/Form/Filters/Constraints/NumberConstraintType.php new file mode 100644 index 00000000..8a76d01c --- /dev/null +++ b/src/Form/Filters/Constraints/NumberConstraintType.php @@ -0,0 +1,82 @@ + '', + '=' => '=', + '!=' => '!=', + '<' => '<', + '>' => '>', + '<=' => '<=', + '>=' => '>=', + 'filter.number_constraint.value.operator.BETWEEN' => 'BETWEEN', + ]; + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'compound' => true, + 'data_class' => NumberConstraint::class, + 'text_suffix' => '', // An suffix which is attached as text-append to the input group. This can for example be used for units + + 'min' => null, + 'max' => null, + 'step' => 'any', + + 'value1_options' => [], // Options for the first value input + 'value2_options' => [], // Options for the second value input + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('value1', NumberType::class, array_merge_recursive([ + 'label' => 'filter.number_constraint.value1', + 'attr' => [ + 'placeholder' => 'filter.number_constraint.value1', + 'max' => $options['max'], + 'min' => $options['min'], + 'step' => $options['step'], + ], + 'required' => false, + 'html5' => true, + ], $options['value1_options'])); + + $builder->add('value2', NumberType::class, array_merge_recursive([ + 'label' => 'filter.number_constraint.value2', + 'attr' => [ + 'placeholder' => 'filter.number_constraint.value2', + 'max' => $options['max'], + 'min' => $options['min'], + 'step' => $options['step'], + ], + 'required' => false, + 'html5' => true, + ], $options['value2_options'])); + + $builder->add('operator', ChoiceType::class, [ + 'label' => 'filter.number_constraint.operator', + 'choices' => static::CHOICES, + 'required' => false, + ]); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + parent::buildView($view, $form, $options); + + $view->vars['text_suffix'] = $options['text_suffix']; + } +} \ No newline at end of file diff --git a/src/Form/Filters/Constraints/ParameterConstraintType.php b/src/Form/Filters/Constraints/ParameterConstraintType.php new file mode 100644 index 00000000..1a4507a6 --- /dev/null +++ b/src/Form/Filters/Constraints/ParameterConstraintType.php @@ -0,0 +1,62 @@ +setDefaults([ + 'compound' => true, + 'data_class' => ParameterConstraint::class, + 'empty_data' => new ParameterConstraint(), + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('name', TextType::class, [ + 'required' => false, + ]); + + $builder->add('unit', SearchType::class, [ + 'required' => false, + ]); + + $builder->add('symbol', SearchType::class, [ + 'required' => false + ]); + + $builder->add('value_text', TextConstraintType::class, [ + //'required' => false, + ] ); + + $builder->add('value', ParameterValueConstraintType::class, [ + ]); + + /* + * I am not quite sure why this is needed, but somehow symfony tries to create a new instance of TextConstraint + * instead of using the existing one for the prototype (or the one from empty data). This fails as the constructor of TextConstraint requires + * arguments. + * Ensure that the data is never null, but use an empty ParameterConstraint instead + */ + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { + $form = $event->getForm(); + $data = $event->getData(); + + if ($data === null) { + $event->setData(new ParameterConstraint()); + } + }); + } +} \ No newline at end of file diff --git a/src/Form/Filters/Constraints/ParameterValueConstraintType.php b/src/Form/Filters/Constraints/ParameterValueConstraintType.php new file mode 100644 index 00000000..571ff9ae --- /dev/null +++ b/src/Form/Filters/Constraints/ParameterValueConstraintType.php @@ -0,0 +1,33 @@ + '', + 'filter.parameter_value_constraint.operator.=' => '=', + 'filter.parameter_value_constraint.operator.!=' => '!=', + 'filter.parameter_value_constraint.operator.<' => '<', + 'filter.parameter_value_constraint.operator.>' => '>', + 'filter.parameter_value_constraint.operator.<=' => '<=', + 'filter.parameter_value_constraint.operator.>=' => '>=', + 'filter.parameter_value_constraint.operator.BETWEEN' => 'BETWEEN', + + //Extensions by ParameterValueConstraint + 'filter.parameter_value_constraint.operator.IN_RANGE' => 'IN_RANGE', + 'filter.parameter_value_constraint.operator.NOT_IN_RANGE' => 'NOT_IN_RANGE', + 'filter.parameter_value_constraint.operator.GREATER_THAN_RANGE' => 'GREATER_THAN_RANGE', + 'filter.parameter_value_constraint.operator.GREATER_EQUAL_RANGE' => 'GREATER_EQUAL_RANGE', + 'filter.parameter_value_constraint.operator.LESS_THAN_RANGE' => 'LESS_THAN_RANGE', + 'filter.parameter_value_constraint.operator.LESS_EQUAL_RANGE' => 'LESS_EQUAL_RANGE', + + 'filter.parameter_value_constraint.operator.RANGE_IN_RANGE' => 'RANGE_IN_RANGE', + 'filter.parameter_value_constraint.operator.RANGE_INTERSECT_RANGE' => 'RANGE_INTERSECT_RANGE' + ]; + + public function getParent(): string + { + return NumberConstraintType::class; + } +} \ No newline at end of file diff --git a/src/Form/Filters/Constraints/StructuralEntityConstraintType.php b/src/Form/Filters/Constraints/StructuralEntityConstraintType.php new file mode 100644 index 00000000..4730caec --- /dev/null +++ b/src/Form/Filters/Constraints/StructuralEntityConstraintType.php @@ -0,0 +1,55 @@ +setDefaults([ + 'compound' => true, + 'data_class' => EntityConstraint::class, + 'text_suffix' => '', // An suffix which is attached as text-append to the input group. This can for example be used for units + ]); + + $resolver->setRequired('entity_class'); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $choices = [ + '' => '', + 'filter.entity_constraint.operator.EQ' => '=', + 'filter.entity_constraint.operator.NEQ' => '!=', + 'filter.entity_constraint.operator.INCLUDING_CHILDREN' => 'INCLUDING_CHILDREN', + 'filter.entity_constraint.operator.EXCLUDING_CHILDREN' => 'EXCLUDING_CHILDREN', + ]; + + $builder->add('value', StructuralEntityType::class, [ + 'class' => $options['entity_class'], + 'required' => false, + ]); + + + $builder->add('operator', ChoiceType::class, [ + 'label' => 'filter.text_constraint.operator', + 'choices' => $choices, + 'required' => false, + ]); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + parent::buildView($view, $form, $options); + $view->vars['text_suffix'] = $options['text_suffix']; + } +} \ No newline at end of file diff --git a/src/Form/Filters/Constraints/TagsConstraintType.php b/src/Form/Filters/Constraints/TagsConstraintType.php new file mode 100644 index 00000000..c5c9d043 --- /dev/null +++ b/src/Form/Filters/Constraints/TagsConstraintType.php @@ -0,0 +1,56 @@ +urlGenerator = $urlGenerator; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'compound' => true, + 'data_class' => TagsConstraint::class, + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $choices = [ + '' => '', + 'filter.tags_constraint.operator.ANY' => 'ANY', + 'filter.tags_constraint.operator.ALL' => 'ALL', + 'filter.tags_constraint.operator.NONE' => 'NONE' + ]; + + $builder->add('value', SearchType::class, [ + 'attr' => [ + 'class' => 'tagsinput', + 'data-controller' => 'elements--tagsinput', + 'data-autocomplete' => $this->urlGenerator->generate('typeahead_tags', ['query' => '__QUERY__']), + ], + 'required' => false, + 'empty_data' => '', + ]); + + + $builder->add('operator', ChoiceType::class, [ + 'label' => 'filter.text_constraint.operator', + 'choices' => $choices, + 'required' => false, + ]); + } +} \ No newline at end of file diff --git a/src/Form/Filters/Constraints/TextConstraintType.php b/src/Form/Filters/Constraints/TextConstraintType.php new file mode 100644 index 00000000..c4b16d51 --- /dev/null +++ b/src/Form/Filters/Constraints/TextConstraintType.php @@ -0,0 +1,61 @@ +setDefaults([ + 'compound' => true, + 'data_class' => TextConstraint::class, + 'text_suffix' => '', // An suffix which is attached as text-append to the input group. This can for example be used for units + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $choices = [ + '' => '', + 'filter.text_constraint.value.operator.EQ' => '=', + 'filter.text_constraint.value.operator.NEQ' => '!=', + 'filter.text_constraint.value.operator.STARTS' => 'STARTS', + 'filter.text_constraint.value.operator.ENDS' => 'ENDS', + 'filter.text_constraint.value.operator.CONTAINS' => 'CONTAINS', + 'filter.text_constraint.value.operator.LIKE' => 'LIKE', + 'filter.text_constraint.value.operator.REGEX' => 'REGEX', + ]; + + $builder->add('value', SearchType::class, [ + 'attr' => [ + 'placeholder' => 'filter.text_constraint.value', + ], + 'required' => false, + 'empty_data' => '', + ]); + + + $builder->add('operator', ChoiceType::class, [ + 'label' => 'filter.text_constraint.operator', + 'choices' => $choices, + 'required' => false, + ]); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + parent::buildView($view, $form, $options); + + $view->vars['text_suffix'] = $options['text_suffix']; + } +} \ No newline at end of file diff --git a/src/Form/Filters/Constraints/UserEntityConstraintType.php b/src/Form/Filters/Constraints/UserEntityConstraintType.php new file mode 100644 index 00000000..048204ad --- /dev/null +++ b/src/Form/Filters/Constraints/UserEntityConstraintType.php @@ -0,0 +1,59 @@ +setDefaults([ + 'compound' => true, + 'data_class' => EntityConstraint::class, + 'text_suffix' => '', // An suffix which is attached as text-append to the input group. This can for example be used for units + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $choices = [ + '' => '', + 'filter.entity_constraint.operator.EQ' => '=', + 'filter.entity_constraint.operator.NEQ' => '!=', + ]; + + $builder->add('value', EntityType::class, [ + 'class' => User::class, + 'required' => false, + 'attr' => [ + 'data-controller' => 'elements--selectpicker', + 'data-live-search' => true, + 'title' => 'selectpicker.nothing_selected', + ] + ]); + + + $builder->add('operator', ChoiceType::class, [ + 'label' => 'filter.text_constraint.operator', + 'choices' => $choices, + 'required' => false, + ]); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + parent::buildView($view, $form, $options); + $view->vars['text_suffix'] = $options['text_suffix']; + } +} \ No newline at end of file diff --git a/src/Form/Filters/LogFilterType.php b/src/Form/Filters/LogFilterType.php new file mode 100644 index 00000000..60371a38 --- /dev/null +++ b/src/Form/Filters/LogFilterType.php @@ -0,0 +1,152 @@ + AbstractLogEntry::LEVEL_DEBUG, + 'log.level.info' => AbstractLogEntry::LEVEL_INFO, + 'log.level.notice' => AbstractLogEntry::LEVEL_NOTICE, + 'log.level.warning' => AbstractLogEntry::LEVEL_WARNING, + 'log.level.error' => AbstractLogEntry::LEVEL_ERROR, + 'log.level.critical' => AbstractLogEntry::LEVEL_CRITICAL, + 'log.level.alert' => AbstractLogEntry::LEVEL_ALERT, + 'log.level.emergency' => AbstractLogEntry::LEVEL_EMERGENCY, + ]; + + protected const TARGET_TYPE_CHOICES = [ + 'log.type.collection_element_deleted' => CollectionElementDeleted::class, + 'log.type.database_updated' => DatabaseUpdatedLogEntry::class, + 'log.type.element_created' => ElementCreatedLogEntry::class, + 'log.type.element_deleted' => ElementDeletedLogEntry::class, + 'log.type.element_edited' => ElementEditedLogEntry::class, + 'log.type.security' => SecurityEventLogEntry::class, + 'log.type.user_login' => UserLoginLogEntry::class, + 'log.type.user_logout' => UserLogoutLogEntry::class, + 'log.type.user_not_allowed' => UserNotAllowedLogEntry::class, + + //Legacy entries + 'log.type.instock_changed' => InstockChangedLogEntry::class, + ]; + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'compound' => true, + 'data_class' => LogFilter::class, + 'csrf_protection' => false, + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->add('dbId', NumberConstraintType::class, [ + 'label' => 'part.filter.dbId', + 'min' => 1, + 'step' => 1, + ]); + + $builder->add('timestamp', DateTimeConstraintType::class, [ + 'label' => 'log.timestamp', + ]); + + + + $builder->add('level', ChoiceConstraintType::class, [ + 'label' => 'log.level', + 'choices' => self::LEVEL_CHOICES, + ]); + + $builder->add('eventType', InstanceOfConstraintType::class, [ + 'label' => 'log.type', + 'choices' => self::TARGET_TYPE_CHOICES + ]); + + $builder->add('user', UserEntityConstraintType::class, [ + 'label' => 'log.user', + ]); + + $builder->add('targetType', ChoiceConstraintType::class, [ + 'label' => 'log.target_type', + 'choices' => [ + 'user.label' => AbstractLogEntry::targetTypeClassToID(User::class), + 'attachment.label' => AbstractLogEntry::targetTypeClassToID(Attachment::class), + 'attachment_type.label' => AbstractLogEntry::targetTypeClassToID(AttachmentType::class), + 'category.label' => AbstractLogEntry::targetTypeClassToID(Category::class), + 'device.label' => AbstractLogEntry::targetTypeClassToID(Device::class), + 'device_part.label' => AbstractLogEntry::targetTypeClassToID(DevicePart::class), + 'footprint.label' => AbstractLogEntry::targetTypeClassToID(Footprint::class), + 'group.label' => AbstractLogEntry::targetTypeClassToID(Group::class), + 'manufacturer.label' => AbstractLogEntry::targetTypeClassToID(Manufacturer::class), + 'part.label' => AbstractLogEntry::targetTypeClassToID(Part::class), + 'storelocation.label' => AbstractLogEntry::targetTypeClassToID(Storelocation::class), + 'supplier.label' => AbstractLogEntry::targetTypeClassToID(Supplier::class), + 'part_lot.label' => AbstractLogEntry::targetTypeClassToID(PartLot::class), + 'currency.label' => AbstractLogEntry::targetTypeClassToID(Currency::class), + 'orderdetail.label' => AbstractLogEntry::targetTypeClassToID(Orderdetail::class), + 'pricedetail.label' => AbstractLogEntry::targetTypeClassToID(Pricedetail::class), + 'measurement_unit.label' => AbstractLogEntry::targetTypeClassToID(MeasurementUnit::class), + 'parameter.label' => AbstractLogEntry::targetTypeClassToID(AbstractParameter::class), + 'label_profile.label' => AbstractLogEntry::targetTypeClassToID(LabelProfile::class), + ] + ]); + + $builder->add('targetId', NumberConstraintType::class, [ + 'label' => 'log.target_id', + 'min' => 1, + 'step' => 1, + ]); + + $builder->add('submit', SubmitType::class, [ + 'label' => 'filter.submit', + ]); + + $builder->add('discard', ResetType::class, [ + 'label' => 'filter.discard', + ]); + } +} \ No newline at end of file diff --git a/src/Form/Filters/PartFilterType.php b/src/Form/Filters/PartFilterType.php new file mode 100644 index 00000000..99948355 --- /dev/null +++ b/src/Form/Filters/PartFilterType.php @@ -0,0 +1,249 @@ +setDefaults([ + 'compound' => true, + 'data_class' => PartFilter::class, + 'csrf_protection' => false, + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + /* + * Common tab + */ + + $builder->add('name', TextConstraintType::class, [ + 'label' => 'part.edit.name', + ]); + + $builder->add('description', TextConstraintType::class, [ + 'label' => 'part.edit.description', + ]); + + $builder->add('category', StructuralEntityConstraintType::class, [ + 'label' => 'part.edit.category', + 'entity_class' => Category::class + ]); + + $builder->add('footprint', StructuralEntityConstraintType::class, [ + 'label' => 'part.edit.footprint', + 'entity_class' => Footprint::class + ]); + + $builder->add('tags', TagsConstraintType::class, [ + 'label' => 'part.edit.tags' + ]); + + $builder->add('comment', TextConstraintType::class, [ + 'label' => 'part.edit.comment' + ]); + + /* + * Advanced tab + */ + + $builder->add('dbId', NumberConstraintType::class, [ + 'label' => 'part.filter.dbId', + 'min' => 1, + 'step' => 1, + ]); + + $builder->add('favorite', BooleanConstraintType::class, [ + 'label' => 'part.edit.is_favorite' + ]); + + $builder->add('needsReview', BooleanConstraintType::class, [ + 'label' => 'part.edit.needs_review' + ]); + + $builder->add('mass', NumberConstraintType::class, [ + 'label' => 'part.edit.mass', + 'text_suffix' => 'g', + 'min' => 0, + ]); + + $builder->add('measurementUnit', StructuralEntityConstraintType::class, [ + 'label' => 'part.edit.partUnit', + 'entity_class' => MeasurementUnit::class + ]); + + $builder->add('lastModified', DateTimeConstraintType::class, [ + 'label' => 'lastModified' + ]); + + $builder->add('addedDate', DateTimeConstraintType::class, [ + 'label' => 'createdAt' + ]); + + + /* + * Manufacturer tab + */ + + $builder->add('manufacturer', StructuralEntityConstraintType::class, [ + 'label' => 'part.edit.manufacturer.label', + 'entity_class' => Manufacturer::class + ]); + + $builder->add('manufacturer_product_url', TextConstraintType::class, [ + 'label' => 'part.edit.manufacturer_url.label' + ]); + + $builder->add('manufacturer_product_number', TextConstraintType::class, [ + 'label' => 'part.edit.mpn' + ]); + + $status_choices = [ + 'm_status.unknown' => '', + 'm_status.announced' => 'announced', + 'm_status.active' => 'active', + 'm_status.nrfnd' => 'nrfnd', + 'm_status.eol' => 'eol', + 'm_status.discontinued' => 'discontinued', + ]; + + $builder->add('manufacturing_status', ChoiceConstraintType::class, [ + 'label' => 'part.edit.manufacturing_status', + 'choices' => $status_choices, + ]); + + /* + * Purchasee informations + */ + + $builder->add('supplier', StructuralEntityConstraintType::class, [ + 'label' => 'supplier.label', + 'entity_class' => Manufacturer::class + ]); + + $builder->add('orderdetailsCount', NumberConstraintType::class, [ + 'label' => 'part.filter.orderdetails_count', + 'step' => 1, + 'min' => 0, + ]); + + $builder->add('obsolete', BooleanConstraintType::class, [ + 'label' => 'orderdetails.edit.obsolete' + ]); + + /* + * Stocks tabs + */ + $builder->add('storelocation', StructuralEntityConstraintType::class, [ + 'label' => 'storelocation.label', + 'entity_class' => Storelocation::class + ]); + + $builder->add('minAmount', NumberConstraintType::class, [ + 'label' => 'part.edit.mininstock', + 'min' => 0, + ]); + + $builder->add('lotCount', NumberConstraintType::class, [ + 'label' => 'part.filter.lot_count', + 'min' => 0, + 'step' => 1, + ]); + + $builder->add('amountSum', NumberConstraintType::class, [ + 'label' => 'part.filter.amount_sum', + 'min' => 0, + ]); + + $builder->add('lotNeedsRefill', BooleanConstraintType::class, [ + 'label' => 'part.filter.lotNeedsRefill' + ]); + + $builder->add('lotUnknownAmount', BooleanConstraintType::class, [ + 'label' => 'part.filter.lotUnknwonAmount' + ]); + + $builder->add('lotExpirationDate', DateTimeConstraintType::class, [ + 'label' => 'part.filter.lotExpirationDate', + 'input_type' => DateType::class, + ]); + + $builder->add('lotDescription', TextConstraintType::class, [ + 'label' => 'part.filter.lotDescription', + ]); + + /** + * Attachments count + */ + $builder->add('attachmentsCount', NumberConstraintType::class, [ + 'label' => 'part.filter.attachments_count', + 'step' => 1, + 'min' => 0, + ]); + + $builder->add('attachmentType', StructuralEntityConstraintType::class, [ + 'label' => 'attachment.attachment_type', + 'entity_class' => AttachmentType::class + ]); + + $builder->add('attachmentName', TextConstraintType::class, [ + 'label' => 'part.filter.attachmentName', + ]); + + $constraint_prototype = new ParameterConstraint(); + + $builder->add('parameters', CollectionType::class, [ + 'label' => false, + 'entry_type' => ParameterConstraintType::class, + 'allow_delete' => true, + 'allow_add' => true, + 'reindex_enable' => false, + 'prototype_data' => $constraint_prototype, + 'empty_data' => $constraint_prototype, + ]); + + $builder->add('parametersCount', NumberConstraintType::class, [ + 'label' => 'part.filter.parameters_count', + 'step' => 1, + 'min' => 0, + ]); + + $builder->add('submit', SubmitType::class, [ + 'label' => 'filter.submit', + ]); + + $builder->add('discard', ResetType::class, [ + 'label' => 'filter.discard', + ]); + + } +} \ No newline at end of file diff --git a/src/Form/ParameterType.php b/src/Form/ParameterType.php index 413df3e4..e8e6db1c 100644 --- a/src/Form/ParameterType.php +++ b/src/Form/ParameterType.php @@ -24,10 +24,23 @@ declare(strict_types=1); namespace App\Form; use App\Entity\Parameters\AbstractParameter; +use App\Entity\Parameters\AttachmentTypeParameter; +use App\Entity\Parameters\CategoryParameter; +use App\Entity\Parameters\CurrencyParameter; +use App\Entity\Parameters\DeviceParameter; +use App\Entity\Parameters\FootprintParameter; +use App\Entity\Parameters\GroupParameter; +use App\Entity\Parameters\ManufacturerParameter; +use App\Entity\Parameters\PartParameter; +use App\Entity\Parameters\StorelocationParameter; +use App\Entity\Parameters\SupplierParameter; +use App\Entity\Parts\MeasurementUnit; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\OptionsResolver; class ParameterType extends AbstractType @@ -117,6 +130,32 @@ class ParameterType extends AbstractType ]); } + public function finishView(FormView $view, FormInterface $form, array $options) + { + //By default use part parameters for autocomplete + $view->vars['type'] = 'part'; + + $map = [ + PartParameter::class => 'part', + AttachmentTypeParameter::class => 'attachment_type', + CategoryParameter::class => 'category', + CurrencyParameter::class => 'currency', + DeviceParameter::class => 'device', + FootprintParameter::class => 'footprint', + GroupParameter::class => 'group', + ManufacturerParameter::class => 'manufacturer', + MeasurementUnit::class => 'measurement_unit', + StorelocationParameter::class => 'storelocation', + SupplierParameter::class => 'supplier', + ]; + + if (isset($map[$options['data_class']])) { + $view->vars['type'] = $map[$options['data_class']]; + } + + parent::finishView($view, $form, $options); // TODO: Change the autogenerated stub + } + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ diff --git a/src/Form/Type/TriStateCheckboxType.php b/src/Form/Type/TriStateCheckboxType.php index 87bdc473..9b3b2514 100644 --- a/src/Form/Type/TriStateCheckboxType.php +++ b/src/Form/Type/TriStateCheckboxType.php @@ -171,10 +171,10 @@ final class TriStateCheckboxType extends AbstractType implements DataTransformer case 'true': return true; case 'false': - case '': return false; case 'indeterminate': case 'null': + case '': return null; default: throw new InvalidArgumentException('Invalid value encountered!: '.$value); diff --git a/src/Repository/AbstractPartsContainingRepository.php b/src/Repository/AbstractPartsContainingRepository.php index adb7c828..88b9d021 100644 --- a/src/Repository/AbstractPartsContainingRepository.php +++ b/src/Repository/AbstractPartsContainingRepository.php @@ -33,7 +33,7 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo * @param object $element the element for which the parts should be determined * @param array $order_by The order of the parts. Format ['name' => 'ASC'] * - * @return Part + * @return Part[] */ abstract public function getParts(object $element, array $order_by = ['name' => 'ASC']): array; diff --git a/src/Repository/ParameterRepository.php b/src/Repository/ParameterRepository.php new file mode 100644 index 00000000..36ee2ff8 --- /dev/null +++ b/src/Repository/ParameterRepository.php @@ -0,0 +1,33 @@ +createQueryBuilder('parameter'); + + $qb->distinct() + ->select('parameter.name') + ->addSelect('parameter.symbol') + ->addSelect('parameter.unit') + ->where('parameter.name LIKE :name'); + if ($exact) { + $qb->setParameter('name', $name); + } else { + $qb->setParameter('name', '%'.$name.'%'); + } + + $qb->setMaxResults($max_results); + + return $qb->getQuery()->getArrayResult(); + } +} \ No newline at end of file diff --git a/src/Services/Trees/NodesListBuilder.php b/src/Services/Trees/NodesListBuilder.php index 3bb13486..0af0341c 100644 --- a/src/Services/Trees/NodesListBuilder.php +++ b/src/Services/Trees/NodesListBuilder.php @@ -84,10 +84,20 @@ class NodesListBuilder return $this->cache->get($key, function (ItemInterface $item) use ($class_name, $parent, $secure_class_name) { // Invalidate when groups, a element with the class or the user changes $item->tag(['groups', 'tree_list', $this->keyGenerator->generateKey(), $secure_class_name]); - /** @var StructuralDBElementRepository */ - $repo = $this->em->getRepository($class_name); - - return $repo->toNodesList($parent); + return $this->em->getRepository($class_name)->toNodesList($parent); }); } + + /** + * Returns a flattened list of all (recursive) children elements of the given AbstractStructuralDBElement. + * The value is cached for performance reasons. + * + * @template T of AbstractStructuralDBElement + * @param T $element + * @return T[] + */ + public function getChildrenFlatList(AbstractStructuralDBElement $element): array + { + return $this->typeToNodesList(get_class($element), $element); + } } diff --git a/src/Twig/AppExtension.php b/src/Twig/AppExtension.php index 578984f4..3e399e91 100644 --- a/src/Twig/AppExtension.php +++ b/src/Twig/AppExtension.php @@ -42,9 +42,20 @@ declare(strict_types=1); namespace App\Twig; +use App\Entity\Attachments\Attachment; use App\Entity\Base\AbstractDBElement; +use App\Entity\Devices\Device; +use App\Entity\LabelSystem\LabelProfile; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\Part; +use App\Entity\Parts\Storelocation; +use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; +use App\Entity\UserSystem\Group; +use App\Entity\UserSystem\User; use App\Services\AmountFormatter; use App\Services\Attachments\AttachmentURLGenerator; use App\Services\EntityURLGenerator; @@ -54,6 +65,7 @@ use App\Services\MoneyFormatter; use App\Services\SIFormatter; use App\Services\Trees\TreeViewGenerator; use Brick\Math\BigDecimal; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Extension\AbstractExtension; @@ -76,12 +88,14 @@ class AppExtension extends AbstractExtension protected $FAIconGenerator; protected $translator; + protected $objectNormalizer; + public function __construct(EntityURLGenerator $entityURLGenerator, MarkdownParser $markdownParser, SerializerInterface $serializer, TreeViewGenerator $treeBuilder, MoneyFormatter $moneyFormatter, SIFormatter $SIFormatter, AmountFormatter $amountFormatter, AttachmentURLGenerator $attachmentURLGenerator, - FAIconGenerator $FAIconGenerator, TranslatorInterface $translator) + FAIconGenerator $FAIconGenerator, TranslatorInterface $translator, ObjectNormalizer $objectNormalizer) { $this->entityURLGenerator = $entityURLGenerator; $this->markdownParser = $markdownParser; @@ -93,6 +107,8 @@ class AppExtension extends AbstractExtension $this->attachmentURLGenerator = $attachmentURLGenerator; $this->FAIconGenerator = $FAIconGenerator; $this->translator = $translator; + + $this->objectNormalizer = $objectNormalizer; } public function getFilters(): array @@ -107,6 +123,8 @@ class AppExtension extends AbstractExtension new TwigFilter('siFormat', [$this, 'siFormat']), new TwigFilter('amountFormat', [$this, 'amountFormat']), new TwigFilter('loginPath', [$this, 'loginPath']), + + new TwigFilter('toArray', [$this, 'toArray']) ]; } @@ -116,6 +134,12 @@ class AppExtension extends AbstractExtension new TwigTest('instanceof', static function ($var, $instance) { return $var instanceof $instance; }), + new TwigTest('entity', static function ($var) { + return $var instanceof AbstractDBElement; + }), + new TwigTest('object', static function ($var) { + return is_object($var); + }), ]; } @@ -125,9 +149,31 @@ class AppExtension extends AbstractExtension new TwigFunction('generateTreeData', [$this, 'treeData']), new TwigFunction('attachment_thumbnail', [$this->attachmentURLGenerator, 'getThumbnailURL']), new TwigFunction('ext_to_fa_icon', [$this->FAIconGenerator, 'fileExtensionToFAType']), + new TwigFunction('entity_type', [$this, 'getEntityType']), ]; } + public function getEntityType($entity): ?string + { + $map = [ + Part::class => 'part', + Footprint::class => 'footprint', + Storelocation::class => 'storelocation', + Manufacturer::class => 'manufacturer', + Category::class => 'category', + Device::class => 'device', + Attachment::class => 'attachment', + Supplier::class => 'supplier', + User::class => 'user', + Group::class => 'group', + Currency::class => 'currency', + MeasurementUnit::class => 'measurement_unit', + LabelProfile::class => 'label_profile', + ]; + + return $map[get_class($entity)] ?? null; + } + public function treeData(AbstractDBElement $element, string $type = 'newEdit'): string { $tree = $this->treeBuilder->getTreeView(get_class($element), null, $type, $element); @@ -135,6 +181,11 @@ class AppExtension extends AbstractExtension return json_encode($tree, JSON_THROW_ON_ERROR); } + public function toArray($object): array + { + return $this->objectNormalizer->normalize($object, null); + } + /** * This function/filter generates an path. */ diff --git a/templates/Form/FilterTypesLayout.html.twig b/templates/Form/FilterTypesLayout.html.twig new file mode 100644 index 00000000..2be7e55c --- /dev/null +++ b/templates/Form/FilterTypesLayout.html.twig @@ -0,0 +1,62 @@ +{% block number_constraint_widget %} +
+ {{ form_widget(form.operator, {"attr": { + "class": "form-select", + "data-filters--number-constraint-target": "operator", + "data-action": "change->filters--number-constraint#update" + }}) }} + {{ form_widget(form.value1) }} + {% trans %}filter.number_constraint.AND{% endtrans %} + {{ form_widget(form.value2, {"attr": {"class": "d-none", "data-filters--number-constraint-target": "thingsToHide"}}) }} + {% if form.vars["text_suffix"] %} + {{ form.vars["text_suffix"] }} + {% endif %} +
+{% endblock %} + +{% block text_constraint_widget %} +
+ {{ form_widget(form.operator, {"attr": {"class": "form-select"}}) }} + {{ form_widget(form.value) }} + {% if form.vars["text_suffix"] is defined and form.vars["text_suffix"] %} + {{ form.vars["text_suffix"] }} + {% endif %} +
+{% endblock %} + +{% block structural_entity_constraint_widget %} + {{ block('text_constraint_widget') }} +{% endblock %} + +{% block user_entity_constraint_widget %} + {{ block('text_constraint_widget') }} +{% endblock %} + +{% block date_time_constraint_widget %} + {{ block('number_constraint_widget') }} +{% endblock %} + +{% block tags_constraint_widget %} + {{ block('text_constraint_widget') }} +{% endblock %} + +{% block choice_constraint_widget %} + {{ block('text_constraint_widget') }} +{% endblock %} + +{% block parameter_constraint_widget %} + {% import 'components/collection_type.macro.html.twig' as collection %} + + {{ form_widget(form.name, {"attr": {"data-pages--parameters-autocomplete-target": "name"}}) }} + {{ form_widget(form.symbol, {"attr": {"data-pages--parameters-autocomplete-target": "symbol", "data-pages--latex-preview-target": "input"}}) }} + {{ form_widget(form.value) }} + {{ form_widget(form.unit, {"attr": {"data-pages--parameters-autocomplete-target": "unit", "data-pages--latex-preview-target": "input"}}) }} + {{ form_widget(form.value_text) }} + + + {{ form_errors(form) }} + + +{% endblock %} \ No newline at end of file diff --git a/templates/LogSystem/log_list.html.twig b/templates/LogSystem/log_list.html.twig index 5ad1d7a3..9bacfa0c 100644 --- a/templates/LogSystem/log_list.html.twig +++ b/templates/LogSystem/log_list.html.twig @@ -3,5 +3,53 @@ {% block title %}{% trans %}log.list.title{% endtrans %}{% endblock %} {% block content %} +
+
+
+ +
+
+
+ +
+
+ + +
+ +
+
+ +
+
+
+ {{ form_start(filterForm, {"attr": {"data-controller": "helpers--form-cleanup", "data-action": "helpers--form-cleanup#submit"}}) }} + + {{ form_row(filterForm.dbId) }} + {{ form_row(filterForm.timestamp) }} + {{ form_row(filterForm.eventType) }} + {{ form_row(filterForm.user) }} + {{ form_row(filterForm.level) }} + {{ form_row(filterForm.targetType) }} + {{ form_row(filterForm.targetId) }} + + {{ form_row(filterForm.submit) }} + {{ form_row(filterForm.discard) }} + +
+
+ +
+
+ + {{ form_end(filterForm) }} +
+
+
+
+ {% include "LogSystem/_log_table.html.twig" %} {% endblock %} \ No newline at end of file diff --git a/templates/Parts/edit/edit_form_styles.html.twig b/templates/Parts/edit/edit_form_styles.html.twig index 8fef44b4..47b66476 100644 --- a/templates/Parts/edit/edit_form_styles.html.twig +++ b/templates/Parts/edit/edit_form_styles.html.twig @@ -68,13 +68,13 @@ {% block parameter_widget %} {% import 'components/collection_type.macro.html.twig' as collection %} - - {{ form_widget(form.name) }}{{ form_errors(form.name) }} - {{ form_widget(form.symbol) }}{{ form_errors(form.symbol) }} + + {{ form_widget(form.name, {"attr": {"data-pages--parameters-autocomplete-target": "name"}}) }}{{ form_errors(form.name) }} + {{ form_widget(form.symbol, {"attr": {"data-pages--parameters-autocomplete-target": "symbol", "data-pages--latex-preview-target": "input"}}) }}{{ form_errors(form.symbol) }} {{ form_widget(form.value_min) }}{{ form_errors(form.value_min) }} {{ form_widget(form.value_typical) }}{{ form_errors(form.value_typical) }} {{ form_widget(form.value_max) }}{{ form_errors(form.value_max) }} - {{ form_widget(form.unit) }}{{ form_errors(form.unit) }} + {{ form_widget(form.unit, {"attr": {"data-pages--parameters-autocomplete-target": "unit", "data-pages--latex-preview-target": "input"}}) }}{{ form_errors(form.unit) }} {{ form_widget(form.value_text) }}{{ form_errors(form.value_text) }} {{ form_widget(form.group) }}{{ form_errors(form.group) }} diff --git a/templates/Parts/info/_picture.html.twig b/templates/Parts/info/_picture.html.twig index 590e3b89..532d0f8f 100644 --- a/templates/Parts/info/_picture.html.twig +++ b/templates/Parts/info/_picture.html.twig @@ -1,7 +1,12 @@ {% if not pictures is empty %} {# Part main image #} -