diff --git a/Documentation/Form.rst b/Documentation/Form.rst index 40ee60544e3ed9d3907ecbef39d8fb52605062b4..908f51a119efd8528ac7ce92252c6d9584067765 100644 --- a/Documentation/Form.rst +++ b/Documentation/Form.rst @@ -2728,17 +2728,35 @@ record (defined by `multiSql`). The Form is shown as a HTML table. -* `multiSql`: Selects the records where the defined FormElements will work on each. +* *multiSql* = `<string>` - Selects the records where the defined FormElements will work on each. * A uniq column `id` or `_id` (not shown) is mandatory and has to reference an existing record in table `primary table`. * Additional columns, defined in `multiSql`, will be shown on the form on the same line, before the FormElements. * Per row, the STORE_PARENT is filled with the current record of the primary table. + * The optional special column `_processRow` will uncheck/check the processRow checkbox during form load. + +Process Row +^^^^^^^^^^^ + +Activating `processRow` adds a checkbox to every row of the `Multi Form`, including the header. +During `save`, only selected rows get processed. + +The checkbox in the header selects all/none rows at once. + +* `Form.parameter`: + + * *processRow* = `<string>` - the value displayed in table header next to the checkbox. + +* `Form.mulitSql`: If there is a column `_processRow`, value of 0/1 per row will control unchecked/checked during form load. + +Implicit Multi Form mode +^^^^^^^^^^^^^^^^^^^^^^^^ The following definition of *Simple* and *Advanced* is just for explanation, there is no *flag* or *mode* which has to be set. Also the *Simple* and *Advanced* variant can be mixed in the same Multi Form. Simple -^^^^^^ +"""""" General: @@ -2753,7 +2771,7 @@ FormElement: * No further definition (`sqlInsert`, `sqlUpdate`, ...) is required. Advanced -^^^^^^^^ +"""""""" To handle foreign records (insert/update/delete), use the :ref:`slave-id` concept. diff --git a/extension/Classes/Core/AbstractBuildForm.php b/extension/Classes/Core/AbstractBuildForm.php index c936c6096db45a3a88d758febc0e3c7a4f6f1dce..28fe871a9502044176d0337be67c5e3158bee926 100644 --- a/extension/Classes/Core/AbstractBuildForm.php +++ b/extension/Classes/Core/AbstractBuildForm.php @@ -198,16 +198,34 @@ abstract class AbstractBuildForm { $rcJson = array(); $parentRecords = $this->evaluate->parse($this->formSpec[F_MULTI_SQL], ROW_REGULAR); + // Check for 'id' or '_id' as column name + $idName = isset($parentRecords[0]['_' . F_MULTI_COL_ID]) ? '_' . F_MULTI_COL_ID : F_MULTI_COL_ID; + + // If form.parameter.processRow is set, checkboxes are added to parentRecords + if (isset($this->formSpec[F_PROCESS_ROW])) { + // Title from query. + $processRowTitle = $this->formSpec[F_PROCESS_ROW]; + + // Will be displayed in <th></th>. + $processRowKey = '<label class="checkbox process-row-all process-row-label-header"><input type="checkbox"><span>' . $processRowTitle . '</span></label>'; + foreach ($parentRecords as &$array) { + // Will be displayed in <td></td>. + $checked = empty($array[F_PROCESS_ROW_COLUMN]) ? '' : 'checked="checked"'; + $processRowName = HelperFormElement::buildFormElementName([FE_NAME => F_PROCESS_ROW_COLUMN], $array[$idName]); + $processRowValue = '<label class="checkbox"><input name="' . $processRowName . + '" type="checkbox" ' . $checked . '></label>'; + $processRow = [$processRowKey => $processRowValue]; + // Inserted at index 0 and thus displayed in first column of table. + $array = $processRow + $array; + } + } // No rows: nothing to do. if (empty($parentRecords)) { return $this->formSpec[F_MULTI_MSG_NO_RECORD]; } - // Check for 'id' or '_id' as column name - $idName = isset($parentRecords[0]['_' . F_MULTI_COL_ID]) ? '_' . F_MULTI_COL_ID : F_MULTI_COL_ID; - - // Check that an column 'id' is given + // Check that a column 'id' is given if (!isset($parentRecords[0][$idName])) { throw new \UserFormException( json_encode([ERROR_MESSAGE_TO_USER => 'Missing column "_' . F_MULTI_COL_ID . '"', ERROR_MESSAGE_TO_DEVELOPER => $this->formSpec[F_MULTI_SQL]]), @@ -254,7 +272,7 @@ abstract class AbstractBuildForm { $tableHead = Support::wrapTag('<tr>', $this->buildMultiFormTableHead($parentRecords[0])); - return '<table class="table"><thead>' . $tableHead . '</thead><tbody>' . $htmlElements . '</tbody></table>'; + return '<table class="table table-multi-form qfq-table-100"><thead>' . $tableHead . '</thead><tbody>' . $htmlElements . '</tbody></table>'; } /** diff --git a/extension/Classes/Core/Constants.php b/extension/Classes/Core/Constants.php index 5adc69dda57c9ac5edfd054c4665dd08060d38c3..0883c276e2cea60dd0615a1a98d60429b7b9bc96 100644 --- a/extension/Classes/Core/Constants.php +++ b/extension/Classes/Core/Constants.php @@ -1160,6 +1160,9 @@ const F_MODE_REQUIRED_OFF_BUT_MARK = 'requiredOffButMark'; const F_MODE_SKIP_REQUIRED_CHECK = 'skipRequiredCheck'; // deprecated since third revision of #9617 const F_MODE_GLOBAL = 'formModeGlobal'; +const F_PROCESS_ROW = 'processRow'; +const F_PROCESS_ROW_COLUMN = '_processRow'; + const F_SAVE_BUTTON_ACTIVE = 'saveButtonActive'; const F_SAVE_BUTTON_TEXT = SYSTEM_SAVE_BUTTON_TEXT; diff --git a/extension/Classes/Core/Save.php b/extension/Classes/Core/Save.php index f87d1df10f11f40877a7579a6a85d52731d6ffde..ea55fa131b52555fa74ba7769aaac36bc77bca85 100644 --- a/extension/Classes/Core/Save.php +++ b/extension/Classes/Core/Save.php @@ -279,9 +279,18 @@ class Save { $fillStoreForm = new FillStoreForm(); $storeVarBase = $this->store->getStore(STORE_VAR); + $flagCheckProcessRow = isset($this->formSpec[F_PROCESS_ROW]) && $this->formSpec[F_PROCESS_ROW] != '0'; foreach ($parentRecords as $row) { + // Checks if row has to be processed. + if ($flagCheckProcessRow) { + $processRowName = HelperFormElement::buildFormElementName([FE_NAME => F_PROCESS_ROW_COLUMN], $row[$idName]); + if ('on' !== $this->store->getVar($processRowName, STORE_CLIENT . STORE_ZERO, SANITIZE_ALLOW_ALNUMX)) { + continue; + } + } + // Always start with a clean STORE_VAR $this->store->setStore($storeVarBase, STORE_VAR, true); @@ -298,7 +307,8 @@ class Save { $rc = $this->processSingle($row[$idName], $formAction); } - return $rc; + // If no rows are selected, saving is still possible, thus requiring a null coalescing operator. + return $rc ?? ''; } /** diff --git a/javascript/src/QfqForm.js b/javascript/src/QfqForm.js index 94b73d8f5f971a616746fbd0b466ff4aab65e921..5c8ca6f448d4077aeb61294162d38a366983e5b9 100644 --- a/javascript/src/QfqForm.js +++ b/javascript/src/QfqForm.js @@ -164,6 +164,13 @@ var QfqNS = QfqNS || {}; $(".radio").append($("<span>", { class: "checkmark", aria: "hidden"})); $(".checkbox").append($("<span>", { class: "checkmark", aria: "hidden"})); + // Feature process all rows + $(".process-row-all input[type=checkbox]").on("click", function() { + var checkboxes = document.querySelectorAll('input[name^="_processRow-"]'); + for (var i = 0; i < checkboxes.length; i++) { + checkboxes[i].checked = $(this).is(':checked'); + } + }); }; n.QfqForm.prototype.on = n.EventEmitter.onMixin; diff --git a/less/qfq-bs.css.less b/less/qfq-bs.css.less index bed131f411283ea737a408ce52e9e84d4367bd3b..b770118764ce96dffd402247981eda06fb1a47c8 100644 --- a/less/qfq-bs.css.less +++ b/less/qfq-bs.css.less @@ -1466,4 +1466,15 @@ input.qfq-password { .CodeMirror { resize: both; +} + +// lower min-height for checkbox in header +.process-row-label-header { + min-height: 20px !important; + font-weight: bold; +} + +// +.table-multi-form > tbody > tr > td { + vertical-align: middle; } \ No newline at end of file