Browse Source

Customer portal : LinkedSet widget UX improvements part 2 (Collapsing widget)

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@4088 a333f486-631f-4898-b8df-5754b55c2be0
glajarige 9 years ago
parent
commit
5e2d175443

+ 3 - 0
datamodels/2.x/itop-portal-base/portal/src/views/bricks/object/mode_create.html.twig

@@ -94,6 +94,9 @@
 			}
 			}
 			oScrollTimeout = setTimeout(scrollHandler_{{ sFormIdSanitized }}, 50);
 			oScrollTimeout = setTimeout(scrollHandler_{{ sFormIdSanitized }}, 50);
 		});
 		});
+		$({% if tIsModal == true %}'.modal.in'{% else %}window{% endif %}).off('shown.bs.collapse hidden.bs.collapse').on('shown.bs.collapse hidden.bs.collapse', function () {
+			scrollHandler_{{ sFormIdSanitized }}();
+		});
 		// - First time call
 		// - First time call
 		scrollHandler_{{ sFormIdSanitized }}();
 		scrollHandler_{{ sFormIdSanitized }}();
 		
 		

+ 13 - 7
datamodels/2.x/itop-portal-base/portal/src/views/bricks/object/mode_search_regular.html.twig

@@ -44,11 +44,21 @@
 	var getColumnsDefinition = function()
 	var getColumnsDefinition = function()
 	{
 	{
 		var aColumnsDefinition = [];
 		var aColumnsDefinition = [];
-		var sFirstColumnId = Object.keys(oColumnProperties)[0];
+		
+		// Checkbox / Radio
+		aColumnsDefinition.push({
+				"width": "auto",
+				"searchable": false,
+				"sortable": false,
+				"title": "",
+				"type": "html",
+				"data": "",
+				"render": function(data, type, row){ return '<span class="row_input"><input type="{{ (bMultipleSelect) ? 'checkbox' : 'radio' }}" name="{{ sTargetAttCode }}" /></span>'; }
+		});
 
 
 		for(sKey in oColumnProperties)
 		for(sKey in oColumnProperties)
 		{
 		{
-			// Level main column
+			// Object attribute
 			aColumnsDefinition.push({
 			aColumnsDefinition.push({
 				"width": "auto",
 				"width": "auto",
 				"searchable": true,
 				"searchable": true,
@@ -72,11 +82,6 @@
 					}
 					}
 					cellElem.attr('data-object-id', row.id).html('<span>' + row.attributes[data].value + '</span>');
 					cellElem.attr('data-object-id', row.id).html('<span>' + row.attributes[data].value + '</span>');
 					
 					
-					if(data === sFirstColumnId)
-					{
-						cellElem.prepend('<span class="row_input"><input type="{{ (bMultipleSelect) ? 'checkbox' : 'radio' }}" name="{{ sTargetAttCode }}" /></span>');
-					}
-
 					return cellElem.prop('outerHTML');
 					return cellElem.prop('outerHTML');
 				},
 				},
 			});
 			});
@@ -113,6 +118,7 @@
 			},
 			},
 			"lengthMenu": [[10, 20, 50, -1], [10, 20, 50, "{{ 'Portal:Datatables:Language:DisplayLength:All'|dict_s }}"]],
 			"lengthMenu": [[10, 20, 50, -1], [10, 20, 50, "{{ 'Portal:Datatables:Language:DisplayLength:All'|dict_s }}"]],
 			"displayLength": {{ constant('Combodo\\iTop\\Portal\\Controller\\ObjectController::DEFAULT_COUNT_PER_PAGE_LIST') }},
 			"displayLength": {{ constant('Combodo\\iTop\\Portal\\Controller\\ObjectController::DEFAULT_COUNT_PER_PAGE_LIST') }},
