Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public String modifyVisibleQuestions( HttpServletRequest request ) throws Access
}

/**
* Checks whether a question is considered filterable based on its entry type service
* Checks whether a question is considered filterable based on its entry type service
*
* @param entryTypeService
* The entry type service to check
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
import fr.paris.lutece.plugins.forms.business.Form;
import fr.paris.lutece.plugins.forms.business.FormHome;
import fr.paris.lutece.plugins.forms.business.MultiviewConfig;
import fr.paris.lutece.plugins.forms.business.Question;
import fr.paris.lutece.plugins.forms.business.QuestionHome;
import fr.paris.lutece.plugins.forms.business.Step;
import fr.paris.lutece.plugins.forms.business.StepHome;
import fr.paris.lutece.plugins.forms.business.action.GlobalFormsAction;
import fr.paris.lutece.plugins.forms.business.action.GlobalFormsActionHome;
import fr.paris.lutece.plugins.forms.business.form.FormItemSortConfig;
Expand All @@ -72,6 +76,7 @@
import fr.paris.lutece.plugins.forms.export.ExportServiceManager;
import fr.paris.lutece.plugins.forms.export.IFormatExport;
import fr.paris.lutece.plugins.forms.service.FormPanelConfigIdService;
import fr.paris.lutece.plugins.forms.service.FormsResourceIdService;
import fr.paris.lutece.plugins.forms.service.FormsPlugin;
import fr.paris.lutece.plugins.forms.service.MultiviewFormService;
import fr.paris.lutece.plugins.forms.util.FormsConstants;
Expand All @@ -97,10 +102,14 @@
import fr.paris.lutece.portal.service.upload.MultipartItem;
import fr.paris.lutece.portal.util.mvc.admin.annotations.Controller;
import fr.paris.lutece.portal.util.mvc.commons.annotations.Action;
import fr.paris.lutece.portal.util.mvc.commons.annotations.ResponseBody;
import fr.paris.lutece.portal.util.mvc.commons.annotations.View;
import fr.paris.lutece.util.filesystem.FileSystemUtil;
import fr.paris.lutece.util.html.AbstractPaginator;
import fr.paris.lutece.util.json.JsonResponse;
import fr.paris.lutece.util.json.JsonUtil;
import fr.paris.lutece.util.url.UrlItem;
import org.apache.commons.lang3.math.NumberUtils;

/**
* Controller which manage the multiview of responses of all Forms
Expand All @@ -116,6 +125,11 @@ public class MultiviewFormsJspBean extends AbstractJspBean
// Actions
private static final String ACTION_EXPORT_RESPONSES = "doExportResponses";
private static final String ACTION_SAVE_MULTIVIEW_CONFIG = "doSaveMultiviewConfig";
private static final String ACTION_REORDER_COLUMNS = "doReorderMultiviewColumns";

// Json responses
private static final String RESPONSE_SUCCESS = "SUCCESS";
private static final String RESPONSE_ERROR = "ERROR";

// Templates
private static final String TEMPLATE_FORMS_MULTIVIEW = "admin/plugins/forms/multiview/forms_multiview.html";
Expand Down Expand Up @@ -693,9 +707,107 @@ protected static String getMultiviewBaseViewUrl( )
return "MultiviewForms.jsp?view=" + VIEW_MULTIVIEW_FORMS;
}

/**
* Persists the multiview column order sent by the drag and drop reordering of the response table columns. Called through AJAX : the request holds one
* {@code multiview_column_order_<idQuestion>} parameter per question backing a reordered column, whose value is the new order of the column. After
* persisting, the cached column list is rebuilt so the new order is reflected on the next reload of the page.
*
* @param request
* The Http request
* @return a JSON response ( SUCCESS or ERROR )
*/
@Action( value = ACTION_REORDER_COLUMNS, securityTokenDisabled = true )
@ResponseBody
public String doReorderMultiviewColumns( HttpServletRequest request )
{
User user = (User) AdminUserService.getAdminUser( request );
Map<Integer, Boolean> mapCheckedForms = new HashMap<>( );

try
{
for ( Map.Entry<String, String [ ]> parameter : request.getParameterMap( ).entrySet( ) )
{
String strParameterName = parameter.getKey( );
String strPrefix = FormsConstants.PARAMETER_MULTIVIEW_ORDER + "_";
if ( !strParameterName.startsWith( strPrefix ) )
{
continue;
}

int nIdQuestion = NumberUtils.toInt( strParameterName.substring( strPrefix.length( ) ), FormsConstants.DEFAULT_ID_VALUE );
int nOrder = NumberUtils.toInt( request.getParameter( strParameterName ), FormsConstants.DEFAULT_ID_VALUE );

if ( nIdQuestion == FormsConstants.DEFAULT_ID_VALUE || nOrder == FormsConstants.DEFAULT_ID_VALUE )
{
continue;
}

Question question = QuestionHome.findByPrimaryKey( nIdQuestion );
if ( question == null )
{
continue;
}

if ( !isAuthorizedToReorder( question, request, mapCheckedForms ) )
{
throw new AccessDeniedException( StringUtils.EMPTY );
}

question.setMultiviewColumnOrder( nOrder );
QuestionHome.update( question );
}
}
catch( AccessDeniedException e )
{
return JsonUtil.buildJsonResponse( new JsonResponse( RESPONSE_ERROR ) );
}

// Rebuild the cached column list so the new order is reflected on the next reload of the page, without resetting the active filters. The bean is
// session scoped and only rebuilds its column list on session loss / panel change, so we force the rebuild here using the currently selected form.
int nSelectedIdForm = NumberUtils.toInt( _strFormSelectedValue, FormsConstants.DEFAULT_ID_VALUE );
Integer nIdForm = nSelectedIdForm != FormsConstants.DEFAULT_ID_VALUE ? nSelectedIdForm : null;
_listFormColumn = _formColumnFactory.buildFormColumnList( nIdForm, getLocale( ), user );
_listFormColumnDisplay = FormDisplayFactory.createFormColumnDisplayList( _listFormColumn );

return JsonUtil.buildJsonResponse( new JsonResponse( RESPONSE_SUCCESS ) );
}

