Commit 84b36b73 authored by Carsten  Rose's avatar Carsten Rose
Browse files

Merge branch 'F5695-Multiform' into 'master'

F5695 multiform

See merge request !186
parents 57330aac 9bc3c9c1
Pipeline #2507 passed with stages
in 2 minutes and 35 seconds
Neuer Build
===========
* release: Wird ein *Tag* vergeben (egal welcher Branch) der mit 'v' beginnt, erzeugt das automatisch einen Build - https://w3.math.uzh.ch/qfq/release.
* snapshot: Jeder Commit (egal welcher Branch) erzeugt einen Snapshot - https://w3.math.uzh.ch/qfq/snapshot.
* nightly: Nach einem Commit auf Branch 'master' tagsueber, wird um 23:55 ein 'nightly' Build erstellt - https://w3.math.uzh.ch/qfq/nightly.
......
......@@ -4136,10 +4136,14 @@ Parameter: slaveId
Note:
* `{{slaveId}}` can be used in any query of the current *FormElement*.
* `{{slaveId:V}}` can be used in any query of the current *FormElement*.
* If the `action`-*FormElement* name exist as a column in the master record: Update that column *automatically* with the
recent slaveId
* After an INSERT the `last_insert_id()` becomes the *slaveId*).
* After an INSERT the `last_insert_id()` becomes the *{{slaveId:V}}*.
* `fillStoreVar` is fired first, than `slaveId`.
* If `slaveId` is known in `fillStoreVar`, set: `slaveId={{someId:V}}`.
Parameter: sqlBefore / sqlInsert / sqlUpdate / sqlDelete / sqlAfter
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
......@@ -4348,10 +4352,63 @@ Action
in one of the specified required FEs.
.. _multi-language-form:
.. _multi-form:
Multi Language Form
-------------------
Multi Form
----------
`Multi Forms` are like a regular form with the difference that the shown FormElements are repeated for *each* selected record
(defined by `multiSql`).
+------------------+----------------------------------+------------------------------------------------+
| Name | | |
+==================+==================================+================================================+
| multiSql | {{!SELECT id, name FROM Person}} | Query to select MulitForm records |
+------------------+----------------------------------+------------------------------------------------+
| multiMgsNoRecord | Default: No data | Message shown if `multiSql` selects no records |
+------------------+----------------------------------+------------------------------------------------+
The Form is shown as a HTML table.
* `multiSql`: Selects the records where the defined FormElements will work on each.
* A uniq column 'id' or '_id' (not shown) is mandatory and has to reflect an existing record id in table `primary table`.
* Additional columns, defined in `multiSql`, will be shown on the form in the same line, before the FormElements.
`
Simple
======
General:
* It's not possible to create new records in simple mode, only existing records can be used.
Form:
* Per row, the STORE_RECORD is filled with the whole record of the primary table, referenced
by `multiSql.id`.
FormElement:
* The FormElement.name represents a column of the defined primary table.
* The existing values of such FormElements are automatically loaded.
* No further definition is required.
Advanced
========
* The `FormElement.name` do not have to be a column of the primary table.
* If `FormElement.name` is not a column of the primary table, the insert/update/delete SQL statement has to be
extra defined.
.. _multiple-languages:
Multiple languages
------------------
QFQ Forms might be configured for up to 5 different languages. Per language there is one extra field in the *Form editor*.
Which field represents which language is configured in configuration_.
......
......@@ -21,10 +21,13 @@ class QfqController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController {
/**
* @return string
* @throws \CodeException
* @throws \UserFormException
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws \UserFormException
*/
public function showAction() {
......
......@@ -194,6 +194,140 @@ abstract class AbstractBuildForm {
abstract public function fillWrap();
/**
* @param $filter
* @param $modeCollectFe
* @param array $rcJson
* @return string
* @throws \CodeException
* @throws \DbException
* @throws \DownloadException
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws \UserFormException
* @throws \UserReportException
*/
private function buildMultiForm($filter, $modeCollectFe, array &$rcJson) {
$htmlElements = '';
$rcJson = array();
$parentRecords = $this->evaluate->parse($this->formSpec[F_MULTI_SQL], ROW_REGULAR);
// 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
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]]),
ERROR_INVALID_OR_MISSING_PARAMETER);
}
// This is a dirty workaround for formSave: clear FORM STORE (already outdated values).
// Otherwise those outdated values will be taken to fill non primary FE in multiForm (which are garbage).
// Better solution would be to have FORM_STORE in sync.
$this->store::unsetStore(STORE_FORM);
$storeVarBase = $this->evaluate->parse($this->formSpec[FE_FILL_STORE_VAR]);
if (!is_array($storeVarBase)) {
$storeVarBase = array();
}
// Per row, iterate over all form elements
foreach ($parentRecords as $row) {
// Always start with a clean STORE_VAR
$this->store->setStore($storeVarBase, STORE_VAR, true);
$this->store->setStore($row, STORE_PARENT_RECORD, true);
$this->store->setVar(F_MULTI_COL_ID, $row[$idName], STORE_PARENT_RECORD); // In case '_id' is used, both '_id' and 'id' should be accessible.
$record = $this->dbArray[$this->dbIndexData]->sql('SELECT * FROM `' . $this->formSpec[F_TABLE_NAME] . '` WHERE id=' . $row[F_MULTI_COL_ID], ROW_EXPECT_1);
$this->store->setStore($record, STORE_RECORD, true);
$jsonTmp = array();
$feTmp = $this->feSpecNative;
$leftColumns = $this->buildMultiFormLeftColumns($row);
$rightInputs = $this->elements($row[$idName], $filter, 0, $jsonTmp, $modeCollectFe,
false, STORE_USE_DEFAULT, FORM_LOAD, true);
$htmlElements .= Support::wrapTag('<tr>', $leftColumns . $rightInputs);
// Clean for the next round
$this->feSpecNative = $feTmp;
$this->store::unsetStore(STORE_RECORD);
$rcJson = array_merge($rcJson, $jsonTmp);
}
$tableHead = Support::wrapTag('<tr>', $this->buildMultiFormTableHead($parentRecords[0]));
return '<table class="table"><thead>' . $tableHead . '</thead><tbody>' . $htmlElements . '</tbody></table>';
}
/**
* @param array $row
* @return string
*/
private function buildMultiFormLeftColumns(array $row) {
$line = '';
// Collect columns
foreach ($row as $key => $value) {
if (($key[0] ?? '') != '_') {
$line .= "<td>$value</td>";
}
}
return $line;
}
/**
* @param array $row
* @return string
* @throws \CodeException
* @throws \UserFormException
*/
private function buildMultiFormTableHead(array $row) {
$line = '';
// Collect columns
foreach ($row as $key => $value) {
if (($key[0] ?? '') != '_') {
$line .= "<th>$key</th>";
}
}
// Collect label from FormElements
foreach ($this->feSpecNative as $formElement) {
$editFeHtml = '';
// debugStack as Tooltip
if ($this->showDebugInfoFlag) {
// Build 'FormElement' Edit symbol
$feEditUrl = $this->createFormEditorUrl(FORM_NAME_FORM_ELEMENT, $formElement[FE_ID], ['formId' => $formElement[FE_FORM_ID]]);
$titleAttr = Support::doAttribute('title', $this->formSpec[FE_NAME] . ' / ' . $formElement[FE_NAME] . ' [' . $formElement[FE_ID] . ']');
$icon = Support::wrapTag('<span class="' . GLYPH_ICON . ' ' . GLYPH_ICON_EDIT . '">', '');
$editFeHtml = ' ' . Support::wrapTag("<a class='hidden " . CLASS_FORM_ELEMENT_EDIT . "' href='$feEditUrl' $titleAttr>", $icon);
}
$line .= '<th>' . $formElement[FE_LABEL] . $editFeHtml . '</th>';
}
return $line;
}
/**
* Builds complete 'form'. Depending of form specification, the layout will be 'plain' / 'table' / 'bootstrap'.
*
......@@ -206,17 +340,19 @@ abstract class AbstractBuildForm {
* @throws \CodeException
* @throws \DbException
* @throws \DownloadException
* @throws \UserFormException
* @throws \UserReportException
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws \UserFormException
* @throws \UserReportException
*/
public function process($mode, $htmlElementNameIdZero = false, $latestFeSpecNative = array()) {
$htmlHead = '';
$htmlTail = '';
$htmlT3vars = '';
$htmlSubrecords = '';
$htmlElements = '';
$json = array();
......@@ -240,18 +376,10 @@ abstract class AbstractBuildForm {
$filter = $this->getProcessFilter();
if ($this->formSpec['multiMode'] !== 'none') {
$parentRecords = $this->dbArray[$this->dbIndexQfq]->sql($this->formSpec['multiSql']);
foreach ($parentRecords as $row) {
$this->store->setStore($row, STORE_PARENT_RECORD, true);
$jsonTmp = array();
$htmlElements = $this->elements($row['_id'], $filter, 0, $jsonTmp, $modeCollectFe);
$json[] = $jsonTmp;
}
} else {
// Form type
if ($this->formSpec[F_MULTI_SQL] === '') {
// Regular Form
$recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);
if (!($recordId == '' || is_numeric($recordId))) {
throw new \UserFormException(
......@@ -271,23 +399,28 @@ abstract class AbstractBuildForm {
// Via 'element-update'
$json[][API_ELEMENT_UPDATE][DIRTY_RECORD_HASH_MD5][API_ELEMENT_ATTRIBUTE]['value'] = $md5;
}
} else {
// Multi Form
if ($mode === FORM_LOAD || $mode === FORM_SAVE) {
$htmlElements = $this->buildMultiForm($filter, $modeCollectFe, $json);
}
}
// <form>
if ($mode === FORM_LOAD) {
$htmlT3vars = $this->prepareT3VarsForSave();
$htmlTail = $this->tail();
$htmlSubrecords = $this->doSubrecords();
}
$htmlHidden = $this->buildAdditionalFormElements();
$htmlSip = $this->buildHiddenSip($json);
return ($mode === FORM_LOAD) ? $htmlHead . $htmlHidden . $htmlElements . $htmlSip . $htmlT3vars . $htmlTail . $htmlSubrecords : $json;
return ($mode === FORM_LOAD) ? $htmlHead . $htmlHidden . $htmlElements . $htmlSip . $htmlT3vars . $htmlTail : $json;
}
/**
* Builds the head area of the form.
* Build the head area of the form.
*
* @param string $mode
* @return string
......@@ -527,11 +660,14 @@ abstract class AbstractBuildForm {
* @throws \CodeException
* @throws \DbException
* @throws \DownloadException
* @throws \UserFormException
* @throws \UserReportException
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws \UserFormException
* @throws \UserReportException
*/
private function processReportSyntax($value) {
......@@ -565,6 +701,7 @@ abstract class AbstractBuildForm {
return $value;
}
/**
* Process all FormElements in $this->feSpecNative: Collect and return all HTML code & JSON.
*
......@@ -576,20 +713,24 @@ abstract class AbstractBuildForm {
* @param bool $htmlElementNameIdZero
* @param string $storeUseDefault
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @param bool $flagMulti
*
* @return string
* @throws \CodeException
* @throws \DbException
* @throws \DownloadException
* @throws \UserFormException
* @throws \UserReportException
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws \UserFormException
* @throws \UserReportException
*/
public function elements($recordId, $filter, $feIdContainer, array &$json,
$modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false,
$storeUseDefault = STORE_USE_DEFAULT, $mode = FORM_LOAD) {
$storeUseDefault = STORE_USE_DEFAULT, $mode = FORM_LOAD, $flagMulti = false) {
$html = '';
// The following 'FormElement.parameter' will never be used during load (fe.type='upload'). FE_PARAMETER has been already expanded.
......@@ -637,9 +778,8 @@ abstract class AbstractBuildForm {
$this->store->appendToStore($fe[FE_FILL_STORE_VAR], STORE_VAR);
}
// for Upload FormElements, it's necessary to pre-calculate an optional given 'slaveId'.
if ($fe[FE_TYPE] === FE_TYPE_UPLOAD) {
Support::setIfNotSet($fe, FE_SLAVE_ID);
// If given, slaveId will be copied to STORE_VAR
if (!empty($fe[FE_SLAVE_ID])) {
$this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_SLAVE_ID, STORE_SYSTEM); // debug
$slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID]));
$this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR);
......@@ -666,11 +806,10 @@ abstract class AbstractBuildForm {
// $fe[FE_MODE_SQL] = '';
// }
if ($flagOutput === true) {
if ($flagOutput === true && !$flagMulti) {
$this->fillWrapLabelInputNote($formElement[FE_BS_LABEL_COLUMNS], $formElement[FE_BS_INPUT_COLUMNS], $formElement[FE_BS_NOTE_COLUMNS]);
}
$value = '';
Support::setIfNotSet($formElement, FE_VALUE);
if (is_array($formElement[FE_VALUE])) {
......@@ -734,19 +873,23 @@ abstract class AbstractBuildForm {
if ($flagOutput) {
// debugStack as Tooltip
if ($this->showDebugInfoFlag) {
if (count($debugStack) > 0) {
$elementHtml .= Support::doTooltip($formElement[FE_HTML_ID] . HTML_ID_EXTENSION_TOOLTIP, implode("\n", $debugStack));
}
// Build 'FormElement' Edit symbol
$feEditUrl = $this->createFormEditorUrl(FORM_NAME_FORM_ELEMENT, $formElement[FE_ID], ['formId' => $formElement[FE_FORM_ID]]);
$titleAttr = Support::doAttribute('title', $this->formSpec[FE_NAME] . ' / ' . $formElement[FE_NAME] . ' [' . $formElement[FE_ID] . ']');
$icon = Support::wrapTag('<span class="' . GLYPH_ICON . ' ' . GLYPH_ICON_EDIT . '">', '');
$elementHtml .= Support::wrapTag("<a class='hidden " . CLASS_FORM_ELEMENT_EDIT . "' href='$feEditUrl' $titleAttr>", $icon);
if (!$flagMulti) {
// Build 'FormElement' Edit symbol. MultiForms: Edit symbol is in thead.
$feEditUrl = $this->createFormEditorUrl(FORM_NAME_FORM_ELEMENT, $formElement[FE_ID], ['formId' => $formElement[FE_FORM_ID]]);
$titleAttr = Support::doAttribute('title', $this->formSpec[FE_NAME] . ' / ' . $formElement[FE_NAME] . ' [' . $formElement[FE_ID] . ']');
$icon = Support::wrapTag('<span class="' . GLYPH_ICON . ' ' . GLYPH_ICON_EDIT . '">', '');
$elementHtml .= Support::wrapTag("<a class='hidden " . CLASS_FORM_ELEMENT_EDIT . "' href='$feEditUrl' $titleAttr>", $icon);
}
}
// Construct Marshaller Name: buildRow
$buildRowName = 'buildRow' . $this->buildRowName[$formElement[FE_TYPE]];
// Construct Marshaller Name: buildRow...
$tmpName = $flagMulti ? 'MultiElement' : $this->buildRowName[$formElement[FE_TYPE]];
$buildRowName = 'buildRow' . $tmpName;
$html .= $formElement[FE_HTML_BEFORE] . $this->$buildRowName($formElement, $elementHtml, $htmlFormElementName) . $formElement[FE_HTML_AFTER];
}
......@@ -760,6 +903,21 @@ abstract class AbstractBuildForm {
return $html;
}
/**
* @param array $formElement
* @param $elementHtml
*
* @return string
*/
public function buildRowMultiElement(array $formElement, $elementHtml) {
$before = ($formElement[FE_HTML_BEFORE] == '') ? '<td>' : $formElement[FE_HTML_BEFORE];
$after = ($formElement[FE_HTML_AFTER] == '') ? '</td>' : $formElement[FE_HTML_AFTER];
return $before . $elementHtml . $after;
}
/**
* Checks if LDAP search is requested.
* Yes: prepare configuration and fire the query.
......@@ -2870,7 +3028,7 @@ abstract class AbstractBuildForm {
$flagWidthLimit = true;
$control[SUBRECORD_COLUMN_MAX_LENGTH][$columnName] = SUBRECORD_COLUMN_DEFAULT_MAX_LENGTH;
// a) 'City@maxLength=40', b) 'Status@icon', c) 'Mailto@maxLength=80@nostrip'
// a) 'City|maxLength=40', b) 'Status|icon', c) 'Mailto@maxLength=80|nostrip'
$arr = KeyValueStringParser::parse($columnName, '=', '|', KVP_IF_VALUE_EMPTY_COPY_KEY);
foreach ($arr as $attribute => $value) {
switch ($attribute) {
......
......@@ -149,6 +149,11 @@ class BuildFormBootstrap extends AbstractBuildForm {
$class = ['tab-content', $this->formSpec[F_CLASS_BODY]];
if ($pill == '') {
$class[] = 'col-md-12';
$class[] = 'qfq-form-body'; // Make an outline on form body
if ($title == '') {
$class[] = 'qfq-form-no-title';
}
}
$html .= "<div " . Support::doAttribute('class', $class) . ">";
......@@ -714,6 +719,54 @@ EOF;
return $html;
}
/**
* Wrap content with $wrapArray or, if specified use $formElement[$wrapName]. Inject $htmlId in wrap.
*
* Result:
* - if $bsColumns==0 and empty $formElement[$wrapName]: no wrap
* - if $formElement[$wrapName] is given: wrap with that one. Else: wrap with $wrapArray
* - if $htmlId is give, inject it in $wrap.
*
* @param array $formElement Complete FormElement, especially some FE_WRAP
* @param string $htmlElement Content to wrap.
* @param string $wrapName FE_WRAP_ROW, FE_WRAP_LABEL, FE_WRAP_INPUT, FE_WRAP_NOTE
* @param int $bsColumns
* @param array $wrapArray System wide Defaults: [ 'open wrap', 'close wrap' ]
* @param string $htmlId
* @param string $class
*
* @return string Wrapped $htmlElement
* @throws \CodeException
* @throws \UserFormException
*/
private function customWrap(array $formElement, $htmlElement, $wrapName, $bsColumns, array $wrapArray, $htmlId = '', $class = '') {
// If $bsColumns==0: do not wrap with default.
if ($bsColumns == '0') {
$wrapArray[0] = '';
$wrapArray[1] = '';
}
// If there is a 'per FormElement'-wrap, take it.
if (isset($formElement[$wrapName])) {
$wrapArray = explode('|', $formElement[$wrapName], 2);
}
if (count($wrapArray) != 2) {
throw new \UserFormException("Need open & close wrap token for FormElement.parameter" . $wrapName . " - E.g.: <div ...>|</div>", ERROR_MISSING_VALUE);
}
if ($wrapArray[0] != '') {
$wrapArray[0] = Support::insertAttribute($wrapArray[0], 'id', $htmlId);
$wrapArray[0] = Support::insertAttribute($wrapArray[0], 'class', $class); // might be problematic, if there is already a 'class' defined.
if ($wrapName == FE_WRAP_LABEL) {
$wrapArray[0] = Support::insertAttribute($wrapArray[0], 'style', 'text-align: ' . $formElement[F_FE_LABEL_ALIGN] . ';'); // might be problematic, if there is already a 'class' defined.
}
}
return $wrapArray[0] . $htmlElement . $wrapArray[1];
}
/**
* @param array $formElement Complete FormElement, especially some FE_WRAP
* @param string $htmlElement Content to wrap.
......@@ -775,55 +828,6 @@ EOF;
return $html;
}
/**
* Wrap content with $wrapArray or, if specified use $formElement[$wrapName]. Inject $htmlId in wrap.
*
* Result:
* - if $bsColumns==0 and empty $formElement[$wrapName]: no wrap
* - if $formElement[$wrapName] is given: wrap with that one. Else: wrap with $wrapArray
* - if $htmlId is give, inject it in $wrap.
*
* @param array $formElement Complete FormElement, especially some FE_WRAP
* @param string $htmlElement Content to wrap.
* @param string $wrapName FE_WRAP_ROW, FE_WRAP_LABEL, FE_WRAP_INPUT, FE_WRAP_NOTE
* @param int $bsColumns
* @param array $wrapArray System wide Defaults: [ 'open wrap', 'close wrap' ]
* @param string $htmlId
* @param string $class
*
* @return string Wrapped $htmlElement
* @throws \CodeException
* @throws \UserFormException
*/
private function customWrap(array $formElement, $htmlElement, $wrapName, $bsColumns, array $wrapArray, $htmlId = '', $class = '') {
// If $bsColumns==0: do not wrap with default.
if ($bsColumns == '0') {
$wrapArray[0] = '';
$wrapArray[1] = '';
}
// If there is a 'per FormElement'-wrap, take it.
if (isset($formElement[$wrapName])) {
$wrapArray = explode('|', $formElement[$wrapName], 2);
}
if (count($wrapArray) != 2) {
throw new \UserFormException("Need open & close wrap token for FormElement.parameter" . $wrapName . " - E.g.: <div ...>|</div>", ERROR_MISSING_VALUE);
}
if ($wrapArray[0] != '') {
$wrapArray[0] = Support::insertAttribute($wrapArray[0], 'id', $htmlId);
$wrapArray[0] = Support::insertAttribute($wrapArray[0], 'class', $class); // might be problematic, if there is already a 'class' defined.
if ($wrapName == FE_WRAP_LABEL) {
$wrapArray[0] = Support::insertAttribute($wrapArray[0], 'style', 'text-align: ' . $formElement[F_FE_LABEL_ALIGN] . ';'); // might be problematic, if there is already a 'class' defined.
}
}
return $wrapArray[0] . $htmlElement . $wrapArray[1];
}
/**
* @param $formElement
* @param $elementHtml
......@@ -891,11 +895,9 @@ EOF;
$attribute = ($formElement[FE_MODE] == FE_MODE_HIDDEN) ? ' style="display: none;"' : '';
$attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID]);
return Support::wrapTag("<span name='qfq-subrecord' $attribute>", $html);
}
/**
* Builds complete 'form'.
*
......@@ -908,11 +910,14 @@ EOF;
* @throws \CodeException
* @throws \DbException
* @throws \DownloadException
* @throws \UserFormException
* @throws \UserReportException
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
* @throws \Twig\Error\LoaderError