+			"order": [[1, "asc"]],
 			"dom": '<"row"<"col-sm-6"l><"col-sm-6"<f><"visible-xs"p>>>t<"row"<"col-sm-6"i><"col-sm-6"p>>',
 			"dom": '<"row"<"col-sm-6"l><"col-sm-6"<f><"visible-xs"p>>>t<"row"<"col-sm-6"i><"col-sm-6"p>>',
 			"columns": getColumnsDefinition(),
 			"columns": getColumnsDefinition(),
 			"select": {
 			"select": {

+ 40 - 7
datamodels/2.x/itop-portal-base/portal/web/css/portal.css

@@ -598,6 +598,43 @@ table .group-actions .item-action-wrapper .panel-body > p:last-child{
 	margin: 10px;
 	margin: 10px;
 	overflow-x: auto;
 	overflow-x: auto;
 }
 }
+/* LinkedSet*/
+.form_linkedset_toggler,
+.form_linkedset_toggler:hover,
+.form_linkedset_toggler:focus{
+	margin-left: 0.4em;
+	text-decoration: none;
+	color: inherit;
+}
+.form_linkedset_toggler > .text:before{
+	content: "(";
+}
+.form_linkedset_toggler > .text:after{
+	content: ")";
+}
+.form_linkedset_toggler > .glyphicon{
+	margin-left: 0.5em;
+	font-size: 0.85em;
+	color: #d9230f; /* TODO : SASS this to primary color */
+}
+/* - DataTables : Loader */
+.form_linkedset_wrapper .datatables_overlay{
+	padding: 8px !important;
+}
+.form_linkedset_wrapper .overlay_content{
+	font-size: 0.6em;
+}
+.form_linkedset_wrapper .content_loader{
+	margin: 0px;
+}
+.form_linkedset_wrapper .content_loader .icon{
+	height: 23px;
+}
+/* - DataTables : Fit the table in the form */
+.form_linkedset_wrapper .dataTables_wrapper{
+	margin-bottom: 5px;
+	padding: 0px;
+}
 /* FileUpload */
 /* FileUpload */
 .fileupload_field_content{
 .fileupload_field_content{
 	padding: 8px 23px;
 	padding: 8px 23px;
@@ -817,14 +854,10 @@ table .group-actions .item-action-wrapper .panel-body > p:last-child{
     color: #c09853;
     color: #c09853;
 }
 }
 
 
-/* DataTables : Fit the table in the form */
-.form_linkedset_wrapper .dataTables_wrapper{
-	margin-bottom: 5px;
-	padding: 0px;
-}
 /* DataTables : Selection inputs */
 /* DataTables : Selection inputs */
+.dataTable.table th span.row_input,
 .dataTable.table td span.row_input{
 .dataTable.table td span.row_input{
 	display: inline-block;
 	display: inline-block;
-	margin-right: 5px;
-	vertical-align: middle;
+	width: 100%;
+	text-align: center;
 }
 }

+ 4 - 1
datamodels/2.x/itop-portal-base/portal/web/index.php

@@ -49,6 +49,9 @@ if (UserRights::GetContactId() == 0)
 	die(Dict::S('Portal:ErrorNoContactForThisUser'));
 	die(Dict::S('Portal:ErrorNoContactForThisUser'));
 }
 }
 
 
+// Checking if debug param is on
+$bDebug = (isset($_REQUEST['debug']) && ($_REQUEST['debug'] === 'true') );
+
 // Initializing Silex framework
 // Initializing Silex framework
 $oApp = new Silex\Application();
 $oApp = new Silex\Application();
 
 
@@ -65,7 +68,7 @@ $oApp->register(new Silex\Provider\TwigServiceProvider(), array(
 ));
 ));
 
 
 // Configuring Silex application
 // Configuring Silex application