/**
* Checks that the current admin user is allowed to modify the params of the form owning the given question. The result is cached per form id for the
* duration of the request to avoid redundant RBAC checks.
*
* @param question
* The question whose owning form permission is checked
* @param request
* The Http request
* @param mapCheckedForms
* A cache of already checked form ids
* @return {@code true} if the user is authorized, {@code false} otherwise
*/
private boolean isAuthorizedToReorder( Question question, HttpServletRequest request, Map<Integer, Boolean> mapCheckedForms )
{
Step step = StepHome.findByPrimaryKey( question.getIdStep( ) );
if ( step == null )
{
return false;
}

return mapCheckedForms.computeIfAbsent( step.getIdForm( ), id -> {
try
{
checkUserPermission( Form.RESOURCE_TYPE, String.valueOf( id ), FormsResourceIdService.PERMISSION_MODIFY_PARAMS, request, null );
return true;
}
catch( AccessDeniedException e )
{
return false;
}
} );
}

/**
* Reload the form column list form the form filter list
*
*
* @param listFormFilter
* the form filter list
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public class FormColumnDisplayEntry extends AbstractFormColumnDisplay
private static final String MARK_ENTRY_VALUES = "entry_values";
private static final String MARK_COLUMN_SORT_ATTRIBUTE = "column_sort_attribute";
private static final String MARK_SORT_URL = "sort_url";
private static final String MARK_QUESTION_IDS = "question_ids";

/**
* {@inheritDoc}
Expand All @@ -87,6 +88,14 @@ public String buildFormColumnHeaderTemplate( String strSortUrl, Locale locale )
String columSort = column.getListEntryCode( ).stream( ).distinct( ).collect( Collectors.joining( "," ) );
String strAttributeSort = FormResponseSearchItem.FIELD_ENTRY_CODE_SUFFIX + columSort + FormResponseSearchItem.FIELD_RESPONSE_FIELD_ITER + "0";

// Collect the id of every question backing this column so the client can persist the column order
String strQuestionIds = column.getListEntryCode( ).stream( ).distinct( )
.flatMap( code -> QuestionHome.findByCode( code ).stream( ) )
.map( q -> String.valueOf( q.getId( ) ) )
.distinct( )
.collect( Collectors.joining( "," ) );
model.put( MARK_QUESTION_IDS, strQuestionIds );

String strEntryCode = column.getListEntryCode( ).get( 0 );
Question question = QuestionHome.findByCode( strEntryCode ).get( 0 );
Entry entry = question.getEntry( );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -713,11 +713,11 @@
});
</script>
</#macro>
<#macro headerSort field title jsp_url attribute type='text' id='' hide=[]>
<#macro headerSort field title jsp_url attribute type='text' id='' hide=[] attr=''>
<#-- Element visibility -->
<#local displayTitleClass = displaySettings(hide,'table-cell') />
<#if jsp_url?contains("?")><#assign sort_url = jsp_url + "&amp;sorted_attribute_name=" + attribute + "&amp;asc_sort=" /><#else><#assign sort_url = jsp_url + "?sorted_attribute_name=" + attribute + "&amp;asc_sort=" /></#if>
<@th class='multiview-th bg-light ${displayTitleClass!}' id='sort${id!}_${attribute!}' params='data-field="${field}"' >
<@th class='multiview-th bg-light ${displayTitleClass!}' id='sort${id!}_${attribute!}' params='data-field="${field}" ${attr!}' >
<@div class='d-flex align-items-center justify-content-between'>
<@span class='text-truncate' params='data-bs-toggle="tooltip" title="${title}"'>${title}</@span>
<@sort jsp_url=jsp_url attribute=attribute type=type />
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<@headerSort field='entry_column_${entry_column_position}' title=column_title! jsp_url=sort_url attribute=column_sort_attribute />
<@headerSort field='entry_column_${entry_column_position}' title=column_title! jsp_url=sort_url attribute=column_sort_attribute attr='data-question-ids="${question_ids!}"' />
9 changes: 9 additions & 0 deletions webapp/themes/admin/shared/plugins/forms/css/forms.css
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ table.table-hover tbody tr:hover {
max-width: 150px;
}

/* Multiview - Drag and drop column reordering */
.multiview-draggable-col.dragging {
opacity: 0.4;
}

