浏览代码

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 年之前
父节点
当前提交
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);
 		});
+		$({% 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
 		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 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)
 		{
-			// Level main column
+			// Object attribute
 			aColumnsDefinition.push({
 				"width": "auto",
 				"searchable": true,
@@ -72,11 +82,6 @@
 					}
 					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');
 				},
 			});
@@ -113,6 +118,7 @@
 			},
 			"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') }},
+			"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>>',
 			"columns": getColumnsDefinition(),
 			"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;
 	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_field_content{
 	padding: 8px 23px;
@@ -817,14 +854,10 @@ table .group-actions .item-action-wrapper .panel-body > p:last-child{
     color: #c09853;
 }
 
-/* DataTables : Fit the table in the form */
-.form_linkedset_wrapper .dataTables_wrapper{
-	margin-bottom: 5px;
-	padding: 0px;
-}
 /* DataTables : Selection inputs */
+.dataTable.table th span.row_input,
 .dataTable.table td span.row_input{
 	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'));
 }
 
+// Checking if debug param is on
+$bDebug = (isset($_REQUEST['debug']) && ($_REQUEST['debug'] === 'true') );
+
 // Initializing Silex framework
 $oApp = new Silex\Application();
 
@@ -65,7 +68,7 @@ $oApp->register(new Silex\Provider\TwigServiceProvider(), array(
 ));
 
 // Configuring Silex application
-$oApp['debug'] = true;
+$oApp['debug'] = $bDebug;
 $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.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())
 		{
 			// 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 . '">');
 			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>');
 
@@ -71,7 +81,7 @@ class BsLinkedSetFieldRenderer extends FieldRenderer
 			// - Output
 			$oOutput->AddHtml(
 <<<EOF
-				<div class="form_linkedset_wrapper">
+				<div class="form_linkedset_wrapper collapse" id="{$sFieldWrapperId}">
 					<div class="row">
 						<div class="col-xs-12">
 							<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');
 			$sLabelGeneralCheckbox = htmlentities(Dict::S('Core:BulkExport:CheckAll') . ' / ' . Dict::S('Core:BulkExport:UncheckAll'), ENT_QUOTES, 'UTF-8');
 			$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
 			$oOutput->AddJs(
 <<<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 oRawDatas_{$this->oField->GetGlobalId()} = {$sItemsAsJson};
 				var oTable_{$this->oField->GetGlobalId()};
@@ -147,22 +178,78 @@ EOF
 					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.
 				// 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
 			);
 
@@ -232,6 +319,8 @@ EOF
 								}
 
 								$('#{$this->oField->GetGlobalId()}').val(JSON.stringify(aObjectIds));
+								// Updating items count
+								updateItemCount();
 							})
 							.always(function(oData){
 								// Hiding loader
@@ -251,6 +340,8 @@ EOF
 							}
 
 							$('#{$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);
 						$('#{$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
 					$('#{$sButtonRemoveId}').off('click').on('click', function(){