-$oApp['debug'] = true;
+$oApp['debug'] = $bDebug;
 $oApp['combodo.absolute_url'] = utils::GetAbsoluteUrlAppRoot();
 $oApp['combodo.absolute_url'] = utils::GetAbsoluteUrlAppRoot();
 $oApp['combodo.portal.base.absolute_url'] = utils::GetAbsoluteUrlAppRoot() . 'env-' . utils::GetCurrentEnvironment() . '/itop-portal-base/portal/web/';
 $oApp['combodo.portal.base.absolute_url'] = utils::GetAbsoluteUrlAppRoot() . 'env-' . utils::GetCurrentEnvironment() . '/itop-portal-base/portal/web/';
 $oApp['combodo.portal.instance.absolute_url'] = utils::GetAbsoluteUrlAppRoot() . 'env-' . utils::GetCurrentEnvironment() . '/' . PORTAL_MODULE_ID . '/';
 $oApp['combodo.portal.instance.absolute_url'] = utils::GetAbsoluteUrlAppRoot() . 'env-' . utils::GetCurrentEnvironment() . '/' . PORTAL_MODULE_ID . '/';

+ 116 - 67
sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php

@@ -58,10 +58,20 @@ class BsLinkedSetFieldRenderer extends FieldRenderer
 		if (!$this->oField->GetHidden())
 		if (!$this->oField->GetHidden())
 		{
 		{
 			// Rendering field
 			// Rendering field
+			$sCollapseTogglerVisibleClass = 'glyphicon-menu-right';
+			$sCollapseTogglerHiddenClass = 'glyphicon-menu-down';
+			$sCollapseTogglerId = 'form_linkedset_toggler_' . $this->oField->GetGlobalId();
+			$sFieldWrapperId = 'form_linkedset_wrapper_' . $this->oField->GetGlobalId();
 			$oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
 			$oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
 			if ($this->oField->GetLabel() !== '')
 			if ($this->oField->GetLabel() !== '')
 			{
 			{
-				$oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">')->AddHtml($this->oField->GetLabel(), true)->AddHtml('</label>');
+				$oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">')
+					->AddHtml($this->oField->GetLabel(), true)
+					->AddHtml('<a id="' . $sCollapseTogglerId . '" class="form_linkedset_toggler" data-toggle="collapse" href="#' . $sFieldWrapperId . '" aria-expanded="false" aria-controls="' . $sFieldWrapperId . '">')
+					->AddHtml('<span class="text">' . count($aItemIds) . '</span>')
+					->AddHtml('<span class="glyphicon ' . $sCollapseTogglerHiddenClass . '"></>')
+					->AddHtml('</a>')
+					->AddHtml('</label>');
 			}
 			}
 			$oOutput->AddHtml('<div class="help-block"></div>');
 			$oOutput->AddHtml('<div class="help-block"></div>');
 
 
@@ -71,7 +81,7 @@ class BsLinkedSetFieldRenderer extends FieldRenderer
 			// - Output
 			// - Output
 			$oOutput->AddHtml(
 			$oOutput->AddHtml(
 <<<EOF
 <<<EOF
-				<div class="form_linkedset_wrapper">
+				<div class="form_linkedset_wrapper collapse" id="{$sFieldWrapperId}">
 					<div class="row">
 					<div class="row">
 						<div class="col-xs-12">
 						<div class="col-xs-12">
 							<input type="hidden" id="{$this->oField->GetGlobalId()}" name="{$this->oField->GetId()}" value="{$sItemIdsAsJson}" />
 							<input type="hidden" id="{$this->oField->GetGlobalId()}" name="{$this->oField->GetId()}" value="{$sItemIdsAsJson}" />
@@ -89,11 +99,32 @@ EOF
 			$sEmptyTableLabel = htmlentities(Dict::S(($this->oField->GetReadOnly()) ? 'Portal:Datatables:Language:EmptyTable' : 'UI:Message:EmptyList:UseAdd'), ENT_QUOTES, 'UTF-8');
 			$sEmptyTableLabel = htmlentities(Dict::S(($this->oField->GetReadOnly()) ? 'Portal:Datatables:Language:EmptyTable' : 'UI:Message:EmptyList:UseAdd'), ENT_QUOTES, 'UTF-8');
 			$sLabelGeneralCheckbox = htmlentities(Dict::S('Core:BulkExport:CheckAll') . ' / ' . Dict::S('Core:BulkExport:UncheckAll'), ENT_QUOTES, 'UTF-8');
 			$sLabelGeneralCheckbox = htmlentities(Dict::S('Core:BulkExport:CheckAll') . ' / ' . Dict::S('Core:BulkExport:UncheckAll'), ENT_QUOTES, 'UTF-8');
 			$sSelectionOptionHtml = ($this->oField->GetReadOnly()) ? 'false' : '{"style": "multi"}';
 			$sSelectionOptionHtml = ($this->oField->GetReadOnly()) ? 'false' : '{"style": "multi"}';
-			$sSelectionInputGlobalHtml = ($this->oField->GetReadOnly()) ? '' : '<span class="row_input"><input type="checkbox" id="' . $this->oField->GetId() . '_check_all" name="' . $this->oField->GetId() . '_check_all" title="' . $sLabelGeneralCheckbox . '" /></span>';
-			$sSelectionInputHtml = ($this->oField->GetReadOnly()) ? '' : '<span class="row_input"><input type="checkbox" name="' . $this->oField->GetId() . '" /></span>';
+			$sSelectionInputGlobalHtml = ($this->oField->GetReadOnly()) ? '' : '<span class="row_input"><input type="checkbox" id="' . $this->oField->GetGlobalId() . '_check_all" name="' . $this->oField->GetGlobalId() . '_check_all" title="' . $sLabelGeneralCheckbox . '" /></span>';
+			$sSelectionInputHtml = ($this->oField->GetReadOnly()) ? '' : '<span class="row_input"><input type="checkbox" name="' . $this->oField->GetGlobalId() . '" /></span>';
 			// - Output
 			// - Output
 			$oOutput->AddJs(
 			$oOutput->AddJs(
 <<<EOF
 <<<EOF
+				// Collapse handlers
+				// - Collapsing by default to optimize form space
+				// It would be better to be able to construct the widget as collapsed, but in this ase, datatables thinks the container is very small and therefore renders the table as if it was in microbox.
+				$('#{$sFieldWrapperId}').collapse({toggle: false});
+				// - Change toggle icon class
+				$('#{$sFieldWrapperId}').on('shown.bs.collapse', function(){
+					// Creating the table if null (first expand). If we create it on start, it will be displayed as if it was in a micro screen due to the div being "display: none;"
+					if(oTable_{$this->oField->GetGlobalId()} === undefined)
+					{
+						buildTable_{$this->oField->GetGlobalId()}();
+					}
+					$('#{$sCollapseTogglerId} > span.glyphicon').removeClass('{$sCollapseTogglerHiddenClass}').addClass('{$sCollapseTogglerVisibleClass}');
+				})
+				.on('hidden.bs.collapse', function(){
+					$('#{$sCollapseTogglerId} > span.glyphicon').removeClass('{$sCollapseTogglerVisibleClass}').addClass('{$sCollapseTogglerHiddenClass}');
+				});
+
+				// Places a loader in the empty datatables
+				$('#{$sTableId} > tbody').html('<tr><td class="datatables_overlay" colspan="100">' + $('#page_overlay').html() + '</td></tr>');
+
+				// Prepares data for datatables
 				var oColumnProperties_{$this->oField->GetGlobalId()} = {$sAttributesToDisplayAsJson};
 				var oColumnProperties_{$this->oField->GetGlobalId()} = {$sAttributesToDisplayAsJson};
 				var oRawDatas_{$this->oField->GetGlobalId()} = {$sItemsAsJson};
 				var oRawDatas_{$this->oField->GetGlobalId()} = {$sItemsAsJson};
 				var oTable_{$this->oField->GetGlobalId()};
 				var oTable_{$this->oField->GetGlobalId()};
@@ -147,22 +178,78 @@ EOF
 					return aColumnsDefinition;
 					return aColumnsDefinition;
 				};
 				};
 
 
+				// Helper to build the datatable
 				// Note : Those options should be externalized in an library so we can use them on any DataTables for the portal.
 				// Note : Those options should be externalized in an library so we can use them on any DataTables for the portal.
 				// We would just have to override / complete the necessary elements
 				// We would just have to override / complete the necessary elements
-				oTable_{$this->oField->GetGlobalId()} = $('#{$sTableId}').DataTable({
-					"language": {
-						"emptyTable":	  "{$sEmptyTableLabel}"
-					},
-					"displayLength": -1,
-					"scrollY": "300px",
-					"scrollCollapse": true,
-					"order": [[1, "asc"]],
-					"dom": 't',
-					"columns": getColumnsDefinition_{$this->oField->GetGlobalId()}(),
-					"select": {$sSelectionOptionHtml},
-					"rowId": "id",
-					"data": oRawDatas_{$this->oField->GetGlobalId()},
-				});
+				var buildTable_{$this->oField->GetGlobalId()} = function()
+				{
+					// Instanciates datatables
+					oTable_{$this->oField->GetGlobalId()} = $('#{$sTableId}').DataTable({
+						"language": {
+							"emptyTable":	  "{$sEmptyTableLabel}"
+						},
+						"displayLength": -1,
+						"scrollY": "300px",
+						"scrollCollapse": true,
+						"order": [[1, "asc"]],
+						"dom": 't',
+						"columns": getColumnsDefinition_{$this->oField->GetGlobalId()}(),
+						"select": {$sSelectionOptionHtml},
+						"rowId": "id",
+						"data": oRawDatas_{$this->oField->GetGlobalId()},
+					});
+						
+					// Handles items selection/deselection
+					// - Directly on the table
+					oTable_{$this->oField->GetGlobalId()}.off('select').on('select', function(oEvent, dt, type, indexes){
+						var aData = oTable_{$this->oField->GetGlobalId()}.rows(indexes).data().toArray();
+
+						// Checking input
+						$('#{$sTableId} tbody tr[role="row"].selected td:first-child input').prop('checked', true);
+						// Saving values in temp array
+						for(var i in aData)
+						{
+							var iItemId = aData[i].id;
+							if(!(iItemId in oSelectedItems_{$this->oField->GetGlobalId()}))
+							{
+								oSelectedItems_{$this->oField->GetGlobalId()}[iItemId] = aData[i].name;
+							}
+						}
+						// Updating remove button
+						updateRemoveButtonState_{$this->oField->GetGlobalId()}();
+					});
+					oTable_{$this->oField->GetGlobalId()}.off('deselect').on('deselect', function(oEvent, dt, type, indexes){
+						var aData = oTable_{$this->oField->GetGlobalId()}.rows(indexes).data().toArray();
+
+						// Checking input
+						$('#{$sTableId} tbody tr[role="row"]:not(.selected) td:first-child input').prop('checked', false);
+						// Saving values in temp array
+						for(var i in aData)
+						{
+							var iItemId = aData[i].id;
+							if(iItemId in oSelectedItems_{$this->oField->GetGlobalId()})
+							{
+								delete oSelectedItems_{$this->oField->GetGlobalId()}[iItemId];
+							}
+						}
+						// Unchecking global checkbox
+						$('#{$this->oField->GetGlobalId()}_check_all').prop('checked', false);
+						// Updating remove button
+						updateRemoveButtonState_{$this->oField->GetGlobalId()}();
+					});
+					// - From the global button
+					$('#{$this->oField->GetGlobalId()}_check_all').off('click').on('click', function(oEvent){
+						if($(this).prop('checked'))
+						{
+							oTable_{$this->oField->GetGlobalId()}.rows().select();
+						}
+						else
+						{
+							oTable_{$this->oField->GetGlobalId()}.rows().deselect();
+						}
+						updateRemoveButtonState_{$this->oField->GetGlobalId()}();
+					});
+				};
 EOF
 EOF
 			);
 			);
 
 
@@ -232,6 +319,8 @@ EOF
 								}
 								}
 
 
 								$('#{$this->oField->GetGlobalId()}').val(JSON.stringify(aObjectIds));
 								$('#{$this->oField->GetGlobalId()}').val(JSON.stringify(aObjectIds));
