From 011d48e86e77bd9e37c166c0be37e893d428ccea Mon Sep 17 00:00:00 2001 From: mgaoVA <16783916+mgaoVA@users.noreply.github.com> Date: Fri, 27 Oct 2023 14:27:21 -0400 Subject: [PATCH 01/65] don't run invalid queries --- LEAF_Request_Portal/sources/Form.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/LEAF_Request_Portal/sources/Form.php b/LEAF_Request_Portal/sources/Form.php index 5ff036e5b..b07113cd2 100644 --- a/LEAF_Request_Portal/sources/Form.php +++ b/LEAF_Request_Portal/sources/Form.php @@ -3943,12 +3943,15 @@ public function getIndicatorsAssociatedWithWorkflow($recordID) } $indicatorList = trim($indicatorList, ','); - $res = $this->db->query( - 'SELECT indicatorID, name, format - FROM indicators - WHERE indicatorID IN ('. $indicatorList .')' - ); - return $res; + $return = []; + if($indicatorList != '') { + $return = $this->db->query( + 'SELECT indicatorID, name, format + FROM indicators + WHERE indicatorID IN ('. $indicatorList .')' + ); + } + return $return; } /** From d58284494ceb35146b8bee18acab8c60f484d70b Mon Sep 17 00:00:00 2001 From: Carrie Hanscom Date: Wed, 1 Nov 2023 09:37:13 -0400 Subject: [PATCH 02/65] LEAF 2285 initial grid multiselect dropd, update repetitive row logic --- .../admin/templates/mod_form.tpl | 48 +- LEAF_Request_Portal/js/form.js | 4 +- LEAF_Request_Portal/js/gridInput.js | 427 ++++++------------ 3 files changed, 154 insertions(+), 325 deletions(-) diff --git a/LEAF_Request_Portal/admin/templates/mod_form.tpl b/LEAF_Request_Portal/admin/templates/mod_form.tpl index c319fadc3..066da0fea 100644 --- a/LEAF_Request_Portal/admin/templates/mod_form.tpl +++ b/LEAF_Request_Portal/admin/templates/mod_form.tpl @@ -905,6 +905,11 @@ function setFormatElementValue() { properties.file = $(this).find('select[id^="dropdown_file_select"]').val(); properties.hasHeader = Boolean(+$(this).find('select[id^="dropdown_file_header_select"]').val()); } + const elMultiCheckbox = this.querySelector('input[id^="grid_multiselect_checkbox_"]'); + if(elMultiCheckbox !== null) { + properties.multiselect = elMultiCheckbox?.checked === true ? 1 : 0; + } + } else { properties.type = 'textarea'; } @@ -1065,7 +1070,13 @@ function makeGrid(columns) { } $(gridBodyElement + ' > div:eq(' + i + ')').css('padding-bottom', '11px'); if($(gridBodyElement + ' > div:eq(' + i + ') > span.dropdown').length === 0){ - $(gridBodyElement + ' > div:eq(' + i + ')').append('
One option per line
'); + $(gridBodyElement + ' > div:eq(' + i + ')').append(` +
One option per line
+ + +
`); } } if(gridJSON[i].type.toString() === 'dropdown_file') { @@ -1087,6 +1098,9 @@ function makeGrid(columns) { + ` ); } @@ -1105,7 +1119,6 @@ function toggleDropDown(type, cell, columnNumber) { elJQDropDown = $(cell).parent().find('span.dropdown'); elJQDropDownFile = $(cell).parent().find('div.dropdown_file'); let ariaStatus = ''; - switch(type) { case 'dropdown': if(elJQDropDownFile.length === 1) { @@ -1117,6 +1130,9 @@ function toggleDropDown(type, cell, columnNumber) { `
One option per line
+
` ); $('#tableStatus').attr('aria-label', ariaStatus); @@ -1139,6 +1155,9 @@ function toggleDropDown(type, cell, columnNumber) { + ` ); ariaStatus += 'Source file select added.'; @@ -1711,31 +1730,6 @@ function gridDropdown(dropDownOptions){ return returnArray; } -/** - * Purpose: Create Array for Multi-Select Options - * @param multiSelectOptions - * @returns {[]|*} - */ -function gridMultiselect(multiSelectOptions){ - if(multiSelectOptions == null || multiSelectOptions.length === 0){ - return multiSelectOptions; - } - let uniqueNames = multiSelectOptions.split("\n"); - let returnArray = []; - uniqueNames = uniqueNames.filter(function(elem, index, self) { - return index == self.indexOf(elem); - }); - - $.each(uniqueNames, function(i, el){ - if(el === "no") { - uniqueNames[i] = "No"; - } - returnArray.push(uniqueNames[i]); - }); - - return returnArray; -} - /** * Purpose: Create Array for Multi-Answer Text * @param multiAnswerValue diff --git a/LEAF_Request_Portal/js/form.js b/LEAF_Request_Portal/js/form.js index 1e4ec01dc..14ad5c911 100644 --- a/LEAF_Request_Portal/js/form.js +++ b/LEAF_Request_Portal/js/form.js @@ -782,7 +782,9 @@ var LeafForm = function (containerID) { if ($("textarea", this).length) { cellArr.push($(this).find("textarea").val()); } else if ($("select", this).length) { - cellArr.push($("option:selected", this).val()); + const val = $("select", this).val(); + //multiselect selections will be in an array + cellArr.push(Array.isArray(val) ? val.join() : val); } else if ($("input", this).length) { cellArr.push($("input", this).val()); } diff --git a/LEAF_Request_Portal/js/gridInput.js b/LEAF_Request_Portal/js/gridInput.js index ce0502181..a86389d26 100644 --- a/LEAF_Request_Portal/js/gridInput.js +++ b/LEAF_Request_Portal/js/gridInput.js @@ -24,16 +24,21 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { } return gridInfo; } - function makeDropdown(options, selected, headerName = "") { - let dropdownElement = `"; - return dropdownElement; + function appendOptions(elSelect = {}, arrOptions = [], selectedValues = []) { + setTimeout(() => { + if((elSelect?.nodeName || '').toLowerCase() === 'select') { + arrOptions.forEach(opt => { + const option = document.createElement('option'); + option.value = opt; + option.innerText = opt; + const isSelected = selectedValues.length > 0 && selectedValues.some(o => o === opt); + if(isSelected) { + option.setAttribute('selected', 'selected'); + } + elSelect.appendChild(option); + }); + } + }); } function upArrows(row, toggle) { if (toggle) { @@ -60,7 +65,6 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { ? values.cells.length : 0; var columns = gridParameters.length; - var element = ""; var columnOrder = []; //fix for report builder @@ -71,7 +75,7 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { //finds and displays column names //gives each cell in table head unique ID from form editor - for (var i = 0; i < columns; i++) { + for (let i = 0; i < columns; i++) { $(gridHeadElement).append( '
  ' ); - //populates table + //populate table rows for (let i = 0; i < rows; i++) { const selectedRowValues = values?.cells[i] || []; $(gridBodyElement).append(""); - //generates row layout + //add td elements to each row for (let j = 0; j < columns; j++) { - switch (gridParameters[j].type) { + const name = gridParameters[j].name; + const val = selectedRowValues[j] || ''; + const type = (gridParameters[j]?.type || '').toLowerCase(); + switch (type) { case "dropdown": - element = makeDropdown( - gridParameters[j].options, - selectedRowValues[j] || null, - gridParameters[j].name - ); - break; case "dropdown_file": - const filename = gridParameters[j].file; - const hasHeader = gridParameters[j].hasHeader; - const loadedOptions = fileOptions[filename]?.options || []; - const firstRow = fileOptions[filename]?.firstRow || ''; - const options = hasHeader ? - loadedOptions.filter(o => o !== firstRow && o !== '') : loadedOptions.filter(o => o !== ''); - element = makeDropdown( - options, - selectedRowValues[j] || null, - gridParameters[j].name + const isMultiple = +gridParameters[j].multiselect === 1; + const selectedValues = isMultiple ? val.split(',') : [ val ]; + $(gridBodyElement + " > tr:last").append( + ` + + ` ); + let options = []; + if(type === 'dropdown_file') { + const filename = gridParameters[j].file; + const hasHeader = gridParameters[j].hasHeader; + const loadedOptions = fileOptions[filename]?.options || []; + const firstRow = fileOptions[filename]?.firstRow || ''; + options = hasHeader ? + loadedOptions.filter(o => o !== firstRow && o !== '') : loadedOptions.filter(o => o !== ''); + } else { + options = gridParameters[j].options || [] + } + let elSelect = document.querySelector(gridBodyElement + " > tr:last-child td:last-child select"); + appendOptions(elSelect, options, selectedValues); break; case "textarea": - element = ``; + $(gridBodyElement + " > tr:last").append( + `` + ); break; case "text": - element = ``; - break; - case "date": - element = ``; - break; - default: - break; - } - $(gridBodyElement + " > tr:eq(" + i + ")").append( - `${element}` - ); - $('input[data-type="grid-date"]').datepicker(); - } - - //assigns pre-existing values to cells based on its column - //if its column has been deleted, the value is not assigned - for (var j = 0; j < values.columns.length; j++) { - if (columnOrder.indexOf(values.columns[j]) !== -1) { - var value = - values.cells === undefined || - values.cells[i] === undefined || - values.cells[i][j] === undefined || - columnOrder.indexOf(values.columns[j]) === -1 - ? "" - : values.cells[i][j]; - var newCoordinates = - gridBodyElement + - " > tr:eq(" + - i + - ") > td:eq(" + - columnOrder.indexOf(values.columns[j]) + - ")"; - switch ($(newCoordinates).children().first().prop("tagName")) { - case "SELECT": - $(newCoordinates + " > select").val(value); - break; - case "TEXTAREA": - $(newCoordinates + " > textarea").val(value); - break; - case "INPUT": - $(newCoordinates + " > input").val(value); - break; - default: - break; - } - } - } - - //arrow logic: - //if there is only one row, arrows are not necessary - if (rows === 1) { - $(gridBodyElement + " > tr:eq(" + i + ")").append( - 'Delete lineMove line up

Move line down' - ); - } else { - switch (i) { - case 0: - //first row only needs down arrow - $(gridBodyElement + " > tr:eq(" + i + ")").append( - 'Delete lineMove line up

Move line down' + $(gridBodyElement + " > tr:last").append( + `` ); break; - case rows - 1: - //last row only needs up arrow - $(gridBodyElement + " > tr:eq(" + i + ")").append( - 'Delete lineMove line up

Move line down' + case "date": + $(gridBodyElement + " > tr:last").append( + `` ); + $('input[data-type="grid-date"]').datepicker(); break; default: - //everything else needs both - $(gridBodyElement + " > tr:eq(" + i + ")").append( - 'Delete lineMove line up

Move line down' - ); break; } } + + //arrow logic: if there is only one row, arrows are not necessary. 1st row only needs down, last row only needs up. + const showUpArrow = rows !== 1 && i !== 0; + const showDownArrow = rows !== 1 && i !== rows - 1; + $(`${gridBodyElement} > tr:eq(${i})`).append( + ` + Delete line + + + Move line up +

+ Move line down + ` + ); } } + function loadFilemanagerFile(fileName, iID) { return new Promise((resolve, reject)=> { const xhttpInds = new XMLHttpRequest(); @@ -357,66 +238,58 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { }); } function addRow() { - var gridBodyElement = - "#grid_" + indicatorID + "_" + series + "_input > tbody"; + const gridBodyElement = `#grid_${indicatorID}_${series}_input > tbody`; //makes down arrow in last row visible $(gridBodyElement + " > tr:last > td:last") .find('[title="Move line down"]') .css("display", "inline"); + $(gridBodyElement).append(""); for (let i = 0; i < gridParameters.length; i++) { - switch (gridParameters[i].type) { + const name = gridParameters[i].name; + const type = (gridParameters[i]?.type || '').toLowerCase(); + switch (type) { case "dropdown": - $(gridBodyElement + " > tr:last").append( - '' + - makeDropdown( - gridParameters[i].options || [], - null, - gridParameters[i].name - ) + - "" - ); - break; case "dropdown_file": - const filename = gridParameters[i].file; - const hasHeader = gridParameters[i].hasHeader; - const loadedOptions = fileOptions[filename]?.options || []; - const firstRow = fileOptions[filename]?.firstRow || ''; - const options = hasHeader ? - loadedOptions.filter(o => o !== firstRow && o !== '') : loadedOptions.filter(o => o !== ''); + const isMultiple = +gridParameters[i].multiselect === 1; + //add initial select element and first option $(gridBodyElement + " > tr:last").append( - '' + - makeDropdown( - options, - null, - gridParameters[i].name - ) + - "" + ` + + ` ); + //options need to be appended afterwards, or multiselect items won't display correctly + let options = []; + if(gridParameters[i].type === 'dropdown_file') { + const filename = gridParameters[i].file; + const hasHeader = gridParameters[i].hasHeader; + const loadedOptions = fileOptions[filename]?.options || []; + const firstRow = fileOptions[filename]?.firstRow || ''; + options = hasHeader ? + loadedOptions.filter(o => o !== firstRow && o !== '') : loadedOptions.filter(o => o !== ''); + } else { + options = gridParameters[i].options || [] + } + let elSelect = document.querySelector(gridBodyElement + " > tr:last-child td:last-child select"); + appendOptions(elSelect, options, []); break; case "textarea": $(gridBodyElement + " > tr:last").append( - '' + ` + ` ); break; case "text": $(gridBodyElement + " > tr:last").append( - '' + `` ); break; case "date": $(gridBodyElement + " > tr:last").append( - '' + `` ); $('input[data-type="grid-date"]').datepicker(); break; @@ -424,70 +297,30 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { break; } } - if ($(gridBodyElement).children().length === 1) { - $(gridBodyElement + " > tr:last").append( - 'Delete lineMove line up

Move line down' - ); - } else { - $(gridBodyElement + " > tr:last").append( - 'Delete lineMove line up

Move line down' - ); - } + const numRows = $(gridBodyElement).children().length; + $(gridBodyElement + " > tr:last").append( + ` + Delete line + + + Move line up +

+ Move line down + ` + ); + $("#tableStatus").attr( "aria-label", - "Row number " + - $(gridBodyElement).children().length + - " added, " + - $(gridBodyElement).children().length + - " total." + `Row number ${numRows} added, ${numRows} total.` ); } // click function for 508 compliance From c7f0834e6b88daa26f70b6aacec5e8d0fcf9dd8f Mon Sep 17 00:00:00 2001 From: Carrie Hanscom Date: Wed, 1 Nov 2023 17:25:25 -0400 Subject: [PATCH 03/65] LEAF 2285 mv controls template and data value cell search to methods, cleanup --- LEAF_Request_Portal/js/gridInput.js | 203 +++++++++++----------------- 1 file changed, 80 insertions(+), 123 deletions(-) diff --git a/LEAF_Request_Portal/js/gridInput.js b/LEAF_Request_Portal/js/gridInput.js index a86389d26..ac542a6d7 100644 --- a/LEAF_Request_Portal/js/gridInput.js +++ b/LEAF_Request_Portal/js/gridInput.js @@ -40,70 +40,91 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { } }); } + function upArrows(row, toggle) { - if (toggle) { - row.find('[title="Move line up"]').css("display", "inline"); - } else { - row.find('[title="Move line up"]').css("display", "none"); - } + row.find('[title="Move line up"]').css("display", `${toggle ? 'inline' : 'none'}`); } function downArrows(row, toggle) { - if (toggle) { - row.find('[title="Move line down"]').css("display", "inline"); - } else { - row.find('[title="Move line down"]').css("display", "none"); + row.find('[title="Move line down"]').css("display", `${toggle ? 'inline' : 'none'}`); + } + + function makeControlColumnTemplate(indicatorID = 0, series = 1, showUpArrow = false, showDownArrow = false) { + return ` + + Delete line + + + Move line up +

+ Move line down + `; + } + /** + * Grid columns can be moved, added or deleted. This method is used when displaying cells that could have saved values. + * A cell id from the indicator's grid parameters is searched for in the data field's columns array. + * If found, the corresponding value of the data field's cells array is returned. + * @param {string} cellID identifying the table column + * @param {object} data saved data field value + * @returns string + */ + function getDataValueByCellID(cellID = '', dataColumnIDs = [], dataRowValues = []) { + let val = ''; + if (dataColumnIDs.includes(cellID)) { + const index = dataColumnIDs.indexOf(cellID); + val = dataRowValues[index]; } + return val; } + + /* data entry display (form view and data entry modals) */ function printTableInput(values) { values = decodeCellHTMLEntities(values, true); - var gridBodyElement = - "#grid_" + indicatorID + "_" + series + "_input > tbody"; - var gridHeadElement = - "#grid_" + indicatorID + "_" + series + "_input > thead"; - var rows = - values.cells !== undefined && values.cells.length > 0 - ? values.cells.length - : 0; - var columns = gridParameters.length; - var columnOrder = []; + const gridBodyElement =`#grid_${indicatorID}_${series}_input > tbody`; + const gridHeadElement = `#grid_${indicatorID}_${series}_input > thead`; - //fix for report builder - //prevents duplicate table from being created on edit + const numRows = values.cells !== undefined && values.cells?.length > 0 ? + values.cells.length : 0; + const numCols = gridParameters.length; + + //fix for report builder to prevent duplicate tables from being created on edit if ($(gridHeadElement + " > td:last").html() !== undefined) { return 0; } - //finds and displays column names - //gives each cell in table head unique ID from form editor - for (let i = 0; i < columns; i++) { + //column names/table headers, with unique ID from form editor + for (let i = 0; i < numCols; i++) { $(gridHeadElement).append( - '
' + - gridParameters[i].name + - "
" + `
${gridParameters[i].name}
` ); - columnOrder.push(gridParameters[i].id); } - //columns for row manipulation $(gridHeadElement).append( '  ' ); //populate table rows - for (let i = 0; i < rows; i++) { - const selectedRowValues = values?.cells[i] || []; + for (let i = 0; i < numRows; i++) { + const selectedRowDataValues = values?.cells[i] || []; $(gridBodyElement).append(""); //add td elements to each row - for (let j = 0; j < columns; j++) { + for (let j = 0; j < numCols; j++) { const name = gridParameters[j].name; - const val = selectedRowValues[j] || ''; + const val = getDataValueByCellID(gridParameters[j].id, values.columns, selectedRowDataValues); + const type = (gridParameters[j]?.type || '').toLowerCase(); switch (type) { case "dropdown": case "dropdown_file": - const isMultiple = +gridParameters[j].multiselect === 1; + const isMultiple = +gridParameters[j].multiselect === 1; //TODO: NOTE: //waiting on implementation, set to false const selectedValues = isMultiple ? val.split(',') : [ val ]; $(gridBodyElement + " > tr:last").append( ` @@ -149,26 +170,10 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { } //arrow logic: if there is only one row, arrows are not necessary. 1st row only needs down, last row only needs up. - const showUpArrow = rows !== 1 && i !== 0; - const showDownArrow = rows !== 1 && i !== rows - 1; + const showUpArrow = numRows !== 1 && i !== 0; + const showDownArrow = numRows !== 1 && i !== numRows - 1; $(`${gridBodyElement} > tr:eq(${i})`).append( - ` - Delete line - - - Move line up -

- Move line down - ` + makeControlColumnTemplate(indicatorID, series, showUpArrow, showDownArrow) ); } } @@ -299,23 +304,7 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { } const numRows = $(gridBodyElement).children().length; $(gridBodyElement + " > tr:last").append( - ` - Delete line - - - Move line up -

- Move line down - ` + makeControlColumnTemplate(indicatorID, series, numRows > 1 , false) ); $("#tableStatus").attr( @@ -442,67 +431,35 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { $(event.target).closest("tbody").children().length ); } + /* form entry review page / print view before submit */ function printTableOutput(values) { values = decodeCellHTMLEntities(values); - var gridBodyElement = - "#grid_" + - indicatorID + - "_" + - series + - "_" + - recordID + - "_output > tbody"; - var gridHeadElement = - "#grid_" + - indicatorID + - "_" + - series + - "_" + - recordID + - "_output > thead"; - var rows = values.cells === undefined ? 0 : values.cells.length; - var columns = gridParameters.length; - var columnOrder = []; + const gridBodyElement =`#grid_${indicatorID}_${series}_${recordID}_output > tbody`; + const gridHeadElement = `#grid_${indicatorID}_${series}_${recordID}_output > thead`; + + const numRows = values.cells === undefined ? 0 : values.cells.length; + const numCols = gridParameters.length; - //finds and displays column names - for (var i = 0; i < columns; i++) { + //display the current column names + for (let i = 0; i < numCols; i++) { $(gridHeadElement).append( - '' + gridParameters[i].name + "" + `${gridParameters[i].name}` ); - columnOrder.push(gridParameters[i].id); } - - //populates table - for (var i = 0; i < rows; i++) { + //populate table + for (let i = 0; i < numRows; i++) { $(gridBodyElement).append(""); - - //generates row layout - for (var j = 0; j < columns; j++) { - $(gridBodyElement + " > tr:eq(" + i + ")").append( - '' + //rows + for (let j = 0; j < numCols; j++) { + const selectedRowDataValues = values?.cells[i] || []; + const val = getDataValueByCellID(gridParameters[j].id, values.columns, selectedRowDataValues); + $(gridBodyElement + " > tr:last").append( + `${val}` ); } - - //assigns pre-existing values to cells based on its column - //if its column has been deleted, the value is not assigned - for (var j = 0; j < values.columns.length; j++) { - if (columnOrder.indexOf(values.columns[j]) !== -1) { - var value = - values.cells[i] === undefined || values.cells[i][j] === undefined - ? "" - : values.cells[i][j]; - $( - gridBodyElement + - " > tr:eq(" + - i + - ") > td:eq(" + - columnOrder.indexOf(values.columns[j]) + - ")" - ).html(value); - } - } } } + /* old Form Editor view indicator display preview */ function printTablePreview() { let previewElement = "#grid" + indicatorID + "_" + series; @@ -543,4 +500,4 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { triggerClick: triggerClick, checkForFileOptions: checkForFileOptions }; -}; +}; \ No newline at end of file From 04c3f7b0d0d6f5e0cdd5318fcd4e3a7cd2c5ec85 Mon Sep 17 00:00:00 2001 From: mgaoVA <16783916+mgaoVA@users.noreply.github.com> Date: Wed, 1 Nov 2023 20:15:32 -0400 Subject: [PATCH 04/65] implement fulltext search --- LEAF_Request_Portal/js/formSearch.js | 10 ++++---- LEAF_Request_Portal/sources/Form.php | 37 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/LEAF_Request_Portal/js/formSearch.js b/LEAF_Request_Portal/js/formSearch.js index f1bfae5e5..3b4d6e713 100644 --- a/LEAF_Request_Portal/js/formSearch.js +++ b/LEAF_Request_Portal/js/formSearch.js @@ -271,9 +271,7 @@ var LeafFormSearch = function (containerID) { $( "#" + prefixID + "widgetCod_" + widgetID ).trigger("chosen:updated"); - $("#" + prefixID + "widgetMat_" + widgetID).val( - match.replace(/\*/g, "") - ); + $("#" + prefixID + "widgetMat_" + widgetID).val(match); $( "#" + prefixID + "widgetMat_" + widgetID ).trigger("chosen:updated"); @@ -290,7 +288,7 @@ var LeafFormSearch = function (containerID) { renderWidget(i); } $("#" + prefixID + "widgetCod_" + i).val(advSearch[i].operator); - if (typeof advSearch[i].match == "string") { + if (typeof advSearch[i].match == "string" && advSearch[i].operator.indexOf('MATCH') == -1) { $("#" + prefixID + "widgetMat_" + i).val( advSearch[i].match.replace(/\*/g, "") ); @@ -1010,7 +1008,8 @@ var LeafFormSearch = function (containerID) { "widgetCod_" + widgetID + '" class="chosen" aria-label="condition" style="width: 120px">\ - \ + \ + \ \ \ \ @@ -1366,6 +1365,7 @@ var LeafFormSearch = function (containerID) { } } ); + $("#" + prefixID + "widgetIndicator_" + widgetID).trigger("chosen:updated"); // trigger render on first load $( "#" + prefixID + diff --git a/LEAF_Request_Portal/sources/Form.php b/LEAF_Request_Portal/sources/Form.php index 5ff036e5b..313b8b1dc 100644 --- a/LEAF_Request_Portal/sources/Form.php +++ b/LEAF_Request_Portal/sources/Form.php @@ -2842,6 +2842,26 @@ public function changeFormType($recordID, $categories) return 1; } + /** + * parseBooleanQuery transforms a user's query to add implied "+" prefixes when + * a "MATCH ALL" condition is selected. + * + * @param $query + * @return string Transformed query + */ + private function parseBooleanQuery(string $query): string + { + $words = explode(' ', $query); + foreach($words as $k => $word) { + $firstChar = substr($word, 0, 1); + if($firstChar != '+' && $firstChar != '-') { + $words[$k] = '+' . $words[$k]; + } + } + + return implode($words); + } + /** * query parses a JSON formatted user query defined in formQuery.js. * @@ -2908,6 +2928,12 @@ public function query(string $inQuery): mixed $q['match'] = '%' . $q['match'] . '%'; } + break; + case 'MATCH ALL': // Only usable when a fulltext index exists AND logic has been implemented + $operator = 'MATCH ALL'; + break; + case 'MATCH ANY': // Only usable when a fulltext index exists AND logic has been implemented + $operator = 'MATCH ANY'; break; case 'RIGHT JOIN': break; @@ -3242,6 +3268,17 @@ public function query(string $inQuery): mixed break; default: + if($joinSearchAllData && $operator == 'MATCH ALL') { + $dataTerm = "MATCH ({$dataTerm})"; + $operator = 'AGAINST'; + $dataMatch = "({$dataMatch} IN BOOLEAN MODE)"; + $vars[":data{$count}"] = $this->parseBooleanQuery($vars[":data{$count}"]); + } + if($joinSearchAllData && $operator == 'MATCH ANY') { + $dataTerm = "MATCH ({$dataTerm})"; + $operator = 'AGAINST'; + $dataMatch = "({$dataMatch} IN BOOLEAN MODE)"; + } break; } From 2345899015d301c928374201768615a2fbad464c Mon Sep 17 00:00:00 2001 From: mgaoVA <16783916+mgaoVA@users.noreply.github.com> Date: Wed, 1 Nov 2023 20:17:03 -0400 Subject: [PATCH 05/65] should be safe? --- LEAF_Request_Portal/sources/Form.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LEAF_Request_Portal/sources/Form.php b/LEAF_Request_Portal/sources/Form.php index 5ff036e5b..9e40e7f47 100644 --- a/LEAF_Request_Portal/sources/Form.php +++ b/LEAF_Request_Portal/sources/Form.php @@ -3424,7 +3424,7 @@ public function query(string $inQuery): mixed if(isset($_GET['debugQuery'])) { if($this->login->checkGroup(1)) { - $debugQuery = str_replace(["\r", "\n","\t", "%0d","%0a","%09","%20", ":", ";", "="], ' ', 'SELECT * FROM records ' . $joins . 'WHERE ' . $conditions . $sort . $limit); + $debugQuery = str_replace(["\r", "\n","\t", "%0d","%0a","%09","%20", ";"], ' ', 'SELECT * FROM records ' . $joins . 'WHERE ' . $conditions . $sort . $limit); $debugVars = []; foreach($vars as $key => $value) { if(strpos($key, ':data') !== false From 82b745527114a0d8ccad1bafbb486f4613f2fe2a Mon Sep 17 00:00:00 2001 From: mgaoVA <16783916+mgaoVA@users.noreply.github.com> Date: Wed, 1 Nov 2023 20:26:10 -0400 Subject: [PATCH 06/65] mask wildcards when they're implicit --- LEAF_Request_Portal/js/formSearch.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/LEAF_Request_Portal/js/formSearch.js b/LEAF_Request_Portal/js/formSearch.js index 3b4d6e713..6cd061e0f 100644 --- a/LEAF_Request_Portal/js/formSearch.js +++ b/LEAF_Request_Portal/js/formSearch.js @@ -271,7 +271,14 @@ var LeafFormSearch = function (containerID) { $( "#" + prefixID + "widgetCod_" + widgetID ).trigger("chosen:updated"); - $("#" + prefixID + "widgetMat_" + widgetID).val(match); + + if(operator.indexOf('MATCH') == -1) { + $("#" + prefixID + "widgetMat_" + widgetID).val(match.replace(/\*/g, "")); + } + else { + $("#" + prefixID + "widgetMat_" + widgetID).val(match); + } + $( "#" + prefixID + "widgetMat_" + widgetID ).trigger("chosen:updated"); From 6dacb9ed40f843c35aef4114e783c40b603520f6 Mon Sep 17 00:00:00 2001 From: mgaoVA <16783916+mgaoVA@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:52:44 -0400 Subject: [PATCH 07/65] update search syntax --- LEAF_Request_Portal/js/formQuery.js | 11 ++++++++-- LEAF_Request_Portal/js/formSearch.js | 27 +++++++++++++++++-------- LEAF_Request_Portal/sources/Form.php | 30 ++++++++++++++++++---------- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/LEAF_Request_Portal/js/formQuery.js b/LEAF_Request_Portal/js/formQuery.js index 7d40dee50..7b728037a 100644 --- a/LEAF_Request_Portal/js/formQuery.js +++ b/LEAF_Request_Portal/js/formQuery.js @@ -222,6 +222,13 @@ var LeafFormQuery = function () { //NOTE: keeping this a var in case custom code abortSignal = signal; } + /** + * encodeReadableURI provides minimal character URI encoding, prioritizing readible URLs + */ + function encodeReadableURI(url) { + return url.replace('+', '%2b'); + } + /** * Execute search query in chunks * @param {number} limitOffset Used in subsequent recursive calls to track current offset @@ -244,7 +251,7 @@ var LeafFormQuery = function () { //NOTE: keeping this a var in case custom code const urlParamJSONP = useJSONP ? "&format=jsonp" : ""; return $.ajax({ type: "GET", - url: `${rootURL}api/form/query?q=${queryUrl + extraParams + urlParamJSONP}`, + url: `${rootURL}api/form/query?q=${encodeReadableURI(queryUrl + extraParams + urlParamJSONP)}`, dataType: dataType, error: (err) => console.log(err) }).then((res, resStatus, resJqXHR) => { @@ -288,7 +295,7 @@ var LeafFormQuery = function () { //NOTE: keeping this a var in case custom code const urlParamJSONP = useJSONP ? "&format=jsonp" : ""; return $.ajax({ type: "GET", - url: `${rootURL}api/form/query?q=${queryUrl + extraParams + urlParamJSONP}`, + url: `${rootURL}api/form/query?q=${encodeReadableURI(queryUrl + extraParams + urlParamJSONP)}`, dataType: dataType, success: successCallback, error: (err) => console.log(err) diff --git a/LEAF_Request_Portal/js/formSearch.js b/LEAF_Request_Portal/js/formSearch.js index 6cd061e0f..40f2698e9 100644 --- a/LEAF_Request_Portal/js/formSearch.js +++ b/LEAF_Request_Portal/js/formSearch.js @@ -634,6 +634,9 @@ var LeafFormSearch = function (containerID) { */ function renderSingleSelectInputType(widgetID, options) { switch ($("#" + prefixID + "widgetCod_" + widgetID).val()) { + case "MATCH ALL": + case "NOT MATCH": + case "MATCH": case "LIKE": case "NOT LIKE": $("#" + prefixID + "widgetMatch_" + widgetID).html( @@ -1016,10 +1019,12 @@ var LeafFormSearch = function (containerID) { widgetID + '" class="chosen" aria-label="condition" style="width: 120px">\ \ - \ - \ + \ + \ \ \ + \ + \ ' ); $( @@ -1284,12 +1289,15 @@ var LeafFormSearch = function (containerID) { '" class="chosen" aria-label="condition" style="width: 120px">\ \ \ - \ - \ + \ + \ + \ \ \ \ \ + \ + \ ' ); var resOptions = @@ -1347,10 +1355,13 @@ var LeafFormSearch = function (containerID) { "widgetCod_" + widgetID + '" class="chosen" aria-label="condition" style="width: 120px">\ - \ - \ - \ - \ + \ + \ + \ + \ + \ + \ + \ ' ); $( diff --git a/LEAF_Request_Portal/sources/Form.php b/LEAF_Request_Portal/sources/Form.php index 313b8b1dc..4f351436c 100644 --- a/LEAF_Request_Portal/sources/Form.php +++ b/LEAF_Request_Portal/sources/Form.php @@ -2859,7 +2859,7 @@ private function parseBooleanQuery(string $query): string } } - return implode($words); + return implode(' ', $words); } /** @@ -2932,8 +2932,11 @@ public function query(string $inQuery): mixed case 'MATCH ALL': // Only usable when a fulltext index exists AND logic has been implemented $operator = 'MATCH ALL'; break; - case 'MATCH ANY': // Only usable when a fulltext index exists AND logic has been implemented - $operator = 'MATCH ANY'; + case 'NOT MATCH': // Only usable when a fulltext index exists AND logic has been implemented + $operator = 'NOT MATCH'; + break; + case 'MATCH': // Only usable when a fulltext index exists AND logic has been implemented + $operator = 'MATCH'; break; case 'RIGHT JOIN': break; @@ -3268,14 +3271,18 @@ public function query(string $inQuery): mixed break; default: - if($joinSearchAllData && $operator == 'MATCH ALL') { - $dataTerm = "MATCH ({$dataTerm})"; - $operator = 'AGAINST'; - $dataMatch = "({$dataMatch} IN BOOLEAN MODE)"; + if($operator == 'MATCH ALL') { $vars[":data{$count}"] = $this->parseBooleanQuery($vars[":data{$count}"]); } - if($joinSearchAllData && $operator == 'MATCH ANY') { - $dataTerm = "MATCH ({$dataTerm})"; + + if(strpos($operator, 'MATCH') !== false) { + if($operator == 'NOT MATCH') { + $dataTerm = "NOT MATCH ({$dataTerm})"; + } + else { + $dataTerm = "MATCH ({$dataTerm})"; + } + $operator = 'AGAINST'; $dataMatch = "({$dataMatch} IN BOOLEAN MODE)"; } @@ -3283,8 +3290,9 @@ public function query(string $inQuery): mixed } // catch default data - if (isset($tResTypeHint[0]['default']) - && $tResTypeHint[0]['default'] == $vars[':data' . $count]) + if ((isset($tResTypeHint[0]['default']) + && $tResTypeHint[0]['default'] == $vars[':data' . $count]) + || $operator == 'NOT LIKE') { $conditions .= "{$gate}({$dataTerm} {$operator} $dataMatch OR {$dataTerm} IS NULL)"; } From 9f950e015a6135d25f626f3b5a0ba1c458856784 Mon Sep 17 00:00:00 2001 From: mgaoVA <16783916+mgaoVA@users.noreply.github.com> Date: Thu, 2 Nov 2023 15:08:04 -0400 Subject: [PATCH 08/65] optimize fulltext search --- LEAF_Request_Portal/sources/Form.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/LEAF_Request_Portal/sources/Form.php b/LEAF_Request_Portal/sources/Form.php index 4f351436c..1f78bc710 100644 --- a/LEAF_Request_Portal/sources/Form.php +++ b/LEAF_Request_Portal/sources/Form.php @@ -2881,6 +2881,7 @@ public function query(string $inQuery): mixed $joinSearchAllData = false; $joinSearchOrgchartEmployeeData = false; $filterActionable = false; + $usingFulltextIndex = false; $vars = array(); $conditions = ''; $joins = ''; @@ -2931,12 +2932,15 @@ public function query(string $inQuery): mixed break; case 'MATCH ALL': // Only usable when a fulltext index exists AND logic has been implemented $operator = 'MATCH ALL'; + $usingFulltextIndex = true; break; case 'NOT MATCH': // Only usable when a fulltext index exists AND logic has been implemented $operator = 'NOT MATCH'; + $usingFulltextIndex = true; break; case 'MATCH': // Only usable when a fulltext index exists AND logic has been implemented $operator = 'MATCH'; + $usingFulltextIndex = true; break; case 'RIGHT JOIN': break; @@ -3442,6 +3446,11 @@ public function query(string $inQuery): mixed break; } } + + // avoid extra sort when using fulltext index + if($usingFulltextIndex) { + $sort = ''; + } // join tables for queries on data fields without filtering by indicatorID if ($joinSearchAllData From a3067fe503736e2b3a6d26d543df5341a457aaa4 Mon Sep 17 00:00:00 2001 From: mgaoVA <16783916+mgaoVA@users.noreply.github.com> Date: Thu, 2 Nov 2023 15:49:14 -0400 Subject: [PATCH 09/65] fix negative search --- LEAF_Request_Portal/sources/Form.php | 59 +++++++++++++++------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/LEAF_Request_Portal/sources/Form.php b/LEAF_Request_Portal/sources/Form.php index 1f78bc710..c30c97ad6 100644 --- a/LEAF_Request_Portal/sources/Form.php +++ b/LEAF_Request_Portal/sources/Form.php @@ -3261,48 +3261,53 @@ public function query(string $inQuery): mixed { $dataTerm = 'lj_data.data'; } + $dataTermSql = ''; $dataMatch = ":data{$count}"; switch ($tResTypeHint[0]['format']) { - case 'number': - case 'currency': - $dataTerm = "CAST({$dataTerm} as DECIMAL(21,5))"; + case 'number': + case 'currency': + $dataTermSql = "CAST({$dataTerm} as DECIMAL(21,5))"; - break; - case 'date': - $dataTerm = "STR_TO_DATE({$dataTerm}, '%m/%d/%Y')"; - $dataMatch = "STR_TO_DATE(:data{$count}, '%m/%d/%Y')"; - - break; - default: - if($operator == 'MATCH ALL') { - $vars[":data{$count}"] = $this->parseBooleanQuery($vars[":data{$count}"]); - } + break; + case 'date': + $dataTermSql = "STR_TO_DATE({$dataTerm}, '%m/%d/%Y')"; + $dataMatch = "STR_TO_DATE(:data{$count}, '%m/%d/%Y')"; - if(strpos($operator, 'MATCH') !== false) { - if($operator == 'NOT MATCH') { - $dataTerm = "NOT MATCH ({$dataTerm})"; - } - else { - $dataTerm = "MATCH ({$dataTerm})"; + break; + default: + if($operator == 'MATCH ALL') { + $vars[":data{$count}"] = $this->parseBooleanQuery($vars[":data{$count}"]); } - $operator = 'AGAINST'; - $dataMatch = "({$dataMatch} IN BOOLEAN MODE)"; - } - break; - } + if(strpos($operator, 'MATCH') !== false) { + if($operator == 'NOT MATCH') { + $dataTermSql = "NOT MATCH ({$dataTerm})"; + } + else { + $dataTermSql = "MATCH ({$dataTerm})"; + } + + $operator = 'AGAINST'; + $dataMatch = "({$dataMatch} IN BOOLEAN MODE)"; + } + break; + } + if($dataTermSql == '') { + $dataTermSql = $dataTerm; + } // catch default data if ((isset($tResTypeHint[0]['default']) && $tResTypeHint[0]['default'] == $vars[':data' . $count]) - || $operator == 'NOT LIKE') + || $q['operator'] == 'NOT LIKE' + || $q['operator'] == 'NOT MATCH') { - $conditions .= "{$gate}({$dataTerm} {$operator} $dataMatch OR {$dataTerm} IS NULL)"; + $conditions .= "{$gate}({$dataTermSql} {$operator} $dataMatch OR {$dataTerm} IS NULL)"; } else { - $conditions .= "{$gate}{$dataTerm} {$operator} $dataMatch"; + $conditions .= "{$gate}{$dataTermSql} {$operator} $dataMatch"; } } } From ecca09c0be73037b71b00f0293e99df87b229e86 Mon Sep 17 00:00:00 2001 From: Carrie Hanscom Date: Thu, 2 Nov 2023 16:52:38 -0400 Subject: [PATCH 10/65] LEAF 2285 gridInput cleanup Remove multisel option from formeditor - waiting on other updates. Clean up some template areas of gridInput and move table data entry, and column id -- data value checking to methods. --- .../admin/templates/mod_form.tpl | 28 +-- LEAF_Request_Portal/js/gridInput.js | 225 +++++++----------- 2 files changed, 88 insertions(+), 165 deletions(-) diff --git a/LEAF_Request_Portal/admin/templates/mod_form.tpl b/LEAF_Request_Portal/admin/templates/mod_form.tpl index 066da0fea..454e5d3bf 100644 --- a/LEAF_Request_Portal/admin/templates/mod_form.tpl +++ b/LEAF_Request_Portal/admin/templates/mod_form.tpl @@ -905,11 +905,6 @@ function setFormatElementValue() { properties.file = $(this).find('select[id^="dropdown_file_select"]').val(); properties.hasHeader = Boolean(+$(this).find('select[id^="dropdown_file_header_select"]').val()); } - const elMultiCheckbox = this.querySelector('input[id^="grid_multiselect_checkbox_"]'); - if(elMultiCheckbox !== null) { - properties.multiselect = elMultiCheckbox?.checked === true ? 1 : 0; - } - } else { properties.type = 'textarea'; } @@ -1070,13 +1065,12 @@ function makeGrid(columns) { } $(gridBodyElement + ' > div:eq(' + i + ')').css('padding-bottom', '11px'); if($(gridBodyElement + ' > div:eq(' + i + ') > span.dropdown').length === 0){ - $(gridBodyElement + ' > div:eq(' + i + ')').append(` -
One option per line
- - -
`); + $(gridBodyElement + ' > div:eq(' + i + ')').append( + ` +
One option per line
+ +
` + ); } } if(gridJSON[i].type.toString() === 'dropdown_file') { @@ -1098,9 +1092,6 @@ function makeGrid(columns) { -
` ); } @@ -1119,6 +1110,7 @@ function toggleDropDown(type, cell, columnNumber) { elJQDropDown = $(cell).parent().find('span.dropdown'); elJQDropDownFile = $(cell).parent().find('div.dropdown_file'); let ariaStatus = ''; + switch(type) { case 'dropdown': if(elJQDropDownFile.length === 1) { @@ -1130,9 +1122,6 @@ function toggleDropDown(type, cell, columnNumber) { `
One option per line
-
` ); $('#tableStatus').attr('aria-label', ariaStatus); @@ -1155,9 +1144,6 @@ function toggleDropDown(type, cell, columnNumber) { - ` ); ariaStatus += 'Source file select added.'; diff --git a/LEAF_Request_Portal/js/gridInput.js b/LEAF_Request_Portal/js/gridInput.js index ac542a6d7..cd4a76519 100644 --- a/LEAF_Request_Portal/js/gridInput.js +++ b/LEAF_Request_Portal/js/gridInput.js @@ -69,11 +69,12 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { `; } /** - * Grid columns can be moved, added or deleted. This method is used when displaying cells that could have saved values. - * A cell id from the indicator's grid parameters is searched for in the data field's columns array. - * If found, the corresponding value of the data field's cells array is returned. - * @param {string} cellID identifying the table column - * @param {object} data saved data field value + * This method is used when displaying cells that could have saved values because cell positions can change. + * The cell id from the indicator format grid parameters element is searched for in the data field's 'columns' array. + * If found, the corresponding value of the data field's cells array is returned. Otherwise, an empty string is returned. + * @param {string} cellID identifying the current table column being rendered. + * @param {array} dataColumnIDs array of cell IDs saved in the data field. + * @param {array} dataRowValues array of cell data saved in the data field. * @returns string */ function getDataValueByCellID(cellID = '', dataColumnIDs = [], dataRowValues = []) { @@ -91,15 +92,14 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { const gridBodyElement =`#grid_${indicatorID}_${series}_input > tbody`; const gridHeadElement = `#grid_${indicatorID}_${series}_input > thead`; - const numRows = values.cells !== undefined && values.cells?.length > 0 ? - values.cells.length : 0; const numCols = gridParameters.length; + const dataColumnIDs = values?.columns || []; + const numRows = values?.cells?.length || 0; //fix for report builder to prevent duplicate tables from being created on edit if ($(gridHeadElement + " > td:last").html() !== undefined) { return 0; } - //column names/table headers, with unique ID from form editor for (let i = 0; i < numCols; i++) { $(gridHeadElement).append( @@ -110,69 +110,18 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { $(gridHeadElement).append( '  ' ); - - //populate table rows + //table rows for (let i = 0; i < numRows; i++) { const selectedRowDataValues = values?.cells[i] || []; $(gridBodyElement).append(""); - //add td elements to each row + //td elements for each row for (let j = 0; j < numCols; j++) { - const name = gridParameters[j].name; - const val = getDataValueByCellID(gridParameters[j].id, values.columns, selectedRowDataValues); - - const type = (gridParameters[j]?.type || '').toLowerCase(); - switch (type) { - case "dropdown": - case "dropdown_file": - const isMultiple = +gridParameters[j].multiselect === 1; //TODO: NOTE: //waiting on implementation, set to false - const selectedValues = isMultiple ? val.split(',') : [ val ]; - $(gridBodyElement + " > tr:last").append( - ` - - ` - ); - let options = []; - if(type === 'dropdown_file') { - const filename = gridParameters[j].file; - const hasHeader = gridParameters[j].hasHeader; - const loadedOptions = fileOptions[filename]?.options || []; - const firstRow = fileOptions[filename]?.firstRow || ''; - options = hasHeader ? - loadedOptions.filter(o => o !== firstRow && o !== '') : loadedOptions.filter(o => o !== ''); - } else { - options = gridParameters[j].options || [] - } - let elSelect = document.querySelector(gridBodyElement + " > tr:last-child td:last-child select"); - appendOptions(elSelect, options, selectedValues); - break; - case "textarea": - $(gridBodyElement + " > tr:last").append( - `` - ); - break; - case "text": - $(gridBodyElement + " > tr:last").append( - `` - ); - break; - case "date": - $(gridBodyElement + " > tr:last").append( - `` - ); - $('input[data-type="grid-date"]').datepicker(); - break; - default: - break; - } + addTableData(gridBodyElement, gridParameters[j], dataColumnIDs, selectedRowDataValues); } - - //arrow logic: if there is only one row, arrows are not necessary. 1st row only needs down, last row only needs up. + //arrow logic (existing rows): if there is only one row, arrows are not necessary. 1st row only needs down, last row only needs up. const showUpArrow = numRows !== 1 && i !== 0; const showDownArrow = numRows !== 1 && i !== numRows - 1; - $(`${gridBodyElement} > tr:eq(${i})`).append( + $(gridBodyElement + " > tr:last").append( makeControlColumnTemplate(indicatorID, series, showUpArrow, showDownArrow) ); } @@ -242,6 +191,58 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { } }); } + + function addTableData(gridBodyElement = '', gridParameters = [], dataColumnIDs = [], selectedRowDataValues = []) { + const id = gridParameters.id; + const name = gridParameters.name; + const val = getDataValueByCellID(id, dataColumnIDs, selectedRowDataValues); + + const type = (gridParameters?.type || '').toLowerCase(); + switch (type) { + case "dropdown": + case "dropdown_file": + const isMultiple = false; //TODO: waiting on implementation, set to false for now. +gridParameters.multiselect === 1 + const selectedValues = isMultiple ? val.split(',') : [ val ]; + $(gridBodyElement + " > tr:last").append( + ` + + ` + ); + let options = []; + if(type === 'dropdown_file') { + const filename = gridParameters.file; + const hasHeader = gridParameters.hasHeader; + const loadedOptions = fileOptions[filename]?.options || []; + const firstRow = fileOptions[filename]?.firstRow || ''; + options = hasHeader ? + loadedOptions.filter(o => o !== firstRow && o !== '') : loadedOptions.filter(o => o !== ''); + } else { + options = gridParameters.options || []; + } + let elSelect = document.querySelector(gridBodyElement + " > tr:last-child td:last-child select"); + appendOptions(elSelect, options, selectedValues); + break; + case "textarea": + $(gridBodyElement + " > tr:last").append( + ` + + ` + ); + break; + case "text": + $(gridBodyElement + " > tr:last").append(``); + break; + case "date": + $(gridBodyElement + " > tr:last").append(``); + $('input[data-type="grid-date"]').datepicker(); + break; + default: + break; + } + } + function addRow() { const gridBodyElement = `#grid_${indicatorID}_${series}_input > tbody`; //makes down arrow in last row visible @@ -251,66 +252,14 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { $(gridBodyElement).append(""); for (let i = 0; i < gridParameters.length; i++) { - const name = gridParameters[i].name; - const type = (gridParameters[i]?.type || '').toLowerCase(); - switch (type) { - case "dropdown": - case "dropdown_file": - const isMultiple = +gridParameters[i].multiselect === 1; - //add initial select element and first option - $(gridBodyElement + " > tr:last").append( - ` - - ` - ); - //options need to be appended afterwards, or multiselect items won't display correctly - let options = []; - if(gridParameters[i].type === 'dropdown_file') { - const filename = gridParameters[i].file; - const hasHeader = gridParameters[i].hasHeader; - const loadedOptions = fileOptions[filename]?.options || []; - const firstRow = fileOptions[filename]?.firstRow || ''; - options = hasHeader ? - loadedOptions.filter(o => o !== firstRow && o !== '') : loadedOptions.filter(o => o !== ''); - } else { - options = gridParameters[i].options || [] - } - let elSelect = document.querySelector(gridBodyElement + " > tr:last-child td:last-child select"); - appendOptions(elSelect, options, []); - break; - case "textarea": - $(gridBodyElement + " > tr:last").append( - ` - ` - ); - break; - case "text": - $(gridBodyElement + " > tr:last").append( - `` - ); - break; - case "date": - $(gridBodyElement + " > tr:last").append( - `` - ); - $('input[data-type="grid-date"]').datepicker(); - break; - default: - break; - } + addTableData(gridBodyElement, gridParameters[i]); } + const numRows = $(gridBodyElement).children().length; $(gridBodyElement + " > tr:last").append( makeControlColumnTemplate(indicatorID, series, numRows > 1 , false) ); - - $("#tableStatus").attr( - "aria-label", - `Row number ${numRows} added, ${numRows} total.` - ); + $("#tableStatus").attr("aria-label", `Row number ${numRows} added, ${numRows} total.`); } // click function for 508 compliance function triggerClick(event) { @@ -350,7 +299,7 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { break; } if (focus !== undefined) { - //ie11 fix + //clear stack setTimeout(function () { focus.focus(); }, 0); @@ -358,11 +307,7 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { $("#tableStatus").attr( "aria-label", - "Row " + - rowDeleted + - " removed, " + - $(tbody).children().length + - " total." + `Row ${rowDeleted} removed, ${$(tbody).children().length} total.` ); } @@ -437,25 +382,21 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { const gridBodyElement =`#grid_${indicatorID}_${series}_${recordID}_output > tbody`; const gridHeadElement = `#grid_${indicatorID}_${series}_${recordID}_output > thead`; - const numRows = values.cells === undefined ? 0 : values.cells.length; const numCols = gridParameters.length; - //display the current column names for (let i = 0; i < numCols; i++) { - $(gridHeadElement).append( - `${gridParameters[i].name}` - ); + $(gridHeadElement).append(`${gridParameters[i].name}`); } //populate table + const dataColumnIDs = values?.columns || []; + const numRows = values?.cells?.length || 0; for (let i = 0; i < numRows; i++) { $(gridBodyElement).append(""); - //rows + //row td elements for each column for (let j = 0; j < numCols; j++) { const selectedRowDataValues = values?.cells[i] || []; - const val = getDataValueByCellID(gridParameters[j].id, values.columns, selectedRowDataValues); - $(gridBodyElement + " > tr:last").append( - `${val}` - ); + const val = getDataValueByCellID(gridParameters[j].id, dataColumnIDs, selectedRowDataValues); + $(gridBodyElement + " > tr:last").append(`${val}`); } } } @@ -465,15 +406,11 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { for (let i = 0; i < gridParameters.length; i++) { $(previewElement).append( - '
Column #' + - (i + 1) + - "
Title:" + - gridParameters[i].name + - "
Type:" + - gridParameters[i].type + - "
" + `
+ Column #${i + 1}
+ Title:${gridParameters[i].name}
+ Type:${gridParameters[i].type}
+
` ); if (gridParameters[i].type === "dropdown") { $(previewElement + "> div:eq(" + i + ")").append( From b0b4b2588feae0f43d7ba029bd78fabf0ea2f82a Mon Sep 17 00:00:00 2001 From: mgaoVA <16783916+mgaoVA@users.noreply.github.com> Date: Thu, 2 Nov 2023 17:04:26 -0400 Subject: [PATCH 11/65] update defaults, resolve employee prefill issue --- LEAF_Request_Portal/js/formSearch.js | 55 ++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/LEAF_Request_Portal/js/formSearch.js b/LEAF_Request_Portal/js/formSearch.js index 40f2698e9..73a5665b7 100644 --- a/LEAF_Request_Portal/js/formSearch.js +++ b/LEAF_Request_Portal/js/formSearch.js @@ -473,6 +473,15 @@ var LeafFormSearch = function (containerID) { } }); empSel.initialize(); + let previousSelectedEmp = $("#" + prefixID + "widgetMat_" + widgetID).val(); + if(previousSelectedEmp != '') { + if(type == 'empUID') { + empSel.forceSearch(`#${previousSelectedEmp}`); + } + else { + empSel.forceSearch(previousSelectedEmp); + } + } }, }); } else { @@ -505,6 +514,15 @@ var LeafFormSearch = function (containerID) { } }); empSel.initialize(); + let previousSelectedEmp = $("#" + prefixID + "widgetMat_" + widgetID).val(); + if(previousSelectedEmp != '') { + if(type == 'empUID') { + empSel.forceSearch(`#${previousSelectedEmp}`); + } + else { + empSel.forceSearch(previousSelectedEmp); + } + } } } @@ -901,7 +919,7 @@ var LeafFormSearch = function (containerID) { widgetID + '" style="width: 140px" class="chosen" aria-label="categoryID">\ \ - \ + \ ' ); url = @@ -925,7 +943,7 @@ var LeafFormSearch = function (containerID) { categories += ''; categories += - ''; + ''; categories += ''; //categories += ''; @@ -1397,6 +1415,28 @@ var LeafFormSearch = function (containerID) { }, }); break; + case "recordID": + $("#" + prefixID + "widgetCondition_" + widgetID).html( + '' + ); + $("#" + prefixID + "widgetMatch_" + widgetID).html( + '' + ); + break; default: $("#" + prefixID + "widgetCondition_" + widgetID).html( '\ Date: Thu, 2 Nov 2023 17:53:45 -0400 Subject: [PATCH 13/65] improve defaults --- LEAF_Request_Portal/js/formSearch.js | 2 -- LEAF_Request_Portal/sources/Form.php | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/LEAF_Request_Portal/js/formSearch.js b/LEAF_Request_Portal/js/formSearch.js index 73a5665b7..7f10a20ec 100644 --- a/LEAF_Request_Portal/js/formSearch.js +++ b/LEAF_Request_Portal/js/formSearch.js @@ -1037,12 +1037,10 @@ var LeafFormSearch = function (containerID) { widgetID + '" class="chosen" aria-label="condition" style="width: 120px">\ \ - \ \ \ \ \ - \ ' ); $( diff --git a/LEAF_Request_Portal/sources/Form.php b/LEAF_Request_Portal/sources/Form.php index 1b6ca6195..5fd4f0cb3 100644 --- a/LEAF_Request_Portal/sources/Form.php +++ b/LEAF_Request_Portal/sources/Form.php @@ -3517,6 +3517,10 @@ public function query(string $inQuery): mixed $recordIDs = trim($recordIDs, ','); $recordIDs = $recordIDs ?: 0; + if(count($res) > count(array_keys($data))) { + header('LEAF-Query: continue'); // signal frontend there might be more data + } + // These all require the recordIDs to be set if (!empty($recordIDs)) { From be63bb9db64dc11fbd42b1c32e9b910453c29ef3 Mon Sep 17 00:00:00 2001 From: Carrie Hanscom Date: Fri, 3 Nov 2023 10:54:57 -0400 Subject: [PATCH 14/65] LEAF 4111 fix non unique id, rename some variables, template cleanup --- LEAF_Request_Portal/js/gridInput.js | 126 ++++++++---------- .../templates/subindicators.tpl | 2 +- 2 files changed, 60 insertions(+), 68 deletions(-) diff --git a/LEAF_Request_Portal/js/gridInput.js b/LEAF_Request_Portal/js/gridInput.js index cd4a76519..9cddc2920 100644 --- a/LEAF_Request_Portal/js/gridInput.js +++ b/LEAF_Request_Portal/js/gridInput.js @@ -118,7 +118,7 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { for (let j = 0; j < numCols; j++) { addTableData(gridBodyElement, gridParameters[j], dataColumnIDs, selectedRowDataValues); } - //arrow logic (existing rows): if there is only one row, arrows are not necessary. 1st row only needs down, last row only needs up. + //arrow controls: no arrows if there is only one row. 1st row only needs down, last row only needs up. const showUpArrow = numRows !== 1 && i !== 0; const showDownArrow = numRows !== 1 && i !== numRows - 1; $(gridBodyElement + " > tr:last").append( @@ -127,7 +127,7 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { } } - function loadFilemanagerFile(fileName, iID) { + function loadFilemanagerFile(fileName = '', iID = '') { return new Promise((resolve, reject)=> { const xhttpInds = new XMLHttpRequest(); xhttpInds.onreadystatechange = () => { @@ -191,19 +191,24 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { } }); } + /** + * @param {String} gridBodySelector targetting table body + * @param {Object} cellParameters information about the current cell + * @param {Array} dataColumnIDs list of column ids from data field (if there are saved values) + * @param {Array} selectedRowDataValues list of data from data field (if there are saved values) + */ + function addTableData(gridBodySelector = '', cellParameters = {}, dataColumnIDs = [], selectedRowDataValues = []) { + const colID = cellParameters.id; + const name = cellParameters.name; + const val = getDataValueByCellID(colID, dataColumnIDs, selectedRowDataValues); - function addTableData(gridBodyElement = '', gridParameters = [], dataColumnIDs = [], selectedRowDataValues = []) { - const id = gridParameters.id; - const name = gridParameters.name; - const val = getDataValueByCellID(id, dataColumnIDs, selectedRowDataValues); - - const type = (gridParameters?.type || '').toLowerCase(); + const type = (cellParameters?.type || '').toLowerCase(); switch (type) { case "dropdown": case "dropdown_file": - const isMultiple = false; //TODO: waiting on implementation, set to false for now. +gridParameters.multiselect === 1 + const isMultiple = false; //TODO: waiting on implementation, set to false for now. const selectedValues = isMultiple ? val.split(',') : [ val ]; - $(gridBodyElement + " > tr:last").append( + $(gridBodySelector + " > tr:last").append( ` ` ); break; case "text": - $(gridBodyElement + " > tr:last").append(``); + $(gridBodySelector + " > tr:last").append(``); break; case "date": - $(gridBodyElement + " > tr:last").append(``); + $(gridBodySelector + " > tr:last").append(``); $('input[data-type="grid-date"]').datepicker(); break; default: @@ -261,23 +266,24 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { ); $("#tableStatus").attr("aria-label", `Row number ${numRows} added, ${numRows} total.`); } - // click function for 508 compliance + function triggerClick(event) { if (event.keyCode === 13) { $(event.target).trigger("click"); } } function deleteRow(event) { - var row = $(event.target).closest("tr"); - var tbody = $(event.target).closest("tbody"); - var rowDeleted = parseInt($(row).index()) + 1; - var focus; - switch (tbody.find("tr").length) { + let row = $(event.target).closest("tr"); + const rowDeleted = parseInt($(row).index()) + 1; + const tbody = $(event.target).closest("tbody"); + + let focus = row.next().find('[title="Delete line"]'); + const currLength = tbody.find("tr").length; + switch(currLength) { case 1: row.remove(); - setTimeout(function () { - $("#addRowBtn").focus(); - }, 0); + focus = $("#addRowBtn_" + indicatorID); + break; case 2: row.remove(); focus = tbody.find('[title="Delete line"]'); @@ -285,7 +291,6 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { downArrows(tbody.find("tr"), false); break; default: - focus = row.next().find('[title="Delete line"]'); if (row.find('[title="Move line down"]').css("display") === "none") { downArrows(row.prev(), false); upArrows(row.prev(), true); @@ -298,11 +303,11 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { row.remove(); break; } - if (focus !== undefined) { + if (focus?.length === 1) { //clear stack setTimeout(function () { focus.focus(); - }, 0); + }); } $("#tableStatus").attr( @@ -312,12 +317,11 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { } function moveDown(event) { - var row = $(event.target).closest("tr"); - var nextRowBottom = - row.next().find('[title="Move line down"]').css("display") === "none"; - var rowTop = row.find('[title="Move line up"]').css("display") === "none"; - var focus; + let row = $(event.target).closest("tr"); + const nextRowBottom = row.next().find('[title="Move line down"]').css("display") === "none"; + const rowTop = row.find('[title="Move line up"]').css("display") === "none"; upArrows(row, true); + if (nextRowBottom) { downArrows(row, false); downArrows(row.next(), true); @@ -326,31 +330,24 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { upArrows(row.next(), false); } row.insertAfter(row.next()); - if (nextRowBottom) { - focus = row.find('td:last > img[title="Move line up"]'); - } else { - focus = row.find('td:last > img[title="Move line down"]'); - } - //ie11 fix + + const focus = row.find(`td:last > img[title="Move line ${nextRowBottom ? 'up' : 'down'}"]`); setTimeout(function () { focus.focus(); - }, 0); + }); + $("#tableStatus").attr( "aria-label", - "Moved down to row " + - (parseInt($(row).index()) + 1) + - " of " + - $(event.target).closest("tbody").children().length + `Moved down to row ${(parseInt($(row).index()) + 1)} of ` + + $(event.target).closest("tbody").children().length ); } function moveUp(event) { - var row = $(event.target).closest("tr"); - var prevRowTop = - row.prev().find('[title="Move line up"]').css("display") === "none"; - var rowBottom = - row.find('[title="Move line down"]').css("display") === "none"; - var focus; + let row = $(event.target).closest("tr"); + const prevRowTop = row.prev().find('[title="Move line up"]').css("display") === "none"; + const rowBottom = row.find('[title="Move line down"]').css("display") === "none"; downArrows(row, true); + if (prevRowTop) { upArrows(row, false); upArrows(row.prev(), true); @@ -359,21 +356,16 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { downArrows(row.prev(), false); } row.insertBefore(row.prev()); - if (prevRowTop) { - focus = row.find('td:last > img[title="Move line down"]'); - } else { - focus = row.find('td:last > img[title="Move line up"]'); - } - //ie11 fix + + const focus = row.find(`td:last > img[title="Move line ${prevRowTop ? 'down' : 'up'}"]`); setTimeout(function () { focus.focus(); - }, 0); + }); + $("#tableStatus").attr( "aria-label", - "Moved up to row " + - (parseInt($(row).index()) + 1) + - " of " + - $(event.target).closest("tbody").children().length + `Moved up to row ${(parseInt($(row).index()) + 1)} of ` + + $(event.target).closest("tbody").children().length ); } /* form entry review page / print view before submit */ @@ -391,19 +383,19 @@ var gridInput = function (gridParameters, indicatorID, series, recordID) { const dataColumnIDs = values?.columns || []; const numRows = values?.cells?.length || 0; for (let i = 0; i < numRows; i++) { + const selectedRowDataValues = values?.cells[i] || []; $(gridBodyElement).append(""); //row td elements for each column for (let j = 0; j < numCols; j++) { - const selectedRowDataValues = values?.cells[i] || []; - const val = getDataValueByCellID(gridParameters[j].id, dataColumnIDs, selectedRowDataValues); + const colID = gridParameters[j].id + const val = getDataValueByCellID(colID, dataColumnIDs, selectedRowDataValues); $(gridBodyElement + " > tr:last").append(`${val}`); } } } - /* old Form Editor view indicator display preview */ + /* admin Form Editor view indicator display preview */ function printTablePreview() { - let previewElement = "#grid" + indicatorID + "_" + series; - + const previewElement = `#grid${indicatorID}_${series}`; for (let i = 0; i < gridParameters.length; i++) { $(previewElement).append( `
diff --git a/LEAF_Request_Portal/templates/subindicators.tpl b/LEAF_Request_Portal/templates/subindicators.tpl index 30005cd6c..b3e96f915 100644 --- a/LEAF_Request_Portal/templates/subindicators.tpl +++ b/LEAF_Request_Portal/templates/subindicators.tpl @@ -67,7 +67,7 @@
- + - - - - -
- - -
- - - - - - - -
- Image Attachment(s) + FileImage Attachment(s) - - -
- - [ ] - -
- - - - - -
- - -
- + } + function deleteFile____() { + dialog_confirm.setTitle('Delete File?'); + dialog_confirm.setContent('Are you sure you want to delete:

'); + dialog_confirm.setSaveHandler(function() { + $.ajax({ + type: 'POST', + url: "ajaxIndex.php?a=deleteattachment&recordID=&indicatorID=&series=", + data: { + recordID: , + indicatorID: , + series: , + file: '', + CSRFToken: '' + }, + success: function(response) { + $('#file____').css('display', 'none'); + dialog_confirm.hide(); + }, + error: function(err) { + console.log(err); + } + }); + }); + dialog_confirm.show(); + } + + + + +
Select additional File to attach: + accept="image/*" /> +
+ +
+
Maximum attachment size is B. +
From f013c58b52c737f61857e150d6645bef7a1e7bb0 Mon Sep 17 00:00:00 2001 From: Carrie Hanscom Date: Wed, 8 Nov 2023 08:09:59 -0500 Subject: [PATCH 23/65] LEAF 3188 mv method, adjust loading indicator --- .../templates/subindicators.tpl | 106 ++++++++++-------- 1 file changed, 57 insertions(+), 49 deletions(-) diff --git a/LEAF_Request_Portal/templates/subindicators.tpl b/LEAF_Request_Portal/templates/subindicators.tpl index 52a24c274..dddbcbad9 100644 --- a/LEAF_Request_Portal/templates/subindicators.tpl +++ b/LEAF_Request_Portal/templates/subindicators.tpl @@ -705,7 +705,60 @@ +
FileImage Attachment(s) @@ -713,7 +766,7 @@
+ style="background-color: #d7e5ff; padding: 4px; display: flex; align-items: center" > [ @@ -723,49 +776,6 @@