.multiview-draggable-col.drag-over {
border-left: 3px solid var(--tblr-primary);
}

.th-active {
background-color: var(--tblr-primary);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,105 @@
/**
* Enable drag and drop reordering of the multiview response table columns.
* Only columns backed by questions (carrying a data-question-ids attribute) can be dragged.
* When the order changes it is persisted through an AJAX call to ManageFormMultiviewConfig.jsp,
* updating the multiview_column_order_XX value of each impacted question.
*
* @param {string} reorderUrl the JSP url handling the doReorderMultiviewColumns action
*/
function initMultiviewColumnDragAndDrop( reorderUrl ){
const table = document.querySelector('#multi-form-list table');
if ( !table || !table.tHead || !table.tHead.rows.length ) {
return;
}

const headerRow = table.tHead.rows[0];
let draggedIndex = null;

function persistColumnOrder(){
const params = new URLSearchParams();
params.append('action_doReorderMultiviewColumns', '1');
// The visible order of the columns becomes the new multiview_column_order value of each question
Array.from(headerRow.cells).forEach(function(th, position){
const ids = th.getAttribute('data-question-ids');
if ( ids ) {
ids.split(',').filter(Boolean).forEach(function(id){
params.append('multiview_column_order_' + id, position);
});
}
});

fetch(reorderUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: params.toString()
}).catch(function(){ /* silently ignore : order is best effort and will be recomputed on reload */ });
}

function moveColumn( fromIndex, toIndex ){
if ( fromIndex === toIndex ) {
return;
}
const rows = [ headerRow ].concat(Array.from(table.tBodies).flatMap(tb => Array.from(tb.rows)));
rows.forEach(function(row){
if ( row.cells.length <= Math.max(fromIndex, toIndex) ) {
return;
}
const moved = row.cells[fromIndex];
const reference = row.cells[toIndex];
if ( fromIndex < toIndex ) {
reference.after(moved);
} else {
reference.before(moved);
}
});
}

Array.from(headerRow.cells).forEach(function(th){
if ( !th.hasAttribute('data-question-ids') ) {
return;
}
th.setAttribute('draggable', 'true');
th.classList.add('multiview-draggable-col');
th.style.cursor = 'move';

th.addEventListener('dragstart', function(e){
draggedIndex = th.cellIndex;
e.dataTransfer.effectAllowed = 'move';
th.classList.add('dragging');
});

th.addEventListener('dragend', function(){
draggedIndex = null;
headerRow.querySelectorAll('th').forEach(function(cell){
cell.classList.remove('dragging', 'drag-over');
});
});

th.addEventListener('dragover', function(e){
if ( draggedIndex === null ) {
return;
}
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
th.classList.add('drag-over');
});

th.addEventListener('dragleave', function(){
th.classList.remove('drag-over');
});

th.addEventListener('drop', function(e){
e.preventDefault();
th.classList.remove('drag-over');
if ( draggedIndex === null || draggedIndex === th.cellIndex ) {
return;
}
moveColumn(draggedIndex, th.cellIndex);
persistColumnOrder();
});
});
}

function redirectOnClick( element ){
const url = element.getAttribute("data-url");
if (url !== null) {
Expand Down Expand Up @@ -176,6 +278,9 @@ document.addEventListener('DOMContentLoaded', function() {
});
});

// Enable drag and drop reordering of the response table columns
initMultiviewColumnDragAndDrop("jsp/admin/plugins/forms/MultiviewForms.jsp");

// Add reset button to search text
const searchedText = document.getElementById("searched_text");
if (searchedText) {
Expand Down