Commit aefcb959 authored by Carsten  Rose's avatar Carsten Rose
Browse files

Merge branch 'B6176IconNotAlignOnError'

parents 20e16ec0 a09ee979
......@@ -1203,7 +1203,7 @@ Bug Fixes
* Dynamic Update has been broken since implementing of 'element-update' (#3180). Now both methods, 'element-update' and 'form-update' should be fine.
* qfq-bs.css.less: Fixed problem with 'typeahead input elements' not expanded to Bootstrap column width. Changed
Layout/Design Typeahead drop-down box. Add hoover for the drop-down box with a blue background
Layout/Design Typeahead drop-down box. Add hover for the drop-down box with a blue background
* AbstractBuildForm.php: #3374 - textarea elements now contains 'maxlength' attribute.
* BuildFormBootstrap.php: wrapping of optional 'submitButtonText' now done with the 'per form' values.
* typeahead.php: if there is an exception, the message body is sent as regular 'content' for the drop-down box. At the
......
# Drag And Drop
## Sort
Initialize a dnd container by adding the class "qfq-dnd"
Set container object class to `class="qfq-dnd qfq-dnd-sort"`.
Add the data elements: `data-dnd-api="url"` and `data-dnd-key="key"`.
For the children inside of the container (just the first children):
add `data-dnd-id` to a reference you can handle (probably record id).
Request will be sent containing following GET variables:
* dragId = `data-dnd-id` of the dragged object,
* dragPosition = client internal old position of the dragged object.
* setTo = "after" or "before",
* hoverId = `data-dnd-id` id of the element the dragged element is now hovering, meaning before or after.
* hoverPosition = client internal position of currently hovered element.
Example: http://something/bla?dragId=uno&dragPosition=1&setTo=before&hoverId=tre&hoverPosition=3
\ No newline at end of file
......@@ -73,11 +73,11 @@ the Client by adding following name/value pairs to the response JSON
Stream
{
"status": "error",
...
"field-name": "<field name>",
"field-message": "<message>",
...
"status": "error",
...
"field-name": "<field name>",
"field-message": "<message>",
...
}
Only one validation failure per request can be reported to Client.
......@@ -465,7 +465,7 @@ Request Method
: GET
URL Parameters
: `s=<SIP>` (form, r)
: `s=<SIP>` (form, r)
: `action=lock`, `action=extend`, `action=release>`
: `recordHashMd5=<value of hidden form element 'recordHashMd5'>`
......@@ -479,7 +479,7 @@ JSON Response from the server (extended [Minimal Response]) containing:
{
"status": "success"|"error"|"conflict"|"conflict_allow_force",
"message": "<message>"
"message": "<message>"e5
}
`status` indicates how the request has been fulfilled by the server.
......@@ -488,6 +488,29 @@ On one of`"error"|"conflict"|"conflict_allow_force"` the Client must display `"<
On `"conflict"` the Client opens the alert as modal dialog (user can't change anything on the form) with a 'reload current
form' button.
On `"conflict_allow_force"` the Client opens the alert non-modal (default).
### Drag And Drop (sort)
Request
: api/dragAndDrop.php
Request Method
: GET
URL Parameters:
: `s=<SIP>` (`form=<formname>`)
:
: `dragId=<data-dnd-id of dragged element>`
: `dragPosition=<client internal position (numbering) of element before dragging>`
: `setTo=before`, `setTo=after`
: `hoverId=<data-dnd-id of dragged element>`
: `hoverPosition=<client internal position (numbering) of element after dragging>`
Server Response
: The response contains at least a [Minimal Response]. In addition, a
[HTML Element Update] may be included.
## Glossary
......
......@@ -2556,7 +2556,7 @@ See also at specific *FormElement* definitions.
+------------------------+--------+----------------------------------------------------------------------------------------------------------+
| htmlAfter | string | HTML Code wrapped after the complete *FormElement* |
+------------------------+--------+----------------------------------------------------------------------------------------------------------+
| wrapRow | string | If specified, skip default wrapping (`<div class='col-md-?>`). Instead the given string is used. |
| wrapRow | string | If specified, skip default wrapping (`<div class='col-md-?'>`). Instead the given string is used. |
+------------------------+--------+ |
| wrapLabel | string | |
+------------------------+--------+ |
......@@ -2937,6 +2937,8 @@ Type: text
the value is an empty string
* *inputType* = number (optional). Typically the HTML tag 'type' will be 'text', 'textarea' or 'number' (detected automatically).
If necessary, the HTML tag 'type' might be forced to a specific given value.
* *step* = Step size of the up/down buttons which increase/decrease the number of in the input field. Optional.
Default 1. Only useful with `inputType=number` (defined explicit via `inputType` or detected automatically).
.. _`input-typeahead`:
......@@ -4119,6 +4121,21 @@ Best practice
Custom default value only for 'new records'
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Method 1
''''''''
On `Form.parameter` define a `fillStoreVar` query with a column name equal to a form field. That's all.
Example: ::
FormElement.name = technicalContact
Form.parameter.fillStoreVar = {{!SELECT CONCAT(p.firstName, ' ', p.name) AS technicalContact FROM Person AS p WHERE p.account='{{feUser:T}}' }}
What we use here is the default STORE prio FSRVD. If the form loads with r=0, 'F', 'S' and 'R' are empty. 'V' is filled.
If r>0, than 'F' and 'S' are empty and 'R' is filled.
Method 2
''''''''
In the specific `FormElement` set `value={{columnName:RSE}}`. The link to the form should be rendered with
'"...&columnName=<data>&..." AS _page'. The trick is that the STORE_RECORD is empty for new records, and therefore the
corresponding value from STORE_SIP will be returned. Existing records will use the already saved value.
......@@ -6218,8 +6235,125 @@ E.g.::
10.sql = SELECT "p:home&r=0|t:Home|c:qfq-100 qfq-left" AS _pagev
Examples
--------
Drag and drop
-------------
Sort/order elements
^^^^^^^^^^^^^^^^^^^
Manually sorting and ordering of elements via `HTML5 drag and drop` is supported via QFQ. Any sortable element
should be represented by a database record with an order column. If the elements are unordered, they will be ordered after
the first 'drag and drop' move of an element.
Functionality is divided into:
* Display list: the records will be displayed via QFQ/report.
* Update database: updates of the order column are managed by a specific 'drag and drop' definition form.
Part 1: Display list
''''''''''''''''''''
Display the list of elements via a regular QFQ content record. All 'drag and drop' elements have to be nested by an HTML
element:
* With `class="qfq-dnd-sort"`.
* With an automatically SIP protected form name: `{{'form=<form name>' AS _data-dnd-api}}`
* Only direct children of such element can be dragged.
* Every children needs a unique identifier `data-dnd-id="<unique>"`. Typically this is the corresponding record id.
* The record needs a dedicated order column, which will be updated through API calls in time.
A `<div>` example HTML output: ::
<div class="qfq-dnd-sort" data-dnd-api="typo3conf/ext/qfq/qfq/api/dragAndDrop.php?s=badcaffee1234">
<div class="anyClass" id="<uniq1>" data-dnd-id="55">
Numbero Uno
</div>
<div class="anyClass" id="<uniq2>" data-dnd-id="18">
Numbero Deux
</div>
<div class="anyClass" id="<uniq3>" data-dnd-id="27">
Numbero Tre
</div>
</div>
A typical QFQ report which generates those `<div>` HTML: ::
10 {
sql = SELECT '<div id="anytag-', n.id,'" data-dnd-id="', n.id,'">' , n.note, '</div>'
FROM Note AS n
WHERE grId=28
ORDER BY n.ord
head = <div class="qfq-dnd-sort" data-dnd-api="{{'form=dndSortNote&grId=28|A:dnd-sort' AS _api}}">
tail = </div>
}
A `<table>` based setup is also possible. Note the attribute `data-columns="3"` - those generates a dropzone
which is the same width as the outer table. ::
<table>
<tbody class="qfq-dnd-sort" data-dnd-api="typo3conf/ext/qfq/qfq/api/dragAndDrop.php?s=badcaffee1234" data-columns="3">
<tr> class="anyClass" id="<uniq1>" data-dnd-id="55">
<td>Numbero Uno</td><td>Numbero Uno.2</td><td>Numbero Uno.3</td>
</tr>
<tr class="anyClass" id="<uniq2>" data-dnd-id="18">
<td>Numbero Deux</td><td>Numbero Deux.2</td><td>Numbero Deux.3</td>
</tr>
<tr class="anyClass" id="<uniq3>" data-dnd-id="27">
<td>Numbero Tre</td><td>Numbero Tre.2</td><td>Numbero Tre.3</td>
</tr>
</tbody>
</table>
A typical QFQ report which generates those HTML: ::
10 {
sql = SELECT '<tr id="anytag-', n.id,'" data-dnd-id="', n.id,'" data-columns="3">' , n.id AS '_+td', n.note AS '_+td', n.ord AS '_+td', '</tr>'
FROM Note AS n
WHERE grId=28
ORDER BY n.ord
head = <table><tbody class="qfq-dnd-sort" {{'form=dndSortNote&grId=28' AS _data-dnd-api}} data-columns="3">
tail = </tbody><table>
}
Part 2: Update database
'''''''''''''''''''''''
A dedicated `Form`, without any `FormElements`, is needed to define the database update definition.
Fields:
* Name: <custom form name> - used in Part 1 in the `_data-dnd-api` variable.
* Table: <table with the element records> - used to the update the records specified by `dragAndDropOrderSql`.
* Parameter:
+-------------------------------------+--------------------------------------------------------------------------------+
| Attribute | Description |
+=====================================+================================================================================+
| orderInterval = <number> | Optional. By default '10'. Might be any number > 0. |
+-------------------------------------+--------------------------------------------------------------------------------+
| orderColumn = <column name> | Optional. By default 'ord'. |
+-------------------------------------+--------------------------------------------------------------------------------+
| dragAndDropOrderSql = | Query to selects the *same* records as the report in the |
| {{!SELECT n.id AS id, n.ord AS ord FROM Note AS n | same *order!* Inconsistencies results in sort differences. |
| ORDER BY n.ord}} | The columns `id` and `ord` are *mandatory.* |
+-------------------------------------------------------+--------------------------------------------------------------+
The form related to the example of part 1: ::
Form.name: dndSortNote
Form.table: Note
Form.parameter: orderInterval = 1
Form.parameter: orderColumn = ord
Form.parameter: dragAndDropOrderSql = {{!SELECT n.id AS id, n.ord AS ord FROM Note AS n WHERE n.grId={{grId:S0}} ORDER BY n.ord}}
Report Examples
---------------
The following section gives some examples of typical reports
......@@ -6571,42 +6705,42 @@ Create / edit `AutoCron` jobs
Create a T3 page with a QFQ record (similar to the formeditor). Such page should be access restricted and is only needed
to edit `AutoCron` jobs: ::
dbIndex={{indexQfq:Y}}
form={{form:S}}
dbIndex={{indexQfq:Y}}
form={{form:S}}
10 {
# Table header.
sql = SELECT CONCAT('p:{{pageId:T}}&form=cron') AS _pagen, 'id', 'Next run','Frequency','Comment','Last run','In progress', 'Status' FROM (SELECT 1) AS fake WHERE '{{form:SE}}'=''
head = <table class='table table-hover qfq-table-50'>
tail = </table>
rbeg = <thead><tr>
rend = </tr></thead>
fbeg = <th>
fend = </th>
10 {
# All Cron Jobs
sql = SELECT CONCAT('<tr class="',
IF(c.lastStatus LIKE 'Error%','danger',''),
IF(c.inProgress!=0 AND DATE_ADD(c.inProgress, INTERVAL 10 MINUTE)<NOW(),' warning',''),
IF(c.status='enable','',' text-muted'),'" ',
IF(c.inProgress!=0 AND DATE_ADD(c.inProgress, INTERVAL 10 MINUTE)<NOW(),'title="inProgress > 10mins"',
IF(c.lastStatus LIKE 'Error%','title="Status: Error"','')),
'>'),
'<td>', CONCAT('p:{{pageId:T}}&form=cron&r=', c.id) AS _pagee, '</td><td>',
c.id, '</td><td>',
IF(c.nextrun=0,"", DATE_FORMAT(c.nextrun, "%d.%m.%y %H:%i:%s")), '</td><td>',
c.frequency, '</td><td>',
c.comment, '</td><td>',
IF(c.lastrun=0,"", DATE_FORMAT(c.lastrun,"%d.%m.%y %H:%i:%s")), '</td><td>',
IF(c.inProgress=0,"", DATE_FORMAT(c.inProgress,"%d.%m.%y %H:%i:%s")), '</td><td>',
LEFT(c.laststatus,40) AS '_+pre', '</td><td>',
CONCAT('U:form=cron&r=', c.id) AS _paged, '</td></tr>'
FROM Cron AS c
ORDER BY c.id
}
}
10 {
# Table header.
sql = SELECT CONCAT('p:{{pageId:T}}&form=cron') AS _pagen, 'id', 'Next run','Frequency','Comment','Last run','In progress', 'Status' FROM (SELECT 1) AS fake WHERE '{{form:SE}}'=''
head = <table class='table table-hover qfq-table-50'>
tail = </table>
rbeg = <thead><tr>
rend = </tr></thead>
fbeg = <th>
fend = </th>
10 {
# All Cron Jobs
sql = SELECT CONCAT('<tr class="',
IF(c.lastStatus LIKE 'Error%','danger',''),
IF(c.inProgress!=0 AND DATE_ADD(c.inProgress, INTERVAL 10 MINUTE)<NOW(),' warning',''),
IF(c.status='enable','',' text-muted'),'" ',
IF(c.inProgress!=0 AND DATE_ADD(c.inProgress, INTERVAL 10 MINUTE)<NOW(),'title="inProgress > 10mins"',
IF(c.lastStatus LIKE 'Error%','title="Status: Error"','')),
'>'),
'<td>', CONCAT('p:{{pageId:T}}&form=cron&r=', c.id) AS _pagee, '</td><td>',
c.id, '</td><td>',
IF(c.nextrun=0,"", DATE_FORMAT(c.nextrun, "%d.%m.%y %H:%i:%s")), '</td><td>',
c.frequency, '</td><td>',
c.comment, '</td><td>',
IF(c.lastrun=0,"", DATE_FORMAT(c.lastrun,"%d.%m.%y %H:%i:%s")), '</td><td>',
IF(c.inProgress=0,"", DATE_FORMAT(c.inProgress,"%d.%m.%y %H:%i:%s")), '</td><td>',
LEFT(c.laststatus,40) AS '_+pre', '</td><td>',
CONCAT('U:form=cron&r=', c.id) AS _paged, '</td></tr>'
FROM Cron AS c
ORDER BY c.id
}
}
Usage
......
<?php
/**
* Created by PhpStorm.
* User: ep
* Date: 12/23/15
* Time: 6:17 PM
*/
namespace qfq;
use qfq;
require_once(__DIR__ . '/../qfq/store/Store.php');
require_once(__DIR__ . '/../qfq/Constants.php');
require_once(__DIR__ . '/../qfq/QuickFormQuery.php');
//require_once(__DIR__ . '/../qfq/exceptions/UserFormException.php');
//require_once(__DIR__ . '/../qfq/exceptions/CodeException.php');
//require_once(__DIR__ . '/../qfq/exceptions/DbException.php');
//require_once(__DIR__ . '/../qfq/exceptions/ErrorHandler.php');
/**
* Return JSON encoded answer
*
* status: success|error
* message: <message>
* redirect: client|url|no
* redirect-url: <url>
* field-name: <field name>
* field-message: <message>
* form-data: [ fieldname1 => value1, fieldname2 => value2, ... ]
* form-control: [ fieldname1 => status1, fieldname2 => status2, ... ] status: show|hide, enabled|disabled,
* readonly|readwrite
*
* Description:
*
* Save successful. Button 'close', 'new'. Form.forward: 'auto'. Client logic decide to redirect or not. Show message
* if no redirect. status = 'success' message = <message> redirect = 'client'
*
* Save successful. Button 'close': Form.forward: 'page'. Client redirect to url.
* status = 'success'
* message = <message>
* redirect = 'url'
* redirect-url = <URL>
*
* Save failed: Button: any. Show message and set 'alert' on _optional_ specified form element. Bring 'pill' of
* specified form element to front. status = 'error' message = <message> redirect = 'no' Optional: field-name = <field
* name> field-message = <message appearing as tooltip (or similar) near the form element>
*/
$answer = array();
$answer[API_REDIRECT] = API_ANSWER_REDIRECT_NO;
$answer[API_STATUS] = API_ANSWER_STATUS_ERROR;
$answer[API_MESSAGE] = '';
try {
$qfq = new QuickFormQuery(['bodytext' => '']);
$data = $qfq->dragAndDrop();
$answer[API_STATUS] = API_ANSWER_STATUS_SUCCESS;
$answer[API_MESSAGE] = 'reorder: success';
// $answer[API_FORM_UPDATE] = $data[API_FORM_UPDATE];
// $answer[API_ELEMENT_UPDATE] = $data[API_ELEMENT_UPDATE];
// unset($answer[API_FORM_UPDATE][API_ELEMENT_UPDATE]);
} catch (qfq\UserFormException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
} catch (qfq\CodeException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
} catch (qfq\DbException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
} catch (\Exception $e) {
$answer[API_MESSAGE] = "Generic Exception: " . $e->getMessage();
}
header("Content-Type: application/json");
echo json_encode($answer);
......@@ -86,6 +86,8 @@ abstract class AbstractBuildForm {
* @param array $feSpecAction
* @param array $feSpecNative
* @param array $db
* @throws CodeException
* @throws UserFormException
*/
public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative, array $db = null) {
$this->formSpec = $formSpec;
......@@ -172,6 +174,8 @@ abstract class AbstractBuildForm {
* @param array $latestFeSpecNative
* @return array|string $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all
* formElement.dynamicUpdate-yes values/states
* @throws CodeException
* @throws DbException
* @throws UserFormException
*/
public function process($mode, $htmlElementNameIdZero = false, $latestFeSpecNative = array()) {
......@@ -247,6 +251,8 @@ abstract class AbstractBuildForm {
*
* @param string $mode
* @return string
* @throws CodeException
* @throws UserFormException
*/
public function head($mode = FORM_LOAD) {
$html = '';
......@@ -276,6 +282,8 @@ abstract class AbstractBuildForm {
*
* @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:...,
* ...]</small>
* @throws CodeException
* @throws UserFormException
*/
public function createFormEditorUrl($form, $recordId, array $param = array()) {
......@@ -321,6 +329,9 @@ abstract class AbstractBuildForm {
* Returns '<form ...>'-tag with various attributes.
*
* @return string
* @throws CodeException
* @throws DbException
* @throws UserFormException
*/
public function getFormTag() {
$md5 = '';
......@@ -338,6 +349,9 @@ abstract class AbstractBuildForm {
* Build MD5 from the current record. Return HTML Input element.
*
* @return string
* @throws CodeException
* @throws DbException
* @throws UserFormException
*/
public function buildInputRecordHashMd5() {
......@@ -357,6 +371,9 @@ abstract class AbstractBuildForm {
* @param $recordId
*
* @return string
* @throws CodeException
* @throws DbException
* @throws UserFormException
*/
public function buildRecordHashMd5($tableName, $recordId) {
$record = array();
......@@ -372,6 +389,8 @@ abstract class AbstractBuildForm {
* Create HTML Input vars to detect bot automatic filling of forms.
*
* @return string
* @throws CodeException
* @throws UserFormException
*/
public function getHoneypotVars() {
$html = '';
......@@ -442,6 +461,9 @@ abstract class AbstractBuildForm {
* See: https://www.w3.org/wiki/HTML/Elements/form#HTML_Attributes
*
* @return string
* @throws CodeException
* @throws DbException
* @throws UserFormException
*/
public function getEncType() {
......@@ -457,6 +479,10 @@ abstract class AbstractBuildForm {
* @param array|string $value
*
* @return array|string
* @throws CodeException
* @throws DbException
* @throws UserFormException
* @throws UserReportException
*/
private function processReportSyntax($value) {
......@@ -502,6 +528,10 @@ abstract class AbstractBuildForm {
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
*
* @return string
* @throws CodeException
* @throws DbException
* @throws UserFormException
* @throws UserReportException
*/
public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0, array &$json,
$modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false,
......@@ -672,6 +702,10 @@ abstract class AbstractBuildForm {
* @param array $formElement
*
* @return array
* @throws CodeException
* @throws DbException
* @throws UserFormException
* @throws UserReportException
*/
private function prepareFillStoreFireLdap(array $formElement) {
......@@ -767,6 +801,8 @@ abstract class AbstractBuildForm {
* save/update.
*
* @return string
* @throws CodeException
* @throws UserFormException
*/
private function prepareT3VarsForSave() {
......@@ -802,6 +838,8 @@ abstract class AbstractBuildForm {
* Get all elements from STORE_ADDITIONAL_FORM_ELEMENTS and return them as a string.
*
* @return string
* @throws CodeException
* @throws UserFormException
*/
private function buildAdditionalFormElements() {
......@@ -816,6 +854,8 @@ abstract class AbstractBuildForm {
* @param array $json
*
* @return string <input type='hidden' name='s' value='<sip>'>
* @throws CodeException
* @throws UserFormException
*/
public function buildHiddenSip(array &$json) {
......@@ -921,6 +961,7 @@ abstract class AbstractBuildForm {
* @param array $feMode
*
* @return array
* @throws UserFormException
*/
private function getJsonFeMode($feMode) {
......@@ -970,6 +1011,7 @@ abstract class AbstractBuildForm {
* @param string $addClass
*
* @return string
* @throws CodeException
*/
public function buildLabel($htmlFormElementName, $label, $addClass = '') {
$attributes = Support::doAttribute('for', $htmlFormElementName);
......@@ -985,6 +1027,8 @@ abstract class AbstractBuildForm {
*
* @param $toolTipNew
* @return string
* @throws CodeException
* @throws UserFormException
*/
public function deriveNewRecordUrlFromExistingSip(&$toolTipNew) {
......@@ -1048,6 +1092,8 @@ abstract class AbstractBuildForm {
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
*
* @return string complete rendered HTML input element.
* @throws CodeException
* @throws UserFormException
*/
public function buildInput(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {
$textarea = '';
......@@ -1154,7 +1200,7 @@ abstract class AbstractBuildForm {
if (empty($formElement[F_FE_DATA_PATTERN_ERROR])) {
$formElement[F_FE_DATA_PATTERN_ERROR] = $sanitizeMessage;
};
$attribute .= $this->getAttributeList($formElement, [F_FE_DATA_PATTERN_ERROR, F_FE_DATA_REQUIRED_ERROR, F_FE_DATA_MATCH_ERROR, F_FE_DATA_ERROR, FE_MIN, FE_MAX]);
$attribute .= $this->getAttributeList($formElement, [F_FE_DATA_PATTERN_ERROR, F_FE_DATA_REQUIRED_ERROR, F_FE_DATA_MATCH_ERROR, F_FE_DATA_ERROR, FE_MIN, FE_MAX, FE_STEP]);
$attribute .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');
$attribute .= Support::doAttribute('title', $formElement[FE_TOOLTIP]);
......@@ -1192,6 +1238,7 @@ abstract class AbstractBuildForm {
* @param array $formElement
*
* @return string
* @throws CodeException
* @throws UserFormException
*/
private function typeAheadBuildParam(array &$formElement) {
......@@ -1297,6 +1344,7 @@ abstract class AbstractBuildForm {
* @param bool $flagOmitEmpty
*
* @return string