+								// Updating items count
+								updateItemCount();
 							})
 							})
 							.always(function(oData){
 							.always(function(oData){
 								// Hiding loader
 								// Hiding loader
@@ -251,6 +340,8 @@ EOF
 							}
 							}
 
 
 							$('#{$this->oField->GetGlobalId()}').val(JSON.stringify(aObjectIds));
 							$('#{$this->oField->GetGlobalId()}').val(JSON.stringify(aObjectIds));
+							// Updating items count
+							updateItemCount();
 						}
 						}
 					}
 					}
 				});
 				});
@@ -291,55 +382,13 @@ EOF
 						var bIsDisabled = (Object.keys(oSelectedItems_{$this->oField->GetGlobalId()}).length == 0);
 						var bIsDisabled = (Object.keys(oSelectedItems_{$this->oField->GetGlobalId()}).length == 0);
 						$('#{$sButtonRemoveId}').prop('disabled', bIsDisabled);
 						$('#{$sButtonRemoveId}').prop('disabled', bIsDisabled);
 					};
 					};
-					// - Directly on the table
-					oTable_{$this->oField->GetGlobalId()}.off('select').on('select', function(oEvent, dt, type, indexes){
-						var aData = oTable_{$this->oField->GetGlobalId()}.rows(indexes).data().toArray();
-
-						// Checking input
-						$('#{$sTableId} tbody tr[role="row"].selected td:first-child input').prop('checked', true);
-						// Saving values in temp array
-						for(var i in aData)
-						{
-							var iItemId = aData[i].id;
-							if(!(iItemId in oSelectedItems_{$this->oField->GetGlobalId()}))
-							{
-								oSelectedItems_{$this->oField->GetGlobalId()}[iItemId] = aData[i].name;
-							}
-						}
-						// Updating remove button
-						updateRemoveButtonState_{$this->oField->GetGlobalId()}();
-					});
-					oTable_{$this->oField->GetGlobalId()}.off('deselect').on('deselect', function(oEvent, dt, type, indexes){
-						var aData = oTable_{$this->oField->GetGlobalId()}.rows(indexes).data().toArray();
-
-						// Checking input
-						$('#{$sTableId} tbody tr[role="row"]:not(.selected) td:first-child input').prop('checked', false);
-						// Saving values in temp array
-						for(var i in aData)
-						{
-							var iItemId = aData[i].id;
-							if(iItemId in oSelectedItems_{$this->oField->GetGlobalId()})
-							{
-								delete oSelectedItems_{$this->oField->GetGlobalId()}[iItemId];
-							}
-						}
-						// Unchecking global checkbox
-						$('#{$this->oField->GetId()}_check_all').prop('checked', false);
-						// Updating remove button
-						updateRemoveButtonState_{$this->oField->GetGlobalId()}();
-					});
-					// - From the global button
-					$('#{$this->oField->GetId()}_check_all').off('click').on('click', function(oEvent){
-						if($(this).prop('checked'))
-						{
-							oTable_{$this->oField->GetGlobalId()}.rows().select();
-						}
-						else
-						{
-							oTable_{$this->oField->GetGlobalId()}.rows().deselect();
-						}
-						updateRemoveButtonState_{$this->oField->GetGlobalId()}();
-					});
+					// - Item count state handler
+					var updateItemCount = function()
+					{
+						console.log('in fct');
+						console.log(oTable_{$this->oField->GetGlobalId()}.rows().count());
+						$('#{$sCollapseTogglerId} > .text').text( oTable_{$this->oField->GetGlobalId()}.rows().count() );
+					};
 
 
 					// Handles items remove/add
 					// Handles items remove/add
 					$('#{$sButtonRemoveId}').off('click').on('click', function(){
 					$('#{$sButtonRemoveId}').off('click').on('click', function(){